diff options
111 files changed, 5107 insertions, 3103 deletions
diff --git a/core/math/aabb.cpp b/core/math/aabb.cpp index 4c89be7f4d..483b0d10ec 100644 --- a/core/math/aabb.cpp +++ b/core/math/aabb.cpp @@ -403,6 +403,7 @@ Variant AABB::intersects_segment_bind(const Vector3 &p_from, const Vector3 &p_to } return Variant(); } + Variant AABB::intersects_ray_bind(const Vector3 &p_from, const Vector3 &p_dir) const { Vector3 inters; if (intersects_ray(p_from, p_dir, &inters)) { diff --git a/core/math/aabb.h b/core/math/aabb.h index acf903eeba..dfeb5b3291 100644 --- a/core/math/aabb.h +++ b/core/math/aabb.h @@ -101,7 +101,7 @@ struct _NO_DISCARD_ AABB { _FORCE_INLINE_ void expand_to(const Vector3 &p_vector); /** expand to contain a point if necessary */ _FORCE_INLINE_ AABB abs() const { - return AABB(Vector3(position.x + MIN(size.x, 0), position.y + MIN(size.y, 0), position.z + MIN(size.z, 0)), size.abs()); + return AABB(Vector3(position.x + MIN(size.x, (real_t)0), position.y + MIN(size.y, (real_t)0), position.z + MIN(size.z, (real_t)0)), size.abs()); } Variant intersects_segment_bind(const Vector3 &p_from, const Vector3 &p_to) const; diff --git a/core/math/basis.cpp b/core/math/basis.cpp index 4b163409ce..743a206ae7 100644 --- a/core/math/basis.cpp +++ b/core/math/basis.cpp @@ -142,8 +142,8 @@ bool Basis::is_symmetric() const { #endif Basis Basis::diagonalize() { -//NOTE: only implemented for symmetric matrices -//with the Jacobi iterative method +// NOTE: only implemented for symmetric matrices +// with the Jacobi iterative method #ifdef MATH_CHECKS ERR_FAIL_COND_V(!is_symmetric(), Basis()); #endif diff --git a/core/math/color.h b/core/math/color.h index 65036f74cc..bb8aa9a529 100644 --- a/core/math/color.h +++ b/core/math/color.h @@ -32,7 +32,8 @@ #define COLOR_H #include "core/math/math_funcs.h" -#include "core/string/ustring.h" + +class String; struct _NO_DISCARD_ Color { union { diff --git a/core/math/plane.cpp b/core/math/plane.cpp index 6881ad4014..3b2eab4ae2 100644 --- a/core/math/plane.cpp +++ b/core/math/plane.cpp @@ -147,6 +147,7 @@ Variant Plane::intersect_3_bind(const Plane &p_plane1, const Plane &p_plane2) co return Variant(); } } + Variant Plane::intersects_ray_bind(const Vector3 &p_from, const Vector3 &p_dir) const { Vector3 inters; if (intersects_ray(p_from, p_dir, &inters)) { @@ -155,6 +156,7 @@ Variant Plane::intersects_ray_bind(const Vector3 &p_from, const Vector3 &p_dir) return Variant(); } } + Variant Plane::intersects_segment_bind(const Vector3 &p_begin, const Vector3 &p_end) const { Vector3 inters; if (intersects_segment(p_begin, p_end, &inters)) { diff --git a/core/math/projection.cpp b/core/math/projection.cpp index d579b641a7..c628bf0bde 100644 --- a/core/math/projection.cpp +++ b/core/math/projection.cpp @@ -838,8 +838,9 @@ float Projection::get_lod_multiplier() const { return 1.0 / (zn / width); } - //usage is lod_size / (lod_distance * multiplier) < threshold + // Usage is lod_size / (lod_distance * multiplier) < threshold } + void Projection::make_scale(const Vector3 &p_scale) { set_identity(); columns[0][0] = p_scale.x; diff --git a/core/math/projection.h b/core/math/projection.h index 1bf5d8b2ed..38fb9781ae 100644 --- a/core/math/projection.h +++ b/core/math/projection.h @@ -31,10 +31,11 @@ #ifndef PROJECTION_H #define PROJECTION_H -#include "core/math/math_defs.h" #include "core/math/vector3.h" #include "core/math/vector4.h" -#include "core/templates/vector.h" + +template <class T> +class Vector; struct AABB; struct Plane; @@ -42,7 +43,7 @@ struct Rect2; struct Transform3D; struct Vector2; -struct Projection { +struct _NO_DISCARD_ Projection { enum Planes { PLANE_NEAR, PLANE_FAR, diff --git a/core/math/quaternion.h b/core/math/quaternion.h index 43d7bffcfc..077fe5f189 100644 --- a/core/math/quaternion.h +++ b/core/math/quaternion.h @@ -31,10 +31,10 @@ #ifndef QUATERNION_H #define QUATERNION_H -#include "core/math/math_defs.h" #include "core/math/math_funcs.h" #include "core/math/vector3.h" -#include "core/string/ustring.h" + +class String; struct _NO_DISCARD_ Quaternion { union { diff --git a/core/math/rect2.h b/core/math/rect2.h index 2d1be3d4f3..5ed2f8236c 100644 --- a/core/math/rect2.h +++ b/core/math/rect2.h @@ -281,7 +281,7 @@ struct _NO_DISCARD_ Rect2 { } _FORCE_INLINE_ Rect2 abs() const { - return Rect2(Point2(position.x + MIN(size.x, 0), position.y + MIN(size.y, 0)), size.abs()); + return Rect2(Point2(position.x + MIN(size.x, (real_t)0), position.y + MIN(size.y, (real_t)0)), size.abs()); } Vector2 get_support(const Vector2 &p_normal) const { diff --git a/core/math/transform_3d.h b/core/math/transform_3d.h index c62e4a7b0e..44d6d826f3 100644 --- a/core/math/transform_3d.h +++ b/core/math/transform_3d.h @@ -34,6 +34,7 @@ #include "core/math/aabb.h" #include "core/math/basis.h" #include "core/math/plane.h" +#include "core/templates/vector.h" struct _NO_DISCARD_ Transform3D { Basis basis; diff --git a/core/math/vector4.h b/core/math/vector4.h index 426c473e13..ac7b6c3aee 100644 --- a/core/math/vector4.h +++ b/core/math/vector4.h @@ -31,10 +31,10 @@ #ifndef VECTOR4_H #define VECTOR4_H -#include "core/math/math_defs.h" +#include "core/error/error_macros.h" #include "core/math/math_funcs.h" -#include "core/math/vector3.h" -#include "core/string/ustring.h" + +class String; struct _NO_DISCARD_ Vector4 { static const int AXIS_COUNT = 4; diff --git a/core/object/class_db.cpp b/core/object/class_db.cpp index 99b20560da..ca56add2ab 100644 --- a/core/object/class_db.cpp +++ b/core/object/class_db.cpp @@ -1538,7 +1538,11 @@ void ClassDB::register_extension_class(ObjectNativeExtension *p_extension) { } void ClassDB::unregister_extension_class(const StringName &p_class) { - ERR_FAIL_COND(!classes.has(p_class)); + ClassInfo *c = classes.getptr(p_class); + ERR_FAIL_COND_MSG(!c, "Class " + p_class + "does not exist"); + for (KeyValue<StringName, MethodBind *> &F : c->method_map) { + memdelete(F.value); + } classes.erase(p_class); } diff --git a/core/os/os.cpp b/core/os/os.cpp index 72d68893f3..bbb2a94fe7 100644 --- a/core/os/os.cpp +++ b/core/os/os.cpp @@ -38,6 +38,7 @@ #include "core/version_generated.gen.h" #include <stdarg.h> +#include <thread> OS *OS::singleton = nullptr; uint64_t OS::target_ticks = 0; @@ -321,7 +322,7 @@ String OS::get_unique_id() const { } int OS::get_processor_count() const { - return 1; + return std::thread::hardware_concurrency(); } String OS::get_processor_name() const { diff --git a/core/os/thread.cpp b/core/os/thread.cpp index e8ebb4597f..712f4793eb 100644 --- a/core/os/thread.cpp +++ b/core/os/thread.cpp @@ -36,10 +36,7 @@ #include "core/object/script_language.h" #include "core/templates/safe_refcount.h" -Error (*Thread::set_name_func)(const String &) = nullptr; -void (*Thread::set_priority_func)(Thread::Priority) = nullptr; -void (*Thread::init_func)() = nullptr; -void (*Thread::term_func)() = nullptr; +Thread::PlatformFunctions Thread::platform_functions; uint64_t Thread::_thread_id_hash(const std::thread::id &p_t) { static std::hash<std::thread::id> hasher; @@ -49,30 +46,27 @@ uint64_t Thread::_thread_id_hash(const std::thread::id &p_t) { Thread::ID Thread::main_thread_id = _thread_id_hash(std::this_thread::get_id()); thread_local Thread::ID Thread::caller_id = 0; -void Thread::_set_platform_funcs( - Error (*p_set_name_func)(const String &), - void (*p_set_priority_func)(Thread::Priority), - void (*p_init_func)(), - void (*p_term_func)()) { - Thread::set_name_func = p_set_name_func; - Thread::set_priority_func = p_set_priority_func; - Thread::init_func = p_init_func; - Thread::term_func = p_term_func; +void Thread::_set_platform_functions(const PlatformFunctions &p_functions) { + platform_functions = p_functions; } void Thread::callback(Thread *p_self, const Settings &p_settings, Callback p_callback, void *p_userdata) { Thread::caller_id = _thread_id_hash(p_self->thread.get_id()); - if (set_priority_func) { - set_priority_func(p_settings.priority); + if (platform_functions.set_priority) { + platform_functions.set_priority(p_settings.priority); } - if (init_func) { - init_func(); + if (platform_functions.init) { + platform_functions.init(); + } + ScriptServer::thread_enter(); // Scripts may need to attach a stack. + if (platform_functions.wrapper) { + platform_functions.wrapper(p_callback, p_userdata); + } else { + p_callback(p_userdata); } - ScriptServer::thread_enter(); //scripts may need to attach a stack - p_callback(p_userdata); ScriptServer::thread_exit(); - if (term_func) { - term_func(); + if (platform_functions.term) { + platform_functions.term(); } } @@ -105,8 +99,8 @@ void Thread::wait_to_finish() { } Error Thread::set_name(const String &p_name) { - if (set_name_func) { - return set_name_func(p_name); + if (platform_functions.set_name) { + return platform_functions.set_name(p_name); } return ERR_UNAVAILABLE; diff --git a/core/os/thread.h b/core/os/thread.h index 7462ec17fb..86442de760 100644 --- a/core/os/thread.h +++ b/core/os/thread.h @@ -63,6 +63,14 @@ public: Settings() { priority = PRIORITY_NORMAL; } }; + struct PlatformFunctions { + Error (*set_name)(const String &) = nullptr; + void (*set_priority)(Thread::Priority) = nullptr; + void (*init)() = nullptr; + void (*wrapper)(Thread::Callback, void *) = nullptr; + void (*term)() = nullptr; + }; + private: friend class Main; @@ -76,17 +84,10 @@ private: static void callback(Thread *p_self, const Settings &p_settings, Thread::Callback p_callback, void *p_userdata); - static Error (*set_name_func)(const String &); - static void (*set_priority_func)(Thread::Priority); - static void (*init_func)(); - static void (*term_func)(); + static PlatformFunctions platform_functions; public: - static void _set_platform_funcs( - Error (*p_set_name_func)(const String &), - void (*p_set_priority_func)(Thread::Priority), - void (*p_init_func)() = nullptr, - void (*p_term_func)() = nullptr); + static void _set_platform_functions(const PlatformFunctions &p_functions); _FORCE_INLINE_ ID get_id() const { return id; } // get the ID of the caller thread diff --git a/doc/classes/Control.xml b/doc/classes/Control.xml index c66a93ce37..9c7cb7b089 100644 --- a/doc/classes/Control.xml +++ b/doc/classes/Control.xml @@ -7,8 +7,8 @@ Base class for all UI-related nodes. [Control] features a bounding rectangle that defines its extents, an anchor position relative to its parent control or the current viewport, and offsets relative to the anchor. The offsets update automatically when the node, any of its parents, or the screen size change. For more information on Godot's UI system, anchors, offsets, and containers, see the related tutorials in the manual. To build flexible UIs, you'll need a mix of UI elements that inherit from [Control] and [Container] nodes. [b]User Interface nodes and input[/b] - Godot sends input events to the scene's root node first, by calling [method Node._input]. [method Node._input] forwards the event down the node tree to the nodes under the mouse cursor, or in focus. To do so, it calls [code]MainLoop._input_event[/code]. - [b]FIXME:[/b] No longer valid after DisplayServer split and Input refactoring. + Godot propagates input events via viewports. Each [Viewport] is responsible for propagating [InputEvent]s to their child nodes. As the [member SceneTree.root] is a [Window], this already happens automatically for all UI elements in your game. + Input events are propagated through the [SceneTree] from the root node to all child nodes by calling [method Node._input]. For UI elements specifically, it makes more sense to override the virtual method [method _gui_input], which filters out unrelated input events, such as by checking z-order, [member mouse_filter], focus, or if the event was inside of the control's bounding box. Call [method accept_event] so no other node receives the event. Once you accept an input, it becomes handled so [method Node._unhandled_input] will not process it. Only one [Control] node can be in focus. Only the node in focus will receive events. To get the focus, call [method grab_focus]. [Control] nodes lose focus when another node grabs it, or if you hide the node in focus. Sets [member mouse_filter] to [constant MOUSE_FILTER_IGNORE] to tell a [Control] node to ignore mouse or touch events. You'll need it if you place an icon on top of a button. diff --git a/doc/classes/NavigationObstacle2D.xml b/doc/classes/NavigationObstacle2D.xml index 6d05e220e3..68d9691ccf 100644 --- a/doc/classes/NavigationObstacle2D.xml +++ b/doc/classes/NavigationObstacle2D.xml @@ -5,7 +5,7 @@ </brief_description> <description> 2D Obstacle used in navigation for collision avoidance. The obstacle needs navigation data to work correctly. [NavigationObstacle2D] is physics safe. - [b]Note:[/b] Obstacles are intended as a last resort option for constantly moving objects that cannot be (re)baked to a navigation mesh efficiently. + Obstacles [b]don't[/b] change the resulting path from the pathfinding, they only affect the navigation agent movement in a radius. Therefore, using obstacles for the static walls in your level won't work because those walls don't exist in the pathfinding. The navigation agent will be pushed in a semi-random direction away while moving inside that radius. Obstacles are intended as a last resort option for constantly moving objects that cannot be (re)baked to a navigation mesh efficiently. </description> <tutorials> </tutorials> diff --git a/doc/classes/NavigationObstacle3D.xml b/doc/classes/NavigationObstacle3D.xml index f5a0bde089..90eb55887f 100644 --- a/doc/classes/NavigationObstacle3D.xml +++ b/doc/classes/NavigationObstacle3D.xml @@ -5,7 +5,7 @@ </brief_description> <description> 3D Obstacle used in navigation for collision avoidance. The obstacle needs navigation data to work correctly. [NavigationObstacle3D] is physics safe. - [b]Note:[/b] Obstacles are intended as a last resort option for constantly moving objects that cannot be (re)baked to a navigation mesh efficiently. + Obstacles [b]don't[/b] change the resulting path from the pathfinding, they only affect the navigation agent movement in a radius. Therefore, using obstacles for the static walls in your level won't work because those walls don't exist in the pathfinding. The navigation agent will be pushed in a semi-random direction away while moving inside that radius. Obstacles are intended as a last resort option for constantly moving objects that cannot be (re)baked to a navigation mesh efficiently. </description> <tutorials> </tutorials> diff --git a/doc/classes/Node.xml b/doc/classes/Node.xml index d9732da3a3..bceb285584 100644 --- a/doc/classes/Node.xml +++ b/doc/classes/Node.xml @@ -110,11 +110,12 @@ <return type="void" /> <param index="0" name="event" type="InputEvent" /> <description> - Called when an [InputEventKey] or [InputEventShortcut] hasn't been consumed by [method _input] or any GUI [Control] item. The input event propagates up through the node tree until a node consumes it. + Called when an [InputEventKey] hasn't been consumed by [method _input] or any GUI [Control] item. The input event propagates up through the node tree until a node consumes it. It is only called if unhandled key input processing is enabled, which is done automatically if this method is overridden, and can be toggled with [method set_process_unhandled_key_input]. To consume the input event and stop it propagating further to other nodes, [method Viewport.set_input_as_handled] can be called. This method can be used to handle Unicode character input with [kbd]Alt[/kbd], [kbd]Alt + Ctrl[/kbd], and [kbd]Alt + Shift[/kbd] modifiers, after shortcuts were handled. For gameplay input, this and [method _unhandled_input] are usually a better fit than [method _input] as they allow the GUI to intercept the events first. + This method also performs better than [method _unhandled_input], since unrelated events such as [InputEventMouseMotion] are automatically filtered. [b]Note:[/b] This method is only called if the node is present in the scene tree (i.e. if it's not an orphan). </description> </method> @@ -145,7 +146,7 @@ [/csharp] [/codeblocks] If you need the child node to be added below a specific node in the list of children, use [method add_sibling] instead of this method. - [b]Note:[/b] If you want a child to be persisted to a [PackedScene], you must set [member owner] in addition to calling [method add_child]. This is typically relevant for [url=$DOCS_URL/tutorials/misc/running_code_in_the_editor.html]tool scripts[/url] and [url=$DOCS_URL/tutorials/plugins/editor/index.html]editor plugins[/url]. If [method add_child] is called without setting [member owner], the newly added [Node] will not be visible in the scene tree, though it will be visible in the 2D/3D view. + [b]Note:[/b] If you want a child to be persisted to a [PackedScene], you must set [member owner] in addition to calling [method add_child]. This is typically relevant for [url=$DOCS_URL/tutorials/plugins/running_code_in_the_editor.html]tool scripts[/url] and [url=$DOCS_URL/tutorials/plugins/editor/index.html]editor plugins[/url]. If [method add_child] is called without setting [member owner], the newly added [Node] will not be visible in the scene tree, though it will be visible in the 2D/3D view. </description> </method> <method name="add_sibling"> @@ -767,7 +768,7 @@ </member> <member name="owner" type="Node" setter="set_owner" getter="get_owner"> The node owner. A node can have any other node as owner (as long as it is a valid parent, grandparent, etc. ascending in the tree). When saving a node (using [PackedScene]), all the nodes it owns will be saved with it. This allows for the creation of complex [SceneTree]s, with instancing and subinstancing. - [b]Note:[/b] If you want a child to be persisted to a [PackedScene], you must set [member owner] in addition to calling [method add_child]. This is typically relevant for [url=$DOCS_URL/tutorials/misc/running_code_in_the_editor.html]tool scripts[/url] and [url=$DOCS_URL/tutorials/plugins/editor/index.html]editor plugins[/url]. If [method add_child] is called without setting [member owner], the newly added [Node] will not be visible in the scene tree, though it will be visible in the 2D/3D view. + [b]Note:[/b] If you want a child to be persisted to a [PackedScene], you must set [member owner] in addition to calling [method add_child]. This is typically relevant for [url=$DOCS_URL/tutorials/plugins/running_code_in_the_editor.html]tool scripts[/url] and [url=$DOCS_URL/tutorials/plugins/editor/index.html]editor plugins[/url]. If [method add_child] is called without setting [member owner], the newly added [Node] will not be visible in the scene tree, though it will be visible in the 2D/3D view. </member> <member name="process_mode" type="int" setter="set_process_mode" getter="get_process_mode" enum="Node.ProcessMode" default="0"> Can be used to pause or unpause the node, or make the node paused based on the [SceneTree], or make it inherit the process mode from its parent (default). diff --git a/doc/classes/PhysicsDirectBodyState3D.xml b/doc/classes/PhysicsDirectBodyState3D.xml index efe63e4093..04087cbfb6 100644 --- a/doc/classes/PhysicsDirectBodyState3D.xml +++ b/doc/classes/PhysicsDirectBodyState3D.xml @@ -226,6 +226,9 @@ <member name="inverse_inertia" type="Vector3" setter="" getter="get_inverse_inertia"> The inverse of the inertia of the body. </member> + <member name="inverse_inertia_tensor" type="Basis" setter="" getter="get_inverse_inertia_tensor"> + The inverse of the inertia tensor of the body. + </member> <member name="inverse_mass" type="float" setter="" getter="get_inverse_mass"> The inverse of the mass of the body. </member> diff --git a/doc/classes/PhysicsDirectBodyState3DExtension.xml b/doc/classes/PhysicsDirectBodyState3DExtension.xml index ade197eadc..4432f89b9d 100644 --- a/doc/classes/PhysicsDirectBodyState3DExtension.xml +++ b/doc/classes/PhysicsDirectBodyState3DExtension.xml @@ -159,6 +159,11 @@ <description> </description> </method> + <method name="_get_inverse_inertia_tensor" qualifiers="virtual const"> + <return type="Basis" /> + <description> + </description> + </method> <method name="_get_inverse_mass" qualifiers="virtual const"> <return type="float" /> <description> diff --git a/doc/classes/StaticBody2D.xml b/doc/classes/StaticBody2D.xml index 21c160b780..2393790e12 100644 --- a/doc/classes/StaticBody2D.xml +++ b/doc/classes/StaticBody2D.xml @@ -5,7 +5,7 @@ </brief_description> <description> Static body for 2D physics. - A static body is a simple body that can't be moved by external forces or contacts. It is ideal for implementing objects in the environment, such as walls or platforms. In contrast to [RigidBody2D], it doesn't consume any CPU resources as long as they don't move. + A static body is a simple body that doesn't move under physics simulation, i.e. it can't be moved by external forces or contacts but its transformation can still be updated manually by the user. It is ideal for implementing objects in the environment, such as walls or platforms. In contrast to [RigidBody2D], it doesn't consume any CPU resources as long as they don't move. They have extra functionalities to move and affect other bodies: [b]Static transform change:[/b] Static bodies can be moved by animation or script. In this case, they are just teleported and don't affect other bodies on their path. [b]Constant velocity:[/b] When [member constant_linear_velocity] or [member constant_angular_velocity] is set, static bodies don't move themselves but affect touching bodies as if they were moving. This is useful for simulating conveyor belts or conveyor wheels. diff --git a/doc/classes/StaticBody3D.xml b/doc/classes/StaticBody3D.xml index daa71d1168..0beaa6bb52 100644 --- a/doc/classes/StaticBody3D.xml +++ b/doc/classes/StaticBody3D.xml @@ -5,7 +5,7 @@ </brief_description> <description> Static body for 3D physics. - A static body is a simple body that can't be moved by external forces or contacts. It is ideal for implementing objects in the environment, such as walls or platforms. In contrast to [RigidBody3D], it doesn't consume any CPU resources as long as they don't move. + A static body is a simple body that doesn't move under physics simulation, i.e. it can't be moved by external forces or contacts but its transformation can still be updated manually by the user. It is ideal for implementing objects in the environment, such as walls or platforms. In contrast to [RigidBody3D], it doesn't consume any CPU resources as long as they don't move. They have extra functionalities to move and affect other bodies: [b]Static transform change:[/b] Static bodies can be moved by animation or script. In this case, they are just teleported and don't affect other bodies on their path. [b]Constant velocity:[/b] When [member constant_linear_velocity] or [member constant_angular_velocity] is set, static bodies don't move themselves but affect touching bodies as if they were moving. This is useful for simulating conveyor belts or conveyor wheels. diff --git a/doc/classes/TextEdit.xml b/doc/classes/TextEdit.xml index 0905c0c20b..2f9b971fc8 100644 --- a/doc/classes/TextEdit.xml +++ b/doc/classes/TextEdit.xml @@ -5,6 +5,7 @@ </brief_description> <description> TextEdit is meant for editing large, multiline text. It also has facilities for editing code, such as syntax highlighting support and multiple levels of undo/redo. + [b]Note:[/b] Most viewport, caret and edit methods contain a [code]caret_index[/code] argument for [member caret_multiple] support. The argument should be one of the following: [code]-1[/code] for all carets, [code]0[/code] for the main caret, or greater than [code]0[/code] for secondary carets. [b]Note:[/b] When holding down [kbd]Alt[/kbd], the vertical scroll wheel will scroll 5 times as fast as it would normally do. This also works in the Godot script editor. </description> <tutorials> @@ -12,18 +13,21 @@ <methods> <method name="_backspace" qualifiers="virtual"> <return type="void" /> + <param index="0" name="caret_index" type="int" /> <description> Override this method to define what happens when the user presses the backspace key. </description> </method> <method name="_copy" qualifiers="virtual"> <return type="void" /> + <param index="0" name="caret_index" type="int" /> <description> Override this method to define what happens when the user performs a copy operation. </description> </method> <method name="_cut" qualifiers="virtual"> <return type="void" /> + <param index="0" name="caret_index" type="int" /> <description> Override this method to define what happens when the user performs a cut operation. </description> @@ -31,23 +35,34 @@ <method name="_handle_unicode_input" qualifiers="virtual"> <return type="void" /> <param index="0" name="unicode_char" type="int" /> + <param index="1" name="caret_index" type="int" /> <description> Override this method to define what happens when the user types in the provided key [param unicode_char]. </description> </method> <method name="_paste" qualifiers="virtual"> <return type="void" /> + <param index="0" name="caret_index" type="int" /> <description> Override this method to define what happens when the user performs a paste operation. </description> </method> <method name="_paste_primary_clipboard" qualifiers="virtual"> <return type="void" /> + <param index="0" name="caret_index" type="int" /> <description> Override this method to define what happens when the user performs a paste operation with middle mouse button. [b]Note:[/b] This method is only implemented on Linux. </description> </method> + <method name="add_caret"> + <return type="int" /> + <param index="0" name="line" type="int" /> + <param index="1" name="col" type="int" /> + <description> + Adds a new caret at the given location. Returns the index of the new caret, or [code]-1[/code] if the location is invalid. + </description> + </method> <method name="add_gutter"> <return type="void" /> <param index="0" name="at" type="int" default="-1" /> @@ -55,14 +70,27 @@ Register a new gutter to this [TextEdit]. Use [param at] to have a specific gutter order. A value of [code]-1[/code] appends the gutter to the right. </description> </method> + <method name="adjust_carets_after_edit"> + <return type="void" /> + <param index="0" name="caret" type="int" /> + <param index="1" name="from_line" type="int" /> + <param index="2" name="from_col" type="int" /> + <param index="3" name="to_line" type="int" /> + <param index="4" name="to_col" type="int" /> + <description> + Reposition the carets affected by the edit. This assumes edits are applied in edit order, see [method get_caret_index_edit_order]. + </description> + </method> <method name="adjust_viewport_to_caret"> <return type="void" /> + <param index="0" name="caret_index" type="int" default="0" /> <description> Adjust the viewport so the caret is visible. </description> </method> <method name="backspace"> <return type="void" /> + <param index="0" name="caret_index" type="int" default="-1" /> <description> Called when the user presses the backspace key. Can be overridden with [method _backspace]. </description> @@ -75,6 +103,7 @@ </method> <method name="center_viewport_to_caret"> <return type="void" /> + <param index="0" name="caret_index" type="int" default="0" /> <description> Centers the viewport on the line the editing caret is at. This also resets the [member scroll_horizontal] value to [code]0[/code]. </description> @@ -93,28 +122,38 @@ </method> <method name="copy"> <return type="void" /> + <param index="0" name="caret_index" type="int" default="-1" /> <description> Copies the current text selection. Can be overridden with [method _copy]. </description> </method> <method name="cut"> <return type="void" /> + <param index="0" name="caret_index" type="int" default="-1" /> <description> Cut's the current selection. Can be overridden with [method _cut]. </description> </method> <method name="delete_selection"> <return type="void" /> + <param index="0" name="caret_index" type="int" default="-1" /> <description> Deletes the selected text. </description> </method> <method name="deselect"> <return type="void" /> + <param index="0" name="caret_index" type="int" default="-1" /> <description> Deselects the current selection. </description> </method> + <method name="end_action"> + <return type="void" /> + <description> + Marks the end of steps in the current action started with [method start_action]. + </description> + </method> <method name="end_complex_operation"> <return type="void" /> <description> @@ -123,24 +162,40 @@ </method> <method name="get_caret_column" qualifiers="const"> <return type="int" /> + <param index="0" name="caret_index" type="int" default="0" /> <description> Returns the column the editing caret is at. </description> </method> + <method name="get_caret_count" qualifiers="const"> + <return type="int" /> + <description> + Returns the number of carets in this [TextEdit]. + </description> + </method> <method name="get_caret_draw_pos" qualifiers="const"> <return type="Vector2" /> + <param index="0" name="caret_index" type="int" default="0" /> <description> Returns the caret pixel draw position. </description> </method> + <method name="get_caret_index_edit_order"> + <return type="PackedInt32Array" /> + <description> + Returns a list of caret indexes in their edit order, this done from bottom to top. Edit order refers to the way actions such as [method insert_text_at_caret] are applied. + </description> + </method> <method name="get_caret_line" qualifiers="const"> <return type="int" /> + <param index="0" name="caret_index" type="int" default="0" /> <description> Returns the line the editing caret is on. </description> </method> <method name="get_caret_wrap_index" qualifiers="const"> <return type="int" /> + <param index="0" name="caret_index" type="int" default="0" /> <description> Returns the wrap index the editing caret is on. </description> @@ -381,32 +436,37 @@ Returns the scroll position for [param wrap_index] of [param line]. </description> </method> - <method name="get_selected_text" qualifiers="const"> + <method name="get_selected_text"> <return type="String" /> + <param index="0" name="caret_index" type="int" default="-1" /> <description> Returns the text inside the selection. </description> </method> <method name="get_selection_column" qualifiers="const"> <return type="int" /> + <param index="0" name="caret_index" type="int" default="0" /> <description> Returns the original start column of the selection. </description> </method> <method name="get_selection_from_column" qualifiers="const"> <return type="int" /> + <param index="0" name="caret_index" type="int" default="0" /> <description> Returns the selection begin column. </description> </method> <method name="get_selection_from_line" qualifiers="const"> <return type="int" /> + <param index="0" name="caret_index" type="int" default="0" /> <description> Returns the selection begin line. </description> </method> <method name="get_selection_line" qualifiers="const"> <return type="int" /> + <param index="0" name="caret_index" type="int" default="0" /> <description> Returns the original start line of the selection. </description> @@ -419,12 +479,14 @@ </method> <method name="get_selection_to_column" qualifiers="const"> <return type="int" /> + <param index="0" name="caret_index" type="int" default="0" /> <description> Returns the selection end column. </description> </method> <method name="get_selection_to_line" qualifiers="const"> <return type="int" /> + <param index="0" name="caret_index" type="int" default="0" /> <description> Returns the selection end line. </description> @@ -476,6 +538,7 @@ </method> <method name="get_word_under_caret" qualifiers="const"> <return type="String" /> + <param index="0" name="caret_index" type="int" default="-1" /> <description> Returns a [String] text with the word under the caret's location. </description> @@ -494,6 +557,7 @@ </method> <method name="has_selection" qualifiers="const"> <return type="bool" /> + <param index="0" name="caret_index" type="int" default="-1" /> <description> Returns [code]true[/code] if the user has selected text. </description> @@ -515,12 +579,14 @@ <method name="insert_text_at_caret"> <return type="void" /> <param index="0" name="text" type="String" /> + <param index="1" name="caret_index" type="int" default="-1" /> <description> Insert the specified text at the caret position. </description> </method> <method name="is_caret_visible" qualifiers="const"> <return type="bool" /> + <param index="0" name="caret_index" type="int" default="0" /> <description> Returns [code]true[/code] if the caret is visible on the screen. </description> @@ -576,6 +642,7 @@ <method name="is_mouse_over_selection" qualifiers="const"> <return type="bool" /> <param index="0" name="edges" type="bool" /> + <param index="1" name="caret_index" type="int" default="-1" /> <description> Returns whether the mouse is over selection. If [param edges] is [code]true[/code], the edges are considered part of the selection. </description> @@ -601,18 +668,41 @@ Merge the gutters from [param from_line] into [param to_line]. Only overwritable gutters will be copied. </description> </method> + <method name="merge_overlapping_carets"> + <return type="void" /> + <description> + Merges any overlapping carets. Will favour the newest caret, or the caret with a selection. + [b]Note:[/b] This is not called when a caret changes position but after certain actions, so it is possible to get into a state where carets overlap. + </description> + </method> <method name="paste"> <return type="void" /> + <param index="0" name="caret_index" type="int" default="-1" /> <description> Paste at the current location. Can be overridden with [method _paste]. </description> </method> + <method name="paste_primary_clipboard"> + <return type="void" /> + <param index="0" name="caret_index" type="int" default="-1" /> + <description> + Pastes the primary clipboard. + </description> + </method> <method name="redo"> <return type="void" /> <description> Perform redo operation. </description> </method> + <method name="remove_caret"> + <return type="void" /> + <param index="0" name="caret" type="int" /> + <description> + Removes the given caret index. + [b]Note:[/b] This can result in adjustment of all other caret indices. + </description> + </method> <method name="remove_gutter"> <return type="void" /> <param index="0" name="gutter" type="int" /> @@ -620,6 +710,12 @@ Removes the gutter from this [TextEdit]. </description> </method> + <method name="remove_secondary_carets"> + <return type="void" /> + <description> + Removes all additional carets. + </description> + </method> <method name="remove_text"> <return type="void" /> <param index="0" name="from_line" type="int" /> @@ -666,6 +762,7 @@ <param index="1" name="from_column" type="int" /> <param index="2" name="to_line" type="int" /> <param index="3" name="to_column" type="int" /> + <param index="4" name="caret_index" type="int" default="0" /> <description> Perform selection, from line/column to line/column. If [member selecting_enabled] is [code]false[/code], no selection will occur. @@ -680,6 +777,7 @@ </method> <method name="select_word_under_caret"> <return type="void" /> + <param index="0" name="caret_index" type="int" default="-1" /> <description> Selects the word under the caret. </description> @@ -688,9 +786,11 @@ <return type="void" /> <param index="0" name="column" type="int" /> <param index="1" name="adjust_viewport" type="bool" default="true" /> + <param index="2" name="caret_index" type="int" default="0" /> <description> Moves the caret to the specified [param column] index. If [param adjust_viewport] is [code]true[/code], the viewport will center at the caret position after the move occurs. + [b]Note:[/b] If supporting multiple carets this will not check for any overlap. See [method merge_overlapping_carets]. </description> </method> <method name="set_caret_line"> @@ -699,10 +799,12 @@ <param index="1" name="adjust_viewport" type="bool" default="true" /> <param index="2" name="can_be_hidden" type="bool" default="true" /> <param index="3" name="wrap_index" type="int" default="0" /> + <param index="4" name="caret_index" type="int" default="0" /> <description> Moves the caret to the specified [param line] index. If [param adjust_viewport] is [code]true[/code], the viewport will center at the caret position after the move occurs. If [param can_be_hidden] is [code]true[/code], the specified [code]line[/code] can be hidden. + [b]Note:[/b] If supporting multiple carets this will not check for any overlap. See [method merge_overlapping_carets]. </description> </method> <method name="set_gutter_clickable"> @@ -872,6 +974,7 @@ <param index="0" name="mode" type="int" enum="TextEdit.SelectionMode" /> <param index="1" name="line" type="int" default="-1" /> <param index="2" name="column" type="int" default="-1" /> + <param index="3" name="caret_index" type="int" default="0" /> <description> Sets the current selection mode. </description> @@ -890,6 +993,14 @@ Provide custom tooltip text. The callback method must take the following args: [code]hovered_word: String[/code] </description> </method> + <method name="start_action"> + <return type="void" /> + <param index="0" name="action" type="int" enum="TextEdit.EditAction" /> + <description> + Starts an action, will end the current action if [code]action[/code] is different. + An action will also end after a call to [method end_action], after [member ProjectSettings.gui/timers/text_edit_idle_detect_sec] is triggered or a new undoable step outside the [method start_action] and [method end_action] calls. + </description> + </method> <method name="swap_lines"> <return type="void" /> <param index="0" name="from_line" type="int" /> @@ -926,6 +1037,9 @@ If [code]true[/code], a right-click moves the caret at the mouse position before displaying the context menu. If [code]false[/code], the context menu disregards mouse location. </member> + <member name="caret_multiple" type="bool" setter="set_multiple_carets_enabled" getter="is_multiple_carets_enabled" default="true"> + Sets if multiple carets are allowed. + </member> <member name="caret_type" type="int" setter="set_caret_type" getter="get_caret_type" enum="TextEdit.CaretType" default="0"> Set the type of caret to draw. </member> @@ -1154,6 +1268,18 @@ <constant name="MENU_MAX" value="28" enum="MenuItems"> Represents the size of the [enum MenuItems] enum. </constant> + <constant name="ACTION_NONE" value="0" enum="EditAction"> + No current action. + </constant> + <constant name="ACTION_TYPING" value="1" enum="EditAction"> + A typing action. + </constant> + <constant name="ACTION_BACKSPACE" value="2" enum="EditAction"> + A backwards delete action. + </constant> + <constant name="ACTION_DELETE" value="3" enum="EditAction"> + A forward delete action. + </constant> <constant name="SEARCH_MATCH_CASE" value="1" enum="SearchFlags"> Match case when searching. </constant> diff --git a/doc/classes/TileMap.xml b/doc/classes/TileMap.xml index fd9c44091c..348184a16a 100644 --- a/doc/classes/TileMap.xml +++ b/doc/classes/TileMap.xml @@ -155,6 +155,7 @@ <method name="get_layers_count" qualifiers="const"> <return type="int" /> <description> + Returns the number of layers in the TileMap. </description> </method> <method name="get_neighbor_cell" qualifiers="const"> diff --git a/doc/classes/Viewport.xml b/doc/classes/Viewport.xml index 87ee26fa32..998e782975 100644 --- a/doc/classes/Viewport.xml +++ b/doc/classes/Viewport.xml @@ -129,6 +129,9 @@ <method name="is_input_handled" qualifiers="const"> <return type="bool" /> <description> + Returns whether the current [InputEvent] has been handled. Input events are not handled until [method set_input_as_handled] has been called during the lifetime of an [InputEvent]. + This is usually done as part of input handling methods like [method Node._input], [method Control._gui_input] or others, as well as in corresponding signal handlers. + If [member handle_input_locally] is set to [code]false[/code], this method will try finding the first parent viewport that is set to handle input locally, and return its value for [method is_input_handled] instead. </description> </method> <method name="push_input"> @@ -136,6 +139,13 @@ <param index="0" name="event" type="InputEvent" /> <param index="1" name="in_local_coords" type="bool" default="false" /> <description> + Triggers the given [param event] in this [Viewport]. This can be used to pass an [InputEvent] between viewports, or to locally apply inputs that were sent over the network or saved to a file. + If [param in_local_coords] is [code]false[/code], the event's position is in the embedder's coordinates and will be converted to viewport coordinates. If [param in_local_coords] is [code]true[/code], the event's position is in viewport coordinates. + While this method serves a similar purpose as [method Input.parse_input_event], it does not remap the specified [param event] based on project settings like [member ProjectSettings.input_devices/pointing/emulate_touch_from_mouse]. + Calling this method will propagate calls to child nodes for following methods in the given order: + - [method Node._input] + - [method Control._gui_input] for [Control] nodes + If an earlier method marks the input as handled via [method set_input_as_handled], any later method in this list will not be called. </description> </method> <method name="push_text_input"> @@ -149,6 +159,15 @@ <param index="0" name="event" type="InputEvent" /> <param index="1" name="in_local_coords" type="bool" default="false" /> <description> + Triggers the given [InputEvent] in this [Viewport]. This can be used to pass input events between viewports, or to locally apply inputs that were sent over the network or saved to a file. + If [param in_local_coords] is [code]false[/code], the event's position is in the embedder's coordinates and will be converted to viewport coordinates. If [param in_local_coords] is [code]true[/code], the event's position is in viewport coordinates. + While this method serves a similar purpose as [method Input.parse_input_event], it does not remap the specified [param event] based on project settings like [member ProjectSettings.input_devices/pointing/emulate_touch_from_mouse]. + Calling this method will propagate calls to child nodes for following methods in the given order: + - [method Node._shortcut_input] + - [method Node._unhandled_input] + - [method Node._unhandled_key_input] + If an earlier method marks the input as handled via [method set_input_as_handled], any later method in this list will not be called. + If none of the methods handle the event and [member physics_object_picking] is [code]true[/code], the event is used for physics object picking. </description> </method> <method name="set_input_as_handled"> @@ -212,6 +231,9 @@ If [code]true[/code], the GUI controls on the viewport will lay pixel perfectly. </member> <member name="handle_input_locally" type="bool" setter="set_handle_input_locally" getter="is_handling_input_locally" default="true"> + If [code]true[/code], this viewport will mark incoming input events as handled by itself. If [code]false[/code], this is instead done by the the first parent viewport that is set to handle input locally. + A [SubViewportContainer] will automatically set this property to [code]false[/code] for the [Viewport] contained inside of it. + See also [method set_input_as_handled] and [method is_input_handled]. </member> <member name="mesh_lod_threshold" type="float" setter="set_mesh_lod_threshold" getter="get_mesh_lod_threshold" default="1.0"> The automatic LOD bias to use for meshes rendered within the [Viewport] (this is analogous to [member ReflectionProbe.mesh_lod_threshold]). Higher values will use less detailed versions of meshes that have LOD variations generated. If set to [code]0.0[/code], automatic LOD is disabled. Increase [member mesh_lod_threshold] to improve performance at the cost of geometry detail. diff --git a/doc/classes/Window.xml b/doc/classes/Window.xml index a688fb36b3..c278f7031f 100644 --- a/doc/classes/Window.xml +++ b/doc/classes/Window.xml @@ -469,7 +469,7 @@ <signal name="window_input"> <param index="0" name="event" type="InputEvent" /> <description> - Emitted when the [Window] is currently focused and receives any input, passing the received event as an argument. + Emitted when the [Window] is currently focused and receives any input, passing the received event as an argument. The event's position, if present, is in the embedder's coordinate system. </description> </signal> </signals> diff --git a/drivers/gles3/rasterizer_canvas_gles3.h b/drivers/gles3/rasterizer_canvas_gles3.h index 372ac00493..15556e3193 100644 --- a/drivers/gles3/rasterizer_canvas_gles3.h +++ b/drivers/gles3/rasterizer_canvas_gles3.h @@ -235,14 +235,14 @@ public: GLuint vertex_buffer; GLuint vertex_array; GLuint index_buffer; - int count; + int count = 0; bool color_disabled = false; Color color; }; struct { HashMap<PolygonID, PolygonBuffers> polygons; - PolygonID last_id; + PolygonID last_id = 0; } polygon_buffers; RendererCanvasRender::PolygonID request_polygon(const Vector<int> &p_indices, const Vector<Point2> &p_points, const Vector<Color> &p_colors, const Vector<Point2> &p_uvs = Vector<Point2>(), const Vector<int> &p_bones = Vector<int>(), const Vector<float> &p_weights = Vector<float>()) override; diff --git a/drivers/pulseaudio/audio_driver_pulseaudio.h b/drivers/pulseaudio/audio_driver_pulseaudio.h index 85e328b49f..ae6e0acc97 100644 --- a/drivers/pulseaudio/audio_driver_pulseaudio.h +++ b/drivers/pulseaudio/audio_driver_pulseaudio.h @@ -48,8 +48,8 @@ class AudioDriverPulseAudio : public AudioDriver { pa_context *pa_ctx = nullptr; pa_stream *pa_str = nullptr; pa_stream *pa_rec_str = nullptr; - pa_channel_map pa_map; - pa_channel_map pa_rec_map; + pa_channel_map pa_map = {}; + pa_channel_map pa_rec_map = {}; String device_name = "Default"; String new_device = "Default"; diff --git a/drivers/unix/dir_access_unix.cpp b/drivers/unix/dir_access_unix.cpp index c25ceba44d..d894ceba59 100644 --- a/drivers/unix/dir_access_unix.cpp +++ b/drivers/unix/dir_access_unix.cpp @@ -71,7 +71,7 @@ bool DirAccessUnix::file_exists(String p_file) { p_file = fix_path(p_file); - struct stat flags; + struct stat flags = {}; bool success = (stat(p_file.utf8().get_data(), &flags) == 0); if (success && S_ISDIR(flags.st_mode)) { @@ -90,7 +90,7 @@ bool DirAccessUnix::dir_exists(String p_dir) { p_dir = fix_path(p_dir); - struct stat flags; + struct stat flags = {}; bool success = (stat(p_dir.utf8().get_data(), &flags) == 0); return (success && S_ISDIR(flags.st_mode)); @@ -125,7 +125,7 @@ uint64_t DirAccessUnix::get_modified_time(String p_file) { p_file = fix_path(p_file); - struct stat flags; + struct stat flags = {}; bool success = (stat(p_file.utf8().get_data(), &flags) == 0); if (success) { @@ -158,7 +158,7 @@ String DirAccessUnix::get_next() { if (entry->d_type == DT_UNKNOWN || entry->d_type == DT_LNK) { String f = current_dir.path_join(fname); - struct stat flags; + struct stat flags = {}; if (stat(f.utf8().get_data(), &flags) == 0) { _cisdir = S_ISDIR(flags.st_mode); } else { @@ -412,7 +412,7 @@ Error DirAccessUnix::remove(String p_path) { p_path = fix_path(p_path); - struct stat flags; + struct stat flags = {}; if ((stat(p_path.utf8().get_data(), &flags) != 0)) { return FAILED; } @@ -431,7 +431,7 @@ bool DirAccessUnix::is_link(String p_file) { p_file = fix_path(p_file); - struct stat flags; + struct stat flags = {}; if ((lstat(p_file.utf8().get_data(), &flags) != 0)) { return FAILED; } diff --git a/drivers/unix/file_access_unix.cpp b/drivers/unix/file_access_unix.cpp index 0df645c4e5..0b80fb1491 100644 --- a/drivers/unix/file_access_unix.cpp +++ b/drivers/unix/file_access_unix.cpp @@ -86,7 +86,7 @@ Error FileAccessUnix::open_internal(const String &p_path, int p_mode_flags) { backend (unix-compatible mostly) supports utf8 encoding */ //printf("opening %s as %s\n", p_path.utf8().get_data(), path.utf8().get_data()); - struct stat st; + struct stat st = {}; int err = stat(path.utf8().get_data(), &st); if (!err) { switch (st.st_mode & S_IFMT) { @@ -252,7 +252,7 @@ void FileAccessUnix::store_buffer(const uint8_t *p_src, uint64_t p_length) { bool FileAccessUnix::file_exists(const String &p_path) { int err; - struct stat st; + struct stat st = {}; String filename = fix_path(p_path); // Does the name exist at all? @@ -284,7 +284,7 @@ bool FileAccessUnix::file_exists(const String &p_path) { uint64_t FileAccessUnix::_get_modified_time(const String &p_file) { String file = fix_path(p_file); - struct stat flags; + struct stat flags = {}; int err = stat(file.utf8().get_data(), &flags); if (!err) { @@ -297,7 +297,7 @@ uint64_t FileAccessUnix::_get_modified_time(const String &p_file) { uint32_t FileAccessUnix::_get_unix_permissions(const String &p_file) { String file = fix_path(p_file); - struct stat flags; + struct stat flags = {}; int err = stat(file.utf8().get_data(), &flags); if (!err) { diff --git a/drivers/unix/os_unix.cpp b/drivers/unix/os_unix.cpp index 748b48d316..10d65b83db 100644 --- a/drivers/unix/os_unix.cpp +++ b/drivers/unix/os_unix.cpp @@ -500,10 +500,6 @@ bool OS_Unix::set_environment(const String &p_var, const String &p_value) const return setenv(p_var.utf8().get_data(), p_value.utf8().get_data(), /* overwrite: */ true) == 0; } -int OS_Unix::get_processor_count() const { - return sysconf(_SC_NPROCESSORS_CONF); -} - String OS_Unix::get_user_data_dir() const { String appname = get_safe_dir_name(ProjectSettings::get_singleton()->get("application/config/name")); if (!appname.is_empty()) { diff --git a/drivers/unix/os_unix.h b/drivers/unix/os_unix.h index ce81ca0aca..fce962e32c 100644 --- a/drivers/unix/os_unix.h +++ b/drivers/unix/os_unix.h @@ -84,8 +84,6 @@ public: virtual bool set_environment(const String &p_var, const String &p_value) const override; virtual String get_locale() const override; - virtual int get_processor_count() const override; - virtual void initialize_debugging() override; virtual String get_executable_path() const override; diff --git a/drivers/unix/thread_posix.cpp b/drivers/unix/thread_posix.cpp index 6292d8b3bc..f6adbee108 100644 --- a/drivers/unix/thread_posix.cpp +++ b/drivers/unix/thread_posix.cpp @@ -70,7 +70,7 @@ static Error set_name(const String &p_name) { } void init_thread_posix() { - Thread::_set_platform_funcs(&set_name, nullptr); + Thread::_set_platform_functions({ .set_name = set_name }); } #endif // UNIX_ENABLED || PTHREAD_ENABLED diff --git a/drivers/vulkan/vulkan_context.cpp b/drivers/vulkan/vulkan_context.cpp index 3441afdb82..e64b0c4a84 100644 --- a/drivers/vulkan/vulkan_context.cpp +++ b/drivers/vulkan/vulkan_context.cpp @@ -2260,8 +2260,6 @@ Error VulkanContext::swap_buffers() { } } #endif - static int total_frames = 0; - total_frames++; // print_line("current buffer: " + itos(current_buffer)); err = fpQueuePresentKHR(present_queue, &present); diff --git a/editor/action_map_editor.cpp b/editor/action_map_editor.cpp index 4d48dd5ac5..9b1ff78727 100644 --- a/editor/action_map_editor.cpp +++ b/editor/action_map_editor.cpp @@ -28,777 +28,12 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "action_map_editor.h" - -#include "core/input/input_map.h" -#include "core/os/keyboard.h" +#include "editor/action_map_editor.h" #include "editor/editor_scale.h" -#include "scene/gui/separator.h" - -bool EventListenerLineEdit::_is_event_allowed(const Ref<InputEvent> &p_event) const { - const Ref<InputEventMouseButton> mb = p_event; - const Ref<InputEventKey> k = p_event; - const Ref<InputEventJoypadButton> jb = p_event; - const Ref<InputEventJoypadMotion> jm = p_event; - - return (mb.is_valid() && (allowed_input_types & INPUT_MOUSE_BUTTON)) || - (k.is_valid() && (allowed_input_types & INPUT_KEY)) || - (jb.is_valid() && (allowed_input_types & INPUT_JOY_BUTTON)) || - (jm.is_valid() && (allowed_input_types & INPUT_JOY_MOTION)); -} - -void EventListenerLineEdit::gui_input(const Ref<InputEvent> &p_event) { - const Ref<InputEventMouseMotion> mm = p_event; - if (mm.is_valid()) { - LineEdit::gui_input(p_event); - return; - } - - // Allow mouse button click on the clear button without being treated as an event. - const Ref<InputEventMouseButton> b = p_event; - if (b.is_valid() && _is_over_clear_button(b->get_position())) { - LineEdit::gui_input(p_event); - return; - } - - // First event will be an event which is used to focus this control - i.e. a mouse click, or a tab press. - // Ignore the first one so that clicking into the LineEdit does not override the current event. - // Ignore is reset to true when the control is unfocused. - if (ignore) { - ignore = false; - return; - } - - accept_event(); - if (!p_event->is_pressed() || p_event->is_echo() || p_event->is_match(event) || !_is_event_allowed(p_event)) { - return; - } - - event = p_event; - set_text(event->as_text()); - emit_signal("event_changed", event); -} - -void EventListenerLineEdit::_on_text_changed(const String &p_text) { - if (p_text.is_empty()) { - clear_event(); - } -} - -void EventListenerLineEdit::_on_focus() { - set_placeholder(TTR("Listening for input...")); -} - -void EventListenerLineEdit::_on_unfocus() { - ignore = true; - set_placeholder(TTR("Filter by event...")); -} - -Ref<InputEvent> EventListenerLineEdit::get_event() const { - return event; -} - -void EventListenerLineEdit::clear_event() { - if (event.is_valid()) { - event = Ref<InputEvent>(); - set_text(""); - emit_signal("event_changed", event); - } -} - -void EventListenerLineEdit::set_allowed_input_types(int input_types) { - allowed_input_types = input_types; -} - -int EventListenerLineEdit::get_allowed_input_types() const { - return allowed_input_types; -} - -void EventListenerLineEdit::_notification(int p_what) { - switch (p_what) { - case NOTIFICATION_ENTER_TREE: { - connect("text_changed", callable_mp(this, &EventListenerLineEdit::_on_text_changed)); - connect("focus_entered", callable_mp(this, &EventListenerLineEdit::_on_focus)); - connect("focus_exited", callable_mp(this, &EventListenerLineEdit::_on_unfocus)); - set_right_icon(get_theme_icon(SNAME("Keyboard"), SNAME("EditorIcons"))); - set_clear_button_enabled(true); - } break; - } -} - -void EventListenerLineEdit::_bind_methods() { - ADD_SIGNAL(MethodInfo("event_changed", PropertyInfo(Variant::OBJECT, "event", PROPERTY_HINT_RESOURCE_TYPE, "InputEvent"))); -} - -EventListenerLineEdit::EventListenerLineEdit() { - set_caret_blink_enabled(false); - set_placeholder(TTR("Filter by event...")); -} - -///////////////////////////////////////// - -// Maps to 2*axis if value is neg, or 2*axis+1 if value is pos. -static const char *_joy_axis_descriptions[(size_t)JoyAxis::MAX * 2] = { - TTRC("Left Stick Left, Joystick 0 Left"), - TTRC("Left Stick Right, Joystick 0 Right"), - TTRC("Left Stick Up, Joystick 0 Up"), - TTRC("Left Stick Down, Joystick 0 Down"), - TTRC("Right Stick Left, Joystick 1 Left"), - TTRC("Right Stick Right, Joystick 1 Right"), - TTRC("Right Stick Up, Joystick 1 Up"), - TTRC("Right Stick Down, Joystick 1 Down"), - TTRC("Joystick 2 Left"), - TTRC("Left Trigger, Sony L2, Xbox LT, Joystick 2 Right"), - TTRC("Joystick 2 Up"), - TTRC("Right Trigger, Sony R2, Xbox RT, Joystick 2 Down"), - TTRC("Joystick 3 Left"), - TTRC("Joystick 3 Right"), - TTRC("Joystick 3 Up"), - TTRC("Joystick 3 Down"), - TTRC("Joystick 4 Left"), - TTRC("Joystick 4 Right"), - TTRC("Joystick 4 Up"), - TTRC("Joystick 4 Down"), -}; - -String InputEventConfigurationDialog::get_event_text(const Ref<InputEvent> &p_event, bool p_include_device) const { - ERR_FAIL_COND_V_MSG(p_event.is_null(), String(), "Provided event is not a valid instance of InputEvent"); - - String text = p_event->as_text(); - - Ref<InputEventKey> key = p_event; - if (key.is_valid() && key->is_command_or_control_autoremap()) { -#ifdef MACOS_ENABLED - text = text.replace("Command", "Command/Ctrl"); -#else - text = text.replace("Ctrl", "Command/Ctrl"); -#endif - } - Ref<InputEventMouse> mouse = p_event; - Ref<InputEventJoypadMotion> jp_motion = p_event; - Ref<InputEventJoypadButton> jp_button = p_event; - if (jp_motion.is_valid()) { - // Joypad motion events will display slightly differently than what the event->as_text() provides. See #43660. - String desc = TTR("Unknown Joypad Axis"); - if (jp_motion->get_axis() < JoyAxis::MAX) { - desc = RTR(_joy_axis_descriptions[2 * (size_t)jp_motion->get_axis() + (jp_motion->get_axis_value() < 0 ? 0 : 1)]); - } - - text = vformat("Joypad Axis %s %s (%s)", itos((int64_t)jp_motion->get_axis()), jp_motion->get_axis_value() < 0 ? "-" : "+", desc); - } - if (p_include_device && (mouse.is_valid() || jp_button.is_valid() || jp_motion.is_valid())) { - String device_string = _get_device_string(p_event->get_device()); - text += vformat(" - %s", device_string); - } - - return text; -} - -void InputEventConfigurationDialog::_set_event(const Ref<InputEvent> &p_event, bool p_update_input_list_selection) { - if (p_event.is_valid()) { - event = p_event; - - // If the event is changed to something which is not the same as the listener, - // clear out the event from the listener text box to avoid confusion. - const Ref<InputEvent> listener_event = event_listener->get_event(); - if (listener_event.is_valid() && !listener_event->is_match(p_event)) { - event_listener->clear_event(); - } - - // Update Label - event_as_text->set_text(get_event_text(event, true)); - - Ref<InputEventKey> k = p_event; - Ref<InputEventMouseButton> mb = p_event; - Ref<InputEventJoypadButton> joyb = p_event; - Ref<InputEventJoypadMotion> joym = p_event; - Ref<InputEventWithModifiers> mod = p_event; - - // Update option values and visibility - bool show_mods = false; - bool show_device = false; - bool show_phys_key = false; - - if (mod.is_valid()) { - show_mods = true; - mod_checkboxes[MOD_ALT]->set_pressed(mod->is_alt_pressed()); - mod_checkboxes[MOD_SHIFT]->set_pressed(mod->is_shift_pressed()); - mod_checkboxes[MOD_CTRL]->set_pressed(mod->is_ctrl_pressed()); - mod_checkboxes[MOD_META]->set_pressed(mod->is_meta_pressed()); - - autoremap_command_or_control_checkbox->set_pressed(mod->is_command_or_control_autoremap()); - } - - if (k.is_valid()) { - show_phys_key = true; - physical_key_checkbox->set_pressed(k->get_physical_keycode() != Key::NONE && k->get_keycode() == Key::NONE); - - } else if (joyb.is_valid() || joym.is_valid() || mb.is_valid()) { - show_device = true; - _set_current_device(event->get_device()); - } - - mod_container->set_visible(show_mods); - device_container->set_visible(show_device); - physical_key_checkbox->set_visible(show_phys_key); - additional_options_container->show(); - - // Update selected item in input list. - if (p_update_input_list_selection && (k.is_valid() || joyb.is_valid() || joym.is_valid() || mb.is_valid())) { - TreeItem *category = input_list_tree->get_root()->get_first_child(); - while (category) { - TreeItem *input_item = category->get_first_child(); - - if (input_item != nullptr) { - // input_type should always be > 0, unless the tree structure has been misconfigured. - int input_type = input_item->get_parent()->get_meta("__type", 0); - if (input_type == 0) { - return; - } - - // If event type matches input types of this category. - if ((k.is_valid() && input_type == INPUT_KEY) || (joyb.is_valid() && input_type == INPUT_JOY_BUTTON) || (joym.is_valid() && input_type == INPUT_JOY_MOTION) || (mb.is_valid() && input_type == INPUT_MOUSE_BUTTON)) { - // Loop through all items of this category until one matches. - while (input_item) { - bool key_match = k.is_valid() && (Variant(k->get_keycode()) == input_item->get_meta("__keycode") || Variant(k->get_physical_keycode()) == input_item->get_meta("__keycode")); - bool joyb_match = joyb.is_valid() && Variant(joyb->get_button_index()) == input_item->get_meta("__index"); - bool joym_match = joym.is_valid() && Variant(joym->get_axis()) == input_item->get_meta("__axis") && joym->get_axis_value() == (float)input_item->get_meta("__value"); - bool mb_match = mb.is_valid() && Variant(mb->get_button_index()) == input_item->get_meta("__index"); - if (key_match || joyb_match || joym_match || mb_match) { - category->set_collapsed(false); - input_item->select(0); - input_list_tree->ensure_cursor_is_visible(); - return; - } - input_item = input_item->get_next(); - } - } - } - - category->set_collapsed(true); // Event not in this category, so collapse; - category = category->get_next(); - } - } - } else { - // Event is not valid, reset dialog - event = p_event; - event_listener->clear_event(); - event_as_text->set_text(TTR("No Event Configured")); - - additional_options_container->hide(); - input_list_tree->deselect_all(); - _update_input_list(); - } -} - -void InputEventConfigurationDialog::_on_listen_input_changed(const Ref<InputEvent> &p_event) { - // Ignore if invalid, echo or not pressed - if (p_event.is_null() || p_event->is_echo() || !p_event->is_pressed()) { - return; - } - - // Create an editable reference - Ref<InputEvent> received_event = p_event; - // Check what the type is and if it is allowed. - Ref<InputEventKey> k = received_event; - Ref<InputEventJoypadButton> joyb = received_event; - Ref<InputEventJoypadMotion> joym = received_event; - Ref<InputEventMouseButton> mb = received_event; - - int type = 0; - if (k.is_valid()) { - type = INPUT_KEY; - } else if (joyb.is_valid()) { - type = INPUT_JOY_BUTTON; - } else if (joym.is_valid()) { - type = INPUT_JOY_MOTION; - } else if (mb.is_valid()) { - type = INPUT_MOUSE_BUTTON; - } - - if (!(allowed_input_types & type)) { - return; - } - - if (joym.is_valid()) { - float axis_value = joym->get_axis_value(); - if (ABS(axis_value) < 0.9) { - // Ignore motion below 0.9 magnitude to avoid accidental touches - return; - } else { - // Always make the value 1 or -1 for display consistency - joym->set_axis_value(SIGN(axis_value)); - } - } - - if (k.is_valid()) { - k->set_pressed(false); // To avoid serialisation of 'pressed' property - doesn't matter for actions anyway. - // Maintain physical keycode option state - if (physical_key_checkbox->is_pressed()) { - k->set_keycode(Key::NONE); - } else { - k->set_physical_keycode(Key::NONE); - } - } - - Ref<InputEventWithModifiers> mod = received_event; - if (mod.is_valid()) { - mod->set_window_id(0); - } - - // Maintain device selection. - received_event->set_device(_get_current_device()); - - _set_event(received_event); -} - -void InputEventConfigurationDialog::_on_listen_focus_changed() { - if (event_listener->has_focus()) { - set_close_on_escape(false); - } else { - set_close_on_escape(true); - } -} - -void InputEventConfigurationDialog::_search_term_updated(const String &) { - _update_input_list(); -} - -void InputEventConfigurationDialog::_update_input_list() { - input_list_tree->clear(); - - TreeItem *root = input_list_tree->create_item(); - String search_term = input_list_search->get_text(); - - bool collapse = input_list_search->get_text().is_empty(); - - if (allowed_input_types & INPUT_KEY) { - TreeItem *kb_root = input_list_tree->create_item(root); - kb_root->set_text(0, TTR("Keyboard Keys")); - kb_root->set_icon(0, icon_cache.keyboard); - kb_root->set_collapsed(collapse); - kb_root->set_meta("__type", INPUT_KEY); - - for (int i = 0; i < keycode_get_count(); i++) { - String name = keycode_get_name_by_index(i); - - if (!search_term.is_empty() && name.findn(search_term) == -1) { - continue; - } - - TreeItem *item = input_list_tree->create_item(kb_root); - item->set_text(0, name); - item->set_meta("__keycode", keycode_get_value_by_index(i)); - } - } - - if (allowed_input_types & INPUT_MOUSE_BUTTON) { - TreeItem *mouse_root = input_list_tree->create_item(root); - mouse_root->set_text(0, TTR("Mouse Buttons")); - mouse_root->set_icon(0, icon_cache.mouse); - mouse_root->set_collapsed(collapse); - mouse_root->set_meta("__type", INPUT_MOUSE_BUTTON); - - MouseButton mouse_buttons[9] = { MouseButton::LEFT, MouseButton::RIGHT, MouseButton::MIDDLE, MouseButton::WHEEL_UP, MouseButton::WHEEL_DOWN, MouseButton::WHEEL_LEFT, MouseButton::WHEEL_RIGHT, MouseButton::MB_XBUTTON1, MouseButton::MB_XBUTTON2 }; - for (int i = 0; i < 9; i++) { - Ref<InputEventMouseButton> mb; - mb.instantiate(); - mb->set_button_index(mouse_buttons[i]); - String desc = get_event_text(mb, false); - - if (!search_term.is_empty() && desc.findn(search_term) == -1) { - continue; - } - - TreeItem *item = input_list_tree->create_item(mouse_root); - item->set_text(0, desc); - item->set_meta("__index", mouse_buttons[i]); - } - } - - if (allowed_input_types & INPUT_JOY_BUTTON) { - TreeItem *joyb_root = input_list_tree->create_item(root); - joyb_root->set_text(0, TTR("Joypad Buttons")); - joyb_root->set_icon(0, icon_cache.joypad_button); - joyb_root->set_collapsed(collapse); - joyb_root->set_meta("__type", INPUT_JOY_BUTTON); - - for (int i = 0; i < (int)JoyButton::MAX; i++) { - Ref<InputEventJoypadButton> joyb; - joyb.instantiate(); - joyb->set_button_index((JoyButton)i); - String desc = get_event_text(joyb, false); - - if (!search_term.is_empty() && desc.findn(search_term) == -1) { - continue; - } - - TreeItem *item = input_list_tree->create_item(joyb_root); - item->set_text(0, desc); - item->set_meta("__index", i); - } - } - - if (allowed_input_types & INPUT_JOY_MOTION) { - TreeItem *joya_root = input_list_tree->create_item(root); - joya_root->set_text(0, TTR("Joypad Axes")); - joya_root->set_icon(0, icon_cache.joypad_axis); - joya_root->set_collapsed(collapse); - joya_root->set_meta("__type", INPUT_JOY_MOTION); - - for (int i = 0; i < (int)JoyAxis::MAX * 2; i++) { - int axis = i / 2; - int direction = (i & 1) ? 1 : -1; - Ref<InputEventJoypadMotion> joym; - joym.instantiate(); - joym->set_axis((JoyAxis)axis); - joym->set_axis_value(direction); - String desc = get_event_text(joym, false); - - if (!search_term.is_empty() && desc.findn(search_term) == -1) { - continue; - } - - TreeItem *item = input_list_tree->create_item(joya_root); - item->set_text(0, desc); - item->set_meta("__axis", i >> 1); - item->set_meta("__value", (i & 1) ? 1 : -1); - } - } -} - -void InputEventConfigurationDialog::_mod_toggled(bool p_checked, int p_index) { - Ref<InputEventWithModifiers> ie = event; - - // Not event with modifiers - if (ie.is_null()) { - return; - } - - if (p_index == 0) { - ie->set_alt_pressed(p_checked); - } else if (p_index == 1) { - ie->set_shift_pressed(p_checked); - } else if (p_index == 2) { - if (!autoremap_command_or_control_checkbox->is_pressed()) { - ie->set_ctrl_pressed(p_checked); - } - } else if (p_index == 3) { - if (!autoremap_command_or_control_checkbox->is_pressed()) { - ie->set_meta_pressed(p_checked); - } - } - - _set_event(ie); -} - -void InputEventConfigurationDialog::_autoremap_command_or_control_toggled(bool p_checked) { - Ref<InputEventWithModifiers> ie = event; - if (ie.is_valid()) { - ie->set_command_or_control_autoremap(p_checked); - _set_event(ie); - } - - if (p_checked) { - mod_checkboxes[MOD_META]->hide(); - mod_checkboxes[MOD_CTRL]->hide(); - } else { - mod_checkboxes[MOD_META]->show(); - mod_checkboxes[MOD_CTRL]->show(); - } -} - -void InputEventConfigurationDialog::_physical_keycode_toggled(bool p_checked) { - Ref<InputEventKey> k = event; - - if (k.is_null()) { - return; - } - - if (p_checked) { - k->set_physical_keycode(k->get_keycode()); - k->set_keycode(Key::NONE); - } else { - k->set_keycode((Key)k->get_physical_keycode()); - k->set_physical_keycode(Key::NONE); - } - - _set_event(k); -} - -void InputEventConfigurationDialog::_input_list_item_selected() { - TreeItem *selected = input_list_tree->get_selected(); - - // Invalid tree selection - type only exists on the "category" items, which are not a valid selection. - if (selected->has_meta("__type")) { - return; - } - - InputType input_type = (InputType)(int)selected->get_parent()->get_meta("__type"); - - switch (input_type) { - case INPUT_KEY: { - Key keycode = (Key)(int)selected->get_meta("__keycode"); - Ref<InputEventKey> k; - k.instantiate(); - - if (physical_key_checkbox->is_pressed()) { - k->set_physical_keycode(keycode); - k->set_keycode(Key::NONE); - } else { - k->set_physical_keycode(Key::NONE); - k->set_keycode(keycode); - } - - // Maintain modifier state from checkboxes - k->set_alt_pressed(mod_checkboxes[MOD_ALT]->is_pressed()); - k->set_shift_pressed(mod_checkboxes[MOD_SHIFT]->is_pressed()); - if (autoremap_command_or_control_checkbox->is_pressed()) { - k->set_command_or_control_autoremap(true); - } else { - k->set_ctrl_pressed(mod_checkboxes[MOD_CTRL]->is_pressed()); - k->set_meta_pressed(mod_checkboxes[MOD_META]->is_pressed()); - } - - _set_event(k, false); - } break; - case INPUT_MOUSE_BUTTON: { - MouseButton idx = (MouseButton)(int)selected->get_meta("__index"); - Ref<InputEventMouseButton> mb; - mb.instantiate(); - mb->set_button_index(idx); - // Maintain modifier state from checkboxes - mb->set_alt_pressed(mod_checkboxes[MOD_ALT]->is_pressed()); - mb->set_shift_pressed(mod_checkboxes[MOD_SHIFT]->is_pressed()); - if (autoremap_command_or_control_checkbox->is_pressed()) { - mb->set_command_or_control_autoremap(true); - } else { - mb->set_ctrl_pressed(mod_checkboxes[MOD_CTRL]->is_pressed()); - mb->set_meta_pressed(mod_checkboxes[MOD_META]->is_pressed()); - } - - // Maintain selected device - mb->set_device(_get_current_device()); - - _set_event(mb, false); - } break; - case INPUT_JOY_BUTTON: { - JoyButton idx = (JoyButton)(int)selected->get_meta("__index"); - Ref<InputEventJoypadButton> jb = InputEventJoypadButton::create_reference(idx); - - // Maintain selected device - jb->set_device(_get_current_device()); - - _set_event(jb, false); - } break; - case INPUT_JOY_MOTION: { - JoyAxis axis = (JoyAxis)(int)selected->get_meta("__axis"); - int value = selected->get_meta("__value"); - - Ref<InputEventJoypadMotion> jm; - jm.instantiate(); - jm->set_axis(axis); - jm->set_axis_value(value); - - // Maintain selected device - jm->set_device(_get_current_device()); - - _set_event(jm, false); - } break; - } -} - -void InputEventConfigurationDialog::_device_selection_changed(int p_option_button_index) { - // Subtract 1 as option index 0 corresponds to "All Devices" (value of -1) - // and option index 1 corresponds to device 0, etc... - event->set_device(p_option_button_index - 1); - event_as_text->set_text(get_event_text(event, true)); -} - -void InputEventConfigurationDialog::_set_current_device(int p_device) { - device_id_option->select(p_device + 1); -} - -int InputEventConfigurationDialog::_get_current_device() const { - return device_id_option->get_selected() - 1; -} - -String InputEventConfigurationDialog::_get_device_string(int p_device) const { - if (p_device == InputMap::ALL_DEVICES) { - return TTR("All Devices"); - } - return TTR("Device") + " " + itos(p_device); -} - -void InputEventConfigurationDialog::_notification(int p_what) { - switch (p_what) { - case NOTIFICATION_ENTER_TREE: - case NOTIFICATION_THEME_CHANGED: { - input_list_search->set_right_icon(input_list_search->get_theme_icon(SNAME("Search"), SNAME("EditorIcons"))); - - physical_key_checkbox->set_icon(get_theme_icon(SNAME("KeyboardPhysical"), SNAME("EditorIcons"))); - - icon_cache.keyboard = get_theme_icon(SNAME("Keyboard"), SNAME("EditorIcons")); - icon_cache.mouse = get_theme_icon(SNAME("Mouse"), SNAME("EditorIcons")); - icon_cache.joypad_button = get_theme_icon(SNAME("JoyButton"), SNAME("EditorIcons")); - icon_cache.joypad_axis = get_theme_icon(SNAME("JoyAxis"), SNAME("EditorIcons")); - - _update_input_list(); - } break; - } -} - -void InputEventConfigurationDialog::popup_and_configure(const Ref<InputEvent> &p_event) { - if (p_event.is_valid()) { - _set_event(p_event); - } else { - // Clear Event - _set_event(p_event); - - // Clear Checkbox Values - for (int i = 0; i < MOD_MAX; i++) { - mod_checkboxes[i]->set_pressed(false); - } - - // Enable the Physical Key checkbox by default to encourage its use. - // Physical Key should be used for most game inputs as it allows keys to work - // on non-QWERTY layouts out of the box. - // This is especially important for WASD movement layouts. - physical_key_checkbox->set_pressed(true); - - autoremap_command_or_control_checkbox->set_pressed(false); - - // Select "All Devices" by default. - device_id_option->select(0); - } - - popup_centered(Size2(0, 400) * EDSCALE); -} - -Ref<InputEvent> InputEventConfigurationDialog::get_event() const { - return event; -} - -void InputEventConfigurationDialog::set_allowed_input_types(int p_type_masks) { - allowed_input_types = p_type_masks; -} - -InputEventConfigurationDialog::InputEventConfigurationDialog() { - allowed_input_types = INPUT_KEY | INPUT_MOUSE_BUTTON | INPUT_JOY_BUTTON | INPUT_JOY_MOTION; - - set_title(TTR("Event Configuration")); - set_min_size(Size2i(550 * EDSCALE, 0)); // Min width - - VBoxContainer *main_vbox = memnew(VBoxContainer); - add_child(main_vbox); - - event_as_text = memnew(Label); - event_as_text->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART); - event_as_text->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); - event_as_text->add_theme_font_override("font", get_theme_font(SNAME("bold"), SNAME("EditorFonts"))); - event_as_text->add_theme_font_size_override("font_size", 18 * EDSCALE); - main_vbox->add_child(event_as_text); - - event_listener = memnew(EventListenerLineEdit); - event_listener->set_h_size_flags(Control::SIZE_EXPAND_FILL); - event_listener->set_stretch_ratio(0.75); - event_listener->connect("event_changed", callable_mp(this, &InputEventConfigurationDialog::_on_listen_input_changed)); - event_listener->connect("focus_entered", callable_mp(this, &InputEventConfigurationDialog::_on_listen_focus_changed)); - event_listener->connect("focus_exited", callable_mp(this, &InputEventConfigurationDialog::_on_listen_focus_changed)); - main_vbox->add_child(event_listener); - - main_vbox->add_child(memnew(HSeparator)); - - // List of all input options to manually select from. - VBoxContainer *manual_vbox = memnew(VBoxContainer); - manual_vbox->set_name(TTR("Manual Selection")); - manual_vbox->set_v_size_flags(Control::SIZE_EXPAND_FILL); - main_vbox->add_child(manual_vbox); - - input_list_search = memnew(LineEdit); - input_list_search->set_h_size_flags(Control::SIZE_EXPAND_FILL); - input_list_search->set_placeholder(TTR("Filter Inputs")); - input_list_search->set_clear_button_enabled(true); - input_list_search->connect("text_changed", callable_mp(this, &InputEventConfigurationDialog::_search_term_updated)); - manual_vbox->add_child(input_list_search); - - input_list_tree = memnew(Tree); - input_list_tree->set_custom_minimum_size(Size2(0, 100 * EDSCALE)); // Min height for tree - input_list_tree->connect("item_selected", callable_mp(this, &InputEventConfigurationDialog::_input_list_item_selected)); - input_list_tree->set_v_size_flags(Control::SIZE_EXPAND_FILL); - manual_vbox->add_child(input_list_tree); - - input_list_tree->set_hide_root(true); - input_list_tree->set_columns(1); - - _update_input_list(); - - // Additional Options - additional_options_container = memnew(VBoxContainer); - additional_options_container->hide(); - - Label *opts_label = memnew(Label); - opts_label->set_theme_type_variation("HeaderSmall"); - opts_label->set_text(TTR("Additional Options")); - additional_options_container->add_child(opts_label); - - // Device Selection - device_container = memnew(HBoxContainer); - device_container->set_h_size_flags(Control::SIZE_EXPAND_FILL); - - Label *device_label = memnew(Label); - device_label->set_theme_type_variation("HeaderSmall"); - device_label->set_text(TTR("Device:")); - device_container->add_child(device_label); - - device_id_option = memnew(OptionButton); - device_id_option->set_h_size_flags(Control::SIZE_EXPAND_FILL); - for (int i = -1; i < 8; i++) { - device_id_option->add_item(_get_device_string(i)); - } - device_id_option->connect("item_selected", callable_mp(this, &InputEventConfigurationDialog::_device_selection_changed)); - _set_current_device(InputMap::ALL_DEVICES); - device_container->add_child(device_id_option); - - device_container->hide(); - additional_options_container->add_child(device_container); - - // Modifier Selection - mod_container = memnew(HBoxContainer); - for (int i = 0; i < MOD_MAX; i++) { - String name = mods[i]; - mod_checkboxes[i] = memnew(CheckBox); - mod_checkboxes[i]->connect("toggled", callable_mp(this, &InputEventConfigurationDialog::_mod_toggled).bind(i)); - mod_checkboxes[i]->set_text(name); - mod_checkboxes[i]->set_tooltip_text(TTR(mods_tip[i])); - mod_container->add_child(mod_checkboxes[i]); - } - - mod_container->add_child(memnew(VSeparator)); - - autoremap_command_or_control_checkbox = memnew(CheckBox); - autoremap_command_or_control_checkbox->connect("toggled", callable_mp(this, &InputEventConfigurationDialog::_autoremap_command_or_control_toggled)); - autoremap_command_or_control_checkbox->set_pressed(false); - autoremap_command_or_control_checkbox->set_text(TTR("Command / Control (auto)")); - autoremap_command_or_control_checkbox->set_tooltip_text(TTR("Automatically remaps between 'Meta' ('Command') and 'Control' depending on current platform.")); - mod_container->add_child(autoremap_command_or_control_checkbox); - - mod_container->hide(); - additional_options_container->add_child(mod_container); - - // Physical Key Checkbox - - physical_key_checkbox = memnew(CheckBox); - physical_key_checkbox->set_text(TTR("Use Physical Keycode")); - physical_key_checkbox->set_tooltip_text(TTR("Stores the physical position of the key on the keyboard rather than the key's value. Used for compatibility with non-latin layouts.\nThis should generally be enabled for most game shortcuts, but not in non-game applications.")); - physical_key_checkbox->connect("toggled", callable_mp(this, &InputEventConfigurationDialog::_physical_keycode_toggled)); - physical_key_checkbox->hide(); - additional_options_container->add_child(physical_key_checkbox); - - main_vbox->add_child(additional_options_container); -} - -///////////////////////////////////////// +#include "editor/event_listener_line_edit.h" +#include "editor/input_event_configuration_dialog.h" +#include "scene/gui/check_button.h" +#include "scene/gui/tree.h" static bool _is_action_name_valid(const String &p_name) { const char32_t *cstr = p_name.get_data(); diff --git a/editor/action_map_editor.h b/editor/action_map_editor.h index f576a2b565..d56ee6f9eb 100644 --- a/editor/action_map_editor.h +++ b/editor/action_map_editor.h @@ -31,135 +31,16 @@ #ifndef ACTION_MAP_EDITOR_H #define ACTION_MAP_EDITOR_H -#include "scene/gui/check_box.h" -#include "scene/gui/check_button.h" -#include "scene/gui/color_rect.h" -#include "scene/gui/dialogs.h" -#include "scene/gui/label.h" -#include "scene/gui/option_button.h" -#include "scene/gui/tab_container.h" -#include "scene/gui/tree.h" - -enum InputType { - INPUT_KEY = 1, - INPUT_MOUSE_BUTTON = 2, - INPUT_JOY_BUTTON = 4, - INPUT_JOY_MOTION = 8 -}; - -class EventListenerLineEdit : public LineEdit { - GDCLASS(EventListenerLineEdit, LineEdit) - - int allowed_input_types = INPUT_KEY | INPUT_MOUSE_BUTTON | INPUT_JOY_BUTTON | INPUT_JOY_MOTION; - bool ignore = true; - bool share_keycodes = false; - Ref<InputEvent> event; - - bool _is_event_allowed(const Ref<InputEvent> &p_event) const; - - void gui_input(const Ref<InputEvent> &p_event) override; - void _on_text_changed(const String &p_text); - - void _on_focus(); - void _on_unfocus(); - -protected: - void _notification(int p_what); - static void _bind_methods(); - -public: - Ref<InputEvent> get_event() const; - void clear_event(); - - void set_allowed_input_types(int input_types); - int get_allowed_input_types() const; - -public: - EventListenerLineEdit(); -}; - -// Confirmation Dialog used when configuring an input event. -// Separate from ActionMapEditor for code cleanliness and separation of responsibilities. -class InputEventConfigurationDialog : public ConfirmationDialog { - GDCLASS(InputEventConfigurationDialog, ConfirmationDialog) -private: - struct IconCache { - Ref<Texture2D> keyboard; - Ref<Texture2D> mouse; - Ref<Texture2D> joypad_button; - Ref<Texture2D> joypad_axis; - } icon_cache; - - Ref<InputEvent> event = Ref<InputEvent>(); - - // Listening for input - EventListenerLineEdit *event_listener = nullptr; - Label *event_as_text = nullptr; - - // List of All Key/Mouse/Joypad input options. - int allowed_input_types; - Tree *input_list_tree = nullptr; - LineEdit *input_list_search = nullptr; - - // Additional Options, shown depending on event selected - VBoxContainer *additional_options_container = nullptr; - - HBoxContainer *device_container = nullptr; - OptionButton *device_id_option = nullptr; - - HBoxContainer *mod_container = nullptr; // Contains the subcontainer and the store command checkbox. - - enum ModCheckbox { - MOD_ALT, - MOD_SHIFT, - MOD_CTRL, - MOD_META, - MOD_MAX - }; -#if defined(MACOS_ENABLED) - String mods[MOD_MAX] = { "Option", "Shift", "Ctrl", "Command" }; -#elif defined(WINDOWS_ENABLED) - String mods[MOD_MAX] = { "Alt", "Shift", "Ctrl", "Windows" }; -#else - String mods[MOD_MAX] = { "Alt", "Shift", "Ctrl", "Meta" }; -#endif - String mods_tip[MOD_MAX] = { "Alt or Option key", "Shift key", "Control key", "Meta/Windows or Command key" }; - - CheckBox *mod_checkboxes[MOD_MAX]; - CheckBox *autoremap_command_or_control_checkbox = nullptr; - - CheckBox *physical_key_checkbox = nullptr; - - void _set_event(const Ref<InputEvent> &p_event, bool p_update_input_list_selection = true); - void _on_listen_input_changed(const Ref<InputEvent> &p_event); - void _on_listen_focus_changed(); - - void _search_term_updated(const String &p_term); - void _update_input_list(); - void _input_list_item_selected(); - - void _mod_toggled(bool p_checked, int p_index); - void _autoremap_command_or_control_toggled(bool p_checked); - void _physical_keycode_toggled(bool p_checked); - - void _device_selection_changed(int p_option_button_index); - void _set_current_device(int p_device); - int _get_current_device() const; - String _get_device_string(int p_device) const; - -protected: - void _notification(int p_what); - -public: - // Pass an existing event to configure it. Alternatively, pass no event to start with a blank configuration. - void popup_and_configure(const Ref<InputEvent> &p_event = Ref<InputEvent>()); - Ref<InputEvent> get_event() const; - String get_event_text(const Ref<InputEvent> &p_event, bool p_include_device) const; - - void set_allowed_input_types(int p_type_masks); - - InputEventConfigurationDialog(); -}; +#include "scene/gui/control.h" + +class Button; +class HBoxContainer; +class EventListenerLineEdit; +class LineEdit; +class CheckButton; +class AcceptDialog; +class InputEventConfigurationDialog; +class Tree; class ActionMapEditor : public Control { GDCLASS(ActionMapEditor, Control); diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index 25556482bc..fe9831d0ef 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -55,6 +55,7 @@ void GotoLineDialog::ok_pressed() { if (get_line() < 1 || get_line() > text_editor->get_line_count()) { return; } + text_editor->remove_secondary_carets(); text_editor->unfold_line(get_line() - 1); text_editor->set_caret_line(get_line() - 1); hide(); @@ -149,7 +150,7 @@ bool FindReplaceBar::_search(uint32_t p_flags, int p_from_line, int p_from_col) text_editor->unfold_line(pos.y); text_editor->set_caret_line(pos.y, false); text_editor->set_caret_column(pos.x + text.length(), false); - text_editor->center_viewport_to_caret(); + text_editor->center_viewport_to_caret(0); text_editor->select(pos.y, pos.x, pos.y, pos.x + text.length()); line_col_changed_for_result = true; @@ -176,11 +177,11 @@ bool FindReplaceBar::_search(uint32_t p_flags, int p_from_line, int p_from_col) } void FindReplaceBar::_replace() { - bool selection_enabled = text_editor->has_selection(); + bool selection_enabled = text_editor->has_selection(0); Point2i selection_begin, selection_end; if (selection_enabled) { - selection_begin = Point2i(text_editor->get_selection_from_line(), text_editor->get_selection_from_column()); - selection_end = Point2i(text_editor->get_selection_to_line(), text_editor->get_selection_to_column()); + selection_begin = Point2i(text_editor->get_selection_from_line(0), text_editor->get_selection_from_column(0)); + selection_end = Point2i(text_editor->get_selection_to_line(0), text_editor->get_selection_to_column(0)); } String replace_text = get_replace_text(); @@ -188,25 +189,25 @@ void FindReplaceBar::_replace() { text_editor->begin_complex_operation(); if (selection_enabled && is_selection_only()) { // To restrict search_current() to selected region - text_editor->set_caret_line(selection_begin.width); - text_editor->set_caret_column(selection_begin.height); + text_editor->set_caret_line(selection_begin.width, false, true, 0, 0); + text_editor->set_caret_column(selection_begin.height, true, 0); } if (search_current()) { text_editor->unfold_line(result_line); - text_editor->select(result_line, result_col, result_line, result_col + search_text_len); + text_editor->select(result_line, result_col, result_line, result_col + search_text_len, 0); if (selection_enabled && is_selection_only()) { Point2i match_from(result_line, result_col); Point2i match_to(result_line, result_col + search_text_len); if (!(match_from < selection_begin || match_to > selection_end)) { - text_editor->insert_text_at_caret(replace_text); + text_editor->insert_text_at_caret(replace_text, 0); if (match_to.x == selection_end.x) { // Adjust selection bounds if necessary selection_end.y += replace_text.length() - search_text_len; } } } else { - text_editor->insert_text_at_caret(replace_text); + text_editor->insert_text_at_caret(replace_text, 0); } } text_editor->end_complex_operation(); @@ -216,29 +217,29 @@ void FindReplaceBar::_replace() { if (selection_enabled && is_selection_only()) { // Reselect in order to keep 'Replace' restricted to selection - text_editor->select(selection_begin.x, selection_begin.y, selection_end.x, selection_end.y); + text_editor->select(selection_begin.x, selection_begin.y, selection_end.x, selection_end.y, 0); } else { - text_editor->deselect(); + text_editor->deselect(0); } } void FindReplaceBar::_replace_all() { text_editor->disconnect("text_changed", callable_mp(this, &FindReplaceBar::_editor_text_changed)); // Line as x so it gets priority in comparison, column as y. - Point2i orig_cursor(text_editor->get_caret_line(), text_editor->get_caret_column()); + Point2i orig_cursor(text_editor->get_caret_line(0), text_editor->get_caret_column(0)); Point2i prev_match = Point2(-1, -1); - bool selection_enabled = text_editor->has_selection(); + bool selection_enabled = text_editor->has_selection(0); Point2i selection_begin, selection_end; if (selection_enabled) { - selection_begin = Point2i(text_editor->get_selection_from_line(), text_editor->get_selection_from_column()); - selection_end = Point2i(text_editor->get_selection_to_line(), text_editor->get_selection_to_column()); + selection_begin = Point2i(text_editor->get_selection_from_line(0), text_editor->get_selection_from_column(0)); + selection_end = Point2i(text_editor->get_selection_to_line(0), text_editor->get_selection_to_column(0)); } int vsval = text_editor->get_v_scroll(); - text_editor->set_caret_line(0); - text_editor->set_caret_column(0); + text_editor->set_caret_line(0, false, true, 0, 0); + text_editor->set_caret_column(0, true, 0); String replace_text = get_replace_text(); int search_text_len = get_search_text().length(); @@ -250,8 +251,8 @@ void FindReplaceBar::_replace_all() { text_editor->begin_complex_operation(); if (selection_enabled && is_selection_only()) { - text_editor->set_caret_line(selection_begin.width); - text_editor->set_caret_column(selection_begin.height); + text_editor->set_caret_line(selection_begin.width, false, true, 0, 0); + text_editor->set_caret_column(selection_begin.height, true, 0); } if (search_current()) { do { @@ -266,7 +267,7 @@ void FindReplaceBar::_replace_all() { prev_match = Point2i(result_line, result_col + replace_text.length()); text_editor->unfold_line(result_line); - text_editor->select(result_line, result_col, result_line, match_to.y); + text_editor->select(result_line, result_col, result_line, match_to.y, 0); if (selection_enabled && is_selection_only()) { if (match_from < selection_begin || match_to > selection_end) { @@ -274,14 +275,14 @@ void FindReplaceBar::_replace_all() { } // Replace but adjust selection bounds. - text_editor->insert_text_at_caret(replace_text); + text_editor->insert_text_at_caret(replace_text, 0); if (match_to.x == selection_end.x) { selection_end.y += replace_text.length() - search_text_len; } } else { // Just replace. - text_editor->insert_text_at_caret(replace_text); + text_editor->insert_text_at_caret(replace_text, 0); } rc++; @@ -293,14 +294,14 @@ void FindReplaceBar::_replace_all() { replace_all_mode = false; // Restore editor state (selection, cursor, scroll). - text_editor->set_caret_line(orig_cursor.x); - text_editor->set_caret_column(orig_cursor.y); + text_editor->set_caret_line(orig_cursor.x, false, true, 0, 0); + text_editor->set_caret_column(orig_cursor.y, true, 0); if (selection_enabled && is_selection_only()) { // Reselect. - text_editor->select(selection_begin.x, selection_begin.y, selection_end.x, selection_end.y); + text_editor->select(selection_begin.x, selection_begin.y, selection_end.x, selection_end.y, 0); } else { - text_editor->deselect(); + text_editor->deselect(0); } text_editor->set_v_scroll(vsval); @@ -314,10 +315,10 @@ void FindReplaceBar::_replace_all() { } void FindReplaceBar::_get_search_from(int &r_line, int &r_col) { - r_line = text_editor->get_caret_line(); - r_col = text_editor->get_caret_column(); + r_line = text_editor->get_caret_line(0); + r_col = text_editor->get_caret_column(0); - if (text_editor->has_selection() && is_selection_only()) { + if (text_editor->has_selection(0) && is_selection_only()) { return; } @@ -434,7 +435,7 @@ bool FindReplaceBar::search_prev() { int line, col; _get_search_from(line, col); - if (text_editor->has_selection()) { + if (text_editor->has_selection(0)) { col--; // Skip currently selected word. } @@ -512,8 +513,8 @@ void FindReplaceBar::_show_search(bool p_focus_replace, bool p_show_only) { search_text->call_deferred(SNAME("grab_focus")); } - if (text_editor->has_selection() && !selection_only->is_pressed()) { - search_text->set_text(text_editor->get_selected_text()); + if (text_editor->has_selection(0) && !selection_only->is_pressed()) { + search_text->set_text(text_editor->get_selected_text(0)); } if (!get_search_text().is_empty()) { @@ -548,9 +549,9 @@ void FindReplaceBar::popup_replace() { hbc_option_replace->show(); } - selection_only->set_pressed((text_editor->has_selection() && text_editor->get_selection_from_line() < text_editor->get_selection_to_line())); + selection_only->set_pressed((text_editor->has_selection(0) && text_editor->get_selection_from_line(0) < text_editor->get_selection_to_line(0))); - _show_search(is_visible() || text_editor->has_selection()); + _show_search(is_visible() || text_editor->has_selection(0)); } void FindReplaceBar::_search_options_changed(bool p_pressed) { @@ -587,7 +588,7 @@ void FindReplaceBar::_search_text_submitted(const String &p_text) { } void FindReplaceBar::_replace_text_submitted(const String &p_text) { - if (selection_only->is_pressed() && text_editor->has_selection()) { + if (selection_only->is_pressed() && text_editor->has_selection(0)) { _replace_all(); _hide_bar(); } else if (Input::get_singleton()->is_key_pressed(Key::SHIFT)) { @@ -1091,6 +1092,7 @@ void CodeTextEditor::trim_trailing_whitespace() { } if (trimed_whitespace) { + text_editor->merge_overlapping_carets(); text_editor->end_complex_operation(); text_editor->queue_redraw(); } @@ -1122,8 +1124,11 @@ void CodeTextEditor::convert_indent_to_spaces() { indent += " "; } - int cursor_line = text_editor->get_caret_line(); - int cursor_column = text_editor->get_caret_column(); + Vector<int> cursor_columns; + cursor_columns.resize(text_editor->get_caret_count()); + for (int c = 0; c < text_editor->get_caret_count(); c++) { + cursor_columns.write[c] = text_editor->get_caret_column(c); + } bool changed_indentation = false; for (int i = 0; i < text_editor->get_line_count(); i++) { @@ -1140,8 +1145,10 @@ void CodeTextEditor::convert_indent_to_spaces() { text_editor->begin_complex_operation(); changed_indentation = true; } - if (cursor_line == i && cursor_column > j) { - cursor_column += indent_size - 1; + for (int c = 0; c < text_editor->get_caret_count(); c++) { + if (text_editor->get_caret_line(c) == i && text_editor->get_caret_column(c) > j) { + cursor_columns.write[c] += indent_size - 1; + } } line = line.left(j) + indent + line.substr(j + 1); } @@ -1152,7 +1159,10 @@ void CodeTextEditor::convert_indent_to_spaces() { } } if (changed_indentation) { - text_editor->set_caret_column(cursor_column); + for (int c = 0; c < text_editor->get_caret_count(); c++) { + text_editor->set_caret_column(cursor_columns[c], c == 0, c); + } + text_editor->merge_overlapping_carets(); text_editor->end_complex_operation(); text_editor->queue_redraw(); } @@ -1162,8 +1172,11 @@ void CodeTextEditor::convert_indent_to_tabs() { int indent_size = EditorSettings::get_singleton()->get("text_editor/behavior/indent/size"); indent_size -= 1; - int cursor_line = text_editor->get_caret_line(); - int cursor_column = text_editor->get_caret_column(); + Vector<int> cursor_columns; + cursor_columns.resize(text_editor->get_caret_count()); + for (int c = 0; c < text_editor->get_caret_count(); c++) { + cursor_columns.write[c] = text_editor->get_caret_column(c); + } bool changed_indentation = false; for (int i = 0; i < text_editor->get_line_count(); i++) { @@ -1184,8 +1197,10 @@ void CodeTextEditor::convert_indent_to_tabs() { text_editor->begin_complex_operation(); changed_indentation = true; } - if (cursor_line == i && cursor_column > j) { - cursor_column -= indent_size; + for (int c = 0; c < text_editor->get_caret_count(); c++) { + if (text_editor->get_caret_line(c) == i && text_editor->get_caret_column(c) > j) { + cursor_columns.write[c] -= indent_size; + } } line = line.left(j - indent_size) + "\t" + line.substr(j + 1); j = 0; @@ -1201,7 +1216,10 @@ void CodeTextEditor::convert_indent_to_tabs() { } } if (changed_indentation) { - text_editor->set_caret_column(cursor_column); + for (int c = 0; c < text_editor->get_caret_count(); c++) { + text_editor->set_caret_column(cursor_columns[c], c == 0, c); + } + text_editor->merge_overlapping_carets(); text_editor->end_complex_operation(); text_editor->queue_redraw(); } @@ -1211,59 +1229,128 @@ void CodeTextEditor::convert_case(CaseStyle p_case) { if (!text_editor->has_selection()) { return; } - text_editor->begin_complex_operation(); - int begin = text_editor->get_selection_from_line(); - int end = text_editor->get_selection_to_line(); - int begin_col = text_editor->get_selection_from_column(); - int end_col = text_editor->get_selection_to_column(); - - for (int i = begin; i <= end; i++) { - int len = text_editor->get_line(i).length(); - if (i == end) { - len = end_col; - } - if (i == begin) { - len -= begin_col; + Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order(); + for (const int &c : caret_edit_order) { + if (!text_editor->has_selection(c)) { + continue; } - String new_line = text_editor->get_line(i).substr(i == begin ? begin_col : 0, len); - switch (p_case) { - case UPPER: { - new_line = new_line.to_upper(); - } break; - case LOWER: { - new_line = new_line.to_lower(); - } break; - case CAPITALIZE: { - new_line = new_line.capitalize(); - } break; - } + int begin = text_editor->get_selection_from_line(c); + int end = text_editor->get_selection_to_line(c); + int begin_col = text_editor->get_selection_from_column(c); + int end_col = text_editor->get_selection_to_column(c); - if (i == begin) { - new_line = text_editor->get_line(i).left(begin_col) + new_line; - } - if (i == end) { - new_line = new_line + text_editor->get_line(i).substr(end_col); + for (int i = begin; i <= end; i++) { + int len = text_editor->get_line(i).length(); + if (i == end) { + len = end_col; + } + if (i == begin) { + len -= begin_col; + } + String new_line = text_editor->get_line(i).substr(i == begin ? begin_col : 0, len); + + switch (p_case) { + case UPPER: { + new_line = new_line.to_upper(); + } break; + case LOWER: { + new_line = new_line.to_lower(); + } break; + case CAPITALIZE: { + new_line = new_line.capitalize(); + } break; + } + + if (i == begin) { + new_line = text_editor->get_line(i).left(begin_col) + new_line; + } + if (i == end) { + new_line = new_line + text_editor->get_line(i).substr(end_col); + } + text_editor->set_line(i, new_line); } - text_editor->set_line(i, new_line); } text_editor->end_complex_operation(); } void CodeTextEditor::move_lines_up() { text_editor->begin_complex_operation(); - if (text_editor->has_selection()) { - int from_line = text_editor->get_selection_from_line(); - int from_col = text_editor->get_selection_from_column(); - int to_line = text_editor->get_selection_to_line(); - int to_column = text_editor->get_selection_to_column(); - int cursor_line = text_editor->get_caret_line(); - for (int i = from_line; i <= to_line; i++) { - int line_id = i; - int next_id = i - 1; + Vector<int> carets_to_remove; + + Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order(); + for (int i = 0; i < caret_edit_order.size(); i++) { + int c = caret_edit_order[i]; + int cl = text_editor->get_caret_line(c); + + bool swaped_caret = false; + for (int j = i + 1; j < caret_edit_order.size(); j++) { + if (text_editor->has_selection(caret_edit_order[j])) { + if (text_editor->get_selection_from_line() == cl) { + carets_to_remove.push_back(caret_edit_order[j]); + continue; + } + + if (text_editor->get_selection_to_line() == cl) { + if (text_editor->has_selection(c)) { + if (text_editor->get_selection_to_line(c) != cl) { + text_editor->select(cl + 1, 0, text_editor->get_selection_to_line(c), text_editor->get_selection_to_column(c), c); + break; + } + } + + carets_to_remove.push_back(c); + i = j - 1; + swaped_caret = true; + break; + } + break; + } + + if (text_editor->get_caret_line(caret_edit_order[j]) == cl) { + carets_to_remove.push_back(caret_edit_order[j]); + i = j; + continue; + } + break; + } + + if (swaped_caret) { + continue; + } + + if (text_editor->has_selection(c)) { + int from_line = text_editor->get_selection_from_line(c); + int from_col = text_editor->get_selection_from_column(c); + int to_line = text_editor->get_selection_to_line(c); + int to_column = text_editor->get_selection_to_column(c); + int cursor_line = text_editor->get_caret_line(c); + + for (int j = from_line; j <= to_line; j++) { + int line_id = j; + int next_id = j - 1; + + if (line_id == 0 || next_id < 0) { + return; + } + + text_editor->unfold_line(line_id); + text_editor->unfold_line(next_id); + + text_editor->swap_lines(line_id, next_id); + text_editor->set_caret_line(next_id, c == 0, true, 0, c); + } + int from_line_up = from_line > 0 ? from_line - 1 : from_line; + int to_line_up = to_line > 0 ? to_line - 1 : to_line; + int cursor_line_up = cursor_line > 0 ? cursor_line - 1 : cursor_line; + text_editor->select(from_line_up, from_col, to_line_up, to_column, c); + text_editor->set_caret_line(cursor_line_up, c == 0, true, 0, c); + } else { + int line_id = text_editor->get_caret_line(c); + int next_id = line_id - 1; if (line_id == 0 || next_id < 0) { return; @@ -1273,238 +1360,336 @@ void CodeTextEditor::move_lines_up() { text_editor->unfold_line(next_id); text_editor->swap_lines(line_id, next_id); - text_editor->set_caret_line(next_id); + text_editor->set_caret_line(next_id, c == 0, true, 0, c); } - int from_line_up = from_line > 0 ? from_line - 1 : from_line; - int to_line_up = to_line > 0 ? to_line - 1 : to_line; - int cursor_line_up = cursor_line > 0 ? cursor_line - 1 : cursor_line; - text_editor->select(from_line_up, from_col, to_line_up, to_column); - text_editor->set_caret_line(cursor_line_up); - } else { - int line_id = text_editor->get_caret_line(); - int next_id = line_id - 1; - - if (line_id == 0 || next_id < 0) { - return; - } - - text_editor->unfold_line(line_id); - text_editor->unfold_line(next_id); - - text_editor->swap_lines(line_id, next_id); - text_editor->set_caret_line(next_id); } text_editor->end_complex_operation(); + text_editor->merge_overlapping_carets(); text_editor->queue_redraw(); } void CodeTextEditor::move_lines_down() { text_editor->begin_complex_operation(); - if (text_editor->has_selection()) { - int from_line = text_editor->get_selection_from_line(); - int from_col = text_editor->get_selection_from_column(); - int to_line = text_editor->get_selection_to_line(); - int to_column = text_editor->get_selection_to_column(); - int cursor_line = text_editor->get_caret_line(); - for (int i = to_line; i >= from_line; i--) { - int line_id = i; - int next_id = i + 1; + Vector<int> carets_to_remove; + + Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order(); + for (int i = 0; i < caret_edit_order.size(); i++) { + int c = caret_edit_order[i]; + int cl = text_editor->get_caret_line(c); + + bool swaped_caret = false; + for (int j = i + 1; j < caret_edit_order.size(); j++) { + if (text_editor->has_selection(caret_edit_order[j])) { + if (text_editor->get_selection_from_line() == cl) { + carets_to_remove.push_back(caret_edit_order[j]); + continue; + } + + if (text_editor->get_selection_to_line() == cl) { + if (text_editor->has_selection(c)) { + if (text_editor->get_selection_to_line(c) != cl) { + text_editor->select(cl + 1, 0, text_editor->get_selection_to_line(c), text_editor->get_selection_to_column(c), c); + break; + } + } + + carets_to_remove.push_back(c); + i = j - 1; + swaped_caret = true; + break; + } + break; + } + + if (text_editor->get_caret_line(caret_edit_order[j]) == cl) { + carets_to_remove.push_back(caret_edit_order[j]); + i = j; + continue; + } + break; + } + + if (swaped_caret) { + continue; + } + + if (text_editor->has_selection(c)) { + int from_line = text_editor->get_selection_from_line(c); + int from_col = text_editor->get_selection_from_column(c); + int to_line = text_editor->get_selection_to_line(c); + int to_column = text_editor->get_selection_to_column(c); + int cursor_line = text_editor->get_caret_line(c); + + for (int l = to_line; l >= from_line; l--) { + int line_id = l; + int next_id = l + 1; + + if (line_id == text_editor->get_line_count() - 1 || next_id > text_editor->get_line_count()) { + continue; + } + + text_editor->unfold_line(line_id); + text_editor->unfold_line(next_id); + + text_editor->swap_lines(line_id, next_id); + text_editor->set_caret_line(next_id, c == 0, true, 0, c); + } + int from_line_down = from_line < text_editor->get_line_count() ? from_line + 1 : from_line; + int to_line_down = to_line < text_editor->get_line_count() ? to_line + 1 : to_line; + int cursor_line_down = cursor_line < text_editor->get_line_count() ? cursor_line + 1 : cursor_line; + text_editor->select(from_line_down, from_col, to_line_down, to_column, c); + text_editor->set_caret_line(cursor_line_down, c == 0, true, 0, c); + } else { + int line_id = text_editor->get_caret_line(c); + int next_id = line_id + 1; if (line_id == text_editor->get_line_count() - 1 || next_id > text_editor->get_line_count()) { - return; + continue; } text_editor->unfold_line(line_id); text_editor->unfold_line(next_id); text_editor->swap_lines(line_id, next_id); - text_editor->set_caret_line(next_id); - } - int from_line_down = from_line < text_editor->get_line_count() ? from_line + 1 : from_line; - int to_line_down = to_line < text_editor->get_line_count() ? to_line + 1 : to_line; - int cursor_line_down = cursor_line < text_editor->get_line_count() ? cursor_line + 1 : cursor_line; - text_editor->select(from_line_down, from_col, to_line_down, to_column); - text_editor->set_caret_line(cursor_line_down); - } else { - int line_id = text_editor->get_caret_line(); - int next_id = line_id + 1; - - if (line_id == text_editor->get_line_count() - 1 || next_id > text_editor->get_line_count()) { - return; + text_editor->set_caret_line(next_id, c == 0, true, 0, c); } + } - text_editor->unfold_line(line_id); - text_editor->unfold_line(next_id); - - text_editor->swap_lines(line_id, next_id); - text_editor->set_caret_line(next_id); + // Sort and remove backwards to preserve indexes. + carets_to_remove.sort(); + for (int i = carets_to_remove.size() - 1; i >= 0; i--) { + text_editor->remove_caret(carets_to_remove[i]); } + + text_editor->merge_overlapping_carets(); text_editor->end_complex_operation(); text_editor->queue_redraw(); } -void CodeTextEditor::_delete_line(int p_line) { +void CodeTextEditor::_delete_line(int p_line, int p_caret) { // this is currently intended to be called within delete_lines() // so `begin_complex_operation` is omitted here text_editor->set_line(p_line, ""); if (p_line == 0 && text_editor->get_line_count() > 1) { - text_editor->set_caret_line(1); - text_editor->set_caret_column(0); + text_editor->set_caret_line(1, p_caret == 0, true, 0, p_caret); + text_editor->set_caret_column(0, p_caret == 0, p_caret); } - text_editor->backspace(); + text_editor->backspace(p_caret); if (p_line < text_editor->get_line_count()) { text_editor->unfold_line(p_line); } - text_editor->set_caret_line(p_line); + text_editor->set_caret_line(p_line, p_caret == 0, true, 0, p_caret); } void CodeTextEditor::delete_lines() { text_editor->begin_complex_operation(); - if (text_editor->has_selection()) { - int to_line = text_editor->get_selection_to_line(); - int from_line = text_editor->get_selection_from_line(); - int count = Math::abs(to_line - from_line) + 1; - text_editor->set_caret_line(from_line, false); - text_editor->deselect(); - for (int i = 0; i < count; i++) { - _delete_line(from_line); + Vector<int> carets_to_remove; + + Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order(); + for (int i = 0; i < caret_edit_order.size(); i++) { + int c = caret_edit_order[i]; + int cl = text_editor->get_caret_line(c); + + bool swaped_caret = false; + for (int j = i + 1; j < caret_edit_order.size(); j++) { + if (text_editor->has_selection(caret_edit_order[j])) { + if (text_editor->get_selection_from_line() == cl) { + carets_to_remove.push_back(caret_edit_order[j]); + continue; + } + + if (text_editor->get_selection_to_line() == cl) { + if (text_editor->has_selection(c)) { + if (text_editor->get_selection_to_line(c) != cl) { + text_editor->select(cl + 1, 0, text_editor->get_selection_to_line(c), text_editor->get_selection_to_column(c), c); + break; + } + } + + carets_to_remove.push_back(c); + i = j - 1; + swaped_caret = true; + break; + } + break; + } + + if (text_editor->get_caret_line(caret_edit_order[j]) == cl) { + carets_to_remove.push_back(caret_edit_order[j]); + i = j; + continue; + } + break; + } + + if (swaped_caret) { + continue; + } + + if (text_editor->has_selection(c)) { + int to_line = text_editor->get_selection_to_line(c); + int from_line = text_editor->get_selection_from_line(c); + int count = Math::abs(to_line - from_line) + 1; + + text_editor->set_caret_line(from_line, false, true, 0, c); + text_editor->deselect(c); + for (int j = 0; j < count; j++) { + _delete_line(from_line, c); + } + } else { + _delete_line(text_editor->get_caret_line(c), c); } - } else { - _delete_line(text_editor->get_caret_line()); } + + // Sort and remove backwards to preserve indexes. + carets_to_remove.sort(); + for (int i = carets_to_remove.size() - 1; i >= 0; i--) { + text_editor->remove_caret(carets_to_remove[i]); + } + text_editor->merge_overlapping_carets(); text_editor->end_complex_operation(); } void CodeTextEditor::duplicate_selection() { - const int cursor_column = text_editor->get_caret_column(); - int from_line = text_editor->get_caret_line(); - int to_line = text_editor->get_caret_line(); - int from_column = 0; - int to_column = 0; - int cursor_new_line = to_line + 1; - int cursor_new_column = text_editor->get_caret_column(); - String new_text = "\n" + text_editor->get_line(from_line); - bool selection_active = false; - - text_editor->set_caret_column(text_editor->get_line(from_line).length()); - if (text_editor->has_selection()) { - from_column = text_editor->get_selection_from_column(); - to_column = text_editor->get_selection_to_column(); - - from_line = text_editor->get_selection_from_line(); - to_line = text_editor->get_selection_to_line(); - cursor_new_line = to_line + text_editor->get_caret_line() - from_line; - cursor_new_column = to_column == cursor_column ? 2 * to_column - from_column : to_column; - new_text = text_editor->get_selected_text(); - selection_active = true; - - text_editor->set_caret_line(to_line); - text_editor->set_caret_column(to_column); - } - text_editor->begin_complex_operation(); - for (int i = from_line; i <= to_line; i++) { - text_editor->unfold_line(i); - } - text_editor->deselect(); - text_editor->insert_text_at_caret(new_text); - text_editor->set_caret_line(cursor_new_line); - text_editor->set_caret_column(cursor_new_column); - if (selection_active) { - text_editor->select(to_line, to_column, 2 * to_line - from_line, to_line == from_line ? 2 * to_column - from_column : to_column); - } + Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order(); + for (const int &c : caret_edit_order) { + const int cursor_column = text_editor->get_caret_column(c); + int from_line = text_editor->get_caret_line(c); + int to_line = text_editor->get_caret_line(c); + int from_column = 0; + int to_column = 0; + int cursor_new_line = to_line + 1; + int cursor_new_column = text_editor->get_caret_column(c); + String new_text = "\n" + text_editor->get_line(from_line); + bool selection_active = false; + + text_editor->set_caret_column(text_editor->get_line(from_line).length(), c == 0, c); + if (text_editor->has_selection(c)) { + from_column = text_editor->get_selection_from_column(c); + to_column = text_editor->get_selection_to_column(c); + + from_line = text_editor->get_selection_from_line(c); + to_line = text_editor->get_selection_to_line(c); + cursor_new_line = to_line + text_editor->get_caret_line(c) - from_line; + cursor_new_column = to_column == cursor_column ? 2 * to_column - from_column : to_column; + new_text = text_editor->get_selected_text(c); + selection_active = true; + + text_editor->set_caret_line(to_line, c == 0, true, 0, c); + text_editor->set_caret_column(to_column, c == 0, c); + } + for (int i = from_line; i <= to_line; i++) { + text_editor->unfold_line(i); + } + text_editor->deselect(c); + text_editor->insert_text_at_caret(new_text, c); + text_editor->set_caret_line(cursor_new_line, c == 0, true, 0, c); + text_editor->set_caret_column(cursor_new_column, c == 0, c); + if (selection_active) { + text_editor->select(to_line, to_column, 2 * to_line - from_line, to_line == from_line ? 2 * to_column - from_column : to_column, c); + } + } + text_editor->merge_overlapping_carets(); text_editor->end_complex_operation(); text_editor->queue_redraw(); } void CodeTextEditor::toggle_inline_comment(const String &delimiter) { text_editor->begin_complex_operation(); - if (text_editor->has_selection()) { - int begin = text_editor->get_selection_from_line(); - int end = text_editor->get_selection_to_line(); - // End of selection ends on the first column of the last line, ignore it. - if (text_editor->get_selection_to_column() == 0) { - end -= 1; - } + Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order(); + for (const int &c : caret_edit_order) { + if (text_editor->has_selection(c)) { + int begin = text_editor->get_selection_from_line(c); + int end = text_editor->get_selection_to_line(c); - int col_to = text_editor->get_selection_to_column(); - int cursor_pos = text_editor->get_caret_column(); + // End of selection ends on the first column of the last line, ignore it. + if (text_editor->get_selection_to_column(c) == 0) { + end -= 1; + } - // Check if all lines in the selected block are commented. - bool is_commented = true; - for (int i = begin; i <= end; i++) { - if (!text_editor->get_line(i).begins_with(delimiter)) { - is_commented = false; - break; + int col_to = text_editor->get_selection_to_column(c); + int cursor_pos = text_editor->get_caret_column(c); + + // Check if all lines in the selected block are commented. + bool is_commented = true; + for (int i = begin; i <= end; i++) { + if (!text_editor->get_line(i).begins_with(delimiter)) { + is_commented = false; + break; + } } - } - for (int i = begin; i <= end; i++) { - String line_text = text_editor->get_line(i); + for (int i = begin; i <= end; i++) { + String line_text = text_editor->get_line(i); - if (line_text.strip_edges().is_empty()) { - line_text = delimiter; - } else { - if (is_commented) { - line_text = line_text.substr(delimiter.length(), line_text.length()); + if (line_text.strip_edges().is_empty()) { + line_text = delimiter; } else { - line_text = delimiter + line_text; + if (is_commented) { + line_text = line_text.substr(delimiter.length(), line_text.length()); + } else { + line_text = delimiter + line_text; + } } + text_editor->set_line(i, line_text); } - text_editor->set_line(i, line_text); - } - // Adjust selection & cursor position. - int offset = (is_commented ? -1 : 1) * delimiter.length(); - int col_from = text_editor->get_selection_from_column() > 0 ? text_editor->get_selection_from_column() + offset : 0; + // Adjust selection & cursor position. + int offset = (is_commented ? -1 : 1) * delimiter.length(); + int col_from = text_editor->get_selection_from_column(c) > 0 ? text_editor->get_selection_from_column(c) + offset : 0; - if (is_commented && text_editor->get_caret_column() == text_editor->get_line(text_editor->get_caret_line()).length() + 1) { - cursor_pos += 1; - } + if (is_commented && text_editor->get_caret_column(c) == text_editor->get_line(text_editor->get_caret_line(c)).length() + 1) { + cursor_pos += 1; + } - if (text_editor->get_selection_to_column() != 0 && col_to != text_editor->get_line(text_editor->get_selection_to_line()).length() + 1) { - col_to += offset; - } + if (text_editor->get_selection_to_column(c) != 0 && col_to != text_editor->get_line(text_editor->get_selection_to_line(c)).length() + 1) { + col_to += offset; + } - if (text_editor->get_caret_column() != 0) { - cursor_pos += offset; - } + if (text_editor->get_caret_column(c) != 0) { + cursor_pos += offset; + } - text_editor->select(begin, col_from, text_editor->get_selection_to_line(), col_to); - text_editor->set_caret_column(cursor_pos); + text_editor->select(begin, col_from, text_editor->get_selection_to_line(c), col_to, c); + text_editor->set_caret_column(cursor_pos, c == 0, c); - } else { - int begin = text_editor->get_caret_line(); - String line_text = text_editor->get_line(begin); - int delimiter_length = delimiter.length(); - - int col = text_editor->get_caret_column(); - if (line_text.begins_with(delimiter)) { - line_text = line_text.substr(delimiter_length, line_text.length()); - col -= delimiter_length; } else { - line_text = delimiter + line_text; - col += delimiter_length; - } + int begin = text_editor->get_caret_line(c); + String line_text = text_editor->get_line(begin); + int delimiter_length = delimiter.length(); + + int col = text_editor->get_caret_column(c); + if (line_text.begins_with(delimiter)) { + line_text = line_text.substr(delimiter_length, line_text.length()); + col -= delimiter_length; + } else { + line_text = delimiter + line_text; + col += delimiter_length; + } - text_editor->set_line(begin, line_text); - text_editor->set_caret_column(col); + text_editor->set_line(begin, line_text); + text_editor->set_caret_column(col, c == 0, c); + } } + text_editor->merge_overlapping_carets(); text_editor->end_complex_operation(); text_editor->queue_redraw(); } void CodeTextEditor::goto_line(int p_line) { + text_editor->remove_secondary_carets(); text_editor->deselect(); text_editor->unfold_line(p_line); text_editor->call_deferred(SNAME("set_caret_line"), p_line); } void CodeTextEditor::goto_line_selection(int p_line, int p_begin, int p_end) { + text_editor->remove_secondary_carets(); text_editor->unfold_line(p_line); text_editor->call_deferred(SNAME("set_caret_line"), p_line); text_editor->call_deferred(SNAME("set_caret_column"), p_begin); @@ -1617,6 +1802,7 @@ void CodeTextEditor::goto_error() { if (text_editor->get_line_count() != error_line) { text_editor->unfold_line(error_line); } + text_editor->remove_secondary_carets(); text_editor->set_caret_line(error_line); text_editor->set_caret_column(error_column); text_editor->center_viewport_to_caret(); @@ -1793,8 +1979,10 @@ void CodeTextEditor::set_warning_count(int p_warning_count) { } void CodeTextEditor::toggle_bookmark() { - int line = text_editor->get_caret_line(); - text_editor->set_line_as_bookmarked(line, !text_editor->is_line_bookmarked(line)); + for (int i = 0; i < text_editor->get_caret_count(); i++) { + int line = text_editor->get_caret_line(i); + text_editor->set_line_as_bookmarked(line, !text_editor->is_line_bookmarked(line)); + } } void CodeTextEditor::goto_next_bookmark() { @@ -1803,6 +1991,7 @@ void CodeTextEditor::goto_next_bookmark() { return; } + text_editor->remove_secondary_carets(); int line = text_editor->get_caret_line(); if (line >= (int)bmarks[bmarks.size() - 1]) { text_editor->unfold_line(bmarks[0]); @@ -1827,6 +2016,7 @@ void CodeTextEditor::goto_prev_bookmark() { return; } + text_editor->remove_secondary_carets(); int line = text_editor->get_caret_line(); if (line <= (int)bmarks[0]) { text_editor->unfold_line(bmarks[bmarks.size() - 1]); diff --git a/editor/code_editor.h b/editor/code_editor.h index 775708954d..c3279e8764 100644 --- a/editor/code_editor.h +++ b/editor/code_editor.h @@ -197,7 +197,7 @@ class CodeTextEditor : public VBoxContainer { void _update_status_bar_theme(); - void _delete_line(int p_line); + void _delete_line(int p_line, int p_caret); void _toggle_scripts_pressed(); protected: diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 913f70c442..34c0c35b12 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -3772,7 +3772,7 @@ int EditorNode::new_scene() { if (editor_data.get_edited_scene_count() > 1) { for (int i = 0; i < editor_data.get_edited_scene_count() - 1; i++) { bool unsaved = get_undo_redo()->is_history_unsaved(editor_data.get_scene_history_id(i)); - if (!unsaved && editor_data.get_scene_path(i).is_empty()) { + if (!unsaved && editor_data.get_scene_path(i).is_empty() && editor_data.get_edited_scene_root(i) == nullptr) { editor_data.remove_scene(i); idx--; } diff --git a/editor/editor_settings_dialog.cpp b/editor/editor_settings_dialog.cpp index b5147bb4eb..db501aac03 100644 --- a/editor/editor_settings_dialog.cpp +++ b/editor/editor_settings_dialog.cpp @@ -41,6 +41,8 @@ #include "editor/editor_scale.h" #include "editor/editor_settings.h" #include "editor/editor_undo_redo_manager.h" +#include "editor/event_listener_line_edit.h" +#include "editor/input_event_configuration_dialog.h" #include "scene/gui/margin_container.h" void EditorSettingsDialog::ok_pressed() { diff --git a/editor/event_listener_line_edit.cpp b/editor/event_listener_line_edit.cpp new file mode 100644 index 0000000000..14e482432a --- /dev/null +++ b/editor/event_listener_line_edit.cpp @@ -0,0 +1,131 @@ +/*************************************************************************/ +/* event_listener_line_edit.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "editor/event_listener_line_edit.h" + +bool EventListenerLineEdit::_is_event_allowed(const Ref<InputEvent> &p_event) const { + const Ref<InputEventMouseButton> mb = p_event; + const Ref<InputEventKey> k = p_event; + const Ref<InputEventJoypadButton> jb = p_event; + const Ref<InputEventJoypadMotion> jm = p_event; + + return (mb.is_valid() && (allowed_input_types & INPUT_MOUSE_BUTTON)) || + (k.is_valid() && (allowed_input_types & INPUT_KEY)) || + (jb.is_valid() && (allowed_input_types & INPUT_JOY_BUTTON)) || + (jm.is_valid() && (allowed_input_types & INPUT_JOY_MOTION)); +} + +void EventListenerLineEdit::gui_input(const Ref<InputEvent> &p_event) { + const Ref<InputEventMouseMotion> mm = p_event; + if (mm.is_valid()) { + LineEdit::gui_input(p_event); + return; + } + + // Allow mouse button click on the clear button without being treated as an event. + const Ref<InputEventMouseButton> b = p_event; + if (b.is_valid() && _is_over_clear_button(b->get_position())) { + LineEdit::gui_input(p_event); + return; + } + + // First event will be an event which is used to focus this control - i.e. a mouse click, or a tab press. + // Ignore the first one so that clicking into the LineEdit does not override the current event. + // Ignore is reset to true when the control is unfocused. + if (ignore) { + ignore = false; + return; + } + + accept_event(); + if (!p_event->is_pressed() || p_event->is_echo() || p_event->is_match(event) || !_is_event_allowed(p_event)) { + return; + } + + event = p_event; + set_text(event->as_text()); + emit_signal("event_changed", event); +} + +void EventListenerLineEdit::_on_text_changed(const String &p_text) { + if (p_text.is_empty()) { + clear_event(); + } +} + +void EventListenerLineEdit::_on_focus() { + set_placeholder(TTR("Listening for input...")); +} + +void EventListenerLineEdit::_on_unfocus() { + ignore = true; + set_placeholder(TTR("Filter by event...")); +} + +Ref<InputEvent> EventListenerLineEdit::get_event() const { + return event; +} + +void EventListenerLineEdit::clear_event() { + if (event.is_valid()) { + event = Ref<InputEvent>(); + set_text(""); + emit_signal("event_changed", event); + } +} + +void EventListenerLineEdit::set_allowed_input_types(int input_types) { + allowed_input_types = input_types; +} + +int EventListenerLineEdit::get_allowed_input_types() const { + return allowed_input_types; +} + +void EventListenerLineEdit::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: { + connect("text_changed", callable_mp(this, &EventListenerLineEdit::_on_text_changed)); + connect("focus_entered", callable_mp(this, &EventListenerLineEdit::_on_focus)); + connect("focus_exited", callable_mp(this, &EventListenerLineEdit::_on_unfocus)); + set_right_icon(get_theme_icon(SNAME("Keyboard"), SNAME("EditorIcons"))); + set_clear_button_enabled(true); + } break; + } +} + +void EventListenerLineEdit::_bind_methods() { + ADD_SIGNAL(MethodInfo("event_changed", PropertyInfo(Variant::OBJECT, "event", PROPERTY_HINT_RESOURCE_TYPE, "InputEvent"))); +} + +EventListenerLineEdit::EventListenerLineEdit() { + set_caret_blink_enabled(false); + set_placeholder(TTR("Filter by event...")); +} diff --git a/editor/event_listener_line_edit.h b/editor/event_listener_line_edit.h new file mode 100644 index 0000000000..5b415e8a2d --- /dev/null +++ b/editor/event_listener_line_edit.h @@ -0,0 +1,74 @@ +/*************************************************************************/ +/* event_listener_line_edit.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef EVENT_LISTENER_LINE_EDIT_H +#define EVENT_LISTENER_LINE_EDIT_H + +#include "scene/gui/line_edit.h" + +enum InputType { + INPUT_KEY = 1, + INPUT_MOUSE_BUTTON = 2, + INPUT_JOY_BUTTON = 4, + INPUT_JOY_MOTION = 8 +}; + +class EventListenerLineEdit : public LineEdit { + GDCLASS(EventListenerLineEdit, LineEdit) + + int allowed_input_types = INPUT_KEY | INPUT_MOUSE_BUTTON | INPUT_JOY_BUTTON | INPUT_JOY_MOTION; + bool ignore = true; + bool share_keycodes = false; + Ref<InputEvent> event; + + bool _is_event_allowed(const Ref<InputEvent> &p_event) const; + + void gui_input(const Ref<InputEvent> &p_event) override; + void _on_text_changed(const String &p_text); + + void _on_focus(); + void _on_unfocus(); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + Ref<InputEvent> get_event() const; + void clear_event(); + + void set_allowed_input_types(int input_types); + int get_allowed_input_types() const; + +public: + EventListenerLineEdit(); +}; + +#endif // EVENT_LISTENER_LINE_EDIT_H diff --git a/editor/input_event_configuration_dialog.cpp b/editor/input_event_configuration_dialog.cpp new file mode 100644 index 0000000000..ed9a5898fb --- /dev/null +++ b/editor/input_event_configuration_dialog.cpp @@ -0,0 +1,700 @@ +/*************************************************************************/ +/* input_event_configuration_dialog.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "editor/input_event_configuration_dialog.h" +#include "core/input/input_map.h" +#include "editor/editor_scale.h" +#include "editor/event_listener_line_edit.h" +#include "scene/gui/check_box.h" +#include "scene/gui/line_edit.h" +#include "scene/gui/option_button.h" +#include "scene/gui/separator.h" +#include "scene/gui/tree.h" + +// Maps to 2*axis if value is neg, or 2*axis+1 if value is pos. +static const char *_joy_axis_descriptions[(size_t)JoyAxis::MAX * 2] = { + TTRC("Left Stick Left, Joystick 0 Left"), + TTRC("Left Stick Right, Joystick 0 Right"), + TTRC("Left Stick Up, Joystick 0 Up"), + TTRC("Left Stick Down, Joystick 0 Down"), + TTRC("Right Stick Left, Joystick 1 Left"), + TTRC("Right Stick Right, Joystick 1 Right"), + TTRC("Right Stick Up, Joystick 1 Up"), + TTRC("Right Stick Down, Joystick 1 Down"), + TTRC("Joystick 2 Left"), + TTRC("Left Trigger, Sony L2, Xbox LT, Joystick 2 Right"), + TTRC("Joystick 2 Up"), + TTRC("Right Trigger, Sony R2, Xbox RT, Joystick 2 Down"), + TTRC("Joystick 3 Left"), + TTRC("Joystick 3 Right"), + TTRC("Joystick 3 Up"), + TTRC("Joystick 3 Down"), + TTRC("Joystick 4 Left"), + TTRC("Joystick 4 Right"), + TTRC("Joystick 4 Up"), + TTRC("Joystick 4 Down"), +}; + +String InputEventConfigurationDialog::get_event_text(const Ref<InputEvent> &p_event, bool p_include_device) const { + ERR_FAIL_COND_V_MSG(p_event.is_null(), String(), "Provided event is not a valid instance of InputEvent"); + + String text = p_event->as_text(); + + Ref<InputEventKey> key = p_event; + if (key.is_valid() && key->is_command_or_control_autoremap()) { +#ifdef MACOS_ENABLED + text = text.replace("Command", "Command/Ctrl"); +#else + text = text.replace("Ctrl", "Command/Ctrl"); +#endif + } + Ref<InputEventMouse> mouse = p_event; + Ref<InputEventJoypadMotion> jp_motion = p_event; + Ref<InputEventJoypadButton> jp_button = p_event; + if (jp_motion.is_valid()) { + // Joypad motion events will display slightly differently than what the event->as_text() provides. See #43660. + String desc = TTR("Unknown Joypad Axis"); + if (jp_motion->get_axis() < JoyAxis::MAX) { + desc = RTR(_joy_axis_descriptions[2 * (size_t)jp_motion->get_axis() + (jp_motion->get_axis_value() < 0 ? 0 : 1)]); + } + + text = vformat("Joypad Axis %s %s (%s)", itos((int64_t)jp_motion->get_axis()), jp_motion->get_axis_value() < 0 ? "-" : "+", desc); + } + if (p_include_device && (mouse.is_valid() || jp_button.is_valid() || jp_motion.is_valid())) { + String device_string = _get_device_string(p_event->get_device()); + text += vformat(" - %s", device_string); + } + + return text; +} + +void InputEventConfigurationDialog::_set_event(const Ref<InputEvent> &p_event, bool p_update_input_list_selection) { + if (p_event.is_valid()) { + event = p_event; + + // If the event is changed to something which is not the same as the listener, + // clear out the event from the listener text box to avoid confusion. + const Ref<InputEvent> listener_event = event_listener->get_event(); + if (listener_event.is_valid() && !listener_event->is_match(p_event)) { + event_listener->clear_event(); + } + + // Update Label + event_as_text->set_text(get_event_text(event, true)); + + Ref<InputEventKey> k = p_event; + Ref<InputEventMouseButton> mb = p_event; + Ref<InputEventJoypadButton> joyb = p_event; + Ref<InputEventJoypadMotion> joym = p_event; + Ref<InputEventWithModifiers> mod = p_event; + + // Update option values and visibility + bool show_mods = false; + bool show_device = false; + bool show_phys_key = false; + + if (mod.is_valid()) { + show_mods = true; + mod_checkboxes[MOD_ALT]->set_pressed(mod->is_alt_pressed()); + mod_checkboxes[MOD_SHIFT]->set_pressed(mod->is_shift_pressed()); + mod_checkboxes[MOD_CTRL]->set_pressed(mod->is_ctrl_pressed()); + mod_checkboxes[MOD_META]->set_pressed(mod->is_meta_pressed()); + + autoremap_command_or_control_checkbox->set_pressed(mod->is_command_or_control_autoremap()); + } + + if (k.is_valid()) { + show_phys_key = true; + physical_key_checkbox->set_pressed(k->get_physical_keycode() != Key::NONE && k->get_keycode() == Key::NONE); + + } else if (joyb.is_valid() || joym.is_valid() || mb.is_valid()) { + show_device = true; + _set_current_device(event->get_device()); + } + + mod_container->set_visible(show_mods); + device_container->set_visible(show_device); + physical_key_checkbox->set_visible(show_phys_key); + additional_options_container->show(); + + // Update selected item in input list. + if (p_update_input_list_selection && (k.is_valid() || joyb.is_valid() || joym.is_valid() || mb.is_valid())) { + TreeItem *category = input_list_tree->get_root()->get_first_child(); + while (category) { + TreeItem *input_item = category->get_first_child(); + + if (input_item != nullptr) { + // input_type should always be > 0, unless the tree structure has been misconfigured. + int input_type = input_item->get_parent()->get_meta("__type", 0); + if (input_type == 0) { + return; + } + + // If event type matches input types of this category. + if ((k.is_valid() && input_type == INPUT_KEY) || (joyb.is_valid() && input_type == INPUT_JOY_BUTTON) || (joym.is_valid() && input_type == INPUT_JOY_MOTION) || (mb.is_valid() && input_type == INPUT_MOUSE_BUTTON)) { + // Loop through all items of this category until one matches. + while (input_item) { + bool key_match = k.is_valid() && (Variant(k->get_keycode()) == input_item->get_meta("__keycode") || Variant(k->get_physical_keycode()) == input_item->get_meta("__keycode")); + bool joyb_match = joyb.is_valid() && Variant(joyb->get_button_index()) == input_item->get_meta("__index"); + bool joym_match = joym.is_valid() && Variant(joym->get_axis()) == input_item->get_meta("__axis") && joym->get_axis_value() == (float)input_item->get_meta("__value"); + bool mb_match = mb.is_valid() && Variant(mb->get_button_index()) == input_item->get_meta("__index"); + if (key_match || joyb_match || joym_match || mb_match) { + category->set_collapsed(false); + input_item->select(0); + input_list_tree->ensure_cursor_is_visible(); + return; + } + input_item = input_item->get_next(); + } + } + } + + category->set_collapsed(true); // Event not in this category, so collapse; + category = category->get_next(); + } + } + } else { + // Event is not valid, reset dialog + event = p_event; + event_listener->clear_event(); + event_as_text->set_text(TTR("No Event Configured")); + + additional_options_container->hide(); + input_list_tree->deselect_all(); + _update_input_list(); + } +} + +void InputEventConfigurationDialog::_on_listen_input_changed(const Ref<InputEvent> &p_event) { + // Ignore if invalid, echo or not pressed + if (p_event.is_null() || p_event->is_echo() || !p_event->is_pressed()) { + return; + } + + // Create an editable reference + Ref<InputEvent> received_event = p_event; + // Check what the type is and if it is allowed. + Ref<InputEventKey> k = received_event; + Ref<InputEventJoypadButton> joyb = received_event; + Ref<InputEventJoypadMotion> joym = received_event; + Ref<InputEventMouseButton> mb = received_event; + + int type = 0; + if (k.is_valid()) { + type = INPUT_KEY; + } else if (joyb.is_valid()) { + type = INPUT_JOY_BUTTON; + } else if (joym.is_valid()) { + type = INPUT_JOY_MOTION; + } else if (mb.is_valid()) { + type = INPUT_MOUSE_BUTTON; + } + + if (!(allowed_input_types & type)) { + return; + } + + if (joym.is_valid()) { + float axis_value = joym->get_axis_value(); + if (ABS(axis_value) < 0.9) { + // Ignore motion below 0.9 magnitude to avoid accidental touches + return; + } else { + // Always make the value 1 or -1 for display consistency + joym->set_axis_value(SIGN(axis_value)); + } + } + + if (k.is_valid()) { + k->set_pressed(false); // To avoid serialisation of 'pressed' property - doesn't matter for actions anyway. + // Maintain physical keycode option state + if (physical_key_checkbox->is_pressed()) { + k->set_keycode(Key::NONE); + } else { + k->set_physical_keycode(Key::NONE); + } + } + + Ref<InputEventWithModifiers> mod = received_event; + if (mod.is_valid()) { + mod->set_window_id(0); + } + + // Maintain device selection. + received_event->set_device(_get_current_device()); + + _set_event(received_event); +} + +void InputEventConfigurationDialog::_on_listen_focus_changed() { + if (event_listener->has_focus()) { + set_close_on_escape(false); + } else { + set_close_on_escape(true); + } +} + +void InputEventConfigurationDialog::_search_term_updated(const String &) { + _update_input_list(); +} + +void InputEventConfigurationDialog::_update_input_list() { + input_list_tree->clear(); + + TreeItem *root = input_list_tree->create_item(); + String search_term = input_list_search->get_text(); + + bool collapse = input_list_search->get_text().is_empty(); + + if (allowed_input_types & INPUT_KEY) { + TreeItem *kb_root = input_list_tree->create_item(root); + kb_root->set_text(0, TTR("Keyboard Keys")); + kb_root->set_icon(0, icon_cache.keyboard); + kb_root->set_collapsed(collapse); + kb_root->set_meta("__type", INPUT_KEY); + + for (int i = 0; i < keycode_get_count(); i++) { + String name = keycode_get_name_by_index(i); + + if (!search_term.is_empty() && name.findn(search_term) == -1) { + continue; + } + + TreeItem *item = input_list_tree->create_item(kb_root); + item->set_text(0, name); + item->set_meta("__keycode", keycode_get_value_by_index(i)); + } + } + + if (allowed_input_types & INPUT_MOUSE_BUTTON) { + TreeItem *mouse_root = input_list_tree->create_item(root); + mouse_root->set_text(0, TTR("Mouse Buttons")); + mouse_root->set_icon(0, icon_cache.mouse); + mouse_root->set_collapsed(collapse); + mouse_root->set_meta("__type", INPUT_MOUSE_BUTTON); + + MouseButton mouse_buttons[9] = { MouseButton::LEFT, MouseButton::RIGHT, MouseButton::MIDDLE, MouseButton::WHEEL_UP, MouseButton::WHEEL_DOWN, MouseButton::WHEEL_LEFT, MouseButton::WHEEL_RIGHT, MouseButton::MB_XBUTTON1, MouseButton::MB_XBUTTON2 }; + for (int i = 0; i < 9; i++) { + Ref<InputEventMouseButton> mb; + mb.instantiate(); + mb->set_button_index(mouse_buttons[i]); + String desc = get_event_text(mb, false); + + if (!search_term.is_empty() && desc.findn(search_term) == -1) { + continue; + } + + TreeItem *item = input_list_tree->create_item(mouse_root); + item->set_text(0, desc); + item->set_meta("__index", mouse_buttons[i]); + } + } + + if (allowed_input_types & INPUT_JOY_BUTTON) { + TreeItem *joyb_root = input_list_tree->create_item(root); + joyb_root->set_text(0, TTR("Joypad Buttons")); + joyb_root->set_icon(0, icon_cache.joypad_button); + joyb_root->set_collapsed(collapse); + joyb_root->set_meta("__type", INPUT_JOY_BUTTON); + + for (int i = 0; i < (int)JoyButton::MAX; i++) { + Ref<InputEventJoypadButton> joyb; + joyb.instantiate(); + joyb->set_button_index((JoyButton)i); + String desc = get_event_text(joyb, false); + + if (!search_term.is_empty() && desc.findn(search_term) == -1) { + continue; + } + + TreeItem *item = input_list_tree->create_item(joyb_root); + item->set_text(0, desc); + item->set_meta("__index", i); + } + } + + if (allowed_input_types & INPUT_JOY_MOTION) { + TreeItem *joya_root = input_list_tree->create_item(root); + joya_root->set_text(0, TTR("Joypad Axes")); + joya_root->set_icon(0, icon_cache.joypad_axis); + joya_root->set_collapsed(collapse); + joya_root->set_meta("__type", INPUT_JOY_MOTION); + + for (int i = 0; i < (int)JoyAxis::MAX * 2; i++) { + int axis = i / 2; + int direction = (i & 1) ? 1 : -1; + Ref<InputEventJoypadMotion> joym; + joym.instantiate(); + joym->set_axis((JoyAxis)axis); + joym->set_axis_value(direction); + String desc = get_event_text(joym, false); + + if (!search_term.is_empty() && desc.findn(search_term) == -1) { + continue; + } + + TreeItem *item = input_list_tree->create_item(joya_root); + item->set_text(0, desc); + item->set_meta("__axis", i >> 1); + item->set_meta("__value", (i & 1) ? 1 : -1); + } + } +} + +void InputEventConfigurationDialog::_mod_toggled(bool p_checked, int p_index) { + Ref<InputEventWithModifiers> ie = event; + + // Not event with modifiers + if (ie.is_null()) { + return; + } + + if (p_index == 0) { + ie->set_alt_pressed(p_checked); + } else if (p_index == 1) { + ie->set_shift_pressed(p_checked); + } else if (p_index == 2) { + if (!autoremap_command_or_control_checkbox->is_pressed()) { + ie->set_ctrl_pressed(p_checked); + } + } else if (p_index == 3) { + if (!autoremap_command_or_control_checkbox->is_pressed()) { + ie->set_meta_pressed(p_checked); + } + } + + _set_event(ie); +} + +void InputEventConfigurationDialog::_autoremap_command_or_control_toggled(bool p_checked) { + Ref<InputEventWithModifiers> ie = event; + if (ie.is_valid()) { + ie->set_command_or_control_autoremap(p_checked); + _set_event(ie); + } + + if (p_checked) { + mod_checkboxes[MOD_META]->hide(); + mod_checkboxes[MOD_CTRL]->hide(); + } else { + mod_checkboxes[MOD_META]->show(); + mod_checkboxes[MOD_CTRL]->show(); + } +} + +void InputEventConfigurationDialog::_physical_keycode_toggled(bool p_checked) { + Ref<InputEventKey> k = event; + + if (k.is_null()) { + return; + } + + if (p_checked) { + k->set_physical_keycode(k->get_keycode()); + k->set_keycode(Key::NONE); + } else { + k->set_keycode((Key)k->get_physical_keycode()); + k->set_physical_keycode(Key::NONE); + } + + _set_event(k); +} + +void InputEventConfigurationDialog::_input_list_item_selected() { + TreeItem *selected = input_list_tree->get_selected(); + + // Invalid tree selection - type only exists on the "category" items, which are not a valid selection. + if (selected->has_meta("__type")) { + return; + } + + InputType input_type = (InputType)(int)selected->get_parent()->get_meta("__type"); + + switch (input_type) { + case INPUT_KEY: { + Key keycode = (Key)(int)selected->get_meta("__keycode"); + Ref<InputEventKey> k; + k.instantiate(); + + if (physical_key_checkbox->is_pressed()) { + k->set_physical_keycode(keycode); + k->set_keycode(Key::NONE); + } else { + k->set_physical_keycode(Key::NONE); + k->set_keycode(keycode); + } + + // Maintain modifier state from checkboxes + k->set_alt_pressed(mod_checkboxes[MOD_ALT]->is_pressed()); + k->set_shift_pressed(mod_checkboxes[MOD_SHIFT]->is_pressed()); + if (autoremap_command_or_control_checkbox->is_pressed()) { + k->set_command_or_control_autoremap(true); + } else { + k->set_ctrl_pressed(mod_checkboxes[MOD_CTRL]->is_pressed()); + k->set_meta_pressed(mod_checkboxes[MOD_META]->is_pressed()); + } + + _set_event(k, false); + } break; + case INPUT_MOUSE_BUTTON: { + MouseButton idx = (MouseButton)(int)selected->get_meta("__index"); + Ref<InputEventMouseButton> mb; + mb.instantiate(); + mb->set_button_index(idx); + // Maintain modifier state from checkboxes + mb->set_alt_pressed(mod_checkboxes[MOD_ALT]->is_pressed()); + mb->set_shift_pressed(mod_checkboxes[MOD_SHIFT]->is_pressed()); + if (autoremap_command_or_control_checkbox->is_pressed()) { + mb->set_command_or_control_autoremap(true); + } else { + mb->set_ctrl_pressed(mod_checkboxes[MOD_CTRL]->is_pressed()); + mb->set_meta_pressed(mod_checkboxes[MOD_META]->is_pressed()); + } + + // Maintain selected device + mb->set_device(_get_current_device()); + + _set_event(mb, false); + } break; + case INPUT_JOY_BUTTON: { + JoyButton idx = (JoyButton)(int)selected->get_meta("__index"); + Ref<InputEventJoypadButton> jb = InputEventJoypadButton::create_reference(idx); + + // Maintain selected device + jb->set_device(_get_current_device()); + + _set_event(jb, false); + } break; + case INPUT_JOY_MOTION: { + JoyAxis axis = (JoyAxis)(int)selected->get_meta("__axis"); + int value = selected->get_meta("__value"); + + Ref<InputEventJoypadMotion> jm; + jm.instantiate(); + jm->set_axis(axis); + jm->set_axis_value(value); + + // Maintain selected device + jm->set_device(_get_current_device()); + + _set_event(jm, false); + } break; + } +} + +void InputEventConfigurationDialog::_device_selection_changed(int p_option_button_index) { + // Subtract 1 as option index 0 corresponds to "All Devices" (value of -1) + // and option index 1 corresponds to device 0, etc... + event->set_device(p_option_button_index - 1); + event_as_text->set_text(get_event_text(event, true)); +} + +void InputEventConfigurationDialog::_set_current_device(int p_device) { + device_id_option->select(p_device + 1); +} + +int InputEventConfigurationDialog::_get_current_device() const { + return device_id_option->get_selected() - 1; +} + +String InputEventConfigurationDialog::_get_device_string(int p_device) const { + if (p_device == InputMap::ALL_DEVICES) { + return TTR("All Devices"); + } + return TTR("Device") + " " + itos(p_device); +} + +void InputEventConfigurationDialog::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_THEME_CHANGED: { + input_list_search->set_right_icon(input_list_search->get_theme_icon(SNAME("Search"), SNAME("EditorIcons"))); + + physical_key_checkbox->set_icon(get_theme_icon(SNAME("KeyboardPhysical"), SNAME("EditorIcons"))); + + icon_cache.keyboard = get_theme_icon(SNAME("Keyboard"), SNAME("EditorIcons")); + icon_cache.mouse = get_theme_icon(SNAME("Mouse"), SNAME("EditorIcons")); + icon_cache.joypad_button = get_theme_icon(SNAME("JoyButton"), SNAME("EditorIcons")); + icon_cache.joypad_axis = get_theme_icon(SNAME("JoyAxis"), SNAME("EditorIcons")); + + _update_input_list(); + } break; + } +} + +void InputEventConfigurationDialog::popup_and_configure(const Ref<InputEvent> &p_event) { + if (p_event.is_valid()) { + _set_event(p_event); + } else { + // Clear Event + _set_event(p_event); + + // Clear Checkbox Values + for (int i = 0; i < MOD_MAX; i++) { + mod_checkboxes[i]->set_pressed(false); + } + + // Enable the Physical Key checkbox by default to encourage its use. + // Physical Key should be used for most game inputs as it allows keys to work + // on non-QWERTY layouts out of the box. + // This is especially important for WASD movement layouts. + physical_key_checkbox->set_pressed(true); + + autoremap_command_or_control_checkbox->set_pressed(false); + + // Select "All Devices" by default. + device_id_option->select(0); + } + + popup_centered(Size2(0, 400) * EDSCALE); +} + +Ref<InputEvent> InputEventConfigurationDialog::get_event() const { + return event; +} + +void InputEventConfigurationDialog::set_allowed_input_types(int p_type_masks) { + allowed_input_types = p_type_masks; +} + +InputEventConfigurationDialog::InputEventConfigurationDialog() { + allowed_input_types = INPUT_KEY | INPUT_MOUSE_BUTTON | INPUT_JOY_BUTTON | INPUT_JOY_MOTION; + + set_title(TTR("Event Configuration")); + set_min_size(Size2i(550 * EDSCALE, 0)); // Min width + + VBoxContainer *main_vbox = memnew(VBoxContainer); + add_child(main_vbox); + + event_as_text = memnew(Label); + event_as_text->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART); + event_as_text->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); + event_as_text->add_theme_font_override("font", get_theme_font(SNAME("bold"), SNAME("EditorFonts"))); + event_as_text->add_theme_font_size_override("font_size", 18 * EDSCALE); + main_vbox->add_child(event_as_text); + + event_listener = memnew(EventListenerLineEdit); + event_listener->set_h_size_flags(Control::SIZE_EXPAND_FILL); + event_listener->set_stretch_ratio(0.75); + event_listener->connect("event_changed", callable_mp(this, &InputEventConfigurationDialog::_on_listen_input_changed)); + event_listener->connect("focus_entered", callable_mp(this, &InputEventConfigurationDialog::_on_listen_focus_changed)); + event_listener->connect("focus_exited", callable_mp(this, &InputEventConfigurationDialog::_on_listen_focus_changed)); + main_vbox->add_child(event_listener); + + main_vbox->add_child(memnew(HSeparator)); + + // List of all input options to manually select from. + VBoxContainer *manual_vbox = memnew(VBoxContainer); + manual_vbox->set_name(TTR("Manual Selection")); + manual_vbox->set_v_size_flags(Control::SIZE_EXPAND_FILL); + main_vbox->add_child(manual_vbox); + + input_list_search = memnew(LineEdit); + input_list_search->set_h_size_flags(Control::SIZE_EXPAND_FILL); + input_list_search->set_placeholder(TTR("Filter Inputs")); + input_list_search->set_clear_button_enabled(true); + input_list_search->connect("text_changed", callable_mp(this, &InputEventConfigurationDialog::_search_term_updated)); + manual_vbox->add_child(input_list_search); + + input_list_tree = memnew(Tree); + input_list_tree->set_custom_minimum_size(Size2(0, 100 * EDSCALE)); // Min height for tree + input_list_tree->connect("item_selected", callable_mp(this, &InputEventConfigurationDialog::_input_list_item_selected)); + input_list_tree->set_v_size_flags(Control::SIZE_EXPAND_FILL); + manual_vbox->add_child(input_list_tree); + + input_list_tree->set_hide_root(true); + input_list_tree->set_columns(1); + + _update_input_list(); + + // Additional Options + additional_options_container = memnew(VBoxContainer); + additional_options_container->hide(); + + Label *opts_label = memnew(Label); + opts_label->set_theme_type_variation("HeaderSmall"); + opts_label->set_text(TTR("Additional Options")); + additional_options_container->add_child(opts_label); + + // Device Selection + device_container = memnew(HBoxContainer); + device_container->set_h_size_flags(Control::SIZE_EXPAND_FILL); + + Label *device_label = memnew(Label); + device_label->set_theme_type_variation("HeaderSmall"); + device_label->set_text(TTR("Device:")); + device_container->add_child(device_label); + + device_id_option = memnew(OptionButton); + device_id_option->set_h_size_flags(Control::SIZE_EXPAND_FILL); + for (int i = -1; i < 8; i++) { + device_id_option->add_item(_get_device_string(i)); + } + device_id_option->connect("item_selected", callable_mp(this, &InputEventConfigurationDialog::_device_selection_changed)); + _set_current_device(InputMap::ALL_DEVICES); + device_container->add_child(device_id_option); + + device_container->hide(); + additional_options_container->add_child(device_container); + + // Modifier Selection + mod_container = memnew(HBoxContainer); + for (int i = 0; i < MOD_MAX; i++) { + String name = mods[i]; + mod_checkboxes[i] = memnew(CheckBox); + mod_checkboxes[i]->connect("toggled", callable_mp(this, &InputEventConfigurationDialog::_mod_toggled).bind(i)); + mod_checkboxes[i]->set_text(name); + mod_checkboxes[i]->set_tooltip_text(TTR(mods_tip[i])); + mod_container->add_child(mod_checkboxes[i]); + } + + mod_container->add_child(memnew(VSeparator)); + + autoremap_command_or_control_checkbox = memnew(CheckBox); + autoremap_command_or_control_checkbox->connect("toggled", callable_mp(this, &InputEventConfigurationDialog::_autoremap_command_or_control_toggled)); + autoremap_command_or_control_checkbox->set_pressed(false); + autoremap_command_or_control_checkbox->set_text(TTR("Command / Control (auto)")); + autoremap_command_or_control_checkbox->set_tooltip_text(TTR("Automatically remaps between 'Meta' ('Command') and 'Control' depending on current platform.")); + mod_container->add_child(autoremap_command_or_control_checkbox); + + mod_container->hide(); + additional_options_container->add_child(mod_container); + + // Physical Key Checkbox + + physical_key_checkbox = memnew(CheckBox); + physical_key_checkbox->set_text(TTR("Use Physical Keycode")); + physical_key_checkbox->set_tooltip_text(TTR("Stores the physical position of the key on the keyboard rather than the key's value. Used for compatibility with non-latin layouts.\nThis should generally be enabled for most game shortcuts, but not in non-game applications.")); + physical_key_checkbox->connect("toggled", callable_mp(this, &InputEventConfigurationDialog::_physical_keycode_toggled)); + physical_key_checkbox->hide(); + additional_options_container->add_child(physical_key_checkbox); + + main_vbox->add_child(additional_options_container); +} diff --git a/editor/input_event_configuration_dialog.h b/editor/input_event_configuration_dialog.h new file mode 100644 index 0000000000..fc590cba26 --- /dev/null +++ b/editor/input_event_configuration_dialog.h @@ -0,0 +1,124 @@ +/*************************************************************************/ +/* input_event_configuration_dialog.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef INPUT_EVENT_CONFIGURATION_DIALOG_H +#define INPUT_EVENT_CONFIGURATION_DIALOG_H + +#include "scene/gui/dialogs.h" + +class OptionButton; +class Tree; +class EventListenerLineEdit; +class CheckBox; + +// Confirmation Dialog used when configuring an input event. +// Separate from ActionMapEditor for code cleanliness and separation of responsibilities. +class InputEventConfigurationDialog : public ConfirmationDialog { + GDCLASS(InputEventConfigurationDialog, ConfirmationDialog) +private: + struct IconCache { + Ref<Texture2D> keyboard; + Ref<Texture2D> mouse; + Ref<Texture2D> joypad_button; + Ref<Texture2D> joypad_axis; + } icon_cache; + + Ref<InputEvent> event = Ref<InputEvent>(); + + // Listening for input + EventListenerLineEdit *event_listener = nullptr; + Label *event_as_text = nullptr; + + // List of All Key/Mouse/Joypad input options. + int allowed_input_types; + Tree *input_list_tree = nullptr; + LineEdit *input_list_search = nullptr; + + // Additional Options, shown depending on event selected + VBoxContainer *additional_options_container = nullptr; + + HBoxContainer *device_container = nullptr; + OptionButton *device_id_option = nullptr; + + HBoxContainer *mod_container = nullptr; // Contains the subcontainer and the store command checkbox. + + enum ModCheckbox { + MOD_ALT, + MOD_SHIFT, + MOD_CTRL, + MOD_META, + MOD_MAX + }; +#if defined(MACOS_ENABLED) + String mods[MOD_MAX] = { "Option", "Shift", "Ctrl", "Command" }; +#elif defined(WINDOWS_ENABLED) + String mods[MOD_MAX] = { "Alt", "Shift", "Ctrl", "Windows" }; +#else + String mods[MOD_MAX] = { "Alt", "Shift", "Ctrl", "Meta" }; +#endif + String mods_tip[MOD_MAX] = { "Alt or Option key", "Shift key", "Control key", "Meta/Windows or Command key" }; + + CheckBox *mod_checkboxes[MOD_MAX]; + CheckBox *autoremap_command_or_control_checkbox = nullptr; + + CheckBox *physical_key_checkbox = nullptr; + + void _set_event(const Ref<InputEvent> &p_event, bool p_update_input_list_selection = true); + void _on_listen_input_changed(const Ref<InputEvent> &p_event); + void _on_listen_focus_changed(); + + void _search_term_updated(const String &p_term); + void _update_input_list(); + void _input_list_item_selected(); + + void _mod_toggled(bool p_checked, int p_index); + void _autoremap_command_or_control_toggled(bool p_checked); + void _physical_keycode_toggled(bool p_checked); + + void _device_selection_changed(int p_option_button_index); + void _set_current_device(int p_device); + int _get_current_device() const; + String _get_device_string(int p_device) const; + +protected: + void _notification(int p_what); + +public: + // Pass an existing event to configure it. Alternatively, pass no event to start with a blank configuration. + void popup_and_configure(const Ref<InputEvent> &p_event = Ref<InputEvent>()); + Ref<InputEvent> get_event() const; + String get_event_text(const Ref<InputEvent> &p_event, bool p_include_device) const; + + void set_allowed_input_types(int p_type_masks); + + InputEventConfigurationDialog(); +}; + +#endif // INPUT_EVENT_CONFIGURATION_DIALOG_H diff --git a/editor/plugins/input_event_editor_plugin.cpp b/editor/plugins/input_event_editor_plugin.cpp index e28c1a4777..b67f0f6ad2 100644 --- a/editor/plugins/input_event_editor_plugin.cpp +++ b/editor/plugins/input_event_editor_plugin.cpp @@ -30,6 +30,9 @@ #include "input_event_editor_plugin.h" +#include "editor/event_listener_line_edit.h" +#include "editor/input_event_configuration_dialog.h" + void InputEventConfigContainer::_bind_methods() { } diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index ae2ed4ddeb..9910a97f88 100644 --- a/editor/plugins/script_text_editor.cpp +++ b/editor/plugins/script_text_editor.cpp @@ -267,6 +267,7 @@ void ScriptTextEditor::_warning_clicked(Variant p_line) { void ScriptTextEditor::_error_clicked(Variant p_line) { if (p_line.get_type() == Variant::INT) { + code_editor->get_text_editor()->remove_secondary_carets(); code_editor->get_text_editor()->set_caret_line(p_line.operator int64_t()); } } @@ -295,6 +296,7 @@ void ScriptTextEditor::reload_text() { void ScriptTextEditor::add_callback(const String &p_function, PackedStringArray p_args) { String code = code_editor->get_text_editor()->get_text(); int pos = script->get_language()->find_function(p_function, code); + code_editor->get_text_editor()->remove_secondary_carets(); if (pos == -1) { //does not exist code_editor->get_text_editor()->deselect(); @@ -1367,6 +1369,7 @@ void ScriptTextEditor::_edit_option(int p_op) { return; } + tx->remove_secondary_carets(); int line = tx->get_caret_line(); // wrap around @@ -1393,6 +1396,7 @@ void ScriptTextEditor::_edit_option(int p_op) { return; } + tx->remove_secondary_carets(); int line = tx->get_caret_line(); // wrap around if (line <= (int)bpoints[0]) { @@ -1413,21 +1417,21 @@ void ScriptTextEditor::_edit_option(int p_op) { } break; case HELP_CONTEXTUAL: { - String text = tx->get_selected_text(); + String text = tx->get_selected_text(0); if (text.is_empty()) { - text = tx->get_word_under_caret(); + text = tx->get_word_under_caret(0); } if (!text.is_empty()) { emit_signal(SNAME("request_help"), text); } } break; case LOOKUP_SYMBOL: { - String text = tx->get_word_under_caret(); + String text = tx->get_word_under_caret(0); if (text.is_empty()) { - text = tx->get_selected_text(); + text = tx->get_selected_text(0); } if (!text.is_empty()) { - _lookup_symbol(text, tx->get_caret_line(), tx->get_caret_column()); + _lookup_symbol(text, tx->get_caret_line(0), tx->get_caret_column(0)); } } break; } @@ -1605,6 +1609,7 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data int col = pos.x; if (d.has("type") && String(d["type"]) == "resource") { + te->remove_secondary_carets(); Ref<Resource> res = d["resource"]; if (!res.is_valid()) { return; @@ -1622,6 +1627,7 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data } if (d.has("type") && (String(d["type"]) == "files" || String(d["type"]) == "files_and_dirs")) { + te->remove_secondary_carets(); Array files = d["files"]; String text_to_drop; @@ -1645,6 +1651,7 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data } if (d.has("type") && String(d["type"]) == "nodes") { + te->remove_secondary_carets(); Node *scene_root = get_tree()->get_edited_scene_root(); if (!scene_root) { EditorNode::get_singleton()->show_warning(TTR("Can't drop nodes without an open scene.")); @@ -1729,6 +1736,7 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data } if (d.has("type") && String(d["type"]) == "obj_property") { + te->remove_secondary_carets(); const String text_to_drop = String(d["property"]).c_escape().quote(quote_style); te->set_caret_line(row); @@ -1749,8 +1757,8 @@ void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) { local_pos = mb->get_global_position() - tx->get_global_position(); create_menu = true; } else if (k.is_valid() && k->is_action("ui_menu", true)) { - tx->adjust_viewport_to_caret(); - local_pos = tx->get_caret_draw_pos(); + tx->adjust_viewport_to_caret(0); + local_pos = tx->get_caret_draw_pos(0); create_menu = true; } @@ -1761,6 +1769,7 @@ void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) { tx->set_move_caret_on_right_click_enabled(EditorSettings::get_singleton()->get("text_editor/behavior/navigation/move_caret_on_right_click")); if (tx->is_move_caret_on_right_click_enabled()) { + tx->remove_secondary_carets(); if (tx->has_selection()) { int from_line = tx->get_selection_from_line(); int to_line = tx->get_selection_to_line(); @@ -1780,10 +1789,10 @@ void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) { String word_at_pos = tx->get_word_at_pos(local_pos); if (word_at_pos.is_empty()) { - word_at_pos = tx->get_word_under_caret(); + word_at_pos = tx->get_word_under_caret(0); } if (word_at_pos.is_empty()) { - word_at_pos = tx->get_selected_text(); + word_at_pos = tx->get_selected_text(0); } bool has_color = (word_at_pos == "Color"); diff --git a/editor/plugins/text_editor.cpp b/editor/plugins/text_editor.cpp index 51e7ee101c..07f0819c7f 100644 --- a/editor/plugins/text_editor.cpp +++ b/editor/plugins/text_editor.cpp @@ -445,6 +445,7 @@ void TextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) { bool is_folded = tx->is_line_folded(row); if (tx->is_move_caret_on_right_click_enabled()) { + tx->remove_secondary_carets(); if (tx->has_selection()) { int from_line = tx->get_selection_from_line(); int to_line = tx->get_selection_to_line(); @@ -471,9 +472,9 @@ void TextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) { Ref<InputEventKey> k = ev; if (k.is_valid() && k->is_pressed() && k->is_action("ui_menu", true)) { CodeEdit *tx = code_editor->get_text_editor(); - int line = tx->get_caret_line(); - tx->adjust_viewport_to_caret(); - _make_context_menu(tx->has_selection(), tx->can_fold_line(line), tx->is_line_folded(line), (get_global_transform().inverse() * tx->get_global_transform()).xform(tx->get_caret_draw_pos())); + int line = tx->get_caret_line(0); + tx->adjust_viewport_to_caret(0); + _make_context_menu(tx->has_selection(0), tx->can_fold_line(line), tx->is_line_folded(line), (get_global_transform().inverse() * tx->get_global_transform()).xform(tx->get_caret_draw_pos(0))); context_menu->grab_focus(); } } diff --git a/editor/plugins/text_shader_editor.cpp b/editor/plugins/text_shader_editor.cpp index 5815bab806..9dea990bd8 100644 --- a/editor/plugins/text_shader_editor.cpp +++ b/editor/plugins/text_shader_editor.cpp @@ -958,6 +958,7 @@ void TextShaderEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) { tx->set_move_caret_on_right_click_enabled(EditorSettings::get_singleton()->get("text_editor/behavior/navigation/move_caret_on_right_click")); if (tx->is_move_caret_on_right_click_enabled()) { + tx->remove_secondary_carets(); if (tx->has_selection()) { int from_line = tx->get_selection_from_line(); int to_line = tx->get_selection_to_line(); diff --git a/editor/project_settings_editor.cpp b/editor/project_settings_editor.cpp index 2da49f11cc..81f37eaeec 100644 --- a/editor/project_settings_editor.cpp +++ b/editor/project_settings_editor.cpp @@ -36,6 +36,7 @@ #include "editor/editor_scale.h" #include "editor/editor_settings.h" #include "editor/editor_undo_redo_manager.h" +#include "scene/gui/check_button.h" #include "servers/movie_writer/movie_writer.h" ProjectSettingsEditor *ProjectSettingsEditor::singleton = nullptr; diff --git a/methods.py b/methods.py index 3a2dd07964..f4afead9f4 100644 --- a/methods.py +++ b/methods.py @@ -7,15 +7,6 @@ from collections import OrderedDict from collections.abc import Mapping from typing import Iterator -# We need to define our own `Action` method to control the verbosity of output -# and whenever we need to run those commands in a subprocess on some platforms. -from SCons import Node -from SCons.Script import Action -from SCons.Script import ARGUMENTS -from SCons.Script import Glob -from SCons.Variables.BoolVariable import _text2bool -from platform_methods import run_in_subprocess - def add_source_files(self, sources, files): # Convert string to list of absolute paths (including expanding wildcard) @@ -220,6 +211,9 @@ def get_cmdline_bool(option, default): """We use `ARGUMENTS.get()` to check if options were manually overridden on the command line, and SCons' _text2bool helper to convert them to booleans, otherwise they're handled as strings. """ + from SCons.Script import ARGUMENTS + from SCons.Variables.BoolVariable import _text2bool + cmdline_val = ARGUMENTS.get(option) if cmdline_val is not None: return _text2bool(cmdline_val) @@ -703,6 +697,9 @@ def generate_cpp_hint_file(filename): def glob_recursive(pattern, node="."): + from SCons import Node + from SCons.Script import Glob + results = [] for f in Glob(str(node) + "/*", source=True): if type(f) is Node.FS.Dir: @@ -913,6 +910,9 @@ def CommandNoCache(env, target, sources, command, **args): def Run(env, function, short_message, subprocess=True): + from SCons.Script import Action + from platform_methods import run_in_subprocess + output_print = short_message if not env["verbose"] else "" if not subprocess: return Action(function, output_print) diff --git a/modules/mono/.gitignore b/modules/mono/.gitignore index fa6d00cbbb..2d62f9f88a 100644 --- a/modules/mono/.gitignore +++ b/modules/mono/.gitignore @@ -1,2 +1,5 @@ # Do not ignore solution files inside the mono module. Overrides Godot's global gitignore. !*.sln + +# Generated by build_assemblies.py. +SdkPackageVersions.props diff --git a/modules/mono/SdkPackageVersions.props b/modules/mono/SdkPackageVersions.props deleted file mode 100644 index 65094aa34f..0000000000 --- a/modules/mono/SdkPackageVersions.props +++ /dev/null @@ -1,8 +0,0 @@ -<Project> - <PropertyGroup> - <PackageFloatingVersion_Godot>4.0.*-*</PackageFloatingVersion_Godot> - <PackageVersion_GodotSharp>4.0.0-dev</PackageVersion_GodotSharp> - <PackageVersion_Godot_NET_Sdk>4.0.0-dev8</PackageVersion_Godot_NET_Sdk> - <PackageVersion_Godot_SourceGenerators>4.0.0-dev8</PackageVersion_Godot_SourceGenerators> - </PropertyGroup> -</Project> diff --git a/modules/mono/build_scripts/build_assemblies.py b/modules/mono/build_scripts/build_assemblies.py index d28c3a0c3a..7343af0b39 100755 --- a/modules/mono/build_scripts/build_assemblies.py +++ b/modules/mono/build_scripts/build_assemblies.py @@ -256,7 +256,57 @@ def build_godot_api(msbuild_tool, module_dir, output_dir, push_nupkgs_local, flo return 0 +def generate_sdk_package_versions(): + # I can't believe importing files in Python is so convoluted when not + # following the golden standard for packages/modules. + import os + import sys + from os.path import dirname + + # We want ../../../methods.py. + script_path = dirname(os.path.abspath(__file__)) + root_path = dirname(dirname(dirname(script_path))) + + sys.path.insert(0, root_path) + from methods import get_version_info + + version_info = get_version_info("") + sys.path.remove(root_path) + + version_str = "{major}.{minor}.{patch}".format(**version_info) + version_status = version_info["status"] + if version_status != "stable": # Pre-release + # If version was overridden to be e.g. "beta3", we insert a dot between + # "beta" and "3" to follow SemVer 2.0. + import re + + match = re.search(r"[\d]+$", version_status) + if match: + pos = match.start() + version_status = version_status[:pos] + "." + version_status[pos:] + version_str += "-" + version_status + + props = """<Project> + <PropertyGroup> + <PackageVersion_GodotSharp>{0}</PackageVersion_GodotSharp> + <PackageVersion_Godot_NET_Sdk>{0}</PackageVersion_Godot_NET_Sdk> + <PackageVersion_Godot_SourceGenerators>{0}</PackageVersion_Godot_SourceGenerators> + </PropertyGroup> +</Project> +""".format( + version_str + ) + + # We write in ../SdkPackageVersions.props. + with open(os.path.join(dirname(script_path), "SdkPackageVersions.props"), "w") as f: + f.write(props) + f.close() + + def build_all(msbuild_tool, module_dir, output_dir, godot_platform, dev_debug, push_nupkgs_local, float_size): + # Generate SdkPackageVersions.props + generate_sdk_package_versions() + # Godot API exit_code = build_godot_api(msbuild_tool, module_dir, output_dir, push_nupkgs_local, float_size) if exit_code != 0: diff --git a/modules/openxr/openxr_api.cpp b/modules/openxr/openxr_api.cpp index 8a69072793..01107ebcc9 100644 --- a/modules/openxr/openxr_api.cpp +++ b/modules/openxr/openxr_api.cpp @@ -245,7 +245,7 @@ bool OpenXRAPI::create_instance() { if (!is_extension_supported(requested_extension.key)) { if (requested_extension.value == nullptr) { // nullptr means this is a manditory extension so we fail - ERR_FAIL_V_MSG(false, "OpenXR: OpenXR Runtime does not support OpenGL extension!"); + ERR_FAIL_V_MSG(false, String("OpenXR: OpenXR Runtime does not support ") + requested_extension.key + String(" extension!")); } else { // set this extension as not supported *requested_extension.value = false; diff --git a/modules/text_server_adv/gdextension_build/SConstruct b/modules/text_server_adv/gdextension_build/SConstruct index 7ca8859ee6..6220e35b54 100644 --- a/modules/text_server_adv/gdextension_build/SConstruct +++ b/modules/text_server_adv/gdextension_build/SConstruct @@ -68,7 +68,7 @@ if env["msdfgen_enabled"] and env["freetype_enabled"]: env.Append(CPPDEFINES=["MODULE_MSDFGEN_ENABLED"]) lib = env_msdfgen.Library( - f'msdfgen_builtin.{env["platform"]}.{env["target"]}.{env["arch_suffix"]}{env["LIBSUFFIX"]}', + f'msdfgen_builtin{env["suffix"]}{env["LIBSUFFIX"]}', thirdparty_msdfgen_sources, ) env.Append(LIBS=[lib]) @@ -199,7 +199,7 @@ if env["freetype_enabled"]: env.Append(CPPDEFINES=["MODULE_FREETYPE_ENABLED"]) lib = env_freetype.Library( - f'freetype_builtin.{env["platform"]}.{env["target"]}.{env["arch_suffix"]}{env["LIBSUFFIX"]}', + f'freetype_builtin{env["suffix"]}{env["LIBSUFFIX"]}', thirdparty_freetype_sources, ) env.Append(LIBS=[lib]) @@ -321,7 +321,7 @@ if env["freetype_enabled"]: env.Append(CPPPATH=["../../../thirdparty/harfbuzz/src"]) lib = env_harfbuzz.Library( - f'harfbuzz_builtin.{env["platform"]}.{env["target"]}.{env["arch_suffix"]}{env["LIBSUFFIX"]}', + f'harfbuzz_builtin{env["suffix"]}{env["LIBSUFFIX"]}', thirdparty_harfbuzz_sources, ) env.Prepend(LIBS=[lib]) @@ -381,7 +381,7 @@ if env["graphite_enabled"] and env["freetype_enabled"]: ) lib = env_graphite.Library( - f'graphite_builtin.{env["platform"]}.{env["target"]}.{env["arch_suffix"]}{env["LIBSUFFIX"]}', + f'graphite_builtin{env["suffix"]}{env["LIBSUFFIX"]}', thirdparty_graphite_sources, ) env.Append(LIBS=[lib]) @@ -640,9 +640,7 @@ env.Append(CPPPATH=["../../../thirdparty/icu4c/common/", "../../../thirdparty/ic if env["platform"] == "windows": env.Append(LIBS=["advapi32"]) -lib = env_icu.Library( - f'icu_builtin.{env["platform"]}.{env["target"]}.{env["arch_suffix"]}{env["LIBSUFFIX"]}', thirdparty_icu_sources -) +lib = env_icu.Library(f'icu_builtin{env["suffix"]}{env["LIBSUFFIX"]}', thirdparty_icu_sources) env.Append(LIBS=[lib]) env.Append(CPPDEFINES=["GDEXTENSION"]) @@ -662,7 +660,7 @@ if env["platform"] == "macos": ) else: library = env.SharedLibrary( - f'./bin/libtextserver_advanced.{env["platform"]}.{env["target"]}.{env["arch_suffix"]}{env["SHLIBSUFFIX"]}', + f'./bin/libtextserver_advanced{env["suffix"]}{env["SHLIBSUFFIX"]}', source=sources, ) diff --git a/modules/text_server_adv/gdextension_build/text_server_adv.gdextension b/modules/text_server_adv/gdextension_build/text_server_adv.gdextension index 11ed271ae9..c12fcfdfdf 100644 --- a/modules/text_server_adv/gdextension_build/text_server_adv.gdextension +++ b/modules/text_server_adv/gdextension_build/text_server_adv.gdextension @@ -4,9 +4,21 @@ entry_symbol = "textserver_advanced_init" [libraries] -linux.64.debug = "bin/libtextserver_advanced.linux.debug.64.so" -linux.64.release = "bin/libtextserver_advanced.linux.release.64.so" -windows.64.debug = "bin/libtextserver_advanced.windows.debug.64.dll" -windows.64.release = "bin/libtextserver_advanced.windows.release.64.dll" -macos.debug = "bin/libtextserver_advanced.macos.debug.framework" -macos.release = "bin/libtextserver_advanced.macos.release.framework" +linux.x86_64.debug = "bin/libtextserver_advanced.linux.template_debug.x86_64.so" +linux.x86_64.release = "bin/libtextserver_advanced.linux.template_release.x86_64.so" +linux.x86_32.debug = "bin/libtextserver_advanced.linux.template_debug.x86_32.so" +linux.x86_32.release = "bin/libtextserver_advanced.linux.template_release.x86_32.so" +linux.arm64.debug = "bin/libtextserver_advanced.linux.template_debug.arm64.so" +linux.arm64.release = "bin/libtextserver_advanced.linux.template_release.arm64.so" +linux.rv64.debug = "bin/libtextserver_advanced.linux.template_debug.rv64.so" +linux.rv64.release = "bin/libtextserver_advanced.linux.template_release.rv64.so" + +windows.x86_64.debug = "bin/libtextserver_advanced.windows.template_debug.x86_64.dll" +windows.x86_64.release = "bin/libtextserver_advanced.windows.template_release.x86_64.dll" +windows.x86_32.debug = "bin/libtextserver_advanced.windows.template_debug.x86_32.dll" +windows.x86_32.release = "bin/libtextserver_advanced.windows.template_release.x86_32.dll" +windows.arm64.debug = "bin/libtextserver_advanced.windows.template_debug.arm64.dll" +windows.arm64.release = "bin/libtextserver_advanced.windows.template_release.arm64.dll" + +macos.debug = "bin/libtextserver_advanced.macos.template_debug.framework" +macos.release = "bin/libtextserver_advanced.macos.template_release.framework" diff --git a/modules/text_server_fb/gdextension_build/SConstruct b/modules/text_server_fb/gdextension_build/SConstruct index eae8e031bf..8ed8f61a43 100644 --- a/modules/text_server_fb/gdextension_build/SConstruct +++ b/modules/text_server_fb/gdextension_build/SConstruct @@ -63,7 +63,7 @@ if env["msdfgen_enabled"] and env["freetype_enabled"]: env.Append(CPPDEFINES=["MODULE_MSDFGEN_ENABLED"]) lib = env_msdfgen.Library( - f'msdfgen_builtin.{env["platform"]}.{env["target"]}.{env["arch_suffix"]}{env["LIBSUFFIX"]}', + f'msdfgen_builtin{env["suffix"]}{env["LIBSUFFIX"]}', thirdparty_msdfgen_sources, ) env.Append(LIBS=[lib]) @@ -194,7 +194,7 @@ if env["freetype_enabled"]: env.Append(CPPDEFINES=["MODULE_FREETYPE_ENABLED"]) lib = env_freetype.Library( - f'freetype_builtin.{env["platform"]}.{env["target"]}.{env["arch_suffix"]}{env["LIBSUFFIX"]}', + f'freetype_builtin{env["suffix"]}{env["LIBSUFFIX"]}', thirdparty_freetype_sources, ) env.Append(LIBS=[lib]) @@ -217,7 +217,7 @@ if env["platform"] == "macos": ) else: library = env.SharedLibrary( - f'./bin/libtextserver_fallback.{env["platform"]}.{env["target"]}.{env["arch_suffix"]}{env["SHLIBSUFFIX"]}', + f'./bin/libtextserver_fallback{env["suffix"]}{env["SHLIBSUFFIX"]}', source=sources, ) diff --git a/modules/text_server_fb/gdextension_build/text_server_fb.gdextension b/modules/text_server_fb/gdextension_build/text_server_fb.gdextension index 9236555d63..58a92e403b 100644 --- a/modules/text_server_fb/gdextension_build/text_server_fb.gdextension +++ b/modules/text_server_fb/gdextension_build/text_server_fb.gdextension @@ -4,9 +4,21 @@ entry_symbol = "textserver_fallback_init" [libraries] -linux.64.debug = "bin/libtextserver_fallback.linux.debug.64.so" -linux.64.release = "bin/libtextserver_fallback.linux.release.64.so" -windows.64.debug = "bin/libtextserver_fallback.windows.debug.64.dll" -windows.64.release = "bin/libtextserver_fallback.windows.release.64.dll" -macos.debug = "bin/libtextserver_fallback.macos.debug.framework" -macos.release = "bin/libtextserver_fallback.macos.release.framework" +linux.x86_64.debug = "bin/libtextserver_fallback.linux.template_debug.x86_64.so" +linux.x86_64.release = "bin/libtextserver_fallback.linux.template_release.x86_64.so" +linux.x86_32.debug = "bin/libtextserver_fallback.linux.template_debug.x86_32.so" +linux.x86_32.release = "bin/libtextserver_fallback.linux.template_release.x86_32.so" +linux.arm64.debug = "bin/libtextserver_fallback.linux.template_debug.arm64.so" +linux.arm64.release = "bin/libtextserver_fallback.linux.template_release.arm64.so" +linux.rv64.debug = "bin/libtextserver_fallback.linux.template_debug.rv64.so" +linux.rv64.release = "bin/libtextserver_fallback.linux.template_release.rv64.so" + +windows.x86_64.debug = "bin/libtextserver_fallback.windows.template_debug.x86_64.dll" +windows.x86_64.release = "bin/libtextserver_fallback.windows.template_release.x86_64.dll" +windows.x86_32.debug = "bin/libtextserver_fallback.windows.template_debug.x86_32.dll" +windows.x86_32.release = "bin/libtextserver_fallback.windows.template_release.x86_32.dll" +windows.arm64.debug = "bin/libtextserver_fallback.windows.template_debug.arm64.dll" +windows.arm64.release = "bin/libtextserver_fallback.windows.template_release.arm64.dll" + +macos.debug = "bin/libtextserver_fallback.macos.template_debug.framework" +macos.release = "bin/libtextserver_fallback.macos.template_release.framework" diff --git a/modules/theora/video_stream_theora.h b/modules/theora/video_stream_theora.h index 3efb653651..9e096ec8b7 100644 --- a/modules/theora/video_stream_theora.h +++ b/modules/theora/video_stream_theora.h @@ -76,7 +76,7 @@ class VideoStreamPlaybackTheora : public VideoStreamPlayback { th_info ti; th_comment tc; th_dec_ctx *td = nullptr; - vorbis_info vi; + vorbis_info vi = {}; vorbis_dsp_state vd; vorbis_block vb; vorbis_comment vc; diff --git a/platform/android/SCsub b/platform/android/SCsub index 9f5b8d8387..e4d04f1df9 100644 --- a/platform/android/SCsub +++ b/platform/android/SCsub @@ -53,10 +53,10 @@ else: print("WARN: Architecture not suitable for embedding into APK; keeping .so at \\bin") if lib_arch_dir != "": - if env.debug_features: - lib_type_dir = "debug" - elif env.dev_build: + if env.dev_build: lib_type_dir = "dev" + elif env.debug_features: + lib_type_dir = "debug" else: # Release lib_type_dir = "release" diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index d3bce12de1..059c296cb7 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -580,6 +580,9 @@ void DisplayServerAndroid::process_gyroscope(const Vector3 &p_gyroscope) { } void DisplayServerAndroid::mouse_set_mode(MouseMode p_mode) { + if (!OS_Android::get_singleton()->get_godot_java()->get_godot_view()->can_update_pointer_icon() || !OS_Android::get_singleton()->get_godot_java()->get_godot_view()->can_capture_pointer()) { + return; + } if (mouse_mode == p_mode) { return; } @@ -612,6 +615,9 @@ MouseButton DisplayServerAndroid::mouse_get_button_state() const { } void DisplayServerAndroid::cursor_set_shape(DisplayServer::CursorShape p_shape) { + if (!OS_Android::get_singleton()->get_godot_java()->get_godot_view()->can_update_pointer_icon()) { + return; + } if (cursor_shape == p_shape) { return; } diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle index 81c7130c03..05608883d7 100644 --- a/platform/android/java/build.gradle +++ b/platform/android/java/build.gradle @@ -29,8 +29,13 @@ allprojects { ext { supportedAbis = ["arm32", "arm64", "x86_32", "x86_64"] - supportedTargetsMap = [release: "release", dev: "debug", debug: "release_debug"] supportedFlavors = ["editor", "template"] + supportedFlavorsBuildTypes = [ + // The editor can't be used with target=release as debugging tools are then not + // included, and it would crash on errors instead of reporting them. + "editor": ["dev", "debug"], + "template": ["dev", "debug", "release"] + ] // Used by gradle to specify which architecture to build for by default when running // `./gradlew build` (this command is usually used by Android Studio). @@ -88,7 +93,7 @@ task copyDebugAARToAppModule(type: Copy) { dependsOn ':lib:assembleTemplateDebug' from('lib/build/outputs/aar') into('app/libs/debug') - include('godot-lib.debug.aar') + include('godot-lib.template_debug.aar') } /** @@ -99,7 +104,7 @@ task copyDebugAARToBin(type: Copy) { dependsOn ':lib:assembleTemplateDebug' from('lib/build/outputs/aar') into(binDir) - include('godot-lib.debug.aar') + include('godot-lib.template_debug.aar') } /** @@ -110,7 +115,7 @@ task copyDevAARToAppModule(type: Copy) { dependsOn ':lib:assembleTemplateDev' from('lib/build/outputs/aar') into('app/libs/dev') - include('godot-lib.dev.aar') + include('godot-lib.template_debug.dev.aar') } /** @@ -121,7 +126,7 @@ task copyDevAARToBin(type: Copy) { dependsOn ':lib:assembleTemplateDev' from('lib/build/outputs/aar') into(binDir) - include('godot-lib.dev.aar') + include('godot-lib.template_debug.dev.aar') } /** @@ -132,7 +137,7 @@ task copyReleaseAARToAppModule(type: Copy) { dependsOn ':lib:assembleTemplateRelease' from('lib/build/outputs/aar') into('app/libs/release') - include('godot-lib.release.aar') + include('godot-lib.template_release.aar') } /** @@ -143,7 +148,7 @@ task copyReleaseAARToBin(type: Copy) { dependsOn ':lib:assembleTemplateRelease' from('lib/build/outputs/aar') into(binDir) - include('godot-lib.release.aar') + include('godot-lib.template_release.aar') } /** @@ -168,13 +173,8 @@ def templateExcludedBuildTask() { if (!isAndroidStudio()) { logger.lifecycle("Excluding Android studio build tasks") for (String flavor : supportedFlavors) { - for (String buildType : supportedTargetsMap.keySet()) { - if (buildType == "release" && flavor == "editor") { - // The editor can't be used with target=release as debugging tools are then not - // included, and it would crash on errors instead of reporting them. - continue - } - + String[] supportedBuildTypes = supportedFlavorsBuildTypes[flavor] + for (String buildType : supportedBuildTypes) { for (String abi : selectedAbis) { excludedTasks += ":lib:" + getSconsTaskName(flavor, buildType, abi) } @@ -188,7 +188,7 @@ def templateBuildTasks() { def tasks = [] // Only build the apks and aar files for which we have native shared libraries. - for (String target : supportedTargetsMap.keySet()) { + for (String target : supportedFlavorsBuildTypes["template"]) { File targetLibs = new File("lib/libs/" + target) if (targetLibs != null && targetLibs.isDirectory() @@ -240,12 +240,7 @@ task generateGodotEditor { def tasks = [] - for (String target : supportedTargetsMap.keySet()) { - if (target == "release") { - // The editor can't be used with target=release as debugging tools are then not - // included, and it would crash on errors instead of reporting them. - continue - } + for (String target : supportedFlavorsBuildTypes["editor"]) { File targetLibs = new File("lib/libs/tools/" + target) if (targetLibs != null && targetLibs.isDirectory() @@ -322,9 +317,9 @@ task cleanGodotTemplates(type: Delete) { delete("$binDir/android_dev.apk") delete("$binDir/android_release.apk") delete("$binDir/android_source.zip") - delete("$binDir/godot-lib.debug.aar") - delete("$binDir/godot-lib.dev.aar") - delete("$binDir/godot-lib.release.aar") + delete("$binDir/godot-lib.template_debug.aar") + delete("$binDir/godot-lib.template_debug.dev.aar") + delete("$binDir/godot-lib.template_release.aar") finalizedBy getTasksByName("clean", true) } diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index 318ae1143f..c9e2a5d7d2 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -100,25 +100,34 @@ android { throw new GradleException("Invalid product flavor: $flavorName") } - boolean toolsFlag = flavorName == "editor" - def buildType = variant.buildType.name - if (buildType == null || buildType == "" || !supportedTargetsMap.containsKey(buildType)) { + if (buildType == null || buildType == "" || !supportedFlavorsBuildTypes[flavorName].contains(buildType)) { throw new GradleException("Invalid build type: $buildType") } - def sconsTarget = supportedTargetsMap[buildType] - if (sconsTarget == null || sconsTarget == "") { - throw new GradleException("Invalid scons target: $sconsTarget") + boolean devBuild = buildType == "dev" + + def sconsTarget = flavorName + if (sconsTarget == "template") { + switch (buildType) { + case "release": + sconsTarget += "_release" + break + case "debug": + case "dev": + default: + sconsTarget += "_debug" + break; + } } // Update the name of the generated library - def outputSuffix = "${buildType}.aar" - if (toolsFlag) { - outputSuffix = "tools.$outputSuffix" + def outputSuffix = "${sconsTarget}" + if (devBuild) { + outputSuffix = "${outputSuffix}.dev" } variant.outputs.all { output -> - output.outputFileName = "godot-lib.${outputSuffix}" + output.outputFileName = "godot-lib.${outputSuffix}.aar" } // Find scons' executable path @@ -159,7 +168,7 @@ android { def taskName = getSconsTaskName(flavorName, buildType, selectedAbi) tasks.create(name: taskName, type: Exec) { executable sconsExecutableFile.absolutePath - args "--directory=${pathToRootDir}", "platform=android", "tools=${toolsFlag}", "target=${sconsTarget}", "arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors() + args "--directory=${pathToRootDir}", "platform=android", "dev_mode=${devBuild}", "dev_build=${devBuild}", "target=${sconsTarget}", "arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors() } // Schedule the tasks so the generated libs are present before the aar file is packaged. diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java index 513021f1d1..3dfc37f6b0 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java @@ -74,7 +74,6 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView private final Godot godot; private final GodotInputHandler inputHandler; private final GodotRenderer godotRenderer; - private PointerIcon pointerIcon; public GodotGLRenderView(Context context, Godot godot, XRMode xrMode, boolean p_use_debug_opengl) { super(context); @@ -84,7 +83,7 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView this.inputHandler = new GodotInputHandler(this); this.godotRenderer = new GodotRenderer(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - pointerIcon = PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT); + setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT)); } init(xrMode, false); } @@ -175,13 +174,16 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView @Keep public void setPointerIcon(int pointerType) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - pointerIcon = PointerIcon.getSystemIcon(getContext(), pointerType); + setPointerIcon(PointerIcon.getSystemIcon(getContext(), pointerType)); } } @Override public PointerIcon onResolvePointerIcon(MotionEvent me, int pointerIndex) { - return pointerIcon; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return getPointerIcon(); + } + return super.onResolvePointerIcon(me, pointerIndex); } private void init(XRMode xrMode, boolean translucent) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java index fa6c3280b9..0becf00d93 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java @@ -48,7 +48,6 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV private final Godot godot; private final GodotInputHandler mInputHandler; private final VkRenderer mRenderer; - private PointerIcon pointerIcon; public GodotVulkanRenderView(Context context, Godot godot) { super(context); @@ -57,7 +56,7 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV mInputHandler = new GodotInputHandler(this); mRenderer = new VkRenderer(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - pointerIcon = PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT); + setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT)); } setFocusableInTouchMode(true); startRenderer(mRenderer); @@ -149,13 +148,16 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV @Keep public void setPointerIcon(int pointerType) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - pointerIcon = PointerIcon.getSystemIcon(getContext(), pointerType); + setPointerIcon(PointerIcon.getSystemIcon(getContext(), pointerType)); } } @Override public PointerIcon onResolvePointerIcon(MotionEvent me, int pointerIndex) { - return pointerIcon; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return getPointerIcon(); + } + return super.onResolvePointerIcon(me, pointerIndex); } @Override diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java index c959b5f28c..01ad5ee415 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java @@ -122,7 +122,7 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene @Override public boolean onEditorAction(final TextView pTextView, final int pActionID, final KeyEvent pKeyEvent) { - if (mEdit == pTextView && isFullScreenEdit()) { + if (mEdit == pTextView && isFullScreenEdit() && pKeyEvent != null) { final String characters = pKeyEvent.getCharacters(); for (int i = 0; i < characters.length(); i++) { diff --git a/platform/android/java_godot_view_wrapper.cpp b/platform/android/java_godot_view_wrapper.cpp index 0153ba96fc..762840a4b1 100644 --- a/platform/android/java_godot_view_wrapper.cpp +++ b/platform/android/java_godot_view_wrapper.cpp @@ -40,13 +40,24 @@ GodotJavaViewWrapper::GodotJavaViewWrapper(jobject godot_view) { _cls = (jclass)env->NewGlobalRef(env->GetObjectClass(godot_view)); - if (android_get_device_api_level() >= __ANDROID_API_O__) { + int android_device_api_level = android_get_device_api_level(); + if (android_device_api_level >= __ANDROID_API_N__) { + _set_pointer_icon = env->GetMethodID(_cls, "setPointerIcon", "(I)V"); + } + if (android_device_api_level >= __ANDROID_API_O__) { _request_pointer_capture = env->GetMethodID(_cls, "requestPointerCapture", "()V"); _release_pointer_capture = env->GetMethodID(_cls, "releasePointerCapture", "()V"); - _set_pointer_icon = env->GetMethodID(_cls, "setPointerIcon", "(I)V"); } } +bool GodotJavaViewWrapper::can_update_pointer_icon() const { + return _set_pointer_icon != nullptr; +} + +bool GodotJavaViewWrapper::can_capture_pointer() const { + return _request_pointer_capture != nullptr && _release_pointer_capture != nullptr; +} + void GodotJavaViewWrapper::request_pointer_capture() { if (_request_pointer_capture != nullptr) { JNIEnv *env = get_jni_env(); diff --git a/platform/android/java_godot_view_wrapper.h b/platform/android/java_godot_view_wrapper.h index c52f459d64..b398c73cac 100644 --- a/platform/android/java_godot_view_wrapper.h +++ b/platform/android/java_godot_view_wrapper.h @@ -50,6 +50,9 @@ private: public: GodotJavaViewWrapper(jobject godot_view); + bool can_update_pointer_icon() const; + bool can_capture_pointer() const; + void request_pointer_capture(); void release_pointer_capture(); void set_pointer_icon(int pointer_type); diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index 07b0d75921..416b98c895 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -78,13 +78,23 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_ _on_godot_setup_completed = p_env->GetMethodID(godot_class, "onGodotSetupCompleted", "()V"); _on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V"); _create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)V"); + _get_render_view = p_env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotRenderView;"); // get some Activity method pointers... _get_class_loader = p_env->GetMethodID(activity_class, "getClassLoader", "()Ljava/lang/ClassLoader;"); } GodotJavaWrapper::~GodotJavaWrapper() { - // nothing to do here for now + if (godot_view) { + delete godot_view; + } + + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + env->DeleteGlobalRef(godot_instance); + env->DeleteGlobalRef(godot_class); + env->DeleteGlobalRef(activity); + env->DeleteGlobalRef(activity_class); } jobject GodotJavaWrapper::get_activity() { @@ -115,14 +125,18 @@ jobject GodotJavaWrapper::get_class_loader() { } GodotJavaViewWrapper *GodotJavaWrapper::get_godot_view() { - if (_godot_view != nullptr) { - return _godot_view; + if (godot_view != nullptr) { + return godot_view; } - JNIEnv *env = get_jni_env(); - ERR_FAIL_NULL_V(env, nullptr); - jmethodID godot_view_getter = env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotRenderView;"); - _godot_view = new GodotJavaViewWrapper(env->CallObjectMethod(godot_instance, godot_view_getter)); - return _godot_view; + if (_get_render_view) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, nullptr); + jobject godot_render_view = env->CallObjectMethod(godot_instance, _get_render_view); + if (!env->IsSameObject(godot_render_view, nullptr)) { + godot_view = new GodotJavaViewWrapper(godot_render_view); + } + } + return godot_view; } bool GodotJavaWrapper::on_video_init(JNIEnv *p_env) { diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index 0e96a4e1f3..fb9c4c77fc 100644 --- a/platform/android/java_godot_wrapper.h +++ b/platform/android/java_godot_wrapper.h @@ -46,7 +46,7 @@ private: jclass godot_class; jclass activity_class; - GodotJavaViewWrapper *_godot_view = nullptr; + GodotJavaViewWrapper *godot_view = nullptr; jmethodID _on_video_init = nullptr; jmethodID _restart = nullptr; @@ -69,6 +69,7 @@ private: jmethodID _on_godot_main_loop_started = nullptr; jmethodID _get_class_loader = nullptr; jmethodID _create_new_godot_instance = nullptr; + jmethodID _get_render_view = nullptr; public: GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance); diff --git a/platform/android/thread_jandroid.cpp b/platform/android/thread_jandroid.cpp index 61b471866f..9f87303341 100644 --- a/platform/android/thread_jandroid.cpp +++ b/platform/android/thread_jandroid.cpp @@ -63,7 +63,7 @@ static void term_thread() { void init_thread_jandroid(JavaVM *p_jvm, JNIEnv *p_env) { java_vm = p_jvm; env = p_env; - Thread::_set_platform_funcs(nullptr, nullptr, &init_thread, &term_thread); + Thread::_set_platform_functions({ .init = init_thread, .term = &term_thread }); } void setup_android_thread() { diff --git a/platform/linuxbsd/crash_handler_linuxbsd.cpp b/platform/linuxbsd/crash_handler_linuxbsd.cpp index 33da094860..8c8c8588b8 100644 --- a/platform/linuxbsd/crash_handler_linuxbsd.cpp +++ b/platform/linuxbsd/crash_handler_linuxbsd.cpp @@ -89,7 +89,7 @@ static void handle_crash(int sig) { // Try to demangle the function name to provide a more readable one if (dladdr(bt_buffer[i], &info) && info.dli_sname) { if (info.dli_sname[0] == '_') { - int status; + int status = 0; char *demangled = abi::__cxa_demangle(info.dli_sname, nullptr, nullptr, &status); if (status == 0 && demangled) { diff --git a/platform/linuxbsd/display_server_x11.cpp b/platform/linuxbsd/display_server_x11.cpp index 1ef1deb971..5256d91a80 100644 --- a/platform/linuxbsd/display_server_x11.cpp +++ b/platform/linuxbsd/display_server_x11.cpp @@ -3142,7 +3142,7 @@ void DisplayServerX11::_window_changed(XEvent *event) { { //the position in xconfigure is not useful here, obtain it manually - int x, y; + int x = 0, y = 0; Window child; XTranslateCoordinates(x11_display, wd.x11_window, DefaultRootWindow(x11_display), 0, 0, &x, &y, &child); new_rect.position.x = x; @@ -3305,7 +3305,7 @@ void DisplayServerX11::_check_pending_events(LocalVector<XEvent> &r_events) { XFlush(x11_display); // Non-blocking wait for next event and remove it from the queue. - XEvent ev; + 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)) { diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 9e958be26f..22a562c21c 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -1361,7 +1361,8 @@ void DisplayServerWindows::window_set_flag(WindowFlags p_flag, bool p_enabled, W if (p_enabled) { //enable per-pixel alpha - DWM_BLURBEHIND bb = { 0 }; + DWM_BLURBEHIND bb; + ZeroMemory(&bb, sizeof(bb)); HRGN hRgn = CreateRectRgn(0, 0, -1, -1); bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION; bb.hRgnBlur = hRgn; @@ -1373,7 +1374,8 @@ void DisplayServerWindows::window_set_flag(WindowFlags p_flag, bool p_enabled, W //disable per-pixel alpha wd.layered_window = false; - DWM_BLURBEHIND bb = { 0 }; + DWM_BLURBEHIND bb; + ZeroMemory(&bb, sizeof(bb)); HRGN hRgn = CreateRectRgn(0, 0, -1, -1); bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION; bb.hRgnBlur = hRgn; @@ -1390,7 +1392,7 @@ void DisplayServerWindows::window_set_flag(WindowFlags p_flag, bool p_enabled, W ERR_FAIL_COND_MSG(IsWindowVisible(wd.hWnd) && (wd.is_popup != p_enabled), "Popup flag can't changed while window is opened."); wd.is_popup = p_enabled; } break; - case WINDOW_FLAG_MAX: + default: break; } } @@ -1419,7 +1421,7 @@ bool DisplayServerWindows::window_get_flag(WindowFlags p_flag, WindowID p_window case WINDOW_FLAG_POPUP: { return wd.is_popup; } break; - case WINDOW_FLAG_MAX: + default: break; } diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index 1978ec5ab6..241e0b382e 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -295,11 +295,12 @@ String OS_Windows::get_distribution_name() const { } String OS_Windows::get_version() const { - typedef LONG NTSTATUS, *PNTSTATUS; + typedef LONG NTSTATUS; typedef NTSTATUS(WINAPI * RtlGetVersionPtr)(PRTL_OSVERSIONINFOW); RtlGetVersionPtr version_ptr = (RtlGetVersionPtr)GetProcAddress(GetModuleHandle("ntdll.dll"), "RtlGetVersion"); if (version_ptr != nullptr) { - RTL_OSVERSIONINFOW fow = { 0 }; + RTL_OSVERSIONINFOW fow; + ZeroMemory(&fow, sizeof(fow)); fow.dwOSVersionInfoSize = sizeof(fow); if (version_ptr(&fow) == 0x00000000) { return vformat("%d.%d.%d", (int64_t)fow.dwMajorVersion, (int64_t)fow.dwMinorVersion, (int64_t)fow.dwBuildNumber); @@ -870,17 +871,6 @@ BOOL is_wow64() { return wow64; } -int OS_Windows::get_processor_count() const { - SYSTEM_INFO sysinfo; - if (is_wow64()) { - GetNativeSystemInfo(&sysinfo); - } else { - GetSystemInfo(&sysinfo); - } - - return sysinfo.dwNumberOfProcessors; -} - String OS_Windows::get_processor_name() const { const String id = "Hardware\\Description\\System\\CentralProcessor\\0"; diff --git a/platform/windows/os_windows.h b/platform/windows/os_windows.h index 419871f8bf..b792f6fa44 100644 --- a/platform/windows/os_windows.h +++ b/platform/windows/os_windows.h @@ -175,7 +175,6 @@ public: virtual String get_locale() const override; - virtual int get_processor_count() const override; virtual String get_processor_name() const override; virtual uint64_t get_embedded_pck_offset() const override; diff --git a/scene/2d/cpu_particles_2d.h b/scene/2d/cpu_particles_2d.h index ea735411a8..141e5f9139 100644 --- a/scene/2d/cpu_particles_2d.h +++ b/scene/2d/cpu_particles_2d.h @@ -152,8 +152,8 @@ private: Vector2 direction = Vector2(1, 0); real_t spread = 45.0; - real_t parameters_min[PARAM_MAX]; - real_t parameters_max[PARAM_MAX]; + real_t parameters_min[PARAM_MAX] = {}; + real_t parameters_max[PARAM_MAX] = {}; Ref<Curve> curve_parameters[PARAM_MAX]; Color color; diff --git a/scene/2d/physics_body_2d.cpp b/scene/2d/physics_body_2d.cpp index 4e6b37a860..d0e35c2ea2 100644 --- a/scene/2d/physics_body_2d.cpp +++ b/scene/2d/physics_body_2d.cpp @@ -1134,6 +1134,7 @@ bool CharacterBody2D::move_and_slide() { if (!current_platform_velocity.is_zero_approx()) { PhysicsServer2D::MotionParameters parameters(get_global_transform(), current_platform_velocity * delta, margin); + parameters.recovery_as_collision = true; // Also report collisions generated only from recovery. parameters.exclude_bodies.insert(platform_rid); if (platform_object_id.is_valid()) { parameters.exclude_objects.insert(platform_object_id); @@ -1192,6 +1193,7 @@ void CharacterBody2D::_move_and_slide_grounded(double p_delta, bool p_was_on_flo for (int iteration = 0; iteration < max_slides; ++iteration) { PhysicsServer2D::MotionParameters parameters(get_global_transform(), motion, margin); + parameters.recovery_as_collision = true; // Also report collisions generated only from recovery. Vector2 prev_position = parameters.from.columns[2]; @@ -1348,6 +1350,7 @@ void CharacterBody2D::_move_and_slide_floating(double p_delta) { bool first_slide = true; for (int iteration = 0; iteration < max_slides; ++iteration) { PhysicsServer2D::MotionParameters parameters(get_global_transform(), motion, margin); + parameters.recovery_as_collision = true; // Also report collisions generated only from recovery. PhysicsServer2D::MotionResult result; bool collided = move_and_collide(parameters, result, false, false); @@ -1394,7 +1397,7 @@ void CharacterBody2D::_snap_on_floor(bool p_was_on_floor, bool p_vel_dir_facing_ real_t length = MAX(floor_snap_length, margin); PhysicsServer2D::MotionParameters parameters(get_global_transform(), -up_direction * length, margin); - parameters.recovery_as_collision = true; // Report margin recovery as collision to improve floor detection. + parameters.recovery_as_collision = true; // Also report collisions generated only from recovery. parameters.collide_separation_ray = true; PhysicsServer2D::MotionResult result; @@ -1430,7 +1433,7 @@ bool CharacterBody2D::_on_floor_if_snapped(bool p_was_on_floor, bool p_vel_dir_f real_t length = MAX(floor_snap_length, margin); PhysicsServer2D::MotionParameters parameters(get_global_transform(), -up_direction * length, margin); - parameters.recovery_as_collision = true; // Report margin recovery as collision to improve floor detection. + parameters.recovery_as_collision = true; // Also report collisions generated only from recovery. parameters.collide_separation_ray = true; PhysicsServer2D::MotionResult result; diff --git a/scene/3d/cpu_particles_3d.h b/scene/3d/cpu_particles_3d.h index 26c702172b..6b70137736 100644 --- a/scene/3d/cpu_particles_3d.h +++ b/scene/3d/cpu_particles_3d.h @@ -156,7 +156,7 @@ private: real_t spread = 45.0; real_t flatness = 0.0; - real_t parameters_min[PARAM_MAX]; + real_t parameters_min[PARAM_MAX] = {}; real_t parameters_max[PARAM_MAX] = {}; Ref<Curve> curve_parameters[PARAM_MAX]; diff --git a/scene/3d/physics_body_3d.cpp b/scene/3d/physics_body_3d.cpp index 594e94644c..91f58b3976 100644 --- a/scene/3d/physics_body_3d.cpp +++ b/scene/3d/physics_body_3d.cpp @@ -1200,6 +1200,7 @@ bool CharacterBody3D::move_and_slide() { if (!current_platform_velocity.is_zero_approx()) { PhysicsServer3D::MotionParameters parameters(get_global_transform(), current_platform_velocity * delta, margin); + parameters.recovery_as_collision = true; // Also report collisions generated only from recovery. parameters.exclude_bodies.insert(platform_rid); if (platform_object_id.is_valid()) { @@ -1264,6 +1265,7 @@ void CharacterBody3D::_move_and_slide_grounded(double p_delta, bool p_was_on_flo for (int iteration = 0; iteration < max_slides; ++iteration) { PhysicsServer3D::MotionParameters parameters(get_global_transform(), motion, margin); parameters.max_collisions = 6; // There can be 4 collisions between 2 walls + 2 more for the floor. + parameters.recovery_as_collision = true; // Also report collisions generated only from recovery. PhysicsServer3D::MotionResult result; bool collided = move_and_collide(parameters, result, false, !sliding_enabled); @@ -1508,6 +1510,7 @@ void CharacterBody3D::_move_and_slide_floating(double p_delta) { bool first_slide = true; for (int iteration = 0; iteration < max_slides; ++iteration) { PhysicsServer3D::MotionParameters parameters(get_global_transform(), motion, margin); + parameters.recovery_as_collision = true; // Also report collisions generated only from recovery. PhysicsServer3D::MotionResult result; bool collided = move_and_collide(parameters, result, false, false); @@ -1562,7 +1565,7 @@ void CharacterBody3D::_snap_on_floor(bool p_was_on_floor, bool p_vel_dir_facing_ PhysicsServer3D::MotionParameters parameters(get_global_transform(), -up_direction * length, margin); parameters.max_collisions = 4; - parameters.recovery_as_collision = true; // Report margin recovery as collision to improve floor detection. + parameters.recovery_as_collision = true; // Also report collisions generated only from recovery. parameters.collide_separation_ray = true; PhysicsServer3D::MotionResult result; @@ -1598,7 +1601,7 @@ bool CharacterBody3D::_on_floor_if_snapped(bool p_was_on_floor, bool p_vel_dir_f PhysicsServer3D::MotionParameters parameters(get_global_transform(), -up_direction * length, margin); parameters.max_collisions = 4; - parameters.recovery_as_collision = true; // Report margin recovery as collision to improve floor detection. + parameters.recovery_as_collision = true; // Also report collisions generated only from recovery. parameters.collide_separation_ray = true; PhysicsServer3D::MotionResult result; diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index 8069ab465b..94d296977b 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -609,72 +609,74 @@ Control::CursorShape CodeEdit::get_cursor_shape(const Point2 &p_pos) const { /* Text manipulation */ // Overridable actions -void CodeEdit::_handle_unicode_input_internal(const uint32_t p_unicode) { - bool had_selection = has_selection(); - String selection_text = (had_selection ? get_selected_text() : ""); +void CodeEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_caret) { + start_action(EditAction::ACTION_TYPING); + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (const int &i : caret_edit_order) { + if (p_caret != -1 && p_caret != i) { + continue; + } - if (had_selection) { - begin_complex_operation(); - delete_selection(); - } + bool had_selection = has_selection(i); + String selection_text = (had_selection ? get_selected_text(i) : ""); - // Remove the old character if in overtype mode and no selection. - if (is_overtype_mode_enabled() && !had_selection) { - begin_complex_operation(); + if (had_selection) { + delete_selection(i); + } - /* Make sure we don't try and remove empty space. */ - if (get_caret_column() < get_line(get_caret_line()).length()) { - remove_text(get_caret_line(), get_caret_column(), get_caret_line(), get_caret_column() + 1); + // Remove the old character if in overtype mode and no selection. + if (is_overtype_mode_enabled() && !had_selection) { + // Make sure we don't try and remove empty space. + if (get_caret_column(i) < get_line(get_caret_line(i)).length()) { + remove_text(get_caret_line(i), get_caret_column(i), get_caret_line(i), get_caret_column(i) + 1); + } } - } - const char32_t chr[2] = { (char32_t)p_unicode, 0 }; + const char32_t chr[2] = { (char32_t)p_unicode, 0 }; - if (auto_brace_completion_enabled) { - int cl = get_caret_line(); - int cc = get_caret_column(); + if (auto_brace_completion_enabled) { + int cl = get_caret_line(i); + int cc = get_caret_column(i); - if (had_selection) { - insert_text_at_caret(chr); + if (had_selection) { + insert_text_at_caret(chr, i); - String close_key = get_auto_brace_completion_close_key(chr); - if (!close_key.is_empty()) { - insert_text_at_caret(selection_text + close_key); - set_caret_column(get_caret_column() - 1); - } - } else { - int caret_move_offset = 1; - - int post_brace_pair = cc < get_line(cl).length() ? _get_auto_brace_pair_close_at_pos(cl, cc) : -1; - - if (has_string_delimiter(chr) && cc > 0 && !is_symbol(get_line(cl)[cc - 1]) && post_brace_pair == -1) { - insert_text_at_caret(chr); - } else if (cc < get_line(cl).length() && !is_symbol(get_line(cl)[cc])) { - insert_text_at_caret(chr); - } else if (post_brace_pair != -1 && auto_brace_completion_pairs[post_brace_pair].close_key[0] == chr[0]) { - caret_move_offset = auto_brace_completion_pairs[post_brace_pair].close_key.length(); - } else if (is_in_comment(cl, cc) != -1 || (is_in_string(cl, cc) != -1 && has_string_delimiter(chr))) { - insert_text_at_caret(chr); + String close_key = get_auto_brace_completion_close_key(chr); + if (!close_key.is_empty()) { + insert_text_at_caret(selection_text + close_key, i); + set_caret_column(get_caret_column(i) - 1, i == 0, i); + } } else { - insert_text_at_caret(chr); + int caret_move_offset = 1; + + int post_brace_pair = cc < get_line(cl).length() ? _get_auto_brace_pair_close_at_pos(cl, cc) : -1; + + if (has_string_delimiter(chr) && cc > 0 && !is_symbol(get_line(cl)[cc - 1]) && post_brace_pair == -1) { + insert_text_at_caret(chr, i); + } else if (cc < get_line(cl).length() && !is_symbol(get_line(cl)[cc])) { + insert_text_at_caret(chr, i); + } else if (post_brace_pair != -1 && auto_brace_completion_pairs[post_brace_pair].close_key[0] == chr[0]) { + caret_move_offset = auto_brace_completion_pairs[post_brace_pair].close_key.length(); + } else if (is_in_comment(cl, cc) != -1 || (is_in_string(cl, cc) != -1 && has_string_delimiter(chr))) { + insert_text_at_caret(chr, i); + } else { + insert_text_at_caret(chr, i); - int pre_brace_pair = _get_auto_brace_pair_open_at_pos(cl, cc + 1); - if (pre_brace_pair != -1) { - insert_text_at_caret(auto_brace_completion_pairs[pre_brace_pair].close_key); + int pre_brace_pair = _get_auto_brace_pair_open_at_pos(cl, cc + 1); + if (pre_brace_pair != -1) { + insert_text_at_caret(auto_brace_completion_pairs[pre_brace_pair].close_key, i); + } } + set_caret_column(cc + caret_move_offset, i == 0, i); } - set_caret_column(cc + caret_move_offset); + } else { + insert_text_at_caret(chr, i); } - } else { - insert_text_at_caret(chr); - } - - if ((is_overtype_mode_enabled() && !had_selection) || (had_selection)) { - end_complex_operation(); } + end_action(); } -void CodeEdit::_backspace_internal() { +void CodeEdit::_backspace_internal(int p_caret) { if (!is_editable()) { return; } @@ -684,51 +686,65 @@ void CodeEdit::_backspace_internal() { return; } - int cc = get_caret_column(); - int cl = get_caret_line(); + begin_complex_operation(); + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (const int &i : caret_edit_order) { + if (p_caret != -1 && p_caret != i) { + continue; + } - if (cc == 0 && cl == 0) { - return; - } + int cc = get_caret_column(i); + int cl = get_caret_line(i); - if (cl > 0 && _is_line_hidden(cl - 1)) { - unfold_line(get_caret_line() - 1); - } + if (cc == 0 && cl == 0) { + continue; + } - int prev_line = cc ? cl : cl - 1; - int prev_column = cc ? (cc - 1) : (get_line(cl - 1).length()); + if (cl > 0 && _is_line_hidden(cl - 1)) { + unfold_line(get_caret_line(i) - 1); + } - merge_gutters(prev_line, cl); + int prev_line = cc ? cl : cl - 1; + int prev_column = cc ? (cc - 1) : (get_line(cl - 1).length()); - if (auto_brace_completion_enabled && cc > 0) { - int idx = _get_auto_brace_pair_open_at_pos(cl, cc); - if (idx != -1) { - prev_column = cc - auto_brace_completion_pairs[idx].open_key.length(); + merge_gutters(prev_line, cl); - if (_get_auto_brace_pair_close_at_pos(cl, cc) == idx) { - remove_text(prev_line, prev_column, cl, cc + auto_brace_completion_pairs[idx].close_key.length()); - } else { - remove_text(prev_line, prev_column, cl, cc); + if (auto_brace_completion_enabled && cc > 0) { + int idx = _get_auto_brace_pair_open_at_pos(cl, cc); + if (idx != -1) { + prev_column = cc - auto_brace_completion_pairs[idx].open_key.length(); + + if (_get_auto_brace_pair_close_at_pos(cl, cc) == idx) { + remove_text(prev_line, prev_column, cl, cc + auto_brace_completion_pairs[idx].close_key.length()); + } else { + remove_text(prev_line, prev_column, cl, cc); + } + set_caret_line(prev_line, false, true, 0, i); + set_caret_column(prev_column, i == 0, i); + + adjust_carets_after_edit(i, prev_line, prev_column, cl, cc + auto_brace_completion_pairs[idx].close_key.length()); + continue; } - set_caret_line(prev_line, false, true); - set_caret_column(prev_column); - return; } - } - // For space indentation we need to do a simple unindent if there are no chars to the left, acting in the - // same way as tabs. - if (indent_using_spaces && cc != 0) { - if (get_first_non_whitespace_column(cl) >= cc) { - prev_column = cc - _calculate_spaces_till_next_left_indent(cc); - prev_line = cl; + // For space indentation we need to do a simple unindent if there are no chars to the left, acting in the + // same way as tabs. + if (indent_using_spaces && cc != 0) { + if (get_first_non_whitespace_column(cl) >= cc) { + prev_column = cc - _calculate_spaces_till_next_left_indent(cc); + prev_line = cl; + } } - } - remove_text(prev_line, prev_column, cl, cc); + remove_text(prev_line, prev_column, cl, cc); + + set_caret_line(prev_line, false, true, 0, i); + set_caret_column(prev_column, i == 0, i); - set_caret_line(prev_line, false, true); - set_caret_column(prev_column); + adjust_carets_after_edit(i, prev_line, prev_column, cl, cc); + } + merge_overlapping_carets(); + end_complex_operation(); } /* Indent management */ @@ -803,10 +819,15 @@ void CodeEdit::do_indent() { return; } - int spaces_to_add = _calculate_spaces_till_next_right_indent(get_caret_column()); - if (spaces_to_add > 0) { - insert_text_at_caret(String(" ").repeat(spaces_to_add)); + begin_complex_operation(); + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (const int &i : caret_edit_order) { + int spaces_to_add = _calculate_spaces_till_next_right_indent(get_caret_column(i)); + if (spaces_to_add > 0) { + insert_text_at_caret(String(" ").repeat(spaces_to_add), i); + } } + end_complex_operation(); } void CodeEdit::indent_lines() { @@ -815,48 +836,49 @@ void CodeEdit::indent_lines() { } begin_complex_operation(); + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (const int &c : caret_edit_order) { + // This value informs us by how much we changed selection position by indenting right. + // Default is 1 for tab indentation. + int selection_offset = 1; + + int start_line = get_caret_line(c); + int end_line = start_line; + if (has_selection(c)) { + start_line = get_selection_from_line(c); + end_line = get_selection_to_line(c); + + // Ignore the last line if the selection is not past the first column. + if (get_selection_to_column(c) == 0) { + selection_offset = 0; + end_line--; + } + } - /* This value informs us by how much we changed selection position by indenting right. */ - /* Default is 1 for tab indentation. */ - int selection_offset = 1; - - int start_line = get_caret_line(); - int end_line = start_line; - if (has_selection()) { - start_line = get_selection_from_line(); - end_line = get_selection_to_line(); + for (int i = start_line; i <= end_line; i++) { + const String line_text = get_line(i); + if (line_text.size() == 0 && has_selection(c)) { + continue; + } - /* Ignore the last line if the selection is not past the first column. */ - if (get_selection_to_column() == 0) { - selection_offset = 0; - end_line--; - } - } + if (!indent_using_spaces) { + set_line(i, '\t' + line_text); + continue; + } - for (int i = start_line; i <= end_line; i++) { - const String line_text = get_line(i); - if (line_text.size() == 0 && has_selection()) { - continue; + // We don't really care where selection is - we just need to know indentation level at the beginning of the line. + // Since we will add this many spaces, we want to move the whole selection and caret by this much. + int spaces_to_add = _calculate_spaces_till_next_right_indent(get_first_non_whitespace_column(i)); + set_line(i, String(" ").repeat(spaces_to_add) + line_text); + selection_offset = spaces_to_add; } - if (!indent_using_spaces) { - set_line(i, '\t' + line_text); - continue; + // Fix selection and caret being off after shifting selection right. + if (has_selection(c)) { + select(start_line, get_selection_from_column(c) + selection_offset, get_selection_to_line(c), get_selection_to_column(c) + selection_offset, c); } - - /* We don't really care where selection is - we just need to know indentation level at the beginning of the line. */ - /* Since we will add this many spaces, we want to move the whole selection and caret by this much. */ - int spaces_to_add = _calculate_spaces_till_next_right_indent(get_first_non_whitespace_column(i)); - set_line(i, String(" ").repeat(spaces_to_add) + line_text); - selection_offset = spaces_to_add; + set_caret_column(get_caret_column(c) + selection_offset, false, c); } - - /* Fix selection and caret being off after shifting selection right.*/ - if (has_selection()) { - select(start_line, get_selection_from_column() + selection_offset, get_selection_to_line(), get_selection_to_column() + selection_offset); - } - set_caret_column(get_caret_column() + selection_offset, false); - end_complex_operation(); } @@ -872,30 +894,36 @@ void CodeEdit::do_unindent() { return; } - int cl = get_caret_line(); - const String &line = get_line(cl); - - if (line[cc - 1] == '\t') { - remove_text(cl, cc - 1, cl, cc); - set_caret_column(MAX(0, cc - 1)); - return; - } + begin_complex_operation(); + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (const int &c : caret_edit_order) { + int cl = get_caret_line(c); + const String &line = get_line(cl); + + if (line[cc - 1] == '\t') { + remove_text(cl, cc - 1, cl, cc); + set_caret_column(MAX(0, cc - 1), c == 0, c); + adjust_carets_after_edit(c, cl, cc, cl, cc - 1); + continue; + } - if (line[cc - 1] != ' ') { - return; - } + if (line[cc - 1] != ' ') { + continue; + } - int spaces_to_remove = _calculate_spaces_till_next_left_indent(cc); - if (spaces_to_remove > 0) { - for (int i = 1; i <= spaces_to_remove; i++) { - if (line[cc - i] != ' ') { - spaces_to_remove = i - 1; - break; + int spaces_to_remove = _calculate_spaces_till_next_left_indent(cc); + if (spaces_to_remove > 0) { + for (int i = 1; i <= spaces_to_remove; i++) { + if (line[cc - i] != ' ') { + spaces_to_remove = i - 1; + break; + } } + remove_text(cl, cc - spaces_to_remove, cl, cc); + set_caret_column(MAX(0, cc - spaces_to_remove), c == 0, c); } - remove_text(cl, cc - spaces_to_remove, cl, cc); - set_caret_column(MAX(0, cc - spaces_to_remove)); } + end_complex_operation(); } void CodeEdit::unindent_lines() { @@ -905,71 +933,73 @@ void CodeEdit::unindent_lines() { begin_complex_operation(); - /* Moving caret and selection after unindenting can get tricky because */ - /* changing content of line can move caret and selection on its own (if new line ends before previous position of either), */ - /* therefore we just remember initial values and at the end of the operation offset them by number of removed characters. */ - int removed_characters = 0; - int initial_selection_end_column = 0; - int initial_cursor_column = get_caret_column(); - - int start_line = get_caret_line(); - int end_line = start_line; - if (has_selection()) { - start_line = get_selection_from_line(); - end_line = get_selection_to_line(); - - /* Ignore the last line if the selection is not past the first column. */ - initial_selection_end_column = get_selection_to_column(); - if (initial_selection_end_column == 0) { - end_line--; + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (const int &c : caret_edit_order) { + // Moving caret and selection after unindenting can get tricky because + // changing content of line can move caret and selection on its own (if new line ends before previous position of either) + // therefore we just remember initial values and at the end of the operation offset them by number of removed characters. + int removed_characters = 0; + int initial_selection_end_column = 0; + int initial_cursor_column = get_caret_column(c); + + int start_line = get_caret_line(c); + int end_line = start_line; + if (has_selection(c)) { + start_line = get_selection_from_line(c); + end_line = get_selection_to_line(c); + + // Ignore the last line if the selection is not past the first column. + initial_selection_end_column = get_selection_to_column(c); + if (initial_selection_end_column == 0) { + end_line--; + } } - } - bool first_line_edited = false; - bool last_line_edited = false; + bool first_line_edited = false; + bool last_line_edited = false; - for (int i = start_line; i <= end_line; i++) { - String line_text = get_line(i); + for (int i = start_line; i <= end_line; i++) { + String line_text = get_line(i); - if (line_text.begins_with("\t")) { - line_text = line_text.substr(1, line_text.length()); + if (line_text.begins_with("\t")) { + line_text = line_text.substr(1, line_text.length()); - set_line(i, line_text); - removed_characters = 1; + set_line(i, line_text); + removed_characters = 1; - first_line_edited = (i == start_line) ? true : first_line_edited; - last_line_edited = (i == end_line) ? true : last_line_edited; - continue; - } + first_line_edited = (i == start_line) ? true : first_line_edited; + last_line_edited = (i == end_line) ? true : last_line_edited; + continue; + } - if (line_text.begins_with(" ")) { - /* When unindenting we aim to remove spaces before line that has selection no matter what is selected, */ - /* Here we remove only enough spaces to align text to nearest full multiple of indentation_size. */ - /* In case where selection begins at the start of indentation_size multiple we remove whole indentation level. */ - int spaces_to_remove = _calculate_spaces_till_next_left_indent(get_first_non_whitespace_column(i)); - line_text = line_text.substr(spaces_to_remove, line_text.length()); + if (line_text.begins_with(" ")) { + // When unindenting we aim to remove spaces before line that has selection no matter what is selected. + // Here we remove only enough spaces to align text to nearest full multiple of indentation_size. + // In case where selection begins at the start of indentation_size multiple we remove whole indentation level. + int spaces_to_remove = _calculate_spaces_till_next_left_indent(get_first_non_whitespace_column(i)); + line_text = line_text.substr(spaces_to_remove, line_text.length()); - set_line(i, line_text); - removed_characters = spaces_to_remove; + set_line(i, line_text); + removed_characters = spaces_to_remove; - first_line_edited = (i == start_line) ? true : first_line_edited; - last_line_edited = (i == end_line) ? true : last_line_edited; + first_line_edited = (i == start_line) ? true : first_line_edited; + last_line_edited = (i == end_line) ? true : last_line_edited; + } } - } - if (has_selection()) { - /* Fix selection being off by one on the first line. */ - if (first_line_edited) { - select(get_selection_from_line(), get_selection_from_column() - removed_characters, get_selection_to_line(), initial_selection_end_column); - } + if (has_selection(c)) { + // Fix selection being off by one on the first line. + if (first_line_edited) { + select(get_selection_from_line(c), get_selection_from_column(c) - removed_characters, get_selection_to_line(c), initial_selection_end_column, c); + } - /* Fix selection being off by one on the last line. */ - if (last_line_edited) { - select(get_selection_from_line(), get_selection_from_column(), get_selection_to_line(), initial_selection_end_column - removed_characters); + // Fix selection being off by one on the last line. + if (last_line_edited) { + select(get_selection_from_line(c), get_selection_from_column(c), get_selection_to_line(c), initial_selection_end_column - removed_characters, c); + } } + set_caret_column(initial_cursor_column - removed_characters, false, c); } - set_caret_column(initial_cursor_column - removed_characters, false); - end_complex_operation(); } @@ -990,106 +1020,108 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) { return; } - /* When not splitting the line, we need to factor in indentation from the end of the current line. */ - const int cc = p_split_current_line ? get_caret_column() : get_line(get_caret_line()).length(); - const int cl = get_caret_line(); - - const String line = get_line(cl); - - String ins = "\n"; + begin_complex_operation(); + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (const int &i : caret_edit_order) { + // When not splitting the line, we need to factor in indentation from the end of the current line. + const int cc = p_split_current_line ? get_caret_column(i) : get_line(get_caret_line(i)).length(); + const int cl = get_caret_line(i); - /* Append current indentation. */ - int space_count = 0; - int line_col = 0; - for (; line_col < cc; line_col++) { - if (line[line_col] == '\t') { - ins += indent_text; - space_count = 0; - continue; - } + const String line = get_line(cl); - if (line[line_col] == ' ') { - space_count++; + String ins = "\n"; - if (space_count == indent_size) { + // Append current indentation. + int space_count = 0; + int line_col = 0; + for (; line_col < cc; line_col++) { + if (line[line_col] == '\t') { ins += indent_text; space_count = 0; + continue; } - continue; - } - break; - } - - if (is_line_folded(cl)) { - unfold_line(cl); - } - /* Indent once again if the previous line needs it, ie ':'. */ - /* Then add an addition new line for any closing pairs aka '()'. */ - /* Skip this in comments or if we are going above. */ - bool brace_indent = false; - if (auto_indent && !p_above && cc > 0 && is_in_comment(cl) == -1) { - bool should_indent = false; - char32_t indent_char = ' '; + if (line[line_col] == ' ') { + space_count++; - for (; line_col < cc; line_col++) { - char32_t c = line[line_col]; - if (auto_indent_prefixes.has(c)) { - should_indent = true; - indent_char = c; + if (space_count == indent_size) { + ins += indent_text; + space_count = 0; + } continue; } + break; + } - /* Make sure this is the last char, trailing whitespace or comments are okay. */ - /* Increment column for comments because the delimiter (#) should be ignored. */ - if (should_indent && (!is_whitespace(c) && is_in_comment(cl, line_col + 1) == -1)) { - should_indent = false; - } + if (is_line_folded(cl)) { + unfold_line(cl); } - if (should_indent) { - ins += indent_text; + // Indent once again if the previous line needs it, ie ':'. + // Then add an addition new line for any closing pairs aka '()'. + // Skip this in comments or if we are going above. + bool brace_indent = false; + if (auto_indent && !p_above && cc > 0 && is_in_comment(cl) == -1) { + bool should_indent = false; + char32_t indent_char = ' '; - String closing_pair = get_auto_brace_completion_close_key(String::chr(indent_char)); - if (!closing_pair.is_empty() && line.find(closing_pair, cc) == cc) { - /* No need to move the brace below if we are not taking the text with us. */ - if (p_split_current_line) { - brace_indent = true; - ins += "\n" + ins.substr(indent_text.size(), ins.length() - 2); - } else { - brace_indent = false; - ins = "\n" + ins.substr(indent_text.size(), ins.length() - 2); + for (; line_col < cc; line_col++) { + char32_t c = line[line_col]; + if (auto_indent_prefixes.has(c)) { + should_indent = true; + indent_char = c; + continue; + } + + // Make sure this is the last char, trailing whitespace or comments are okay. + // Increment column for comments because the delimiter (#) should be ignored. + if (should_indent && (!is_whitespace(c) && is_in_comment(cl, line_col + 1) == -1)) { + should_indent = false; } } - } - } - begin_complex_operation(); + if (should_indent) { + ins += indent_text; + + String closing_pair = get_auto_brace_completion_close_key(String::chr(indent_char)); + if (!closing_pair.is_empty() && line.find(closing_pair, cc) == cc) { + // No need to move the brace below if we are not taking the text with us. + if (p_split_current_line) { + brace_indent = true; + ins += "\n" + ins.substr(indent_text.size(), ins.length() - 2); + } else { + brace_indent = false; + ins = "\n" + ins.substr(indent_text.size(), ins.length() - 2); + } + } + } + } - bool first_line = false; - if (!p_split_current_line) { - deselect(); + bool first_line = false; + if (!p_split_current_line) { + deselect(i); - if (p_above) { - if (cl > 0) { - set_caret_line(cl - 1, false); - set_caret_column(get_line(get_caret_line()).length()); + if (p_above) { + if (cl > 0) { + set_caret_line(cl - 1, false, true, 0, i); + set_caret_column(get_line(get_caret_line(i)).length(), i == 0, i); + } else { + set_caret_column(0, i == 0, i); + first_line = true; + } } else { - set_caret_column(0); - first_line = true; + set_caret_column(line.length(), i == 0, i); } - } else { - set_caret_column(line.length()); } - } - insert_text_at_caret(ins); + insert_text_at_caret(ins, i); - if (first_line) { - set_caret_line(0); - } else if (brace_indent) { - set_caret_line(get_caret_line() - 1, false); - set_caret_column(get_line(get_caret_line()).length()); + if (first_line) { + set_caret_line(0, i == 0, true, 0, i); + } else if (brace_indent) { + set_caret_line(get_caret_line(i) - 1, false, true, 0, i); + set_caret_column(get_line(get_caret_line(i)).length(), i == 0, i); + } } end_complex_operation(); @@ -1522,22 +1554,26 @@ void CodeEdit::fold_line(int p_line) { _set_line_as_hidden(i, true); } - /* Fix selection. */ - if (has_selection()) { - if (_is_line_hidden(get_selection_from_line()) && _is_line_hidden(get_selection_to_line())) { - deselect(); - } else if (_is_line_hidden(get_selection_from_line())) { - select(p_line, 9999, get_selection_to_line(), get_selection_to_column()); - } else if (_is_line_hidden(get_selection_to_line())) { - select(get_selection_from_line(), get_selection_from_column(), p_line, 9999); + for (int i = 0; i < get_caret_count(); i++) { + // Fix selection. + if (has_selection(i)) { + if (_is_line_hidden(get_selection_from_line(i)) && _is_line_hidden(get_selection_to_line(i))) { + deselect(i); + } else if (_is_line_hidden(get_selection_from_line(i))) { + select(p_line, 9999, get_selection_to_line(i), get_selection_to_column(i), i); + } else if (_is_line_hidden(get_selection_to_line(i))) { + select(get_selection_from_line(i), get_selection_from_column(i), p_line, 9999, i); + } } - } - /* Reset caret. */ - if (_is_line_hidden(get_caret_line())) { - set_caret_line(p_line, false, false); - set_caret_column(get_line(p_line).length(), false); + // Reset caret. + if (_is_line_hidden(get_caret_line(i))) { + set_caret_line(p_line, false, false, 0, i); + set_caret_column(get_line(p_line).length(), false, i); + } } + + merge_overlapping_carets(); queue_redraw(); } @@ -1950,98 +1986,108 @@ void CodeEdit::confirm_code_completion(bool p_replace) { return; } + char32_t caret_last_completion_char; begin_complex_operation(); + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (const int &i : caret_edit_order) { + int caret_line = get_caret_line(i); + + const String &insert_text = code_completion_options[code_completion_current_selected].insert_text; + const String &display_text = code_completion_options[code_completion_current_selected].display; + + if (p_replace) { + // Find end of current section. + const String line = get_line(caret_line); + int caret_col = get_caret_column(i); + int caret_remove_line = caret_line; + + bool merge_text = true; + int in_string = is_in_string(caret_line, caret_col); + if (in_string != -1) { + Point2 string_end = get_delimiter_end_position(caret_line, caret_col); + if (string_end.x != -1) { + merge_text = false; + caret_remove_line = string_end.y; + caret_col = string_end.x - 1; + } + } - int caret_line = get_caret_line(); - - const String &insert_text = code_completion_options[code_completion_current_selected].insert_text; - const String &display_text = code_completion_options[code_completion_current_selected].display; - - if (p_replace) { - /* Find end of current section */ - const String line = get_line(caret_line); - int caret_col = get_caret_column(); - int caret_remove_line = caret_line; - - bool merge_text = true; - int in_string = is_in_string(caret_line, caret_col); - if (in_string != -1) { - Point2 string_end = get_delimiter_end_position(caret_line, caret_col); - if (string_end.x != -1) { - merge_text = false; - caret_remove_line = string_end.y; - caret_col = string_end.x - 1; + if (merge_text) { + for (; caret_col < line.length(); caret_col++) { + if (is_symbol(line[caret_col])) { + break; + } + } } - } - if (merge_text) { - for (; caret_col < line.length(); caret_col++) { - if (is_symbol(line[caret_col])) { + // Replace. + remove_text(caret_line, get_caret_column(i) - code_completion_base.length(), caret_remove_line, caret_col); + adjust_carets_after_edit(i, caret_line, caret_col - code_completion_base.length(), caret_remove_line, caret_col); + set_caret_column(get_caret_column(i) - code_completion_base.length(), false, i); + insert_text_at_caret(insert_text, i); + } else { + // Get first non-matching char. + const String line = get_line(caret_line); + int caret_col = get_caret_column(i); + int matching_chars = code_completion_base.length(); + for (; matching_chars <= insert_text.length(); matching_chars++) { + if (caret_col >= line.length() || line[caret_col] != insert_text[matching_chars]) { break; } + caret_col++; } + + // Remove base completion text. + remove_text(caret_line, get_caret_column(i) - code_completion_base.length(), caret_line, get_caret_column(i)); + adjust_carets_after_edit(i, caret_line, get_caret_column(i) - code_completion_base.length(), caret_line, get_caret_column(i)); + set_caret_column(get_caret_column(i) - code_completion_base.length(), false, i); + + // Merge with text. + insert_text_at_caret(insert_text.substr(0, code_completion_base.length()), i); + set_caret_column(caret_col, false, i); + insert_text_at_caret(insert_text.substr(matching_chars), i); } - /* Replace. */ - remove_text(caret_line, get_caret_column() - code_completion_base.length(), caret_remove_line, caret_col); - set_caret_column(get_caret_column() - code_completion_base.length(), false); - insert_text_at_caret(insert_text); - } else { - /* Get first non-matching char. */ + //* Handle merging of symbols eg strings, brackets. const String line = get_line(caret_line); - int caret_col = get_caret_column(); - int matching_chars = code_completion_base.length(); - for (; matching_chars <= insert_text.length(); matching_chars++) { - if (caret_col >= line.length() || line[caret_col] != insert_text[matching_chars]) { - break; - } - caret_col++; + char32_t next_char = line[get_caret_column(i)]; + char32_t last_completion_char = insert_text[insert_text.length() - 1]; + if (i == 0) { + caret_last_completion_char = last_completion_char; } + char32_t last_completion_char_display = display_text[display_text.length() - 1]; - /* Remove base completion text. */ - remove_text(caret_line, get_caret_column() - code_completion_base.length(), caret_line, get_caret_column()); - set_caret_column(get_caret_column() - code_completion_base.length(), false); - - /* Merge with text. */ - insert_text_at_caret(insert_text.substr(0, code_completion_base.length())); - set_caret_column(caret_col, false); - insert_text_at_caret(insert_text.substr(matching_chars)); - } - - /* Handle merging of symbols eg strings, brackets. */ - const String line = get_line(caret_line); - char32_t next_char = line[get_caret_column()]; - char32_t last_completion_char = insert_text[insert_text.length() - 1]; - char32_t last_completion_char_display = display_text[display_text.length() - 1]; + int pre_brace_pair = get_caret_column(i) > 0 ? _get_auto_brace_pair_open_at_pos(caret_line, get_caret_column(i)) : -1; + int post_brace_pair = get_caret_column(i) < get_line(caret_line).length() ? _get_auto_brace_pair_close_at_pos(caret_line, get_caret_column(i)) : -1; - int pre_brace_pair = get_caret_column() > 0 ? _get_auto_brace_pair_open_at_pos(caret_line, get_caret_column()) : -1; - int post_brace_pair = get_caret_column() < get_line(caret_line).length() ? _get_auto_brace_pair_close_at_pos(caret_line, get_caret_column()) : -1; - - if (post_brace_pair != -1 && (last_completion_char == next_char || last_completion_char_display == next_char)) { - remove_text(caret_line, get_caret_column(), caret_line, get_caret_column() + 1); - } + if (post_brace_pair != -1 && (last_completion_char == next_char || last_completion_char_display == next_char)) { + remove_text(caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1); + adjust_carets_after_edit(i, caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1); + } - if (pre_brace_pair != -1 && pre_brace_pair != post_brace_pair && (last_completion_char == next_char || last_completion_char_display == next_char)) { - remove_text(caret_line, get_caret_column(), caret_line, get_caret_column() + 1); - } else if (auto_brace_completion_enabled && pre_brace_pair != -1 && post_brace_pair == -1) { - insert_text_at_caret(auto_brace_completion_pairs[pre_brace_pair].close_key); - set_caret_column(get_caret_column() - auto_brace_completion_pairs[pre_brace_pair].close_key.length()); - } + if (pre_brace_pair != -1 && pre_brace_pair != post_brace_pair && (last_completion_char == next_char || last_completion_char_display == next_char)) { + remove_text(caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1); + adjust_carets_after_edit(i, caret_line, get_caret_column(i), caret_line, get_caret_column(i) + 1); + } else if (auto_brace_completion_enabled && pre_brace_pair != -1 && post_brace_pair == -1) { + insert_text_at_caret(auto_brace_completion_pairs[pre_brace_pair].close_key, i); + set_caret_column(get_caret_column(i) - auto_brace_completion_pairs[pre_brace_pair].close_key.length(), i == 0, i); + } - if (pre_brace_pair == -1 && post_brace_pair == -1 && get_caret_column() > 0 && get_caret_column() < get_line(caret_line).length()) { - pre_brace_pair = _get_auto_brace_pair_open_at_pos(caret_line, get_caret_column() + 1); - if (pre_brace_pair != -1 && pre_brace_pair == _get_auto_brace_pair_close_at_pos(caret_line, get_caret_column() - 1)) { - remove_text(caret_line, get_caret_column() - 2, caret_line, get_caret_column()); - if (_get_auto_brace_pair_close_at_pos(caret_line, get_caret_column() - 1) != pre_brace_pair) { - set_caret_column(get_caret_column() - 1); + if (pre_brace_pair == -1 && post_brace_pair == -1 && get_caret_column(i) > 0 && get_caret_column(i) < get_line(caret_line).length()) { + pre_brace_pair = _get_auto_brace_pair_open_at_pos(caret_line, get_caret_column(i) + 1); + if (pre_brace_pair != -1 && pre_brace_pair == _get_auto_brace_pair_close_at_pos(caret_line, get_caret_column(i) - 1)) { + remove_text(caret_line, get_caret_column(i) - 2, caret_line, get_caret_column(i)); + adjust_carets_after_edit(i, caret_line, get_caret_column(i) - 2, caret_line, get_caret_column(i)); + if (_get_auto_brace_pair_close_at_pos(caret_line, get_caret_column(i) - 1) != pre_brace_pair) { + set_caret_column(get_caret_column(i) - 1, i == 0, i); + } } } } - end_complex_operation(); cancel_code_completion(); - if (code_completion_prefixes.has(last_completion_char)) { + if (code_completion_prefixes.has(caret_last_completion_char)) { request_code_completion(); } } @@ -2399,6 +2445,7 @@ void CodeEdit::_gutter_clicked(int p_line, int p_gutter) { } if (p_gutter == line_number_gutter) { + remove_secondary_carets(); set_selection_mode(TextEdit::SelectionMode::SELECTION_MODE_LINE, p_line, 0); select(p_line, 0, p_line + 1, 0); set_caret_line(p_line + 1); diff --git a/scene/gui/code_edit.h b/scene/gui/code_edit.h index 2065f3e681..09c7ef80bf 100644 --- a/scene/gui/code_edit.h +++ b/scene/gui/code_edit.h @@ -261,8 +261,8 @@ protected: /* Text manipulation */ // Overridable actions - virtual void _handle_unicode_input_internal(const uint32_t p_unicode) override; - virtual void _backspace_internal() override; + virtual void _handle_unicode_input_internal(const uint32_t p_unicode, int p_caret) override; + virtual void _backspace_internal(int p_caret) override; GDVIRTUAL1(_confirm_code_completion, bool) GDVIRTUAL1(_request_code_completion, bool) diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 38302136d6..cc0fa200a6 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -536,145 +536,145 @@ void TextEdit::_notification(int p_what) { RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(), get_size()), background_color); } - int brace_open_match_line = -1; - int brace_open_match_column = -1; - bool brace_open_matching = false; - bool brace_open_mismatch = false; - int brace_close_match_line = -1; - int brace_close_match_column = -1; - bool brace_close_matching = false; - bool brace_close_mismatch = false; - - if (highlight_matching_braces_enabled && caret.line >= 0 && caret.line < text.size() && caret.column >= 0) { - if (caret.column < text[caret.line].length()) { - // Check for open. - char32_t c = text[caret.line][caret.column]; - char32_t closec = 0; - - if (c == '[') { - closec = ']'; - } else if (c == '{') { - closec = '}'; - } else if (c == '(') { - closec = ')'; + Vector<BraceMatchingData> brace_matching; + if (highlight_matching_braces_enabled) { + brace_matching.resize(carets.size()); + + for (int caret = 0; caret < carets.size(); caret++) { + if (get_caret_line(caret) < 0 || get_caret_line(caret) >= text.size() || get_caret_column(caret) < 0) { + continue; } - if (closec != 0) { - int stack = 1; - - for (int i = caret.line; i < text.size(); i++) { - int from = i == caret.line ? caret.column + 1 : 0; - for (int j = from; j < text[i].length(); j++) { - char32_t cc = text[i][j]; - // Ignore any brackets inside a string. - if (cc == '"' || cc == '\'') { - char32_t quotation = cc; - do { - j++; - if (!(j < text[i].length())) { - break; - } - cc = text[i][j]; - // Skip over escaped quotation marks inside strings. - if (cc == '\\') { - bool escaped = true; - while (j + 1 < text[i].length() && text[i][j + 1] == '\\') { - escaped = !escaped; - j++; + if (get_caret_column(caret) < text[get_caret_line(caret)].length()) { + // Check for open. + char32_t c = text[get_caret_line(caret)][get_caret_column(caret)]; + char32_t closec = 0; + + if (c == '[') { + closec = ']'; + } else if (c == '{') { + closec = '}'; + } else if (c == '(') { + closec = ')'; + } + + if (closec != 0) { + int stack = 1; + + for (int i = get_caret_line(caret); i < text.size(); i++) { + int from = i == get_caret_line(caret) ? get_caret_column(caret) + 1 : 0; + for (int j = from; j < text[i].length(); j++) { + char32_t cc = text[i][j]; + // Ignore any brackets inside a string. + if (cc == '"' || cc == '\'') { + char32_t quotation = cc; + do { + j++; + if (!(j < text[i].length())) { + break; } - if (escaped) { - j++; - continue; + cc = text[i][j]; + // Skip over escaped quotation marks inside strings. + if (cc == '\\') { + bool escaped = true; + while (j + 1 < text[i].length() && text[i][j + 1] == '\\') { + escaped = !escaped; + j++; + } + if (escaped) { + j++; + continue; + } } - } - } while (cc != quotation); - } else if (cc == c) { - stack++; - } else if (cc == closec) { - stack--; - } + } while (cc != quotation); + } else if (cc == c) { + stack++; + } else if (cc == closec) { + stack--; + } - if (stack == 0) { - brace_open_match_line = i; - brace_open_match_column = j; - brace_open_matching = true; + if (stack == 0) { + brace_matching.write[caret].open_match_line = i; + brace_matching.write[caret].open_match_column = j; + brace_matching.write[caret].open_matching = true; + break; + } + } + if (brace_matching.write[caret].open_match_line != -1) { break; } } - if (brace_open_match_line != -1) { - break; - } - } - if (!brace_open_matching) { - brace_open_mismatch = true; + if (!brace_matching.write[caret].open_matching) { + brace_matching.write[caret].open_mismatch = true; + } } } - } - if (caret.column > 0) { - char32_t c = text[caret.line][caret.column - 1]; - char32_t closec = 0; + if (get_caret_column(caret) > 0) { + char32_t c = text[get_caret_line(caret)][get_caret_column(caret) - 1]; + char32_t closec = 0; - if (c == ']') { - closec = '['; - } else if (c == '}') { - closec = '{'; - } else if (c == ')') { - closec = '('; - } + if (c == ']') { + closec = '['; + } else if (c == '}') { + closec = '{'; + } else if (c == ')') { + closec = '('; + } - if (closec != 0) { - int stack = 1; - - for (int i = caret.line; i >= 0; i--) { - int from = i == caret.line ? caret.column - 2 : text[i].length() - 1; - for (int j = from; j >= 0; j--) { - char32_t cc = text[i][j]; - // Ignore any brackets inside a string. - if (cc == '"' || cc == '\'') { - char32_t quotation = cc; - do { - j--; - if (!(j >= 0)) { - break; - } - cc = text[i][j]; - // Skip over escaped quotation marks inside strings. - if (cc == quotation) { - bool escaped = false; - while (j - 1 >= 0 && text[i][j - 1] == '\\') { - escaped = !escaped; - j--; + if (closec != 0) { + int stack = 1; + + for (int i = get_caret_line(caret); i >= 0; i--) { + int from = i == get_caret_line(caret) ? get_caret_column(caret) - 2 : text[i].length() - 1; + for (int j = from; j >= 0; j--) { + char32_t cc = text[i][j]; + // Ignore any brackets inside a string. + if (cc == '"' || cc == '\'') { + char32_t quotation = cc; + do { + j--; + if (!(j >= 0)) { + break; } - if (escaped) { - cc = '\\'; - continue; + cc = text[i][j]; + // Skip over escaped quotation marks inside strings. + if (cc == quotation) { + bool escaped = false; + while (j - 1 >= 0 && text[i][j - 1] == '\\') { + escaped = !escaped; + j--; + } + if (escaped) { + cc = '\\'; + continue; + } } - } - } while (cc != quotation); - } else if (cc == c) { - stack++; - } else if (cc == closec) { - stack--; - } + } while (cc != quotation); + } else if (cc == c) { + stack++; + } else if (cc == closec) { + stack--; + } - if (stack == 0) { - brace_close_match_line = i; - brace_close_match_column = j; - brace_close_matching = true; + if (stack == 0) { + brace_matching.write[caret].close_match_line = i; + brace_matching.write[caret].close_match_column = j; + brace_matching.write[caret].close_matching = true; + break; + } + } + if (brace_matching.write[caret].close_match_line != -1) { break; } } - if (brace_close_match_line != -1) { - break; - } - } - if (!brace_close_matching) { - brace_close_mismatch = true; + if (!brace_matching.write[caret].close_matching) { + brace_matching.write[caret].close_mismatch = true; + } } } } @@ -683,12 +683,20 @@ void TextEdit::_notification(int p_what) { bool draw_placeholder = text.size() == 1 && text[0].length() == 0; // Get the highlighted words. - String highlighted_text = get_selected_text(); + String highlighted_text = get_selected_text(0); // Check if highlighted words contain only whitespaces (tabs or spaces). bool only_whitespaces_highlighted = highlighted_text.strip_edges().is_empty(); - const int caret_wrap_index = get_caret_wrap_index(); + HashMap<int, HashSet<int>> caret_line_wrap_index_map; + Vector<int> carets_wrap_index; + carets_wrap_index.resize(carets.size()); + for (int i = 0; i < carets.size(); i++) { + carets.write[i].visible = false; + int wrap_index = get_caret_wrap_index(i); + caret_line_wrap_index_map[get_caret_line(i)].insert(wrap_index); + carets_wrap_index.write[i] = wrap_index; + } int first_visible_line = get_first_visible_line() - 1; int draw_amount = visible_rows + (smooth_scroll_enabled ? 1 : 0); @@ -783,7 +791,7 @@ void TextEdit::_notification(int p_what) { last_wrap_column += wrap_rows[line_wrap_index - 1].length(); } - if (minimap_line == caret.line && caret_wrap_index == line_wrap_index && highlight_current_line) { + if (caret_line_wrap_index_map.has(minimap_line) && caret_line_wrap_index_map[minimap_line].has(line_wrap_index) && highlight_current_line) { if (rtl) { RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - (xmargin_end + 2) - minimap_width, i * 3, minimap_width, 2), current_line_color); } else { @@ -875,7 +883,6 @@ void TextEdit::_notification(int p_what) { } // Draw main text. - caret.visible = false; line_drawing_cache.clear(); int row_height = draw_placeholder ? placeholder_line_height + line_spacing : get_line_height(); int line = first_visible_line; @@ -921,7 +928,7 @@ void TextEdit::_notification(int p_what) { } const String &str = wrap_rows[line_wrap_index]; - int char_margin = xmargin_beg - caret.x_ofs; + int char_margin = xmargin_beg - first_visible_col; int ofs_x = 0; int ofs_y = 0; @@ -934,7 +941,7 @@ void TextEdit::_notification(int p_what) { } ofs_y += i * row_height + line_spacing / 2; - ofs_y -= caret.wrap_ofs * row_height; + ofs_y -= first_visible_line_wrap_ofs * row_height; ofs_y -= _get_v_scroll_offset() * row_height; bool clipped = false; @@ -960,7 +967,7 @@ void TextEdit::_notification(int p_what) { if (str.length() == 0) { // Draw line background if empty as we won't loop at all. - if (line == caret.line && caret_wrap_index == line_wrap_index && highlight_current_line) { + if (caret_line_wrap_index_map.has(line) && caret_line_wrap_index_map[line].has(line_wrap_index) && highlight_current_line) { if (rtl) { RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - ofs_x - xmargin_end, ofs_y, xmargin_end, row_height), current_line_color); } else { @@ -969,17 +976,19 @@ void TextEdit::_notification(int p_what) { } // Give visual indication of empty selected line. - if (selection.active && line >= selection.from_line && line <= selection.to_line && char_margin >= xmargin_beg) { - float char_w = font->get_char_size(' ', font_size).width; - if (rtl) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - xmargin_beg - ofs_x - char_w, ofs_y, char_w, row_height), selection_color); - } else { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(xmargin_beg + ofs_x, ofs_y, char_w, row_height), selection_color); + for (int c = 0; c < carets.size(); c++) { + if (has_selection(c) && line >= get_selection_from_line(c) && line <= get_selection_to_line(c) && char_margin >= xmargin_beg) { + float char_w = font->get_char_size(' ', font_size).width; + if (rtl) { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - xmargin_beg - ofs_x - char_w, ofs_y, char_w, row_height), selection_color); + } else { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(xmargin_beg + ofs_x, ofs_y, char_w, row_height), selection_color); + } } } } else { // If it has text, then draw current line marker in the margin, as line number etc will draw over it, draw the rest of line marker later. - if (line == caret.line && caret_wrap_index == line_wrap_index && highlight_current_line) { + if (caret_line_wrap_index_map.has(line) && caret_line_wrap_index_map[line].has(line_wrap_index) && highlight_current_line) { if (rtl) { RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - ofs_x - xmargin_end, ofs_y, xmargin_end, row_height), current_line_color); } else { @@ -1074,23 +1083,25 @@ void TextEdit::_notification(int p_what) { char_margin = size.width - char_margin - TS->shaped_text_get_size(rid).x; } - if (!clipped && selection.active && line >= selection.from_line && line <= selection.to_line) { // Selection - int sel_from = (line > selection.from_line) ? TS->shaped_text_get_range(rid).x : selection.from_column; - int sel_to = (line < selection.to_line) ? TS->shaped_text_get_range(rid).y : selection.to_column; - Vector<Vector2> sel = TS->shaped_text_get_selection(rid, sel_from, sel_to); - for (int j = 0; j < sel.size(); j++) { - Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, sel[j].y - sel[j].x, row_height); - if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) { - continue; - } - if (rect.position.x < xmargin_beg) { - rect.size.x -= (xmargin_beg - rect.position.x); - rect.position.x = xmargin_beg; - } - if (rect.position.x + rect.size.x > xmargin_end) { - rect.size.x = xmargin_end - rect.position.x; + for (int c = 0; c < carets.size(); c++) { + if (!clipped && has_selection(c) && line >= get_selection_from_line(c) && line <= get_selection_to_line(c)) { // Selection + int sel_from = (line > get_selection_from_line(c)) ? TS->shaped_text_get_range(rid).x : get_selection_from_column(c); + int sel_to = (line < get_selection_to_line(c)) ? TS->shaped_text_get_range(rid).y : get_selection_to_column(c); + Vector<Vector2> sel = TS->shaped_text_get_selection(rid, sel_from, sel_to); + for (int j = 0; j < sel.size(); j++) { + Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, sel[j].y - sel[j].x, row_height); + if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) { + continue; + } + if (rect.position.x < xmargin_beg) { + rect.size.x -= (xmargin_beg - rect.position.x); + rect.position.x = xmargin_beg; + } + if (rect.position.x + rect.size.x > xmargin_end) { + rect.size.x = xmargin_end - rect.position.x; + } + draw_rect(rect, selection_color, true); } - draw_rect(rect, selection_color, true); } } @@ -1203,34 +1214,38 @@ void TextEdit::_notification(int p_what) { } Color gl_color = current_color; - if (selection.active && line >= selection.from_line && line <= selection.to_line) { // Selection - int sel_from = (line > selection.from_line) ? TS->shaped_text_get_range(rid).x : selection.from_column; - int sel_to = (line < selection.to_line) ? TS->shaped_text_get_range(rid).y : selection.to_column; + for (int c = 0; c < carets.size(); c++) { + if (has_selection(c) && line >= get_selection_from_line(c) && line <= get_selection_to_line(c)) { // Selection + int sel_from = (line > get_selection_from_line(c)) ? TS->shaped_text_get_range(rid).x : get_selection_from_column(c); + int sel_to = (line < get_selection_to_line(c)) ? TS->shaped_text_get_range(rid).y : get_selection_to_column(c); - if (glyphs[j].start >= sel_from && glyphs[j].end <= sel_to && override_selected_font_color) { - gl_color = font_selected_color; + if (glyphs[j].start >= sel_from && glyphs[j].end <= sel_to && override_selected_font_color) { + gl_color = font_selected_color; + } } } float char_pos = char_ofs + char_margin + ofs_x; if (char_pos >= xmargin_beg) { if (highlight_matching_braces_enabled) { - if ((brace_open_match_line == line && brace_open_match_column == glyphs[j].start) || - (caret.column == glyphs[j].start && caret.line == line && caret_wrap_index == line_wrap_index && (brace_open_matching || brace_open_mismatch))) { - if (brace_open_mismatch) { - gl_color = brace_mismatch_color; + for (int c = 0; c < carets.size(); c++) { + if ((brace_matching[c].open_match_line == line && brace_matching[c].open_match_column == glyphs[j].start) || + (get_caret_column(c) == glyphs[j].start && get_caret_line(c) == line && carets_wrap_index[c] == line_wrap_index && (brace_matching[c].open_matching || brace_matching[c].open_mismatch))) { + if (brace_matching[c].open_mismatch) { + gl_color = brace_mismatch_color; + } + Rect2 rect = Rect2(char_pos, ofs_y + font->get_underline_position(font_size), glyphs[j].advance * glyphs[j].repeat, MAX(font->get_underline_thickness(font_size) * get_theme_default_base_scale(), 1)); + draw_rect(rect, gl_color); } - Rect2 rect = Rect2(char_pos, ofs_y + font->get_underline_position(font_size), glyphs[j].advance * glyphs[j].repeat, MAX(font->get_underline_thickness(font_size) * get_theme_default_base_scale(), 1)); - draw_rect(rect, gl_color); - } - if ((brace_close_match_line == line && brace_close_match_column == glyphs[j].start) || - (caret.column == glyphs[j].start + 1 && caret.line == line && caret_wrap_index == line_wrap_index && (brace_close_matching || brace_close_mismatch))) { - if (brace_close_mismatch) { - gl_color = brace_mismatch_color; + if ((brace_matching[c].close_match_line == line && brace_matching[c].close_match_column == glyphs[j].start) || + (get_caret_column(c) == glyphs[j].start + 1 && get_caret_line(c) == line && carets_wrap_index[c] == line_wrap_index && (brace_matching[c].close_matching || brace_matching[c].close_mismatch))) { + if (brace_matching[c].close_mismatch) { + gl_color = brace_mismatch_color; + } + Rect2 rect = Rect2(char_pos, ofs_y + font->get_underline_position(font_size), glyphs[j].advance * glyphs[j].repeat, MAX(font->get_underline_thickness(font_size) * get_theme_default_base_scale(), 1)); + draw_rect(rect, gl_color); } - Rect2 rect = Rect2(char_pos, ofs_y + font->get_underline_position(font_size), glyphs[j].advance * glyphs[j].repeat, MAX(font->get_underline_thickness(font_size) * get_theme_default_base_scale(), 1)); - draw_rect(rect, gl_color); } } @@ -1289,137 +1304,139 @@ void TextEdit::_notification(int p_what) { // Prevent carets from disappearing at theme scales below 1.0 (if the caret width is 1). const int caret_width = get_theme_constant(SNAME("caret_width")) * MAX(1, get_theme_default_base_scale()); - if (!clipped && caret.line == line && line_wrap_index == caret_wrap_index) { - caret.draw_pos.y = ofs_y + ldata->get_line_descent(line_wrap_index); + for (int c = 0; c < carets.size(); c++) { + if (!clipped && get_caret_line(c) == line && carets_wrap_index[c] == line_wrap_index) { + carets.write[c].draw_pos.y = ofs_y + ldata->get_line_descent(line_wrap_index); - if (ime_text.length() == 0) { - CaretInfo ts_caret; - if (str.length() != 0) { - // Get carets. - ts_caret = TS->shaped_text_get_carets(rid, caret.column); - } else { - // No carets, add one at the start. - int h = font->get_height(font_size); - if (rtl) { - ts_caret.l_dir = TextServer::DIRECTION_RTL; - ts_caret.l_caret = Rect2(Vector2(xmargin_end - char_margin + ofs_x, -h / 2), Size2(caret_width * 4, h)); + if (ime_text.length() == 0) { + CaretInfo ts_caret; + if (str.length() != 0) { + // Get carets. + ts_caret = TS->shaped_text_get_carets(rid, get_caret_column(c)); } else { - ts_caret.l_dir = TextServer::DIRECTION_LTR; - ts_caret.l_caret = Rect2(Vector2(char_ofs, -h / 2), Size2(caret_width * 4, h)); + // No carets, add one at the start. + int h = font->get_height(font_size); + if (rtl) { + ts_caret.l_dir = TextServer::DIRECTION_RTL; + ts_caret.l_caret = Rect2(Vector2(xmargin_end - char_margin + ofs_x, -h / 2), Size2(caret_width * 4, h)); + } else { + ts_caret.l_dir = TextServer::DIRECTION_LTR; + ts_caret.l_caret = Rect2(Vector2(char_ofs, -h / 2), Size2(caret_width * 4, h)); + } } - } - if ((ts_caret.l_caret != Rect2() && (ts_caret.l_dir == TextServer::DIRECTION_AUTO || ts_caret.l_dir == (TextServer::Direction)input_direction)) || (ts_caret.t_caret == Rect2())) { - caret.draw_pos.x = char_margin + ofs_x + ts_caret.l_caret.position.x; - } else { - caret.draw_pos.x = char_margin + ofs_x + ts_caret.t_caret.position.x; - } + if ((ts_caret.l_caret != Rect2() && (ts_caret.l_dir == TextServer::DIRECTION_AUTO || ts_caret.l_dir == (TextServer::Direction)input_direction)) || (ts_caret.t_caret == Rect2())) { + carets.write[c].draw_pos.x = char_margin + ofs_x + ts_caret.l_caret.position.x; + } else { + carets.write[c].draw_pos.x = char_margin + ofs_x + ts_caret.t_caret.position.x; + } - if (caret.draw_pos.x >= xmargin_beg && caret.draw_pos.x < xmargin_end) { - caret.visible = true; - if (draw_caret || drag_caret_force_displayed) { - if (caret_type == CaretType::CARET_TYPE_BLOCK || overtype_mode) { - //Block or underline caret, draw trailing carets at full height. - int h = font->get_height(font_size); - - if (ts_caret.t_caret != Rect2()) { - if (overtype_mode) { - ts_caret.t_caret.position.y = TS->shaped_text_get_descent(rid); - ts_caret.t_caret.size.y = caret_width; - } else { - ts_caret.t_caret.position.y = -TS->shaped_text_get_ascent(rid); - ts_caret.t_caret.size.y = h; - } - ts_caret.t_caret.position += Vector2(char_margin + ofs_x, ofs_y); - draw_rect(ts_caret.t_caret, caret_color, overtype_mode); + if (get_caret_draw_pos(c).x >= xmargin_beg && get_caret_draw_pos(c).x < xmargin_end) { + carets.write[c].visible = true; + if (draw_caret || drag_caret_force_displayed) { + if (caret_type == CaretType::CARET_TYPE_BLOCK || overtype_mode) { + //Block or underline caret, draw trailing carets at full height. + int h = font->get_height(font_size); - if (ts_caret.l_caret != Rect2() && ts_caret.l_dir != ts_caret.t_dir) { - ts_caret.l_caret.position += Vector2(char_margin + ofs_x, ofs_y); - ts_caret.l_caret.size.x = caret_width; - draw_rect(ts_caret.l_caret, caret_color * Color(1, 1, 1, 0.5)); - } - } else { // End of the line. - if (gl_size > 0) { - // Adjust for actual line dimensions. + if (ts_caret.t_caret != Rect2()) { if (overtype_mode) { - ts_caret.l_caret.position.y = TS->shaped_text_get_descent(rid); + ts_caret.t_caret.position.y = TS->shaped_text_get_descent(rid); + ts_caret.t_caret.size.y = caret_width; + } else { + ts_caret.t_caret.position.y = -TS->shaped_text_get_ascent(rid); + ts_caret.t_caret.size.y = h; + } + ts_caret.t_caret.position += Vector2(char_margin + ofs_x, ofs_y); + draw_rect(ts_caret.t_caret, caret_color, overtype_mode); + + if (ts_caret.l_caret != Rect2() && ts_caret.l_dir != ts_caret.t_dir) { + ts_caret.l_caret.position += Vector2(char_margin + ofs_x, ofs_y); + ts_caret.l_caret.size.x = caret_width; + draw_rect(ts_caret.l_caret, caret_color * Color(1, 1, 1, 0.5)); + } + } else { // End of the line. + if (gl_size > 0) { + // Adjust for actual line dimensions. + if (overtype_mode) { + ts_caret.l_caret.position.y = TS->shaped_text_get_descent(rid); + ts_caret.l_caret.size.y = caret_width; + } else { + ts_caret.l_caret.position.y = -TS->shaped_text_get_ascent(rid); + ts_caret.l_caret.size.y = h; + } + } else if (overtype_mode) { + ts_caret.l_caret.position.y += ts_caret.l_caret.size.y; ts_caret.l_caret.size.y = caret_width; + } + if (ts_caret.l_caret.position.x >= TS->shaped_text_get_size(rid).x) { + ts_caret.l_caret.size.x = font->get_char_size('m', font_size).x; } else { - ts_caret.l_caret.position.y = -TS->shaped_text_get_ascent(rid); - ts_caret.l_caret.size.y = h; + ts_caret.l_caret.size.x = 3 * caret_width; + } + ts_caret.l_caret.position += Vector2(char_margin + ofs_x, ofs_y); + if (ts_caret.l_dir == TextServer::DIRECTION_RTL) { + ts_caret.l_caret.position.x -= ts_caret.l_caret.size.x; } - } else if (overtype_mode) { - ts_caret.l_caret.position.y += ts_caret.l_caret.size.y; - ts_caret.l_caret.size.y = caret_width; + draw_rect(ts_caret.l_caret, caret_color, overtype_mode); } - if (ts_caret.l_caret.position.x >= TS->shaped_text_get_size(rid).x) { - ts_caret.l_caret.size.x = font->get_char_size('m', font_size).x; - } else { - ts_caret.l_caret.size.x = 3 * caret_width; + } else { + // Normal caret. + if (ts_caret.l_caret != Rect2() && ts_caret.l_dir == TextServer::DIRECTION_AUTO) { + // Draw extra marker on top of mid caret. + Rect2 trect = Rect2(ts_caret.l_caret.position.x - 3 * caret_width, ts_caret.l_caret.position.y, 6 * caret_width, caret_width); + trect.position += Vector2(char_margin + ofs_x, ofs_y); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, trect, caret_color); } ts_caret.l_caret.position += Vector2(char_margin + ofs_x, ofs_y); - if (ts_caret.l_dir == TextServer::DIRECTION_RTL) { - ts_caret.l_caret.position.x -= ts_caret.l_caret.size.x; - } - draw_rect(ts_caret.l_caret, caret_color, overtype_mode); - } - } else { - // Normal caret. - if (ts_caret.l_caret != Rect2() && ts_caret.l_dir == TextServer::DIRECTION_AUTO) { - // Draw extra marker on top of mid caret. - Rect2 trect = Rect2(ts_caret.l_caret.position.x - 3 * caret_width, ts_caret.l_caret.position.y, 6 * caret_width, caret_width); - trect.position += Vector2(char_margin + ofs_x, ofs_y); - RenderingServer::get_singleton()->canvas_item_add_rect(ci, trect, caret_color); - } - ts_caret.l_caret.position += Vector2(char_margin + ofs_x, ofs_y); - ts_caret.l_caret.size.x = caret_width; + ts_caret.l_caret.size.x = caret_width; - draw_rect(ts_caret.l_caret, caret_color); + draw_rect(ts_caret.l_caret, caret_color); - ts_caret.t_caret.position += Vector2(char_margin + ofs_x, ofs_y); - ts_caret.t_caret.size.x = caret_width; + ts_caret.t_caret.position += Vector2(char_margin + ofs_x, ofs_y); + ts_caret.t_caret.size.x = caret_width; - draw_rect(ts_caret.t_caret, caret_color); + draw_rect(ts_caret.t_caret, caret_color); + } } } - } - } else { - { - // IME Intermediate text range. - Vector<Vector2> sel = TS->shaped_text_get_selection(rid, caret.column, caret.column + ime_text.length()); - for (int j = 0; j < sel.size(); j++) { - Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, sel[j].y - sel[j].x, text_height); - if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) { - continue; - } - if (rect.position.x < xmargin_beg) { - rect.size.x -= (xmargin_beg - rect.position.x); - rect.position.x = xmargin_beg; - } else if (rect.position.x + rect.size.x > xmargin_end) { - rect.size.x = xmargin_end - rect.position.x; + } else { + { + // IME Intermediate text range. + Vector<Vector2> sel = TS->shaped_text_get_selection(rid, get_caret_column(c), get_caret_column(c) + ime_text.length()); + for (int j = 0; j < sel.size(); j++) { + Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, sel[j].y - sel[j].x, text_height); + if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) { + continue; + } + if (rect.position.x < xmargin_beg) { + rect.size.x -= (xmargin_beg - rect.position.x); + rect.position.x = xmargin_beg; + } else if (rect.position.x + rect.size.x > xmargin_end) { + rect.size.x = xmargin_end - rect.position.x; + } + rect.size.y = caret_width; + draw_rect(rect, caret_color); + carets.write[c].draw_pos.x = rect.position.x; } - rect.size.y = caret_width; - draw_rect(rect, caret_color); - caret.draw_pos.x = rect.position.x; } - } - { - // IME caret. - Vector<Vector2> sel = TS->shaped_text_get_selection(rid, caret.column + ime_selection.x, caret.column + ime_selection.x + ime_selection.y); - for (int j = 0; j < sel.size(); j++) { - Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, sel[j].y - sel[j].x, text_height); - if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) { - continue; - } - if (rect.position.x < xmargin_beg) { - rect.size.x -= (xmargin_beg - rect.position.x); - rect.position.x = xmargin_beg; - } else if (rect.position.x + rect.size.x > xmargin_end) { - rect.size.x = xmargin_end - rect.position.x; + { + // IME caret. + Vector<Vector2> sel = TS->shaped_text_get_selection(rid, get_caret_column(c) + ime_selection.x, get_caret_column(c) + ime_selection.x + ime_selection.y); + for (int j = 0; j < sel.size(); j++) { + Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, sel[j].y - sel[j].x, text_height); + if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) { + continue; + } + if (rect.position.x < xmargin_beg) { + rect.size.x -= (xmargin_beg - rect.position.x); + rect.position.x = xmargin_beg; + } else if (rect.position.x + rect.size.x > xmargin_end) { + rect.size.x = xmargin_end - rect.position.x; + } + rect.size.y = caret_width * 3; + draw_rect(rect, caret_color); + carets.write[c].draw_pos.x = rect.position.x; } - rect.size.y = caret_width * 3; - draw_rect(rect, caret_color); - caret.draw_pos.x = rect.position.x; } } } @@ -1434,7 +1451,7 @@ void TextEdit::_notification(int p_what) { if (has_focus()) { if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { DisplayServer::get_singleton()->window_set_ime_active(true, get_viewport()->get_window_id()); - DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + caret.draw_pos, get_viewport()->get_window_id()); + DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + get_caret_draw_pos(), get_viewport()->get_window_id()); } } } break; @@ -1455,13 +1472,13 @@ void TextEdit::_notification(int p_what) { int caret_start = -1; int caret_end = -1; - if (!selection.active) { - String full_text = _base_get_text(0, 0, caret.line, caret.column); + if (!has_selection(0)) { + String full_text = _base_get_text(0, 0, get_caret_line(), get_caret_column()); caret_start = full_text.length(); } else { - String pre_text = _base_get_text(0, 0, selection.from_line, selection.from_column); - String post_text = get_selected_text(); + String pre_text = _base_get_text(0, 0, get_selection_from_line(), get_selection_from_column()); + String post_text = get_selected_text(0); caret_start = pre_text.length(); caret_end = caret_start + post_text.length(); @@ -1483,14 +1500,16 @@ void TextEdit::_notification(int p_what) { if (!ime_text.is_empty()) { ime_text = ""; ime_selection = Point2(); - text.invalidate_cache(caret.line, caret.column, true, ime_text); + for (int i = 0; i < carets.size(); i++) { + text.invalidate_cache(get_caret_line(i), get_caret_column(i), true, ime_text); + } } if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { DisplayServer::get_singleton()->virtual_keyboard_hide(); } - if (deselect_on_focus_loss_enabled && !selection.drag_attempt) { + if (deselect_on_focus_loss_enabled && !selection_drag_attempt) { deselect(); } } break; @@ -1500,20 +1519,21 @@ void TextEdit::_notification(int p_what) { ime_text = DisplayServer::get_singleton()->ime_get_text(); ime_selection = DisplayServer::get_singleton()->ime_get_selection(); - String t; - if (caret.column >= 0) { - t = text[caret.line].substr(0, caret.column) + ime_text + text[caret.line].substr(caret.column, text[caret.line].length()); - } else { - t = ime_text; + for (int i = 0; i < carets.size(); i++) { + String t; + if (get_caret_column(i) >= 0) { + t = text[get_caret_line(i)].substr(0, get_caret_column(i)) + ime_text + text[get_caret_line(i)].substr(get_caret_column(i), text[get_caret_line(i)].length()); + } else { + t = ime_text; + } + text.invalidate_cache(get_caret_line(i), get_caret_column(i), true, t, structured_text_parser(st_parser, st_args, t)); } - - text.invalidate_cache(caret.line, caret.column, true, t, structured_text_parser(st_parser, st_args, t)); queue_redraw(); } } break; case NOTIFICATION_DRAG_BEGIN: { - selection.selecting_mode = SelectionMode::SELECTION_MODE_NONE; + selecting_mode = SelectionMode::SELECTION_MODE_NONE; drag_action = true; dragging_minimap = false; dragging_selection = false; @@ -1523,8 +1543,8 @@ void TextEdit::_notification(int p_what) { case NOTIFICATION_DRAG_END: { if (is_drag_successful()) { - if (selection.drag_attempt) { - selection.drag_attempt = false; + if (selection_drag_attempt) { + selection_drag_attempt = false; if (is_editable() && !Input::get_singleton()->is_key_pressed(Key::CTRL)) { delete_selection(); } else if (deselect_on_focus_loss_enabled) { @@ -1532,7 +1552,7 @@ void TextEdit::_notification(int p_what) { } } } else { - selection.drag_attempt = false; + selection_drag_attempt = false; } drag_action = false; drag_caret_force_displayed = false; @@ -1672,77 +1692,103 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { } } - int prev_col = caret.column; - int prev_line = caret.line; - - set_caret_line(row, false, false); - set_caret_column(col); - selection.drag_attempt = false; - - if (selecting_enabled && mb->is_shift_pressed() && (caret.column != prev_col || caret.line != prev_line)) { - if (!selection.active) { - selection.active = true; - selection.selecting_mode = SelectionMode::SELECTION_MODE_POINTER; - selection.from_column = prev_col; - selection.from_line = prev_line; - selection.to_column = caret.column; - selection.to_line = caret.line; - - if (selection.from_line > selection.to_line || (selection.from_line == selection.to_line && selection.from_column > selection.to_column)) { - SWAP(selection.from_column, selection.to_column); - SWAP(selection.from_line, selection.to_line); - selection.shiftclick_left = false; + int caret = carets.size() - 1; + int prev_col = get_caret_column(caret); + int prev_line = get_caret_line(caret); + + const int triple_click_timeout = 600; + const int triple_click_tolerance = 5; + bool is_triple_click = (!mb->is_double_click() && (OS::get_singleton()->get_ticks_msec() - last_dblclk) < triple_click_timeout && mb->get_position().distance_to(last_dblclk_pos) < triple_click_tolerance); + + if (!is_mouse_over_selection() && !mb->is_double_click() && !is_triple_click) { + if (mb->is_alt_pressed()) { + prev_line = row; + prev_col = col; + + caret = add_caret(row, col); + if (caret == -1) { + return; + } + + carets.write[caret].selection.selecting_line = row; + carets.write[caret].selection.selecting_column = col; + + last_dblclk = 0; + } else if (!mb->is_shift_pressed()) { + caret = 0; + remove_secondary_carets(); + } + } + + set_caret_line(row, false, true, 0, caret); + set_caret_column(col, false, caret); + selection_drag_attempt = false; + + if (selecting_enabled && mb->is_shift_pressed() && (get_caret_column(caret) != prev_col || get_caret_line(caret) != prev_line)) { + if (!has_selection(caret)) { + carets.write[caret].selection.active = true; + selecting_mode = SelectionMode::SELECTION_MODE_POINTER; + carets.write[caret].selection.from_column = prev_col; + carets.write[caret].selection.from_line = prev_line; + carets.write[caret].selection.to_column = carets[caret].column; + carets.write[caret].selection.to_line = carets[caret].line; + + if (carets[caret].selection.from_line > carets[caret].selection.to_line || (carets[caret].selection.from_line == carets[caret].selection.to_line && carets[caret].selection.from_column > carets[caret].selection.to_column)) { + SWAP(carets.write[caret].selection.from_column, carets.write[caret].selection.to_column); + SWAP(carets.write[caret].selection.from_line, carets.write[caret].selection.to_line); + carets.write[caret].selection.shiftclick_left = false; } else { - selection.shiftclick_left = true; + carets.write[caret].selection.shiftclick_left = true; } - selection.selecting_line = prev_line; - selection.selecting_column = prev_col; + carets.write[caret].selection.selecting_line = prev_line; + carets.write[caret].selection.selecting_column = prev_col; + caret_index_edit_dirty = true; + merge_overlapping_carets(); queue_redraw(); } else { - if (caret.line < selection.selecting_line || (caret.line == selection.selecting_line && caret.column < selection.selecting_column)) { - if (selection.shiftclick_left) { - selection.shiftclick_left = !selection.shiftclick_left; + if (carets[caret].line < carets[caret].selection.selecting_line || (carets[caret].line == carets[caret].selection.selecting_line && carets[caret].column < carets[caret].selection.selecting_column)) { + if (carets[caret].selection.shiftclick_left) { + carets.write[caret].selection.shiftclick_left = !carets[caret].selection.shiftclick_left; } - selection.from_column = caret.column; - selection.from_line = caret.line; - - } else if (caret.line > selection.selecting_line || (caret.line == selection.selecting_line && caret.column > selection.selecting_column)) { - if (!selection.shiftclick_left) { - SWAP(selection.from_column, selection.to_column); - SWAP(selection.from_line, selection.to_line); - selection.shiftclick_left = !selection.shiftclick_left; + carets.write[caret].selection.from_column = carets[caret].column; + carets.write[caret].selection.from_line = carets[caret].line; + + } else if (carets[caret].line > carets[caret].selection.selecting_line || (carets[caret].line == carets[caret].selection.selecting_line && carets[caret].column > carets[caret].selection.selecting_column)) { + if (!carets[caret].selection.shiftclick_left) { + SWAP(carets.write[caret].selection.from_column, carets.write[caret].selection.to_column); + SWAP(carets.write[caret].selection.from_line, carets.write[caret].selection.to_line); + carets.write[caret].selection.shiftclick_left = !carets[caret].selection.shiftclick_left; } - selection.to_column = caret.column; - selection.to_line = caret.line; + carets.write[caret].selection.to_column = carets[caret].column; + carets.write[caret].selection.to_line = carets[caret].line; } else { - selection.active = false; + deselect(caret); } - + caret_index_edit_dirty = true; + merge_overlapping_carets(); queue_redraw(); } } else if (drag_and_drop_selection_enabled && is_mouse_over_selection()) { - selection.selecting_mode = SelectionMode::SELECTION_MODE_NONE; - selection.drag_attempt = true; - } else { - selection.active = false; - selection.selecting_mode = SelectionMode::SELECTION_MODE_POINTER; - selection.selecting_line = row; - selection.selecting_column = col; + set_selection_mode(SelectionMode::SELECTION_MODE_NONE, get_selection_line(caret), get_selection_column(caret), caret); + // We use the main caret for dragging, so reset this one. + set_caret_line(prev_line, false, true, 0, caret); + set_caret_column(prev_col, false, caret); + selection_drag_attempt = true; + } else if (caret == 0) { + deselect(); + set_selection_mode(SelectionMode::SELECTION_MODE_POINTER, row, col); } - const int triple_click_timeout = 600; - const int triple_click_tolerance = 5; - - if (!mb->is_double_click() && (OS::get_singleton()->get_ticks_msec() - last_dblclk) < triple_click_timeout && mb->get_position().distance_to(last_dblclk_pos) < triple_click_tolerance) { + if (is_triple_click) { // Triple-click select line. - selection.selecting_mode = SelectionMode::SELECTION_MODE_LINE; - selection.drag_attempt = false; + selecting_mode = SelectionMode::SELECTION_MODE_LINE; + selection_drag_attempt = false; _update_selection_mode_line(); last_dblclk = 0; - } else if (mb->is_double_click() && text[caret.line].length()) { + } else if (mb->is_double_click() && text[get_caret_line(caret)].length()) { // Double-click select word. - selection.selecting_mode = SelectionMode::SELECTION_MODE_WORD; + selecting_mode = SelectionMode::SELECTION_MODE_WORD; _update_selection_mode_word(); last_dblclk = OS::get_singleton()->get_ticks_msec(); last_dblclk_pos = mb->get_position(); @@ -1760,23 +1806,25 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { Point2i pos = get_line_column_at_pos(mpos); int row = pos.y; int col = pos.x; + int caret = carets.size() - 1; if (is_move_caret_on_right_click_enabled()) { - if (has_selection()) { - int from_line = get_selection_from_line(); - int to_line = get_selection_to_line(); - int from_column = get_selection_from_column(); - int to_column = get_selection_to_column(); + if (has_selection(caret)) { + int from_line = get_selection_from_line(caret); + int to_line = get_selection_to_line(caret); + int from_column = get_selection_from_column(caret); + int to_column = get_selection_to_column(caret); if (row < from_line || row > to_line || (row == from_line && col < from_column) || (row == to_line && col > to_column)) { // Right click is outside the selected text. - deselect(); + deselect(caret); } } - if (!has_selection()) { - set_caret_line(row, true, false); - set_caret_column(col); + if (!has_selection(caret)) { + set_caret_line(row, true, false, 0, caret); + set_caret_column(col, true, caret); } + merge_overlapping_carets(); } if (context_menu_enabled) { @@ -1789,15 +1837,15 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { } } else { if (mb->get_button_index() == MouseButton::LEFT) { - if (selection.drag_attempt && is_mouse_over_selection()) { - selection.active = false; + if (selection_drag_attempt && is_mouse_over_selection()) { + deselect(); } dragging_minimap = false; dragging_selection = false; can_drag_minimap = false; click_select_held->stop(); if (!drag_action) { - selection.drag_attempt = false; + selection_drag_attempt = false; } if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text()); @@ -1841,7 +1889,7 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { } if (!dragging_minimap) { - switch (selection.selecting_mode) { + switch (selecting_mode) { case SelectionMode::SELECTION_MODE_POINTER: { _update_selection_mode_pointer(); } break; @@ -1887,8 +1935,8 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { if (drag_action && can_drop_data(mpos, get_viewport()->gui_get_drag_data())) { drag_caret_force_displayed = true; Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); - set_caret_line(pos.y, false); - set_caret_column(pos.x); + set_caret_line(pos.y, false, true, 0, 0); + set_caret_column(pos.x, true, 0); dragging_selection = true; } } @@ -1923,8 +1971,6 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { // * No Modifiers are pressed (except shift) bool allow_unicode_handling = !(k->is_command_or_control_pressed() || k->is_ctrl_pressed() || k->is_alt_pressed() || k->is_meta_pressed()); - selection.selecting_text = false; - // Check and handle all built in shortcuts. // NEWLINES. @@ -2146,7 +2192,9 @@ void TextEdit::_swap_current_input_direction() { } else { input_direction = TEXT_DIRECTION_LTR; } - set_caret_column(caret.column); + for (int i = 0; i < carets.size(); i++) { + set_caret_column(get_caret_column(i), i == 0, i); + } queue_redraw(); } @@ -2156,325 +2204,429 @@ void TextEdit::_new_line(bool p_split_current_line, bool p_above) { } begin_complex_operation(); - - bool first_line = false; - if (!p_split_current_line) { - deselect(); - if (p_above) { - if (caret.line > 0) { - set_caret_line(caret.line - 1, false); - set_caret_column(text[caret.line].length()); + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (const int &i : caret_edit_order) { + bool first_line = false; + if (!p_split_current_line) { + deselect(i); + if (p_above) { + if (get_caret_line(i) > 0) { + set_caret_line(get_caret_line(i) - 1, false, true, 0, i); + set_caret_column(text[get_caret_line(i)].length(), i == 0, i); + } else { + set_caret_column(0, i == 0, i); + first_line = true; + } } else { - set_caret_column(0); - first_line = true; + set_caret_column(text[get_caret_line(i)].length(), i == 0, i); } - } else { - set_caret_column(text[caret.line].length()); } - } - insert_text_at_caret("\n"); + insert_text_at_caret("\n", i); - if (first_line) { - set_caret_line(0); + if (first_line) { + set_caret_line(0, i == 0, true, 0, i); + } } - end_complex_operation(); } void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { - // Handle selection - if (p_select) { - _pre_shift_selection(); - } else if (selection.active && !p_move_by_word) { - // If a selection is active, move caret to start of selection - set_caret_line(selection.from_line); - set_caret_column(selection.from_column); - deselect(); - return; - } else { - deselect(); - } - - if (p_move_by_word) { - int cc = caret.column; - // If the caret is at the start of the line, and not on the first line, move it up to the end of the previous line. - if (cc == 0 && caret.line > 0) { - set_caret_line(caret.line - 1); - set_caret_column(text[caret.line].length()); + for (int i = 0; i < carets.size(); i++) { + // Handle selection. + if (p_select) { + _pre_shift_selection(i); + } else if (has_selection(i) && !p_move_by_word) { + // If a selection is active, move caret to start of selection. + set_caret_line(get_selection_from_line(i), false, true, 0, i); + set_caret_column(get_selection_from_column(i), i == 0, i); + deselect(i); + continue; } else { - PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(caret.line)->get_rid()); - if (words.is_empty() || cc <= words[0]) { - // This solves the scenario where there are no words but glyfs that can be ignored. - cc = 0; + deselect(i); + } + + if (p_move_by_word) { + int cc = get_caret_column(i); + // If the caret is at the start of the line, and not on the first line, move it up to the end of the previous line. + if (cc == 0 && get_caret_line(i) > 0) { + set_caret_line(get_caret_line(i) - 1, false, true, 0, i); + set_caret_column(text[get_caret_line(i)].length(), i == 0, i); } else { - for (int i = words.size() - 2; i >= 0; i = i - 2) { - if (words[i] < cc) { - cc = words[i]; - break; + PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(i))->get_rid()); + if (words.is_empty() || cc <= words[0]) { + // This solves the scenario where there are no words but glyfs that can be ignored. + cc = 0; + } else { + for (int j = words.size() - 2; j >= 0; j = j - 2) { + if (words[j] < cc) { + cc = words[j]; + break; + } } } - } - set_caret_column(cc); - } - } else { - // If the caret is at the start of the line, and not on the first line, move it up to the end of the previous line. - if (caret.column == 0) { - if (caret.line > 0) { - set_caret_line(caret.line - get_next_visible_line_offset_from(CLAMP(caret.line - 1, 0, text.size() - 1), -1)); - set_caret_column(text[caret.line].length()); + set_caret_column(cc, i == 0, i); } } else { - if (caret_mid_grapheme_enabled) { - set_caret_column(get_caret_column() - 1); + // If the caret is at the start of the line, and not on the first line, move it up to the end of the previous line. + if (get_caret_column(i) == 0) { + if (get_caret_line(i) > 0) { + set_caret_line(get_caret_line(i) - get_next_visible_line_offset_from(CLAMP(get_caret_line(i) - 1, 0, text.size() - 1), -1), false, true, 0, i); + set_caret_column(text[get_caret_line(i)].length(), i == 0, i); + } } else { - set_caret_column(TS->shaped_text_prev_grapheme_pos(text.get_line_data(caret.line)->get_rid(), get_caret_column())); + if (caret_mid_grapheme_enabled) { + set_caret_column(get_caret_column(i) - 1, i == 0, i); + } else { + set_caret_column(TS->shaped_text_prev_grapheme_pos(text.get_line_data(get_caret_line(i))->get_rid(), get_caret_column(i)), i == 0, i); + } } } - } - if (p_select) { - _post_shift_selection(); + if (p_select) { + _post_shift_selection(i); + } } + merge_overlapping_carets(); } void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { - // Handle selection - if (p_select) { - _pre_shift_selection(); - } else if (selection.active && !p_move_by_word) { - // If a selection is active, move caret to end of selection - set_caret_line(selection.to_line); - set_caret_column(selection.to_column); - deselect(); - return; - } else { - deselect(); - } - - if (p_move_by_word) { - int cc = caret.column; - // If the caret is at the end of the line, and not on the last line, move it down to the beginning of the next line. - if (cc == text[caret.line].length() && caret.line < text.size() - 1) { - set_caret_line(caret.line + 1); - set_caret_column(0); + for (int i = 0; i < carets.size(); i++) { + // Handle selection + if (p_select) { + _pre_shift_selection(i); + } else if (has_selection(i) && !p_move_by_word) { + // If a selection is active, move caret to end of selection + set_caret_line(get_selection_to_line(i), false, true, 0, i); + set_caret_column(get_selection_to_column(i), i == 0, i); + deselect(i); + continue; } else { - PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(caret.line)->get_rid()); - if (words.is_empty() || cc >= words[words.size() - 1]) { - // This solves the scenario where there are no words but glyfs that can be ignored. - cc = text[caret.line].length(); + deselect(i); + } + + if (p_move_by_word) { + int cc = get_caret_column(i); + // If the caret is at the end of the line, and not on the last line, move it down to the beginning of the next line. + if (cc == text[get_caret_line(i)].length() && get_caret_line(i) < text.size() - 1) { + set_caret_line(get_caret_line(i) + 1, false, true, 0, i); + set_caret_column(0, i == 0, i); } else { - for (int i = 1; i < words.size(); i = i + 2) { - if (words[i] > cc) { - cc = words[i]; - break; + PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(i))->get_rid()); + if (words.is_empty() || cc >= words[words.size() - 1]) { + // This solves the scenario where there are no words but glyfs that can be ignored. + cc = text[get_caret_line(i)].length(); + } else { + for (int j = 1; j < words.size(); j = j + 2) { + if (words[j] > cc) { + cc = words[j]; + break; + } } } - } - set_caret_column(cc); - } - } else { - // If we are at the end of the line, move the caret to the next line down. - if (caret.column == text[caret.line].length()) { - if (caret.line < text.size() - 1) { - set_caret_line(get_caret_line() + get_next_visible_line_offset_from(CLAMP(caret.line + 1, 0, text.size() - 1), 1), true, false); - set_caret_column(0); + set_caret_column(cc, i == 0, i); } } else { - if (caret_mid_grapheme_enabled) { - set_caret_column(get_caret_column() + 1); + // If we are at the end of the line, move the caret to the next line down. + if (get_caret_column(i) == text[get_caret_line(i)].length()) { + if (get_caret_line(i) < text.size() - 1) { + set_caret_line(get_caret_line(i) + get_next_visible_line_offset_from(CLAMP(get_caret_line(i) + 1, 0, text.size() - 1), 1), false, false, 0, i); + set_caret_column(0, i == 0, i); + } } else { - set_caret_column(TS->shaped_text_next_grapheme_pos(text.get_line_data(caret.line)->get_rid(), get_caret_column())); + if (caret_mid_grapheme_enabled) { + set_caret_column(get_caret_column(i) + 1, i == 0, i); + } else { + set_caret_column(TS->shaped_text_next_grapheme_pos(text.get_line_data(get_caret_line(i))->get_rid(), get_caret_column(i)), i == 0, i); + } } } - } - if (p_select) { - _post_shift_selection(); + if (p_select) { + _post_shift_selection(i); + } } + merge_overlapping_carets(); } void TextEdit::_move_caret_up(bool p_select) { - if (p_select) { - _pre_shift_selection(); - } else { - deselect(); - } + for (int i = 0; i < carets.size(); i++) { + if (p_select) { + _pre_shift_selection(i); + } else { + deselect(i); + } - int cur_wrap_index = get_caret_wrap_index(); - if (cur_wrap_index > 0) { - set_caret_line(caret.line, true, false, cur_wrap_index - 1); - } else if (caret.line == 0) { - set_caret_column(0); - } else { - int new_line = caret.line - get_next_visible_line_offset_from(caret.line - 1, -1); - if (is_line_wrapped(new_line)) { - set_caret_line(new_line, true, false, get_line_wrap_count(new_line)); + int cur_wrap_index = get_caret_wrap_index(i); + if (cur_wrap_index > 0) { + set_caret_line(get_caret_line(i), true, false, cur_wrap_index - 1, i); + } else if (get_caret_line(i) == 0) { + set_caret_column(0, i == 0, i); } else { - set_caret_line(new_line, true, false); + int new_line = get_caret_line(i) - get_next_visible_line_offset_from(get_caret_line(i) - 1, -1); + if (is_line_wrapped(new_line)) { + set_caret_line(new_line, i == 0, false, get_line_wrap_count(new_line), i); + } else { + set_caret_line(new_line, i == 0, false, 0, i); + } } - } - if (p_select) { - _post_shift_selection(); + if (p_select) { + _post_shift_selection(i); + } } + merge_overlapping_carets(); } void TextEdit::_move_caret_down(bool p_select) { - if (p_select) { - _pre_shift_selection(); - } else { - deselect(); - } + for (int i = 0; i < carets.size(); i++) { + if (p_select) { + _pre_shift_selection(i); + } else { + deselect(i); + } - int cur_wrap_index = get_caret_wrap_index(); - if (cur_wrap_index < get_line_wrap_count(caret.line)) { - set_caret_line(caret.line, true, false, cur_wrap_index + 1); - } else if (caret.line == get_last_unhidden_line()) { - set_caret_column(text[caret.line].length()); - } else { - int new_line = caret.line + get_next_visible_line_offset_from(CLAMP(caret.line + 1, 0, text.size() - 1), 1); - set_caret_line(new_line, true, false, 0); - } + int cur_wrap_index = get_caret_wrap_index(i); + if (cur_wrap_index < get_line_wrap_count(get_caret_line(i))) { + set_caret_line(get_caret_line(i), i == 0, false, cur_wrap_index + 1, i); + } else if (get_caret_line(i) == get_last_unhidden_line()) { + set_caret_column(text[get_caret_line(i)].length()); + } else { + int new_line = get_caret_line(i) + get_next_visible_line_offset_from(CLAMP(get_caret_line(i) + 1, 0, text.size() - 1), 1); + set_caret_line(new_line, i == 0, false, 0, i); + } - if (p_select) { - _post_shift_selection(); + if (p_select) { + _post_shift_selection(i); + } } + merge_overlapping_carets(); } void TextEdit::_move_caret_to_line_start(bool p_select) { - if (p_select) { - _pre_shift_selection(); - } else { - deselect(); - } + for (int i = 0; i < carets.size(); i++) { + if (p_select) { + _pre_shift_selection(i); + } else { + deselect(i); + } - // Move caret column to start of wrapped row and then to start of text. - Vector<String> rows = get_line_wrapped_text(caret.line); - int wi = get_caret_wrap_index(); - int row_start_col = 0; - for (int i = 0; i < wi; i++) { - row_start_col += rows[i].length(); - } - if (caret.column == row_start_col || wi == 0) { - // Compute whitespace symbols sequence length. - int current_line_whitespace_len = get_first_non_whitespace_column(caret.line); - if (get_caret_column() == current_line_whitespace_len) { - set_caret_column(0); + // Move caret column to start of wrapped row and then to start of text. + Vector<String> rows = get_line_wrapped_text(get_caret_line(i)); + int wi = get_caret_wrap_index(i); + int row_start_col = 0; + for (int j = 0; j < wi; j++) { + row_start_col += rows[j].length(); + } + if (get_caret_column(i) == row_start_col || wi == 0) { + // Compute whitespace symbols sequence length. + int current_line_whitespace_len = get_first_non_whitespace_column(get_caret_line(i)); + if (get_caret_column(i) == current_line_whitespace_len) { + set_caret_column(0, i == 0, i); + } else { + set_caret_column(current_line_whitespace_len, i == 0, i); + } } else { - set_caret_column(current_line_whitespace_len); + set_caret_column(row_start_col, i == 0, i); } - } else { - set_caret_column(row_start_col); - } - if (p_select) { - _post_shift_selection(); + if (p_select) { + _post_shift_selection(i); + } } + merge_overlapping_carets(); } void TextEdit::_move_caret_to_line_end(bool p_select) { - if (p_select) { - _pre_shift_selection(); - } else { - deselect(); - } + for (int i = 0; i < carets.size(); i++) { + if (p_select) { + _pre_shift_selection(i); + } else { + deselect(i); + } - // Move caret column to end of wrapped row and then to end of text. - Vector<String> rows = get_line_wrapped_text(caret.line); - int wi = get_caret_wrap_index(); - int row_end_col = -1; - for (int i = 0; i < wi + 1; i++) { - row_end_col += rows[i].length(); - } - if (wi == rows.size() - 1 || caret.column == row_end_col) { - set_caret_column(text[caret.line].length()); - } else { - set_caret_column(row_end_col); - } + // Move caret column to end of wrapped row and then to end of text. + Vector<String> rows = get_line_wrapped_text(get_caret_line(i)); + int wi = get_caret_wrap_index(i); + int row_end_col = -1; + for (int j = 0; j < wi + 1; j++) { + row_end_col += rows[j].length(); + } + if (wi == rows.size() - 1 || get_caret_column(i) == row_end_col) { + set_caret_column(text[get_caret_line(i)].length(), i == 0, i); + } else { + set_caret_column(row_end_col, i == 0, i); + } - if (p_select) { - _post_shift_selection(); + carets.write[i].last_fit_x = INT_MAX; + + if (p_select) { + _post_shift_selection(i); + } } + merge_overlapping_carets(); } void TextEdit::_move_caret_page_up(bool p_select) { - if (p_select) { - _pre_shift_selection(); - } else { - deselect(); - } + for (int i = 0; i < carets.size(); i++) { + if (p_select) { + _pre_shift_selection(i); + } else { + deselect(i); + } - Point2i next_line = get_next_visible_line_index_offset_from(caret.line, get_caret_wrap_index(), -get_visible_line_count()); - int n_line = caret.line - next_line.x + 1; - set_caret_line(n_line, true, false, next_line.y); + Point2i next_line = get_next_visible_line_index_offset_from(get_caret_line(i), get_caret_wrap_index(i), -get_visible_line_count()); + int n_line = get_caret_line(i) - next_line.x + 1; + set_caret_line(n_line, i == 0, false, next_line.y, i); - if (p_select) { - _post_shift_selection(); + if (p_select) { + _post_shift_selection(i); + } } + merge_overlapping_carets(); } void TextEdit::_move_caret_page_down(bool p_select) { - if (p_select) { - _pre_shift_selection(); - } else { - deselect(); - } + for (int i = 0; i < carets.size(); i++) { + if (p_select) { + _pre_shift_selection(i); + } else { + deselect(i); + } - Point2i next_line = get_next_visible_line_index_offset_from(caret.line, get_caret_wrap_index(), get_visible_line_count()); - int n_line = caret.line + next_line.x - 1; - set_caret_line(n_line, true, false, next_line.y); + Point2i next_line = get_next_visible_line_index_offset_from(get_caret_line(i), get_caret_wrap_index(i), get_visible_line_count()); + int n_line = get_caret_line(i) + next_line.x - 1; + set_caret_line(n_line, i == 0, false, next_line.y, i); - if (p_select) { - _post_shift_selection(); + if (p_select) { + _post_shift_selection(i); + } } + merge_overlapping_carets(); } void TextEdit::_do_backspace(bool p_word, bool p_all_to_left) { - if (!editable || (caret.column == 0 && caret.line == 0 && !has_selection())) { + if (!editable) { return; } - if (has_selection() || (!p_all_to_left && !p_word) || caret.column == 0) { - backspace(); - return; - } + start_action(EditAction::ACTION_BACKSPACE); + Vector<int> carets_to_remove; - if (p_all_to_left) { - int caret_current_column = caret.column; - set_caret_column(0); - _remove_text(caret.line, 0, caret.line, caret_current_column); - return; - } + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (int i = 0; i < caret_edit_order.size(); i++) { + int caret_idx = caret_edit_order[i]; + if (get_caret_column(caret_idx) == 0 && get_caret_line(caret_idx) == 0 && !has_selection(caret_idx)) { + continue; + } - if (p_word) { - int column = caret.column; - // Check for the case "<word><space><caret>" and ignore the space. - // No need to check for column being 0 since it is checked above. - if (is_whitespace(text[caret.line][caret.column - 1])) { - column -= 1; + if (has_selection(caret_idx) || (!p_all_to_left && !p_word) || get_caret_column(caret_idx) == 0) { + backspace(caret_idx); + continue; } - // Get a list with the indices of the word bounds of the given text line. - const PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(caret.line)->get_rid()); - if (words.is_empty() || column <= words[0]) { - // If "words" is empty, meaning no words are left, we can remove everything until the beginning of the line. - column = 0; - } else { - // Otherwise search for the first word break that is smaller than the index from which we're currently deleting. - for (int i = words.size() - 2; i >= 0; i = i - 2) { - if (words[i] < column) { - column = words[i]; + + if (p_all_to_left) { + int caret_current_column = get_caret_column(caret_idx); + set_caret_column(0, caret_idx == 0, caret_idx); + _remove_text(get_caret_line(caret_idx), 0, get_caret_line(caret_idx), caret_current_column); + adjust_carets_after_edit(caret_idx, get_caret_line(caret_idx), caret_current_column, get_caret_line(caret_idx), get_caret_column(caret_idx)); + + // Check for any overlapping carets since we removed the entire line. + for (int j = i + 1; j < caret_edit_order.size(); j++) { + // Selection only end on this line, only the one as carets cannot overlap. + if (has_selection(caret_edit_order[j]) && get_selection_from_line(caret_edit_order[j]) != get_caret_line(caret_idx) && get_selection_to_line(caret_edit_order[j]) == get_caret_line(caret_idx)) { + carets.write[caret_edit_order[j]].selection.to_column = 0; + break; + } + + // Check for caret. + if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx) || (has_selection(caret_edit_order[j]) && get_selection_from_line(caret_edit_order[j]) != get_caret_line(caret_idx))) { break; } + + deselect(caret_edit_order[j]); + carets_to_remove.push_back(caret_edit_order[j]); + set_caret_column(0, caret_idx == 0, caret_idx); + i = j; } + continue; } - _remove_text(caret.line, column, caret.line, caret.column); + if (p_word) { + // Save here as the caret may change when resolving overlaps. + int from_column = get_caret_column(caret_idx); + int column = get_caret_column(caret_idx); + // Check for the case "<word><space><caret>" and ignore the space. + // No need to check for column being 0 since it is checked above. + if (is_whitespace(text[get_caret_line(caret_idx)][get_caret_column(caret_idx) - 1])) { + column -= 1; + } + // Get a list with the indices of the word bounds of the given text line. + const PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(caret_idx))->get_rid()); + if (words.is_empty() || column <= words[0]) { + // If "words" is empty, meaning no words are left, we can remove everything until the beginning of the line. + column = 0; + } else { + // Otherwise search for the first word break that is smaller than the index from we're currently deleting. + for (int c = words.size() - 2; c >= 0; c = c - 2) { + if (words[c] < column) { + column = words[c]; + break; + } + } + } - set_caret_line(caret.line, false); - set_caret_column(column); - return; + // Check for any other carets in this range. + int overlapping_caret_index = -1; + for (int j = i + 1; j < caret_edit_order.size(); j++) { + // Check caret and selection in on the right line. + if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx) && (!has_selection(caret_edit_order[j]) || get_selection_to_line(caret_edit_order[j]) != get_caret_line(caret_idx))) { + break; + } + + // If it has a selection, check it ends with in the range. + if ((has_selection(caret_edit_order[j]) && get_selection_to_column(caret_edit_order[j]) < column)) { + break; + } + + // If it has a selection and it starts outside our word, we need to adjust the selection, and handle it later to prevent overlap. + if ((has_selection(caret_edit_order[j]) && get_selection_from_column(caret_edit_order[j]) < column)) { + carets.write[caret_edit_order[j]].selection.to_column = column; + overlapping_caret_index = caret_edit_order[j]; + break; + } + + // Otherwise we can remove it. + if (get_caret_column(caret_edit_order[j]) > column || (has_selection(caret_edit_order[j]) && get_selection_from_column(caret_edit_order[j]) > column)) { + deselect(caret_edit_order[j]); + carets_to_remove.push_back(caret_edit_order[j]); + set_caret_column(0, caret_idx == 0, caret_idx); + i = j; + } + } + + _remove_text(get_caret_line(caret_idx), column, get_caret_line(caret_idx), from_column); + + set_caret_line(get_caret_line(caret_idx), false, true, 0, caret_idx); + set_caret_column(column, caret_idx == 0, caret_idx); + + // Now we can clean up the overlaping caret. + if (overlapping_caret_index != -1) { + backspace(overlapping_caret_index); + i++; + carets_to_remove.push_back(overlapping_caret_index); + set_caret_column(get_caret_column(overlapping_caret_index), caret_idx == 0, caret_idx); + } + continue; + } + } + + // Sort and remove backwards to preserve indexes. + carets_to_remove.sort(); + for (int i = carets_to_remove.size() - 1; i >= 0; i--) { + remove_caret(carets_to_remove[i]); } + end_action(); } void TextEdit::_delete(bool p_word, bool p_all_to_right) { @@ -2482,82 +2634,147 @@ void TextEdit::_delete(bool p_word, bool p_all_to_right) { return; } - if (has_selection()) { - delete_selection(); - return; - } - int curline_len = text[caret.line].length(); - - if (caret.line == text.size() - 1 && caret.column == curline_len) { - return; // Last line, last column: Nothing to do. - } + start_action(EditAction::ACTION_DELETE); + Vector<int> carets_to_remove; - int next_line = caret.column < curline_len ? caret.line : caret.line + 1; - int next_column; + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (int i = 0; i < caret_edit_order.size(); i++) { + int caret_idx = caret_edit_order[i]; + if (has_selection(caret_idx)) { + delete_selection(caret_idx); + continue; + } + int curline_len = text[get_caret_line(caret_idx)].length(); - if (p_all_to_right) { - if (caret.column == curline_len) { - return; + if (get_caret_line(caret_idx) == text.size() - 1 && get_caret_column(caret_idx) == curline_len) { + continue; // Last line, last column: Nothing to do. } - // Delete everything to right of caret - next_column = curline_len; - next_line = caret.line; - } else if (p_word && caret.column < curline_len - 1) { - // Delete next word to right of caret - int line = caret.line; - int column = caret.column; + int next_line = get_caret_column(caret_idx) < curline_len ? get_caret_line(caret_idx) : get_caret_line(caret_idx) + 1; + int next_column; - PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid()); - for (int i = 1; i < words.size(); i = i + 2) { - if (words[i] > column) { - column = words[i]; - break; + if (p_all_to_right) { + // Get caret furthest to the left + for (int j = i + 1; j < caret_edit_order.size(); j++) { + if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx)) { + break; + } + + if (has_selection(caret_edit_order[j]) && get_selection_from_line(caret_edit_order[j]) != get_caret_line(caret_idx)) { + break; + } + + if (!has_selection(caret_edit_order[j])) { + i = j; + caret_idx = caret_edit_order[i]; + } } - } - next_line = line; - next_column = column; - } else { - // Delete one character - if (caret_mid_grapheme_enabled) { - next_column = caret.column < curline_len ? (caret.column + 1) : 0; + if (get_caret_column(caret_idx) == curline_len) { + continue; + } + + // Delete everything to right of caret + next_column = curline_len; + next_line = get_caret_line(caret_idx); + + // Remove overlapping carets. + for (int j = i - 1; j >= 0; j--) { + if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx)) { + break; + } + carets_to_remove.push_back(caret_edit_order[j]); + } + + } else if (p_word && get_caret_column(caret_idx) < curline_len - 1) { + // Delete next word to right of caret + int line = get_caret_line(caret_idx); + int column = get_caret_column(caret_idx); + + PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid()); + for (int j = 1; j < words.size(); j = j + 2) { + if (words[j] > column) { + column = words[j]; + break; + } + } + + next_line = line; + next_column = column; + + // Remove overlapping carets. + for (int j = i - 1; j >= 0; j--) { + if (get_caret_line(caret_edit_order[j]) != get_caret_line(caret_idx)) { + break; + } + + if (get_caret_column(caret_edit_order[j]) > column) { + break; + } + carets_to_remove.push_back(caret_edit_order[j]); + } } else { - next_column = caret.column < curline_len ? TS->shaped_text_next_grapheme_pos(text.get_line_data(caret.line)->get_rid(), (caret.column)) : 0; + // Delete one character + if (caret_mid_grapheme_enabled) { + next_column = get_caret_column(caret_idx) < curline_len ? (get_caret_column(caret_idx) + 1) : 0; + } else { + next_column = get_caret_column(caret_idx) < curline_len ? TS->shaped_text_next_grapheme_pos(text.get_line_data(get_caret_line(caret_idx))->get_rid(), (get_caret_column(caret_idx))) : 0; + } + + // Remove overlapping carets. + if (i > 0) { + int prev_caret_idx = caret_edit_order[i - 1]; + if (get_caret_line(prev_caret_idx) == next_line && get_caret_column(prev_caret_idx) == next_column) { + carets_to_remove.push_back(prev_caret_idx); + } + } } + + _remove_text(get_caret_line(caret_idx), get_caret_column(caret_idx), next_line, next_column); + adjust_carets_after_edit(caret_idx, get_caret_line(caret_idx), get_caret_column(caret_idx), next_line, next_column); + } + + // Sort and remove backwards to preserve indexes. + carets_to_remove.sort(); + for (int i = carets_to_remove.size() - 1; i >= 0; i--) { + remove_caret(carets_to_remove[i]); } - _remove_text(caret.line, caret.column, next_line, next_column); + // If we are deleting from the end of a line, due to column preservation we could still overlap with another caret. + merge_overlapping_carets(); + end_action(); queue_redraw(); } void TextEdit::_move_caret_document_start(bool p_select) { + remove_secondary_carets(); if (p_select) { - _pre_shift_selection(); + _pre_shift_selection(0); } else { deselect(); } - set_caret_line(0); + set_caret_line(0, false); set_caret_column(0); if (p_select) { - _post_shift_selection(); + _post_shift_selection(0); } } void TextEdit::_move_caret_document_end(bool p_select) { + remove_secondary_carets(); if (p_select) { - _pre_shift_selection(); + _pre_shift_selection(0); } else { deselect(); } set_caret_line(get_last_unhidden_line(), true, false, 9999); - set_caret_column(text[caret.line].length()); + set_caret_column(text[get_caret_line()].length()); if (p_select) { - _post_shift_selection(); + _post_shift_selection(0); } } @@ -2677,7 +2894,7 @@ bool TextEdit::is_text_field() const { } Variant TextEdit::get_drag_data(const Point2 &p_point) { - if (selection.active && selection.drag_attempt) { + if (has_selection() && selection_drag_attempt) { String t = get_selected_text(); Label *l = memnew(Label); l->set_text(t); @@ -2704,34 +2921,41 @@ void TextEdit::drop_data(const Point2 &p_point, const Variant &p_data) { Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); int caret_row_tmp = pos.y; int caret_column_tmp = pos.x; - if (selection.drag_attempt) { - selection.drag_attempt = false; + if (selection_drag_attempt) { + selection_drag_attempt = false; if (!is_mouse_over_selection(!Input::get_singleton()->is_key_pressed(Key::CTRL))) { + // Set caret back at selection for undo / redo. + set_caret_line(get_selection_to_line(), false, false); + set_caret_column(get_selection_to_column()); + begin_complex_operation(); if (!Input::get_singleton()->is_key_pressed(Key::CTRL)) { - if (caret_row_tmp > selection.to_line) { - caret_row_tmp = caret_row_tmp - (selection.to_line - selection.from_line); - } else if (caret_row_tmp == selection.to_line && caret_column_tmp >= selection.to_column) { - caret_column_tmp = caret_column_tmp - (selection.to_column - selection.from_column); + if (caret_row_tmp > get_selection_to_line()) { + caret_row_tmp = caret_row_tmp - (get_selection_to_line() - get_selection_from_line()); + } else if (caret_row_tmp == get_selection_to_line() && caret_column_tmp >= get_selection_to_column()) { + caret_column_tmp = caret_column_tmp - (get_selection_to_column() - get_selection_from_column()); } delete_selection(); } else { deselect(); } + remove_secondary_carets(); set_caret_line(caret_row_tmp, true, false); set_caret_column(caret_column_tmp); insert_text_at_caret(p_data); end_complex_operation(); } } else if (is_mouse_over_selection()) { - caret_row_tmp = selection.from_line; - caret_column_tmp = selection.from_column; + remove_secondary_carets(); + caret_row_tmp = get_selection_from_line(); + caret_column_tmp = get_selection_from_column(); set_caret_line(caret_row_tmp, true, false); set_caret_column(caret_column_tmp); insert_text_at_caret(p_data); grab_focus(); } else { + remove_secondary_carets(); deselect(); set_caret_line(caret_row_tmp, true, false); set_caret_column(caret_column_tmp); @@ -2739,8 +2963,8 @@ void TextEdit::drop_data(const Point2 &p_point, const Variant &p_data) { grab_focus(); } - if (caret_row_tmp != caret.line || caret_column_tmp != caret.column) { - select(caret_row_tmp, caret_column_tmp, caret.line, caret.column); + if (caret_row_tmp != get_caret_line() || caret_column_tmp != get_caret_column()) { + select(caret_row_tmp, caret_column_tmp, get_caret_line(), get_caret_column()); } } } @@ -2976,6 +3200,7 @@ void TextEdit::clear() { void TextEdit::_clear() { if (editable && undo_enabled) { + remove_secondary_carets(); _move_caret_document_start(false); begin_complex_operation(); @@ -2991,13 +3216,14 @@ void TextEdit::_clear() { clear_undo_history(); text.clear(); + remove_secondary_carets(); set_caret_line(0, false); set_caret_column(0); - caret.x_ofs = 0; - caret.line_ofs = 0; - caret.wrap_ofs = 0; - caret.last_fit_x = 0; - selection.active = false; + first_visible_col = 0; + first_visible_line = 0; + first_visible_line_wrap_ofs = 0; + carets.write[0].last_fit_x = 0; + deselect(); emit_signal(SNAME("lines_edited_from"), old_text_size, 0); } @@ -3010,6 +3236,7 @@ void TextEdit::set_text(const String &p_text) { } if (undo_enabled) { + remove_secondary_carets(); set_caret_line(0); set_caret_column(0); @@ -3065,11 +3292,14 @@ void TextEdit::set_line(int p_line, const String &p_new_text) { begin_complex_operation(); _remove_text(p_line, 0, p_line, text[p_line].length()); _insert_text(p_line, 0, p_new_text); - if (caret.line == p_line && caret.column > p_new_text.length()) { - set_caret_column(p_new_text.length(), false); - } - if (has_selection() && p_line == selection.to_line && selection.to_column > text[p_line].length()) { - selection.to_column = text[p_line].length(); + for (int i = 0; i < carets.size(); i++) { + if (get_caret_line(i) == p_line && get_caret_column(i) > p_new_text.length()) { + set_caret_column(p_new_text.length(), false, i); + } + + if (has_selection(i) && p_line == get_selection_to_line(i) && get_selection_to_column(i) > text[p_line].length()) { + carets.write[i].selection.to_column = text[p_line].length(); + } } end_complex_operation(); } @@ -3136,42 +3366,54 @@ void TextEdit::insert_line_at(int p_at, const String &p_text) { ERR_FAIL_INDEX(p_at, text.size()); _insert_text(p_at, 0, p_text + "\n"); - if (caret.line >= p_at) { - // offset caret when located after inserted line - set_caret_line(caret.line + 1, false); - } - if (has_selection()) { - if (selection.from_line >= p_at) { - // offset selection when located after inserted line - ++selection.from_line; - ++selection.to_line; - } else if (selection.to_line >= p_at) { - // extend selection that includes inserted line - ++selection.to_line; + + for (int i = 0; i < carets.size(); i++) { + if (get_caret_line(i) >= p_at) { + // offset caret when located after inserted line + set_caret_line(get_caret_line(i) + 1, false, true, 0, i); + } + if (has_selection(i)) { + if (get_selection_from_line(i) >= p_at) { + // offset selection when located after inserted line + select(get_selection_from_line(i) + 1, get_selection_from_column(i), get_selection_to_line(i) + 1, get_selection_to_column(i), i); + } else if (get_selection_to_line(i) >= p_at) { + // extend selection that includes inserted line + select(get_selection_from_line(i), get_selection_from_column(i), get_selection_to_line(i) + 1, get_selection_to_column(i), i); + } } } + + // Need to apply the above adjustments to the undo / redo carets. + current_op.end_carets = carets; queue_redraw(); } -void TextEdit::insert_text_at_caret(const String &p_text) { - bool had_selection = has_selection(); - if (had_selection) { - begin_complex_operation(); - } +void TextEdit::insert_text_at_caret(const String &p_text, int p_caret) { + ERR_FAIL_COND(p_caret > carets.size()); + + begin_complex_operation(); + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (const int &i : caret_edit_order) { + if (p_caret != -1 && p_caret != i) { + continue; + } - delete_selection(); + delete_selection(i); - int new_column, new_line; - _insert_text(caret.line, caret.column, p_text, &new_line, &new_column); - _update_scrollbars(); + int from_line = get_caret_line(i); + int from_col = get_caret_column(i); - set_caret_line(new_line, false); - set_caret_column(new_column); - queue_redraw(); + int new_column, new_line; + _insert_text(from_line, from_col, p_text, &new_line, &new_column); + _update_scrollbars(); - if (had_selection) { - end_complex_operation(); + set_caret_line(new_line, false, true, 0, i); + set_caret_column(new_column, i == 0, i); + + adjust_carets_after_edit(i, new_line, new_column, from_line, from_col); } + end_complex_operation(); + queue_redraw(); } void TextEdit::remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column) { @@ -3295,46 +3537,46 @@ Point2i TextEdit::get_next_visible_line_index_offset_from(int p_line_from, int p } // Overridable actions -void TextEdit::handle_unicode_input(const uint32_t p_unicode) { - if (GDVIRTUAL_CALL(_handle_unicode_input, p_unicode)) { +void TextEdit::handle_unicode_input(const uint32_t p_unicode, int p_caret) { + if (GDVIRTUAL_CALL(_handle_unicode_input, p_unicode, p_caret)) { return; } - _handle_unicode_input_internal(p_unicode); + _handle_unicode_input_internal(p_unicode, p_caret); } -void TextEdit::backspace() { - if (GDVIRTUAL_CALL(_backspace)) { +void TextEdit::backspace(int p_caret) { + if (GDVIRTUAL_CALL(_backspace, p_caret)) { return; } - _backspace_internal(); + _backspace_internal(p_caret); } -void TextEdit::cut() { - if (GDVIRTUAL_CALL(_cut)) { +void TextEdit::cut(int p_caret) { + if (GDVIRTUAL_CALL(_cut, p_caret)) { return; } - _cut_internal(); + _cut_internal(p_caret); } -void TextEdit::copy() { - if (GDVIRTUAL_CALL(_copy)) { +void TextEdit::copy(int p_caret) { + if (GDVIRTUAL_CALL(_copy, p_caret)) { return; } - _copy_internal(); + _copy_internal(p_caret); } -void TextEdit::paste() { - if (GDVIRTUAL_CALL(_paste)) { +void TextEdit::paste(int p_caret) { + if (GDVIRTUAL_CALL(_paste, p_caret)) { return; } - _paste_internal(); + _paste_internal(p_caret); } -void TextEdit::paste_primary_clipboard() { - if (GDVIRTUAL_CALL(_paste_primary_clipboard)) { +void TextEdit::paste_primary_clipboard(int p_caret) { + if (GDVIRTUAL_CALL(_paste_primary_clipboard, p_caret)) { return; } - _paste_primary_clipboard_internal(); + _paste_primary_clipboard_internal(p_caret); } // Context menu. @@ -3471,22 +3713,53 @@ void TextEdit::menu_option(int p_option) { } /* Versioning */ +void TextEdit::start_action(EditAction p_action) { + if (current_action != p_action) { + if (current_action != EditAction::ACTION_NONE) { + in_action = false; + pending_action_end = false; + end_complex_operation(); + } + + if (p_action != EditAction::ACTION_NONE) { + in_action = true; + begin_complex_operation(); + } + } else if (current_action != EditAction::ACTION_NONE) { + pending_action_end = false; + } + current_action = p_action; +} + +void TextEdit::end_action() { + if (current_action != EditAction::ACTION_NONE) { + pending_action_end = true; + } +} + +TextEdit::EditAction TextEdit::get_current_action() const { + return current_action; +} + void TextEdit::begin_complex_operation() { _push_current_op(); if (complex_operation_count == 0) { next_operation_is_complex = true; + current_op.start_carets = carets; } complex_operation_count++; } void TextEdit::end_complex_operation() { _push_current_op(); - ERR_FAIL_COND(undo_stack.size() == 0); complex_operation_count = MAX(complex_operation_count - 1, 0); if (complex_operation_count > 0) { return; } + ERR_FAIL_COND(undo_stack.size() == 0); + + undo_stack.back()->get().end_carets = carets; if (undo_stack.back()->get().chain_forward) { undo_stack.back()->get().chain_forward = false; return; @@ -3546,17 +3819,24 @@ void TextEdit::undo() { } } - if (op.type != TextOperation::TYPE_INSERT && (op.from_line != op.to_line || op.to_column != op.from_column + 1)) { - select(op.from_line, op.from_column, op.to_line, op.to_column); + _update_scrollbars(); + bool dirty_carets = carets.size() != undo_stack_pos->get().start_carets.size(); + if (!dirty_carets) { + for (int i = 0; i < carets.size(); i++) { + if (carets[i].line != undo_stack_pos->get().start_carets[i].line || carets[i].column != undo_stack_pos->get().start_carets[i].column) { + dirty_carets = true; + break; + } + } } - _update_scrollbars(); - if (undo_stack_pos->get().type == TextOperation::TYPE_REMOVE) { - set_caret_line(undo_stack_pos->get().to_line, false); - set_caret_column(undo_stack_pos->get().to_column); - } else { - set_caret_line(undo_stack_pos->get().from_line, false); - set_caret_column(undo_stack_pos->get().from_column); + carets = undo_stack_pos->get().start_carets; + + if (dirty_carets && !caret_pos_dirty) { + if (is_inside_tree()) { + MessageQueue::get_singleton()->push_call(this, "_emit_caret_changed"); + } + caret_pos_dirty = true; } queue_redraw(); } @@ -3565,6 +3845,7 @@ void TextEdit::redo() { if (!editable) { return; } + _push_current_op(); if (undo_stack_pos == nullptr) { @@ -3590,9 +3871,25 @@ void TextEdit::redo() { } _update_scrollbars(); - set_caret_line(undo_stack_pos->get().to_line, false); - set_caret_column(undo_stack_pos->get().to_column); + bool dirty_carets = carets.size() != undo_stack_pos->get().end_carets.size(); + if (!dirty_carets) { + for (int i = 0; i < carets.size(); i++) { + if (carets[i].line != undo_stack_pos->get().end_carets[i].line || carets[i].column != undo_stack_pos->get().end_carets[i].column) { + dirty_carets = true; + break; + } + } + } + + carets = undo_stack_pos->get().end_carets; undo_stack_pos = undo_stack_pos->next(); + + if (dirty_carets && !caret_pos_dirty) { + if (is_inside_tree()) { + MessageQueue::get_singleton()->push_call(this, "_emit_caret_changed"); + } + caret_pos_dirty = true; + } queue_redraw(); } @@ -3604,7 +3901,7 @@ void TextEdit::clear_undo_history() { } bool TextEdit::is_insert_text_operation() const { - return (current_op.type == TextOperation::TYPE_INSERT); + return (current_op.type == TextOperation::TYPE_INSERT || current_action == EditAction::ACTION_TYPING); } void TextEdit::tag_saved_version() { @@ -3793,7 +4090,7 @@ Point2i TextEdit::get_line_column_at_pos(const Point2i &p_pos, bool p_allow_out_ int wrap_index = 0; if (get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE || _is_hiding_enabled()) { - Point2i f_ofs = get_next_visible_line_index_offset_from(first_vis_line, caret.wrap_ofs, rows + (1 * SIGN(rows))); + Point2i f_ofs = get_next_visible_line_index_offset_from(first_vis_line, first_visible_line_wrap_ofs, rows + (1 * SIGN(rows))); wrap_index = f_ofs.y; if (rows < 0) { @@ -3821,7 +4118,7 @@ Point2i TextEdit::get_line_column_at_pos(const Point2i &p_pos, bool p_allow_out_ int col = 0; int colx = p_pos.x - (style_normal->get_margin(SIDE_LEFT) + gutters_width + gutter_padding); - colx += caret.x_ofs; + colx += first_visible_col; col = _get_char_pos_for_line(colx, row, wrap_index); if (get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && wrap_index < get_line_wrap_count(row)) { // Move back one if we are at the end of the row. @@ -3918,7 +4215,7 @@ int TextEdit::get_minimap_line_at_pos(const Point2i &p_pos) const { int row = minimap_line + Math::floor(rows); if (get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE || _is_hiding_enabled()) { - int f_ofs = get_next_visible_line_index_offset_from(minimap_line, caret.wrap_ofs, rows + (1 * SIGN(rows))).x - 1; + int f_ofs = get_next_visible_line_index_offset_from(minimap_line, first_visible_line_wrap_ofs, rows + (1 * SIGN(rows))).x - 1; if (rows < 0) { row = minimap_line - f_ofs; } else { @@ -3941,19 +4238,31 @@ bool TextEdit::is_dragging_cursor() const { return dragging_selection || dragging_minimap; } -bool TextEdit::is_mouse_over_selection(bool p_edges) const { - if (!has_selection()) { - return false; - } - Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); - int row = pos.y; - int col = pos.x; - if (p_edges) { - if ((row == selection.from_line && col == selection.from_column) || (row == selection.to_line && col == selection.to_column)) { +bool TextEdit::is_mouse_over_selection(bool p_edges, int p_caret) const { + for (int i = 0; i < carets.size(); i++) { + if (p_caret != -1 && p_caret != i) { + continue; + } + + if (!has_selection(i)) { + continue; + } + + Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); + int row = pos.y; + int col = pos.x; + if (p_edges) { + if ((row == get_selection_from_line(i) && col == get_selection_from_column(i)) || (row == get_selection_to_line(i) && col == get_selection_to_column(i))) { + return true; + } + } + + if (row >= get_selection_from_line(i) && row <= get_selection_to_line(i) && (row > get_selection_from_line(i) || col > get_selection_from_column(i)) && (row < get_selection_to_line(i) || col < get_selection_to_column(i))) { return true; } } - return (row >= selection.from_line && row <= selection.to_line && (row > selection.from_line || col > selection.from_column) && (row < selection.to_line || col < selection.to_column)); + + return false; } /* Caret */ @@ -4016,15 +4325,233 @@ bool TextEdit::is_caret_mid_grapheme_enabled() const { return caret_mid_grapheme_enabled; } -bool TextEdit::is_caret_visible() const { - return caret.visible; +void TextEdit::set_multiple_carets_enabled(bool p_enabled) { + multi_carets_enabled = p_enabled; + if (!multi_carets_enabled) { + remove_secondary_carets(); + } +} + +bool TextEdit::is_multiple_carets_enabled() const { + return multi_carets_enabled; +} + +int TextEdit::add_caret(int p_line, int p_col) { + if (!multi_carets_enabled) { + return -1; + } + + p_line = CLAMP(p_line, 0, text.size() - 1); + p_col = CLAMP(p_col, 0, get_line(p_line).length()); + + for (int i = 0; i < carets.size(); i++) { + if (get_caret_line(i) == p_line && get_caret_column(i) == p_col) { + return -1; + } + + if (has_selection(i)) { + if (p_line >= get_selection_from_line(i) && p_line <= get_selection_to_line(i) && (p_line > get_selection_from_line(i) || p_col >= get_selection_from_column(i)) && (p_line < get_selection_to_line(i) || p_col <= get_selection_to_column(i))) { + return -1; + } + } + } + + carets.push_back(Caret()); + set_caret_line(p_line, false, false, 0, carets.size() - 1); + set_caret_column(p_col, false, carets.size() - 1); + caret_index_edit_dirty = true; + return carets.size() - 1; +} + +void TextEdit::remove_caret(int p_caret) { + ERR_FAIL_COND(carets.size() <= 0); + ERR_FAIL_INDEX(p_caret, carets.size()); + carets.remove_at(p_caret); + caret_index_edit_dirty = true; +} + +void TextEdit::remove_secondary_carets() { + carets.resize(1); + caret_index_edit_dirty = true; + queue_redraw(); +} + +void TextEdit::merge_overlapping_carets() { + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (int i = 0; i < caret_edit_order.size() - 1; i++) { + int first_caret = caret_edit_order[i]; + int second_caret = caret_edit_order[i + 1]; + + // Both have selection. + if (has_selection(first_caret) && has_selection(second_caret)) { + bool should_merge = false; + if (get_selection_from_line(first_caret) >= get_selection_from_line(second_caret) && get_selection_from_line(first_caret) <= get_selection_to_line(second_caret) && (get_selection_from_line(first_caret) > get_selection_from_line(second_caret) || get_selection_from_column(first_caret) >= get_selection_from_column(second_caret)) && (get_selection_from_line(first_caret) < get_selection_to_line(second_caret) || get_selection_from_column(first_caret) <= get_selection_to_column(second_caret))) { + should_merge = true; + } + + if (get_selection_to_line(first_caret) >= get_selection_from_line(second_caret) && get_selection_to_line(first_caret) <= get_selection_to_line(second_caret) && (get_selection_to_line(first_caret) > get_selection_from_line(second_caret) || get_selection_to_column(first_caret) >= get_selection_from_column(second_caret)) && (get_selection_to_line(first_caret) < get_selection_to_line(second_caret) || get_selection_to_column(first_caret) <= get_selection_to_column(second_caret))) { + should_merge = true; + } + + if (!should_merge) { + continue; + } + + // Save the newest one for click+drag. + int caret_to_save = first_caret; + int caret_to_remove = second_caret; + if (first_caret < second_caret) { + caret_to_save = second_caret; + caret_to_remove = first_caret; + } + + int from_line = MIN(get_selection_from_line(caret_to_save), get_selection_from_line(caret_to_remove)); + int to_line = MAX(get_selection_to_line(caret_to_save), get_selection_to_line(caret_to_remove)); + int from_col = get_selection_from_column(caret_to_save); + int to_col = get_selection_to_column(caret_to_save); + int selection_line = get_selection_line(caret_to_save); + int selection_col = get_selection_column(caret_to_save); + + bool at_from = (get_caret_line(caret_to_save) == get_selection_from_line(caret_to_save) && get_caret_column(caret_to_save) == get_selection_from_column(caret_to_save)); + + if (at_from) { + if (get_selection_line(caret_to_remove) > get_selection_line(caret_to_save) || (get_selection_line(caret_to_remove) == get_selection_line(caret_to_save) && get_selection_column(caret_to_remove) >= get_selection_column(caret_to_save))) { + selection_line = get_selection_line(caret_to_remove); + selection_col = get_selection_column(caret_to_remove); + } + } else if (get_selection_line(caret_to_remove) < get_selection_line(caret_to_save) || (get_selection_line(caret_to_remove) == get_selection_line(caret_to_save) && get_selection_column(caret_to_remove) <= get_selection_column(caret_to_save))) { + selection_line = get_selection_line(caret_to_remove); + selection_col = get_selection_column(caret_to_remove); + } + + if (get_selection_from_line(caret_to_remove) < get_selection_from_line(caret_to_save) || (get_selection_from_line(caret_to_remove) == get_selection_from_line(caret_to_save) && get_selection_from_column(caret_to_remove) <= get_selection_from_column(caret_to_save))) { + from_col = get_selection_from_column(caret_to_remove); + } else { + to_col = get_selection_to_column(caret_to_remove); + } + + select(from_line, from_col, to_line, to_col, caret_to_save); + set_selection_mode(selecting_mode, selection_line, selection_col, caret_to_save); + set_caret_line((at_from ? from_line : to_line), caret_to_save == 0, true, 0, caret_to_save); + set_caret_column((at_from ? from_col : to_col), caret_to_save == 0, caret_to_save); + remove_caret(caret_to_remove); + i--; + caret_edit_order = get_caret_index_edit_order(); + continue; + } + + // Only first has selection. + if (has_selection(first_caret)) { + if (get_caret_line(second_caret) >= get_selection_from_line(first_caret) && get_caret_line(second_caret) <= get_selection_to_line(first_caret) && (get_caret_line(second_caret) > get_selection_from_line(first_caret) || get_caret_column(second_caret) >= get_selection_from_column(first_caret)) && (get_caret_line(second_caret) < get_selection_to_line(first_caret) || get_caret_column(second_caret) <= get_selection_to_column(first_caret))) { + remove_caret(second_caret); + caret_edit_order = get_caret_index_edit_order(); + i--; + } + continue; + } + + // Only second has Selection. + if (has_selection(second_caret)) { + if (get_caret_line(first_caret) >= get_selection_from_line(second_caret) && get_caret_line(first_caret) <= get_selection_to_line(second_caret) && (get_caret_line(first_caret) > get_selection_from_line(second_caret) || get_caret_column(first_caret) >= get_selection_from_column(second_caret)) && (get_caret_line(first_caret) < get_selection_to_line(second_caret) || get_caret_column(first_caret) <= get_selection_to_column(second_caret))) { + remove_caret(first_caret); + caret_edit_order = get_caret_index_edit_order(); + i--; + } + continue; + } + + // Both have no selection. + if (get_caret_line(first_caret) == get_caret_line(second_caret) && get_caret_column(first_caret) == get_caret_column(second_caret)) { + // Save the newest one for click+drag. + if (first_caret < second_caret) { + remove_caret(first_caret); + } else { + remove_caret(second_caret); + } + i--; + caret_edit_order = get_caret_index_edit_order(); + continue; + } + } } -Point2 TextEdit::get_caret_draw_pos() const { - return caret.draw_pos; +int TextEdit::get_caret_count() const { + return carets.size(); } -void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_hidden, int p_wrap_index) { +Vector<int> TextEdit::get_caret_index_edit_order() { + if (!caret_index_edit_dirty) { + return caret_index_edit_order; + } + + caret_index_edit_order.clear(); + caret_index_edit_order.push_back(0); + for (int i = 1; i < carets.size(); i++) { + int j = 0; + + int line = carets[i].selection.active ? carets[i].selection.to_line : carets[i].line; + int col = carets[i].selection.active ? carets[i].selection.to_column : carets[i].column; + + for (; j < caret_index_edit_order.size(); j++) { + int idx = caret_index_edit_order[j]; + int other_line = carets[idx].selection.active ? carets[idx].selection.to_line : carets[idx].line; + int other_col = carets[idx].selection.active ? carets[idx].selection.to_column : carets[idx].column; + if (line > other_line || (line == other_line && col > other_col)) { + break; + } + } + caret_index_edit_order.insert(j, i); + } + caret_index_edit_dirty = false; + return caret_index_edit_order; +} + +void TextEdit::adjust_carets_after_edit(int p_caret, int p_from_line, int p_from_col, int p_to_line, int p_to_col) { + int edit_height = p_from_line - p_to_line; + int edit_size = ((edit_height == 0) ? p_from_col : 0) - p_to_col; + + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (int j = 0; j < caret_edit_order.size(); j++) { + if (caret_edit_order[j] == p_caret) { + return; + } + + // Adjust caret. + // set_caret_line could adjust the column, so save here. + int cc = get_caret_column(caret_edit_order[j]); + if (edit_height != 0) { + set_caret_line(get_caret_line(caret_edit_order[j]) + edit_height, false, true, 0, caret_edit_order[j]); + } + if (get_caret_line(p_caret) == get_caret_line(caret_edit_order[j])) { + set_caret_column(cc + edit_size, false, caret_edit_order[j]); + } + + // Adjust selection. + if (!has_selection(caret_edit_order[j])) { + continue; + } + if (edit_height != 0) { + carets.write[caret_edit_order[j]].selection.from_line += edit_height; + carets.write[caret_edit_order[j]].selection.to_line += edit_height; + } + if (get_caret_line(p_caret) == carets[caret_edit_order[j]].selection.from_line) { + carets.write[caret_edit_order[j]].selection.from_column += edit_size; + } + } +} + +bool TextEdit::is_caret_visible(int p_caret) const { + ERR_FAIL_INDEX_V(p_caret, carets.size(), 0); + return carets[p_caret].visible; +} + +Point2 TextEdit::get_caret_draw_pos(int p_caret) const { + ERR_FAIL_INDEX_V(p_caret, carets.size(), Point2(0, 0)); + return carets[p_caret].draw_pos; +} + +void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_hidden, int p_wrap_index, int p_caret) { + ERR_FAIL_INDEX(p_caret, carets.size()); if (setting_caret_line) { return; } @@ -4053,10 +4580,10 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_ } } } - bool caret_moved = caret.line != p_line; - caret.line = p_line; + bool caret_moved = get_caret_line(p_caret) != p_line; + carets.write[p_caret].line = p_line; - int n_col = _get_char_pos_for_line(caret.last_fit_x, p_line, p_wrap_index); + int n_col = _get_char_pos_for_line(carets[p_caret].last_fit_x, p_line, p_wrap_index); if (n_col != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && p_wrap_index < get_line_wrap_count(p_line)) { Vector<String> rows = get_line_wrapped_text(p_line); int row_end_col = 0; @@ -4067,11 +4594,11 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_ n_col -= 1; } } - caret_moved = (caret_moved || caret.column != n_col); - caret.column = n_col; + caret_moved = (caret_moved || get_caret_column(p_caret) != n_col); + carets.write[p_caret].column = n_col; if (is_inside_tree() && p_adjust_viewport) { - adjust_viewport_to_caret(); + adjust_viewport_to_caret(p_caret); } setting_caret_line = false; @@ -4084,25 +4611,27 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_ } } -int TextEdit::get_caret_line() const { - return caret.line; +int TextEdit::get_caret_line(int p_caret) const { + ERR_FAIL_INDEX_V(p_caret, carets.size(), 0); + return carets[p_caret].line; } -void TextEdit::set_caret_column(int p_col, bool p_adjust_viewport) { +void TextEdit::set_caret_column(int p_col, bool p_adjust_viewport, int p_caret) { + ERR_FAIL_INDEX(p_caret, carets.size()); if (p_col < 0) { p_col = 0; } - if (p_col > get_line(caret.line).length()) { - p_col = get_line(caret.line).length(); + if (p_col > get_line(get_caret_line(p_caret)).length()) { + p_col = get_line(get_caret_line(p_caret)).length(); } - bool caret_moved = caret.column != p_col; - caret.column = p_col; + bool caret_moved = get_caret_column(p_caret) != p_col; + carets.write[p_caret].column = p_col; - caret.last_fit_x = _get_column_x_offset_for_line(caret.column, caret.line); + carets.write[p_caret].last_fit_x = _get_column_x_offset_for_line(get_caret_column(p_caret), get_caret_line(p_caret), get_caret_column(p_caret)); if (is_inside_tree() && p_adjust_viewport) { - adjust_viewport_to_caret(); + adjust_viewport_to_caret(p_caret); } if (caret_moved && !caret_pos_dirty) { @@ -4113,24 +4642,36 @@ void TextEdit::set_caret_column(int p_col, bool p_adjust_viewport) { } } -int TextEdit::get_caret_column() const { - return caret.column; +int TextEdit::get_caret_column(int p_caret) const { + ERR_FAIL_INDEX_V(p_caret, carets.size(), 0); + return carets[p_caret].column; } -int TextEdit::get_caret_wrap_index() const { - return get_line_wrap_index_at_column(caret.line, caret.column); +int TextEdit::get_caret_wrap_index(int p_caret) const { + ERR_FAIL_INDEX_V(p_caret, carets.size(), 0); + return get_line_wrap_index_at_column(get_caret_line(p_caret), get_caret_column(p_caret)); } -String TextEdit::get_word_under_caret() const { - ERR_FAIL_INDEX_V(caret.line, text.size(), ""); - ERR_FAIL_INDEX_V(caret.column, text[caret.line].length() + 1, ""); - PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(caret.line)->get_rid()); - for (int i = 0; i < words.size(); i = i + 2) { - if (words[i] <= caret.column && words[i + 1] > caret.column) { - return text[caret.line].substr(words[i], words[i + 1] - words[i]); +String TextEdit::get_word_under_caret(int p_caret) const { + ERR_FAIL_COND_V(p_caret > carets.size(), ""); + + StringBuilder selected_text; + for (int c = 0; c < carets.size(); c++) { + if (p_caret != -1 && p_caret != c) { + continue; + } + + PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(c))->get_rid()); + for (int i = 0; i < words.size(); i = i + 2) { + if (words[i] <= get_caret_column(c) && words[i + 1] > get_caret_column(c)) { + selected_text += text[get_caret_line(c)].substr(words[i], words[i + 1] - words[i]); + if (p_caret == -1 && c != carets.size() - 1) { + selected_text += "\n"; + } + } } } - return ""; + return selected_text.as_string(); } /* Selection. */ @@ -4156,7 +4697,7 @@ void TextEdit::set_deselect_on_focus_loss_enabled(const bool p_enabled) { } deselect_on_focus_loss_enabled = p_enabled; - if (p_enabled && selection.active && !has_focus()) { + if (p_enabled && has_selection() && !has_focus()) { deselect(); } } @@ -4181,22 +4722,24 @@ bool TextEdit::is_overriding_selected_font_color() const { return override_selected_font_color; } -void TextEdit::set_selection_mode(SelectionMode p_mode, int p_line, int p_column) { - selection.selecting_mode = p_mode; +void TextEdit::set_selection_mode(SelectionMode p_mode, int p_line, int p_column, int p_caret) { + ERR_FAIL_INDEX(p_caret, carets.size()); + + selecting_mode = p_mode; if (p_line >= 0) { ERR_FAIL_INDEX(p_line, text.size()); - selection.selecting_line = p_line; - selection.selecting_column = CLAMP(selection.selecting_column, 0, text[selection.selecting_line].length()); + carets.write[p_caret].selection.selecting_line = p_line; + carets.write[p_caret].selection.selecting_column = CLAMP(carets[p_caret].selection.selecting_column, 0, text[carets[p_caret].selection.selecting_line].length()); } if (p_column >= 0) { - ERR_FAIL_INDEX(selection.selecting_line, text.size()); - ERR_FAIL_INDEX(p_column, text[selection.selecting_line].length()); - selection.selecting_column = p_column; + ERR_FAIL_INDEX(carets[p_caret].selection.selecting_line, text.size()); + ERR_FAIL_INDEX(p_column, text[carets[p_caret].selection.selecting_line].length() + 1); + carets.write[p_caret].selection.selecting_column = p_column; } } TextEdit::SelectionMode TextEdit::get_selection_mode() const { - return selection.selecting_mode; + return selecting_mode; } void TextEdit::select_all() { @@ -4207,21 +4750,19 @@ void TextEdit::select_all() { if (text.size() == 1 && text[0].length() == 0) { return; } - selection.active = true; - selection.from_line = 0; - selection.from_column = 0; - selection.selecting_line = 0; - selection.selecting_column = 0; - selection.to_line = text.size() - 1; - selection.to_column = text[selection.to_line].length(); - selection.selecting_mode = SelectionMode::SELECTION_MODE_SHIFT; - selection.shiftclick_left = true; - set_caret_line(selection.to_line, false); - set_caret_column(selection.to_column, false); + + remove_secondary_carets(); + select(0, 0, text.size() - 1, text[text.size() - 1].length()); + set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT, 0, 0); + carets.write[0].selection.shiftclick_left = true; + set_caret_line(get_selection_to_line(), false); + set_caret_column(get_selection_to_column(), false); queue_redraw(); } -void TextEdit::select_word_under_caret() { +void TextEdit::select_word_under_caret(int p_caret) { + ERR_FAIL_COND(p_caret > carets.size()); + if (!selecting_enabled) { return; } @@ -4230,36 +4771,43 @@ void TextEdit::select_word_under_caret() { return; } - if (selection.active) { - /* Allow toggling selection by pressing the shortcut a second time. */ - /* This is also usable as a general-purpose "deselect" shortcut after */ - /* selecting anything. */ - deselect(); - return; - } + for (int c = 0; c < carets.size(); c++) { + if (p_caret != -1 && p_caret != c) { + continue; + } - int begin = 0; - int end = 0; - const PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(caret.line)->get_rid()); - for (int i = 0; i < words.size(); i = i + 2) { - if ((words[i] <= caret.column && words[i + 1] >= caret.column) || (i == words.size() - 2 && caret.column == words[i + 1])) { - begin = words[i]; - end = words[i + 1]; - break; + if (has_selection(c)) { + // Allow toggling selection by pressing the shortcut a second time. + // This is also usable as a general-purpose "deselect" shortcut after + // selecting anything. + deselect(c); + continue; } - } - // No word found. - if (begin == 0 && end == 0) { - return; - } + int begin = 0; + int end = 0; + const PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(c))->get_rid()); + for (int i = 0; i < words.size(); i = i + 2) { + if ((words[i] <= get_caret_column(c) && words[i + 1] >= get_caret_column(c)) || (i == words.size() - 2 && get_caret_column(c) == words[i + 1])) { + begin = words[i]; + end = words[i + 1]; + break; + } + } - select(caret.line, begin, caret.line, end); - /* Move the caret to the end of the word for easier editing. */ - set_caret_column(end, false); + // No word found. + if (begin == 0 && end == 0) { + continue; + } + + select(get_caret_line(c), begin, get_caret_column(c), end, c); + // Move the caret to the end of the word for easier editing. + set_caret_column(end, false, c); + } } -void TextEdit::select(int p_from_line, int p_from_column, int p_to_line, int p_to_column) { +void TextEdit::select(int p_from_line, int p_from_column, int p_to_line, int p_to_column, int p_caret) { + ERR_FAIL_INDEX(p_caret, carets.size()); if (!selecting_enabled) { return; } @@ -4288,91 +4836,143 @@ void TextEdit::select(int p_from_line, int p_from_column, int p_to_line, int p_t p_to_column = 0; } - selection.from_line = p_from_line; - selection.from_column = p_from_column; - selection.to_line = p_to_line; - selection.to_column = p_to_column; + carets.write[p_caret].selection.from_line = p_from_line; + carets.write[p_caret].selection.from_column = p_from_column; + carets.write[p_caret].selection.to_line = p_to_line; + carets.write[p_caret].selection.to_column = p_to_column; - selection.active = true; + carets.write[p_caret].selection.active = true; - if (selection.from_line == selection.to_line) { - if (selection.from_column == selection.to_column) { - selection.active = false; + if (carets[p_caret].selection.from_line == carets[p_caret].selection.to_line) { + if (carets[p_caret].selection.from_column == carets[p_caret].selection.to_column) { + carets.write[p_caret].selection.active = false; - } else if (selection.from_column > selection.to_column) { - selection.shiftclick_left = false; - SWAP(selection.from_column, selection.to_column); + } else if (carets[p_caret].selection.from_column > carets[p_caret].selection.to_column) { + carets.write[p_caret].selection.shiftclick_left = false; + SWAP(carets.write[p_caret].selection.from_column, carets.write[p_caret].selection.to_column); } else { - selection.shiftclick_left = true; + carets.write[p_caret].selection.shiftclick_left = true; } - } else if (selection.from_line > selection.to_line) { - selection.shiftclick_left = false; - SWAP(selection.from_line, selection.to_line); - SWAP(selection.from_column, selection.to_column); + } else if (carets[p_caret].selection.from_line > carets[p_caret].selection.to_line) { + carets.write[p_caret].selection.shiftclick_left = false; + SWAP(carets.write[p_caret].selection.from_line, carets.write[p_caret].selection.to_line); + SWAP(carets.write[p_caret].selection.from_column, carets.write[p_caret].selection.to_column); } else { - selection.shiftclick_left = true; + carets.write[p_caret].selection.shiftclick_left = true; } + caret_index_edit_dirty = true; queue_redraw(); } -bool TextEdit::has_selection() const { - return selection.active; +bool TextEdit::has_selection(int p_caret) const { + ERR_FAIL_COND_V(p_caret > carets.size(), false); + for (int i = 0; i < carets.size(); i++) { + if (p_caret != -1 && p_caret != i) { + continue; + } + + if (carets[i].selection.active) { + return true; + } + } + return false; } -String TextEdit::get_selected_text() const { - if (!selection.active) { - return ""; +String TextEdit::get_selected_text(int p_caret) { + ERR_FAIL_COND_V(p_caret > carets.size(), ""); + + StringBuilder selected_text; + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (int i = caret_edit_order.size() - 1; i >= 0; i--) { + int caret_idx = caret_edit_order[i]; + if (p_caret != -1 && p_caret != caret_idx) { + continue; + } + + if (!has_selection(caret_idx)) { + continue; + } + selected_text += _base_get_text(get_selection_from_line(caret_idx), get_selection_from_column(caret_idx), get_selection_to_line(caret_idx), get_selection_to_column(caret_idx)); + if (p_caret == -1 && i != 0) { + selected_text += "\n"; + } } - return _base_get_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); + return selected_text.as_string(); } -int TextEdit::get_selection_line() const { - ERR_FAIL_COND_V(!selection.active, -1); - return selection.selecting_line; +int TextEdit::get_selection_line(int p_caret) const { + ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); + ERR_FAIL_COND_V(!has_selection(p_caret), -1); + return carets[p_caret].selection.selecting_line; } -int TextEdit::get_selection_column() const { - ERR_FAIL_COND_V(!selection.active, -1); - return selection.selecting_column; +int TextEdit::get_selection_column(int p_caret) const { + ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); + ERR_FAIL_COND_V(!has_selection(p_caret), -1); + return carets[p_caret].selection.selecting_column; } -int TextEdit::get_selection_from_line() const { - ERR_FAIL_COND_V(!selection.active, -1); - return selection.from_line; +int TextEdit::get_selection_from_line(int p_caret) const { + ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); + ERR_FAIL_COND_V(!has_selection(p_caret), -1); + return carets[p_caret].selection.from_line; } -int TextEdit::get_selection_from_column() const { - ERR_FAIL_COND_V(!selection.active, -1); - return selection.from_column; +int TextEdit::get_selection_from_column(int p_caret) const { + ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); + ERR_FAIL_COND_V(!has_selection(p_caret), -1); + return carets[p_caret].selection.from_column; } -int TextEdit::get_selection_to_line() const { - ERR_FAIL_COND_V(!selection.active, -1); - return selection.to_line; +int TextEdit::get_selection_to_line(int p_caret) const { + ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); + ERR_FAIL_COND_V(!has_selection(p_caret), -1); + return carets[p_caret].selection.to_line; } -int TextEdit::get_selection_to_column() const { - ERR_FAIL_COND_V(!selection.active, -1); - return selection.to_column; +int TextEdit::get_selection_to_column(int p_caret) const { + ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); + ERR_FAIL_COND_V(!has_selection(p_caret), -1); + return carets[p_caret].selection.to_column; } -void TextEdit::deselect() { - selection.active = false; +void TextEdit::deselect(int p_caret) { + ERR_FAIL_COND(p_caret > carets.size()); + for (int i = 0; i < carets.size(); i++) { + if (p_caret != -1 && p_caret != i) { + continue; + } + carets.write[i].selection.active = false; + } + caret_index_edit_dirty = true; queue_redraw(); } -void TextEdit::delete_selection() { - if (!has_selection()) { - return; - } +void TextEdit::delete_selection(int p_caret) { + ERR_FAIL_COND(p_caret > carets.size()); + + begin_complex_operation(); + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (const int &i : caret_edit_order) { + if (p_caret != -1 && p_caret != i) { + continue; + } + + if (!has_selection(i)) { + continue; + } - selection.active = false; - selection.selecting_mode = SelectionMode::SELECTION_MODE_NONE; - _remove_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); - set_caret_line(selection.from_line, false, false); - set_caret_column(selection.from_column); + selecting_mode = SelectionMode::SELECTION_MODE_NONE; + _remove_text(carets[i].selection.from_line, carets[i].selection.from_column, carets[i].selection.to_line, carets[i].selection.to_column); + set_caret_line(carets[i].selection.from_line, false, false, 0, i); + set_caret_column(carets[i].selection.from_column, i == 0, i); + carets.write[i].selection.active = false; + + adjust_carets_after_edit(i, carets[i].selection.from_line, carets[i].selection.from_column, carets[i].selection.to_line, carets[i].selection.to_column); + } + end_complex_operation(); queue_redraw(); } @@ -4543,7 +5143,7 @@ void TextEdit::set_line_as_first_visible(int p_line, int p_wrap_index) { } int TextEdit::get_first_visible_line() const { - return CLAMP(caret.line_ofs, 0, text.size() - 1); + return CLAMP(first_visible_line, 0, text.size() - 1); } void TextEdit::set_line_as_center_visible(int p_line, int p_wrap_index) { @@ -4582,14 +5182,14 @@ void TextEdit::set_line_as_last_visible(int p_line, int p_wrap_index) { int TextEdit::get_last_full_visible_line() const { int first_vis_line = get_first_visible_line(); int last_vis_line = 0; - last_vis_line = first_vis_line + get_next_visible_line_index_offset_from(first_vis_line, caret.wrap_ofs, get_visible_line_count()).x - 1; + last_vis_line = first_vis_line + get_next_visible_line_index_offset_from(first_vis_line, first_visible_line_wrap_ofs, get_visible_line_count()).x - 1; last_vis_line = CLAMP(last_vis_line, 0, text.size() - 1); return last_vis_line; } int TextEdit::get_last_full_visible_line_wrap_index() const { int first_vis_line = get_first_visible_line(); - return get_next_visible_line_index_offset_from(first_vis_line, caret.wrap_ofs, get_visible_line_count()).y; + return get_next_visible_line_index_offset_from(first_vis_line, first_visible_line_wrap_ofs, get_visible_line_count()).y; } int TextEdit::get_visible_line_count() const { @@ -4625,16 +5225,18 @@ int TextEdit::get_total_visible_line_count() const { } // Auto adjust -void TextEdit::adjust_viewport_to_caret() { +void TextEdit::adjust_viewport_to_caret(int p_caret) { + ERR_FAIL_INDEX(p_caret, carets.size()); + // Make sure Caret is visible on the screen. scrolling = false; minimap_clicked = false; - int cur_line = caret.line; - int cur_wrap = get_caret_wrap_index(); + int cur_line = get_caret_line(p_caret); + int cur_wrap = get_caret_wrap_index(p_caret); int first_vis_line = get_first_visible_line(); - int first_vis_wrap = caret.wrap_ofs; + int first_vis_wrap = first_visible_line_wrap_ofs; int last_vis_line = get_last_full_visible_line(); int last_vis_wrap = get_last_full_visible_line_wrap_index(); @@ -4661,43 +5263,45 @@ void TextEdit::adjust_viewport_to_caret() { // Get position of the start of caret. if (ime_text.length() != 0 && ime_selection.x != 0) { - caret_pos.x = _get_column_x_offset_for_line(caret.column + ime_selection.x, caret.line); + caret_pos.x = _get_column_x_offset_for_line(get_caret_column(p_caret) + ime_selection.x, get_caret_line(p_caret), get_caret_column(p_caret)); } else { - caret_pos.x = _get_column_x_offset_for_line(caret.column, caret.line); + caret_pos.x = _get_column_x_offset_for_line(get_caret_column(p_caret), get_caret_line(p_caret), get_caret_column(p_caret)); } // Get position of the end of caret. if (ime_text.length() != 0) { if (ime_selection.y != 0) { - caret_pos.y = _get_column_x_offset_for_line(caret.column + ime_selection.x + ime_selection.y, caret.line); + caret_pos.y = _get_column_x_offset_for_line(get_caret_column(p_caret) + ime_selection.x + ime_selection.y, get_caret_line(p_caret), get_caret_column(p_caret)); } else { - caret_pos.y = _get_column_x_offset_for_line(caret.column + ime_text.size(), caret.line); + caret_pos.y = _get_column_x_offset_for_line(get_caret_column(p_caret) + ime_text.size(), get_caret_line(p_caret), get_caret_column(p_caret)); } } else { caret_pos.y = caret_pos.x; } - if (MAX(caret_pos.x, caret_pos.y) > (caret.x_ofs + visible_width)) { - caret.x_ofs = MAX(caret_pos.x, caret_pos.y) - visible_width + 1; + if (MAX(caret_pos.x, caret_pos.y) > (first_visible_col + visible_width)) { + first_visible_col = MAX(caret_pos.x, caret_pos.y) - visible_width + 1; } - if (MIN(caret_pos.x, caret_pos.y) < caret.x_ofs) { - caret.x_ofs = MIN(caret_pos.x, caret_pos.y); + if (MIN(caret_pos.x, caret_pos.y) < first_visible_col) { + first_visible_col = MIN(caret_pos.x, caret_pos.y); } } else { - caret.x_ofs = 0; + first_visible_col = 0; } - h_scroll->set_value(caret.x_ofs); + h_scroll->set_value(first_visible_col); queue_redraw(); } -void TextEdit::center_viewport_to_caret() { +void TextEdit::center_viewport_to_caret(int p_caret) { + ERR_FAIL_INDEX(p_caret, carets.size()); + // Move viewport so the caret is in the center of the screen. scrolling = false; minimap_clicked = false; - set_line_as_center_visible(caret.line, get_caret_wrap_index()); + set_line_as_center_visible(get_caret_line(p_caret), get_caret_wrap_index(p_caret)); int visible_width = get_size().width - style_normal->get_minimum_size().width - gutters_width - gutter_padding; if (draw_minimap) { visible_width -= minimap_width; @@ -4714,33 +5318,33 @@ void TextEdit::center_viewport_to_caret() { // Get position of the start of caret. if (ime_text.length() != 0 && ime_selection.x != 0) { - caret_pos.x = _get_column_x_offset_for_line(caret.column + ime_selection.x, caret.line); + caret_pos.x = _get_column_x_offset_for_line(get_caret_column(p_caret) + ime_selection.x, get_caret_line(p_caret), get_caret_column(p_caret)); } else { - caret_pos.x = _get_column_x_offset_for_line(caret.column, caret.line); + caret_pos.x = _get_column_x_offset_for_line(get_caret_column(p_caret), get_caret_line(p_caret), get_caret_column(p_caret)); } // Get position of the end of caret. if (ime_text.length() != 0) { if (ime_selection.y != 0) { - caret_pos.y = _get_column_x_offset_for_line(caret.column + ime_selection.x + ime_selection.y, caret.line); + caret_pos.y = _get_column_x_offset_for_line(get_caret_column(p_caret) + ime_selection.x + ime_selection.y, get_caret_line(p_caret), get_caret_column(p_caret)); } else { - caret_pos.y = _get_column_x_offset_for_line(caret.column + ime_text.size(), caret.line); + caret_pos.y = _get_column_x_offset_for_line(get_caret_column(p_caret) + ime_text.size(), get_caret_line(p_caret), get_caret_column(p_caret)); } } else { caret_pos.y = caret_pos.x; } - if (MAX(caret_pos.x, caret_pos.y) > (caret.x_ofs + visible_width)) { - caret.x_ofs = MAX(caret_pos.x, caret_pos.y) - visible_width + 1; + if (MAX(caret_pos.x, caret_pos.y) > (first_visible_col + visible_width)) { + first_visible_col = MAX(caret_pos.x, caret_pos.y) - visible_width + 1; } - if (MIN(caret_pos.x, caret_pos.y) < caret.x_ofs) { - caret.x_ofs = MIN(caret_pos.x, caret_pos.y); + if (MIN(caret_pos.x, caret_pos.y) < first_visible_col) { + first_visible_col = MIN(caret_pos.x, caret_pos.y); } } else { - caret.x_ofs = 0; + first_visible_col = 0; } - h_scroll->set_value(caret.x_ofs); + h_scroll->set_value(first_visible_col); queue_redraw(); } @@ -5187,7 +5791,7 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("swap_lines", "from_line", "to_line"), &TextEdit::swap_lines); ClassDB::bind_method(D_METHOD("insert_line_at", "line", "text"), &TextEdit::insert_line_at); - ClassDB::bind_method(D_METHOD("insert_text_at_caret", "text"), &TextEdit::insert_text_at_caret); + ClassDB::bind_method(D_METHOD("insert_text_at_caret", "text", "caret_index"), &TextEdit::insert_text_at_caret, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("remove_text", "from_line", "from_column", "to_line", "to_column"), &TextEdit::remove_text); @@ -5196,18 +5800,19 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("get_next_visible_line_index_offset_from", "line", "wrap_index", "visible_amount"), &TextEdit::get_next_visible_line_index_offset_from); // Overridable actions - ClassDB::bind_method(D_METHOD("backspace"), &TextEdit::backspace); + ClassDB::bind_method(D_METHOD("backspace", "caret_index"), &TextEdit::backspace, DEFVAL(-1)); - ClassDB::bind_method(D_METHOD("cut"), &TextEdit::cut); - ClassDB::bind_method(D_METHOD("copy"), &TextEdit::copy); - ClassDB::bind_method(D_METHOD("paste"), &TextEdit::paste); + ClassDB::bind_method(D_METHOD("cut", "caret_index"), &TextEdit::cut, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("copy", "caret_index"), &TextEdit::copy, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("paste", "caret_index"), &TextEdit::paste, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("paste_primary_clipboard", "caret_index"), &TextEdit::paste_primary_clipboard, DEFVAL(-1)); - GDVIRTUAL_BIND(_handle_unicode_input, "unicode_char") - GDVIRTUAL_BIND(_backspace) - GDVIRTUAL_BIND(_cut) - GDVIRTUAL_BIND(_copy) - GDVIRTUAL_BIND(_paste) - GDVIRTUAL_BIND(_paste_primary_clipboard) + GDVIRTUAL_BIND(_handle_unicode_input, "unicode_char", "caret_index") + GDVIRTUAL_BIND(_backspace, "caret_index") + GDVIRTUAL_BIND(_cut, "caret_index") + GDVIRTUAL_BIND(_copy, "caret_index") + GDVIRTUAL_BIND(_paste, "caret_index") + GDVIRTUAL_BIND(_paste_primary_clipboard, "caret_index") // Context Menu BIND_ENUM_CONSTANT(MENU_CUT); @@ -5241,6 +5846,14 @@ void TextEdit::_bind_methods() { BIND_ENUM_CONSTANT(MENU_MAX); /* Versioning */ + BIND_ENUM_CONSTANT(ACTION_NONE); + BIND_ENUM_CONSTANT(ACTION_TYPING); + BIND_ENUM_CONSTANT(ACTION_BACKSPACE); + BIND_ENUM_CONSTANT(ACTION_DELETE); + + ClassDB::bind_method(D_METHOD("start_action", "action"), &TextEdit::start_action); + ClassDB::bind_method(D_METHOD("end_action"), &TextEdit::end_complex_operation); + ClassDB::bind_method(D_METHOD("begin_complex_operation"), &TextEdit::begin_complex_operation); ClassDB::bind_method(D_METHOD("end_complex_operation"), &TextEdit::end_complex_operation); @@ -5280,7 +5893,7 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("get_minimap_line_at_pos", "position"), &TextEdit::get_minimap_line_at_pos); ClassDB::bind_method(D_METHOD("is_dragging_cursor"), &TextEdit::is_dragging_cursor); - ClassDB::bind_method(D_METHOD("is_mouse_over_selection", "edges"), &TextEdit::is_mouse_over_selection); + ClassDB::bind_method(D_METHOD("is_mouse_over_selection", "edges", "caret_index"), &TextEdit::is_mouse_over_selection, DEFVAL(-1)); /* Caret. */ BIND_ENUM_CONSTANT(CARET_TYPE_LINE); @@ -5304,18 +5917,30 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_caret_mid_grapheme_enabled", "enabled"), &TextEdit::set_caret_mid_grapheme_enabled); ClassDB::bind_method(D_METHOD("is_caret_mid_grapheme_enabled"), &TextEdit::is_caret_mid_grapheme_enabled); - ClassDB::bind_method(D_METHOD("is_caret_visible"), &TextEdit::is_caret_visible); - ClassDB::bind_method(D_METHOD("get_caret_draw_pos"), &TextEdit::get_caret_draw_pos); + ClassDB::bind_method(D_METHOD("set_multiple_carets_enabled", "enabled"), &TextEdit::set_multiple_carets_enabled); + ClassDB::bind_method(D_METHOD("is_multiple_carets_enabled"), &TextEdit::is_multiple_carets_enabled); + + ClassDB::bind_method(D_METHOD("add_caret", "line", "col"), &TextEdit::add_caret); + ClassDB::bind_method(D_METHOD("remove_caret", "caret"), &TextEdit::remove_caret); + ClassDB::bind_method(D_METHOD("remove_secondary_carets"), &TextEdit::remove_secondary_carets); + ClassDB::bind_method(D_METHOD("merge_overlapping_carets"), &TextEdit::merge_overlapping_carets); + ClassDB::bind_method(D_METHOD("get_caret_count"), &TextEdit::get_caret_count); - ClassDB::bind_method(D_METHOD("set_caret_line", "line", "adjust_viewport", "can_be_hidden", "wrap_index"), &TextEdit::set_caret_line, DEFVAL(true), DEFVAL(true), DEFVAL(0)); - ClassDB::bind_method(D_METHOD("get_caret_line"), &TextEdit::get_caret_line); + ClassDB::bind_method(D_METHOD("get_caret_index_edit_order"), &TextEdit::get_caret_index_edit_order); + ClassDB::bind_method(D_METHOD("adjust_carets_after_edit", "caret", "from_line", "from_col", "to_line", "to_col"), &TextEdit::adjust_carets_after_edit); - ClassDB::bind_method(D_METHOD("set_caret_column", "column", "adjust_viewport"), &TextEdit::set_caret_column, DEFVAL(true)); - ClassDB::bind_method(D_METHOD("get_caret_column"), &TextEdit::get_caret_column); + ClassDB::bind_method(D_METHOD("is_caret_visible", "caret_index"), &TextEdit::is_caret_visible, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_caret_draw_pos", "caret_index"), &TextEdit::get_caret_draw_pos, DEFVAL(0)); - ClassDB::bind_method(D_METHOD("get_caret_wrap_index"), &TextEdit::get_caret_wrap_index); + ClassDB::bind_method(D_METHOD("set_caret_line", "line", "adjust_viewport", "can_be_hidden", "wrap_index", "caret_index"), &TextEdit::set_caret_line, DEFVAL(true), DEFVAL(true), DEFVAL(0), DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_caret_line", "caret_index"), &TextEdit::get_caret_line, DEFVAL(0)); - ClassDB::bind_method(D_METHOD("get_word_under_caret"), &TextEdit::get_word_under_caret); + ClassDB::bind_method(D_METHOD("set_caret_column", "column", "adjust_viewport", "caret_index"), &TextEdit::set_caret_column, DEFVAL(true), DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_caret_column", "caret_index"), &TextEdit::get_caret_column, DEFVAL(0)); + + ClassDB::bind_method(D_METHOD("get_caret_wrap_index", "caret_index"), &TextEdit::get_caret_wrap_index, DEFVAL(0)); + + ClassDB::bind_method(D_METHOD("get_word_under_caret", "caret_index"), &TextEdit::get_word_under_caret, DEFVAL(-1)); /* Selection. */ BIND_ENUM_CONSTANT(SELECTION_MODE_NONE); @@ -5336,27 +5961,27 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_override_selected_font_color", "override"), &TextEdit::set_override_selected_font_color); ClassDB::bind_method(D_METHOD("is_overriding_selected_font_color"), &TextEdit::is_overriding_selected_font_color); - ClassDB::bind_method(D_METHOD("set_selection_mode", "mode", "line", "column"), &TextEdit::set_selection_mode, DEFVAL(-1), DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("set_selection_mode", "mode", "line", "column", "caret_index"), &TextEdit::set_selection_mode, DEFVAL(-1), DEFVAL(-1), DEFVAL(0)); ClassDB::bind_method(D_METHOD("get_selection_mode"), &TextEdit::get_selection_mode); ClassDB::bind_method(D_METHOD("select_all"), &TextEdit::select_all); - ClassDB::bind_method(D_METHOD("select_word_under_caret"), &TextEdit::select_word_under_caret); - ClassDB::bind_method(D_METHOD("select", "from_line", "from_column", "to_line", "to_column"), &TextEdit::select); + ClassDB::bind_method(D_METHOD("select_word_under_caret", "caret_index"), &TextEdit::select_word_under_caret, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("select", "from_line", "from_column", "to_line", "to_column", "caret_index"), &TextEdit::select, DEFVAL(0)); - ClassDB::bind_method(D_METHOD("has_selection"), &TextEdit::has_selection); + ClassDB::bind_method(D_METHOD("has_selection", "caret_index"), &TextEdit::has_selection, DEFVAL(-1)); - ClassDB::bind_method(D_METHOD("get_selected_text"), &TextEdit::get_selected_text); + ClassDB::bind_method(D_METHOD("get_selected_text", "caret_index"), &TextEdit::get_selected_text, DEFVAL(-1)); - ClassDB::bind_method(D_METHOD("get_selection_line"), &TextEdit::get_selection_line); - ClassDB::bind_method(D_METHOD("get_selection_column"), &TextEdit::get_selection_column); + ClassDB::bind_method(D_METHOD("get_selection_line", "caret_index"), &TextEdit::get_selection_line, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_selection_column", "caret_index"), &TextEdit::get_selection_column, DEFVAL(0)); - ClassDB::bind_method(D_METHOD("get_selection_from_line"), &TextEdit::get_selection_from_line); - ClassDB::bind_method(D_METHOD("get_selection_from_column"), &TextEdit::get_selection_from_column); - ClassDB::bind_method(D_METHOD("get_selection_to_line"), &TextEdit::get_selection_to_line); - ClassDB::bind_method(D_METHOD("get_selection_to_column"), &TextEdit::get_selection_to_column); + ClassDB::bind_method(D_METHOD("get_selection_from_line", "caret_index"), &TextEdit::get_selection_from_line, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_selection_from_column", "caret_index"), &TextEdit::get_selection_from_column, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_selection_to_line", "caret_index"), &TextEdit::get_selection_to_line, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_selection_to_column", "caret_index"), &TextEdit::get_selection_to_column, DEFVAL(0)); - ClassDB::bind_method(D_METHOD("deselect"), &TextEdit::deselect); - ClassDB::bind_method(D_METHOD("delete_selection"), &TextEdit::delete_selection); + ClassDB::bind_method(D_METHOD("deselect", "caret_index"), &TextEdit::deselect, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("delete_selection", "caret_index"), &TextEdit::delete_selection, DEFVAL(-1)); /* Line wrapping. */ BIND_ENUM_CONSTANT(LINE_WRAPPING_NONE); @@ -5411,8 +6036,8 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("get_total_visible_line_count"), &TextEdit::get_total_visible_line_count); // Auto adjust - ClassDB::bind_method(D_METHOD("adjust_viewport_to_caret"), &TextEdit::adjust_viewport_to_caret); - ClassDB::bind_method(D_METHOD("center_viewport_to_caret"), &TextEdit::center_viewport_to_caret); + ClassDB::bind_method(D_METHOD("adjust_viewport_to_caret", "caret_index"), &TextEdit::adjust_viewport_to_caret, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("center_viewport_to_caret", "caret_index"), &TextEdit::center_viewport_to_caret, DEFVAL(0)); // Minimap ClassDB::bind_method(D_METHOD("set_draw_minimap", "enabled"), &TextEdit::set_draw_minimap); @@ -5528,6 +6153,7 @@ void TextEdit::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "caret_blink_interval", PROPERTY_HINT_RANGE, "0.1,10,0.01,suffix:s"), "set_caret_blink_interval", "get_caret_blink_interval"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_move_on_right_click"), "set_move_caret_on_right_click_enabled", "is_move_caret_on_right_click_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_mid_grapheme"), "set_caret_mid_grapheme_enabled", "is_caret_mid_grapheme_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_multiple"), "set_multiple_carets_enabled", "is_multiple_carets_enabled"); ADD_GROUP("BiDi", ""); ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,Left-to-Right,Right-to-Left,Inherited"), "set_text_direction", "get_text_direction"); @@ -5613,157 +6239,238 @@ void TextEdit::_set_symbol_lookup_word(const String &p_symbol) { /* Text manipulation */ // Overridable actions -void TextEdit::_handle_unicode_input_internal(const uint32_t p_unicode) { +void TextEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_caret) { + ERR_FAIL_COND(p_caret > carets.size()); if (!editable) { return; } - bool had_selection = has_selection(); - if (had_selection) { - begin_complex_operation(); - delete_selection(); - } - - /* Remove the old character if in insert mode and no selection. */ - if (overtype_mode && !had_selection) { - begin_complex_operation(); - - /* Make sure we don't try and remove empty space. */ - int cl = get_caret_line(); - int cc = get_caret_column(); - if (cc < get_line(cl).length()) { - _remove_text(cl, cc, cl, cc + 1); + start_action(EditAction::ACTION_TYPING); + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (const int &i : caret_edit_order) { + if (p_caret != -1 && p_caret != i) { + continue; } - } - const char32_t chr[2] = { (char32_t)p_unicode, 0 }; - insert_text_at_caret(chr); + /* Remove the old character if in insert mode and no selection. */ + if (overtype_mode && !has_selection(i)) { + /* Make sure we don't try and remove empty space. */ + int cl = get_caret_line(i); + int cc = get_caret_column(i); + if (cc < get_line(cl).length()) { + _remove_text(cl, cc, cl, cc + 1); + } + } - if ((overtype_mode && !had_selection) || (had_selection)) { - end_complex_operation(); + const char32_t chr[2] = { (char32_t)p_unicode, 0 }; + insert_text_at_caret(chr, i); } + end_action(); } -void TextEdit::_backspace_internal() { +void TextEdit::_backspace_internal(int p_caret) { + ERR_FAIL_COND(p_caret > carets.size()); if (!editable) { return; } - if (has_selection()) { - delete_selection(); + if (has_selection(p_caret)) { + delete_selection(p_caret); return; } - int cc = get_caret_column(); - int cl = get_caret_line(); + begin_complex_operation(); + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (const int &i : caret_edit_order) { + if (p_caret != -1 && p_caret != i) { + continue; + } - if (cc == 0 && cl == 0) { - return; - } + int cc = get_caret_column(i); + int cl = get_caret_line(i); - int prev_line = cc ? cl : cl - 1; - int prev_column = cc ? (cc - 1) : (text[cl - 1].length()); + if (cc == 0 && cl == 0) { + continue; + } - merge_gutters(prev_line, cl); + int prev_line = cc ? cl : cl - 1; + int prev_column = cc ? (cc - 1) : (text[cl - 1].length()); - if (_is_line_hidden(cl)) { - _set_line_as_hidden(prev_line, true); - } - _remove_text(prev_line, prev_column, cl, cc); + merge_gutters(prev_line, cl); + + if (_is_line_hidden(cl)) { + _set_line_as_hidden(prev_line, true); + } + _remove_text(prev_line, prev_column, cl, cc); + + set_caret_line(prev_line, false, true, 0, i); + set_caret_column(prev_column, i == 0, i); - set_caret_line(prev_line, false, true); - set_caret_column(prev_column); + adjust_carets_after_edit(i, prev_line, prev_column, cl, cc); + } + merge_overlapping_carets(); + end_complex_operation(); } -void TextEdit::_cut_internal() { +void TextEdit::_cut_internal(int p_caret) { + ERR_FAIL_COND(p_caret > carets.size()); if (!editable) { return; } - if (has_selection()) { - DisplayServer::get_singleton()->clipboard_set(get_selected_text()); - delete_selection(); + if (has_selection(p_caret)) { + DisplayServer::get_singleton()->clipboard_set(get_selected_text(p_caret)); + delete_selection(p_caret); cut_copy_line = ""; return; } - int cl = get_caret_line(); - int cc = get_caret_column(); - int indent_level = get_indent_level(cl); - double hscroll = get_h_scroll(); + begin_complex_operation(); + Vector<int> carets_to_remove; + + StringBuilder clipboard; + // This is the exception and has to edit in reverse order else the string copied to the clipboard will be backwards. + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (int i = caret_edit_order.size() - 1; i >= 0; i--) { + int caret_idx = caret_edit_order[i]; + if (p_caret != -1 && p_caret != caret_idx) { + continue; + } + + int cl = get_caret_line(caret_idx); + int cc = get_caret_column(caret_idx); + int indent_level = get_indent_level(cl); + double hscroll = get_h_scroll(); - String clipboard = text[cl]; - DisplayServer::get_singleton()->clipboard_set(clipboard); - set_caret_column(0); + // Check for overlaping carets. + // We don't need to worry about selections as that is caught before this entire section. + for (int j = i - 1; j >= 0; j--) { + if (get_caret_line(caret_edit_order[j]) == cl) { + carets_to_remove.push_back(caret_edit_order[j]); + i = j; + } + } - if (cl == 0 && get_line_count() > 1) { - _remove_text(cl, 0, cl + 1, 0); - } else { - _remove_text(cl, 0, cl, text[cl].length()); - backspace(); - set_caret_line(get_caret_line() + 1); - } + clipboard += text[cl]; + if (p_caret == -1 && caret_idx != 0) { + clipboard += "\n"; + } + + if (cl == 0 && get_line_count() > 1) { + _remove_text(cl, 0, cl + 1, 0); + adjust_carets_after_edit(caret_idx, cl, 0, cl + 1, text[cl].length()); + } else { + _remove_text(cl, 0, cl, text[cl].length()); + set_caret_column(0, false, caret_idx); + backspace(caret_idx); + set_caret_line(get_caret_line(caret_idx) + 1, caret_idx == 0, 0, 0, caret_idx); + } - // Correct the visually perceived caret column taking care of indentation level of the lines. - int diff_indent = indent_level - get_indent_level(get_caret_line()); - cc += diff_indent; - if (diff_indent != 0) { - cc += diff_indent > 0 ? -1 : 1; + // Correct the visually perceived caret column taking care of indentation level of the lines. + int diff_indent = indent_level - get_indent_level(get_caret_line(caret_idx)); + cc += diff_indent; + if (diff_indent != 0) { + cc += diff_indent > 0 ? -1 : 1; + } + + // Restore horizontal scroll and caret column modified by the backspace() call. + set_h_scroll(hscroll); + set_caret_column(cc, caret_idx == 0, caret_idx); } - // Restore horizontal scroll and caret column modified by the backspace() call. - set_h_scroll(hscroll); - set_caret_column(cc); + // Sort and remove backwards to preserve indexes. + carets_to_remove.sort(); + for (int i = carets_to_remove.size() - 1; i >= 0; i--) { + remove_caret(carets_to_remove[i]); + } + end_complex_operation(); - cut_copy_line = clipboard; + String clipboard_string = clipboard.as_string(); + DisplayServer::get_singleton()->clipboard_set(clipboard_string); + cut_copy_line = clipboard_string; } -void TextEdit::_copy_internal() { - if (has_selection()) { - DisplayServer::get_singleton()->clipboard_set(get_selected_text()); +void TextEdit::_copy_internal(int p_caret) { + ERR_FAIL_COND(p_caret > carets.size()); + if (has_selection(p_caret)) { + DisplayServer::get_singleton()->clipboard_set(get_selected_text(p_caret)); cut_copy_line = ""; return; } - int cl = get_caret_line(); - if (text[cl].length() != 0) { - String clipboard = _base_get_text(cl, 0, cl, text[cl].length()); - DisplayServer::get_singleton()->clipboard_set(clipboard); - cut_copy_line = clipboard; + StringBuilder clipboard; + Vector<int> caret_edit_order = get_caret_index_edit_order(); + for (int i = caret_edit_order.size() - 1; i >= 0; i--) { + int caret_idx = caret_edit_order[i]; + if (p_caret != -1 && p_caret != caret_idx) { + continue; + } + + int cl = get_caret_line(caret_idx); + if (text[cl].length() != 0) { + clipboard += _base_get_text(cl, 0, cl, text[cl].length()); + if (p_caret == -1 && i != 0) { + clipboard += "\n"; + } + } } + + String clipboard_string = clipboard.as_string(); + DisplayServer::get_singleton()->clipboard_set(clipboard_string); + cut_copy_line = clipboard_string; } -void TextEdit::_paste_internal() { +void TextEdit::_paste_internal(int p_caret) { + ERR_FAIL_COND(p_caret > carets.size()); if (!editable) { return; } String clipboard = DisplayServer::get_singleton()->clipboard_get(); + Vector<String> clipboad_lines = clipboard.split("\n"); + bool insert_line_per_caret = p_caret == -1 && carets.size() > 1 && clipboad_lines.size() == carets.size(); begin_complex_operation(); - if (has_selection()) { - delete_selection(); - } else if (!cut_copy_line.is_empty() && cut_copy_line == clipboard) { - set_caret_column(0); - String ins = "\n"; - clipboard += ins; - } + Vector<int> caret_edit_order = get_caret_index_edit_order(); + int clipboad_line = clipboad_lines.size() - 1; + for (const int &i : caret_edit_order) { + if (p_caret != -1 && p_caret != i) { + continue; + } + + if (has_selection(i)) { + delete_selection(i); + } else if (!cut_copy_line.is_empty() && cut_copy_line == clipboard) { + set_caret_column(0, i == 0, i); + String ins = "\n"; + clipboard += ins; + } + + if (insert_line_per_caret) { + clipboard = clipboad_lines[clipboad_line]; + } - insert_text_at_caret(clipboard); + insert_text_at_caret(clipboard, i); + clipboad_line--; + } end_complex_operation(); } -void TextEdit::_paste_primary_clipboard_internal() { +void TextEdit::_paste_primary_clipboard_internal(int p_caret) { + ERR_FAIL_COND(p_caret > carets.size()); if (!is_editable() || !DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { return; } String paste_buffer = DisplayServer::get_singleton()->clipboard_get_primary(); - Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); - deselect(); - set_caret_line(pos.y, true, false); - set_caret_column(pos.x); + if (carets.size() == 1) { + Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); + deselect(); + set_caret_line(pos.y, true, false); + set_caret_column(pos.x); + } + if (!paste_buffer.is_empty()) { insert_text_at_caret(paste_buffer); } @@ -5880,6 +6587,10 @@ Key TextEdit::_get_menu_action_accelerator(const String &p_action) { /* Versioning */ void TextEdit::_push_current_op() { + if (pending_action_end) { + start_action(EditAction::ACTION_NONE); + return; + } if (current_op.type == TextOperation::TYPE_NONE) { return; // Nothing to do. } @@ -5981,6 +6692,7 @@ int TextEdit::_get_char_pos_for_line(int p_px, int p_line, int p_wrap_index) con void TextEdit::_emit_caret_changed() { emit_signal(SNAME("caret_changed")); caret_pos_dirty = false; + caret_index_edit_dirty = true; } void TextEdit::_reset_caret_blink_timer() { @@ -6003,7 +6715,7 @@ void TextEdit::_toggle_draw_caret() { } } -int TextEdit::_get_column_x_offset_for_line(int p_char, int p_line) const { +int TextEdit::_get_column_x_offset_for_line(int p_char, int p_line, int p_column) const { ERR_FAIL_INDEX_V(p_line, text.size(), 0); int row = 0; @@ -6016,7 +6728,7 @@ int TextEdit::_get_column_x_offset_for_line(int p_char, int p_line) const { } RID text_rid = text.get_line_data(p_line)->get_line_rid(row); - CaretInfo ts_caret = TS->shaped_text_get_carets(text_rid, caret.column); + CaretInfo ts_caret = TS->shaped_text_get_carets(text_rid, p_column); if ((ts_caret.l_caret != Rect2() && (ts_caret.l_dir == TextServer::DIRECTION_AUTO || ts_caret.l_dir == (TextServer::Direction)input_direction)) || (ts_caret.t_caret == Rect2())) { return ts_caret.l_caret.position.x; } else { @@ -6029,8 +6741,8 @@ void TextEdit::_click_selection_held() { // Warning: is_mouse_button_pressed(MouseButton::LEFT) returns false for double+ clicks, so this doesn't work for MODE_WORD // and MODE_LINE. However, moving the mouse triggers _gui_input, which calls these functions too, so that's not a huge problem. // I'm unsure if there's an actual fix that doesn't have a ton of side effects. - if (Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT) && selection.selecting_mode != SelectionMode::SELECTION_MODE_NONE) { - switch (selection.selecting_mode) { + if (Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT) && get_selection_mode() != SelectionMode::SELECTION_MODE_NONE) { + switch (get_selection_mode()) { case SelectionMode::SELECTION_MODE_POINTER: { _update_selection_mode_pointer(); } break; @@ -6056,14 +6768,16 @@ void TextEdit::_update_selection_mode_pointer() { Point2i pos = get_line_column_at_pos(mp); int line = pos.y; int col = pos.x; + int caret_idx = carets.size() - 1; - select(selection.selecting_line, selection.selecting_column, line, col); + select(carets[caret_idx].selection.selecting_line, carets[caret_idx].selection.selecting_column, line, col, caret_idx); - set_caret_line(line, false); - set_caret_column(col); + set_caret_line(line, false, true, 0, caret_idx); + set_caret_column(col, true, caret_idx); queue_redraw(); click_select_held->start(); + merge_overlapping_carets(); } void TextEdit::_update_selection_mode_word() { @@ -6073,6 +6787,7 @@ void TextEdit::_update_selection_mode_word() { Point2i pos = get_line_column_at_pos(mp); int line = pos.y; int col = pos.x; + int caret_idx = carets.size() - 1; int caret_pos = CLAMP(col, 0, text[line].length()); int beg = caret_pos; @@ -6087,25 +6802,25 @@ void TextEdit::_update_selection_mode_word() { } /* Initial selection. */ - if (!selection.active) { - select(line, beg, line, end); - selection.selecting_column = beg; - selection.selected_word_beg = beg; - selection.selected_word_end = end; - selection.selected_word_origin = beg; - set_caret_line(line, false); - set_caret_column(end); + if (!has_selection(caret_idx)) { + select(line, beg, line, end, caret_idx); + carets.write[caret_idx].selection.selecting_column = beg; + carets.write[caret_idx].selection.selected_word_beg = beg; + carets.write[caret_idx].selection.selected_word_end = end; + carets.write[caret_idx].selection.selected_word_origin = beg; + set_caret_line(line, false, true, 0, caret_idx); + set_caret_column(end, true, caret_idx); } else { - if ((col <= selection.selected_word_origin && line == selection.selecting_line) || line < selection.selecting_line) { - selection.selecting_column = selection.selected_word_end; - select(line, beg, selection.selecting_line, selection.selected_word_end); - set_caret_line(selection.from_line, false); - set_caret_column(selection.from_column); + if ((col <= carets[caret_idx].selection.selected_word_origin && line == get_selection_line(caret_idx)) || line < get_selection_line(caret_idx)) { + carets.write[caret_idx].selection.selecting_column = carets[caret_idx].selection.selected_word_end; + select(line, beg, get_selection_line(caret_idx), carets[caret_idx].selection.selected_word_end, caret_idx); + set_caret_line(get_selection_from_line(caret_idx), false, true, 0, caret_idx); + set_caret_column(get_selection_from_column(caret_idx), true, caret_idx); } else { - selection.selecting_column = selection.selected_word_beg; - select(selection.selecting_line, selection.selected_word_beg, line, end); - set_caret_line(selection.to_line, false); - set_caret_column(selection.to_column); + carets.write[caret_idx].selection.selecting_column = carets[caret_idx].selection.selected_word_beg; + select(get_selection_line(caret_idx), carets[caret_idx].selection.selected_word_beg, line, end, caret_idx); + set_caret_line(get_selection_to_line(caret_idx), false, true, 0, caret_idx); + set_caret_column(get_selection_to_column(caret_idx), true, caret_idx); } } @@ -6116,6 +6831,7 @@ void TextEdit::_update_selection_mode_word() { queue_redraw(); click_select_held->start(); + merge_overlapping_carets(); } void TextEdit::_update_selection_mode_line() { @@ -6125,21 +6841,22 @@ void TextEdit::_update_selection_mode_line() { Point2i pos = get_line_column_at_pos(mp); int line = pos.y; int col = pos.x; + int caret_idx = carets.size() - 1; col = 0; - if (line < selection.selecting_line) { + if (line < carets[caret_idx].selection.selecting_line) { /* Caret is above us. */ - set_caret_line(line - 1, false); - selection.selecting_column = text[selection.selecting_line].length(); + set_caret_line(line - 1, false, true, 0, caret_idx); + carets.write[caret_idx].selection.selecting_column = text[get_selection_line(caret_idx)].length(); } else { /* Caret is below us. */ - set_caret_line(line + 1, false); - selection.selecting_column = 0; + set_caret_line(line + 1, false, true, 0, caret_idx); + carets.write[caret_idx].selection.selecting_column = 0; col = text[line].length(); } - set_caret_column(0); + set_caret_column(0, false, caret_idx); - select(selection.selecting_line, selection.selecting_column, line, col); + select(carets[caret_idx].selection.selecting_line, carets[caret_idx].selection.selecting_column, line, col, caret_idx); if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text()); } @@ -6147,33 +6864,31 @@ void TextEdit::_update_selection_mode_line() { queue_redraw(); click_select_held->start(); + merge_overlapping_carets(); } -void TextEdit::_pre_shift_selection() { +void TextEdit::_pre_shift_selection(int p_caret) { if (!selecting_enabled) { return; } - if (!selection.active || selection.selecting_mode == SelectionMode::SELECTION_MODE_NONE) { - selection.selecting_line = caret.line; - selection.selecting_column = caret.column; - selection.active = true; + if (!has_selection(p_caret) || get_selection_mode() == SelectionMode::SELECTION_MODE_NONE) { + carets.write[p_caret].selection.active = true; + set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT, get_caret_line(p_caret), get_caret_column(p_caret), p_caret); + return; } - selection.selecting_mode = SelectionMode::SELECTION_MODE_SHIFT; + set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT, get_selection_line(p_caret), get_selection_column(p_caret), p_caret); } -void TextEdit::_post_shift_selection() { +void TextEdit::_post_shift_selection(int p_caret) { if (!selecting_enabled) { return; } - if (selection.active && selection.selecting_mode == SelectionMode::SELECTION_MODE_SHIFT) { - select(selection.selecting_line, selection.selecting_column, caret.line, caret.column); - queue_redraw(); + if (has_selection(p_caret) && get_selection_mode() == SelectionMode::SELECTION_MODE_SHIFT) { + select(get_selection_line(p_caret), get_selection_column(p_caret), get_caret_line(p_caret), get_caret_column(p_caret), p_caret); } - - selection.selecting_text = true; } /* Line Wrapping */ @@ -6199,17 +6914,14 @@ void TextEdit::_update_wrap_at_column(bool p_force) { _update_placeholder(); } - _update_caret_wrap_offset(); -} - -void TextEdit::_update_caret_wrap_offset() { + // Update viewport. int first_vis_line = get_first_visible_line(); if (is_line_wrapped(first_vis_line)) { - caret.wrap_ofs = MIN(caret.wrap_ofs, get_line_wrap_count(first_vis_line)); + first_visible_line_wrap_ofs = MIN(first_visible_line_wrap_ofs, get_line_wrap_count(first_vis_line)); } else { - caret.wrap_ofs = 0; + first_visible_line_wrap_ofs = 0; } - set_line_as_first_visible(caret.line_ofs, caret.wrap_ofs); + set_line_as_first_visible(first_visible_line, first_visible_line_wrap_ofs); } /* Viewport. */ @@ -6258,8 +6970,8 @@ void TextEdit::_update_scrollbars() { set_v_scroll(get_v_scroll()); } else { - caret.line_ofs = 0; - caret.wrap_ofs = 0; + first_visible_line = 0; + first_visible_line_wrap_ofs = 0; v_scroll->set_value(0); v_scroll->set_max(0); v_scroll->hide(); @@ -6269,15 +6981,15 @@ void TextEdit::_update_scrollbars() { h_scroll->show(); h_scroll->set_max(total_width); h_scroll->set_page(visible_width); - if (caret.x_ofs > (total_width - visible_width)) { - caret.x_ofs = (total_width - visible_width); + if (first_visible_col > (total_width - visible_width)) { + first_visible_col = (total_width - visible_width); } - if (fabs(h_scroll->get_value() - (double)caret.x_ofs) >= 1) { - h_scroll->set_value(caret.x_ofs); + if (fabs(h_scroll->get_value() - (double)first_visible_col) >= 1) { + h_scroll->set_value(first_visible_col); } } else { - caret.x_ofs = 0; + first_visible_col = 0; h_scroll->set_value(0); h_scroll->set_max(0); h_scroll->hide(); @@ -6306,7 +7018,7 @@ void TextEdit::_scroll_moved(double p_to_val) { } if (h_scroll->is_visible_in_tree()) { - caret.x_ofs = h_scroll->get_value(); + first_visible_col = h_scroll->get_value(); } if (v_scroll->is_visible_in_tree()) { // Set line ofs and wrap ofs. @@ -6329,8 +7041,8 @@ void TextEdit::_scroll_moved(double p_to_val) { int wi = line_wrap_amount - (sc - v_scroll_i - 1); wi = CLAMP(wi, 0, line_wrap_amount); - caret.line_ofs = n_line; - caret.wrap_ofs = wi; + first_visible_line = n_line; + first_visible_line_wrap_ofs = wi; } queue_redraw(); } @@ -6411,16 +7123,18 @@ void TextEdit::_scroll_lines_up() { set_v_scroll(get_v_scroll() - 1); // Adjust the caret to viewport. - if (!selection.active) { - int cur_line = caret.line; - int cur_wrap = get_caret_wrap_index(); + for (int i = 0; i < carets.size(); i++) { + if (has_selection(i)) { + continue; + } + int last_vis_line = get_last_full_visible_line(); int last_vis_wrap = get_last_full_visible_line_wrap_index(); - - if (cur_line > last_vis_line || (cur_line == last_vis_line && cur_wrap > last_vis_wrap)) { - set_caret_line(last_vis_line, false, false, last_vis_wrap); + if (get_caret_line(i) > last_vis_line || (get_caret_line(i) == last_vis_line && get_caret_wrap_index(i) > last_vis_wrap)) { + set_caret_line(last_vis_line, false, false, last_vis_wrap, i); } } + merge_overlapping_carets(); } void TextEdit::_scroll_lines_down() { @@ -6431,16 +7145,17 @@ void TextEdit::_scroll_lines_down() { set_v_scroll(get_v_scroll() + 1); // Adjust the caret to viewport. - if (!selection.active) { - int cur_line = caret.line; - int cur_wrap = get_caret_wrap_index(); - int first_vis_line = get_first_visible_line(); - int first_vis_wrap = caret.wrap_ofs; + for (int i = 0; i < carets.size(); i++) { + if (has_selection(i)) { + continue; + } - if (cur_line < first_vis_line || (cur_line == first_vis_line && cur_wrap < first_vis_wrap)) { - set_caret_line(first_vis_line, false, false, first_vis_wrap); + int first_vis_line = get_first_visible_line(); + if (get_caret_line(i) < first_vis_line || (get_caret_line(i) == first_vis_line && get_caret_wrap_index(i) < first_visible_line_wrap_ofs)) { + set_caret_line(first_vis_line, false, false, first_visible_line_wrap_ofs, i); } } + merge_overlapping_carets(); } // Minimap @@ -6577,6 +7292,8 @@ void TextEdit::_insert_text(int p_line, int p_char, const String &p_text, int *r op.version = ++version; op.chain_forward = false; op.chain_backward = false; + op.start_carets = carets; + op.end_carets = carets; // See if it should just be set as current op. if (current_op.type != op.type) { @@ -6599,6 +7316,7 @@ void TextEdit::_insert_text(int p_line, int p_char, const String &p_text, int *r current_op.to_column = retchar; current_op.to_line = retline; current_op.version = op.version; + current_op.end_carets = carets; } void TextEdit::_remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column) { @@ -6629,6 +7347,8 @@ void TextEdit::_remove_text(int p_from_line, int p_from_column, int p_to_line, i op.version = ++version; op.chain_forward = false; op.chain_backward = false; + op.start_carets = carets; + op.end_carets = carets; // See if it should just be set as current op. if (current_op.type != op.type) { @@ -6643,6 +7363,7 @@ void TextEdit::_remove_text(int p_from_line, int p_from_column, int p_to_line, i current_op.text = text + current_op.text; current_op.from_line = p_from_line; current_op.from_column = p_from_column; + current_op.end_carets = carets; return; // Update current op. } @@ -6692,7 +7413,7 @@ void TextEdit::_base_insert_text(int p_line, int p_char, const String &p_text, i r_end_line = p_line + substrings.size() - 1; r_end_column = text[r_end_line].length() - postinsert_text.length(); - TextServer::Direction dir = TS->shaped_text_get_dominant_direction_in_range(text.get_line_data(r_end_line)->get_rid(), (r_end_line == p_line) ? caret.column : 0, r_end_column); + TextServer::Direction dir = TS->shaped_text_get_dominant_direction_in_range(text.get_line_data(r_end_line)->get_rid(), (r_end_line == p_line) ? carets[0].column : 0, r_end_column); if (dir != TextServer::DIRECTION_AUTO) { input_direction = (TextDirection)dir; } @@ -6754,6 +7475,7 @@ void TextEdit::_base_remove_text(int p_from_line, int p_from_column, int p_to_li TextEdit::TextEdit(const String &p_placeholder) { placeholder_data_buf.instantiate(); + carets.push_back(Caret()); clear(); set_focus_mode(FOCUS_ALL); diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index a8da878ede..a8e087909e 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -42,6 +42,14 @@ class TextEdit : public Control { GDCLASS(TextEdit, Control); public: + /* Edit Actions. */ + enum EditAction { + ACTION_NONE, + ACTION_TYPING, + ACTION_BACKSPACE, + ACTION_DELETE, + }; + /* Caret. */ enum CaretType { CARET_TYPE_LINE, @@ -299,12 +307,15 @@ private: Key _get_menu_action_accelerator(const String &p_action); /* Versioning */ + struct Caret; struct TextOperation { enum Type { TYPE_NONE, TYPE_INSERT, TYPE_REMOVE }; + Vector<Caret> start_carets; + Vector<Caret> end_carets; Type type = TYPE_NONE; int from_line = 0; @@ -321,6 +332,10 @@ private: bool undo_enabled = true; int undo_stack_max_size = 50; + EditAction current_action = EditAction::ACTION_NONE; + bool pending_action_end = false; + bool in_action = false; + int complex_operation_count = 0; bool next_operation_is_complex = false; @@ -361,19 +376,39 @@ private: int _get_char_pos_for_line(int p_px, int p_line, int p_wrap_index = 0) const; /* Caret. */ + struct Selection { + bool active = false; + bool shiftclick_left = false; + + int selecting_line = 0; + int selecting_column = 0; + int selected_word_beg = 0; + int selected_word_end = 0; + int selected_word_origin = 0; + + int from_line = 0; + int from_column = 0; + int to_line = 0; + int to_column = 0; + }; + struct Caret { + Selection selection; + Point2 draw_pos; bool visible = false; int last_fit_x = 0; int line = 0; int column = 0; - int x_ofs = 0; - int line_ofs = 0; - int wrap_ofs = 0; - } caret; + }; + + // Vector containing all the carets, index '0' is the "main caret" and should never be removed. + Vector<Caret> carets; + Vector<int> caret_index_edit_order; bool setting_caret_line = false; bool caret_pos_dirty = false; + bool caret_index_edit_dirty = true; Color caret_color = Color(1, 1, 1); Color caret_background_color = Color(0, 0, 0); @@ -389,6 +424,8 @@ private: bool caret_mid_grapheme_enabled = true; + bool multi_carets_enabled = true; + bool drag_action = false; bool drag_caret_force_displayed = false; @@ -397,28 +434,10 @@ private: void _reset_caret_blink_timer(); void _toggle_draw_caret(); - int _get_column_x_offset_for_line(int p_char, int p_line) const; + int _get_column_x_offset_for_line(int p_char, int p_line, int p_column) const; /* Selection. */ - struct Selection { - SelectionMode selecting_mode = SelectionMode::SELECTION_MODE_NONE; - int selecting_line = 0; - int selecting_column = 0; - int selected_word_beg = 0; - int selected_word_end = 0; - int selected_word_origin = 0; - bool selecting_text = false; - - bool active = false; - - int from_line = 0; - int from_column = 0; - int to_line = 0; - int to_column = 0; - - bool shiftclick_left = false; - bool drag_attempt = false; - } selection; + SelectionMode selecting_mode = SelectionMode::SELECTION_MODE_NONE; bool selecting_enabled = true; bool deselect_on_focus_loss_enabled = true; @@ -428,6 +447,7 @@ private: Color selection_color = Color(1, 1, 1); bool override_selected_font_color = false; + bool selection_drag_attempt = false; bool dragging_selection = false; Timer *click_select_held = nullptr; @@ -439,8 +459,8 @@ private: void _update_selection_mode_word(); void _update_selection_mode_line(); - void _pre_shift_selection(); - void _post_shift_selection(); + void _pre_shift_selection(int p_caret); + void _post_shift_selection(int p_caret); /* Line wrapping. */ LineWrappingMode line_wrapping_mode = LineWrappingMode::LINE_WRAPPING_NONE; @@ -450,8 +470,6 @@ private: void _update_wrap_at_column(bool p_force = false); - void _update_caret_wrap_offset(); - /* Viewport. */ HScrollBar *h_scroll = nullptr; VScrollBar *v_scroll = nullptr; @@ -466,6 +484,10 @@ private: float v_scroll_speed = 80.0; // Scrolling. + int first_visible_line = 0; + int first_visible_line_wrap_ofs = 0; + int first_visible_col = 0; + bool scrolling = false; bool updating_scrolls = false; @@ -583,6 +605,17 @@ protected: /* Internal API for CodeEdit, pending public API. */ // brace matching + struct BraceMatchingData { + int open_match_line = -1; + int open_match_column = -1; + bool open_matching = false; + bool open_mismatch = false; + int close_match_line = -1; + int close_match_column = -1; + bool close_matching = false; + bool close_mismatch = false; + }; + bool highlight_matching_braces_enabled = false; Color brace_mismatch_color; @@ -607,20 +640,20 @@ protected: /* Text manipulation */ // Overridable actions - virtual void _handle_unicode_input_internal(const uint32_t p_unicode); - virtual void _backspace_internal(); + virtual void _handle_unicode_input_internal(const uint32_t p_unicode, int p_caret); + virtual void _backspace_internal(int p_caret); - virtual void _cut_internal(); - virtual void _copy_internal(); - virtual void _paste_internal(); - virtual void _paste_primary_clipboard_internal(); + virtual void _cut_internal(int p_caret); + virtual void _copy_internal(int p_caret); + virtual void _paste_internal(int p_caret); + virtual void _paste_primary_clipboard_internal(int p_caret); - GDVIRTUAL1(_handle_unicode_input, int) - GDVIRTUAL0(_backspace) - GDVIRTUAL0(_cut) - GDVIRTUAL0(_copy) - GDVIRTUAL0(_paste) - GDVIRTUAL0(_paste_primary_clipboard) + GDVIRTUAL2(_handle_unicode_input, int, int) + GDVIRTUAL1(_backspace, int) + GDVIRTUAL1(_cut, int) + GDVIRTUAL1(_copy, int) + GDVIRTUAL1(_paste, int) + GDVIRTUAL1(_paste_primary_clipboard, int) public: /* General overrides. */ @@ -696,7 +729,7 @@ public: void swap_lines(int p_from_line, int p_to_line); void insert_line_at(int p_at, const String &p_text); - void insert_text_at_caret(const String &p_text); + void insert_text_at_caret(const String &p_text, int p_caret = -1); void remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column); @@ -705,13 +738,13 @@ public: Point2i get_next_visible_line_index_offset_from(int p_line_from, int p_wrap_index_from, int p_visible_amount) const; // Overridable actions - void handle_unicode_input(const uint32_t p_unicode); - void backspace(); + void handle_unicode_input(const uint32_t p_unicode, int p_caret = -1); + void backspace(int p_caret = -1); - void cut(); - void copy(); - void paste(); - void paste_primary_clipboard(); + void cut(int p_caret = -1); + void copy(int p_caret = -1); + void paste(int p_caret = -1); + void paste_primary_clipboard(int p_caret = -1); // Context menu. PopupMenu *get_menu() const; @@ -719,6 +752,10 @@ public: void menu_option(int p_option); /* Versioning */ + void start_action(EditAction p_action); + void end_action(); + EditAction get_current_action() const; + void begin_complex_operation(); void end_complex_operation(); @@ -753,7 +790,7 @@ public: int get_minimap_line_at_pos(const Point2i &p_pos) const; bool is_dragging_cursor() const; - bool is_mouse_over_selection(bool p_edges = true) const; + bool is_mouse_over_selection(bool p_edges = true, int p_caret = -1) const; /* Caret */ void set_caret_type(CaretType p_type); @@ -771,18 +808,30 @@ public: void set_caret_mid_grapheme_enabled(const bool p_enabled); bool is_caret_mid_grapheme_enabled() const; - bool is_caret_visible() const; - Point2 get_caret_draw_pos() const; + void set_multiple_carets_enabled(bool p_enabled); + bool is_multiple_carets_enabled() const; + + int add_caret(int p_line, int p_col); + void remove_caret(int p_caret); + void remove_secondary_carets(); + void merge_overlapping_carets(); + int get_caret_count() const; + + Vector<int> get_caret_index_edit_order(); + void adjust_carets_after_edit(int p_caret, int p_from_line, int p_from_col, int p_to_line, int p_to_col); + + bool is_caret_visible(int p_caret = 0) const; + Point2 get_caret_draw_pos(int p_caret = 0) const; - void set_caret_line(int p_line, bool p_adjust_viewport = true, bool p_can_be_hidden = true, int p_wrap_index = 0); - int get_caret_line() const; + void set_caret_line(int p_line, bool p_adjust_viewport = true, bool p_can_be_hidden = true, int p_wrap_index = 0, int p_caret = 0); + int get_caret_line(int p_caret = 0) const; - void set_caret_column(int p_col, bool p_adjust_viewport = true); - int get_caret_column() const; + void set_caret_column(int p_col, bool p_adjust_viewport = true, int p_caret = 0); + int get_caret_column(int p_caret = 0) const; - int get_caret_wrap_index() const; + int get_caret_wrap_index(int p_caret = 0) const; - String get_word_under_caret() const; + String get_word_under_caret(int p_caret = -1) const; /* Selection. */ void set_selecting_enabled(const bool p_enabled); @@ -797,27 +846,27 @@ public: void set_override_selected_font_color(bool p_override_selected_font_color); bool is_overriding_selected_font_color() const; - void set_selection_mode(SelectionMode p_mode, int p_line = -1, int p_column = -1); + void set_selection_mode(SelectionMode p_mode, int p_line = -1, int p_column = -1, int p_caret = 0); SelectionMode get_selection_mode() const; void select_all(); - void select_word_under_caret(); - void select(int p_from_line, int p_from_column, int p_to_line, int p_to_column); + void select_word_under_caret(int p_caret = -1); + void select(int p_from_line, int p_from_column, int p_to_line, int p_to_column, int p_caret = 0); - bool has_selection() const; + bool has_selection(int p_caret = -1) const; - String get_selected_text() const; + String get_selected_text(int p_caret = -1); - int get_selection_line() const; - int get_selection_column() const; + int get_selection_line(int p_caret = 0) const; + int get_selection_column(int p_caret = 0) const; - int get_selection_from_line() const; - int get_selection_from_column() const; - int get_selection_to_line() const; - int get_selection_to_column() const; + int get_selection_from_line(int p_caret = 0) const; + int get_selection_from_column(int p_caret = 0) const; + int get_selection_to_line(int p_caret = 0) const; + int get_selection_to_column(int p_caret = 0) const; - void deselect(); - void delete_selection(); + void deselect(int p_caret = -1); + void delete_selection(int p_caret = -1); /* Line wrapping. */ void set_line_wrapping_mode(LineWrappingMode p_wrapping_mode); @@ -866,8 +915,8 @@ public: int get_total_visible_line_count() const; // Auto Adjust - void adjust_viewport_to_caret(); - void center_viewport_to_caret(); + void adjust_viewport_to_caret(int p_caret = 0); + void center_viewport_to_caret(int p_caret = 0); // Minimap void set_draw_minimap(bool p_enabled); @@ -949,6 +998,7 @@ public: TextEdit(const String &p_placeholder = String()); }; +VARIANT_ENUM_CAST(TextEdit::EditAction); VARIANT_ENUM_CAST(TextEdit::CaretType); VARIANT_ENUM_CAST(TextEdit::LineWrappingMode); VARIANT_ENUM_CAST(TextEdit::SelectionMode); diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp index f82a853e56..db7d390b16 100644 --- a/scene/gui/tree.cpp +++ b/scene/gui/tree.cpp @@ -1163,7 +1163,7 @@ bool TreeItem::is_editable(int p_column) { void TreeItem::set_custom_color(int p_column, const Color &p_color) { ERR_FAIL_INDEX(p_column, cells.size()); - if (cells[p_column].custom_color == true) { + if (cells[p_column].custom_color && cells[p_column].color == p_color) { return; } diff --git a/scene/resources/curve.cpp b/scene/resources/curve.cpp index 0ea5264935..49b78a091d 100644 --- a/scene/resources/curve.cpp +++ b/scene/resources/curve.cpp @@ -1667,6 +1667,10 @@ Vector3 Curve3D::sample_baked_up_vector(real_t p_offset, bool p_apply_tilt) cons idx = (end + start) / 2; } + if (idx == count - 1) { + return p_apply_tilt ? r[idx].rotated((rp[idx] - rp[idx - 1]).normalized(), rt[idx]) : r[idx]; + } + real_t offset_begin = baked_dist_cache[idx]; real_t offset_end = baked_dist_cache[idx + 1]; @@ -1675,10 +1679,6 @@ Vector3 Curve3D::sample_baked_up_vector(real_t p_offset, bool p_apply_tilt) cons real_t frac = (p_offset - offset_begin) / idx_interval; - if (idx == count - 1) { - return p_apply_tilt ? r[idx].rotated((rp[idx] - rp[idx - 1]).normalized(), rt[idx]) : r[idx]; - } - Vector3 forward = (rp[idx + 1] - rp[idx]).normalized(); Vector3 up = r[idx]; Vector3 up1 = r[idx + 1]; diff --git a/scene/resources/particle_process_material.h b/scene/resources/particle_process_material.h index 9430e5797d..8fe9223a47 100644 --- a/scene/resources/particle_process_material.h +++ b/scene/resources/particle_process_material.h @@ -265,9 +265,9 @@ private: float spread = 0.0f; float flatness = 0.0f; - float params_min[PARAM_MAX]; - float params_max[PARAM_MAX]; - float params[PARAM_MAX]; + float params_min[PARAM_MAX] = {}; + float params_max[PARAM_MAX] = {}; + float params[PARAM_MAX] = {}; Ref<Texture2D> tex_parameters[PARAM_MAX]; Color color; diff --git a/scene/resources/surface_tool.cpp b/scene/resources/surface_tool.cpp index 9829c7e86b..94967352c8 100644 --- a/scene/resources/surface_tool.cpp +++ b/scene/resources/surface_tool.cpp @@ -986,7 +986,7 @@ void SurfaceTool::append_from(const Ref<Mesh> &p_existing, int p_surface, const format = 0; } - uint32_t nformat; + uint32_t nformat = 0; LocalVector<Vertex> nvertices; LocalVector<int> nindices; _create_list(p_existing, p_surface, &nvertices, &nindices, nformat); diff --git a/scene/resources/tile_set.cpp b/scene/resources/tile_set.cpp index 65f3767449..813be773b7 100644 --- a/scene/resources/tile_set.cpp +++ b/scene/resources/tile_set.cpp @@ -5017,7 +5017,7 @@ void TileData::add_custom_data_layer(int p_to_pos) { void TileData::move_custom_data_layer(int p_from_index, int p_to_pos) { ERR_FAIL_INDEX(p_from_index, custom_data.size()); ERR_FAIL_INDEX(p_to_pos, custom_data.size() + 1); - custom_data.insert(p_to_pos, navigation[p_from_index]); + custom_data.insert(p_to_pos, custom_data[p_from_index]); custom_data.remove_at(p_to_pos < p_from_index ? p_from_index + 1 : p_from_index); } diff --git a/servers/extensions/physics_server_3d_extension.cpp b/servers/extensions/physics_server_3d_extension.cpp index 27f9ed2cb3..811ff231fc 100644 --- a/servers/extensions/physics_server_3d_extension.cpp +++ b/servers/extensions/physics_server_3d_extension.cpp @@ -60,6 +60,7 @@ void PhysicsDirectBodyState3DExtension::_bind_methods() { GDVIRTUAL_BIND(_get_inverse_mass); GDVIRTUAL_BIND(_get_inverse_inertia); + GDVIRTUAL_BIND(_get_inverse_inertia_tensor); GDVIRTUAL_BIND(_set_linear_velocity, "velocity"); GDVIRTUAL_BIND(_get_linear_velocity); diff --git a/servers/physics_2d/godot_body_pair_2d.cpp b/servers/physics_2d/godot_body_pair_2d.cpp index 2f2af15da7..7b7c67bbc2 100644 --- a/servers/physics_2d/godot_body_pair_2d.cpp +++ b/servers/physics_2d/godot_body_pair_2d.cpp @@ -170,7 +170,7 @@ bool GodotBodyPair2D::_test_ccd(real_t p_step, GodotBody2D *p_A, int p_shape_A, Vector2 mnormal = motion / mlen; - real_t min, max; + real_t min = 0.0, max = 0.0; p_A->get_shape(p_shape_A)->project_rangev(mnormal, p_xform_A, min, max); // Did it move enough in this direction to even attempt raycast? diff --git a/servers/physics_2d/godot_collision_solver_2d.cpp b/servers/physics_2d/godot_collision_solver_2d.cpp index 52237539c0..9c7b8fbe53 100644 --- a/servers/physics_2d/godot_collision_solver_2d.cpp +++ b/servers/physics_2d/godot_collision_solver_2d.cpp @@ -197,7 +197,7 @@ bool GodotCollisionSolver2D::solve_concave(const GodotShape2D *p_shape_A, const real_t axis_scale = 1.0 / axis.length(); axis *= axis_scale; - real_t smin, smax; + real_t smin = 0.0, smax = 0.0; p_shape_A->project_rangev(axis, rel_transform, smin, smax); smin *= axis_scale; smax *= axis_scale; diff --git a/servers/physics_2d/godot_collision_solver_2d_sat.cpp b/servers/physics_2d/godot_collision_solver_2d_sat.cpp index 8aa30ad6a4..f8924239ea 100644 --- a/servers/physics_2d/godot_collision_solver_2d_sat.cpp +++ b/servers/physics_2d/godot_collision_solver_2d_sat.cpp @@ -234,7 +234,7 @@ public: axis = Vector2(0.0, 1.0); } - real_t min_A, max_A, min_B, max_B; + real_t min_A = 0.0, max_A = 0.0, min_B = 0.0, max_B = 0.0; if (castA) { shape_A->project_range_cast(motion_A, axis, *transform_A, min_A, max_A); diff --git a/servers/physics_3d/godot_body_pair_3d.cpp b/servers/physics_3d/godot_body_pair_3d.cpp index eebbe0196d..7e6cc6f834 100644 --- a/servers/physics_3d/godot_body_pair_3d.cpp +++ b/servers/physics_3d/godot_body_pair_3d.cpp @@ -170,7 +170,7 @@ bool GodotBodyPair3D::_test_ccd(real_t p_step, GodotBody3D *p_A, int p_shape_A, Vector3 mnormal = motion / mlen; - real_t min, max; + real_t min = 0.0, max = 0.0; p_A->get_shape(p_shape_A)->project_range(mnormal, p_xform_A, min, max); // Did it move enough in this direction to even attempt raycast? diff --git a/servers/physics_3d/godot_collision_solver_3d.cpp b/servers/physics_3d/godot_collision_solver_3d.cpp index 9fe0e3eb84..ca76a819ec 100644 --- a/servers/physics_3d/godot_collision_solver_3d.cpp +++ b/servers/physics_3d/godot_collision_solver_3d.cpp @@ -48,7 +48,7 @@ bool GodotCollisionSolver3D::solve_static_world_boundary(const GodotShape3D *p_s static const int max_supports = 16; Vector3 supports[max_supports]; int support_count; - GodotShape3D::FeatureType support_type; + GodotShape3D::FeatureType support_type = GodotShape3D::FeatureType::FEATURE_POINT; p_shape_B->get_supports(p_transform_B.basis.xform_inv(-p.normal).normalized(), max_supports, supports, support_count, support_type); if (support_type == GodotShape3D::FEATURE_CIRCLE) { @@ -338,7 +338,7 @@ bool GodotCollisionSolver3D::solve_concave(const GodotShape3D *p_shape_A, const real_t axis_scale = 1.0 / axis.length(); axis *= axis_scale; - real_t smin, smax; + real_t smin = 0.0, smax = 0.0; p_shape_A->project_range(axis, rel_transform, smin, smax); smin -= p_margin_A; smax += p_margin_A; diff --git a/servers/physics_3d/godot_collision_solver_3d_sat.cpp b/servers/physics_3d/godot_collision_solver_3d_sat.cpp index 56e644b57b..933a5e28df 100644 --- a/servers/physics_3d/godot_collision_solver_3d_sat.cpp +++ b/servers/physics_3d/godot_collision_solver_3d_sat.cpp @@ -634,7 +634,7 @@ public: axis = Vector3(0.0, 1.0, 0.0); } - real_t min_A, max_A, min_B, max_B; + real_t min_A = 0.0, max_A = 0.0, min_B = 0.0, max_B = 0.0; shape_A->project_range(axis, *transform_A, min_A, max_A); shape_B->project_range(axis, *transform_B, min_B, max_B); diff --git a/servers/physics_server_2d.h b/servers/physics_server_2d.h index a3feb97d5d..95425e72e8 100644 --- a/servers/physics_server_2d.h +++ b/servers/physics_server_2d.h @@ -490,7 +490,7 @@ public: bool collide_separation_ray = false; HashSet<RID> exclude_bodies; HashSet<ObjectID> exclude_objects; - bool recovery_as_collision = false; // Don't report margin recovery as collision by default, only used for floor snapping. + bool recovery_as_collision = false; MotionParameters() {} diff --git a/servers/physics_server_3d.cpp b/servers/physics_server_3d.cpp index 24cc7e8459..db21e1db06 100644 --- a/servers/physics_server_3d.cpp +++ b/servers/physics_server_3d.cpp @@ -99,6 +99,7 @@ void PhysicsDirectBodyState3D::_bind_methods() { ClassDB::bind_method(D_METHOD("get_inverse_mass"), &PhysicsDirectBodyState3D::get_inverse_mass); ClassDB::bind_method(D_METHOD("get_inverse_inertia"), &PhysicsDirectBodyState3D::get_inverse_inertia); + ClassDB::bind_method(D_METHOD("get_inverse_inertia_tensor"), &PhysicsDirectBodyState3D::get_inverse_inertia_tensor); ClassDB::bind_method(D_METHOD("set_linear_velocity", "velocity"), &PhysicsDirectBodyState3D::set_linear_velocity); ClassDB::bind_method(D_METHOD("get_linear_velocity"), &PhysicsDirectBodyState3D::get_linear_velocity); @@ -153,6 +154,7 @@ void PhysicsDirectBodyState3D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "total_angular_damp"), "", "get_total_angular_damp"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "total_linear_damp"), "", "get_total_linear_damp"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "inverse_inertia"), "", "get_inverse_inertia"); + ADD_PROPERTY(PropertyInfo(Variant::BASIS, "inverse_inertia_tensor"), "", "get_inverse_inertia_tensor"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "total_gravity"), "", "get_total_gravity"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "center_of_mass"), "", "get_center_of_mass"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "center_of_mass_local"), "", "get_center_of_mass_local"); diff --git a/servers/physics_server_3d.h b/servers/physics_server_3d.h index b21c4e9249..87877ad52d 100644 --- a/servers/physics_server_3d.h +++ b/servers/physics_server_3d.h @@ -527,7 +527,7 @@ public: bool collide_separation_ray = false; HashSet<RID> exclude_bodies; HashSet<ObjectID> exclude_objects; - bool recovery_as_collision = false; // Don't report margin recovery as collision by default, only used for floor snapping. + bool recovery_as_collision = false; MotionParameters() {} diff --git a/servers/rendering/renderer_rd/environment/fog.cpp b/servers/rendering/renderer_rd/environment/fog.cpp index 4c72f0a56e..74082906c4 100644 --- a/servers/rendering/renderer_rd/environment/fog.cpp +++ b/servers/rendering/renderer_rd/environment/fog.cpp @@ -143,7 +143,7 @@ Vector3 Fog::fog_volume_get_extents(RID p_fog_volume) const { bool Fog::FogMaterialData::update_parameters(const HashMap<StringName, Variant> &p_parameters, bool p_uniform_dirty, bool p_textures_dirty) { uniform_set_updated = true; - return update_parameters_uniform_set(p_parameters, p_uniform_dirty, p_textures_dirty, shader_data->uniforms, shader_data->ubo_offsets.ptr(), shader_data->texture_uniforms, shader_data->default_texture_params, shader_data->ubo_size, uniform_set, Fog::get_singleton()->volumetric_fog.shader.version_get_shader(shader_data->version, 0), VolumetricFogShader::FogSet::FOG_SET_MATERIAL); + return update_parameters_uniform_set(p_parameters, p_uniform_dirty, p_textures_dirty, shader_data->uniforms, shader_data->ubo_offsets.ptr(), shader_data->texture_uniforms, shader_data->default_texture_params, shader_data->ubo_size, uniform_set, Fog::get_singleton()->volumetric_fog.shader.version_get_shader(shader_data->version, 0), VolumetricFogShader::FogSet::FOG_SET_MATERIAL, true); } Fog::FogMaterialData::~FogMaterialData() { diff --git a/servers/rendering/renderer_rd/environment/sky.cpp b/servers/rendering/renderer_rd/environment/sky.cpp index 84130f9504..4ff2a66975 100644 --- a/servers/rendering/renderer_rd/environment/sky.cpp +++ b/servers/rendering/renderer_rd/environment/sky.cpp @@ -268,7 +268,7 @@ bool SkyRD::SkyMaterialData::update_parameters(const HashMap<StringName, Variant uniform_set_updated = true; - return update_parameters_uniform_set(p_parameters, p_uniform_dirty, p_textures_dirty, shader_data->uniforms, shader_data->ubo_offsets.ptr(), shader_data->texture_uniforms, shader_data->default_texture_params, shader_data->ubo_size, uniform_set, scene_singleton->sky.sky_shader.shader.version_get_shader(shader_data->version, 0), SKY_SET_MATERIAL); + return update_parameters_uniform_set(p_parameters, p_uniform_dirty, p_textures_dirty, shader_data->uniforms, shader_data->ubo_offsets.ptr(), shader_data->texture_uniforms, shader_data->default_texture_params, shader_data->ubo_size, uniform_set, scene_singleton->sky.sky_shader.shader.version_get_shader(shader_data->version, 0), SKY_SET_MATERIAL, true); } SkyRD::SkyMaterialData::~SkyMaterialData() { diff --git a/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp b/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp index cf08949d99..69d444aeeb 100644 --- a/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp +++ b/servers/rendering/renderer_rd/forward_clustered/render_forward_clustered.cpp @@ -2174,11 +2174,11 @@ void RenderForwardClustered::_render_shadow_pass(RID p_light, RID p_shadow_atlas } } - int directional_shadow_size = light_storage->directional_shadow_get_size(); - atlas_rect.position /= directional_shadow_size; - atlas_rect.size /= directional_shadow_size; - - light_storage->light_instance_set_directional_shadow_atlas_rect(p_light, p_pass, atlas_rect); + float directional_shadow_size = light_storage->directional_shadow_get_size(); + Rect2 atlas_rect_norm = atlas_rect; + atlas_rect_norm.position /= directional_shadow_size; + atlas_rect_norm.size /= directional_shadow_size; + light_storage->light_instance_set_directional_shadow_atlas_rect(p_light, p_pass, atlas_rect_norm); zfar = RSG::light_storage->light_get_param(base, RS::LIGHT_PARAM_RANGE); diff --git a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp index 1987577464..c01919a606 100644 --- a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp +++ b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.cpp @@ -509,7 +509,7 @@ void SceneShaderForwardClustered::MaterialData::set_next_pass(RID p_pass) { bool SceneShaderForwardClustered::MaterialData::update_parameters(const HashMap<StringName, Variant> &p_parameters, bool p_uniform_dirty, bool p_textures_dirty) { SceneShaderForwardClustered *shader_singleton = (SceneShaderForwardClustered *)SceneShaderForwardClustered::singleton; - return update_parameters_uniform_set(p_parameters, p_uniform_dirty, p_textures_dirty, shader_data->uniforms, shader_data->ubo_offsets.ptr(), shader_data->texture_uniforms, shader_data->default_texture_params, shader_data->ubo_size, uniform_set, shader_singleton->shader.version_get_shader(shader_data->version, 0), RenderForwardClustered::MATERIAL_UNIFORM_SET, RD::BARRIER_MASK_RASTER); + return update_parameters_uniform_set(p_parameters, p_uniform_dirty, p_textures_dirty, shader_data->uniforms, shader_data->ubo_offsets.ptr(), shader_data->texture_uniforms, shader_data->default_texture_params, shader_data->ubo_size, uniform_set, shader_singleton->shader.version_get_shader(shader_data->version, 0), RenderForwardClustered::MATERIAL_UNIFORM_SET, true, RD::BARRIER_MASK_RASTER); } SceneShaderForwardClustered::MaterialData::~MaterialData() { diff --git a/servers/rendering/renderer_rd/forward_mobile/render_forward_mobile.cpp b/servers/rendering/renderer_rd/forward_mobile/render_forward_mobile.cpp index c3ce1b05f1..2b2090f8ed 100644 --- a/servers/rendering/renderer_rd/forward_mobile/render_forward_mobile.cpp +++ b/servers/rendering/renderer_rd/forward_mobile/render_forward_mobile.cpp @@ -1128,11 +1128,11 @@ void RenderForwardMobile::_render_shadow_pass(RID p_light, RID p_shadow_atlas, i } } - int directional_shadow_size = light_storage->directional_shadow_get_size(); - atlas_rect.position /= directional_shadow_size; - atlas_rect.size /= directional_shadow_size; - - light_storage->light_instance_set_directional_shadow_atlas_rect(p_light, p_pass, atlas_rect); + float directional_shadow_size = light_storage->directional_shadow_get_size(); + Rect2 atlas_rect_norm = atlas_rect; + atlas_rect_norm.position /= directional_shadow_size; + atlas_rect_norm.size /= directional_shadow_size; + light_storage->light_instance_set_directional_shadow_atlas_rect(p_light, p_pass, atlas_rect_norm); zfar = RSG::light_storage->light_get_param(base, RS::LIGHT_PARAM_RANGE); diff --git a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp index 691d431b82..71fb3ee4f5 100644 --- a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp +++ b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.cpp @@ -464,7 +464,7 @@ void SceneShaderForwardMobile::MaterialData::set_next_pass(RID p_pass) { bool SceneShaderForwardMobile::MaterialData::update_parameters(const HashMap<StringName, Variant> &p_parameters, bool p_uniform_dirty, bool p_textures_dirty) { SceneShaderForwardMobile *shader_singleton = (SceneShaderForwardMobile *)SceneShaderForwardMobile::singleton; - return update_parameters_uniform_set(p_parameters, p_uniform_dirty, p_textures_dirty, shader_data->uniforms, shader_data->ubo_offsets.ptr(), shader_data->texture_uniforms, shader_data->default_texture_params, shader_data->ubo_size, uniform_set, shader_singleton->shader.version_get_shader(shader_data->version, 0), RenderForwardMobile::MATERIAL_UNIFORM_SET, RD::BARRIER_MASK_RASTER); + return update_parameters_uniform_set(p_parameters, p_uniform_dirty, p_textures_dirty, shader_data->uniforms, shader_data->ubo_offsets.ptr(), shader_data->texture_uniforms, shader_data->default_texture_params, shader_data->ubo_size, uniform_set, shader_singleton->shader.version_get_shader(shader_data->version, 0), RenderForwardMobile::MATERIAL_UNIFORM_SET, true, RD::BARRIER_MASK_RASTER); } SceneShaderForwardMobile::MaterialData::~MaterialData() { diff --git a/servers/rendering/renderer_rd/renderer_canvas_render_rd.cpp b/servers/rendering/renderer_rd/renderer_canvas_render_rd.cpp index 3de6f05e84..4c14ef7b9e 100644 --- a/servers/rendering/renderer_rd/renderer_canvas_render_rd.cpp +++ b/servers/rendering/renderer_rd/renderer_canvas_render_rd.cpp @@ -2324,7 +2324,7 @@ RendererRD::MaterialStorage::ShaderData *RendererCanvasRenderRD::_create_shader_ bool RendererCanvasRenderRD::CanvasMaterialData::update_parameters(const HashMap<StringName, Variant> &p_parameters, bool p_uniform_dirty, bool p_textures_dirty) { RendererCanvasRenderRD *canvas_singleton = static_cast<RendererCanvasRenderRD *>(RendererCanvasRender::singleton); - return update_parameters_uniform_set(p_parameters, p_uniform_dirty, p_textures_dirty, shader_data->uniforms, shader_data->ubo_offsets.ptr(), shader_data->texture_uniforms, shader_data->default_texture_params, shader_data->ubo_size, uniform_set, canvas_singleton->shader.canvas_shader.version_get_shader(shader_data->version, 0), MATERIAL_UNIFORM_SET); + return update_parameters_uniform_set(p_parameters, p_uniform_dirty, p_textures_dirty, shader_data->uniforms, shader_data->ubo_offsets.ptr(), shader_data->texture_uniforms, shader_data->default_texture_params, shader_data->ubo_size, uniform_set, canvas_singleton->shader.canvas_shader.version_get_shader(shader_data->version, 0), MATERIAL_UNIFORM_SET, false); } RendererCanvasRenderRD::CanvasMaterialData::~CanvasMaterialData() { diff --git a/servers/rendering/renderer_rd/storage_rd/material_storage.cpp b/servers/rendering/renderer_rd/storage_rd/material_storage.cpp index 626738d971..1fecac9045 100644 --- a/servers/rendering/renderer_rd/storage_rd/material_storage.cpp +++ b/servers/rendering/renderer_rd/storage_rd/material_storage.cpp @@ -1270,7 +1270,7 @@ void MaterialStorage::MaterialData::free_parameters_uniform_set(RID p_uniform_se } } -bool MaterialStorage::MaterialData::update_parameters_uniform_set(const HashMap<StringName, Variant> &p_parameters, bool p_uniform_dirty, bool p_textures_dirty, const HashMap<StringName, ShaderLanguage::ShaderNode::Uniform> &p_uniforms, const uint32_t *p_uniform_offsets, const Vector<ShaderCompiler::GeneratedCode::Texture> &p_texture_uniforms, const HashMap<StringName, HashMap<int, RID>> &p_default_texture_params, uint32_t p_ubo_size, RID &uniform_set, RID p_shader, uint32_t p_shader_uniform_set, uint32_t p_barrier) { +bool MaterialStorage::MaterialData::update_parameters_uniform_set(const HashMap<StringName, Variant> &p_parameters, bool p_uniform_dirty, bool p_textures_dirty, const HashMap<StringName, ShaderLanguage::ShaderNode::Uniform> &p_uniforms, const uint32_t *p_uniform_offsets, const Vector<ShaderCompiler::GeneratedCode::Texture> &p_texture_uniforms, const HashMap<StringName, HashMap<int, RID>> &p_default_texture_params, uint32_t p_ubo_size, RID &uniform_set, RID p_shader, uint32_t p_shader_uniform_set, bool p_use_linear_color, uint32_t p_barrier) { if ((uint32_t)ubo_data.size() != p_ubo_size) { p_uniform_dirty = true; if (uniform_buffer.is_valid()) { @@ -1294,7 +1294,7 @@ bool MaterialStorage::MaterialData::update_parameters_uniform_set(const HashMap< //check whether buffer changed if (p_uniform_dirty && ubo_data.size()) { - update_uniform_buffer(p_uniforms, p_uniform_offsets, p_parameters, ubo_data.ptrw(), ubo_data.size(), true); + update_uniform_buffer(p_uniforms, p_uniform_offsets, p_parameters, ubo_data.ptrw(), ubo_data.size(), p_use_linear_color); RD::get_singleton()->buffer_update(uniform_buffer, 0, ubo_data.size(), ubo_data.ptrw(), p_barrier); } diff --git a/servers/rendering/renderer_rd/storage_rd/material_storage.h b/servers/rendering/renderer_rd/storage_rd/material_storage.h index 330eac777f..19c2f39553 100644 --- a/servers/rendering/renderer_rd/storage_rd/material_storage.h +++ b/servers/rendering/renderer_rd/storage_rd/material_storage.h @@ -79,7 +79,7 @@ public: virtual ~MaterialData(); //to be used internally by update_parameters, in the most common configuration of material parameters - bool update_parameters_uniform_set(const HashMap<StringName, Variant> &p_parameters, bool p_uniform_dirty, bool p_textures_dirty, const HashMap<StringName, ShaderLanguage::ShaderNode::Uniform> &p_uniforms, const uint32_t *p_uniform_offsets, const Vector<ShaderCompiler::GeneratedCode::Texture> &p_texture_uniforms, const HashMap<StringName, HashMap<int, RID>> &p_default_texture_params, uint32_t p_ubo_size, RID &uniform_set, RID p_shader, uint32_t p_shader_uniform_set, uint32_t p_barrier = RD::BARRIER_MASK_ALL); + bool update_parameters_uniform_set(const HashMap<StringName, Variant> &p_parameters, bool p_uniform_dirty, bool p_textures_dirty, const HashMap<StringName, ShaderLanguage::ShaderNode::Uniform> &p_uniforms, const uint32_t *p_uniform_offsets, const Vector<ShaderCompiler::GeneratedCode::Texture> &p_texture_uniforms, const HashMap<StringName, HashMap<int, RID>> &p_default_texture_params, uint32_t p_ubo_size, RID &uniform_set, RID p_shader, uint32_t p_shader_uniform_set, bool p_use_linear_color, uint32_t p_barrier = RD::BARRIER_MASK_ALL); void free_parameters_uniform_set(RID p_uniform_set); private: diff --git a/servers/rendering/renderer_rd/storage_rd/particles_storage.cpp b/servers/rendering/renderer_rd/storage_rd/particles_storage.cpp index 18303ce870..78f4c410c3 100644 --- a/servers/rendering/renderer_rd/storage_rd/particles_storage.cpp +++ b/servers/rendering/renderer_rd/storage_rd/particles_storage.cpp @@ -1712,7 +1712,7 @@ MaterialStorage::ShaderData *ParticlesStorage::_create_particles_shader_func() { } bool ParticlesStorage::ParticleProcessMaterialData::update_parameters(const HashMap<StringName, Variant> &p_parameters, bool p_uniform_dirty, bool p_textures_dirty) { - return update_parameters_uniform_set(p_parameters, p_uniform_dirty, p_textures_dirty, shader_data->uniforms, shader_data->ubo_offsets.ptr(), shader_data->texture_uniforms, shader_data->default_texture_params, shader_data->ubo_size, uniform_set, ParticlesStorage::get_singleton()->particles_shader.shader.version_get_shader(shader_data->version, 0), 3); + return update_parameters_uniform_set(p_parameters, p_uniform_dirty, p_textures_dirty, shader_data->uniforms, shader_data->ubo_offsets.ptr(), shader_data->texture_uniforms, shader_data->default_texture_params, shader_data->ubo_size, uniform_set, ParticlesStorage::get_singleton()->particles_shader.shader.version_get_shader(shader_data->version, 0), 3, true); } ParticlesStorage::ParticleProcessMaterialData::~ParticleProcessMaterialData() { diff --git a/tests/scene/test_text_edit.h b/tests/scene/test_text_edit.h index 225316b293..6b831bc9c7 100644 --- a/tests/scene/test_text_edit.h +++ b/tests/scene/test_text_edit.h @@ -93,10 +93,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->undo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "test text"); - CHECK(text_edit->get_caret_column() == 9); + CHECK(text_edit->get_caret_column() == 0); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_set"); text_edit->redo(); @@ -104,18 +104,18 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_text() == ""); CHECK(text_edit->get_caret_column() == 0); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_set"); // Cannot undo when not-editable but should still clear. text_edit->undo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "test text"); - CHECK(text_edit->get_caret_column() == 9); + CHECK(text_edit->get_caret_column() == 0); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_set"); // Clear. @@ -131,8 +131,8 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_text() == ""); CHECK(text_edit->get_caret_column() == 0); SIGNAL_CHECK("text_set", empty_signal_args); - SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_clear_args); + SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); text_edit->set_editable(true); @@ -252,6 +252,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_line(0) == "te"); CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "te"); + CHECK(text_edit->get_caret_column() == 2); SIGNAL_CHECK("lines_edited_from", lines_edited_args); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); @@ -263,6 +264,8 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_line(0) == "test text"); CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "test"); + CHECK(text_edit->get_caret_column() == 4); SIGNAL_CHECK("lines_edited_from", lines_edited_args); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); @@ -271,7 +274,8 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->redo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_line(0) == "te"); - CHECK_FALSE(text_edit->has_selection()); // Currently not handled. + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_column() == 2); SIGNAL_CHECK("lines_edited_from", lines_edited_args); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); @@ -425,7 +429,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->undo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "testing\nswap"); - CHECK_FALSE(text_edit->has_selection()); // Not currently handled. + CHECK(text_edit->has_selection()); SIGNAL_CHECK("lines_edited_from", lines_edited_args); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); @@ -436,7 +440,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->redo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "new\ntesting\nswap"); - CHECK_FALSE(text_edit->has_selection()); // Not currently handled. + CHECK(text_edit->has_selection()); SIGNAL_CHECK("lines_edited_from", lines_edited_args); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); @@ -448,7 +452,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->has_selection()); CHECK(text_edit->get_selection_from_line() == 0); CHECK(text_edit->get_selection_to_line() == 2); - SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("caret_changed"); ((Array)lines_edited_args[0])[0] = 2; ((Array)lines_edited_args[0])[1] = 3; @@ -533,7 +537,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "temidsting\nswap"); CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 10); + CHECK(text_edit->get_caret_column() == 5); CHECK(text_edit->has_selection()); SIGNAL_CHECK("lines_edited_from", lines_edited_args); SIGNAL_CHECK("text_changed", empty_signal_args); @@ -950,11 +954,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->delete_selection(); CHECK(text_edit->get_text() == "this is some text\nfor selection"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 8); text_edit->select(0, 8, 0, 4); CHECK(text_edit->has_selection()); SEND_GUI_ACTION(text_edit, "ui_text_backspace"); CHECK(text_edit->get_text() == "thissome text\nfor selection"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); text_edit->undo(); CHECK(text_edit->has_selection()); @@ -966,7 +974,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_text() == "thissome text\nfor selection"); CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 8); + CHECK(text_edit->get_caret_column() == 4); text_edit->undo(); CHECK(text_edit->has_selection()); @@ -991,7 +999,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_text() == "thissome text\nfor selection"); CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 8); + CHECK(text_edit->get_caret_column() == 4); text_edit->undo(); CHECK(text_edit->has_selection()); @@ -1176,8 +1184,8 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->undo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "this is\nsome\n"); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 6); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); @@ -1187,9 +1195,9 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->redo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "some\n"); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 0); - SIGNAL_CHECK_FALSE("caret_changed"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); @@ -1259,9 +1267,14 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { lines_edited_args.push_back(args1); SUBCASE("[TextEdit] ui_text_newline_above") { - text_edit->set_text("this is some test text."); + text_edit->set_text("this is some test text.\nthis is some test text."); text_edit->select(0, 0, 0, 4); text_edit->set_caret_column(4); + + text_edit->add_caret(1, 4); + text_edit->select(1, 0, 1, 4, 1); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -1269,50 +1282,78 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - ((Array)lines_edited_args[0])[1] = 1; + // For the second caret. + Array args2; + args2.push_back(0); + args2.push_back(1); + lines_edited_args.push_front(args2); + + ((Array)lines_edited_args[1])[1] = 1; SEND_GUI_ACTION(text_edit, "ui_text_newline_above"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\nthis is some test text."); + CHECK(text_edit->get_text() == "\nthis is some test text.\n\nthis is some test text."); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); text_edit->set_caret_line(1); text_edit->set_caret_column(4); - text_edit->select(0, 0, 0, 4); + + text_edit->set_caret_line(3, false, true, 0, 1); + text_edit->set_caret_column(4, false, 1); MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("caret_changed"); text_edit->set_editable(false); SEND_GUI_ACTION(text_edit, "ui_text_newline_above"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\nthis is some test text."); + CHECK(text_edit->get_text() == "\nthis is some test text.\n\nthis is some test text."); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 4); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_editable(true); + ((Array)lines_edited_args[0])[0] = 2; + ((Array)lines_edited_args[0])[1] = 3; + SEND_GUI_ACTION(text_edit, "ui_text_newline_above"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\n\nthis is some test text."); + CHECK(text_edit->get_text() == "\n\nthis is some test text.\n\n\nthis is some test text."); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 4); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); } SUBCASE("[TextEdit] ui_text_newline_blank") { - text_edit->set_text("this is some test text."); + text_edit->set_text("this is some test text.\nthis is some test text."); text_edit->select(0, 0, 0, 4); text_edit->set_caret_column(4); + + text_edit->add_caret(1, 4); + text_edit->select(1, 0, 1, 4, 1); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -1320,13 +1361,23 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - ((Array)lines_edited_args[0])[1] = 1; + // For the second caret. + Array args2; + args2.push_back(1); + args2.push_back(2); + lines_edited_args.push_front(args2); + + ((Array)lines_edited_args[1])[1] = 1; SEND_GUI_ACTION(text_edit, "ui_text_newline_blank"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "this is some test text.\n"); + CHECK(text_edit->get_text() == "this is some test text.\n\nthis is some test text.\n"); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); @@ -1334,10 +1385,14 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_editable(false); SEND_GUI_ACTION(text_edit, "ui_text_newline_blank"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "this is some test text.\n"); + CHECK(text_edit->get_text() == "this is some test text.\n\nthis is some test text.\n"); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); @@ -1345,9 +1400,14 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { } SUBCASE("[TextEdit] ui_text_newline") { - text_edit->set_text("this is some test text."); + text_edit->set_text("this is some test text.\nthis is some test text."); text_edit->select(0, 0, 0, 4); text_edit->set_caret_column(4); + + text_edit->add_caret(1, 4); + text_edit->select(1, 0, 1, 4, 1); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -1355,14 +1415,27 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - lines_edited_args.push_back(lines_edited_args[0].duplicate()); - ((Array)lines_edited_args[1])[1] = 1; + // For the second caret. + Array args2; + args2.push_back(1); + args2.push_back(1); + lines_edited_args.push_front(args2); + lines_edited_args.push_front(args2.duplicate()); + ((Array)lines_edited_args[1])[1] = 2; + + lines_edited_args.push_back(lines_edited_args[2].duplicate()); + ((Array)lines_edited_args[3])[1] = 1; + SEND_GUI_ACTION(text_edit, "ui_text_newline"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\n is some test text."); + CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text."); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); @@ -1370,10 +1443,14 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_editable(false); SEND_GUI_ACTION(text_edit, "ui_text_newline"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\n is some test text."); + CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text."); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); @@ -1381,10 +1458,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { } SUBCASE("[TextEdit] ui_text_backspace_all_to_left") { - text_edit->set_text("\nthis is some test text."); + text_edit->set_text("\nthis is some test text.\n\nthis is some test text."); text_edit->select(1, 0, 1, 4); text_edit->set_caret_line(1); text_edit->set_caret_column(4); + + text_edit->add_caret(3, 4); + text_edit->select(3, 0, 3, 4, 1); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); Ref<InputEvent> tmpevent = InputEventKey::create_reference(Key::BACKSPACE | KeyModifierMask::ALT | KeyModifierMask::CMD_OR_CTRL); @@ -1395,34 +1477,50 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // For the second caret. + Array args2; + args2.push_back(3); + args2.push_back(3); + lines_edited_args.push_front(args2); + // With selection should be a normal backspace. - ((Array)lines_edited_args[0])[0] = 1; - ((Array)lines_edited_args[0])[1] = 1; + ((Array)lines_edited_args[1])[0] = 1; + ((Array)lines_edited_args[1])[1] = 1; SEND_GUI_ACTION(text_edit, "ui_text_backspace_all_to_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\n is some test text."); + CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text."); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - ((Array)lines_edited_args[0])[1] = 0; + ((Array)lines_edited_args[0])[1] = 2; + ((Array)lines_edited_args[1])[1] = 0; // Start of line should also be a normal backspace. SEND_GUI_ACTION(text_edit, "ui_text_backspace_all_to_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text."); + CHECK(text_edit->get_text() == " is some test text.\n is some test text."); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); text_edit->set_caret_column(text_edit->get_line(0).length()); + text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -1433,23 +1531,33 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_editable(false); SEND_GUI_ACTION(text_edit, "ui_text_backspace_all_to_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text."); + CHECK(text_edit->get_text() == " is some test text.\n is some test text."); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length()); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length()); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_editable(true); - ((Array)lines_edited_args[0])[0] = 0; + ((Array)lines_edited_args[0])[0] = 1; + ((Array)lines_edited_args[0])[1] = 1; + ((Array)lines_edited_args[1])[0] = 0; SEND_GUI_ACTION(text_edit, "ui_text_backspace_all_to_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_text() == "\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); @@ -1458,10 +1566,14 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { } SUBCASE("[TextEdit] ui_text_backspace_word") { - text_edit->set_text("\nthis is some test text."); + text_edit->set_text("\nthis is some test text.\n\nthis is some test text."); text_edit->select(1, 0, 1, 4); text_edit->set_caret_line(1); text_edit->set_caret_column(4); + + text_edit->add_caret(3, 4); + text_edit->select(3, 0, 3, 4, 1); + CHECK(text_edit->get_caret_count() == 2); MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -1469,30 +1581,45 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // For the second caret. + Array args2; + args2.push_back(3); + args2.push_back(3); + lines_edited_args.push_front(args2); + // With selection should be a normal backspace. - ((Array)lines_edited_args[0])[0] = 1; - ((Array)lines_edited_args[0])[1] = 1; + ((Array)lines_edited_args[1])[0] = 1; + ((Array)lines_edited_args[1])[1] = 1; SEND_GUI_ACTION(text_edit, "ui_text_backspace_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\n is some test text."); + CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text."); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); text_edit->end_complex_operation(); - ((Array)lines_edited_args[0])[1] = 0; + ((Array)lines_edited_args[0])[1] = 2; + ((Array)lines_edited_args[1])[1] = 0; // Start of line should also be a normal backspace. SEND_GUI_ACTION(text_edit, "ui_text_backspace_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text."); + CHECK(text_edit->get_text() == " is some test text.\n is some test text."); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); @@ -1500,16 +1627,21 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_editable(false); SEND_GUI_ACTION(text_edit, "ui_text_backspace_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text."); + CHECK(text_edit->get_text() == " is some test text.\n is some test text."); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_editable(true); text_edit->set_caret_column(text_edit->get_line(0).length()); + text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -1517,24 +1649,35 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - ((Array)lines_edited_args[0])[0] = 0; + ((Array)lines_edited_args[0])[0] = 1; + ((Array)lines_edited_args[0])[1] = 1; + ((Array)lines_edited_args[1])[0] = 0; SEND_GUI_ACTION(text_edit, "ui_text_backspace_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test "); + CHECK(text_edit->get_text() == " is some test \n is some test "); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 14); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 14); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); } SUBCASE("[TextEdit] ui_text_backspace") { - text_edit->set_text("\nthis is some test text."); + text_edit->set_text("\nthis is some test text.\n\nthis is some test text."); text_edit->select(1, 0, 1, 4); text_edit->set_caret_line(1); text_edit->set_caret_column(4); + + text_edit->add_caret(3, 4); + text_edit->select(3, 0, 3, 4, 1); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -1542,34 +1685,50 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // For the second caret. + Array args2; + args2.push_back(3); + args2.push_back(3); + lines_edited_args.push_front(args2); + // With selection should be a normal backspace. - ((Array)lines_edited_args[0])[0] = 1; - ((Array)lines_edited_args[0])[1] = 1; + ((Array)lines_edited_args[1])[0] = 1; + ((Array)lines_edited_args[1])[1] = 1; SEND_GUI_ACTION(text_edit, "ui_text_backspace"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\n is some test text."); + CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text."); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - ((Array)lines_edited_args[0])[1] = 0; + ((Array)lines_edited_args[0])[1] = 2; + ((Array)lines_edited_args[1])[1] = 0; // Start of line should also be a normal backspace. SEND_GUI_ACTION(text_edit, "ui_text_backspace"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text."); + CHECK(text_edit->get_text() == " is some test text.\n is some test text."); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); text_edit->set_caret_column(text_edit->get_line(0).length()); + text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -1580,23 +1739,33 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_editable(false); SEND_GUI_ACTION(text_edit, "ui_text_backspace"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text."); + CHECK(text_edit->get_text() == " is some test text.\n is some test text."); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length()); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length()); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_editable(true); - ((Array)lines_edited_args[0])[0] = 0; + ((Array)lines_edited_args[0])[0] = 1; + ((Array)lines_edited_args[0])[1] = 1; + ((Array)lines_edited_args[1])[0] = 0; SEND_GUI_ACTION(text_edit, "ui_text_backspace"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text"); + CHECK(text_edit->get_text() == " is some test text\n is some test text"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 18); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 18); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); @@ -1605,6 +1774,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->select(0, 18, 0, 0); text_edit->set_caret_line(0); text_edit->set_caret_column(0); + + text_edit->select(1, 18, 1, 0, 1); + text_edit->set_caret_line(1, false, true, 0, 1); + text_edit->set_caret_column(0, false, 1); MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -1612,12 +1785,12 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - ((Array)lines_edited_args[0])[0] = 0; - SEND_GUI_ACTION(text_edit, "ui_text_backspace"); - CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_text() == "\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); @@ -1627,10 +1800,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { Ref<InputEvent> tmpevent = InputEventKey::create_reference(Key::BACKSPACE | KeyModifierMask::ALT | KeyModifierMask::CMD_OR_CTRL); InputMap::get_singleton()->action_add_event("ui_text_delete_all_to_right", tmpevent); - text_edit->set_text("this is some test text.\n"); + text_edit->set_text("this is some test text.\nthis is some test text.\n"); text_edit->select(0, 0, 0, 4); text_edit->set_caret_line(0); text_edit->set_caret_column(4); + + text_edit->add_caret(1, 4); + text_edit->select(1, 0, 1, 4, 1); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -1638,19 +1816,30 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // For the second caret. + Array args2; + args2.push_back(1); + args2.push_back(1); + lines_edited_args.push_front(args2); + // With selection should be a normal delete. SEND_GUI_ACTION(text_edit, "ui_text_delete_all_to_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text.\n"); + CHECK(text_edit->get_text() == " is some test text.\n is some test text.\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); // End of line should not do anything. text_edit->set_caret_column(text_edit->get_line(0).length()); + text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -1660,15 +1849,20 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SEND_GUI_ACTION(text_edit, "ui_text_delete_all_to_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text.\n"); + CHECK(text_edit->get_text() == " is some test text.\n is some test text.\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length()); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length()); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_caret_column(0); + text_edit->set_caret_column(0, false, 1); MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -1679,10 +1873,14 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_editable(false); SEND_GUI_ACTION(text_edit, "ui_text_delete_all_to_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " is some test text.\n"); + CHECK(text_edit->get_text() == " is some test text.\n is some test text.\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); @@ -1690,10 +1888,14 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SEND_GUI_ACTION(text_edit, "ui_text_delete_all_to_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\n"); + CHECK(text_edit->get_text() == "\n\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); @@ -1705,10 +1907,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_caret_mid_grapheme_enabled(true); CHECK(text_edit->is_caret_mid_grapheme_enabled()); - text_edit->set_text("this ffi some test text.\n"); + text_edit->set_text("this ffi some test text.\n\nthis ffi some test text.\n"); text_edit->select(0, 0, 0, 4); text_edit->set_caret_line(0); text_edit->set_caret_column(4); + + text_edit->add_caret(2, 4); + text_edit->select(2, 0, 2, 4, 1); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -1716,20 +1923,32 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // For the second caret. + Array args2; + args2.push_back(2); + args2.push_back(2); + lines_edited_args.push_front(args2); + // With selection should be a normal delete. SEND_GUI_ACTION(text_edit, "ui_text_delete_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " ffi some test text.\n"); + CHECK(text_edit->get_text() == " ffi some test text.\n\n ffi some test text.\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); // With selection should be a normal delete. - ((Array)lines_edited_args[0])[0] = 1; + ((Array)lines_edited_args[0])[0] = 3; + ((Array)lines_edited_args[1])[0] = 1; text_edit->set_caret_column(text_edit->get_line(0).length()); + text_edit->set_caret_column(text_edit->get_line(2).length(), false, 1); MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -1739,16 +1958,23 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SEND_GUI_ACTION(text_edit, "ui_text_delete_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " ffi some test text."); + CHECK(text_edit->get_text() == " ffi some test text.\n ffi some test text."); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length()); CHECK_FALSE(text_edit->has_selection()); - SIGNAL_CHECK_FALSE("caret_changed"); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length()); + CHECK_FALSE(text_edit->has_selection(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - ((Array)lines_edited_args[0])[0] = 0; + ((Array)lines_edited_args[1])[0] = 0; + ((Array)lines_edited_args[0])[0] = 1; + ((Array)lines_edited_args[0])[1] = 1; text_edit->set_caret_column(0); + text_edit->set_caret_column(0, false, 1); MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -1759,10 +1985,14 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_editable(false); SEND_GUI_ACTION(text_edit, "ui_text_delete_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " ffi some test text."); + CHECK(text_edit->get_text() == " ffi some test text.\n ffi some test text."); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); @@ -1770,10 +2000,14 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SEND_GUI_ACTION(text_edit, "ui_text_delete_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " some test text."); + CHECK(text_edit->get_text() == " some test text.\n some test text."); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); @@ -1783,10 +2017,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_caret_mid_grapheme_enabled(true); CHECK(text_edit->is_caret_mid_grapheme_enabled()); - text_edit->set_text("this ffi some test text.\n"); + text_edit->set_text("this ffi some test text.\nthis ffi some test text."); text_edit->select(0, 0, 0, 4); text_edit->set_caret_line(0); text_edit->set_caret_column(4); + + text_edit->add_caret(1, 4); + text_edit->select(1, 0, 1, 4, 1); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -1794,19 +2033,31 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // For the second caret. + Array args2; + args2.push_back(1); + args2.push_back(1); + lines_edited_args.push_front(args2); + // With selection should be a normal delete. SEND_GUI_ACTION(text_edit, "ui_text_delete"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " ffi some test text.\n"); + CHECK(text_edit->get_text() == " ffi some test text.\n ffi some test text."); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); // With selection should be a normal delete. + lines_edited_args.remove_at(0); ((Array)lines_edited_args[0])[0] = 1; + text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); text_edit->set_caret_column(text_edit->get_line(0).length()); MessageQueue::get_singleton()->flush(); @@ -1817,16 +2068,25 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SEND_GUI_ACTION(text_edit, "ui_text_delete"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " ffi some test text."); + CHECK(text_edit->get_text() == " ffi some test text. ffi some test text."); CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length()); - CHECK_FALSE(text_edit->has_selection()); - SIGNAL_CHECK_FALSE("caret_changed"); + CHECK(text_edit->get_caret_column() == 20); + CHECK_FALSE(text_edit->has_selection(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - ((Array)lines_edited_args[0])[0] = 0; + // Caret should be removed due to column preservation. + CHECK(text_edit->get_caret_count() == 1); + + // Lets add it back. text_edit->set_caret_column(0); + text_edit->add_caret(0, 20); + + ((Array)lines_edited_args[0])[0] = 0; + lines_edited_args.push_back(args2); + ((Array)lines_edited_args[1])[0] = 0; + ((Array)lines_edited_args[1])[1] = 0; MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -1837,43 +2097,59 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_editable(false); SEND_GUI_ACTION(text_edit, "ui_text_delete"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " ffi some test text."); + CHECK(text_edit->get_text() == " ffi some test text. ffi some test text."); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 20); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_editable(true); + text_edit->start_action(TextEdit::EditAction::ACTION_NONE); + SEND_GUI_ACTION(text_edit, "ui_text_delete"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "ffi some test text."); + CHECK(text_edit->get_text() == "ffi some test text.ffi some test text."); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); - SIGNAL_CHECK_FALSE("caret_changed"); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 19); + CHECK_FALSE(text_edit->has_selection(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + text_edit->start_action(TextEdit::EditAction::ACTION_NONE); + SEND_GUI_ACTION(text_edit, "ui_text_delete"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "fi some test text."); + CHECK(text_edit->get_text() == "fi some test text.fi some test text."); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); - SIGNAL_CHECK_FALSE("caret_changed"); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 18); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); text_edit->set_caret_mid_grapheme_enabled(false); CHECK_FALSE(text_edit->is_caret_mid_grapheme_enabled()); + text_edit->start_action(TextEdit::EditAction::ACTION_NONE); + text_edit->undo(); - text_edit->set_caret_line(0); - text_edit->set_caret_column(0); MessageQueue::get_singleton()->flush(); - CHECK(text_edit->get_text() == "ffi some test text."); + CHECK(text_edit->get_text() == "ffi some test text.ffi some test text."); SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); @@ -1882,19 +2158,26 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SEND_GUI_ACTION(text_edit, "ui_text_delete"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " some test text."); + CHECK(text_edit->get_text() == " some test text. some test text."); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); - SIGNAL_CHECK_FALSE("caret_changed"); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 16); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); } SUBCASE("[TextEdit] ui_text_caret_word_left") { - text_edit->set_text("\nthis is some test text."); + text_edit->set_text("\nthis is some test text.\nthis is some test text."); text_edit->set_caret_line(1); text_edit->set_caret_column(7); + + text_edit->add_caret(2, 7); + CHECK(text_edit->get_caret_count() == 2); MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -1902,47 +2185,67 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // Shift should select. #ifdef MACOS_ENABLED SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::ALT | KeyModifierMask::SHIFT); #else SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT); #endif CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\nthis is some test text."); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 5); - CHECK(text_edit->get_selected_text() == "is"); - CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text(0) == "is"); + CHECK(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 5); + CHECK(text_edit->get_selected_text(1) == "is"); + CHECK(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // Should still move caret with selection. SEND_GUI_ACTION(text_edit, "ui_text_caret_word_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\nthis is some test text."); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // Normal word left. SEND_GUI_ACTION(text_edit, "ui_text_caret_word_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\nthis is some test text."); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 23); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); } SUBCASE("[TextEdit] ui_text_caret_left") { - text_edit->set_text("\nthis is some test text."); + text_edit->set_text("\nthis is some test text.\nthis is some test text."); text_edit->set_caret_line(1); text_edit->set_caret_column(7); text_edit->select(1, 2, 1, 7); + + text_edit->add_caret(2, 7); + text_edit->select(2, 2, 2, 7, 1); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -1950,62 +2253,91 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // Normal left shoud deselect and place at selection start. SEND_GUI_ACTION(text_edit, "ui_text_caret_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\nthis is some test text."); + CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 2); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 2); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // With shift should select. SEND_GUI_KEY_EVENT(text_edit, Key::LEFT | KeyModifierMask::SHIFT); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\nthis is some test text."); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 1); - CHECK(text_edit->get_selected_text() == "h"); - CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text(0) == "h"); + CHECK(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 1); + CHECK(text_edit->get_selected_text(1) == "h"); + CHECK(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // All ready at select left, should only deselect. SEND_GUI_ACTION(text_edit, "ui_text_caret_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\nthis is some test text."); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 1); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 1); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // Normal left. SEND_GUI_ACTION(text_edit, "ui_text_caret_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\nthis is some test text."); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 0); CHECK_FALSE(text_edit->has_selection()); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // Left at col 0 should go up a line. SEND_GUI_ACTION(text_edit, "ui_text_caret_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\nthis is some test text."); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 23); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); } SUBCASE("[TextEdit] ui_text_caret_word_right") { - text_edit->set_text("this is some test text\n"); + text_edit->set_text("this is some test text\n\nthis is some test text\n"); text_edit->set_caret_line(0); text_edit->set_caret_column(13); + + text_edit->add_caret(2, 13); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -2013,47 +2345,67 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // Shift should select. #ifdef MACOS_ENABLED SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::ALT | KeyModifierMask::SHIFT); #else SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT); #endif CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "this is some test text\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 17); - CHECK(text_edit->get_selected_text() == "test"); - CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text(0) == "test"); + CHECK(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 17); + CHECK(text_edit->get_selected_text(1) == "test"); + CHECK(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // Should still move caret with selection. SEND_GUI_ACTION(text_edit, "ui_text_caret_word_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "this is some test text\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 22); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 22); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // Normal word right. SEND_GUI_ACTION(text_edit, "ui_text_caret_word_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "this is some test text\n"); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); } SUBCASE("[TextEdit] ui_text_caret_right") { - text_edit->set_text("this is some test text\n"); + text_edit->set_text("this is some test text\n\nthis is some test text\n"); text_edit->set_caret_line(0); text_edit->set_caret_column(16); text_edit->select(0, 16, 0, 20); + + text_edit->add_caret(2, 16); + text_edit->select(2, 16, 2, 20, 1); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); @@ -2061,53 +2413,76 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // Normal right shoud deselect and place at selection start. SEND_GUI_ACTION(text_edit, "ui_text_caret_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "this is some test text\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 20); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 20); + CHECK_FALSE(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // With shift should select. SEND_GUI_KEY_EVENT(text_edit, Key::RIGHT | KeyModifierMask::SHIFT); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "this is some test text\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 21); - CHECK(text_edit->get_selected_text() == "x"); - CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text(0) == "x"); + CHECK(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 21); + CHECK(text_edit->get_selected_text(1) == "x"); + CHECK(text_edit->has_selection(1)); + SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // All ready at select right, should only deselect. SEND_GUI_ACTION(text_edit, "ui_text_caret_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "this is some test text\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 21); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 21); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // Normal right. SEND_GUI_ACTION(text_edit, "ui_text_caret_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "this is some test text\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 22); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 22); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // Right at end col should go down a line. SEND_GUI_ACTION(text_edit, "ui_text_caret_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "this is some test text\n"); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); @@ -2117,9 +2492,13 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY); text_edit->set_size(Size2(110, 100)); - text_edit->set_text("this is some\nother test\nlines\ngo here"); - text_edit->set_caret_line(4); + text_edit->set_text("this is some\nother test\nlines\ngo here\nthis is some\nother test\nlines\ngo here"); + text_edit->set_caret_line(3); text_edit->set_caret_column(7); + + text_edit->add_caret(7, 7); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); CHECK(text_edit->is_line_wrapped(0)); @@ -2128,44 +2507,61 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // Select + up should select everything to the left on that line. SEND_GUI_KEY_EVENT(text_edit, Key::UP | KeyModifierMask::SHIFT); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "this is some\nother test\nlines\ngo here"); CHECK(text_edit->get_caret_line() == 2); CHECK(text_edit->get_caret_column() == 5); - CHECK(text_edit->get_selected_text() == "\ngo here"); - CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text(0) == "\ngo here"); + CHECK(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 6); + CHECK(text_edit->get_caret_column(1) == 5); + CHECK(text_edit->get_selected_text(1) == "\ngo here"); + CHECK(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // Should deselect and move up. SEND_GUI_ACTION(text_edit, "ui_text_caret_up"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "this is some\nother test\nlines\ngo here"); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 8); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 5); + CHECK(text_edit->get_caret_column(1) == 8); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // Normal up over wrapped line. SEND_GUI_ACTION(text_edit, "ui_text_caret_up"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "this is some\nother test\nlines\ngo here"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 12); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 4); + CHECK(text_edit->get_caret_column(1) == 12); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_caret_column(12, false); + // Normal up over wrapped line to line 0. SEND_GUI_ACTION(text_edit, "ui_text_caret_up"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "this is some\nother test\nlines\ngo here"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 7); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 4); + CHECK(text_edit->get_caret_column(1) == 7); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); @@ -2175,9 +2571,13 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY); text_edit->set_size(Size2(110, 100)); - text_edit->set_text("go here\nlines\nother test\nthis is some"); + text_edit->set_text("go here\nlines\nother test\nthis is some\ngo here\nlines\nother test\nthis is some"); text_edit->set_caret_line(0); text_edit->set_caret_column(7); + + text_edit->add_caret(4, 7); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); CHECK(text_edit->is_line_wrapped(3)); @@ -2186,44 +2586,61 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // Select + down should select everything to the right on that line. SEND_GUI_KEY_EVENT(text_edit, Key::DOWN | KeyModifierMask::SHIFT); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "go here\nlines\nother test\nthis is some"); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 5); - CHECK(text_edit->get_selected_text() == "\nlines"); - CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text(0) == "\nlines"); + CHECK(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 5); + CHECK(text_edit->get_caret_column(1) == 5); + CHECK(text_edit->get_selected_text(1) == "\nlines"); + CHECK(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // Should deselect and move down. SEND_GUI_ACTION(text_edit, "ui_text_caret_down"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "go here\nlines\nother test\nthis is some"); CHECK(text_edit->get_caret_line() == 2); CHECK(text_edit->get_caret_column() == 8); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 6); + CHECK(text_edit->get_caret_column(1) == 8); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // Normal down over wrapped line. SEND_GUI_ACTION(text_edit, "ui_text_caret_down"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "go here\nlines\nother test\nthis is some"); CHECK(text_edit->get_caret_line() == 3); CHECK(text_edit->get_caret_column() == 7); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 7); + CHECK(text_edit->get_caret_column(1) == 7); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_caret_column(7, false); + // Normal down over wrapped line to last wrapped line. SEND_GUI_ACTION(text_edit, "ui_text_caret_down"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "go here\nlines\nother test\nthis is some"); CHECK(text_edit->get_caret_line() == 3); CHECK(text_edit->get_caret_column() == 12); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 7); + CHECK(text_edit->get_caret_column(1) == 12); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); @@ -2236,6 +2653,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_text("this is some\nother test\nlines\ngo here"); text_edit->set_caret_line(4); text_edit->set_caret_column(7); + + text_edit->add_caret(3, 2); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); CHECK(text_edit->is_line_wrapped(0)); @@ -2258,6 +2679,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + CHECK(text_edit->get_caret_count() == 1); SEND_GUI_ACTION(text_edit, "ui_text_caret_document_start"); CHECK(text_edit->get_viewport()->is_input_handled()); @@ -2277,6 +2699,9 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_text("go here\nlines\nother test\nthis is some"); text_edit->set_caret_line(0); text_edit->set_caret_column(0); + + text_edit->add_caret(1, 0); + CHECK(text_edit->get_caret_count() == 2); MessageQueue::get_singleton()->flush(); CHECK(text_edit->is_line_wrapped(3)); @@ -2299,6 +2724,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + CHECK(text_edit->get_caret_count() == 1); SEND_GUI_ACTION(text_edit, "ui_text_caret_document_end"); CHECK(text_edit->get_viewport()->is_input_handled()); @@ -2315,9 +2741,12 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY); text_edit->set_size(Size2(110, 100)); - text_edit->set_text(" this is some"); + text_edit->set_text(" this is some\n this is some"); text_edit->set_caret_line(0); text_edit->set_caret_column(text_edit->get_line(0).length()); + + text_edit->add_caret(1, text_edit->get_line(1).length()); + CHECK(text_edit->get_caret_count() == 2); MessageQueue::get_singleton()->flush(); CHECK(text_edit->is_line_wrapped(0)); @@ -2334,8 +2763,13 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_viewport()->is_input_handled()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 10); - CHECK(text_edit->has_selection()); - CHECK(text_edit->get_selected_text() == "some"); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "some"); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 10); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "some"); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); @@ -2344,7 +2778,11 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_viewport()->is_input_handled()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 2); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 2); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); @@ -2353,7 +2791,11 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_viewport()->is_input_handled()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); @@ -2362,7 +2804,11 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_viewport()->is_input_handled()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 2); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 2); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); @@ -2372,9 +2818,12 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY); text_edit->set_size(Size2(110, 100)); - text_edit->set_text(" this is some"); + text_edit->set_text(" this is some\n this is some"); text_edit->set_caret_line(0); text_edit->set_caret_column(0); + + text_edit->add_caret(1, 0); + CHECK(text_edit->get_caret_count() == 2); MessageQueue::get_singleton()->flush(); CHECK(text_edit->is_line_wrapped(0)); @@ -2391,8 +2840,13 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_viewport()->is_input_handled()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 9); - CHECK(text_edit->has_selection()); - CHECK(text_edit->get_selected_text() == " this is"); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == " this is"); + + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 9); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == " this is"); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); @@ -2401,13 +2855,24 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_viewport()->is_input_handled()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length()); - CHECK_FALSE(text_edit->has_selection()); + CHECK_FALSE(text_edit->has_selection(0)); + + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == text_edit->get_line(1).length()); + CHECK_FALSE(text_edit->has_selection(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); } SUBCASE("[TextEdit] unicode") { + text_edit->set_text("\n"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + + text_edit->add_caret(1, 0); + CHECK(text_edit->get_caret_count() == 2); text_edit->insert_text_at_caret("a"); MessageQueue::get_singleton()->flush(); @@ -2416,10 +2881,17 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // For the second caret. + Array args2; + args2.push_back(1); + args2.push_back(1); + lines_edited_args.push_front(args2); + SEND_GUI_KEY_EVENT(text_edit, Key::A); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "aA"); + CHECK(text_edit->get_text() == "aA\naA"); CHECK(text_edit->get_caret_column() == 2); + CHECK(text_edit->get_caret_column(1) == 2); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); @@ -2427,20 +2899,24 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_editable(false); SEND_GUI_KEY_EVENT(text_edit, Key::A); CHECK_FALSE(text_edit->get_viewport()->is_input_handled()); // Should this be handled? - CHECK(text_edit->get_text() == "aA"); + CHECK(text_edit->get_text() == "aA\naA"); CHECK(text_edit->get_caret_column() == 2); + CHECK(text_edit->get_caret_column(1) == 2); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_editable(true); - lines_edited_args.push_back(lines_edited_args[0].duplicate()); + lines_edited_args.push_back(lines_edited_args[1].duplicate()); + lines_edited_args.push_front(args2.duplicate()); text_edit->select(0, 0, 0, 1); + text_edit->select(1, 0, 1, 1, 1); SEND_GUI_KEY_EVENT(text_edit, Key::B); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "BA"); + CHECK(text_edit->get_text() == "BA\nBA"); CHECK(text_edit->get_caret_column() == 1); + CHECK(text_edit->get_caret_column(1) == 1); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); @@ -2450,17 +2926,19 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SEND_GUI_KEY_EVENT(text_edit, Key::B); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "BB"); + CHECK(text_edit->get_text() == "BB\nBB"); CHECK(text_edit->get_caret_column() == 2); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); text_edit->select(0, 0, 0, 1); + text_edit->select(1, 0, 1, 1, 1); SEND_GUI_KEY_EVENT(text_edit, Key::A); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "AB"); + CHECK(text_edit->get_text() == "AB\nAB"); CHECK(text_edit->get_caret_column() == 1); + CHECK(text_edit->get_caret_column(1) == 1); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); @@ -2558,7 +3036,7 @@ TEST_CASE("[SceneTree][TextEdit] versioning") { CHECK(text_edit->has_redo()); text_edit->redo(); - CHECK(text_edit->get_line(0) == "test nested ops"); + CHECK(text_edit->get_line(0) == "test ops nested"); CHECK(text_edit->get_version() == 3); CHECK(text_edit->get_saved_version() == 3); CHECK(text_edit->has_undo()); @@ -2779,6 +3257,94 @@ TEST_CASE("[SceneTree][TextEdit] caret") { memdelete(text_edit); } +TEST_CASE("[SceneTree][TextEdit] muiticaret") { + TextEdit *text_edit = memnew(TextEdit); + SceneTree::get_singleton()->get_root()->add_child(text_edit); + text_edit->set_multiple_carets_enabled(true); + + Array empty_signal_args; + empty_signal_args.push_back(Array()); + + SIGNAL_WATCH(text_edit, "caret_changed"); + + text_edit->set_text("this is\nsome test\ntext"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + SUBCASE("[TextEdit] add remove caret") { + // Overlapping + CHECK(text_edit->add_caret(0, 0) == -1); + MessageQueue::get_singleton()->flush(); + SIGNAL_CHECK_FALSE("caret_changed"); + + // Selection + text_edit->select(0, 0, 2, 4); + CHECK(text_edit->add_caret(0, 0) == -1); + CHECK(text_edit->add_caret(2, 4) == -1); + CHECK(text_edit->add_caret(1, 2) == -1); + + // Out of bounds + CHECK(text_edit->add_caret(-1, 0) == -1); + CHECK(text_edit->add_caret(5, 0) == -1); + CHECK(text_edit->add_caret(0, 100) == -1); + + MessageQueue::get_singleton()->flush(); + SIGNAL_CHECK_FALSE("caret_changed"); + + CHECK(text_edit->get_caret_count() == 1); + + text_edit->deselect(); + SIGNAL_CHECK_FALSE("caret_changed"); + + CHECK(text_edit->add_caret(0, 1) == 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_CHECK("caret_changed", empty_signal_args); + CHECK(text_edit->get_caret_count() == 2); + + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 1); + + ERR_PRINT_OFF; + text_edit->remove_caret(-1); + text_edit->remove_caret(5); + ERR_PRINT_ON; + CHECK(text_edit->get_caret_count() == 2); + SIGNAL_CHECK_FALSE("caret_changed"); + + text_edit->remove_caret(0); + SIGNAL_CHECK_FALSE("caret_changed"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 1); + } + + SUBCASE("[TextEdit] caret index edit order") { + Vector<int> caret_index_get_order; + caret_index_get_order.push_back(1); + caret_index_get_order.push_back(0); + + CHECK(text_edit->add_caret(1, 0)); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_caret_index_edit_order() == caret_index_get_order); + + text_edit->remove_secondary_carets(); + text_edit->set_caret_line(1); + CHECK(text_edit->add_caret(0, 0)); + CHECK(text_edit->get_caret_count() == 2); + + caret_index_get_order.write[0] = 0; + caret_index_get_order.write[1] = 1; + CHECK(text_edit->get_caret_index_edit_order() == caret_index_get_order); + } + + memdelete(text_edit); +} + TEST_CASE("[SceneTree][TextEdit] line wrapping") { TextEdit *text_edit = memnew(TextEdit); SceneTree::get_singleton()->get_root()->add_child(text_edit); |