diff options
106 files changed, 9669 insertions, 3960 deletions
diff --git a/core/templates/command_queue_mt.cpp b/core/templates/command_queue_mt.cpp index 0c5c6394a1..d9e5e0b217 100644 --- a/core/templates/command_queue_mt.cpp +++ b/core/templates/command_queue_mt.cpp @@ -41,35 +41,6 @@ void CommandQueueMT::unlock() { mutex.unlock(); } -void CommandQueueMT::wait_for_flush() { - // wait one millisecond for a flush to happen - OS::get_singleton()->delay_usec(1000); -} - -CommandQueueMT::SyncSemaphore *CommandQueueMT::_alloc_sync_sem() { - int idx = -1; - - while (true) { - lock(); - for (int i = 0; i < SYNC_SEMAPHORES; i++) { - if (!sync_sems[i].in_use) { - sync_sems[i].in_use = true; - idx = i; - break; - } - } - unlock(); - - if (idx == -1) { - wait_for_flush(); - } else { - break; - } - } - - return &sync_sems[idx]; -} - CommandQueueMT::CommandQueueMT() { } diff --git a/core/templates/command_queue_mt.h b/core/templates/command_queue_mt.h index a4ac338bed..c149861467 100644 --- a/core/templates/command_queue_mt.h +++ b/core/templates/command_queue_mt.h @@ -32,9 +32,9 @@ #define COMMAND_QUEUE_MT_H #include "core/object/worker_thread_pool.h" +#include "core/os/condition_variable.h" #include "core/os/memory.h" #include "core/os/mutex.h" -#include "core/os/semaphore.h" #include "core/string/print_string.h" #include "core/templates/local_vector.h" #include "core/templates/simple_type.h" @@ -251,14 +251,14 @@ #define DECL_PUSH(N) \ template <typename T, typename M COMMA(N) COMMA_SEP_LIST(TYPE_PARAM, N)> \ void push(T *p_instance, M p_method COMMA(N) COMMA_SEP_LIST(PARAM, N)) { \ - CMD_TYPE(N) *cmd = allocate_and_lock<CMD_TYPE(N)>(); \ + MutexLock mlock(mutex); \ + CMD_TYPE(N) *cmd = allocate<CMD_TYPE(N)>(); \ cmd->instance = p_instance; \ cmd->method = p_method; \ SEMIC_SEP_LIST(CMD_ASSIGN_PARAM, N); \ if (pump_task_id != WorkerThreadPool::INVALID_TASK_ID) { \ WorkerThreadPool::get_singleton()->notify_yield_over(pump_task_id); \ } \ - unlock(); \ } #define CMD_RET_TYPE(N) CommandRet##N<T, M, COMMA_SEP_LIST(TYPE_ARG, N) COMMA(N) R> @@ -266,19 +266,17 @@ #define DECL_PUSH_AND_RET(N) \ template <typename T, typename M, COMMA_SEP_LIST(TYPE_PARAM, N) COMMA(N) typename R> \ void push_and_ret(T *p_instance, M p_method, COMMA_SEP_LIST(PARAM, N) COMMA(N) R *r_ret) { \ - SyncSemaphore *ss = _alloc_sync_sem(); \ - CMD_RET_TYPE(N) *cmd = allocate_and_lock<CMD_RET_TYPE(N)>(); \ + MutexLock mlock(mutex); \ + CMD_RET_TYPE(N) *cmd = allocate<CMD_RET_TYPE(N)>(); \ cmd->instance = p_instance; \ cmd->method = p_method; \ SEMIC_SEP_LIST(CMD_ASSIGN_PARAM, N); \ cmd->ret = r_ret; \ - cmd->sync_sem = ss; \ if (pump_task_id != WorkerThreadPool::INVALID_TASK_ID) { \ WorkerThreadPool::get_singleton()->notify_yield_over(pump_task_id); \ } \ - unlock(); \ - ss->sem.wait(); \ - ss->in_use = false; \ + sync_tail++; \ + _wait_for_sync(mlock); \ } #define CMD_SYNC_TYPE(N) CommandSync##N<T, M COMMA(N) COMMA_SEP_LIST(TYPE_ARG, N)> @@ -286,39 +284,31 @@ #define DECL_PUSH_AND_SYNC(N) \ template <typename T, typename M COMMA(N) COMMA_SEP_LIST(TYPE_PARAM, N)> \ void push_and_sync(T *p_instance, M p_method COMMA(N) COMMA_SEP_LIST(PARAM, N)) { \ - SyncSemaphore *ss = _alloc_sync_sem(); \ - CMD_SYNC_TYPE(N) *cmd = allocate_and_lock<CMD_SYNC_TYPE(N)>(); \ + MutexLock mlock(mutex); \ + CMD_SYNC_TYPE(N) *cmd = allocate<CMD_SYNC_TYPE(N)>(); \ cmd->instance = p_instance; \ cmd->method = p_method; \ SEMIC_SEP_LIST(CMD_ASSIGN_PARAM, N); \ - cmd->sync_sem = ss; \ if (pump_task_id != WorkerThreadPool::INVALID_TASK_ID) { \ WorkerThreadPool::get_singleton()->notify_yield_over(pump_task_id); \ } \ - unlock(); \ - ss->sem.wait(); \ - ss->in_use = false; \ + sync_tail++; \ + _wait_for_sync(mlock); \ } #define MAX_CMD_PARAMS 15 class CommandQueueMT { - struct SyncSemaphore { - Semaphore sem; - bool in_use = false; - }; - struct CommandBase { + bool sync = false; virtual void call() = 0; - virtual SyncSemaphore *get_sync_semaphore() { return nullptr; } virtual ~CommandBase() = default; // Won't be called. }; struct SyncCommand : public CommandBase { - SyncSemaphore *sync_sem = nullptr; - - virtual SyncSemaphore *get_sync_semaphore() override { - return sync_sem; + virtual void call() override {} + SyncCommand() { + sync = true; } }; @@ -340,9 +330,11 @@ class CommandQueueMT { SYNC_SEMAPHORES = 8 }; + BinaryMutex mutex; LocalVector<uint8_t> command_mem; - SyncSemaphore sync_sems[SYNC_SEMAPHORES]; - Mutex mutex; + ConditionVariable sync_cond_var; + uint32_t sync_head = 0; + uint32_t sync_tail = 0; WorkerThreadPool::TaskID pump_task_id = WorkerThreadPool::INVALID_TASK_ID; uint64_t flush_read_ptr = 0; @@ -357,32 +349,23 @@ class CommandQueueMT { return cmd; } - template <typename T> - T *allocate_and_lock() { - lock(); - T *ret = allocate<T>(); - return ret; - } - void _flush() { - lock(); - if (unlikely(flush_read_ptr)) { // Re-entrant call. - unlock(); return; } + lock(); + WorkerThreadPool::thread_enter_command_queue_mt_flush(this); while (flush_read_ptr < command_mem.size()) { uint64_t size = *(uint64_t *)&command_mem[flush_read_ptr]; flush_read_ptr += 8; CommandBase *cmd = reinterpret_cast<CommandBase *>(&command_mem[flush_read_ptr]); - - SyncSemaphore *sync_sem = cmd->get_sync_semaphore(); cmd->call(); - if (sync_sem) { - sync_sem->sem.post(); // Release in case it needs sync/ret. + if (unlikely(cmd->sync)) { + sync_head++; + sync_cond_var.notify_all(); } flush_read_ptr += size; @@ -394,8 +377,12 @@ class CommandQueueMT { unlock(); } - void wait_for_flush(); - SyncSemaphore *_alloc_sync_sem(); + _FORCE_INLINE_ void _wait_for_sync(MutexLock<BinaryMutex> &p_lock) { + uint32_t sync_head_goal = sync_tail; + do { + sync_cond_var.wait(p_lock); + } while (sync_head != sync_head_goal); // Can't use lower-than because of wraparound. + } public: void lock(); diff --git a/core/variant/method_ptrcall.h b/core/variant/method_ptrcall.h index 123f2067e2..c8d1241d3d 100644 --- a/core/variant/method_ptrcall.h +++ b/core/variant/method_ptrcall.h @@ -159,10 +159,7 @@ MAKE_PTRARG_BY_REFERENCE(Variant); template <typename T> struct PtrToArg<T *> { _FORCE_INLINE_ static T *convert(const void *p_ptr) { - if (p_ptr == nullptr) { - return nullptr; - } - return const_cast<T *>(*reinterpret_cast<T *const *>(p_ptr)); + return likely(p_ptr) ? const_cast<T *>(*reinterpret_cast<T *const *>(p_ptr)) : nullptr; } typedef Object *EncodeT; _FORCE_INLINE_ static void encode(T *p_var, void *p_ptr) { @@ -173,10 +170,7 @@ struct PtrToArg<T *> { template <typename T> struct PtrToArg<const T *> { _FORCE_INLINE_ static const T *convert(const void *p_ptr) { - if (p_ptr == nullptr) { - return nullptr; - } - return *reinterpret_cast<T *const *>(p_ptr); + return likely(p_ptr) ? *reinterpret_cast<T *const *>(p_ptr) : nullptr; } typedef const Object *EncodeT; _FORCE_INLINE_ static void encode(T *p_var, void *p_ptr) { diff --git a/core/variant/variant_setget.cpp b/core/variant/variant_setget.cpp index 9d5ed22b1a..f49e9e54b3 100644 --- a/core/variant/variant_setget.cpp +++ b/core/variant/variant_setget.cpp @@ -251,15 +251,21 @@ void Variant::set_named(const StringName &p_member, const Variant &p_value, bool return; } } else if (type == Variant::DICTIONARY) { - Variant *v = VariantGetInternalPtr<Dictionary>::get_ptr(this)->getptr(p_member); + Dictionary &dict = *VariantGetInternalPtr<Dictionary>::get_ptr(this); + + if (dict.is_read_only()) { + r_valid = false; + return; + } + + Variant *v = dict.getptr(p_member); if (v) { *v = p_value; - r_valid = true; } else { - VariantGetInternalPtr<Dictionary>::get_ptr(this)->operator[](p_member) = p_value; - r_valid = true; + dict[p_member] = p_value; } + r_valid = true; } else { r_valid = false; } diff --git a/doc/classes/BaseMaterial3D.xml b/doc/classes/BaseMaterial3D.xml index 1bd4183b3c..33c58885d8 100644 --- a/doc/classes/BaseMaterial3D.xml +++ b/doc/classes/BaseMaterial3D.xml @@ -609,6 +609,9 @@ <constant name="BLEND_MODE_MUL" value="3" enum="BlendMode"> The color of the object is multiplied by the background. </constant> + <constant name="BLEND_MODE_PREMULT_ALPHA" value="4" enum="BlendMode"> + The color of the object is added to the background and the alpha channel is used to mask out the background. This is effectively a hybrid of the blend mix and add modes, useful for FX like fire where you want the flame to add but the smoke to mix. By default, this works with unshaded materials using premultiplied textures. For shaded materials, use the PREMUL_ALPHA_FACTOR built-in so that lighting can be modulated as well. + </constant> <constant name="ALPHA_ANTIALIASING_OFF" value="0" enum="AlphaAntiAliasing"> Disables Alpha AntiAliasing for the material. </constant> diff --git a/doc/classes/CodeEdit.xml b/doc/classes/CodeEdit.xml index 7c6f1a51c4..d455799c29 100644 --- a/doc/classes/CodeEdit.xml +++ b/doc/classes/CodeEdit.xml @@ -14,7 +14,7 @@ <return type="void" /> <param index="0" name="replace" type="bool" /> <description> - Override this method to define how the selected entry should be inserted. If [param replace] is true, any existing text should be replaced. + Override this method to define how the selected entry should be inserted. If [param replace] is [code]true[/code], any existing text should be replaced. </description> </method> <method name="_filter_code_completion_candidates" qualifiers="virtual const"> @@ -29,7 +29,7 @@ <return type="void" /> <param index="0" name="force" type="bool" /> <description> - Override this method to define what happens when the user requests code completion. If [param force] is true, any checks should be bypassed. + Override this method to define what happens when the user requests code completion. If [param force] is [code]true[/code], any checks should be bypassed. </description> </method> <method name="add_auto_brace_completion_pair"> @@ -123,7 +123,7 @@ <return type="void" /> <param index="0" name="replace" type="bool" default="false" /> <description> - Inserts the selected entry into the text. If [param replace] is true, any existing text is replaced rather than merged. + Inserts the selected entry into the text. If [param replace] is [code]true[/code], any existing text is replaced rather than merged. </description> </method> <method name="convert_indent"> @@ -144,6 +144,12 @@ Code regions are delimited using start and end tags (respectively [code]region[/code] and [code]endregion[/code] by default) preceded by one line comment delimiter. (eg. [code]#region[/code] and [code]#endregion[/code]) </description> </method> + <method name="delete_lines"> + <return type="void" /> + <description> + Deletes all lines that are selected or have a caret on them. + </description> + </method> <method name="do_indent"> <return type="void" /> <description> @@ -156,6 +162,12 @@ Duplicates all lines currently selected with any caret. Duplicates the entire line beneath the current one no matter where the caret is within the line. </description> </method> + <method name="duplicate_selection"> + <return type="void" /> + <description> + Duplicates all selected text and duplicates all lines with a caret on them. + </description> + </method> <method name="fold_all_lines"> <return type="void" /> <description> @@ -379,6 +391,18 @@ Returns whether the line at the specified index is folded or not. </description> </method> + <method name="move_lines_down"> + <return type="void" /> + <description> + Moves all lines down that are selected or have a caret on them. + </description> + </method> + <method name="move_lines_up"> + <return type="void" /> + <description> + Moves all lines up that are selected or have a caret on them. + </description> + </method> <method name="remove_comment_delimiter"> <return type="void" /> <param index="0" name="start_key" type="String" /> @@ -397,7 +421,7 @@ <return type="void" /> <param index="0" name="force" type="bool" default="false" /> <description> - Emits [signal code_completion_requested], if [param force] is true will bypass all checks. Otherwise will check that the caret is in a word or in front of a prefix. Will ignore the request if all current options are of type file path, node path or signal. + Emits [signal code_completion_requested], if [param force] is [code]true[/code] will bypass all checks. Otherwise will check that the caret is in a word or in front of a prefix. Will ignore the request if all current options are of type file path, node path, or signal. </description> </method> <method name="set_code_completion_selected_index"> @@ -467,6 +491,12 @@ Toggle the folding of the code block at the given line. </description> </method> + <method name="toggle_foldable_lines_at_carets"> + <return type="void" /> + <description> + Toggle the folding of the code block on all lines with a caret on them. + </description> + </method> <method name="unfold_all_lines"> <return type="void" /> <description> diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml index e614a897d5..fe67c2a38e 100644 --- a/doc/classes/DisplayServer.xml +++ b/doc/classes/DisplayServer.xml @@ -58,7 +58,7 @@ </method> <method name="create_status_indicator"> <return type="int" /> - <param index="0" name="icon" type="Image" /> + <param index="0" name="icon" type="Texture2D" /> <param index="1" name="tooltip" type="String" /> <param index="2" name="callback" type="Callable" /> <description> @@ -1178,11 +1178,22 @@ <method name="status_indicator_set_icon"> <return type="void" /> <param index="0" name="id" type="int" /> - <param index="1" name="icon" type="Image" /> + <param index="1" name="icon" type="Texture2D" /> <description> Sets the application status indicator icon. </description> </method> + <method name="status_indicator_set_menu"> + <return type="void" /> + <param index="0" name="id" type="int" /> + <param index="1" name="menu_rid" type="RID" /> + <description> + Sets the application status indicator native popup menu. + [b]Note:[/b] On macOS, the menu is activated by any mouse button. Its activation callback is [i]not[/i] triggered. + [b]Note:[/b] On Windows, the menu is activated by the right mouse button, selecting the status icon and pressing [kbd]Shift + F10[/kbd], or the applications key. The menu's activation callback for the other mouse buttons is still triggered. + [b]Note:[/b] Native popup is only supported if [NativeMenu] supports the [constant NativeMenu.FEATURE_POPUP_MENU] feature. + </description> + </method> <method name="status_indicator_set_tooltip"> <return type="void" /> <param index="0" name="id" type="int" /> diff --git a/doc/classes/Node2D.xml b/doc/classes/Node2D.xml index 091acdf6f2..851290de7b 100644 --- a/doc/classes/Node2D.xml +++ b/doc/classes/Node2D.xml @@ -44,7 +44,8 @@ <return type="void" /> <param index="0" name="point" type="Vector2" /> <description> - Rotates the node so it points towards the [param point], which is expected to use global coordinates. + Rotates the node so that its local +X axis points towards the [param point], which is expected to use global coordinates. + [param point] should not be the same as the node's position, otherwise the node always looks to the right. </description> </method> <method name="move_local_x"> diff --git a/doc/classes/RenderingServer.xml b/doc/classes/RenderingServer.xml index 5d90cd6b92..519bba4e7c 100644 --- a/doc/classes/RenderingServer.xml +++ b/doc/classes/RenderingServer.xml @@ -1979,6 +1979,12 @@ [b]Warning:[/b] This function is primarily intended for editor usage. For in-game use cases, prefer physics collision. </description> </method> + <method name="is_on_render_thread"> + <return type="bool" /> + <description> + Returns [code]true[/code] if our code is currently executing on the rendering thread. + </description> + </method> <method name="light_directional_set_blend_splits"> <return type="void" /> <param index="0" name="light" type="RID" /> diff --git a/doc/classes/StatusIndicator.xml b/doc/classes/StatusIndicator.xml index e1fcc35ad7..fb156b3c9f 100644 --- a/doc/classes/StatusIndicator.xml +++ b/doc/classes/StatusIndicator.xml @@ -9,9 +9,13 @@ <tutorials> </tutorials> <members> - <member name="icon" type="Image" setter="set_icon" getter="get_icon"> + <member name="icon" type="Texture2D" setter="set_icon" getter="get_icon"> Status indicator icon. </member> + <member name="menu" type="NodePath" setter="set_menu" getter="get_menu" default="NodePath("")"> + Status indicator native popup menu. If this is set, the [signal pressed] signal is not emitted. + [b]Note:[/b] Native popup is only supported if [NativeMenu] supports [constant NativeMenu.FEATURE_POPUP_MENU] feature. + </member> <member name="tooltip" type="String" setter="set_tooltip" getter="get_tooltip" default=""""> Status indicator tooltip. </member> diff --git a/doc/classes/String.xml b/doc/classes/String.xml index a33a1aea41..59733e9696 100644 --- a/doc/classes/String.xml +++ b/doc/classes/String.xml @@ -747,7 +747,7 @@ <method name="reverse" qualifiers="const"> <return type="String" /> <description> - Returns the copy of this string in reverse order. + Returns the copy of this string in reverse order. This operation works on unicode codepoints, rather than sequences of codepoints, and may break things like compound letters or emojis. </description> </method> <method name="rfind" qualifiers="const"> diff --git a/doc/classes/StringName.xml b/doc/classes/StringName.xml index e837b65199..e3c254fb48 100644 --- a/doc/classes/StringName.xml +++ b/doc/classes/StringName.xml @@ -648,7 +648,7 @@ <method name="reverse" qualifiers="const"> <return type="String" /> <description> - Returns the copy of this string in reverse order. + Returns the copy of this string in reverse order. This operation works on unicode codepoints, rather than sequences of codepoints, and may break things like compound letters or emojis. </description> </method> <method name="rfind" qualifiers="const"> diff --git a/doc/classes/TextEdit.xml b/doc/classes/TextEdit.xml index db0c1f17b0..2959ec4cfa 100644 --- a/doc/classes/TextEdit.xml +++ b/doc/classes/TextEdit.xml @@ -5,7 +5,7 @@ </brief_description> <description> A multiline text editor. It also has limited facilities for editing code, such as syntax highlighting support. For more advanced facilities for editing code, see [CodeEdit]. - [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] 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 in the order they were created. [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> @@ -58,7 +58,7 @@ <method name="add_caret"> <return type="int" /> <param index="0" name="line" type="int" /> - <param index="1" name="col" type="int" /> + <param index="1" name="column" 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> @@ -67,7 +67,7 @@ <return type="void" /> <param index="0" name="below" type="bool" /> <description> - Adds an additional caret above or below every caret. If [param below] is true the new caret will be added below and above otherwise. + Adds an additional caret above or below every caret. If [param below] is [code]true[/code] the new caret will be added below and above otherwise. </description> </method> <method name="add_gutter"> @@ -83,7 +83,7 @@ Adds a selection and a caret for the next occurrence of the current selection. If there is no active selection, selects word under caret. </description> </method> - <method name="adjust_carets_after_edit"> + <method name="adjust_carets_after_edit" deprecated="No longer necessary since methods now adjust carets themselves."> <return type="void" /> <param index="0" name="caret" type="int" /> <param index="1" name="from_line" type="int" /> @@ -91,7 +91,7 @@ <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]. + This method does nothing. </description> </method> <method name="adjust_viewport_to_caret"> @@ -120,6 +120,23 @@ Starts a multipart edit. All edits will be treated as one action until [method end_complex_operation] is called. </description> </method> + <method name="begin_multicaret_edit"> + <return type="void" /> + <description> + Starts an edit for multiple carets. The edit must be ended with [method end_multicaret_edit]. Multicaret edits can be used to edit text at multiple carets and delay merging the carets until the end, so the caret indexes aren't affected immediately. [method begin_multicaret_edit] and [method end_multicaret_edit] can be nested, and the merge will happen at the last [method end_multicaret_edit]. + Example usage: + [codeblock] + begin_complex_operation() + begin_multicaret_edit() + for i in range(get_caret_count()): + if multicaret_edit_ignore_caret(i): + continue + # Logic here. + end_multicaret_edit() + end_complex_operation() + [/codeblock] + </description> + </method> <method name="cancel_ime"> <return type="void" /> <description> @@ -145,6 +162,20 @@ Clears the undo history. </description> </method> + <method name="collapse_carets"> + <return type="void" /> + <param index="0" name="from_line" type="int" /> + <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="inclusive" type="bool" default="false" /> + <description> + Collapse all carets in the given range to the [param from_line] and [param from_column] position. + [param inclusive] applies to both ends. + If [method is_in_mulitcaret_edit] is [code]true[/code], carets that are collapsed will be [code]true[/code] for [method multicaret_edit_ignore_caret]. + [method merge_overlapping_carets] will be called if any carets were collapsed. + </description> + </method> <method name="copy"> <return type="void" /> <param index="0" name="caret_index" type="int" default="-1" /> @@ -185,6 +216,12 @@ Ends a multipart edit, started with [method begin_complex_operation]. If called outside a complex operation, the current operation is pushed onto the undo/redo stack. </description> </method> + <method name="end_multicaret_edit"> + <return type="void" /> + <description> + Ends an edit for multiple carets, that was started with [method begin_multicaret_edit]. If this was the last [method end_multicaret_edit] and [method merge_overlapping_carets] was called, carets will be merged. + </description> + </method> <method name="get_caret_column" qualifiers="const"> <return type="int" /> <param index="0" name="caret_index" type="int" default="0" /> @@ -205,7 +242,7 @@ Returns the caret pixel draw position. </description> </method> - <method name="get_caret_index_edit_order"> + <method name="get_caret_index_edit_order" deprecated="Carets no longer need to be edited in any specific order. If the carets need to be sorted, use [method get_sorted_carets] instead."> <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. @@ -363,6 +400,15 @@ [b]Note:[/b] The return value is influenced by [theme_item line_spacing] and [theme_item font_size]. And it will not be less than [code]1[/code]. </description> </method> + <method name="get_line_ranges_from_carets" qualifiers="const"> + <return type="Vector2i[]" /> + <param index="0" name="only_selections" type="bool" default="false" /> + <param index="1" name="merge_adjacent" type="bool" default="true" /> + <description> + Returns an [Array] of line ranges where [code]x[/code] is the first line and [code]y[/code] is the last line. All lines within these ranges will have a caret on them or be part of a selection. Each line will only be part of one line range, even if it has multiple carets on it. + If a selection's end column ([method get_selection_to_column]) is at column [code]0[/code], that line will not be included. If a selection begins on the line after another selection ends and [param merge_adjacent] is [code]true[/code], or they begin and end on the same line, one line range will include both selections. + </description> + </method> <method name="get_line_width" qualifiers="const"> <return type="int" /> <param index="0" name="line" type="int" /> @@ -514,7 +560,18 @@ Returns the text inside the selection of a caret, or all the carets if [param caret_index] is its default value [code]-1[/code]. </description> </method> - <method name="get_selection_column" qualifiers="const"> + <method name="get_selection_at_line_column" qualifiers="const"> + <return type="int" /> + <param index="0" name="line" type="int" /> + <param index="1" name="column" type="int" /> + <param index="2" name="include_edges" type="bool" default="true" /> + <param index="3" name="only_selections" type="bool" default="true" /> + <description> + Returns the caret index of the selection at the given [param line] and [param column], or [code]-1[/code] if there is none. + If [param include_edges] is [code]false[/code], the position must be inside the selection and not at either end. If [param only_selections] is [code]false[/code], carets without a selection will also be considered. + </description> + </method> + <method name="get_selection_column" qualifiers="const" deprecated="Use [method get_selection_origin_column] instead."> <return type="int" /> <param index="0" name="caret_index" type="int" default="0" /> <description> @@ -525,17 +582,17 @@ <return type="int" /> <param index="0" name="caret_index" type="int" default="0" /> <description> - Returns the selection begin column. + Returns the selection begin column. Returns the caret column if there is no selection. </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. + Returns the selection begin line. Returns the caret line if there is no selection. </description> </method> - <method name="get_selection_line" qualifiers="const"> + <method name="get_selection_line" qualifiers="const" deprecated="Use [method get_selection_origin_line] instead."> <return type="int" /> <param index="0" name="caret_index" type="int" default="0" /> <description> @@ -548,18 +605,40 @@ Returns the current selection mode. </description> </method> + <method name="get_selection_origin_column" qualifiers="const"> + <return type="int" /> + <param index="0" name="caret_index" type="int" default="0" /> + <description> + Returns the origin column of the selection. This is the opposite end from the caret. + </description> + </method> + <method name="get_selection_origin_line" qualifiers="const"> + <return type="int" /> + <param index="0" name="caret_index" type="int" default="0" /> + <description> + Returns the origin line of the selection. This is the opposite end from the caret. + </description> + </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. + Returns the selection end column. Returns the caret column if there is no selection. </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. + Returns the selection end line. Returns the caret line if there is no selection. + </description> + </method> + <method name="get_sorted_carets" qualifiers="const"> + <return type="PackedInt32Array" /> + <param index="0" name="include_ignored_carets" type="bool" default="false" /> + <description> + Returns the carets sorted by selection beginning from lowest line and column to highest (from top to bottom of text). + If [param include_ignored_carets] is [code]false[/code], carets from [method multicaret_edit_ignore_caret] will be ignored. </description> </method> <method name="get_tab_size" qualifiers="const"> @@ -653,6 +732,19 @@ Inserts a new line with [param text] at [param line]. </description> </method> + <method name="insert_text"> + <return type="void" /> + <param index="0" name="text" type="String" /> + <param index="1" name="line" type="int" /> + <param index="2" name="column" type="int" /> + <param index="3" name="before_selection_begin" type="bool" default="true" /> + <param index="4" name="before_selection_end" type="bool" default="false" /> + <description> + Inserts the [param text] at [param line] and [param column]. + If [param before_selection_begin] is [code]true[/code], carets and selections that begin at [param line] and [param column] will moved to the end of the inserted text, along with all carets after it. + If [param before_selection_end] is [code]true[/code], selections that end at [param line] and [param column] will be extended to the end of the inserted text. These parameters can be used to insert text inside of or outside of selections. + </description> + </method> <method name="insert_text_at_caret"> <return type="void" /> <param index="0" name="text" type="String" /> @@ -661,6 +753,13 @@ Insert the specified text at the caret position. </description> </method> + <method name="is_caret_after_selection_origin" qualifiers="const"> + <return type="bool" /> + <param index="0" name="caret_index" type="int" default="0" /> + <description> + Returns [code]true[/code] if the caret of the selection is after the selection origin. This can be used to determine the direction of the selection. + </description> + </method> <method name="is_caret_visible" qualifiers="const"> <return type="bool" /> <param index="0" name="caret_index" type="int" default="0" /> @@ -671,7 +770,7 @@ <method name="is_dragging_cursor" qualifiers="const"> <return type="bool" /> <description> - Returns [code]true[/code] if the user is dragging their mouse for scrolling or selecting. + Returns [code]true[/code] if the user is dragging their mouse for scrolling, selecting, or text dragging. </description> </method> <method name="is_gutter_clickable" qualifiers="const"> @@ -695,6 +794,12 @@ Returns whether the gutter is overwritable. </description> </method> + <method name="is_in_mulitcaret_edit" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if a [method begin_multicaret_edit] has been called and [method end_multicaret_edit] has not yet been called. + </description> + </method> <method name="is_line_gutter_clickable" qualifiers="const"> <return type="bool" /> <param index="0" name="line" type="int" /> @@ -749,9 +854,18 @@ <return type="void" /> <description> Merges any overlapping carets. Will favor the newest caret, or the caret with a selection. + If [method is_in_mulitcaret_edit] is [code]true[/code], the merge will be queued to happen at the end of the multicaret edit. See [method begin_multicaret_edit] and [method end_multicaret_edit]. [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="multicaret_edit_ignore_caret" qualifiers="const"> + <return type="bool" /> + <param index="0" name="caret_index" type="int" /> + <description> + Returns [code]true[/code] if the given [param caret_index] should be ignored as part of a multicaret edit. See [method begin_multicaret_edit] and [method end_multicaret_edit]. Carets that should be ignored are ones that were part of removed text and will likely be merged at the end of the edit, or carets that were added during the edit. + It is recommended to [code]continue[/code] within a loop iterating on multiple carets if a caret should be ignored. + </description> + </method> <method name="paste"> <return type="void" /> <param index="0" name="caret_index" type="int" default="-1" /> @@ -787,6 +901,15 @@ Removes the gutter from this [TextEdit]. </description> </method> + <method name="remove_line_at"> + <return type="void" /> + <param index="0" name="line" type="int" /> + <param index="1" name="move_carets_down" type="bool" default="true" /> + <description> + Removes the line of text at [param line]. Carets on this line will attempt to match their previous visual x position. + If [param move_carets_down] is [code]true[/code] carets will move to the next line down, otherwise carets will move up. + </description> + </method> <method name="remove_secondary_carets"> <return type="void" /> <description> @@ -801,7 +924,6 @@ <param index="3" name="to_column" type="int" /> <description> Removes text between the given positions. - [b]Note:[/b] This does not adjust the caret or selection, which as a result it can end up in an invalid position. </description> </method> <method name="search" qualifiers="const"> @@ -809,7 +931,7 @@ <param index="0" name="text" type="String" /> <param index="1" name="flags" type="int" /> <param index="2" name="from_line" type="int" /> - <param index="3" name="from_colum" type="int" /> + <param index="3" name="from_column" type="int" /> <description> Perform a search inside the text. Search flags can be specified in the [enum SearchFlags] enum. In the returned vector, [code]x[/code] is the column, [code]y[/code] is the line. If no results are found, both are equal to [code]-1[/code]. @@ -835,14 +957,15 @@ </method> <method name="select"> <return type="void" /> - <param index="0" name="from_line" type="int" /> - <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="0" name="origin_line" type="int" /> + <param index="1" name="origin_column" type="int" /> + <param index="2" name="caret_line" type="int" /> + <param index="3" name="caret_column" type="int" /> <param index="4" name="caret_index" type="int" default="0" /> <description> - Perform selection, from line/column to line/column. + Selects text from [param origin_line] and [param origin_column] to [param caret_line] and [param caret_column] for the given [param caret_index]. This moves the selection origin and the caret. If the positions are the same, the selection will be deselected. If [member selecting_enabled] is [code]false[/code], no selection will occur. + [b]Note:[/b] If supporting multiple carets this will not check for any overlap. See [method merge_overlapping_carets]. </description> </method> <method name="select_all"> @@ -878,9 +1001,10 @@ <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. + Moves the caret to the specified [param line] index. The caret column will be moved to the same visual position it was at the last time [method set_caret_column] was called, or clamped to the end of the line. 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 [param line] can be hidden. + If [param wrap_index] is [code]-1[/code], the caret column will be clamped to the [param line]'s length. If [param wrap_index] is greater than [code]-1[/code], the column will be moved to attempt to match the visual x position on the line's [param wrap_index] to the position from the last time [method set_caret_column] was called. [b]Note:[/b] If supporting multiple carets this will not check for any overlap. See [method merge_overlapping_carets]. </description> </method> @@ -945,7 +1069,8 @@ <param index="0" name="line" type="int" /> <param index="1" name="new_text" type="String" /> <description> - Sets the text for a specific line. + Sets the text for a specific [param line]. + Carets on the line will attempt to keep their visual x position. </description> </method> <method name="set_line_as_center_visible"> @@ -1049,13 +1174,30 @@ <method name="set_selection_mode"> <return type="void" /> <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> </method> + <method name="set_selection_origin_column"> + <return type="void" /> + <param index="0" name="column" type="int" /> + <param index="1" name="caret_index" type="int" default="0" /> + <description> + Sets the selection origin column to the [param column] for the given [param caret_index]. If the selection origin is moved to the caret position, the selection will deselect. + </description> + </method> + <method name="set_selection_origin_line"> + <return type="void" /> + <param index="0" name="line" type="int" /> + <param index="1" name="can_be_hidden" type="bool" default="true" /> + <param index="2" name="wrap_index" type="int" default="-1" /> + <param index="3" name="caret_index" type="int" default="0" /> + <description> + Sets the selection origin line to the [param line] for the given [param caret_index]. If the selection origin is moved to the caret position, the selection will deselect. + If [param can_be_hidden] is [code]false[/code], The line will be set to the nearest unhidden line below or above. + If [param wrap_index] is [code]-1[/code], the selection origin column will be clamped to the [param line]'s length. If [param wrap_index] is greater than [code]-1[/code], the column will be moved to attempt to match the visual x position on the line's [param wrap_index] to the position from the last time [method set_selection_origin_column] or [method select] was called. + </description> + </method> <method name="set_tab_size"> <return type="void" /> <param index="0" name="size" type="int" /> @@ -1089,7 +1231,7 @@ <param index="0" name="from_line" type="int" /> <param index="1" name="to_line" type="int" /> <description> - Swaps the two lines. + Swaps the two lines. Carets will be swapped with the lines. </description> </method> <method name="tag_saved_version"> @@ -1140,7 +1282,7 @@ If [code]true[/code], the selected text will be deselected when focus is lost. </member> <member name="drag_and_drop_selection_enabled" type="bool" setter="set_drag_and_drop_selection_enabled" getter="is_drag_and_drop_selection_enabled" default="true"> - If [code]true[/code], allow drag and drop of selected text. + If [code]true[/code], allow drag and drop of selected text. Text can still be dropped from other sources. </member> <member name="draw_control_chars" type="bool" setter="set_draw_control_chars" getter="get_draw_control_chars" default="false"> If [code]true[/code], control characters are displayed. @@ -1231,7 +1373,7 @@ <signals> <signal name="caret_changed"> <description> - Emitted when the caret changes position. + Emitted when any caret changes position. </description> </signal> <signal name="gutter_added"> diff --git a/doc/classes/Variant.xml b/doc/classes/Variant.xml index eb837a4643..b420933285 100644 --- a/doc/classes/Variant.xml +++ b/doc/classes/Variant.xml @@ -42,8 +42,8 @@ # Note that Objects are their own special category. # To get the name of the underlying Object type, you need the `get_class()` method. print("foo is a(n) %s" % foo.get_class()) # inject the class name into a formatted string. - # Note also that there is not yet any way to get a script's `class_name` string easily. - # To fetch that value, you can use ProjectSettings.get_global_class_list(). + # Note that this does not get the script's `class_name` global identifier. + # If the `class_name` is needed, use `foo.get_script().get_global_name()` instead. [/gdscript] [csharp] Variant foo = 2; diff --git a/doc/classes/XRServer.xml b/doc/classes/XRServer.xml index d5714980c3..4179ba821c 100644 --- a/doc/classes/XRServer.xml +++ b/doc/classes/XRServer.xml @@ -37,8 +37,8 @@ You should call this method after a few seconds have passed. For example, when the user requests a realignment of the display holding a designated button on a controller for a short period of time, or when implementing a teleport mechanism. </description> </method> - <method name="clear_reference_frame" qualifiers="const"> - <return type="Transform3D" /> + <method name="clear_reference_frame"> + <return type="void" /> <description> Clears the reference frame that was set by previous calls to [method center_on_hmd]. </description> diff --git a/drivers/gles3/rasterizer_scene_gles3.cpp b/drivers/gles3/rasterizer_scene_gles3.cpp index 606bcee775..ee770be3da 100644 --- a/drivers/gles3/rasterizer_scene_gles3.cpp +++ b/drivers/gles3/rasterizer_scene_gles3.cpp @@ -3041,6 +3041,11 @@ void RasterizerSceneGLES3::_render_list_template(RenderListParameters *p_params, } } break; + case GLES3::SceneShaderData::BLEND_MODE_PREMULT_ALPHA: { + glBlendEquation(GL_FUNC_ADD); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + } break; case GLES3::SceneShaderData::BLEND_MODE_ALPHA_TO_COVERAGE: { // Do nothing for now. } break; diff --git a/drivers/gles3/shaders/scene.glsl b/drivers/gles3/shaders/scene.glsl index 797b9066a9..22d72337b9 100644 --- a/drivers/gles3/shaders/scene.glsl +++ b/drivers/gles3/shaders/scene.glsl @@ -1442,6 +1442,9 @@ void main() { float clearcoat_roughness = 0.0; float anisotropy = 0.0; vec2 anisotropy_flow = vec2(1.0, 0.0); +#ifdef PREMULT_ALPHA_USED + float premult_alpha = 1.0; +#endif #ifndef FOG_DISABLED vec4 fog = vec4(0.0); #endif // !FOG_DISABLED @@ -2141,9 +2144,12 @@ void main() { frag_color.rgb += additive_light_color; #endif // USE_ADDITIVE_LIGHTING - frag_color.rgb *= scene_data.luminance_multiplier; #endif // !RENDER_MATERIAL #endif // !MODE_RENDER_DEPTH + +#ifdef PREMULT_ALPHA_USED + frag_color.rgb *= premult_alpha; +#endif } diff --git a/drivers/gles3/storage/material_storage.cpp b/drivers/gles3/storage/material_storage.cpp index 62d22dac4d..1952502fb2 100644 --- a/drivers/gles3/storage/material_storage.cpp +++ b/drivers/gles3/storage/material_storage.cpp @@ -1243,6 +1243,7 @@ MaterialStorage::MaterialStorage() { actions.renames["NORMAL_MAP_DEPTH"] = "normal_map_depth"; actions.renames["ALBEDO"] = "albedo"; actions.renames["ALPHA"] = "alpha"; + actions.renames["PREMULT_ALPHA_FACTOR"] = "premult_alpha"; actions.renames["METALLIC"] = "metallic"; actions.renames["SPECULAR"] = "specular"; actions.renames["ROUGHNESS"] = "roughness"; @@ -1327,6 +1328,7 @@ MaterialStorage::MaterialStorage() { actions.usage_defines["ALPHA_HASH_SCALE"] = "#define ALPHA_HASH_USED\n"; actions.usage_defines["ALPHA_ANTIALIASING_EDGE"] = "#define ALPHA_ANTIALIASING_EDGE_USED\n"; actions.usage_defines["ALPHA_TEXTURE_COORDINATE"] = "@ALPHA_ANTIALIASING_EDGE"; + actions.usage_defines["PREMULT_ALPHA_FACTOR"] = "#define PREMULT_ALPHA_USED"; actions.usage_defines["SSS_STRENGTH"] = "#define ENABLE_SSS\n"; actions.usage_defines["SSS_TRANSMITTANCE_DEPTH"] = "#define ENABLE_TRANSMITTANCE\n"; @@ -2908,6 +2910,7 @@ void SceneShaderData::set_code(const String &p_code) { actions.render_mode_values["blend_mix"] = Pair<int *, int>(&blend_modei, BLEND_MODE_MIX); actions.render_mode_values["blend_sub"] = Pair<int *, int>(&blend_modei, BLEND_MODE_SUB); actions.render_mode_values["blend_mul"] = Pair<int *, int>(&blend_modei, BLEND_MODE_MUL); + actions.render_mode_values["blend_premult_alpha"] = Pair<int *, int>(&blend_modei, BLEND_MODE_PREMULT_ALPHA); actions.render_mode_values["alpha_to_coverage"] = Pair<int *, int>(&alpha_antialiasing_modei, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE); actions.render_mode_values["alpha_to_coverage_and_one"] = Pair<int *, int>(&alpha_antialiasing_modei, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE_AND_TO_ONE); diff --git a/drivers/gles3/storage/material_storage.h b/drivers/gles3/storage/material_storage.h index 02aecf33d6..392ebcc570 100644 --- a/drivers/gles3/storage/material_storage.h +++ b/drivers/gles3/storage/material_storage.h @@ -248,6 +248,7 @@ struct SceneShaderData : public ShaderData { BLEND_MODE_ADD, BLEND_MODE_SUB, BLEND_MODE_MUL, + BLEND_MODE_PREMULT_ALPHA, BLEND_MODE_ALPHA_TO_COVERAGE }; diff --git a/drivers/png/SCsub b/drivers/png/SCsub index dd4777a19b..e38f3c4760 100644 --- a/drivers/png/SCsub +++ b/drivers/png/SCsub @@ -39,7 +39,7 @@ if env["builtin_libpng"]: if env["arch"].startswith("arm"): if env.msvc: # Can't compile assembly files with MSVC. - env_thirdparty.Append(CPPDEFINES=[("PNG_ARM_NEON_OPT"), 0]) + env_thirdparty.Append(CPPDEFINES=[("PNG_ARM_NEON_OPT", 0)]) else: env_neon = env_thirdparty.Clone() if "S_compiler" in env: diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index 49896d66d8..cfeb495690 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -33,7 +33,6 @@ #include "core/input/input.h" #include "core/os/keyboard.h" #include "core/string/string_builder.h" -#include "core/templates/pair.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/plugins/script_editor_plugin.h" @@ -173,10 +172,8 @@ bool FindReplaceBar::_search(uint32_t p_flags, int p_from_line, int p_from_col) if (pos.x != -1) { if (!preserve_cursor && !is_selection_only()) { 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(0); text_editor->select(pos.y, pos.x, pos.y, pos.x + text.length()); + text_editor->center_viewport_to_caret(0); line_col_changed_for_result = true; } @@ -216,7 +213,7 @@ void FindReplaceBar::_replace() { text_editor->begin_complex_operation(); if (selection_enabled && is_selection_only()) { // Restrict search_current() to selected region. - text_editor->set_caret_line(selection_begin.width, false, true, 0, 0); + text_editor->set_caret_line(selection_begin.width, false, true, -1, 0); text_editor->set_caret_column(selection_begin.height, true, 0); } @@ -285,10 +282,10 @@ void FindReplaceBar::_replace_all() { text_editor->begin_complex_operation(); if (selection_enabled && is_selection_only()) { - text_editor->set_caret_line(selection_begin.width, false, true, 0, 0); + text_editor->set_caret_line(selection_begin.width, false, true, -1, 0); text_editor->set_caret_column(selection_begin.height, true, 0); } else { - text_editor->set_caret_line(0, false, true, 0, 0); + text_editor->set_caret_line(0, false, true, -1, 0); text_editor->set_caret_column(0, true, 0); } @@ -812,22 +809,22 @@ void CodeTextEditor::input(const Ref<InputEvent> &event) { } if (ED_IS_SHORTCUT("script_text_editor/move_up", key_event)) { - move_lines_up(); + text_editor->move_lines_up(); accept_event(); return; } if (ED_IS_SHORTCUT("script_text_editor/move_down", key_event)) { - move_lines_down(); + text_editor->move_lines_down(); accept_event(); return; } if (ED_IS_SHORTCUT("script_text_editor/delete_line", key_event)) { - delete_lines(); + text_editor->delete_lines(); accept_event(); return; } if (ED_IS_SHORTCUT("script_text_editor/duplicate_selection", key_event)) { - duplicate_selection(); + text_editor->duplicate_selection(); accept_event(); return; } @@ -1116,31 +1113,23 @@ void CodeTextEditor::trim_trailing_whitespace() { break; } } - text_editor->set_line(i, line.substr(0, end)); + text_editor->remove_text(i, end, i, line.length()); } } if (trimmed_whitespace) { text_editor->merge_overlapping_carets(); text_editor->end_complex_operation(); - text_editor->queue_redraw(); } } void CodeTextEditor::insert_final_newline() { int final_line = text_editor->get_line_count() - 1; - String line = text_editor->get_line(final_line); // Length 0 means it's already an empty line, no need to add a newline. if (line.length() > 0 && !line.ends_with("\n")) { - text_editor->begin_complex_operation(); - - line += "\n"; - text_editor->set_line(final_line, line); - - text_editor->end_complex_operation(); - text_editor->queue_redraw(); + text_editor->insert_text("\n", final_line, line.length(), false); } } @@ -1149,9 +1138,12 @@ void CodeTextEditor::convert_case(CaseStyle p_case) { return; } text_editor->begin_complex_operation(); + text_editor->begin_multicaret_edit(); - Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order(); - for (const int &c : caret_edit_order) { + for (int c = 0; c < text_editor->get_caret_count(); c++) { + if (text_editor->multicaret_edit_ignore_caret(c)) { + continue; + } if (!text_editor->has_selection(c)) { continue; } @@ -1192,6 +1184,7 @@ void CodeTextEditor::convert_case(CaseStyle p_case) { text_editor->set_line(i, new_line); } } + text_editor->end_multicaret_edit(); text_editor->end_complex_operation(); } @@ -1200,308 +1193,24 @@ void CodeTextEditor::set_indent_using_spaces(bool p_use_spaces) { indentation_txt->set_text(p_use_spaces ? TTR("Spaces", "Indentation") : TTR("Tabs", "Indentation")); } -void CodeTextEditor::move_lines_up() { - text_editor->begin_complex_operation(); - - Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order(); - - // Lists of carets representing each group. - Vector<Vector<int>> caret_groups; - Vector<Pair<int, int>> group_borders; - - // Search for groups of carets and their selections residing on the same lines. - for (int i = 0; i < caret_edit_order.size(); i++) { - int c = caret_edit_order[i]; - - Vector<int> new_group{ c }; - Pair<int, int> group_border; - group_border.first = _get_affected_lines_from(c); - group_border.second = _get_affected_lines_to(c); - - for (int j = i; j < caret_edit_order.size() - 1; j++) { - int c_current = caret_edit_order[j]; - int c_next = caret_edit_order[j + 1]; - - int next_start_pos = _get_affected_lines_from(c_next); - int next_end_pos = _get_affected_lines_to(c_next); - - int current_start_pos = text_editor->has_selection(c_current) ? text_editor->get_selection_from_line(c_current) : text_editor->get_caret_line(c_current); - - i = j; - if (next_end_pos != current_start_pos && next_end_pos + 1 != current_start_pos) { - break; - } - group_border.first = next_start_pos; - new_group.push_back(c_next); - // If the last caret is added to the current group there is no need to process it again. - if (j + 1 == caret_edit_order.size() - 1) { - i++; - } - } - group_borders.push_back(group_border); - caret_groups.push_back(new_group); - } - - for (int i = group_borders.size() - 1; i >= 0; i--) { - if (group_borders[i].first - 1 < 0) { - continue; - } - - // If the group starts overlapping with the upper group don't move it. - if (i < group_borders.size() - 1 && group_borders[i].first - 1 <= group_borders[i + 1].second) { - continue; - } - - // We have to remember caret positions and selections prior to line swapping. - Vector<Vector<int>> caret_group_parameters; - - for (int j = 0; j < caret_groups[i].size(); j++) { - int c = caret_groups[i][j]; - int cursor_line = text_editor->get_caret_line(c); - int cursor_column = text_editor->get_caret_column(c); - - if (!text_editor->has_selection(c)) { - caret_group_parameters.push_back(Vector<int>{ -1, -1, -1, -1, cursor_line, cursor_column }); - continue; - } - 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); - caret_group_parameters.push_back(Vector<int>{ from_line, from_col, to_line, to_column, cursor_line, cursor_column }); - } - - for (int line_id = group_borders[i].first; line_id <= group_borders[i].second; line_id++) { - text_editor->unfold_line(line_id); - text_editor->unfold_line(line_id - 1); - - text_editor->swap_lines(line_id - 1, line_id); - } - - for (int j = 0; j < caret_groups[i].size(); j++) { - int c = caret_groups[i][j]; - const Vector<int> &caret_parameters = caret_group_parameters[j]; - text_editor->set_caret_line(caret_parameters[4] - 1, c == 0, true, 0, c); - text_editor->set_caret_column(caret_parameters[5], c == 0, c); - - if (caret_parameters[0] >= 0) { - text_editor->select(caret_parameters[0] - 1, caret_parameters[1], caret_parameters[2] - 1, caret_parameters[3], c); - } - } - } - - text_editor->end_complex_operation(); - text_editor->merge_overlapping_carets(); - text_editor->queue_redraw(); -} - -void CodeTextEditor::move_lines_down() { - text_editor->begin_complex_operation(); - - Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order(); - - // Lists of carets representing each group. - Vector<Vector<int>> caret_groups; - Vector<Pair<int, int>> group_borders; - Vector<int> group_border_ends; - // Search for groups of carets and their selections residing on the same lines. - for (int i = 0; i < caret_edit_order.size(); i++) { - int c = caret_edit_order[i]; - - Vector<int> new_group{ c }; - Pair<int, int> group_border; - group_border.first = _get_affected_lines_from(c); - group_border.second = _get_affected_lines_to(c); - - for (int j = i; j < caret_edit_order.size() - 1; j++) { - int c_current = caret_edit_order[j]; - int c_next = caret_edit_order[j + 1]; - - int next_start_pos = _get_affected_lines_from(c_next); - int next_end_pos = _get_affected_lines_to(c_next); - - int current_start_pos = text_editor->has_selection(c_current) ? text_editor->get_selection_from_line(c_current) : text_editor->get_caret_line(c_current); - - i = j; - if (next_end_pos == current_start_pos || next_end_pos + 1 == current_start_pos) { - group_border.first = next_start_pos; - new_group.push_back(c_next); - // If the last caret is added to the current group there is no need to process it again. - if (j + 1 == caret_edit_order.size() - 1) { - i++; - } - } else { - break; - } - } - group_borders.push_back(group_border); - group_border_ends.push_back(text_editor->has_selection(c) ? text_editor->get_selection_to_line(c) : text_editor->get_caret_line(c)); - caret_groups.push_back(new_group); - } - - for (int i = 0; i < group_borders.size(); i++) { - if (group_border_ends[i] + 1 > text_editor->get_line_count() - 1) { - continue; - } - - // If the group starts overlapping with the upper group don't move it. - if (i > 0 && group_border_ends[i] + 1 >= group_borders[i - 1].first) { - continue; - } - - // We have to remember caret positions and selections prior to line swapping. - Vector<Vector<int>> caret_group_parameters; - - for (int j = 0; j < caret_groups[i].size(); j++) { - int c = caret_groups[i][j]; - int cursor_line = text_editor->get_caret_line(c); - int cursor_column = text_editor->get_caret_column(c); - - if (!text_editor->has_selection(c)) { - caret_group_parameters.push_back(Vector<int>{ -1, -1, -1, -1, cursor_line, cursor_column }); - continue; - } - 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); - caret_group_parameters.push_back(Vector<int>{ from_line, from_col, to_line, to_column, cursor_line, cursor_column }); - } - - for (int line_id = group_borders[i].second; line_id >= group_borders[i].first; line_id--) { - text_editor->unfold_line(line_id); - text_editor->unfold_line(line_id + 1); - - text_editor->swap_lines(line_id + 1, line_id); - } - - for (int j = 0; j < caret_groups[i].size(); j++) { - int c = caret_groups[i][j]; - const Vector<int> &caret_parameters = caret_group_parameters[j]; - text_editor->set_caret_line(caret_parameters[4] + 1, c == 0, true, 0, c); - text_editor->set_caret_column(caret_parameters[5], c == 0, c); - - if (caret_parameters[0] >= 0) { - text_editor->select(caret_parameters[0] + 1, caret_parameters[1], caret_parameters[2] + 1, caret_parameters[3], c); - } - } - } - - text_editor->merge_overlapping_carets(); - text_editor->end_complex_operation(); - text_editor->queue_redraw(); -} - -void CodeTextEditor::delete_lines() { - text_editor->begin_complex_operation(); - - Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order(); - Vector<int> lines; - int last_line = INT_MAX; - for (const int &c : caret_edit_order) { - for (int line = _get_affected_lines_to(c); line >= _get_affected_lines_from(c); line--) { - if (line >= last_line) { - continue; - } - last_line = line; - lines.append(line); - } - } - - for (const int &line : lines) { - if (line != text_editor->get_line_count() - 1) { - text_editor->remove_text(line, 0, line + 1, 0); - } else { - text_editor->remove_text(line - 1, text_editor->get_line(line - 1).length(), line, text_editor->get_line(line).length()); - } - // Readjust carets. - int new_line = MIN(line, text_editor->get_line_count() - 1); - text_editor->unfold_line(new_line); - for (const int &c : caret_edit_order) { - if (text_editor->get_caret_line(c) == line || (text_editor->get_caret_line(c) == line + 1 && text_editor->get_caret_column(c) == 0)) { - text_editor->deselect(c); - text_editor->set_caret_line(new_line, c == 0, true, 0, c); - continue; - } - if (text_editor->get_caret_line(c) > line) { - text_editor->set_caret_line(text_editor->get_caret_line(c) - 1, c == 0, true, 0, c); - continue; - } - break; - } - } - text_editor->merge_overlapping_carets(); - text_editor->end_complex_operation(); -} - -void CodeTextEditor::duplicate_selection() { - text_editor->begin_complex_operation(); - - 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(); + text_editor->begin_multicaret_edit(); - Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order(); - caret_edit_order.reverse(); - int last_line = -1; + Vector<Point2i> line_ranges = text_editor->get_line_ranges_from_carets(); int folded_to = 0; - for (const int &c1 : caret_edit_order) { - int from = _get_affected_lines_from(c1); - from += from == last_line ? 1 + folded_to : 0; - int to = _get_affected_lines_to(c1); - last_line = to; + for (Point2i line_range : line_ranges) { + int from_line = line_range.x; + int to_line = line_range.y; // If last line is folded, extends to the end of the folded section - if (text_editor->is_line_folded(to)) { - folded_to = text_editor->get_next_visible_line_offset_from(to + 1, 1) - 1; - to += folded_to; + if (text_editor->is_line_folded(to_line)) { + folded_to = text_editor->get_next_visible_line_offset_from(to_line + 1, 1) - 1; + to_line += folded_to; } // Check first if there's any uncommented lines in selection. bool is_commented = true; bool is_all_empty = true; - for (int line = from; line <= to; line++) { + for (int line = from_line; line <= to_line; line++) { // `+ delimiter.length()` here because comment delimiter is not actually `in comment` so we check first character after it int delimiter_idx = text_editor->is_in_comment(line, text_editor->get_first_non_whitespace_column(line) + delimiter.length()); // Empty lines should not be counted. @@ -1517,58 +1226,24 @@ void CodeTextEditor::toggle_inline_comment(const String &delimiter) { // Special case for commenting empty lines, treat it/them as uncommented lines. is_commented = is_commented && !is_all_empty; - // Caret positions need to be saved since they could be moved at the eol. - Vector<int> caret_cols; - Vector<int> selection_to_cols; - for (const int &c2 : caret_edit_order) { - if (text_editor->get_caret_line(c2) >= from && text_editor->get_caret_line(c2) <= to) { - caret_cols.append(text_editor->get_caret_column(c2)); - } - if (text_editor->has_selection(c2) && text_editor->get_selection_to_line(c2) >= from && text_editor->get_selection_to_line(c2) <= to) { - selection_to_cols.append(text_editor->get_selection_to_column(c2)); - } - } - // Comment/uncomment. - for (int line = from; line <= to; line++) { - String line_text = text_editor->get_line(line); + for (int line = from_line; line <= to_line; line++) { if (is_all_empty) { - text_editor->set_line(line, delimiter); + text_editor->insert_text(delimiter, line, 0); continue; } if (is_commented) { - text_editor->set_line(line, line_text.replace_first(delimiter, "")); + int delimiter_column = text_editor->get_line(line).find(delimiter); + text_editor->remove_text(line, delimiter_column, line, delimiter_column + delimiter.length()); } else { - text_editor->set_line(line, line_text.insert(text_editor->get_first_non_whitespace_column(line), delimiter)); - } - } - - // Readjust carets and selections. - int caret_i = 0; - int selection_i = 0; - int offset = (is_commented ? -1 : 1) * delimiter.length(); - for (const int &c2 : caret_edit_order) { - bool is_line_selection = text_editor->has_selection(c2) && text_editor->get_selection_from_line(c2) < text_editor->get_selection_to_line(c2); - if (text_editor->get_caret_line(c2) >= from && text_editor->get_caret_line(c2) <= to) { - int caret_col = caret_cols[caret_i++]; - caret_col += (is_line_selection && caret_col == 0) ? 0 : offset; - text_editor->set_caret_column(caret_col, c2 == 0, c2); - } - if (text_editor->has_selection(c2) && text_editor->get_selection_to_line(c2) >= from && text_editor->get_selection_to_line(c2) <= to) { - int from_col = text_editor->get_selection_from_column(c2); - from_col += (is_line_selection && from_col == 0) ? 0 : offset; - int to_col = selection_to_cols[selection_i++]; - to_col += (to_col == 0) ? 0 : offset; - text_editor->select( - text_editor->get_selection_from_line(c2), from_col, - text_editor->get_selection_to_line(c2), to_col, c2); + text_editor->insert_text(delimiter, line, text_editor->get_first_non_whitespace_column(line)); } } } - text_editor->merge_overlapping_carets(); + + text_editor->end_multicaret_edit(); text_editor->end_complex_operation(); - text_editor->queue_redraw(); } void CodeTextEditor::goto_line(int p_line) { @@ -1815,22 +1490,6 @@ void CodeTextEditor::_toggle_scripts_pressed() { update_toggle_scripts_button(); } -int CodeTextEditor::_get_affected_lines_from(int p_caret) { - return text_editor->has_selection(p_caret) ? text_editor->get_selection_from_line(p_caret) : text_editor->get_caret_line(p_caret); -} - -int CodeTextEditor::_get_affected_lines_to(int p_caret) { - if (!text_editor->has_selection(p_caret)) { - return text_editor->get_caret_line(p_caret); - } - int line = text_editor->get_selection_to_line(p_caret); - // Don't affect a line with no selected characters. - if (text_editor->get_selection_to_column(p_caret) == 0) { - line--; - } - return line; -} - void CodeTextEditor::_error_pressed(const Ref<InputEvent> &p_event) { Ref<InputEventMouseButton> mb = p_event; if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { @@ -1879,13 +1538,12 @@ void CodeTextEditor::set_warning_count(int p_warning_count) { } void CodeTextEditor::toggle_bookmark() { - Vector<int> caret_edit_order = text_editor->get_caret_index_edit_order(); - caret_edit_order.reverse(); + Vector<int> sorted_carets = text_editor->get_sorted_carets(); int last_line = -1; - for (const int &c : caret_edit_order) { - int from = text_editor->has_selection(c) ? text_editor->get_selection_from_line(c) : text_editor->get_caret_line(c); + for (const int &c : sorted_carets) { + int from = text_editor->get_selection_from_line(c); from += from == last_line ? 1 : 0; - int to = text_editor->has_selection(c) ? text_editor->get_selection_to_line(c) : text_editor->get_caret_line(c); + int to = text_editor->get_selection_to_line(c); if (to < from) { continue; } diff --git a/editor/code_editor.h b/editor/code_editor.h index c36eedb580..75a2a68d58 100644 --- a/editor/code_editor.h +++ b/editor/code_editor.h @@ -207,9 +207,6 @@ class CodeTextEditor : public VBoxContainer { void _toggle_scripts_pressed(); - int _get_affected_lines_from(int p_caret); - int _get_affected_lines_to(int p_caret); - protected: virtual void _load_theme_settings() {} virtual void _validate_script() {} @@ -238,11 +235,6 @@ public: void set_indent_using_spaces(bool p_use_spaces); - void move_lines_up(); - void move_lines_down(); - void delete_lines(); - void duplicate_selection(); - /// Toggle inline comment on currently selected lines, or on current line if nothing is selected, /// by adding or removing comment delimiter void toggle_inline_comment(const String &delimiter); diff --git a/editor/editor_audio_buses.cpp b/editor/editor_audio_buses.cpp index b4e9faa4fd..658bc33ddc 100644 --- a/editor/editor_audio_buses.cpp +++ b/editor/editor_audio_buses.cpp @@ -87,9 +87,14 @@ void EditorAudioBus::_notification(int p_what) { disabled_vu = get_editor_theme_icon(SNAME("BusVuFrozen")); - Color solo_color = EditorThemeManager::is_dark_theme() ? Color(1.0, 0.89, 0.22) : Color(1.0, 0.92, 0.44); - Color mute_color = EditorThemeManager::is_dark_theme() ? Color(1.0, 0.16, 0.16) : Color(1.0, 0.44, 0.44); - Color bypass_color = EditorThemeManager::is_dark_theme() ? Color(0.13, 0.8, 1.0) : Color(0.44, 0.87, 1.0); + Color solo_color = EditorThemeManager::is_dark_theme() ? Color(1.0, 0.89, 0.22) : Color(1.9, 1.74, 0.83); + Color mute_color = EditorThemeManager::is_dark_theme() ? Color(1.0, 0.16, 0.16) : Color(2.35, 1.03, 1.03); + Color bypass_color = EditorThemeManager::is_dark_theme() ? Color(0.13, 0.8, 1.0) : Color(1.03, 2.04, 2.35); + float darkening_factor = EditorThemeManager::is_dark_theme() ? 0.15 : 0.65; + + Ref<StyleBoxFlat>(solo->get_theme_stylebox("pressed"))->set_border_color(solo_color.darkened(darkening_factor)); + Ref<StyleBoxFlat>(mute->get_theme_stylebox("pressed"))->set_border_color(mute_color.darkened(darkening_factor)); + Ref<StyleBoxFlat>(bypass->get_theme_stylebox("pressed"))->set_border_color(bypass_color.darkened(darkening_factor)); solo->set_icon(get_editor_theme_icon(SNAME("AudioBusSolo"))); solo->add_theme_color_override("icon_pressed_color", solo_color); @@ -835,7 +840,13 @@ EditorAudioBus::EditorAudioBus(EditorAudioBuses *p_buses, bool p_is_master) { child->add_theme_style_override("normal", sbempty); child->add_theme_style_override("hover", sbempty); child->add_theme_style_override("focus", sbempty); - child->add_theme_style_override("pressed", sbempty); + + Ref<StyleBoxFlat> sbflat = memnew(StyleBoxFlat); + sbflat->set_content_margin_all(0); + sbflat->set_bg_color(Color(1, 1, 1, 0)); + sbflat->set_border_width(Side::SIDE_BOTTOM, Math::round(3 * EDSCALE)); + child->add_theme_style_override("pressed", sbflat); + child->end_bulk_theme_override(); } diff --git a/editor/editor_dock_manager.cpp b/editor/editor_dock_manager.cpp index b6250671ee..06dd33d8ab 100644 --- a/editor/editor_dock_manager.cpp +++ b/editor/editor_dock_manager.cpp @@ -147,7 +147,6 @@ void EditorDockManager::_update_layout() { if (!dock_context_popup->is_inside_tree() || EditorNode::get_singleton()->is_exiting()) { return; } - EditorNode::get_singleton()->edit_current(); dock_context_popup->docks_updated(); _update_docks_menu(); EditorNode::get_singleton()->save_editor_layout_delayed(); diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp index 41396a2ed1..f1e487d79b 100644 --- a/editor/editor_inspector.cpp +++ b/editor/editor_inspector.cpp @@ -4024,14 +4024,16 @@ void EditorInspector::_notification(int p_what) { } break; case NOTIFICATION_PREDELETE: { - edit(nullptr); //just in case + if (EditorNode::get_singleton() && !EditorNode::get_singleton()->is_exiting()) { + // Don't need to clean up if exiting, and object may already be freed. + edit(nullptr); + } } break; case NOTIFICATION_EXIT_TREE: { if (!sub_inspector) { get_tree()->disconnect("node_removed", callable_mp(this, &EditorInspector::_node_removed)); } - edit(nullptr); } break; case NOTIFICATION_VISIBILITY_CHANGED: { diff --git a/editor/editor_resource_picker.cpp b/editor/editor_resource_picker.cpp index 9e4bf2b7d9..e082366c44 100644 --- a/editor/editor_resource_picker.cpp +++ b/editor/editor_resource_picker.cpp @@ -488,8 +488,8 @@ void EditorResourcePicker::set_create_options(Object *p_menu_node) { if (!base_type.is_empty()) { int idx = 0; - HashSet<StringName> allowed_types; - _get_allowed_types(false, &allowed_types); + _ensure_allowed_types(); + HashSet<StringName> allowed_types = allowed_types_without_convert; for (const StringName &E : allowed_types) { const String &t = E; @@ -593,23 +593,29 @@ static void _add_allowed_type(const StringName &p_type, HashSet<StringName> *p_v } } -void EditorResourcePicker::_get_allowed_types(bool p_with_convert, HashSet<StringName> *p_vector) const { +void EditorResourcePicker::_ensure_allowed_types() const { + if (!allowed_types_without_convert.is_empty()) { + return; + } + Vector<String> allowed_types = base_type.split(","); int size = allowed_types.size(); for (int i = 0; i < size; i++) { - String base = allowed_types[i].strip_edges(); + const String base = allowed_types[i].strip_edges(); + _add_allowed_type(base, &allowed_types_without_convert); + } - _add_allowed_type(base, p_vector); + allowed_types_with_convert = HashSet<StringName>(allowed_types_without_convert); - if (p_with_convert) { - if (base == "BaseMaterial3D") { - p_vector->insert("Texture2D"); - } else if (base == "ShaderMaterial") { - p_vector->insert("Shader"); - } else if (base == "Texture2D") { - p_vector->insert("Image"); - } + for (int i = 0; i < size; i++) { + const String base = allowed_types[i].strip_edges(); + if (base == "BaseMaterial3D") { + allowed_types_with_convert.insert("Texture2D"); + } else if (base == "ShaderMaterial") { + allowed_types_with_convert.insert("Shader"); + } else if (base == "Texture2D") { + allowed_types_with_convert.insert("Image"); } } } @@ -645,8 +651,8 @@ bool EditorResourcePicker::_is_drop_valid(const Dictionary &p_drag_data) const { } } - HashSet<StringName> allowed_types; - _get_allowed_types(true, &allowed_types); + _ensure_allowed_types(); + HashSet<StringName> allowed_types = allowed_types_with_convert; if (res.is_valid()) { String res_type = _get_resource_type(res); @@ -713,8 +719,8 @@ void EditorResourcePicker::drop_data_fw(const Point2 &p_point, const Variant &p_ } if (dropped_resource.is_valid()) { - HashSet<StringName> allowed_types; - _get_allowed_types(false, &allowed_types); + _ensure_allowed_types(); + HashSet<StringName> allowed_types = allowed_types_without_convert; String res_type = _get_resource_type(dropped_resource); @@ -835,8 +841,8 @@ void EditorResourcePicker::set_base_type(const String &p_base_type) { // There is a possibility that the new base type is conflicting with the existing value. // Keep the value, but warn the user that there is a potential mistake. if (!base_type.is_empty() && edited_resource.is_valid()) { - HashSet<StringName> allowed_types; - _get_allowed_types(true, &allowed_types); + _ensure_allowed_types(); + HashSet<StringName> allowed_types = allowed_types_with_convert; StringName custom_class; bool is_custom = false; @@ -857,8 +863,8 @@ String EditorResourcePicker::get_base_type() const { } Vector<String> EditorResourcePicker::get_allowed_types() const { - HashSet<StringName> allowed_types; - _get_allowed_types(false, &allowed_types); + _ensure_allowed_types(); + HashSet<StringName> allowed_types = allowed_types_without_convert; Vector<String> types; types.resize(allowed_types.size()); @@ -881,8 +887,8 @@ void EditorResourcePicker::set_edited_resource(Ref<Resource> p_resource) { } if (!base_type.is_empty()) { - HashSet<StringName> allowed_types; - _get_allowed_types(true, &allowed_types); + _ensure_allowed_types(); + HashSet<StringName> allowed_types = allowed_types_with_convert; StringName custom_class; bool is_custom = false; diff --git a/editor/editor_resource_picker.h b/editor/editor_resource_picker.h index 333d9c9915..28229e6b37 100644 --- a/editor/editor_resource_picker.h +++ b/editor/editor_resource_picker.h @@ -52,6 +52,8 @@ class EditorResourcePicker : public HBoxContainer { bool dropping = false; Vector<String> inheritors_array; + mutable HashSet<StringName> allowed_types_without_convert; + mutable HashSet<StringName> allowed_types_with_convert; Button *assign_button = nullptr; TextureRect *preview_rect = nullptr; @@ -97,7 +99,7 @@ class EditorResourcePicker : public HBoxContainer { void _button_input(const Ref<InputEvent> &p_event); String _get_resource_type(const Ref<Resource> &p_resource) const; - void _get_allowed_types(bool p_with_convert, HashSet<StringName> *p_vector) const; + void _ensure_allowed_types() const; bool _is_drop_valid(const Dictionary &p_drag_data) const; bool _is_type_valid(const String &p_type_name, const HashSet<StringName> &p_allowed_types) const; diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index a642f35d6f..561edcf8bf 100644 --- a/editor/plugins/script_text_editor.cpp +++ b/editor/plugins/script_text_editor.cpp @@ -284,8 +284,7 @@ void ScriptTextEditor::_warning_clicked(const Variant &p_line) { if (prev_line.contains("@warning_ignore")) { const int closing_bracket_idx = prev_line.find(")"); const String text_to_insert = ", " + code.quote(quote_style); - prev_line = prev_line.insert(closing_bracket_idx, text_to_insert); - text_editor->set_line(line - 1, prev_line); + text_editor->insert_text(text_to_insert, line - 1, closing_bracket_idx); } else { const int indent = text_editor->get_indent_level(line) / text_editor->get_indent_size(); String annotation_indent; @@ -352,22 +351,26 @@ void ScriptTextEditor::add_callback(const String &p_function, const PackedString if (!language->can_make_function()) { return; } - + code_editor->get_text_editor()->begin_complex_operation(); + code_editor->get_text_editor()->remove_secondary_carets(); + code_editor->get_text_editor()->deselect(); String code = code_editor->get_text_editor()->get_text(); int pos = 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(); - pos = code_editor->get_text_editor()->get_line_count() + 2; + // Function does not exist, create it at the end of the file. + int last_line = code_editor->get_text_editor()->get_line_count() - 1; String func = language->make_function("", p_function, p_args); - //code=code+func; - code_editor->get_text_editor()->set_caret_line(pos + 1); - code_editor->get_text_editor()->set_caret_column(1000000); //none shall be that big - code_editor->get_text_editor()->insert_text_at_caret("\n\n" + func); + code_editor->get_text_editor()->insert_text("\n\n" + func, last_line, code_editor->get_text_editor()->get_line(last_line).length()); + pos = last_line + 3; + } + // Put caret on the line after the function, after the indent. + int indent_column = 1; + if (EDITOR_GET("text_editor/behavior/indent/type")) { + indent_column = EDITOR_GET("text_editor/behavior/indent/size"); } - code_editor->get_text_editor()->set_caret_line(pos); - code_editor->get_text_editor()->set_caret_column(1); + code_editor->get_text_editor()->set_caret_line(pos, true, true, -1); + code_editor->get_text_editor()->set_caret_column(indent_column); + code_editor->get_text_editor()->end_complex_operation(); } bool ScriptTextEditor::show_members_overview() { @@ -1335,10 +1338,10 @@ void ScriptTextEditor::_edit_option(int p_op) { callable_mp((Control *)tx, &Control::grab_focus).call_deferred(); } break; case EDIT_MOVE_LINE_UP: { - code_editor->move_lines_up(); + code_editor->get_text_editor()->move_lines_up(); } break; case EDIT_MOVE_LINE_DOWN: { - code_editor->move_lines_down(); + code_editor->get_text_editor()->move_lines_down(); } break; case EDIT_INDENT: { Ref<Script> scr = script; @@ -1355,24 +1358,16 @@ void ScriptTextEditor::_edit_option(int p_op) { tx->unindent_lines(); } break; case EDIT_DELETE_LINE: { - code_editor->delete_lines(); + code_editor->get_text_editor()->delete_lines(); } break; case EDIT_DUPLICATE_SELECTION: { - code_editor->duplicate_selection(); + code_editor->get_text_editor()->duplicate_selection(); } break; case EDIT_DUPLICATE_LINES: { code_editor->get_text_editor()->duplicate_lines(); } break; case EDIT_TOGGLE_FOLD_LINE: { - int prev_line = -1; - for (int caret_idx : tx->get_caret_index_edit_order()) { - int line_idx = tx->get_caret_line(caret_idx); - if (line_idx != prev_line) { - tx->toggle_foldable_line(line_idx); - prev_line = line_idx; - } - } - tx->queue_redraw(); + tx->toggle_foldable_lines_at_carets(); } break; case EDIT_FOLD_ALL_LINES: { tx->fold_all_lines(); @@ -1399,24 +1394,34 @@ void ScriptTextEditor::_edit_option(int p_op) { } tx->begin_complex_operation(); - int begin, end; + tx->begin_multicaret_edit(); + int begin = tx->get_line_count() - 1, end = 0; if (tx->has_selection()) { - begin = tx->get_selection_from_line(); - end = tx->get_selection_to_line(); - // ignore if the cursor is not past the first column - if (tx->get_selection_to_column() == 0) { - end--; + // Auto indent all lines that have a caret or selection on it. + Vector<Point2i> line_ranges = tx->get_line_ranges_from_carets(); + for (Point2i line_range : line_ranges) { + scr->get_language()->auto_indent_code(text, line_range.x, line_range.y); + if (line_range.x < begin) { + begin = line_range.x; + } + if (line_range.y > end) { + end = line_range.y; + } } } else { + // Auto indent entire text. begin = 0; end = tx->get_line_count() - 1; + scr->get_language()->auto_indent_code(text, begin, end); } - scr->get_language()->auto_indent_code(text, begin, end); + + // Apply auto indented code. Vector<String> lines = text.split("\n"); for (int i = begin; i <= end; ++i) { tx->set_line(i, lines[i]); } + tx->end_multicaret_edit(); tx->end_complex_operation(); } break; case EDIT_TRIM_TRAILING_WHITESAPCE: { @@ -1515,13 +1520,12 @@ void ScriptTextEditor::_edit_option(int p_op) { code_editor->remove_all_bookmarks(); } break; case DEBUG_TOGGLE_BREAKPOINT: { - Vector<int> caret_edit_order = tx->get_caret_index_edit_order(); - caret_edit_order.reverse(); + Vector<int> sorted_carets = tx->get_sorted_carets(); int last_line = -1; - for (const int &c : caret_edit_order) { - int from = tx->has_selection(c) ? tx->get_selection_from_line(c) : tx->get_caret_line(c); + for (const int &c : sorted_carets) { + int from = tx->get_selection_from_line(c); from += from == last_line ? 1 : 0; - int to = tx->has_selection(c) ? tx->get_selection_to_line(c) : tx->get_caret_line(c); + int to = tx->get_selection_to_line(c); if (to < from) { continue; } @@ -2008,45 +2012,32 @@ void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) { tx->apply_ime(); Point2i pos = tx->get_line_column_at_pos(local_pos); - int row = pos.y; - int col = pos.x; + int mouse_line = pos.y; + int mouse_column = pos.x; tx->set_move_caret_on_right_click_enabled(EDITOR_GET("text_editor/behavior/navigation/move_caret_on_right_click")); - int caret_clicked = -1; + int selection_clicked = -1; if (tx->is_move_caret_on_right_click_enabled()) { - if (tx->has_selection()) { - for (int i = 0; i < tx->get_caret_count(); i++) { - int from_line = tx->get_selection_from_line(i); - int to_line = tx->get_selection_to_line(i); - int from_column = tx->get_selection_from_column(i); - int to_column = tx->get_selection_to_column(i); - - if (row >= from_line && row <= to_line && (row != from_line || col >= from_column) && (row != to_line || col <= to_column)) { - // Right click in one of the selected text - caret_clicked = i; - break; - } - } - } - if (caret_clicked < 0) { + selection_clicked = tx->get_selection_at_line_column(mouse_line, mouse_column, true); + if (selection_clicked < 0) { tx->deselect(); tx->remove_secondary_carets(); - caret_clicked = 0; - tx->set_caret_line(row, false, false); - tx->set_caret_column(col); + selection_clicked = 0; + tx->set_caret_line(mouse_line, false, false, -1); + tx->set_caret_column(mouse_column); } } 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(caret_clicked); + word_at_pos = tx->get_word_under_caret(selection_clicked); } if (word_at_pos.is_empty()) { - word_at_pos = tx->get_selected_text(caret_clicked); + word_at_pos = tx->get_selected_text(selection_clicked); } bool has_color = (word_at_pos == "Color"); - bool foldable = tx->can_fold_line(row) || tx->is_line_folded(row); + bool foldable = tx->can_fold_line(mouse_line) || tx->is_line_folded(mouse_line); bool open_docs = false; bool goto_definition = false; @@ -2064,9 +2055,9 @@ void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) { } if (has_color) { - String line = tx->get_line(row); - color_position.x = row; - color_position.y = col; + String line = tx->get_line(mouse_line); + color_position.x = mouse_line; + color_position.y = mouse_column; int begin = -1; int end = -1; @@ -2076,7 +2067,7 @@ void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) { COLOR_NAME, // Color.COLOR_NAME } expression_pattern = NOT_PARSED; - for (int i = col; i < line.length(); i++) { + for (int i = mouse_column; i < line.length(); i++) { if (line[i] == '(') { if (expression_pattern == NOT_PARSED) { begin = i; @@ -2155,7 +2146,6 @@ void ScriptTextEditor::_color_changed(const Color &p_color) { code_editor->get_text_editor()->begin_complex_operation(); code_editor->get_text_editor()->set_line(color_position.x, line_with_replaced_args); code_editor->get_text_editor()->end_complex_operation(); - code_editor->get_text_editor()->queue_redraw(); } void ScriptTextEditor::_prepare_edit_menu() { diff --git a/editor/plugins/text_editor.cpp b/editor/plugins/text_editor.cpp index 6070e08739..e19d9d933a 100644 --- a/editor/plugins/text_editor.cpp +++ b/editor/plugins/text_editor.cpp @@ -380,10 +380,10 @@ void TextEditor::_edit_option(int p_op) { callable_mp((Control *)tx, &Control::grab_focus).call_deferred(); } break; case EDIT_MOVE_LINE_UP: { - code_editor->move_lines_up(); + code_editor->get_text_editor()->move_lines_up(); } break; case EDIT_MOVE_LINE_DOWN: { - code_editor->move_lines_down(); + code_editor->get_text_editor()->move_lines_down(); } break; case EDIT_INDENT: { tx->indent_lines(); @@ -392,24 +392,16 @@ void TextEditor::_edit_option(int p_op) { tx->unindent_lines(); } break; case EDIT_DELETE_LINE: { - code_editor->delete_lines(); + code_editor->get_text_editor()->delete_lines(); } break; case EDIT_DUPLICATE_SELECTION: { - code_editor->duplicate_selection(); + code_editor->get_text_editor()->duplicate_selection(); } break; case EDIT_DUPLICATE_LINES: { code_editor->get_text_editor()->duplicate_lines(); } break; case EDIT_TOGGLE_FOLD_LINE: { - int previous_line = -1; - for (int caret_idx : tx->get_caret_index_edit_order()) { - int line_idx = tx->get_caret_line(caret_idx); - if (line_idx != previous_line) { - tx->toggle_foldable_line(line_idx); - previous_line = line_idx; - } - } - tx->queue_redraw(); + tx->toggle_foldable_lines_at_carets(); } break; case EDIT_FOLD_ALL_LINES: { tx->fold_all_lines(); @@ -531,7 +523,7 @@ void TextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) { } } if (!tx->has_selection()) { - tx->set_caret_line(row, true, false); + tx->set_caret_line(row, true, false, -1); tx->set_caret_column(col); } } diff --git a/editor/plugins/text_shader_editor.cpp b/editor/plugins/text_shader_editor.cpp index 40bd51a442..83a1700306 100644 --- a/editor/plugins/text_shader_editor.cpp +++ b/editor/plugins/text_shader_editor.cpp @@ -653,10 +653,10 @@ void TextShaderEditor::_menu_option(int p_option) { code_editor->get_text_editor()->select_all(); } break; case EDIT_MOVE_LINE_UP: { - code_editor->move_lines_up(); + code_editor->get_text_editor()->move_lines_up(); } break; case EDIT_MOVE_LINE_DOWN: { - code_editor->move_lines_down(); + code_editor->get_text_editor()->move_lines_down(); } break; case EDIT_INDENT: { if (shader.is_null() && shader_inc.is_null()) { @@ -671,10 +671,10 @@ void TextShaderEditor::_menu_option(int p_option) { code_editor->get_text_editor()->unindent_lines(); } break; case EDIT_DELETE_LINE: { - code_editor->delete_lines(); + code_editor->get_text_editor()->delete_lines(); } break; case EDIT_DUPLICATE_SELECTION: { - code_editor->duplicate_selection(); + code_editor->get_text_editor()->duplicate_selection(); } break; case EDIT_DUPLICATE_LINES: { code_editor->get_text_editor()->duplicate_lines(); @@ -1010,7 +1010,7 @@ void TextShaderEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) { } } if (!tx->has_selection()) { - tx->set_caret_line(row, true, false); + tx->set_caret_line(row, true, false, -1); tx->set_caret_column(col); } } diff --git a/editor/plugins/visual_shader_editor_plugin.cpp b/editor/plugins/visual_shader_editor_plugin.cpp index 0dd845270a..1dcbaf444b 100644 --- a/editor/plugins/visual_shader_editor_plugin.cpp +++ b/editor/plugins/visual_shader_editor_plugin.cpp @@ -1002,7 +1002,7 @@ void VisualShaderGraphPlugin::add_node(VisualShader::Type p_type, int p_id, bool button->hide(); } - if (i == 0 && custom_editor) { + if (j == 0 && custom_editor) { hb->add_child(custom_editor); custom_editor->set_h_size_flags(Control::SIZE_EXPAND_FILL); } else { @@ -1019,22 +1019,22 @@ void VisualShaderGraphPlugin::add_node(VisualShader::Type p_type, int p_id, bool type_box->add_item(TTR("Boolean")); type_box->add_item(TTR("Transform")); type_box->add_item(TTR("Sampler")); - type_box->select(group_node->get_input_port_type(i)); + type_box->select(group_node->get_input_port_type(j)); type_box->set_custom_minimum_size(Size2(100 * EDSCALE, 0)); - type_box->connect("item_selected", callable_mp(editor, &VisualShaderEditor::_change_input_port_type).bind(p_id, i), CONNECT_DEFERRED); + type_box->connect("item_selected", callable_mp(editor, &VisualShaderEditor::_change_input_port_type).bind(p_id, j), CONNECT_DEFERRED); LineEdit *name_box = memnew(LineEdit); hb->add_child(name_box); name_box->set_custom_minimum_size(Size2(65 * EDSCALE, 0)); name_box->set_h_size_flags(Control::SIZE_EXPAND_FILL); name_box->set_text(name_left); - name_box->connect("text_submitted", callable_mp(editor, &VisualShaderEditor::_change_input_port_name).bind(name_box, p_id, i), CONNECT_DEFERRED); - name_box->connect("focus_exited", callable_mp(editor, &VisualShaderEditor::_port_name_focus_out).bind(name_box, p_id, i, false), CONNECT_DEFERRED); + name_box->connect("text_submitted", callable_mp(editor, &VisualShaderEditor::_change_input_port_name).bind(name_box, p_id, j), CONNECT_DEFERRED); + name_box->connect("focus_exited", callable_mp(editor, &VisualShaderEditor::_port_name_focus_out).bind(name_box, p_id, j, false), CONNECT_DEFERRED); Button *remove_btn = memnew(Button); remove_btn->set_icon(EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("Remove"), EditorStringName(EditorIcons))); remove_btn->set_tooltip_text(TTR("Remove") + " " + name_left); - remove_btn->connect("pressed", callable_mp(editor, &VisualShaderEditor::_remove_input_port).bind(p_id, i), CONNECT_DEFERRED); + remove_btn->connect("pressed", callable_mp(editor, &VisualShaderEditor::_remove_input_port).bind(p_id, j), CONNECT_DEFERRED); hb->add_child(remove_btn); } else { Label *label = memnew(Label); @@ -1043,7 +1043,7 @@ void VisualShaderGraphPlugin::add_node(VisualShader::Type p_type, int p_id, bool label->add_theme_style_override("normal", editor->get_theme_stylebox(SNAME("label_style"), SNAME("VShaderEditor"))); //more compact hb->add_child(label); - if (vsnode->is_input_port_default(i, mode) && !port_left_used) { + if (vsnode->is_input_port_default(j, mode) && !port_left_used) { Label *hint_label = memnew(Label); hint_label->set_text(TTR("[default]")); hint_label->add_theme_color_override("font_color", editor->get_theme_color(SNAME("font_readonly_color"), SNAME("TextEdit"))); diff --git a/methods.py b/methods.py index 30c7cb0331..0c29632f10 100644 --- a/methods.py +++ b/methods.py @@ -34,39 +34,41 @@ class ANSI(Enum): internal value, or an empty string in a non-colorized scope. """ - GRAY = "\x1b[0;30m" - RED = "\x1b[0;31m" - GREEN = "\x1b[0;32m" - YELLOW = "\x1b[0;33m" - BLUE = "\x1b[0;34m" - PURPLE = "\x1b[0;35m" - CYAN = "\x1b[0;36m" - WHITE = "\x1b[0;37m" - - BOLD_GRAY = "\x1b[1;90m" - BOLD_RED = "\x1b[1;91m" - BOLD_GREEN = "\x1b[1;92m" - BOLD_YELLOW = "\x1b[1;93m" - BOLD_BLUE = "\x1b[1;94m" - BOLD_PURPLE = "\x1b[1;95m" - BOLD_CYAN = "\x1b[1;96m" - BOLD_WHITE = "\x1b[1;97m" - RESET = "\x1b[0m" - def __str__(self): + BOLD = "\x1b[1m" + ITALIC = "\x1b[3m" + UNDERLINE = "\x1b[4m" + STRIKETHROUGH = "\x1b[9m" + REGULAR = "\x1b[22;23;24;29m" + + BLACK = "\x1b[30m" + RED = "\x1b[31m" + GREEN = "\x1b[32m" + YELLOW = "\x1b[33m" + BLUE = "\x1b[34m" + MAGENTA = "\x1b[35m" + CYAN = "\x1b[36m" + WHITE = "\x1b[37m" + + PURPLE = "\x1b[38;5;93m" + PINK = "\x1b[38;5;206m" + ORANGE = "\x1b[38;5;214m" + GRAY = "\x1b[38;5;244m" + + def __str__(self) -> str: global _colorize - return self.value if _colorize else "" + return str(self.value) if _colorize else "" def print_warning(*values: object) -> None: """Prints a warning message with formatting.""" - print(f"{ANSI.BOLD_YELLOW}WARNING:{ANSI.YELLOW}", *values, ANSI.RESET, file=sys.stderr) + print(f"{ANSI.YELLOW}{ANSI.BOLD}WARNING:{ANSI.REGULAR}", *values, ANSI.RESET, file=sys.stderr) def print_error(*values: object) -> None: """Prints an error message with formatting.""" - print(f"{ANSI.BOLD_RED}ERROR:{ANSI.RED}", *values, ANSI.RESET, file=sys.stderr) + print(f"{ANSI.RED}{ANSI.BOLD}ERROR:{ANSI.REGULAR}", *values, ANSI.RESET, file=sys.stderr) def add_source_files_orig(self, sources, files, allow_gen=False): @@ -647,33 +649,33 @@ def use_windows_spawn_fix(self, platform=None): def no_verbose(env): - colors = [ANSI.BLUE, ANSI.BOLD_BLUE, ANSI.RESET] + colors = [ANSI.BLUE, ANSI.BOLD, ANSI.REGULAR, ANSI.RESET] # There is a space before "..." to ensure that source file names can be # Ctrl + clicked in the VS Code terminal. - compile_source_message = "{0}Compiling {1}$SOURCE{0} ...{2}".format(*colors) - java_compile_source_message = "{0}Compiling {1}$SOURCE{0} ...{2}".format(*colors) - compile_shared_source_message = "{0}Compiling shared {1}$SOURCE{0} ...{2}".format(*colors) - link_program_message = "{0}Linking Program {1}$TARGET{0} ...{2}".format(*colors) - link_library_message = "{0}Linking Static Library {1}$TARGET{0} ...{2}".format(*colors) - ranlib_library_message = "{0}Ranlib Library {1}$TARGET{0} ...{2}".format(*colors) - link_shared_library_message = "{0}Linking Shared Library {1}$TARGET{0} ...{2}".format(*colors) - java_library_message = "{0}Creating Java Archive {1}$TARGET{0} ...{2}".format(*colors) - compiled_resource_message = "{0}Creating Compiled Resource {1}$TARGET{0} ...{2}".format(*colors) - generated_file_message = "{0}Generating {1}$TARGET{0} ...{2}".format(*colors) - - env.Append(CXXCOMSTR=compile_source_message) - env.Append(CCCOMSTR=compile_source_message) - env.Append(SHCCCOMSTR=compile_shared_source_message) - env.Append(SHCXXCOMSTR=compile_shared_source_message) - env.Append(ARCOMSTR=link_library_message) - env.Append(RANLIBCOMSTR=ranlib_library_message) - env.Append(SHLINKCOMSTR=link_shared_library_message) - env.Append(LINKCOMSTR=link_program_message) - env.Append(JARCOMSTR=java_library_message) - env.Append(JAVACCOMSTR=java_compile_source_message) - env.Append(RCCOMSTR=compiled_resource_message) - env.Append(GENCOMSTR=generated_file_message) + compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors) + java_compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors) + compile_shared_source_message = "{}Compiling shared {}$SOURCE{} ...{}".format(*colors) + link_program_message = "{}Linking Program {}$TARGET{} ...{}".format(*colors) + link_library_message = "{}Linking Static Library {}$TARGET{} ...{}".format(*colors) + ranlib_library_message = "{}Ranlib Library {}$TARGET{} ...{}".format(*colors) + link_shared_library_message = "{}Linking Shared Library {}$TARGET{} ...{}".format(*colors) + java_library_message = "{}Creating Java Archive {}$TARGET{} ...{}".format(*colors) + compiled_resource_message = "{}Creating Compiled Resource {}$TARGET{} ...{}".format(*colors) + generated_file_message = "{}Generating {}$TARGET{} ...{}".format(*colors) + + env["CXXCOMSTR"] = compile_source_message + env["CCCOMSTR"] = compile_source_message + env["SHCCCOMSTR"] = compile_shared_source_message + env["SHCXXCOMSTR"] = compile_shared_source_message + env["ARCOMSTR"] = link_library_message + env["RANLIBCOMSTR"] = ranlib_library_message + env["SHLINKCOMSTR"] = link_shared_library_message + env["LINKCOMSTR"] = link_program_message + env["JARCOMSTR"] = java_library_message + env["JAVACCOMSTR"] = java_compile_source_message + env["RCCOMSTR"] = compiled_resource_message + env["GENCOMSTR"] = generated_file_message def detect_visual_c_compiler_version(tools_env): diff --git a/misc/dist/html/full-size.html b/misc/dist/html/full-size.html index 8ae25362f8..874fe2695e 100644 --- a/misc/dist/html/full-size.html +++ b/misc/dist/html/full-size.html @@ -2,135 +2,95 @@ <html lang="en"> <head> <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, user-scalable=no"> + <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0"> <title>$GODOT_PROJECT_NAME</title> <style> -body { - touch-action: none; +html, body, #canvas { margin: 0; - border: 0 none; padding: 0; - text-align: center; + border: 0; +} + +body { + color: white; background-color: black; + overflow: hidden; + touch-action: none; } #canvas { display: block; - margin: 0; - color: white; } #canvas:focus { outline: none; } -.godot { - font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif; - color: #e0e0e0; - background-color: #3b3943; - background-image: linear-gradient(to bottom, #403e48, #35333c); - border: 1px solid #45434e; - box-shadow: 0 0 1px 1px #2f2d35; -} - -/* Status display */ - -#status { +#status, #status-splash, #status-progress { position: absolute; left: 0; - top: 0; right: 0; +} + +#status, #status-splash { + top: 0; bottom: 0; +} + +#status { + background-color: #38363A; display: flex; + flex-direction: column; justify-content: center; align-items: center; - /* don't consume click events - make children visible explicitly */ visibility: hidden; } -#status-progress { - width: 366px; - height: 7px; - background-color: #38363A; - border: 1px solid #444246; - padding: 1px; - box-shadow: 0 0 2px 1px #1B1C22; - border-radius: 2px; - visibility: visible; -} - -@media only screen and (orientation:portrait) { - #status-progress { - width: 61.8%; - } +#status-splash { + max-height: 100%; + max-width: 100%; + margin: auto; } -#status-progress-inner { - height: 100%; - width: 0; - box-sizing: border-box; - transition: width 0.5s linear; - background-color: #202020; - border: 1px solid #222223; - box-shadow: 0 0 1px 1px #27282E; - border-radius: 3px; +#status-progress, #status-notice { + display: none; } -#status-indeterminate { - height: 42px; - visibility: visible; - position: relative; -} - -#status-indeterminate > div { - width: 4.5px; - height: 0; - border-style: solid; - border-width: 9px 3px 0 3px; - border-color: #2b2b2b transparent transparent transparent; - transform-origin: center 21px; - position: absolute; +#status-progress { + bottom: 10%; + width: 50%; + margin: 0 auto; } -#status-indeterminate > div:nth-child(1) { transform: rotate( 22.5deg); } -#status-indeterminate > div:nth-child(2) { transform: rotate( 67.5deg); } -#status-indeterminate > div:nth-child(3) { transform: rotate(112.5deg); } -#status-indeterminate > div:nth-child(4) { transform: rotate(157.5deg); } -#status-indeterminate > div:nth-child(5) { transform: rotate(202.5deg); } -#status-indeterminate > div:nth-child(6) { transform: rotate(247.5deg); } -#status-indeterminate > div:nth-child(7) { transform: rotate(292.5deg); } -#status-indeterminate > div:nth-child(8) { transform: rotate(337.5deg); } - #status-notice { - margin: 0 100px; + background-color: #5b3943; + border-radius: 0.5rem; + border: 1px solid #9b3943; + color: #e0e0e0; + font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif; line-height: 1.3; - visibility: visible; - padding: 4px 6px; - visibility: visible; + margin: 0 2rem; + overflow: hidden; + padding: 1rem; + text-align: center; + z-index: 1; } </style> $GODOT_HEAD_INCLUDE </head> <body> <canvas id="canvas"> - HTML5 canvas appears to be unsupported in the current browser.<br > - Please try updating or use a different browser. + Your browser does not support the canvas tag. </canvas> + + <noscript> + Your browser does not support JavaScript. + </noscript> + <div id="status"> - <div id="status-progress" style="display: none;" oncontextmenu="event.preventDefault();"> - <div id ="status-progress-inner"></div> - </div> - <div id="status-indeterminate" style="display: none;" oncontextmenu="event.preventDefault();"> - <div></div> - <div></div> - <div></div> - <div></div> - <div></div> - <div></div> - <div></div> - <div></div> - </div> - <div id="status-notice" class="godot" style="display: none;"></div> + <img id="status-splash" src="$GODOT_SPLASH" alt=""> + <progress id="status-progress"></progress> + <div id="status-notice"></div> </div> <script src="$GODOT_URL"></script> @@ -140,58 +100,25 @@ const GODOT_THREADS_ENABLED = $GODOT_THREADS_ENABLED; const engine = new Engine(GODOT_CONFIG); (function () { - const INDETERMINATE_STATUS_STEP_MS = 100; + const statusOverlay = document.getElementById('status'); const statusProgress = document.getElementById('status-progress'); - const statusProgressInner = document.getElementById('status-progress-inner'); - const statusIndeterminate = document.getElementById('status-indeterminate'); const statusNotice = document.getElementById('status-notice'); let initializing = true; - let statusMode = 'hidden'; - - let animationCallbacks = []; - function animate(time) { - animationCallbacks.forEach((callback) => callback(time)); - requestAnimationFrame(animate); - } - requestAnimationFrame(animate); - - function animateStatusIndeterminate(ms) { - const i = Math.floor((ms / INDETERMINATE_STATUS_STEP_MS) % 8); - if (statusIndeterminate.children[i].style.borderTopColor === '') { - Array.prototype.slice.call(statusIndeterminate.children).forEach((child) => { - child.style.borderTopColor = ''; - }); - statusIndeterminate.children[i].style.borderTopColor = '#dfdfdf'; - } - } + let statusMode = ''; function setStatusMode(mode) { if (statusMode === mode || !initializing) { return; } - [statusProgress, statusIndeterminate, statusNotice].forEach((elem) => { - elem.style.display = 'none'; - }); - animationCallbacks = animationCallbacks.filter(function (value) { - return (value !== animateStatusIndeterminate); - }); - switch (mode) { - case 'progress': - statusProgress.style.display = 'block'; - break; - case 'indeterminate': - statusIndeterminate.style.display = 'block'; - animationCallbacks.push(animateStatusIndeterminate); - break; - case 'notice': - statusNotice.style.display = 'block'; - break; - case 'hidden': - break; - default: - throw new Error('Invalid status mode'); + if (mode === 'hidden') { + statusOverlay.remove(); + initializing = false; + return; } + statusOverlay.style.visibility = 'visible'; + statusProgress.style.display = mode === 'progress' ? 'block' : 'none'; + statusNotice.style.display = mode === 'notice' ? 'block' : 'none'; statusMode = mode; } @@ -217,6 +144,7 @@ const engine = new Engine(GODOT_CONFIG); const missing = Engine.getMissingFeatures({ threads: GODOT_THREADS_ENABLED, }); + if (missing.length !== 0) { if (GODOT_CONFIG['serviceWorker'] && GODOT_CONFIG['ensureCrossOriginIsolationHeaders'] && 'serviceWorker' in navigator) { // There's a chance that installing the service worker would fix the issue @@ -242,25 +170,19 @@ const engine = new Engine(GODOT_CONFIG); displayFailureNotice(missingMsg + missing.join('\n')); } } else { - setStatusMode('indeterminate'); + setStatusMode('progress'); engine.startGame({ 'onProgress': function (current, total) { - if (total > 0) { - statusProgressInner.style.width = `${(current / total) * 100}%`; - setStatusMode('progress'); - if (current === total) { - // wait for progress bar animation - setTimeout(() => { - setStatusMode('indeterminate'); - }, 500); - } + if (current > 0 && total > 0) { + statusProgress.value = current; + statusProgress.max = total; } else { - setStatusMode('indeterminate'); + statusProgress.removeAttribute('value'); + statusProgress.removeAttribute('max'); } }, }).then(() => { setStatusMode('hidden'); - initializing = false; }, displayFailureNotice); } }()); diff --git a/misc/extension_api_validation/4.2-stable.expected b/misc/extension_api_validation/4.2-stable.expected index 4706ed37f0..c0731b18fc 100644 --- a/misc/extension_api_validation/4.2-stable.expected +++ b/misc/extension_api_validation/4.2-stable.expected @@ -315,3 +315,11 @@ Validate extension JSON: Error: Field 'classes/TextServer/methods/shaped_text_ge Validate extension JSON: Error: Field 'classes/TextServerExtension/methods/_shaped_text_get_word_breaks/arguments': size changed value in new API, from 2 to 3. Added optional argument. Compatibility method registered. + + +GH-86978 +-------- +Validate extension JSON: Error: Field 'classes/TextEdit/methods/set_selection_mode/arguments': size changed value in new API, from 4 to 1. + +Removed optional arguments set_selection_mode, use set_selection_origin_line/column instead. +Compatibility methods registered. diff --git a/modules/gdscript/gdscript_byte_codegen.cpp b/modules/gdscript/gdscript_byte_codegen.cpp index bfe090edb0..5a50bd8648 100644 --- a/modules/gdscript/gdscript_byte_codegen.cpp +++ b/modules/gdscript/gdscript_byte_codegen.cpp @@ -1196,23 +1196,49 @@ void GDScriptByteCodeGenerator::write_call_builtin_type_static(const Address &p_ } void GDScriptByteCodeGenerator::write_call_native_static(const Address &p_target, const StringName &p_class, const StringName &p_method, const Vector<Address> &p_arguments) { - bool is_validated = false; - MethodBind *method = ClassDB::get_method(p_class, p_method); - if (!is_validated) { - // Perform regular call. - append_opcode_and_argcount(GDScriptFunction::OPCODE_CALL_NATIVE_STATIC, p_arguments.size() + 1); - for (int i = 0; i < p_arguments.size(); i++) { - append(p_arguments[i]); + // Perform regular call. + append_opcode_and_argcount(GDScriptFunction::OPCODE_CALL_NATIVE_STATIC, p_arguments.size() + 1); + for (int i = 0; i < p_arguments.size(); i++) { + append(p_arguments[i]); + } + CallTarget ct = get_call_target(p_target); + append(ct.target); + append(method); + append(p_arguments.size()); + ct.cleanup(); + return; +} + +void GDScriptByteCodeGenerator::write_call_native_static_validated(const GDScriptCodeGenerator::Address &p_target, MethodBind *p_method, const Vector<GDScriptCodeGenerator::Address> &p_arguments) { + Variant::Type return_type = Variant::NIL; + bool has_return = p_method->has_return(); + + if (has_return) { + PropertyInfo return_info = p_method->get_return_info(); + return_type = return_info.type; + } + + CallTarget ct = get_call_target(p_target, return_type); + + if (has_return) { + Variant::Type temp_type = temporaries[ct.target.address].type; + if (temp_type != return_type) { + write_type_adjust(ct.target, return_type); } - CallTarget ct = get_call_target(p_target); - append(ct.target); - append(method); - append(p_arguments.size()); - ct.cleanup(); - return; } + + GDScriptFunction::Opcode code = p_method->has_return() ? GDScriptFunction::OPCODE_CALL_NATIVE_STATIC_VALIDATED_RETURN : GDScriptFunction::OPCODE_CALL_NATIVE_STATIC_VALIDATED_NO_RETURN; + append_opcode_and_argcount(code, 1 + p_arguments.size()); + + for (int i = 0; i < p_arguments.size(); i++) { + append(p_arguments[i]); + } + append(ct.target); + append(p_arguments.size()); + append(p_method); + ct.cleanup(); } void GDScriptByteCodeGenerator::write_call_method_bind(const Address &p_target, const Address &p_base, MethodBind *p_method, const Vector<Address> &p_arguments) { diff --git a/modules/gdscript/gdscript_byte_codegen.h b/modules/gdscript/gdscript_byte_codegen.h index 5a736b2554..34f56a2f5c 100644 --- a/modules/gdscript/gdscript_byte_codegen.h +++ b/modules/gdscript/gdscript_byte_codegen.h @@ -518,6 +518,7 @@ public: virtual void write_call_builtin_type(const Address &p_target, const Address &p_base, Variant::Type p_type, const StringName &p_method, const Vector<Address> &p_arguments) override; virtual void write_call_builtin_type_static(const Address &p_target, Variant::Type p_type, const StringName &p_method, const Vector<Address> &p_arguments) override; virtual void write_call_native_static(const Address &p_target, const StringName &p_class, const StringName &p_method, const Vector<Address> &p_arguments) override; + virtual void write_call_native_static_validated(const Address &p_target, MethodBind *p_method, const Vector<Address> &p_arguments) override; virtual void write_call_method_bind(const Address &p_target, const Address &p_base, MethodBind *p_method, const Vector<Address> &p_arguments) override; virtual void write_call_method_bind_validated(const Address &p_target, const Address &p_base, MethodBind *p_method, const Vector<Address> &p_arguments) override; virtual void write_call_self(const Address &p_target, const StringName &p_function_name, const Vector<Address> &p_arguments) override; diff --git a/modules/gdscript/gdscript_codegen.h b/modules/gdscript/gdscript_codegen.h index 4c33ed499a..c1c0b61395 100644 --- a/modules/gdscript/gdscript_codegen.h +++ b/modules/gdscript/gdscript_codegen.h @@ -131,6 +131,7 @@ public: virtual void write_call_builtin_type(const Address &p_target, const Address &p_base, Variant::Type p_type, const StringName &p_method, const Vector<Address> &p_arguments) = 0; virtual void write_call_builtin_type_static(const Address &p_target, Variant::Type p_type, const StringName &p_method, const Vector<Address> &p_arguments) = 0; virtual void write_call_native_static(const Address &p_target, const StringName &p_class, const StringName &p_method, const Vector<Address> &p_arguments) = 0; + virtual void write_call_native_static_validated(const Address &p_target, MethodBind *p_method, const Vector<Address> &p_arguments) = 0; virtual void write_call_method_bind(const Address &p_target, const Address &p_base, MethodBind *p_method, const Vector<Address> &p_arguments) = 0; virtual void write_call_method_bind_validated(const Address &p_target, const Address &p_base, MethodBind *p_method, const Vector<Address> &p_arguments) = 0; virtual void write_call_self(const Address &p_target, const StringName &p_function_name, const Vector<Address> &p_arguments) = 0; diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp index 734e37bc09..a8a7f3d9f7 100644 --- a/modules/gdscript/gdscript_compiler.cpp +++ b/modules/gdscript/gdscript_compiler.cpp @@ -673,7 +673,15 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code } else if (!call->is_super && subscript->base->type == GDScriptParser::Node::IDENTIFIER && call->function_name != SNAME("new") && ClassDB::class_exists(static_cast<GDScriptParser::IdentifierNode *>(subscript->base)->name) && !Engine::get_singleton()->has_singleton(static_cast<GDScriptParser::IdentifierNode *>(subscript->base)->name)) { // It's a static native method call. - gen->write_call_native_static(result, static_cast<GDScriptParser::IdentifierNode *>(subscript->base)->name, subscript->attribute->name, arguments); + StringName class_name = static_cast<GDScriptParser::IdentifierNode *>(subscript->base)->name; + MethodBind *method = ClassDB::get_method(class_name, subscript->attribute->name); + if (_can_use_validate_call(method, arguments)) { + // Exact arguments, use validated call. + gen->write_call_native_static_validated(result, method, arguments); + } else { + // Not exact arguments, use regular static call + gen->write_call_native_static(result, class_name, subscript->attribute->name, arguments); + } } else { GDScriptCodeGenerator::Address base = _parse_expression(codegen, r_error, subscript->base); if (r_error) { diff --git a/modules/gdscript/gdscript_disassembler.cpp b/modules/gdscript/gdscript_disassembler.cpp index c7873dcd52..8dd04c76dd 100644 --- a/modules/gdscript/gdscript_disassembler.cpp +++ b/modules/gdscript/gdscript_disassembler.cpp @@ -678,6 +678,50 @@ void GDScriptFunction::disassemble(const Vector<String> &p_code_lines) const { incr += 4 + argc; } break; + case OPCODE_CALL_NATIVE_STATIC_VALIDATED_RETURN: { + int instr_var_args = _code_ptr[++ip]; + text += "call native static method validated (return) "; + MethodBind *method = _methods_ptr[_code_ptr[ip + 2 + instr_var_args]]; + int argc = _code_ptr[ip + 1 + instr_var_args]; + text += DADDR(1 + argc) + " = "; + text += method->get_instance_class(); + text += "."; + text += method->get_name(); + text += "("; + for (int i = 0; i < argc; i++) { + if (i > 0) + text += ", "; + text += DADDR(1 + i); + } + text += ")"; + incr = 4 + argc; + } break; + + case OPCODE_CALL_NATIVE_STATIC_VALIDATED_NO_RETURN: { + int instr_var_args = _code_ptr[++ip]; + + text += "call native static method validated (no return) "; + + MethodBind *method = _methods_ptr[_code_ptr[ip + 2 + instr_var_args]]; + + int argc = _code_ptr[ip + 1 + instr_var_args]; + + text += method->get_instance_class(); + text += "."; + text += method->get_name(); + text += "("; + + for (int i = 0; i < argc; i++) { + if (i > 0) { + text += ", "; + } + text += DADDR(1 + i); + } + text += ")"; + + incr = 4 + argc; + } break; + case OPCODE_CALL_METHOD_BIND_VALIDATED_RETURN: { int instr_var_args = _code_ptr[++ip]; text += "call method-bind validated (return) "; diff --git a/modules/gdscript/gdscript_function.h b/modules/gdscript/gdscript_function.h index 184d256bcd..430b96115b 100644 --- a/modules/gdscript/gdscript_function.h +++ b/modules/gdscript/gdscript_function.h @@ -264,6 +264,8 @@ public: OPCODE_CALL_METHOD_BIND_RET, OPCODE_CALL_BUILTIN_STATIC, OPCODE_CALL_NATIVE_STATIC, + OPCODE_CALL_NATIVE_STATIC_VALIDATED_RETURN, + OPCODE_CALL_NATIVE_STATIC_VALIDATED_NO_RETURN, OPCODE_CALL_METHOD_BIND_VALIDATED_RETURN, OPCODE_CALL_METHOD_BIND_VALIDATED_NO_RETURN, OPCODE_AWAIT, diff --git a/modules/gdscript/gdscript_vm.cpp b/modules/gdscript/gdscript_vm.cpp index 842975698b..163ffcb35b 100644 --- a/modules/gdscript/gdscript_vm.cpp +++ b/modules/gdscript/gdscript_vm.cpp @@ -211,156 +211,158 @@ void (*type_init_function_table[])(Variant *) = { }; #if defined(__GNUC__) -#define OPCODES_TABLE \ - static const void *switch_table_ops[] = { \ - &&OPCODE_OPERATOR, \ - &&OPCODE_OPERATOR_VALIDATED, \ - &&OPCODE_TYPE_TEST_BUILTIN, \ - &&OPCODE_TYPE_TEST_ARRAY, \ - &&OPCODE_TYPE_TEST_NATIVE, \ - &&OPCODE_TYPE_TEST_SCRIPT, \ - &&OPCODE_SET_KEYED, \ - &&OPCODE_SET_KEYED_VALIDATED, \ - &&OPCODE_SET_INDEXED_VALIDATED, \ - &&OPCODE_GET_KEYED, \ - &&OPCODE_GET_KEYED_VALIDATED, \ - &&OPCODE_GET_INDEXED_VALIDATED, \ - &&OPCODE_SET_NAMED, \ - &&OPCODE_SET_NAMED_VALIDATED, \ - &&OPCODE_GET_NAMED, \ - &&OPCODE_GET_NAMED_VALIDATED, \ - &&OPCODE_SET_MEMBER, \ - &&OPCODE_GET_MEMBER, \ - &&OPCODE_SET_STATIC_VARIABLE, \ - &&OPCODE_GET_STATIC_VARIABLE, \ - &&OPCODE_ASSIGN, \ - &&OPCODE_ASSIGN_NULL, \ - &&OPCODE_ASSIGN_TRUE, \ - &&OPCODE_ASSIGN_FALSE, \ - &&OPCODE_ASSIGN_TYPED_BUILTIN, \ - &&OPCODE_ASSIGN_TYPED_ARRAY, \ - &&OPCODE_ASSIGN_TYPED_NATIVE, \ - &&OPCODE_ASSIGN_TYPED_SCRIPT, \ - &&OPCODE_CAST_TO_BUILTIN, \ - &&OPCODE_CAST_TO_NATIVE, \ - &&OPCODE_CAST_TO_SCRIPT, \ - &&OPCODE_CONSTRUCT, \ - &&OPCODE_CONSTRUCT_VALIDATED, \ - &&OPCODE_CONSTRUCT_ARRAY, \ - &&OPCODE_CONSTRUCT_TYPED_ARRAY, \ - &&OPCODE_CONSTRUCT_DICTIONARY, \ - &&OPCODE_CALL, \ - &&OPCODE_CALL_RETURN, \ - &&OPCODE_CALL_ASYNC, \ - &&OPCODE_CALL_UTILITY, \ - &&OPCODE_CALL_UTILITY_VALIDATED, \ - &&OPCODE_CALL_GDSCRIPT_UTILITY, \ - &&OPCODE_CALL_BUILTIN_TYPE_VALIDATED, \ - &&OPCODE_CALL_SELF_BASE, \ - &&OPCODE_CALL_METHOD_BIND, \ - &&OPCODE_CALL_METHOD_BIND_RET, \ - &&OPCODE_CALL_BUILTIN_STATIC, \ - &&OPCODE_CALL_NATIVE_STATIC, \ - &&OPCODE_CALL_METHOD_BIND_VALIDATED_RETURN, \ - &&OPCODE_CALL_METHOD_BIND_VALIDATED_NO_RETURN, \ - &&OPCODE_AWAIT, \ - &&OPCODE_AWAIT_RESUME, \ - &&OPCODE_CREATE_LAMBDA, \ - &&OPCODE_CREATE_SELF_LAMBDA, \ - &&OPCODE_JUMP, \ - &&OPCODE_JUMP_IF, \ - &&OPCODE_JUMP_IF_NOT, \ - &&OPCODE_JUMP_TO_DEF_ARGUMENT, \ - &&OPCODE_JUMP_IF_SHARED, \ - &&OPCODE_RETURN, \ - &&OPCODE_RETURN_TYPED_BUILTIN, \ - &&OPCODE_RETURN_TYPED_ARRAY, \ - &&OPCODE_RETURN_TYPED_NATIVE, \ - &&OPCODE_RETURN_TYPED_SCRIPT, \ - &&OPCODE_ITERATE_BEGIN, \ - &&OPCODE_ITERATE_BEGIN_INT, \ - &&OPCODE_ITERATE_BEGIN_FLOAT, \ - &&OPCODE_ITERATE_BEGIN_VECTOR2, \ - &&OPCODE_ITERATE_BEGIN_VECTOR2I, \ - &&OPCODE_ITERATE_BEGIN_VECTOR3, \ - &&OPCODE_ITERATE_BEGIN_VECTOR3I, \ - &&OPCODE_ITERATE_BEGIN_STRING, \ - &&OPCODE_ITERATE_BEGIN_DICTIONARY, \ - &&OPCODE_ITERATE_BEGIN_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_PACKED_BYTE_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_PACKED_INT32_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_PACKED_INT64_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_PACKED_FLOAT32_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_PACKED_FLOAT64_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_PACKED_STRING_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_PACKED_VECTOR2_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_PACKED_VECTOR3_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_PACKED_COLOR_ARRAY, \ - &&OPCODE_ITERATE_BEGIN_OBJECT, \ - &&OPCODE_ITERATE, \ - &&OPCODE_ITERATE_INT, \ - &&OPCODE_ITERATE_FLOAT, \ - &&OPCODE_ITERATE_VECTOR2, \ - &&OPCODE_ITERATE_VECTOR2I, \ - &&OPCODE_ITERATE_VECTOR3, \ - &&OPCODE_ITERATE_VECTOR3I, \ - &&OPCODE_ITERATE_STRING, \ - &&OPCODE_ITERATE_DICTIONARY, \ - &&OPCODE_ITERATE_ARRAY, \ - &&OPCODE_ITERATE_PACKED_BYTE_ARRAY, \ - &&OPCODE_ITERATE_PACKED_INT32_ARRAY, \ - &&OPCODE_ITERATE_PACKED_INT64_ARRAY, \ - &&OPCODE_ITERATE_PACKED_FLOAT32_ARRAY, \ - &&OPCODE_ITERATE_PACKED_FLOAT64_ARRAY, \ - &&OPCODE_ITERATE_PACKED_STRING_ARRAY, \ - &&OPCODE_ITERATE_PACKED_VECTOR2_ARRAY, \ - &&OPCODE_ITERATE_PACKED_VECTOR3_ARRAY, \ - &&OPCODE_ITERATE_PACKED_COLOR_ARRAY, \ - &&OPCODE_ITERATE_OBJECT, \ - &&OPCODE_STORE_GLOBAL, \ - &&OPCODE_STORE_NAMED_GLOBAL, \ - &&OPCODE_TYPE_ADJUST_BOOL, \ - &&OPCODE_TYPE_ADJUST_INT, \ - &&OPCODE_TYPE_ADJUST_FLOAT, \ - &&OPCODE_TYPE_ADJUST_STRING, \ - &&OPCODE_TYPE_ADJUST_VECTOR2, \ - &&OPCODE_TYPE_ADJUST_VECTOR2I, \ - &&OPCODE_TYPE_ADJUST_RECT2, \ - &&OPCODE_TYPE_ADJUST_RECT2I, \ - &&OPCODE_TYPE_ADJUST_VECTOR3, \ - &&OPCODE_TYPE_ADJUST_VECTOR3I, \ - &&OPCODE_TYPE_ADJUST_TRANSFORM2D, \ - &&OPCODE_TYPE_ADJUST_VECTOR4, \ - &&OPCODE_TYPE_ADJUST_VECTOR4I, \ - &&OPCODE_TYPE_ADJUST_PLANE, \ - &&OPCODE_TYPE_ADJUST_QUATERNION, \ - &&OPCODE_TYPE_ADJUST_AABB, \ - &&OPCODE_TYPE_ADJUST_BASIS, \ - &&OPCODE_TYPE_ADJUST_TRANSFORM3D, \ - &&OPCODE_TYPE_ADJUST_PROJECTION, \ - &&OPCODE_TYPE_ADJUST_COLOR, \ - &&OPCODE_TYPE_ADJUST_STRING_NAME, \ - &&OPCODE_TYPE_ADJUST_NODE_PATH, \ - &&OPCODE_TYPE_ADJUST_RID, \ - &&OPCODE_TYPE_ADJUST_OBJECT, \ - &&OPCODE_TYPE_ADJUST_CALLABLE, \ - &&OPCODE_TYPE_ADJUST_SIGNAL, \ - &&OPCODE_TYPE_ADJUST_DICTIONARY, \ - &&OPCODE_TYPE_ADJUST_ARRAY, \ - &&OPCODE_TYPE_ADJUST_PACKED_BYTE_ARRAY, \ - &&OPCODE_TYPE_ADJUST_PACKED_INT32_ARRAY, \ - &&OPCODE_TYPE_ADJUST_PACKED_INT64_ARRAY, \ - &&OPCODE_TYPE_ADJUST_PACKED_FLOAT32_ARRAY, \ - &&OPCODE_TYPE_ADJUST_PACKED_FLOAT64_ARRAY, \ - &&OPCODE_TYPE_ADJUST_PACKED_STRING_ARRAY, \ - &&OPCODE_TYPE_ADJUST_PACKED_VECTOR2_ARRAY, \ - &&OPCODE_TYPE_ADJUST_PACKED_VECTOR3_ARRAY, \ - &&OPCODE_TYPE_ADJUST_PACKED_COLOR_ARRAY, \ - &&OPCODE_ASSERT, \ - &&OPCODE_BREAKPOINT, \ - &&OPCODE_LINE, \ - &&OPCODE_END \ - }; \ +#define OPCODES_TABLE \ + static const void *switch_table_ops[] = { \ + &&OPCODE_OPERATOR, \ + &&OPCODE_OPERATOR_VALIDATED, \ + &&OPCODE_TYPE_TEST_BUILTIN, \ + &&OPCODE_TYPE_TEST_ARRAY, \ + &&OPCODE_TYPE_TEST_NATIVE, \ + &&OPCODE_TYPE_TEST_SCRIPT, \ + &&OPCODE_SET_KEYED, \ + &&OPCODE_SET_KEYED_VALIDATED, \ + &&OPCODE_SET_INDEXED_VALIDATED, \ + &&OPCODE_GET_KEYED, \ + &&OPCODE_GET_KEYED_VALIDATED, \ + &&OPCODE_GET_INDEXED_VALIDATED, \ + &&OPCODE_SET_NAMED, \ + &&OPCODE_SET_NAMED_VALIDATED, \ + &&OPCODE_GET_NAMED, \ + &&OPCODE_GET_NAMED_VALIDATED, \ + &&OPCODE_SET_MEMBER, \ + &&OPCODE_GET_MEMBER, \ + &&OPCODE_SET_STATIC_VARIABLE, \ + &&OPCODE_GET_STATIC_VARIABLE, \ + &&OPCODE_ASSIGN, \ + &&OPCODE_ASSIGN_NULL, \ + &&OPCODE_ASSIGN_TRUE, \ + &&OPCODE_ASSIGN_FALSE, \ + &&OPCODE_ASSIGN_TYPED_BUILTIN, \ + &&OPCODE_ASSIGN_TYPED_ARRAY, \ + &&OPCODE_ASSIGN_TYPED_NATIVE, \ + &&OPCODE_ASSIGN_TYPED_SCRIPT, \ + &&OPCODE_CAST_TO_BUILTIN, \ + &&OPCODE_CAST_TO_NATIVE, \ + &&OPCODE_CAST_TO_SCRIPT, \ + &&OPCODE_CONSTRUCT, \ + &&OPCODE_CONSTRUCT_VALIDATED, \ + &&OPCODE_CONSTRUCT_ARRAY, \ + &&OPCODE_CONSTRUCT_TYPED_ARRAY, \ + &&OPCODE_CONSTRUCT_DICTIONARY, \ + &&OPCODE_CALL, \ + &&OPCODE_CALL_RETURN, \ + &&OPCODE_CALL_ASYNC, \ + &&OPCODE_CALL_UTILITY, \ + &&OPCODE_CALL_UTILITY_VALIDATED, \ + &&OPCODE_CALL_GDSCRIPT_UTILITY, \ + &&OPCODE_CALL_BUILTIN_TYPE_VALIDATED, \ + &&OPCODE_CALL_SELF_BASE, \ + &&OPCODE_CALL_METHOD_BIND, \ + &&OPCODE_CALL_METHOD_BIND_RET, \ + &&OPCODE_CALL_BUILTIN_STATIC, \ + &&OPCODE_CALL_NATIVE_STATIC, \ + &&OPCODE_CALL_NATIVE_STATIC_VALIDATED_RETURN, \ + &&OPCODE_CALL_NATIVE_STATIC_VALIDATED_NO_RETURN, \ + &&OPCODE_CALL_METHOD_BIND_VALIDATED_RETURN, \ + &&OPCODE_CALL_METHOD_BIND_VALIDATED_NO_RETURN, \ + &&OPCODE_AWAIT, \ + &&OPCODE_AWAIT_RESUME, \ + &&OPCODE_CREATE_LAMBDA, \ + &&OPCODE_CREATE_SELF_LAMBDA, \ + &&OPCODE_JUMP, \ + &&OPCODE_JUMP_IF, \ + &&OPCODE_JUMP_IF_NOT, \ + &&OPCODE_JUMP_TO_DEF_ARGUMENT, \ + &&OPCODE_JUMP_IF_SHARED, \ + &&OPCODE_RETURN, \ + &&OPCODE_RETURN_TYPED_BUILTIN, \ + &&OPCODE_RETURN_TYPED_ARRAY, \ + &&OPCODE_RETURN_TYPED_NATIVE, \ + &&OPCODE_RETURN_TYPED_SCRIPT, \ + &&OPCODE_ITERATE_BEGIN, \ + &&OPCODE_ITERATE_BEGIN_INT, \ + &&OPCODE_ITERATE_BEGIN_FLOAT, \ + &&OPCODE_ITERATE_BEGIN_VECTOR2, \ + &&OPCODE_ITERATE_BEGIN_VECTOR2I, \ + &&OPCODE_ITERATE_BEGIN_VECTOR3, \ + &&OPCODE_ITERATE_BEGIN_VECTOR3I, \ + &&OPCODE_ITERATE_BEGIN_STRING, \ + &&OPCODE_ITERATE_BEGIN_DICTIONARY, \ + &&OPCODE_ITERATE_BEGIN_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_PACKED_BYTE_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_PACKED_INT32_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_PACKED_INT64_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_PACKED_FLOAT32_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_PACKED_FLOAT64_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_PACKED_STRING_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_PACKED_VECTOR2_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_PACKED_VECTOR3_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_PACKED_COLOR_ARRAY, \ + &&OPCODE_ITERATE_BEGIN_OBJECT, \ + &&OPCODE_ITERATE, \ + &&OPCODE_ITERATE_INT, \ + &&OPCODE_ITERATE_FLOAT, \ + &&OPCODE_ITERATE_VECTOR2, \ + &&OPCODE_ITERATE_VECTOR2I, \ + &&OPCODE_ITERATE_VECTOR3, \ + &&OPCODE_ITERATE_VECTOR3I, \ + &&OPCODE_ITERATE_STRING, \ + &&OPCODE_ITERATE_DICTIONARY, \ + &&OPCODE_ITERATE_ARRAY, \ + &&OPCODE_ITERATE_PACKED_BYTE_ARRAY, \ + &&OPCODE_ITERATE_PACKED_INT32_ARRAY, \ + &&OPCODE_ITERATE_PACKED_INT64_ARRAY, \ + &&OPCODE_ITERATE_PACKED_FLOAT32_ARRAY, \ + &&OPCODE_ITERATE_PACKED_FLOAT64_ARRAY, \ + &&OPCODE_ITERATE_PACKED_STRING_ARRAY, \ + &&OPCODE_ITERATE_PACKED_VECTOR2_ARRAY, \ + &&OPCODE_ITERATE_PACKED_VECTOR3_ARRAY, \ + &&OPCODE_ITERATE_PACKED_COLOR_ARRAY, \ + &&OPCODE_ITERATE_OBJECT, \ + &&OPCODE_STORE_GLOBAL, \ + &&OPCODE_STORE_NAMED_GLOBAL, \ + &&OPCODE_TYPE_ADJUST_BOOL, \ + &&OPCODE_TYPE_ADJUST_INT, \ + &&OPCODE_TYPE_ADJUST_FLOAT, \ + &&OPCODE_TYPE_ADJUST_STRING, \ + &&OPCODE_TYPE_ADJUST_VECTOR2, \ + &&OPCODE_TYPE_ADJUST_VECTOR2I, \ + &&OPCODE_TYPE_ADJUST_RECT2, \ + &&OPCODE_TYPE_ADJUST_RECT2I, \ + &&OPCODE_TYPE_ADJUST_VECTOR3, \ + &&OPCODE_TYPE_ADJUST_VECTOR3I, \ + &&OPCODE_TYPE_ADJUST_TRANSFORM2D, \ + &&OPCODE_TYPE_ADJUST_VECTOR4, \ + &&OPCODE_TYPE_ADJUST_VECTOR4I, \ + &&OPCODE_TYPE_ADJUST_PLANE, \ + &&OPCODE_TYPE_ADJUST_QUATERNION, \ + &&OPCODE_TYPE_ADJUST_AABB, \ + &&OPCODE_TYPE_ADJUST_BASIS, \ + &&OPCODE_TYPE_ADJUST_TRANSFORM3D, \ + &&OPCODE_TYPE_ADJUST_PROJECTION, \ + &&OPCODE_TYPE_ADJUST_COLOR, \ + &&OPCODE_TYPE_ADJUST_STRING_NAME, \ + &&OPCODE_TYPE_ADJUST_NODE_PATH, \ + &&OPCODE_TYPE_ADJUST_RID, \ + &&OPCODE_TYPE_ADJUST_OBJECT, \ + &&OPCODE_TYPE_ADJUST_CALLABLE, \ + &&OPCODE_TYPE_ADJUST_SIGNAL, \ + &&OPCODE_TYPE_ADJUST_DICTIONARY, \ + &&OPCODE_TYPE_ADJUST_ARRAY, \ + &&OPCODE_TYPE_ADJUST_PACKED_BYTE_ARRAY, \ + &&OPCODE_TYPE_ADJUST_PACKED_INT32_ARRAY, \ + &&OPCODE_TYPE_ADJUST_PACKED_INT64_ARRAY, \ + &&OPCODE_TYPE_ADJUST_PACKED_FLOAT32_ARRAY, \ + &&OPCODE_TYPE_ADJUST_PACKED_FLOAT64_ARRAY, \ + &&OPCODE_TYPE_ADJUST_PACKED_STRING_ARRAY, \ + &&OPCODE_TYPE_ADJUST_PACKED_VECTOR2_ARRAY, \ + &&OPCODE_TYPE_ADJUST_PACKED_VECTOR3_ARRAY, \ + &&OPCODE_TYPE_ADJUST_PACKED_COLOR_ARRAY, \ + &&OPCODE_ASSERT, \ + &&OPCODE_BREAKPOINT, \ + &&OPCODE_LINE, \ + &&OPCODE_END \ + }; \ static_assert((sizeof(switch_table_ops) / sizeof(switch_table_ops[0]) == (OPCODE_END + 1)), "Opcodes in jump table aren't the same as opcodes in enum."); #define OPCODE(m_op) \ @@ -1956,6 +1958,78 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a } DISPATCH_OPCODE; + OPCODE(OPCODE_CALL_NATIVE_STATIC_VALIDATED_RETURN) { + LOAD_INSTRUCTION_ARGS + CHECK_SPACE(3 + instr_arg_count); + + ip += instr_arg_count; + + int argc = _code_ptr[ip + 1]; + GD_ERR_BREAK(argc < 0); + + GD_ERR_BREAK(_code_ptr[ip + 2] < 0 || _code_ptr[ip + 2] >= _methods_count); + MethodBind *method = _methods_ptr[_code_ptr[ip + 2]]; + + Variant **argptrs = instruction_args; + +#ifdef DEBUG_ENABLED + uint64_t call_time = 0; + if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) { + call_time = OS::get_singleton()->get_ticks_usec(); + } +#endif + + GET_INSTRUCTION_ARG(ret, argc); + method->validated_call(nullptr, (const Variant **)argptrs, ret); + +#ifdef DEBUG_ENABLED + if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) { + uint64_t t_taken = OS::get_singleton()->get_ticks_usec() - call_time; + _profile_native_call(t_taken, method->get_name(), method->get_instance_class()); + function_call_time += t_taken; + } +#endif + + ip += 3; + } + DISPATCH_OPCODE; + + OPCODE(OPCODE_CALL_NATIVE_STATIC_VALIDATED_NO_RETURN) { + LOAD_INSTRUCTION_ARGS + CHECK_SPACE(3 + instr_arg_count); + + ip += instr_arg_count; + + int argc = _code_ptr[ip + 1]; + GD_ERR_BREAK(argc < 0); + + GD_ERR_BREAK(_code_ptr[ip + 2] < 0 || _code_ptr[ip + 2] >= _methods_count); + MethodBind *method = _methods_ptr[_code_ptr[ip + 2]]; + + Variant **argptrs = instruction_args; +#ifdef DEBUG_ENABLED + uint64_t call_time = 0; + if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) { + call_time = OS::get_singleton()->get_ticks_usec(); + } +#endif + + GET_INSTRUCTION_ARG(ret, argc); + VariantInternal::initialize(ret, Variant::NIL); + method->validated_call(nullptr, (const Variant **)argptrs, nullptr); + +#ifdef DEBUG_ENABLED + if (GDScriptLanguage::get_singleton()->profiling && GDScriptLanguage::get_singleton()->profile_native_calls) { + uint64_t t_taken = OS::get_singleton()->get_ticks_usec() - call_time; + _profile_native_call(t_taken, method->get_name(), method->get_instance_class()); + function_call_time += t_taken; + } +#endif + + ip += 3; + } + DISPATCH_OPCODE; + OPCODE(OPCODE_CALL_METHOD_BIND_VALIDATED_RETURN) { LOAD_INSTRUCTION_ARGS CHECK_SPACE(3 + instr_arg_count); diff --git a/modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.gd b/modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.gd new file mode 100644 index 0000000000..2f31ecc52f --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.gd @@ -0,0 +1,4 @@ +func test(): + var dictionary := { "a": 0 } + dictionary.make_read_only() + dictionary.a = 1 diff --git a/modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.out b/modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.out new file mode 100644 index 0000000000..da7ce58d73 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/errors/read_only_dictionary.out @@ -0,0 +1,6 @@ +GDTEST_RUNTIME_ERROR +>> SCRIPT ERROR +>> on function: test() +>> runtime/errors/read_only_dictionary.gd +>> 4 +>> Invalid assignment of property or key 'a' with value of type 'int' on a base object of type 'Dictionary'. diff --git a/modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.gd b/modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.gd new file mode 100644 index 0000000000..35e4dbd6a0 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.gd @@ -0,0 +1,7 @@ +func test(): + # Validated native static call with return value. + print(FileAccess.file_exists("some_file")) + + # Validated native static call without return value. + Node.print_orphan_nodes() + diff --git a/modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.out b/modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.out new file mode 100644 index 0000000000..44302c8137 --- /dev/null +++ b/modules/gdscript/tests/scripts/runtime/features/call_native_static_method_validated.out @@ -0,0 +1,2 @@ +GDTEST_OK +false diff --git a/modules/openxr/doc_classes/OpenXRAPIExtension.xml b/modules/openxr/doc_classes/OpenXRAPIExtension.xml index f737f3b642..4419d24dd3 100644 --- a/modules/openxr/doc_classes/OpenXRAPIExtension.xml +++ b/modules/openxr/doc_classes/OpenXRAPIExtension.xml @@ -54,7 +54,7 @@ <method name="get_next_frame_time"> <return type="int" /> <description> - Returns the timing for the next frame. + Returns the predicted display timing for the next frame. </description> </method> <method name="get_play_space"> @@ -63,6 +63,12 @@ Returns the play space, which is an [url=https://registry.khronos.org/OpenXR/specs/1.0/man/html/XrSpace.html]XrSpace[/url] cast to an integer. </description> </method> + <method name="get_predicted_display_time"> + <return type="int" /> + <description> + Returns the predicted display timing for the current frame. + </description> + </method> <method name="get_session"> <return type="int" /> <description> diff --git a/modules/openxr/doc_classes/OpenXRInterface.xml b/modules/openxr/doc_classes/OpenXRInterface.xml index 05dff7d6ae..9ae59a12af 100644 --- a/modules/openxr/doc_classes/OpenXRInterface.xml +++ b/modules/openxr/doc_classes/OpenXRInterface.xml @@ -147,6 +147,11 @@ </member> </members> <signals> + <signal name="instance_exiting"> + <description> + Informs our OpenXR instance is exiting. + </description> + </signal> <signal name="pose_recentered"> <description> Informs the user queued a recenter of the player position. @@ -169,6 +174,11 @@ Informs our OpenXR session now has focus. </description> </signal> + <signal name="session_loss_pending"> + <description> + Informs our OpenXR session is in the process of being lost. + </description> + </signal> <signal name="session_stopping"> <description> Informs our OpenXR session is stopping. diff --git a/modules/openxr/extensions/openxr_composition_layer_extension.cpp b/modules/openxr/extensions/openxr_composition_layer_extension.cpp index 1fba8e5f8b..51f4a03d52 100644 --- a/modules/openxr/extensions/openxr_composition_layer_extension.cpp +++ b/modules/openxr/extensions/openxr_composition_layer_extension.cpp @@ -274,7 +274,7 @@ bool OpenXRViewportCompositionLayerProvider::update_and_acquire_swapchain(bool p if (swapchain_size == viewport_size && !p_static_image && !static_image) { // We're all good! Just acquire it. // We can ignore should_render here, return will be false. - XrBool32 should_render = true; + bool should_render = true; return swapchain_info.acquire(should_render); } @@ -296,7 +296,7 @@ bool OpenXRViewportCompositionLayerProvider::update_and_acquire_swapchain(bool p // Acquire our image so we can start rendering into it, // we can ignore should_render here, ret will be false. - XrBool32 should_render = true; + bool should_render = true; bool ret = swapchain_info.acquire(should_render); swapchain_size = viewport_size; diff --git a/modules/openxr/extensions/openxr_hand_tracking_extension.cpp b/modules/openxr/extensions/openxr_hand_tracking_extension.cpp index 12fa3bed7e..b8a2f58935 100644 --- a/modules/openxr/extensions/openxr_hand_tracking_extension.cpp +++ b/modules/openxr/extensions/openxr_hand_tracking_extension.cpp @@ -128,7 +128,7 @@ void OpenXRHandTrackingExtension::on_process() { } // process our hands - const XrTime time = OpenXRAPI::get_singleton()->get_next_frame_time(); // This data will be used for the next frame we render + const XrTime time = OpenXRAPI::get_singleton()->get_predicted_display_time(); if (time == 0) { // we don't have timing info yet, or we're skipping a frame... return; diff --git a/modules/openxr/openxr_api.cpp b/modules/openxr/openxr_api.cpp index 1fe402341b..40e3ecfefc 100644 --- a/modules/openxr/openxr_api.cpp +++ b/modules/openxr/openxr_api.cpp @@ -160,7 +160,7 @@ void OpenXRAPI::OpenXRSwapChainInfo::free() { } } -bool OpenXRAPI::OpenXRSwapChainInfo::acquire(XrBool32 &p_should_render) { +bool OpenXRAPI::OpenXRSwapChainInfo::acquire(bool &p_should_render) { ERR_FAIL_COND_V(image_acquired, true); // This was not released when it should be, error out and reuse... OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); @@ -193,10 +193,18 @@ bool OpenXRAPI::OpenXRSwapChainInfo::acquire(XrBool32 &p_should_render) { XrSwapchainImageWaitInfo swapchain_image_wait_info = { XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO, // type nullptr, // next - 17000000 // timeout in nanoseconds + 1000000000 // 1s timeout in nanoseconds }; - result = openxr_api->xrWaitSwapchainImage(swapchain, &swapchain_image_wait_info); + // Wait for a maximum of 10 seconds before calling it a critical failure... + for (int retry = 0; retry < 10; retry++) { + result = openxr_api->xrWaitSwapchainImage(swapchain, &swapchain_image_wait_info); + if (result != XR_TIMEOUT_EXPIRED) { + break; + } + WARN_PRINT("OpenXR: timed out waiting for swapchain image."); + } + if (!XR_UNQUALIFIED_SUCCESS(result)) { // Make sure end_frame knows we need to submit an empty frame p_should_render = false; @@ -206,6 +214,8 @@ bool OpenXRAPI::OpenXRSwapChainInfo::acquire(XrBool32 &p_should_render) { print_line("OpenXR: failed to wait for swapchain image [", openxr_api->get_error_string(result), "]"); return false; } else { + WARN_PRINT("OpenXR: couldn't to wait for swapchain but not a complete error [" + openxr_api->get_error_string(result) + "]"); + // Make sure to skip trying to acquire the swapchain image in the next frame skip_acquire_swapchain = true; return false; @@ -760,21 +770,6 @@ bool OpenXRAPI::load_supported_view_configuration_views(XrViewConfigurationType print_verbose(String(" - recommended render sample count: ") + itos(view_configuration_views[i].recommendedSwapchainSampleCount)); } - // Allocate buffers we'll be populating with view information. - views = (XrView *)memalloc(sizeof(XrView) * view_count); - ERR_FAIL_NULL_V_MSG(views, false, "OpenXR Couldn't allocate memory for views"); - memset(views, 0, sizeof(XrView) * view_count); - - projection_views = (XrCompositionLayerProjectionView *)memalloc(sizeof(XrCompositionLayerProjectionView) * view_count); - ERR_FAIL_NULL_V_MSG(projection_views, false, "OpenXR Couldn't allocate memory for projection views"); - memset(projection_views, 0, sizeof(XrCompositionLayerProjectionView) * view_count); - - if (submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available()) { - depth_views = (XrCompositionLayerDepthInfoKHR *)memalloc(sizeof(XrCompositionLayerDepthInfoKHR) * view_count); - ERR_FAIL_NULL_V_MSG(depth_views, false, "OpenXR Couldn't allocate memory for depth views"); - memset(depth_views, 0, sizeof(XrCompositionLayerDepthInfoKHR) * view_count); - } - return true; } @@ -927,6 +922,9 @@ bool OpenXRAPI::setup_play_space() { // If we've previously created a play space, clean it up first. if (play_space != XR_NULL_HANDLE) { + // TODO Investigate if destroying our play space here is safe, + // it may still be used in the rendering thread. + xrDestroySpace(play_space); } play_space = new_play_space; @@ -936,7 +934,11 @@ bool OpenXRAPI::setup_play_space() { if (emulating_local_floor) { // We'll use the STAGE space to get the floor height, but we can't do that until // after xrWaitFrame(), so just set this flag for now. + // Render state will be updated then. should_reset_emulated_floor_height = true; + } else { + // Update render state so this play space is used rendering the upcoming frame. + set_render_play_space(play_space); } return true; @@ -1016,7 +1018,7 @@ bool OpenXRAPI::reset_emulated_floor_height() { identityPose, // pose }; - result = xrLocateSpace(stage_space, local_space, get_next_frame_time(), &stage_location); + result = xrLocateSpace(stage_space, local_space, get_predicted_display_time(), &stage_location); xrDestroySpace(local_space); xrDestroySpace(stage_space); @@ -1042,6 +1044,9 @@ bool OpenXRAPI::reset_emulated_floor_height() { // report that as the reference space to the outside world. reference_space = XR_REFERENCE_SPACE_TYPE_LOCAL_FLOOR_EXT; + // Update render state so this play space is used rendering the upcoming frame. + set_render_play_space(play_space); + return true; } @@ -1136,6 +1141,7 @@ bool OpenXRAPI::obtain_swapchain_formats() { } bool OpenXRAPI::create_main_swapchains(Size2i p_size) { + ERR_NOT_ON_RENDER_THREAD_V(false); ERR_FAIL_NULL_V(graphics_extension, false); ERR_FAIL_COND_V(session == XR_NULL_HANDLE, false); @@ -1154,12 +1160,12 @@ bool OpenXRAPI::create_main_swapchains(Size2i p_size) { as we render 3D content into internal buffers that are copied into the swapchain, we do now have (basic) VRS support */ - main_swapchain_size = p_size; + render_state.main_swapchain_size = p_size; uint32_t sample_count = 1; // We start with our color swapchain... if (color_swapchain_format != 0) { - if (!main_swapchains[OPENXR_SWAPCHAIN_COLOR].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT, color_swapchain_format, main_swapchain_size.width, main_swapchain_size.height, sample_count, view_count)) { + if (!render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT, color_swapchain_format, render_state.main_swapchain_size.width, render_state.main_swapchain_size.height, sample_count, view_count)) { return false; } } @@ -1169,7 +1175,7 @@ bool OpenXRAPI::create_main_swapchains(Size2i p_size) { // - we support our depth layer extension // - we have our spacewarp extension (not yet implemented) if (depth_swapchain_format != 0 && submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available()) { - if (!main_swapchains[OPENXR_SWAPCHAIN_DEPTH].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, depth_swapchain_format, main_swapchain_size.width, main_swapchain_size.height, sample_count, view_count)) { + if (!render_state.main_swapchains[OPENXR_SWAPCHAIN_DEPTH].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, depth_swapchain_format, render_state.main_swapchain_size.width, render_state.main_swapchain_size.height, sample_count, view_count)) { return false; } } @@ -1180,36 +1186,36 @@ bool OpenXRAPI::create_main_swapchains(Size2i p_size) { // TBD } - for (uint32_t i = 0; i < view_count; i++) { - views[i].type = XR_TYPE_VIEW; - views[i].next = nullptr; - - projection_views[i].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW; - projection_views[i].next = nullptr; - projection_views[i].subImage.swapchain = main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_swapchain(); - projection_views[i].subImage.imageArrayIndex = i; - projection_views[i].subImage.imageRect.offset.x = 0; - projection_views[i].subImage.imageRect.offset.y = 0; - projection_views[i].subImage.imageRect.extent.width = main_swapchain_size.width; - projection_views[i].subImage.imageRect.extent.height = main_swapchain_size.height; - - if (submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available() && depth_views) { - projection_views[i].next = &depth_views[i]; - - depth_views[i].type = XR_TYPE_COMPOSITION_LAYER_DEPTH_INFO_KHR; - depth_views[i].next = nullptr; - depth_views[i].subImage.swapchain = main_swapchains[OPENXR_SWAPCHAIN_DEPTH].get_swapchain(); - depth_views[i].subImage.imageArrayIndex = i; - depth_views[i].subImage.imageRect.offset.x = 0; - depth_views[i].subImage.imageRect.offset.y = 0; - depth_views[i].subImage.imageRect.extent.width = main_swapchain_size.width; - depth_views[i].subImage.imageRect.extent.height = main_swapchain_size.height; + for (uint32_t i = 0; i < render_state.view_count; i++) { + render_state.views[i].type = XR_TYPE_VIEW; + render_state.views[i].next = nullptr; + + render_state.projection_views[i].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW; + render_state.projection_views[i].next = nullptr; + render_state.projection_views[i].subImage.swapchain = render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_swapchain(); + render_state.projection_views[i].subImage.imageArrayIndex = i; + render_state.projection_views[i].subImage.imageRect.offset.x = 0; + render_state.projection_views[i].subImage.imageRect.offset.y = 0; + render_state.projection_views[i].subImage.imageRect.extent.width = render_state.main_swapchain_size.width; + render_state.projection_views[i].subImage.imageRect.extent.height = render_state.main_swapchain_size.height; + + if (render_state.submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available() && render_state.depth_views) { + render_state.projection_views[i].next = &render_state.depth_views[i]; + + render_state.depth_views[i].type = XR_TYPE_COMPOSITION_LAYER_DEPTH_INFO_KHR; + render_state.depth_views[i].next = nullptr; + render_state.depth_views[i].subImage.swapchain = render_state.main_swapchains[OPENXR_SWAPCHAIN_DEPTH].get_swapchain(); + render_state.depth_views[i].subImage.imageArrayIndex = i; + render_state.depth_views[i].subImage.imageRect.offset.x = 0; + render_state.depth_views[i].subImage.imageRect.offset.y = 0; + render_state.depth_views[i].subImage.imageRect.extent.width = render_state.main_swapchain_size.width; + render_state.depth_views[i].subImage.imageRect.extent.height = render_state.main_swapchain_size.height; // OpenXR spec says that: minDepth < maxDepth. - depth_views[i].minDepth = 0.0; - depth_views[i].maxDepth = 1.0; + render_state.depth_views[i].minDepth = 0.0; + render_state.depth_views[i].maxDepth = 1.0; // But we can reverse near and far for reverse-Z. - depth_views[i].nearZ = 100.0; // Near and far Z will be set to the correct values in fill_projection_matrix - depth_views[i].farZ = 0.01; + render_state.depth_views[i].nearZ = 100.0; // Near and far Z will be set to the correct values in fill_projection_matrix + render_state.depth_views[i].farZ = 0.01; } }; @@ -1217,23 +1223,33 @@ bool OpenXRAPI::create_main_swapchains(Size2i p_size) { }; void OpenXRAPI::destroy_session() { - if (running && session != XR_NULL_HANDLE) { - xrEndSession(session); + // TODO need to figure out if we're still rendering our current frame + // in a separate rendering thread and if so, + // if we need to wait for completion. + // We could be pulling the rug from underneath rendering... + + if (running) { + if (session != XR_NULL_HANDLE) { + xrEndSession(session); + } + + running = false; + render_state.running = false; } - if (views != nullptr) { - memfree(views); - views = nullptr; + if (render_state.views != nullptr) { + memfree(render_state.views); + render_state.views = nullptr; } - if (projection_views != nullptr) { - memfree(projection_views); - projection_views = nullptr; + if (render_state.projection_views != nullptr) { + memfree(render_state.projection_views); + render_state.projection_views = nullptr; } - if (depth_views != nullptr) { - memfree(depth_views); - depth_views = nullptr; + if (render_state.depth_views != nullptr) { + memfree(render_state.depth_views); + render_state.depth_views = nullptr; } free_main_swapchains(); @@ -1248,6 +1264,7 @@ void OpenXRAPI::destroy_session() { if (play_space != XR_NULL_HANDLE) { xrDestroySpace(play_space); play_space = XR_NULL_HANDLE; + render_state.play_space = XR_NULL_HANDLE; } if (view_space != XR_NULL_HANDLE) { xrDestroySpace(view_space); @@ -1298,6 +1315,7 @@ bool OpenXRAPI::on_state_ready() { // we're running running = true; + set_render_session_running(true); for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) { wrapper->on_state_ready(); @@ -1374,34 +1392,37 @@ bool OpenXRAPI::on_state_stopping() { } running = false; + set_render_session_running(false); } - // TODO further cleanup - return true; } bool OpenXRAPI::on_state_loss_pending() { print_verbose("On state loss pending"); + if (xr_interface) { + xr_interface->on_state_loss_pending(); + } + for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) { wrapper->on_state_loss_pending(); } - // TODO need to look into the correct action here, read up on the spec but we may need to signal Godot to exit (if it's not already exiting) - return true; } bool OpenXRAPI::on_state_exiting() { print_verbose("On state existing"); + if (xr_interface) { + xr_interface->on_state_exiting(); + } + for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) { wrapper->on_state_exiting(); } - // TODO need to look into the correct action here, read up on the spec but we may need to signal Godot to exit (if it's not already exiting) - return true; } @@ -1419,10 +1440,7 @@ void OpenXRAPI::set_view_configuration(XrViewConfigurationType p_view_configurat bool OpenXRAPI::set_requested_reference_space(XrReferenceSpaceType p_requested_reference_space) { requested_reference_space = p_requested_reference_space; - - if (is_initialized()) { - return setup_play_space(); - } + play_space_is_dirty = true; return true; } @@ -1625,11 +1643,6 @@ bool OpenXRAPI::initialize_session() { return false; } - if (!setup_play_space()) { - destroy_session(); - return false; - } - if (!setup_view_space()) { destroy_session(); return false; @@ -1645,6 +1658,8 @@ bool OpenXRAPI::initialize_session() { return false; } + allocate_view_buffers(view_count, submit_depth_buffer); + return true; } @@ -1696,12 +1711,18 @@ XrHandTrackerEXT OpenXRAPI::get_hand_tracker(int p_hand_index) { } Size2 OpenXRAPI::get_recommended_target_size() { + RenderingServer *rendering_server = RenderingServer::get_singleton(); ERR_FAIL_NULL_V(view_configuration_views, Size2()); Size2 target_size; - target_size.width = view_configuration_views[0].recommendedImageRectWidth * render_target_size_multiplier; - target_size.height = view_configuration_views[0].recommendedImageRectHeight * render_target_size_multiplier; + if (rendering_server && rendering_server->is_on_render_thread()) { + target_size.width = view_configuration_views[0].recommendedImageRectWidth * render_state.render_target_size_multiplier; + target_size.height = view_configuration_views[0].recommendedImageRectHeight * render_state.render_target_size_multiplier; + } else { + target_size.width = view_configuration_views[0].recommendedImageRectWidth * render_target_size_multiplier; + target_size.height = view_configuration_views[0].recommendedImageRectHeight * render_target_size_multiplier; + } return target_size; } @@ -1713,14 +1734,12 @@ XRPose::TrackingConfidence OpenXRAPI::get_head_center(Transform3D &r_transform, return XRPose::XR_TRACKING_CONFIDENCE_NONE; } - // xrWaitFrame not run yet - if (frame_state.predictedDisplayTime == 0) { + // Get display time + XrTime display_time = get_predicted_display_time(); + if (display_time == 0) { return XRPose::XR_TRACKING_CONFIDENCE_NONE; } - // Get timing for the next frame, as that is the current frame we're processing - XrTime display_time = get_next_frame_time(); - XrSpaceVelocity velocity = { XR_TYPE_SPACE_VELOCITY, // type nullptr, // next @@ -1764,54 +1783,47 @@ XRPose::TrackingConfidence OpenXRAPI::get_head_center(Transform3D &r_transform, } bool OpenXRAPI::get_view_transform(uint32_t p_view, Transform3D &r_transform) { - if (!running) { - return false; - } + ERR_NOT_ON_RENDER_THREAD_V(false); - // xrWaitFrame not run yet - if (frame_state.predictedDisplayTime == 0) { + if (!render_state.running) { return false; } // we don't have valid view info - if (views == nullptr || !view_pose_valid) { + if (render_state.views == nullptr || !render_state.view_pose_valid) { return false; } // Note, the timing of this is set right before rendering, which is what we need here. - r_transform = transform_from_pose(views[p_view].pose); + r_transform = transform_from_pose(render_state.views[p_view].pose); return true; } bool OpenXRAPI::get_view_projection(uint32_t p_view, double p_z_near, double p_z_far, Projection &p_camera_matrix) { + ERR_NOT_ON_RENDER_THREAD_V(false); ERR_FAIL_NULL_V(graphics_extension, false); - if (!running) { - return false; - } - - // xrWaitFrame not run yet - if (frame_state.predictedDisplayTime == 0) { + if (!render_state.running) { return false; } // we don't have valid view info - if (views == nullptr || !view_pose_valid) { + if (render_state.views == nullptr || !render_state.view_pose_valid) { return false; } // if we're using depth views, make sure we update our near and far there... - if (depth_views != nullptr) { - for (uint32_t i = 0; i < view_count; i++) { + if (render_state.depth_views != nullptr) { + for (uint32_t i = 0; i < render_state.view_count; i++) { // As we are using reverse-Z these need to be flipped. - depth_views[i].nearZ = p_z_far; - depth_views[i].farZ = p_z_near; + render_state.depth_views[i].nearZ = p_z_far; + render_state.depth_views[i].farZ = p_z_near; } } // now update our projection - return graphics_extension->create_projection_fov(views[p_view].fov, p_z_near, p_z_far, p_camera_matrix); + return graphics_extension->create_projection_fov(render_state.views[p_view].fov, p_z_near, p_z_far, p_camera_matrix); } bool OpenXRAPI::poll_events() { @@ -1934,53 +1946,85 @@ bool OpenXRAPI::poll_events() { } } -bool OpenXRAPI::process() { - ERR_FAIL_COND_V(instance == XR_NULL_HANDLE, false); +void OpenXRAPI::_allocate_view_buffers(uint32_t p_view_count, bool p_submit_depth_buffer) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; - if (!poll_events()) { - return false; - } + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL(openxr_api); - if (!running) { - return false; - } + openxr_api->render_state.view_count = p_view_count; + openxr_api->render_state.submit_depth_buffer = p_submit_depth_buffer; - for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) { - wrapper->on_process(); + // Allocate buffers we'll be populating with view information. + openxr_api->render_state.views = (XrView *)memalloc(sizeof(XrView) * p_view_count); + ERR_FAIL_NULL_MSG(openxr_api->render_state.views, "OpenXR Couldn't allocate memory for views"); + memset(openxr_api->render_state.views, 0, sizeof(XrView) * p_view_count); + + openxr_api->render_state.projection_views = (XrCompositionLayerProjectionView *)memalloc(sizeof(XrCompositionLayerProjectionView) * p_view_count); + ERR_FAIL_NULL_MSG(openxr_api->render_state.projection_views, "OpenXR Couldn't allocate memory for projection views"); + memset(openxr_api->render_state.projection_views, 0, sizeof(XrCompositionLayerProjectionView) * p_view_count); + + if (p_submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available()) { + openxr_api->render_state.depth_views = (XrCompositionLayerDepthInfoKHR *)memalloc(sizeof(XrCompositionLayerDepthInfoKHR) * p_view_count); + ERR_FAIL_NULL_MSG(openxr_api->render_state.depth_views, "OpenXR Couldn't allocate memory for depth views"); + memset(openxr_api->render_state.depth_views, 0, sizeof(XrCompositionLayerDepthInfoKHR) * p_view_count); } +} - return true; +void OpenXRAPI::_set_render_session_running(bool p_is_running) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; + + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL(openxr_api); + openxr_api->render_state.running = p_is_running; } -void OpenXRAPI::free_main_swapchains() { - for (int i = 0; i < OPENXR_SWAPCHAIN_MAX; i++) { - main_swapchains[i].queue_free(); - } +void OpenXRAPI::_set_render_display_info(XrTime p_predicted_display_time, bool p_should_render) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; + + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL(openxr_api); + openxr_api->render_state.predicted_display_time = p_predicted_display_time; + openxr_api->render_state.should_render = p_should_render; } -void OpenXRAPI::pre_render() { - ERR_FAIL_COND(instance == XR_NULL_HANDLE); +void OpenXRAPI::_set_render_play_space(uint64_t p_play_space) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; - if (!running) { - return; - } + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL(openxr_api); + openxr_api->render_state.play_space = XrSpace(p_play_space); +} - // Process any swapchains that were queued to be freed - OpenXRSwapChainInfo::free_queued(); +void OpenXRAPI::_set_render_state_multiplier(double p_render_target_size_multiplier) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; - Size2i swapchain_size = get_recommended_target_size(); - if (swapchain_size != main_swapchain_size) { - // Out with the old. - free_main_swapchains(); + OpenXRAPI *openxr_api = OpenXRAPI::get_singleton(); + ERR_FAIL_NULL(openxr_api); + openxr_api->render_state.render_target_size_multiplier = p_render_target_size_multiplier; +} - // In with the new. - create_main_swapchains(swapchain_size); +bool OpenXRAPI::process() { + ERR_FAIL_COND_V(instance == XR_NULL_HANDLE, false); + + if (!poll_events()) { + return false; } - // Waitframe does 2 important things in our process: - // 1) It provides us with predictive timing, telling us when OpenXR expects to display the frame we're about to commit - // 2) It will use the previous timing to pause our thread so that rendering starts as close to displaying as possible - // This must thus be called as close to when we start rendering as possible + if (!running) { + return false; + } + + // We call xrWaitFrame as early as possible, this will allow OpenXR to get + // proper timing info between this point, and when we're ready to start rendering. + // As the name suggests, OpenXR can pause the thread to minimize the time between + // retrieving tracking data and using that tracking data to render. + // OpenXR thus works best if rendering is performed on a separate thread. XrFrameWaitInfo frame_wait_info = { XR_TYPE_FRAME_WAIT_INFO, nullptr }; frame_state.predictedDisplayTime = 0; frame_state.predictedDisplayPeriod = 0; @@ -1995,7 +2039,9 @@ void OpenXRAPI::pre_render() { frame_state.predictedDisplayPeriod = 0; frame_state.shouldRender = false; - return; + set_render_display_info(0, false); + + return false; } if (frame_state.predictedDisplayPeriod > 500000000) { @@ -2004,12 +2050,54 @@ void OpenXRAPI::pre_render() { frame_state.predictedDisplayPeriod = 0; } + set_render_display_info(frame_state.predictedDisplayTime, frame_state.shouldRender); + + if (unlikely(play_space_is_dirty)) { + setup_play_space(); + play_space_is_dirty = false; + } + if (unlikely(should_reset_emulated_floor_height)) { reset_emulated_floor_height(); should_reset_emulated_floor_height = false; } for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) { + wrapper->on_process(); + } + + return true; +} + +void OpenXRAPI::free_main_swapchains() { + for (int i = 0; i < OPENXR_SWAPCHAIN_MAX; i++) { + render_state.main_swapchains[i].queue_free(); + } +} + +void OpenXRAPI::pre_render() { + ERR_FAIL_COND(session == XR_NULL_HANDLE); + + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; + + if (!render_state.running) { + return; + } + + // Process any swapchains that were queued to be freed + OpenXRSwapChainInfo::free_queued(); + + Size2i swapchain_size = get_recommended_target_size(); + if (swapchain_size != render_state.main_swapchain_size) { + // Out with the old. + free_main_swapchains(); + + // In with the new. + create_main_swapchains(swapchain_size); + } + + for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) { wrapper->on_pre_render(); } @@ -2028,8 +2116,8 @@ void OpenXRAPI::pre_render() { XR_TYPE_VIEW_LOCATE_INFO, // type nullptr, // next view_configuration, // viewConfigurationType - frame_state.predictedDisplayTime, // displayTime - play_space // space + render_state.predicted_display_time, // displayTime + render_state.play_space // space }; XrViewState view_state = { XR_TYPE_VIEW_STATE, // type @@ -2037,7 +2125,7 @@ void OpenXRAPI::pre_render() { 0 // viewStateFlags }; uint32_t view_count_output; - result = xrLocateViews(session, &view_locate_info, &view_state, view_count, &view_count_output, views); + XrResult result = xrLocateViews(session, &view_locate_info, &view_state, render_state.view_count, &view_count_output, render_state.views); if (XR_FAILED(result)) { print_line("OpenXR: Couldn't locate views [", get_error_string(result), "]"); return; @@ -2050,9 +2138,9 @@ void OpenXRAPI::pre_render() { pose_valid = false; } } - if (view_pose_valid != pose_valid) { - view_pose_valid = pose_valid; - if (!view_pose_valid) { + if (render_state.view_pose_valid != pose_valid) { + render_state.view_pose_valid = pose_valid; + if (!render_state.view_pose_valid) { print_verbose("OpenXR View pose became invalid"); } else { print_verbose("OpenXR View pose became valid"); @@ -2071,23 +2159,24 @@ void OpenXRAPI::pre_render() { } // Reset this, we haven't found a viewport for output yet - has_xr_viewport = false; + render_state.has_xr_viewport = false; } bool OpenXRAPI::pre_draw_viewport(RID p_render_target) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD_V(false); + // We found an XR viewport! - has_xr_viewport = true; + render_state.has_xr_viewport = true; - if (!can_render()) { + if (instance == XR_NULL_HANDLE || session == XR_NULL_HANDLE || !render_state.running || !render_state.view_pose_valid || !render_state.should_render) { return false; } - // TODO: at some point in time we may support multiple viewports in which case we need to handle that... - // Acquire our images for (int i = 0; i < OPENXR_SWAPCHAIN_MAX; i++) { - if (!main_swapchains[i].is_image_acquired() && main_swapchains[i].get_swapchain() != XR_NULL_HANDLE) { - if (!main_swapchains[i].acquire(frame_state.shouldRender)) { + if (!render_state.main_swapchains[i].is_image_acquired() && render_state.main_swapchains[i].get_swapchain() != XR_NULL_HANDLE) { + if (!render_state.main_swapchains[i].acquire(render_state.should_render)) { return false; } } @@ -2101,24 +2190,33 @@ bool OpenXRAPI::pre_draw_viewport(RID p_render_target) { } XrSwapchain OpenXRAPI::get_color_swapchain() { - return main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_swapchain(); + ERR_NOT_ON_RENDER_THREAD_V(XR_NULL_HANDLE); + + return render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_swapchain(); } RID OpenXRAPI::get_color_texture() { - return main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_image(); + ERR_NOT_ON_RENDER_THREAD_V(RID()); + + return render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_image(); } RID OpenXRAPI::get_depth_texture() { + ERR_NOT_ON_RENDER_THREAD_V(RID()); + // Note, image will not be acquired if we didn't have a suitable swap chain format. - if (submit_depth_buffer) { - return main_swapchains[OPENXR_SWAPCHAIN_DEPTH].get_image(); + if (render_state.submit_depth_buffer && render_state.main_swapchains[OPENXR_SWAPCHAIN_DEPTH].is_image_acquired()) { + return render_state.main_swapchains[OPENXR_SWAPCHAIN_DEPTH].get_image(); } else { return RID(); } } void OpenXRAPI::post_draw_viewport(RID p_render_target) { - if (!can_render()) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; + + if (instance == XR_NULL_HANDLE || session == XR_NULL_HANDLE || !render_state.running || !render_state.view_pose_valid || !render_state.should_render) { return; } @@ -2130,30 +2228,33 @@ void OpenXRAPI::post_draw_viewport(RID p_render_target) { void OpenXRAPI::end_frame() { XrResult result; - ERR_FAIL_COND(instance == XR_NULL_HANDLE); + ERR_FAIL_COND(session == XR_NULL_HANDLE); - if (!running) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; + + if (!render_state.running) { return; } - if (frame_state.shouldRender && view_pose_valid) { - if (!has_xr_viewport) { + if (render_state.should_render && render_state.view_pose_valid) { + if (!render_state.has_xr_viewport) { print_line("OpenXR: No viewport was marked with use_xr, there is no rendered output!"); - } else if (!main_swapchains[OPENXR_SWAPCHAIN_COLOR].is_image_acquired()) { + } else if (!render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].is_image_acquired()) { print_line("OpenXR: No swapchain could be acquired to render to!"); } } // must have: - // - shouldRender set to true + // - should_render set to true // - a valid view pose for projection_views[eye].pose to submit layer // - an image to render - if (!frame_state.shouldRender || !view_pose_valid || !main_swapchains[OPENXR_SWAPCHAIN_COLOR].is_image_acquired()) { + if (!render_state.should_render || !render_state.view_pose_valid || !render_state.main_swapchains[OPENXR_SWAPCHAIN_COLOR].is_image_acquired()) { // submit 0 layers when we shouldn't render XrFrameEndInfo frame_end_info = { XR_TYPE_FRAME_END_INFO, // type nullptr, // next - frame_state.predictedDisplayTime, // displayTime + render_state.predicted_display_time, // displayTime environment_blend_mode, // environmentBlendMode 0, // layerCount nullptr // layers @@ -2170,14 +2271,14 @@ void OpenXRAPI::end_frame() { // release our swapchain image if we acquired it for (int i = 0; i < OPENXR_SWAPCHAIN_MAX; i++) { - if (main_swapchains[i].is_image_acquired()) { - main_swapchains[i].release(); + if (render_state.main_swapchains[i].is_image_acquired()) { + render_state.main_swapchains[i].release(); } } - for (uint32_t eye = 0; eye < view_count; eye++) { - projection_views[eye].fov = views[eye].fov; - projection_views[eye].pose = views[eye].pose; + for (uint32_t eye = 0; eye < render_state.view_count; eye++) { + render_state.projection_views[eye].fov = render_state.views[eye].fov; + render_state.projection_views[eye].pose = render_state.views[eye].pose; } Vector<OrderedCompositionLayer> ordered_layers_list; @@ -2210,9 +2311,9 @@ void OpenXRAPI::end_frame() { XR_TYPE_COMPOSITION_LAYER_PROJECTION, // type nullptr, // next layer_flags, // layerFlags - play_space, // space - view_count, // viewCount - projection_views, // views + render_state.play_space, // space + render_state.view_count, // viewCount + render_state.projection_views, // views }; ordered_layers_list.push_back({ (const XrCompositionLayerBaseHeader *)&projection_layer, 0 }); @@ -2228,7 +2329,7 @@ void OpenXRAPI::end_frame() { XrFrameEndInfo frame_end_info = { XR_TYPE_FRAME_END_INFO, // type nullptr, // next - frame_state.predictedDisplayTime, // displayTime + render_state.predicted_display_time, // displayTime environment_blend_mode, // environmentBlendMode static_cast<uint32_t>(layers_list.size()), // layerCount layers_list.ptr() // layers @@ -2271,6 +2372,7 @@ double OpenXRAPI::get_render_target_size_multiplier() const { void OpenXRAPI::set_render_target_size_multiplier(double multiplier) { render_target_size_multiplier = multiplier; + set_render_state_multiplier(multiplier); } bool OpenXRAPI::is_foveation_supported() const { @@ -2414,10 +2516,6 @@ OpenXRAPI::OpenXRAPI() { submit_depth_buffer = GLOBAL_GET("xr/openxr/submit_depth_buffer"); } - - // Reset a few things that can't be done in our class definition. - frame_state.predictedDisplayTime = 0; - frame_state.predictedDisplayPeriod = 0; } OpenXRAPI::~OpenXRAPI() { @@ -3132,7 +3230,7 @@ XRPose::TrackingConfidence OpenXRAPI::get_action_pose(RID p_action, RID p_tracke return XRPose::XR_TRACKING_CONFIDENCE_NONE; } - XrTime display_time = get_next_frame_time(); + XrTime display_time = get_predicted_display_time(); if (display_time == 0) { return XRPose::XR_TRACKING_CONFIDENCE_NONE; } diff --git a/modules/openxr/openxr_api.h b/modules/openxr/openxr_api.h index e835366200..c95867810c 100644 --- a/modules/openxr/openxr_api.h +++ b/modules/openxr/openxr_api.h @@ -46,13 +46,11 @@ #include "core/templates/rb_map.h" #include "core/templates/rid_owner.h" #include "core/templates/vector.h" +#include "servers/rendering_server.h" #include "servers/xr/xr_pose.h" #include <openxr/openxr.h> -// Note, OpenXR code that we wrote for our plugin makes use of C++20 notation for initializing structs which ensures zeroing out unspecified members. -// Godot is currently restricted to C++17 which doesn't allow this notation. Make sure critical fields are set. - // forward declarations, we don't want to include these fully class OpenXRInterface; @@ -77,7 +75,7 @@ public: static void free_queued(); void free(); - bool acquire(XrBool32 &p_should_render); + bool acquire(bool &p_should_render); bool release(); RID get_image(); }; @@ -151,9 +149,6 @@ private: uint32_t view_count = 0; XrViewConfigurationView *view_configuration_views = nullptr; - XrView *views = nullptr; - XrCompositionLayerProjectionView *projection_views = nullptr; - XrCompositionLayerDepthInfoKHR *depth_views = nullptr; // Only used by Composition Layer Depth Extension if available enum OpenXRSwapChainTypes { OPENXR_SWAPCHAIN_COLOR, @@ -164,14 +159,11 @@ private: int64_t color_swapchain_format = 0; int64_t depth_swapchain_format = 0; - Size2i main_swapchain_size = { 0, 0 }; - OpenXRSwapChainInfo main_swapchains[OPENXR_SWAPCHAIN_MAX]; + bool play_space_is_dirty = true; XrSpace play_space = XR_NULL_HANDLE; XrSpace view_space = XR_NULL_HANDLE; - bool view_pose_valid = false; XRPose::TrackingConfidence head_pose_confidence = XRPose::XR_TRACKING_CONFIDENCE_NONE; - bool has_xr_viewport = false; bool emulating_local_floor = false; bool should_reset_emulated_floor_height = false; @@ -328,6 +320,72 @@ private: // convenience void copy_string_to_char_buffer(const String p_string, char *p_buffer, int p_buffer_len); + // Render state, Only accessible in rendering thread + struct RenderState { + bool running = false; + bool should_render = false; + bool has_xr_viewport = false; + XrTime predicted_display_time = 0; + XrSpace play_space = XR_NULL_HANDLE; + double render_target_size_multiplier = 1.0; + + uint32_t view_count = 0; + XrView *views = nullptr; + XrCompositionLayerProjectionView *projection_views = nullptr; + XrCompositionLayerDepthInfoKHR *depth_views = nullptr; // Only used by Composition Layer Depth Extension if available + bool submit_depth_buffer = false; // if set to true we submit depth buffers to OpenXR if a suitable extension is enabled. + bool view_pose_valid = false; + + Size2i main_swapchain_size; + OpenXRSwapChainInfo main_swapchains[OPENXR_SWAPCHAIN_MAX]; + } render_state; + + static void _allocate_view_buffers(uint32_t p_view_count, bool p_submit_depth_buffer); + static void _set_render_session_running(bool p_is_running); + static void _set_render_display_info(XrTime p_predicted_display_time, bool p_should_render); + static void _set_render_play_space(uint64_t p_play_space); + static void _set_render_state_multiplier(double p_render_target_size_multiplier); + + _FORCE_INLINE_ void allocate_view_buffers(uint32_t p_view_count, bool p_submit_depth_buffer) { + // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready... + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_allocate_view_buffers).bind(p_view_count, p_submit_depth_buffer)); + } + + _FORCE_INLINE_ void set_render_session_running(bool p_is_running) { + // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready... + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_set_render_session_running).bind(p_is_running)); + } + + _FORCE_INLINE_ void set_render_display_info(XrTime p_predicted_display_time, bool p_should_render) { + // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready... + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_set_render_display_info).bind(p_predicted_display_time, p_should_render)); + } + + _FORCE_INLINE_ void set_render_play_space(XrSpace p_play_space) { + // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready... + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_set_render_play_space).bind(uint64_t(p_play_space))); + } + + _FORCE_INLINE_ void set_render_state_multiplier(double p_render_target_size_multiplier) { + // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready... + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp_static(&OpenXRAPI::_set_render_state_multiplier).bind(p_render_target_size_multiplier)); + } + public: XrInstance get_instance() const { return instance; }; XrSystemId get_system_id() const { return system_id; }; @@ -384,9 +442,13 @@ public: bool initialize_session(); void finish(); - XrSpace get_play_space() const { return play_space; } - XrTime get_next_frame_time() { return frame_state.predictedDisplayTime + frame_state.predictedDisplayPeriod; } - bool can_render() { return instance != XR_NULL_HANDLE && session != XR_NULL_HANDLE && running && view_pose_valid && frame_state.shouldRender; } + _FORCE_INLINE_ XrSpace get_play_space() const { return play_space; } + _FORCE_INLINE_ XrTime get_predicted_display_time() { return frame_state.predictedDisplayTime; } + _FORCE_INLINE_ XrTime get_next_frame_time() { return frame_state.predictedDisplayTime + frame_state.predictedDisplayPeriod; } + _FORCE_INLINE_ bool can_render() { + ERR_ON_RENDER_THREAD_V(false); + return instance != XR_NULL_HANDLE && session != XR_NULL_HANDLE && running && frame_state.shouldRender; + } XrHandTrackerEXT get_hand_tracker(int p_hand_index); diff --git a/modules/openxr/openxr_api_extension.cpp b/modules/openxr/openxr_api_extension.cpp index fae0fc13d3..a1744fa1db 100644 --- a/modules/openxr/openxr_api_extension.cpp +++ b/modules/openxr/openxr_api_extension.cpp @@ -48,6 +48,7 @@ void OpenXRAPIExtension::_bind_methods() { ClassDB::bind_method(D_METHOD("is_running"), &OpenXRAPIExtension::is_running); ClassDB::bind_method(D_METHOD("get_play_space"), &OpenXRAPIExtension::get_play_space); + ClassDB::bind_method(D_METHOD("get_predicted_display_time"), &OpenXRAPIExtension::get_predicted_display_time); ClassDB::bind_method(D_METHOD("get_next_frame_time"), &OpenXRAPIExtension::get_next_frame_time); ClassDB::bind_method(D_METHOD("can_render"), &OpenXRAPIExtension::can_render); @@ -130,8 +131,17 @@ uint64_t OpenXRAPIExtension::get_play_space() { return (uint64_t)OpenXRAPI::get_singleton()->get_play_space(); } +int64_t OpenXRAPIExtension::get_predicted_display_time() { + ERR_FAIL_NULL_V(OpenXRAPI::get_singleton(), 0); + return (XrTime)OpenXRAPI::get_singleton()->get_predicted_display_time(); +} + int64_t OpenXRAPIExtension::get_next_frame_time() { ERR_FAIL_NULL_V(OpenXRAPI::get_singleton(), 0); + + // In the past we needed to look a frame ahead, may be calling this unintentionally so lets warn the dev. + WARN_PRINT_ONCE("OpenXR: Next frame timing called, verify this is intended."); + return (XrTime)OpenXRAPI::get_singleton()->get_next_frame_time(); } diff --git a/modules/openxr/openxr_api_extension.h b/modules/openxr/openxr_api_extension.h index 576e497798..cff2c4738e 100644 --- a/modules/openxr/openxr_api_extension.h +++ b/modules/openxr/openxr_api_extension.h @@ -69,6 +69,7 @@ public: bool is_running(); uint64_t get_play_space(); + int64_t get_predicted_display_time(); int64_t get_next_frame_time(); bool can_render(); diff --git a/modules/openxr/openxr_interface.cpp b/modules/openxr/openxr_interface.cpp index aa68441f03..def1150b83 100644 --- a/modules/openxr/openxr_interface.cpp +++ b/modules/openxr/openxr_interface.cpp @@ -43,6 +43,8 @@ void OpenXRInterface::_bind_methods() { ADD_SIGNAL(MethodInfo("session_stopping")); ADD_SIGNAL(MethodInfo("session_focussed")); ADD_SIGNAL(MethodInfo("session_visible")); + ADD_SIGNAL(MethodInfo("session_loss_pending")); + ADD_SIGNAL(MethodInfo("instance_exiting")); ADD_SIGNAL(MethodInfo("pose_recentered")); ADD_SIGNAL(MethodInfo("refresh_rate_changed", PropertyInfo(Variant::FLOAT, "refresh_rate"))); @@ -1258,6 +1260,14 @@ void OpenXRInterface::on_state_stopping() { emit_signal(SNAME("session_stopping")); } +void OpenXRInterface::on_state_loss_pending() { + emit_signal(SNAME("session_loss_pending")); +} + +void OpenXRInterface::on_state_exiting() { + emit_signal(SNAME("instance_exiting")); +} + void OpenXRInterface::on_pose_recentered() { emit_signal(SNAME("pose_recentered")); } diff --git a/modules/openxr/openxr_interface.h b/modules/openxr/openxr_interface.h index e916c7dac2..4c42fe6024 100644 --- a/modules/openxr/openxr_interface.h +++ b/modules/openxr/openxr_interface.h @@ -31,6 +31,29 @@ #ifndef OPENXR_INTERFACE_H #define OPENXR_INTERFACE_H +// A note on multithreading and thread safety in OpenXR. +// +// Most entry points will be called from the main thread in Godot +// however a number of entry points will be called from the +// rendering thread, potentially while we're already processing +// the next frame on the main thread. +// +// OpenXR itself has been designed with threading in mind including +// a high likelihood that the XR runtime runs in separate threads +// as well. +// Hence all the frame timing information, use of swapchains and +// sync functions. +// Do note that repeated calls to tracking APIs will provide +// increasingly more accurate data for the same timestamp as +// tracking data is continuously updated. +// +// For our code we mostly implement this in our OpenXRAPI class. +// We store data accessed from the rendering thread in a separate +// struct, setting values through our renderer command queue. +// +// As some data is setup before we start rendering, and cleaned up +// after we've stopped, that is accessed directly from both threads. + #include "action_map/openxr_action_map.h" #include "extensions/openxr_hand_tracking_extension.h" #include "openxr_api.h" @@ -173,6 +196,8 @@ public: void on_state_visible(); void on_state_focused(); void on_state_stopping(); + void on_state_loss_pending(); + void on_state_exiting(); void on_pose_recentered(); void on_refresh_rate_changes(float p_new_rate); void tracker_profile_changed(RID p_tracker, RID p_interaction_profile); diff --git a/modules/raycast/config.py b/modules/raycast/config.py index 26329d813a..0fd35af528 100644 --- a/modules/raycast/config.py +++ b/modules/raycast/config.py @@ -1,8 +1,9 @@ def can_build(env, platform): - # Supported architectures depend on the Embree library. + # Supported architectures and platforms depend on the Embree library. + if env["arch"] == "arm64" and platform == "windows": + return False if env["arch"] in ["x86_64", "arm64", "wasm32"]: return True - # x86_32 only seems supported on Windows for now. if env["arch"] == "x86_32" and platform == "windows": return True return False diff --git a/modules/text_server_adv/gdextension_build/methods.py b/modules/text_server_adv/gdextension_build/methods.py index 1c43759c55..3453c3e8f0 100644 --- a/modules/text_server_adv/gdextension_build/methods.py +++ b/modules/text_server_adv/gdextension_build/methods.py @@ -15,59 +15,61 @@ class ANSI(Enum): internal value, or an empty string in a non-colorized scope. """ - GRAY = "\x1b[0;30m" - RED = "\x1b[0;31m" - GREEN = "\x1b[0;32m" - YELLOW = "\x1b[0;33m" - BLUE = "\x1b[0;34m" - PURPLE = "\x1b[0;35m" - CYAN = "\x1b[0;36m" - WHITE = "\x1b[0;37m" - - BOLD_GRAY = "\x1b[1;90m" - BOLD_RED = "\x1b[1;91m" - BOLD_GREEN = "\x1b[1;92m" - BOLD_YELLOW = "\x1b[1;93m" - BOLD_BLUE = "\x1b[1;94m" - BOLD_PURPLE = "\x1b[1;95m" - BOLD_CYAN = "\x1b[1;96m" - BOLD_WHITE = "\x1b[1;97m" - RESET = "\x1b[0m" - def __str__(self): + BOLD = "\x1b[1m" + ITALIC = "\x1b[3m" + UNDERLINE = "\x1b[4m" + STRIKETHROUGH = "\x1b[9m" + REGULAR = "\x1b[22;23;24;29m" + + BLACK = "\x1b[30m" + RED = "\x1b[31m" + GREEN = "\x1b[32m" + YELLOW = "\x1b[33m" + BLUE = "\x1b[34m" + MAGENTA = "\x1b[35m" + CYAN = "\x1b[36m" + WHITE = "\x1b[37m" + + PURPLE = "\x1b[38;5;93m" + PINK = "\x1b[38;5;206m" + ORANGE = "\x1b[38;5;214m" + GRAY = "\x1b[38;5;244m" + + def __str__(self) -> str: global _colorize - return self.value if _colorize else "" + return str(self.value) if _colorize else "" def no_verbose(env): - colors = [ANSI.BLUE, ANSI.BOLD_BLUE, ANSI.RESET] + colors = [ANSI.BLUE, ANSI.BOLD, ANSI.REGULAR, ANSI.RESET] # There is a space before "..." to ensure that source file names can be # Ctrl + clicked in the VS Code terminal. - compile_source_message = "{0}Compiling {1}$SOURCE{0} ...{2}".format(*colors) - java_compile_source_message = "{0}Compiling {1}$SOURCE{0} ...{2}".format(*colors) - compile_shared_source_message = "{0}Compiling shared {1}$SOURCE{0} ...{2}".format(*colors) - link_program_message = "{0}Linking Program {1}$TARGET{0} ...{2}".format(*colors) - link_library_message = "{0}Linking Static Library {1}$TARGET{0} ...{2}".format(*colors) - ranlib_library_message = "{0}Ranlib Library {1}$TARGET{0} ...{2}".format(*colors) - link_shared_library_message = "{0}Linking Shared Library {1}$TARGET{0} ...{2}".format(*colors) - java_library_message = "{0}Creating Java Archive {1}$TARGET{0} ...{2}".format(*colors) - compiled_resource_message = "{0}Creating Compiled Resource {1}$TARGET{0} ...{2}".format(*colors) - generated_file_message = "{0}Generating {1}$TARGET{0} ...{2}".format(*colors) - - env.Append(CXXCOMSTR=compile_source_message) - env.Append(CCCOMSTR=compile_source_message) - env.Append(SHCCCOMSTR=compile_shared_source_message) - env.Append(SHCXXCOMSTR=compile_shared_source_message) - env.Append(ARCOMSTR=link_library_message) - env.Append(RANLIBCOMSTR=ranlib_library_message) - env.Append(SHLINKCOMSTR=link_shared_library_message) - env.Append(LINKCOMSTR=link_program_message) - env.Append(JARCOMSTR=java_library_message) - env.Append(JAVACCOMSTR=java_compile_source_message) - env.Append(RCCOMSTR=compiled_resource_message) - env.Append(GENCOMSTR=generated_file_message) + compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors) + java_compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors) + compile_shared_source_message = "{}Compiling shared {}$SOURCE{} ...{}".format(*colors) + link_program_message = "{}Linking Program {}$TARGET{} ...{}".format(*colors) + link_library_message = "{}Linking Static Library {}$TARGET{} ...{}".format(*colors) + ranlib_library_message = "{}Ranlib Library {}$TARGET{} ...{}".format(*colors) + link_shared_library_message = "{}Linking Shared Library {}$TARGET{} ...{}".format(*colors) + java_library_message = "{}Creating Java Archive {}$TARGET{} ...{}".format(*colors) + compiled_resource_message = "{}Creating Compiled Resource {}$TARGET{} ...{}".format(*colors) + generated_file_message = "{}Generating {}$TARGET{} ...{}".format(*colors) + + env["CXXCOMSTR"] = compile_source_message + env["CCCOMSTR"] = compile_source_message + env["SHCCCOMSTR"] = compile_shared_source_message + env["SHCXXCOMSTR"] = compile_shared_source_message + env["ARCOMSTR"] = link_library_message + env["RANLIBCOMSTR"] = ranlib_library_message + env["SHLINKCOMSTR"] = link_shared_library_message + env["LINKCOMSTR"] = link_program_message + env["JARCOMSTR"] = java_library_message + env["JAVACCOMSTR"] = java_compile_source_message + env["RCCOMSTR"] = compiled_resource_message + env["GENCOMSTR"] = generated_file_message def disable_warnings(self): diff --git a/modules/text_server_fb/gdextension_build/methods.py b/modules/text_server_fb/gdextension_build/methods.py index 1c43759c55..3453c3e8f0 100644 --- a/modules/text_server_fb/gdextension_build/methods.py +++ b/modules/text_server_fb/gdextension_build/methods.py @@ -15,59 +15,61 @@ class ANSI(Enum): internal value, or an empty string in a non-colorized scope. """ - GRAY = "\x1b[0;30m" - RED = "\x1b[0;31m" - GREEN = "\x1b[0;32m" - YELLOW = "\x1b[0;33m" - BLUE = "\x1b[0;34m" - PURPLE = "\x1b[0;35m" - CYAN = "\x1b[0;36m" - WHITE = "\x1b[0;37m" - - BOLD_GRAY = "\x1b[1;90m" - BOLD_RED = "\x1b[1;91m" - BOLD_GREEN = "\x1b[1;92m" - BOLD_YELLOW = "\x1b[1;93m" - BOLD_BLUE = "\x1b[1;94m" - BOLD_PURPLE = "\x1b[1;95m" - BOLD_CYAN = "\x1b[1;96m" - BOLD_WHITE = "\x1b[1;97m" - RESET = "\x1b[0m" - def __str__(self): + BOLD = "\x1b[1m" + ITALIC = "\x1b[3m" + UNDERLINE = "\x1b[4m" + STRIKETHROUGH = "\x1b[9m" + REGULAR = "\x1b[22;23;24;29m" + + BLACK = "\x1b[30m" + RED = "\x1b[31m" + GREEN = "\x1b[32m" + YELLOW = "\x1b[33m" + BLUE = "\x1b[34m" + MAGENTA = "\x1b[35m" + CYAN = "\x1b[36m" + WHITE = "\x1b[37m" + + PURPLE = "\x1b[38;5;93m" + PINK = "\x1b[38;5;206m" + ORANGE = "\x1b[38;5;214m" + GRAY = "\x1b[38;5;244m" + + def __str__(self) -> str: global _colorize - return self.value if _colorize else "" + return str(self.value) if _colorize else "" def no_verbose(env): - colors = [ANSI.BLUE, ANSI.BOLD_BLUE, ANSI.RESET] + colors = [ANSI.BLUE, ANSI.BOLD, ANSI.REGULAR, ANSI.RESET] # There is a space before "..." to ensure that source file names can be # Ctrl + clicked in the VS Code terminal. - compile_source_message = "{0}Compiling {1}$SOURCE{0} ...{2}".format(*colors) - java_compile_source_message = "{0}Compiling {1}$SOURCE{0} ...{2}".format(*colors) - compile_shared_source_message = "{0}Compiling shared {1}$SOURCE{0} ...{2}".format(*colors) - link_program_message = "{0}Linking Program {1}$TARGET{0} ...{2}".format(*colors) - link_library_message = "{0}Linking Static Library {1}$TARGET{0} ...{2}".format(*colors) - ranlib_library_message = "{0}Ranlib Library {1}$TARGET{0} ...{2}".format(*colors) - link_shared_library_message = "{0}Linking Shared Library {1}$TARGET{0} ...{2}".format(*colors) - java_library_message = "{0}Creating Java Archive {1}$TARGET{0} ...{2}".format(*colors) - compiled_resource_message = "{0}Creating Compiled Resource {1}$TARGET{0} ...{2}".format(*colors) - generated_file_message = "{0}Generating {1}$TARGET{0} ...{2}".format(*colors) - - env.Append(CXXCOMSTR=compile_source_message) - env.Append(CCCOMSTR=compile_source_message) - env.Append(SHCCCOMSTR=compile_shared_source_message) - env.Append(SHCXXCOMSTR=compile_shared_source_message) - env.Append(ARCOMSTR=link_library_message) - env.Append(RANLIBCOMSTR=ranlib_library_message) - env.Append(SHLINKCOMSTR=link_shared_library_message) - env.Append(LINKCOMSTR=link_program_message) - env.Append(JARCOMSTR=java_library_message) - env.Append(JAVACCOMSTR=java_compile_source_message) - env.Append(RCCOMSTR=compiled_resource_message) - env.Append(GENCOMSTR=generated_file_message) + compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors) + java_compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors) + compile_shared_source_message = "{}Compiling shared {}$SOURCE{} ...{}".format(*colors) + link_program_message = "{}Linking Program {}$TARGET{} ...{}".format(*colors) + link_library_message = "{}Linking Static Library {}$TARGET{} ...{}".format(*colors) + ranlib_library_message = "{}Ranlib Library {}$TARGET{} ...{}".format(*colors) + link_shared_library_message = "{}Linking Shared Library {}$TARGET{} ...{}".format(*colors) + java_library_message = "{}Creating Java Archive {}$TARGET{} ...{}".format(*colors) + compiled_resource_message = "{}Creating Compiled Resource {}$TARGET{} ...{}".format(*colors) + generated_file_message = "{}Generating {}$TARGET{} ...{}".format(*colors) + + env["CXXCOMSTR"] = compile_source_message + env["CCCOMSTR"] = compile_source_message + env["SHCCCOMSTR"] = compile_shared_source_message + env["SHCXXCOMSTR"] = compile_shared_source_message + env["ARCOMSTR"] = link_library_message + env["RANLIBCOMSTR"] = ranlib_library_message + env["SHLINKCOMSTR"] = link_shared_library_message + env["LINKCOMSTR"] = link_program_message + env["JARCOMSTR"] = java_library_message + env["JAVACCOMSTR"] = java_compile_source_message + env["RCCOMSTR"] = compiled_resource_message + env["GENCOMSTR"] = generated_file_message def disable_warnings(self): diff --git a/platform/macos/display_server_macos.h b/platform/macos/display_server_macos.h index 5d38bf55ea..083e9731c9 100644 --- a/platform/macos/display_server_macos.h +++ b/platform/macos/display_server_macos.h @@ -192,7 +192,7 @@ private: HashMap<WindowID, WindowData> windows; struct IndicatorData { - id view; + id delegate; id item; }; @@ -431,9 +431,10 @@ public: virtual void set_native_icon(const String &p_filename) override; virtual void set_icon(const Ref<Image> &p_icon) override; - virtual IndicatorID create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) override; - virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) override; + virtual IndicatorID create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback) override; + virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon) override; virtual void status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip) override; + virtual void status_indicator_set_menu(IndicatorID p_id, const RID &p_menu_rid) override; virtual void status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) override; virtual void delete_status_indicator(IndicatorID p_id) override; diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index 6461f50818..5e48602da4 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -3151,10 +3151,11 @@ void DisplayServerMacOS::set_icon(const Ref<Image> &p_icon) { } } -DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) { +DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback) { NSImage *nsimg = nullptr; if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) { - Ref<Image> img = p_icon->duplicate(); + Ref<Image> img = p_icon->get_image(); + img = img->duplicate(); img->convert(Image::FORMAT_RGBA8); NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc] @@ -3192,13 +3193,18 @@ DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref IndicatorData idat; - idat.item = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; - idat.view = [[GodotStatusItemView alloc] init]; + NSStatusItem *item = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; + idat.item = item; + idat.delegate = [[GodotStatusItemDelegate alloc] init]; + [idat.delegate setCallback:p_callback]; - [idat.view setToolTip:[NSString stringWithUTF8String:p_tooltip.utf8().get_data()]]; - [idat.view setImage:nsimg]; - [idat.view setCallback:p_callback]; - [idat.item setView:idat.view]; + item.button.image = nsimg; + item.button.imagePosition = NSImageOnly; + item.button.imageScaling = NSImageScaleProportionallyUpOrDown; + item.button.target = idat.delegate; + item.button.action = @selector(click:); + [item.button sendActionOn:(NSEventMaskLeftMouseDown | NSEventMaskRightMouseDown | NSEventMaskOtherMouseDown)]; + item.button.toolTip = [NSString stringWithUTF8String:p_tooltip.utf8().get_data()]; IndicatorID iid = indicator_id_counter++; indicators[iid] = idat; @@ -3206,12 +3212,13 @@ DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref return iid; } -void DisplayServerMacOS::status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) { +void DisplayServerMacOS::status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon) { ERR_FAIL_COND(!indicators.has(p_id)); NSImage *nsimg = nullptr; if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) { - Ref<Image> img = p_icon->duplicate(); + Ref<Image> img = p_icon->get_image(); + img = img->duplicate(); img->convert(Image::FORMAT_RGBA8); NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc] @@ -3247,19 +3254,33 @@ void DisplayServerMacOS::status_indicator_set_icon(IndicatorID p_id, const Ref<I } } - [indicators[p_id].view setImage:nsimg]; + NSStatusItem *item = indicators[p_id].item; + item.button.image = nsimg; } void DisplayServerMacOS::status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip) { ERR_FAIL_COND(!indicators.has(p_id)); - [indicators[p_id].view setToolTip:[NSString stringWithUTF8String:p_tooltip.utf8().get_data()]]; + NSStatusItem *item = indicators[p_id].item; + item.button.toolTip = [NSString stringWithUTF8String:p_tooltip.utf8().get_data()]; +} + +void DisplayServerMacOS::status_indicator_set_menu(IndicatorID p_id, const RID &p_menu_rid) { + ERR_FAIL_COND(!indicators.has(p_id)); + + NSStatusItem *item = indicators[p_id].item; + if (p_menu_rid.is_valid() && native_menu->has_menu(p_menu_rid)) { + NSMenu *menu = native_menu->get_native_menu_handle(p_menu_rid); + item.menu = menu; + } else { + item.menu = nullptr; + } } void DisplayServerMacOS::status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) { ERR_FAIL_COND(!indicators.has(p_id)); - [indicators[p_id].view setCallback:p_callback]; + [indicators[p_id].delegate setCallback:p_callback]; } void DisplayServerMacOS::delete_status_indicator(IndicatorID p_id) { diff --git a/platform/macos/godot_status_item.h b/platform/macos/godot_status_item.h index 1827baa9bd..5bc790956e 100644 --- a/platform/macos/godot_status_item.h +++ b/platform/macos/godot_status_item.h @@ -37,13 +37,12 @@ #import <AppKit/AppKit.h> #import <Foundation/Foundation.h> -@interface GodotStatusItemView : NSView { - NSImage *image; +@interface GodotStatusItemDelegate : NSObject { Callable cb; } -- (void)processMouseEvent:(NSEvent *)event index:(MouseButton)index; -- (void)setImage:(NSImage *)image; +- (IBAction)click:(id)sender; + - (void)setCallback:(const Callable &)callback; @end diff --git a/platform/macos/godot_status_item.mm b/platform/macos/godot_status_item.mm index 71ed0a0f71..0990a16b2b 100644 --- a/platform/macos/godot_status_item.mm +++ b/platform/macos/godot_status_item.mm @@ -32,30 +32,32 @@ #include "display_server_macos.h" -@implementation GodotStatusItemView +@implementation GodotStatusItemDelegate - (id)init { self = [super init]; - image = nullptr; return self; } -- (void)setImage:(NSImage *)newImage { - image = newImage; - [self setNeedsDisplayInRect:self.frame]; -} - -- (void)setCallback:(const Callable &)callback { - cb = callback; -} - -- (void)drawRect:(NSRect)rect { - if (image) { - [image drawInRect:rect]; +- (IBAction)click:(id)sender { + NSEvent *current_event = [NSApp currentEvent]; + MouseButton index = MouseButton::LEFT; + if (current_event) { + if (current_event.type == NSEventTypeLeftMouseDown) { + index = MouseButton::LEFT; + } else if (current_event.type == NSEventTypeRightMouseDown) { + index = MouseButton::RIGHT; + } else if (current_event.type == NSEventTypeOtherMouseDown) { + if ((int)[current_event buttonNumber] == 2) { + index = MouseButton::MIDDLE; + } else if ((int)[current_event buttonNumber] == 3) { + index = MouseButton::MB_XBUTTON1; + } else if ((int)[current_event buttonNumber] == 4) { + index = MouseButton::MB_XBUTTON2; + } + } } -} -- (void)processMouseEvent:(NSEvent *)event index:(MouseButton)index { DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); if (!ds) { return; @@ -71,31 +73,8 @@ } } -- (void)mouseDown:(NSEvent *)event { - [super mouseDown:event]; - if (([event modifierFlags] & NSEventModifierFlagControl)) { - [self processMouseEvent:event index:MouseButton::RIGHT]; - } else { - [self processMouseEvent:event index:MouseButton::LEFT]; - } -} - -- (void)rightMouseDown:(NSEvent *)event { - [super rightMouseDown:event]; - - [self processMouseEvent:event index:MouseButton::RIGHT]; -} - -- (void)otherMouseDown:(NSEvent *)event { - [super otherMouseDown:event]; - - if ((int)[event buttonNumber] == 2) { - [self processMouseEvent:event index:MouseButton::MIDDLE]; - } else if ((int)[event buttonNumber] == 3) { - [self processMouseEvent:event index:MouseButton::MB_XBUTTON1]; - } else if ((int)[event buttonNumber] == 4) { - [self processMouseEvent:event index:MouseButton::MB_XBUTTON2]; - } +- (void)setCallback:(const Callable &)callback { + cb = callback; } @end diff --git a/platform/macos/native_menu_macos.h b/platform/macos/native_menu_macos.h index 1d9feb64a7..b5dbb8b9b0 100644 --- a/platform/macos/native_menu_macos.h +++ b/platform/macos/native_menu_macos.h @@ -85,6 +85,8 @@ public: virtual bool has_menu(const RID &p_rid) const override; virtual void free_menu(const RID &p_rid) override; + NSMenu *get_native_menu_handle(const RID &p_rid); + virtual Size2 get_size(const RID &p_rid) const override; virtual void popup(const RID &p_rid, const Vector2i &p_position) override; diff --git a/platform/macos/native_menu_macos.mm b/platform/macos/native_menu_macos.mm index 8c2dd98862..1cf13a2d69 100644 --- a/platform/macos/native_menu_macos.mm +++ b/platform/macos/native_menu_macos.mm @@ -248,6 +248,13 @@ void NativeMenuMacOS::free_menu(const RID &p_rid) { } } +NSMenu *NativeMenuMacOS::get_native_menu_handle(const RID &p_rid) { + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, nullptr); + + return md->menu; +} + Size2 NativeMenuMacOS::get_size(const RID &p_rid) const { const MenuData *md = menus.get_or_null(p_rid); ERR_FAIL_NULL_V(md, Size2()); diff --git a/platform/web/export/export_plugin.cpp b/platform/web/export/export_plugin.cpp index 41c969b5f4..92da7799e7 100644 --- a/platform/web/export/export_plugin.cpp +++ b/platform/web/export/export_plugin.cpp @@ -170,6 +170,7 @@ void EditorExportPlatformWeb::_fix_html(Vector<uint8_t> &p_html, const Ref<Edito replaces["$GODOT_PROJECT_NAME"] = GLOBAL_GET("application/config/name"); replaces["$GODOT_HEAD_INCLUDE"] = head_include + custom_head_include; replaces["$GODOT_CONFIG"] = str_config; + replaces["$GODOT_SPLASH"] = p_name + ".png"; if (p_preset->get("variant/thread_support")) { replaces["$GODOT_THREADS_ENABLED"] = "true"; diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index ebae00017b..dbba9b4308 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -3171,14 +3171,12 @@ void DisplayServerWindows::set_icon(const Ref<Image> &p_icon) { } } -DisplayServer::IndicatorID DisplayServerWindows::create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) { +DisplayServer::IndicatorID DisplayServerWindows::create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback) { HICON hicon = nullptr; if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) { - Ref<Image> img = p_icon; - if (img != icon) { - img = img->duplicate(); - img->convert(Image::FORMAT_RGBA8); - } + Ref<Image> img = p_icon->get_image(); + img = img->duplicate(); + img->convert(Image::FORMAT_RGBA8); int w = img->get_width(); int h = img->get_height(); @@ -3241,16 +3239,14 @@ DisplayServer::IndicatorID DisplayServerWindows::create_status_indicator(const R return iid; } -void DisplayServerWindows::status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) { +void DisplayServerWindows::status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon) { ERR_FAIL_COND(!indicators.has(p_id)); HICON hicon = nullptr; if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) { - Ref<Image> img = p_icon; - if (img != icon) { - img = img->duplicate(); - img->convert(Image::FORMAT_RGBA8); - } + Ref<Image> img = p_icon->get_image(); + img = img->duplicate(); + img->convert(Image::FORMAT_RGBA8); int w = img->get_width(); int h = img->get_height(); @@ -3317,6 +3313,12 @@ void DisplayServerWindows::status_indicator_set_tooltip(IndicatorID p_id, const Shell_NotifyIconW(NIM_MODIFY, &ndat); } +void DisplayServerWindows::status_indicator_set_menu(IndicatorID p_id, const RID &p_menu_rid) { + ERR_FAIL_COND(!indicators.has(p_id)); + + indicators[p_id].menu_rid = p_menu_rid; +} + void DisplayServerWindows::status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) { ERR_FAIL_COND(!indicators.has(p_id)); @@ -3838,7 +3840,19 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA mb = MouseButton::MB_XBUTTON1; } if (indicators.has(iid)) { - if (indicators[iid].callback.is_valid()) { + if (lParam == WM_RBUTTONDOWN && indicators[iid].menu_rid.is_valid() && native_menu->has_menu(indicators[iid].menu_rid)) { + NOTIFYICONIDENTIFIER nid; + ZeroMemory(&nid, sizeof(NOTIFYICONIDENTIFIER)); + nid.cbSize = sizeof(NOTIFYICONIDENTIFIER); + nid.hWnd = windows[MAIN_WINDOW_ID].hWnd; + nid.uID = iid; + nid.guidItem = GUID_NULL; + + RECT rect; + if (Shell_NotifyIconGetRect(&nid, &rect) == S_OK) { + native_menu->popup(indicators[iid].menu_rid, Vector2i((rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2)); + } + } else if (indicators[iid].callback.is_valid()) { Variant v_button = mb; Variant v_pos = mouse_get_position(); Variant *v_args[2] = { &v_button, &v_pos }; diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index 2fe1b0733d..12350d6b34 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -464,6 +464,7 @@ class DisplayServerWindows : public DisplayServer { WNDPROC user_proc = nullptr; struct IndicatorData { + RID menu_rid; Callable callback; }; @@ -684,9 +685,10 @@ public: virtual void set_native_icon(const String &p_filename) override; virtual void set_icon(const Ref<Image> &p_icon) override; - virtual IndicatorID create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) override; - virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) override; + virtual IndicatorID create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback) override; + virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon) override; virtual void status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip) override; + virtual void status_indicator_set_menu(IndicatorID p_id, const RID &p_rid) override; virtual void status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) override; virtual void delete_status_indicator(IndicatorID p_id) override; diff --git a/scene/3d/cpu_particles_3d.cpp b/scene/3d/cpu_particles_3d.cpp index 0dc9834539..db7b80683c 100644 --- a/scene/3d/cpu_particles_3d.cpp +++ b/scene/3d/cpu_particles_3d.cpp @@ -880,7 +880,7 @@ void CPUParticles3D::_particles_process(double p_delta) { } break; case EMISSION_SHAPE_RING: { real_t ring_random_angle = Math::randf() * Math_TAU; - real_t ring_random_radius = Math::randf() * (emission_ring_radius - emission_ring_inner_radius) + emission_ring_inner_radius; + real_t ring_random_radius = Math::sqrt(Math::randf() * (emission_ring_radius - emission_ring_inner_radius * emission_ring_inner_radius) + emission_ring_inner_radius * emission_ring_inner_radius); Vector3 axis = emission_ring_axis == Vector3(0.0, 0.0, 0.0) ? Vector3(0.0, 0.0, 1.0) : emission_ring_axis.normalized(); Vector3 ortho_axis; if (axis.abs() == Vector3(1.0, 0.0, 0.0)) { diff --git a/scene/3d/xr_hand_modifier_3d.cpp b/scene/3d/xr_hand_modifier_3d.cpp index 1e78a4630f..baaa9eee48 100644 --- a/scene/3d/xr_hand_modifier_3d.cpp +++ b/scene/3d/xr_hand_modifier_3d.cpp @@ -70,6 +70,11 @@ void XRHandModifier3D::_get_joint_data() { return; } + if (has_stored_previous_transforms) { + previous_relative_transforms.clear(); + has_stored_previous_transforms = false; + } + // Table of bone names for different rig types. static const String bone_names[XRHandTracker::HAND_JOINT_MAX] = { "Palm", @@ -196,6 +201,18 @@ void XRHandModifier3D::_process_modification() { // Skip if no tracking data if (!tracker->get_has_tracking_data()) { + if (!has_stored_previous_transforms) { + return; + } + + // Apply previous relative transforms if they are stored. + for (int joint = 0; joint < XRHandTracker::HAND_JOINT_MAX; joint++) { + if (bone_update == BONE_UPDATE_FULL) { + skeleton->set_bone_pose_position(joints[joint].bone, previous_relative_transforms[joint].origin); + } + + skeleton->set_bone_pose_rotation(joints[joint].bone, Quaternion(previous_relative_transforms[joint].basis)); + } return; } @@ -223,6 +240,12 @@ void XRHandModifier3D::_process_modification() { return; } + if (!has_stored_previous_transforms) { + previous_relative_transforms.resize(XRHandTracker::HAND_JOINT_MAX); + has_stored_previous_transforms = true; + } + Transform3D *previous_relative_transforms_ptr = previous_relative_transforms.ptrw(); + for (int joint = 0; joint < XRHandTracker::HAND_JOINT_MAX; joint++) { // Get the skeleton bone (skip if none). const int bone = joints[joint].bone; @@ -233,6 +256,7 @@ void XRHandModifier3D::_process_modification() { // Calculate the relative relationship to the parent bone joint. const int parent_joint = joints[joint].parent_joint; const Transform3D relative_transform = inv_transforms[parent_joint] * transforms[joint]; + previous_relative_transforms_ptr[joint] = relative_transform; // Update the bone position if enabled by update mode. if (bone_update == BONE_UPDATE_FULL) { diff --git a/scene/3d/xr_hand_modifier_3d.h b/scene/3d/xr_hand_modifier_3d.h index 67d1694d41..3d78f32b64 100644 --- a/scene/3d/xr_hand_modifier_3d.h +++ b/scene/3d/xr_hand_modifier_3d.h @@ -73,6 +73,9 @@ private: BoneUpdate bone_update = BONE_UPDATE_FULL; JointData joints[XRHandTracker::HAND_JOINT_MAX]; + bool has_stored_previous_transforms = false; + Vector<Transform3D> previous_relative_transforms; + void _get_joint_data(); void _tracker_changed(StringName p_tracker_name, XRServer::TrackerType p_tracker_type); }; diff --git a/scene/animation/animation_mixer.cpp b/scene/animation/animation_mixer.cpp index 5a3a5f9bc0..bc5c99dbfe 100644 --- a/scene/animation/animation_mixer.cpp +++ b/scene/animation/animation_mixer.cpp @@ -1617,7 +1617,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { } if (seeked) { // Seek. - int idx = a->track_find_key(i, time, is_external_seeking ? Animation::FIND_MODE_NEAREST : Animation::FIND_MODE_EXACT, true); + int idx = a->track_find_key(i, time, Animation::FIND_MODE_NEAREST, true); if (idx < 0) { continue; } @@ -1630,6 +1630,9 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { double at_anim_pos = 0.0; switch (anim->get_loop_mode()) { case Animation::LOOP_NONE: { + if (!is_external_seeking && ((!backward && time >= pos + (double)anim->get_length()) || (backward && time <= pos))) { + continue; // Do nothing if current time is outside of length when started. + } at_anim_pos = MIN((double)anim->get_length(), time - pos); // Seek to end. } break; case Animation::LOOP_LINEAR: { @@ -1641,7 +1644,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { default: break; } - if (player2->is_playing()) { + if (player2->is_playing() || !is_external_seeking) { player2->seek(at_anim_pos, false, p_update_only); player2->play(anim_name); t->playing = true; diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index 4f90504e35..8131fe7aaa 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -624,16 +624,31 @@ Control::CursorShape CodeEdit::get_cursor_shape(const Point2 &p_pos) const { return TextEdit::get_cursor_shape(p_pos); } +void CodeEdit::_unhide_carets() { + // Unfold caret and selection origin. + for (int i = 0; i < get_caret_count(); i++) { + if (_is_line_hidden(get_caret_line(i))) { + unfold_line(get_caret_line(i)); + } + if (has_selection(i) && _is_line_hidden(get_selection_origin_line(i))) { + unfold_line(get_selection_origin_line(i)); + } + } +} + /* Text manipulation */ // Overridable actions 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) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { if (p_caret != -1 && p_caret != i) { continue; } + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } bool had_selection = has_selection(i); String selection_text = (had_selection ? get_selected_text(i) : ""); @@ -691,6 +706,7 @@ void CodeEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_ca insert_text_at_caret(chr, i); } } + end_multicaret_edit(); end_action(); } @@ -705,66 +721,80 @@ void CodeEdit::_backspace_internal(int p_caret) { } begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { if (p_caret != -1 && p_caret != i) { continue; } + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } - int cc = get_caret_column(i); - int cl = get_caret_line(i); + int to_line = get_caret_line(i); + int to_column = get_caret_column(i); - if (cc == 0 && cl == 0) { + if (to_column == 0 && to_line == 0) { continue; } - if (cl > 0 && _is_line_hidden(cl - 1)) { - unfold_line(get_caret_line(i) - 1); + if (to_line > 0 && _is_line_hidden(to_line - 1)) { + unfold_line(to_line - 1); } - int prev_line = cc ? cl : cl - 1; - int prev_column = cc ? (cc - 1) : (get_line(cl - 1).length()); + int from_line = to_column > 0 ? to_line : to_line - 1; + int from_column = to_column > 0 ? (to_column - 1) : (get_line(to_line - 1).length()); - merge_gutters(prev_line, cl); + merge_gutters(from_line, to_line); - if (auto_brace_completion_enabled && cc > 0) { - int idx = _get_auto_brace_pair_open_at_pos(cl, cc); + if (auto_brace_completion_enabled && to_column > 0) { + int idx = _get_auto_brace_pair_open_at_pos(to_line, to_column); if (idx != -1) { - prev_column = cc - auto_brace_completion_pairs[idx].open_key.length(); + from_column = to_column - auto_brace_completion_pairs[idx].open_key.length(); - if (_get_auto_brace_pair_close_at_pos(cl, cc) == idx) { - cc += auto_brace_completion_pairs[idx].close_key.length(); + if (_get_auto_brace_pair_close_at_pos(to_line, to_column) == idx) { + to_column += auto_brace_completion_pairs[idx].close_key.length(); } - - 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); - continue; } } // For space indentation we need to do a basic unindent if there are no chars to the left, acting 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; + if (indent_using_spaces && to_column != 0) { + if (get_first_non_whitespace_column(to_line) >= to_column) { + from_column = to_column - _calculate_spaces_till_next_left_indent(to_column); + from_line = to_line; } } - 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); + remove_text(from_line, from_column, to_line, to_column); - adjust_carets_after_edit(i, prev_line, prev_column, cl, cc); + set_caret_line(from_line, false, true, -1, i); + set_caret_column(from_column, i == 0, i); } - merge_overlapping_carets(); + + end_multicaret_edit(); end_complex_operation(); } +void CodeEdit::_cut_internal(int p_caret) { + // Overridden to unfold lines. + _copy_internal(p_caret); + + if (!is_editable()) { + return; + } + + if (has_selection(p_caret)) { + delete_selection(p_caret); + return; + } + if (p_caret == -1) { + delete_lines(); + } else { + unfold_line(get_caret_line(p_caret)); + remove_line_at(get_caret_line(p_caret)); + } +} + /* Indent management */ void CodeEdit::set_indent_size(const int p_size) { ERR_FAIL_COND_MSG(p_size <= 0, "Indend size must be greater than 0."); @@ -838,13 +868,17 @@ void CodeEdit::do_indent() { } begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { + if (multicaret_edit_ignore_caret(i)) { + continue; + } 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_multicaret_edit(); end_complex_operation(); } @@ -854,51 +888,28 @@ 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); + begin_multicaret_edit(); - // 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--; - } - } - - for (int i = start_line; i <= end_line; i++) { + Vector<Point2i> line_ranges = get_line_ranges_from_carets(); + for (Point2i line_range : line_ranges) { + for (int i = line_range.x; i <= line_range.y; i++) { const String line_text = get_line(i); - if (line_text.size() == 0 && has_selection(c)) { + if (line_text.size() == 0) { + // Ignore empty lines. continue; } - if (!indent_using_spaces) { - set_line(i, '\t' + line_text); - continue; + if (indent_using_spaces) { + int spaces_to_add = _calculate_spaces_till_next_right_indent(get_first_non_whitespace_column(i)); + insert_text(String(" ").repeat(spaces_to_add), i, 0, false); + } else { + insert_text("\t", i, 0, false); } - - // 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; } - - // 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); - } - set_caret_column(get_caret_column(c) + selection_offset, false, c); } + + end_multicaret_edit(); end_complex_operation(); - queue_redraw(); } void CodeEdit::unindent_lines() { @@ -907,76 +918,25 @@ void CodeEdit::unindent_lines() { } begin_complex_operation(); + begin_multicaret_edit(); - 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; - - for (int i = start_line; i <= end_line; i++) { - String line_text = get_line(i); + Vector<Point2i> line_ranges = get_line_ranges_from_carets(); + for (Point2i line_range : line_ranges) { + for (int i = line_range.x; i <= line_range.y; i++) { + const String line_text = get_line(i); if (line_text.begins_with("\t")) { - line_text = line_text.substr(1, line_text.length()); - - 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; - } - - 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. + remove_text(i, 0, i, 1); + } else if (line_text.begins_with(" ")) { + // Remove only enough spaces to align text to nearest full multiple of indentation_size. 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; - - first_line_edited = (i == start_line) ? true : first_line_edited; - last_line_edited = (i == end_line) ? true : last_line_edited; + remove_text(i, 0, i, spaces_to_remove); } } - - 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(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); } + + end_multicaret_edit(); end_complex_operation(); - queue_redraw(); } void CodeEdit::convert_indent(int p_from_line, int p_to_line) { @@ -992,27 +952,6 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) { ERR_FAIL_COND(p_to_line >= get_line_count()); ERR_FAIL_COND(p_to_line < p_from_line); - // Store caret states. - Vector<int> caret_columns; - Vector<Pair<int, int>> from_selections; - Vector<Pair<int, int>> to_selections; - caret_columns.resize(get_caret_count()); - from_selections.resize(get_caret_count()); - to_selections.resize(get_caret_count()); - for (int c = 0; c < get_caret_count(); c++) { - caret_columns.write[c] = get_caret_column(c); - - // Set "selection_from_line" to -1 to allow checking if there was a selection later. - if (!has_selection(c)) { - from_selections.write[c].first = -1; - continue; - } - from_selections.write[c].first = get_selection_from_line(c); - from_selections.write[c].second = get_selection_from_column(c); - to_selections.write[c].first = get_selection_to_line(c); - to_selections.write[c].second = get_selection_to_column(c); - } - // Check lines within range. const char32_t from_indent_char = indent_using_spaces ? '\t' : ' '; int size_diff = indent_using_spaces ? indent_size - 1 : -(indent_size - 1); @@ -1044,23 +983,10 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) { line_changed = true; if (!changed_indentation) { begin_complex_operation(); + begin_multicaret_edit(); changed_indentation = true; } - // Calculate new caret state. - for (int c = 0; c < get_caret_count(); c++) { - if (get_caret_line(c) != i || caret_columns[c] <= j) { - continue; - } - caret_columns.write[c] += size_diff; - - if (from_selections.write[c].first == -1) { - continue; - } - from_selections.write[c].second = from_selections[c].first == i ? from_selections[c].second + size_diff : from_selections[c].second; - to_selections.write[c].second = to_selections[c].first == i ? to_selections[c].second + size_diff : to_selections[c].second; - } - // Calculate new line. line = line.left(j + ((size_diff < 0) ? size_diff : 0)) + indent_text + line.substr(j + 1); @@ -1069,6 +995,7 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) { } if (line_changed) { + // Use set line to preserve carets visual position. set_line(i, line); } } @@ -1077,16 +1004,9 @@ void CodeEdit::convert_indent(int p_from_line, int p_to_line) { return; } - // Restore caret states. - for (int c = 0; c < get_caret_count(); c++) { - set_caret_column(caret_columns[c], c == 0, c); - if (from_selections.write[c].first != -1) { - select(from_selections.write[c].first, from_selections.write[c].second, to_selections.write[c].first, to_selections.write[c].second, c); - } - } merge_overlapping_carets(); + end_multicaret_edit(); end_complex_operation(); - queue_redraw(); } int CodeEdit::_calculate_spaces_till_next_left_indent(int p_column) const { @@ -1107,15 +1027,22 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) { } begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + + for (int i = 0; i < get_caret_count(); i++) { + if (multicaret_edit_ignore_caret(i)) { + continue; + } // 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); const String line = get_line(cl); - String ins = "\n"; + String ins = ""; + if (!p_above) { + ins = "\n"; + } // Append current indentation. int space_count = 0; @@ -1138,6 +1065,9 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) { } break; } + if (p_above) { + ins += "\n"; + } if (is_line_folded(cl)) { unfold_line(cl); @@ -1183,33 +1113,22 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) { } } - bool first_line = false; - if (!p_split_current_line) { + if (p_split_current_line) { + insert_text_at_caret(ins, i); + } else { + insert_text(ins, cl, p_above ? 0 : get_line(cl).length(), p_above, p_above); deselect(i); - - 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(line.length(), i == 0, i); - } + set_caret_line(p_above ? cl : cl + 1, false, true, -1, i); + set_caret_column(get_line(get_caret_line(i)).length(), i == 0, i); } - - insert_text_at_caret(ins, i); - - if (first_line) { - set_caret_line(0, i == 0, true, 0, i); - } else if (brace_indent) { + if (brace_indent) { + // Move to inner indented line. 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_multicaret_edit(); end_complex_operation(); } @@ -1700,27 +1619,8 @@ void CodeEdit::fold_line(int p_line) { _set_line_as_hidden(i, true); } - 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(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(); + // Collapse any carets in the hidden area. + collapse_carets(p_line, get_line(p_line).length(), end_line, get_line(end_line).length(), true); } void CodeEdit::unfold_line(int p_line) { @@ -1769,6 +1669,23 @@ void CodeEdit::toggle_foldable_line(int p_line) { fold_line(p_line); } +void CodeEdit::toggle_foldable_lines_at_carets() { + begin_multicaret_edit(); + int previous_line = -1; + Vector<int> sorted = get_sorted_carets(); + for (int caret_idx : sorted) { + if (multicaret_edit_ignore_caret(caret_idx)) { + continue; + } + int line_idx = get_caret_line(caret_idx); + if (line_idx != previous_line) { + toggle_foldable_line(line_idx); + previous_line = line_idx; + } + } + end_multicaret_edit(); +} + bool CodeEdit::is_line_folded(int p_line) const { ERR_FAIL_INDEX_V(p_line, get_line_count(), false); return p_line + 1 < get_line_count() && !_is_line_hidden(p_line) && _is_line_hidden(p_line + 1); @@ -1795,49 +1712,29 @@ void CodeEdit::create_code_region() { WARN_PRINT_ONCE("Cannot create code region without any one line comment delimiters"); return; } + String region_name = atr(ETR("New Code Region")); + begin_complex_operation(); - // Merge selections if selection starts on the same line the previous one ends. - Vector<int> caret_edit_order = get_caret_index_edit_order(); - Vector<int> carets_to_remove; - for (int i = 1; i < caret_edit_order.size(); i++) { - int current_caret = caret_edit_order[i - 1]; - int next_caret = caret_edit_order[i]; - if (get_selection_from_line(current_caret) == get_selection_to_line(next_caret)) { - select(get_selection_from_line(next_caret), get_selection_from_column(next_caret), get_selection_to_line(current_caret), get_selection_to_column(current_caret), next_caret); - carets_to_remove.append(current_caret); - } - } - // Sort and remove backwards to preserve indices. - carets_to_remove.sort(); - for (int i = carets_to_remove.size() - 1; i >= 0; i--) { - remove_caret(carets_to_remove[i]); - } - - // Adding start and end region tags. - int first_region_start = -1; - for (int caret_idx : get_caret_index_edit_order()) { - if (!has_selection(caret_idx)) { - continue; - } - int from_line = get_selection_from_line(caret_idx); - if (first_region_start == -1 || from_line < first_region_start) { - first_region_start = from_line; - } - int to_line = get_selection_to_line(caret_idx); - set_line(to_line, get_line(to_line) + "\n" + code_region_end_string); - insert_line_at(from_line, code_region_start_string + " " + atr(ETR("New Code Region"))); - fold_line(from_line); + begin_multicaret_edit(); + Vector<Point2i> line_ranges = get_line_ranges_from_carets(true, false); + + // Add start and end region tags. + int line_offset = 0; + for (Point2i line_range : line_ranges) { + insert_text("\n" + code_region_end_string, line_range.y + line_offset, get_line(line_range.y + line_offset).length()); + insert_line_at(line_range.x + line_offset, code_region_start_string + " " + region_name); + fold_line(line_range.x + line_offset); + line_offset += 2; } + int first_region_start = line_ranges[0].x; // Select name of the first region to allow quick edit. remove_secondary_carets(); - set_caret_line(first_region_start); - int tag_length = code_region_start_string.length() + atr(ETR("New Code Region")).length() + 1; - set_caret_column(tag_length); + int tag_length = code_region_start_string.length() + region_name.length() + 1; select(first_region_start, code_region_start_string.length() + 1, first_region_start, tag_length); + end_multicaret_edit(); end_complex_operation(); - queue_redraw(); } String CodeEdit::get_code_region_start_tag() const { @@ -2236,8 +2133,12 @@ void CodeEdit::confirm_code_completion(bool p_replace) { char32_t caret_last_completion_char = 0; begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + + for (int i = 0; i < get_caret_count(); i++) { + if (multicaret_edit_ignore_caret(i)) { + continue; + } int caret_line = get_caret_line(i); const String &insert_text = code_completion_options[code_completion_current_selected].insert_text; @@ -2270,8 +2171,6 @@ void CodeEdit::confirm_code_completion(bool p_replace) { // 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. @@ -2287,8 +2186,6 @@ void CodeEdit::confirm_code_completion(bool p_replace) { // 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); @@ -2313,12 +2210,10 @@ void CodeEdit::confirm_code_completion(bool p_replace) { if (has_string_delimiter(String::chr(last_completion_char))) { if (post_brace_pair != -1 && last_char_matches) { 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 (pre_brace_pair != -1 && pre_brace_pair != post_brace_pair && last_char_matches) { 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) { 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); @@ -2329,13 +2224,16 @@ void CodeEdit::confirm_code_completion(bool p_replace) { 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); + 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); + } else { + set_caret_column(get_caret_column(i) + 2, i == 0, i); } } } } + + end_multicaret_edit(); end_complex_operation(); cancel_code_completion(); @@ -2418,65 +2316,154 @@ void CodeEdit::set_symbol_lookup_word_as_valid(bool p_valid) { } /* Text manipulation */ -void CodeEdit::duplicate_lines() { +void CodeEdit::move_lines_up() { begin_complex_operation(); + begin_multicaret_edit(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &caret_index : caret_edit_order) { - // The text that will be inserted. All lines in one string. - String insert_text; - - // The new line position of the caret after the operation. - int new_caret_line = get_caret_line(caret_index); - // The new column position of the caret after the operation. - int new_caret_column = get_caret_column(caret_index); - // The caret positions of the selection. Stays -1 if there is no selection. - int select_from_line = -1; - int select_to_line = -1; - int select_from_column = -1; - int select_to_column = -1; - // Number of lines of the selection. - int select_num_lines = -1; - - if (has_selection(caret_index)) { - select_from_line = get_selection_from_line(caret_index); - select_to_line = get_selection_to_line(caret_index); - select_from_column = get_selection_from_column(caret_index); - select_to_column = get_selection_to_column(caret_index); - select_num_lines = select_to_line - select_from_line + 1; - - for (int i = select_from_line; i <= select_to_line; i++) { - insert_text += "\n" + get_line(i); - unfold_line(i); - } - new_caret_line = select_to_line + select_num_lines; - } else { - insert_text = "\n" + get_line(new_caret_line); - new_caret_line++; + // Move lines up by swapping each line with the one above it. + Vector<Point2i> line_ranges = get_line_ranges_from_carets(); + for (Point2i line_range : line_ranges) { + if (line_range.x == 0) { + continue; + } + unfold_line(line_range.x - 1); + for (int line = line_range.x; line <= line_range.y; line++) { + unfold_line(line); + swap_lines(line - 1, line); + } + } - unfold_line(get_caret_line(caret_index)); + // Fix selection if it ended at column 0, since it wasn't moved. + for (int i = 0; i < get_caret_count(); i++) { + if (has_selection(i) && get_selection_to_column(i) == 0 && get_selection_to_line(i) != 0) { + if (is_caret_after_selection_origin(i)) { + set_caret_line(get_caret_line(i) - 1, false, true, -1, i); + } else { + set_selection_origin_line(get_selection_origin_line(i) - 1, true, -1, i); + } } + } - // The text will be inserted at the end of the current line. - set_caret_column(get_line(get_caret_line(caret_index)).length(), false, caret_index); + end_multicaret_edit(); + end_complex_operation(); +} - deselect(caret_index); +void CodeEdit::move_lines_down() { + begin_complex_operation(); + begin_multicaret_edit(); - insert_text_at_caret(insert_text, caret_index); - set_caret_line(new_caret_line, false, true, 0, caret_index); - set_caret_column(new_caret_column, true, caret_index); + Vector<Point2i> line_ranges = get_line_ranges_from_carets(); - if (select_from_line != -1) { - // Advance the selection by the number of duplicated lines. - select_from_line += select_num_lines; - select_to_line += select_num_lines; + // Fix selection if it ended at column 0, since it won't be moved. + for (int i = 0; i < get_caret_count(); i++) { + if (has_selection(i) && get_selection_to_column(i) == 0 && get_selection_to_line(i) != get_line_count() - 1) { + if (is_caret_after_selection_origin(i)) { + set_caret_line(get_caret_line(i) + 1, false, true, -1, i); + } else { + set_selection_origin_line(get_selection_origin_line(i) + 1, true, -1, i); + } + } + } - select(select_from_line, select_from_column, select_to_line, select_to_column, caret_index); + // Move lines down by swapping each line with the one below it. + for (Point2i line_range : line_ranges) { + if (line_range.y == get_line_count() - 1) { + continue; + } + unfold_line(line_range.y + 1); + for (int line = line_range.y; line >= line_range.x; line--) { + unfold_line(line); + swap_lines(line + 1, line); } } + end_multicaret_edit(); + end_complex_operation(); +} + +void CodeEdit::delete_lines() { + begin_complex_operation(); + begin_multicaret_edit(); + + Vector<Point2i> line_ranges = get_line_ranges_from_carets(); + int line_offset = 0; + for (Point2i line_range : line_ranges) { + // Remove last line of range separately to preserve carets. + unfold_line(line_range.y + line_offset); + remove_line_at(line_range.y + line_offset); + if (line_range.x != line_range.y) { + remove_text(line_range.x + line_offset, 0, line_range.y + line_offset, 0); + } + line_offset += line_range.x - line_range.y - 1; + } + + // Deselect all. + deselect(); + + end_multicaret_edit(); + end_complex_operation(); +} + +void CodeEdit::duplicate_selection() { + begin_complex_operation(); + begin_multicaret_edit(); + + // Duplicate lines from carets without selections first. + for (int i = 0; i < get_caret_count(); i++) { + if (multicaret_edit_ignore_caret(i)) { + continue; + } + for (int l = get_selection_from_line(i); l <= get_selection_to_line(i); l++) { + unfold_line(l); + } + if (has_selection(i)) { + continue; + } + + String text_to_insert = get_line(get_caret_line(i)) + "\n"; + // Insert new text before the line, so the caret is on the second one. + insert_text(text_to_insert, get_caret_line(i), 0); + } + + // Duplicate selections. + for (int i = 0; i < get_caret_count(); i++) { + if (multicaret_edit_ignore_caret(i)) { + continue; + } + if (!has_selection(i)) { + continue; + } + + // Insert new text before the selection, so the caret is on the second one. + insert_text(get_selected_text(i), get_selection_from_line(i), get_selection_from_column(i)); + } + + end_multicaret_edit(); + end_complex_operation(); +} + +void CodeEdit::duplicate_lines() { + begin_complex_operation(); + begin_multicaret_edit(); + + Vector<Point2i> line_ranges = get_line_ranges_from_carets(false, false); + int line_offset = 0; + for (Point2i line_range : line_ranges) { + // The text that will be inserted. All lines in one string. + String text_to_insert; + + for (int i = line_range.x + line_offset; i <= line_range.y + line_offset; i++) { + text_to_insert += get_line(i) + "\n"; + unfold_line(i); + } + + // Insert new text before the line. + insert_text(text_to_insert, line_range.x + line_offset, 0); + line_offset += line_range.y - line_range.x + 1; + } + + end_multicaret_edit(); end_complex_operation(); - queue_redraw(); } /* Visual */ @@ -2578,6 +2565,7 @@ void CodeEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("fold_all_lines"), &CodeEdit::fold_all_lines); ClassDB::bind_method(D_METHOD("unfold_all_lines"), &CodeEdit::unfold_all_lines); ClassDB::bind_method(D_METHOD("toggle_foldable_line", "line"), &CodeEdit::toggle_foldable_line); + ClassDB::bind_method(D_METHOD("toggle_foldable_lines_at_carets"), &CodeEdit::toggle_foldable_lines_at_carets); ClassDB::bind_method(D_METHOD("is_line_folded", "line"), &CodeEdit::is_line_folded); ClassDB::bind_method(D_METHOD("get_folded_lines"), &CodeEdit::get_folded_lines); @@ -2679,6 +2667,10 @@ void CodeEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_symbol_lookup_word_as_valid", "valid"), &CodeEdit::set_symbol_lookup_word_as_valid); /* Text manipulation */ + ClassDB::bind_method(D_METHOD("move_lines_up"), &CodeEdit::move_lines_up); + ClassDB::bind_method(D_METHOD("move_lines_down"), &CodeEdit::move_lines_down); + ClassDB::bind_method(D_METHOD("delete_lines"), &CodeEdit::delete_lines); + ClassDB::bind_method(D_METHOD("duplicate_selection"), &CodeEdit::duplicate_selection); ClassDB::bind_method(D_METHOD("duplicate_lines"), &CodeEdit::duplicate_lines); /* Inspector */ @@ -2846,10 +2838,12 @@ 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); - set_caret_column(0); + set_selection_mode(TextEdit::SelectionMode::SELECTION_MODE_LINE); + if (p_line == get_line_count() - 1) { + select(p_line, 0, p_line, INT_MAX); + } else { + select(p_line, 0, p_line + 1, 0); + } return; } diff --git a/scene/gui/code_edit.h b/scene/gui/code_edit.h index 1770d4f4d8..56f8cce548 100644 --- a/scene/gui/code_edit.h +++ b/scene/gui/code_edit.h @@ -309,11 +309,14 @@ protected: static void _bind_compatibility_methods(); #endif + virtual void _unhide_carets() override; + /* Text manipulation */ // Overridable actions virtual void _handle_unicode_input_internal(const uint32_t p_unicode, int p_caret) override; virtual void _backspace_internal(int p_caret) override; + virtual void _cut_internal(int p_caret) override; GDVIRTUAL1(_confirm_code_completion, bool) GDVIRTUAL1(_request_code_completion, bool) @@ -409,6 +412,7 @@ public: void fold_all_lines(); void unfold_all_lines(); void toggle_foldable_line(int p_line); + void toggle_foldable_lines_at_carets(); bool is_line_folded(int p_line) const; TypedArray<int> get_folded_lines() const; @@ -489,6 +493,10 @@ public: void set_symbol_lookup_word_as_valid(bool p_valid); /* Text manipulation */ + void move_lines_up(); + void move_lines_down(); + void delete_lines(); + void duplicate_selection(); void duplicate_lines(); CodeEdit(); diff --git a/scene/gui/text_edit.compat.inc b/scene/gui/text_edit.compat.inc new file mode 100644 index 0000000000..bf73229868 --- /dev/null +++ b/scene/gui/text_edit.compat.inc @@ -0,0 +1,41 @@ +/**************************************************************************/ +/* text_edit.compat.inc */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef DISABLE_DEPRECATED + +void TextEdit::_set_selection_mode_compat_86978(SelectionMode p_mode, int p_line, int p_column, int p_caret) { + set_selection_mode(p_mode); +} + +void TextEdit::_bind_compatibility_methods() { + ClassDB::bind_compatibility_method(D_METHOD("set_selection_mode", "mode", "line", "column", "caret_index"), &TextEdit::_set_selection_mode_compat_86978, DEFVAL(-1), DEFVAL(-1), DEFVAL(0)); +} + +#endif diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 38b4ffc8ae..4fda49a877 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -29,6 +29,7 @@ /**************************************************************************/ #include "text_edit.h" +#include "text_edit.compat.inc" #include "core/config/project_settings.h" #include "core/input/input.h" @@ -451,7 +452,7 @@ void TextEdit::_notification(int p_what) { callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); } if (text_changed_dirty) { - callable_mp(this, &TextEdit::_text_changed_emit).call_deferred(); + callable_mp(this, &TextEdit::_emit_text_changed).call_deferred(); } _update_wrap_at_column(true); } break; @@ -565,9 +566,9 @@ void TextEdit::_notification(int p_what) { Vector<BraceMatchingData> brace_matching; if (highlight_matching_braces_enabled) { - brace_matching.resize(carets.size()); + brace_matching.resize(get_caret_count()); - for (int caret = 0; caret < carets.size(); caret++) { + for (int caret = 0; caret < get_caret_count(); caret++) { if (get_caret_line(caret) < 0 || get_caret_line(caret) >= text.size() || get_caret_column(caret) < 0) { continue; } @@ -1104,7 +1105,7 @@ void TextEdit::_notification(int p_what) { // Draw selections. float char_w = theme_cache.font->get_char_size(' ', theme_cache.font_size).width; - for (int c = 0; c < carets.size(); c++) { + for (int c = 0; c < get_caret_count(); c++) { if (!clipped && has_selection(c) && line >= get_selection_from_line(c) && line <= get_selection_to_line(c)) { 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); @@ -1257,7 +1258,7 @@ void TextEdit::_notification(int p_what) { } Color gl_color = current_color; - for (int c = 0; c < carets.size(); c++) { + for (int c = 0; c < get_caret_count(); 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); @@ -1271,7 +1272,7 @@ void TextEdit::_notification(int p_what) { float char_pos = char_ofs + char_margin + ofs_x; if (char_pos >= xmargin_beg) { if (highlight_matching_braces_enabled) { - for (int c = 0; c < carets.size(); c++) { + for (int c = 0; c < get_caret_count(); 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) { @@ -1562,10 +1563,15 @@ void TextEdit::_notification(int p_what) { case MainLoop::NOTIFICATION_OS_IME_UPDATE: { if (has_focus()) { + bool had_ime_text = has_ime_text(); ime_text = DisplayServer::get_singleton()->ime_get_text(); ime_selection = DisplayServer::get_singleton()->ime_get_selection(); - if (!ime_text.is_empty() && has_selection()) { + if (!had_ime_text && has_ime_text()) { + _cancel_drag_and_drop_text(); + } + + if (has_ime_text() && has_selection()) { delete_selection(); } @@ -1576,7 +1582,7 @@ void TextEdit::_notification(int p_what) { } break; case NOTIFICATION_DRAG_BEGIN: { - selecting_mode = SelectionMode::SELECTION_MODE_NONE; + set_selection_mode(SelectionMode::SELECTION_MODE_NONE); drag_action = true; dragging_minimap = false; dragging_selection = false; @@ -1587,19 +1593,31 @@ void TextEdit::_notification(int p_what) { case NOTIFICATION_DRAG_END: { if (is_drag_successful()) { if (selection_drag_attempt) { - selection_drag_attempt = false; + // Dropped elsewhere. if (is_editable() && !Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) { delete_selection(); } else if (deselect_on_focus_loss_enabled) { deselect(); } } - } else { - selection_drag_attempt = false; } + if (drag_caret_index >= 0) { + if (drag_caret_index < carets.size()) { + remove_caret(drag_caret_index); + } + drag_caret_index = -1; + } + selection_drag_attempt = false; drag_action = false; drag_caret_force_displayed = false; } break; + + case NOTIFICATION_MOUSE_EXIT_SELF: { + if (drag_caret_force_displayed) { + drag_caret_force_displayed = false; + queue_redraw(); + } + } break; } } @@ -1702,15 +1720,17 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { if (mb->get_button_index() == MouseButton::WHEEL_RIGHT) { h_scroll->set_value(h_scroll->get_value() + (100 * mb->get_factor())); } + if (mb->get_button_index() == MouseButton::LEFT) { _reset_caret_blink_timer(); apply_ime(); Point2i pos = get_line_column_at_pos(mpos); - int row = pos.y; + int line = pos.y; int col = pos.x; + // Gutters. int left_margin = theme_cache.style_normal->get_margin(SIDE_LEFT); for (int i = 0; i < gutters.size(); i++) { if (!gutters[i].draw || gutters[i].width <= 0) { @@ -1718,14 +1738,14 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { } if (mpos.x >= left_margin && mpos.x <= left_margin + gutters[i].width) { - emit_signal(SNAME("gutter_clicked"), row, i); + emit_signal(SNAME("gutter_clicked"), line, i); return; } left_margin += gutters[i].width; } - // Minimap + // Minimap. if (draw_minimap) { _update_minimap_click(); if (dragging_minimap) { @@ -1733,121 +1753,86 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { } } + // Update caret. + int caret = carets.size() - 1; int prev_col = get_caret_column(caret); int prev_line = get_caret_line(caret); + int mouse_over_selection_caret = get_selection_at_line_column(line, col, true); + 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 (!mb->is_double_click() && !is_triple_click) { if (mb->is_alt_pressed()) { - prev_line = row; + prev_line = line; prev_col = col; // Remove caret at clicked location. - if (carets.size() > 1) { - for (int i = 0; i < carets.size(); i++) { - // Deselect if clicked on caret or its selection. - if ((get_caret_column(i) == col && get_caret_line(i) == row) || is_mouse_over_selection(true, i)) { - remove_caret(i); - last_dblclk = 0; - return; - } + if (get_caret_count() > 1) { + // Deselect if clicked on caret or its selection. + int clicked_caret = get_selection_at_line_column(line, col, true, false); + if (clicked_caret != -1) { + remove_caret(clicked_caret); + last_dblclk = 0; + return; } } - if (is_mouse_over_selection()) { + if (mouse_over_selection_caret >= 0) { + // Did not remove selection under mouse, don't add a new caret. return; } - caret = add_caret(row, col); + // Create new caret at clicked location. + caret = add_caret(line, 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() && !is_mouse_over_selection()) { - caret = 0; - remove_secondary_carets(); - } - } - - _push_current_op(); - 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 (get_selection_from_line(caret) > get_selection_to_line(caret) || (get_selection_from_line(caret) == get_selection_to_line(caret) && get_selection_from_column(caret) > get_selection_to_column(caret))) { - 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 { - carets.write[caret].selection.shiftclick_left = true; - } - 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 (carets[caret].line < get_selection_line(caret) || (carets[caret].line == get_selection_line(caret) && carets[caret].column < get_selection_column(caret))) { - if (carets[caret].selection.shiftclick_left) { - carets.write[caret].selection.shiftclick_left = !carets[caret].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 > get_selection_line(caret) || (carets[caret].line == get_selection_line(caret) && carets[caret].column > get_selection_column(caret))) { - 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; - } - carets.write[caret].selection.to_column = carets[caret].column; - carets.write[caret].selection.to_line = carets[caret].line; - + } else if (!mb->is_shift_pressed()) { + if (drag_and_drop_selection_enabled && mouse_over_selection_caret >= 0) { + // Try to drag and drop. + set_selection_mode(SelectionMode::SELECTION_MODE_NONE); + selection_drag_attempt = true; + drag_and_drop_origin_caret_index = mouse_over_selection_caret; + last_dblclk = 0; + // Don't update caret until we know if it is not drag and drop. + return; } else { - deselect(caret); + // A regular click clears all other carets. + caret = 0; + remove_secondary_carets(); + deselect(); } - caret_index_edit_dirty = true; - merge_overlapping_carets(); - queue_redraw(); } - } else if (drag_and_drop_selection_enabled && is_mouse_over_selection()) { - 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); - } - if (is_triple_click) { - // Triple-click select line. - selecting_mode = SelectionMode::SELECTION_MODE_LINE; + _push_current_op(); + set_caret_line(line, false, true, -1, caret); + set_caret_column(col, false, caret); selection_drag_attempt = false; - _update_selection_mode_line(); + bool caret_moved = get_caret_column(caret) != prev_col || get_caret_line(caret) != prev_line; + + if (selecting_enabled && mb->is_shift_pressed() && !has_selection(caret) && caret_moved) { + // Select from the previous caret position. + select(prev_line, prev_col, line, col, caret); + } + + // Start regular select mode. + set_selection_mode(SelectionMode::SELECTION_MODE_POINTER); + _update_selection_mode_pointer(true); + } else if (is_triple_click) { + // Start triple-click select line mode. + set_selection_mode(SelectionMode::SELECTION_MODE_LINE); + _update_selection_mode_line(true); last_dblclk = 0; - } else if (mb->is_double_click() && text[get_caret_line(caret)].length()) { - // Double-click select word. - selecting_mode = SelectionMode::SELECTION_MODE_WORD; - _update_selection_mode_word(); + } else if (mb->is_double_click()) { + // Start double-click select word mode. + set_selection_mode(SelectionMode::SELECTION_MODE_WORD); + _update_selection_mode_word(true); last_dblclk = OS::get_singleton()->get_ticks_msec(); last_dblclk_pos = mb->get_position(); } @@ -1863,34 +1848,20 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { _push_current_op(); _reset_caret_blink_timer(); apply_ime(); + _cancel_drag_and_drop_text(); Point2i pos = get_line_column_at_pos(mpos); - int row = pos.y; - int col = pos.x; + int mouse_line = pos.y; + int mouse_column = pos.x; - bool selection_clicked = false; if (is_move_caret_on_right_click_enabled()) { - if (has_selection()) { - for (int i = 0; i < get_caret_count(); i++) { - int from_line = get_selection_from_line(i); - int to_line = get_selection_to_line(i); - int from_column = get_selection_from_column(i); - int to_column = get_selection_to_column(i); - - if (row >= from_line && row <= to_line && (row != from_line || col >= from_column) && (row != to_line || col <= to_column)) { - // Right click in one of the selected text - selection_clicked = true; - break; - } - } - } + bool selection_clicked = get_selection_at_line_column(mouse_line, mouse_column, true) >= 0; if (!selection_clicked) { deselect(); remove_secondary_carets(); - set_caret_line(row, false, false); - set_caret_column(col); + set_caret_line(mouse_line, false, false, -1); + set_caret_column(mouse_column); } - merge_overlapping_carets(); } if (context_menu_enabled) { @@ -1908,22 +1879,20 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { } if (mb->get_button_index() == MouseButton::LEFT) { - if (selection_drag_attempt && is_mouse_over_selection()) { + if (!drag_action && selection_drag_attempt && is_mouse_over_selection()) { + // This is not a drag and drop attempt, update the caret. + selection_drag_attempt = false; remove_secondary_carets(); + deselect(); Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); - set_caret_line(pos.y, false, true, 0, 0); + set_caret_line(pos.y, false, true, -1, 0); set_caret_column(pos.x, true, 0); - - deselect(); } dragging_minimap = false; dragging_selection = false; can_drag_minimap = false; click_select_held->stop(); - if (!drag_action) { - selection_drag_attempt = false; - } if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text()); } @@ -1958,7 +1927,8 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { mpos.x = get_size().x - mpos.x; } - if (mm->get_button_mask().has_flag(MouseButtonMask::LEFT) && get_viewport()->gui_get_drag_data() == Variant()) { // Ignore if dragging. + if (mm->get_button_mask().has_flag(MouseButtonMask::LEFT) && get_viewport()->gui_get_drag_data() == Variant()) { + // Update if not in drag and drop. _reset_caret_blink_timer(); if (draw_minimap && !dragging_selection) { @@ -2011,10 +1981,19 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { if (drag_action && can_drop_data(mpos, get_viewport()->gui_get_drag_data())) { apply_ime(); + // Update drag and drop caret. drag_caret_force_displayed = true; Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); - set_caret_line(pos.y, false, true, 0, 0); - set_caret_column(pos.x, true, 0); + + if (drag_caret_index == -1) { + // Force create a new caret for drag and drop. + carets.push_back(Caret()); + drag_caret_index = carets.size() - 1; + } + + drag_caret_force_displayed = true; + set_caret_line(pos.y, false, true, -1, drag_caret_index); + set_caret_column(pos.x, true, drag_caret_index); dragging_selection = true; } } @@ -2043,6 +2022,8 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { return; } + _cancel_drag_and_drop_text(); + _reset_caret_blink_timer(); // Allow unicode handling if: @@ -2321,42 +2302,36 @@ void TextEdit::_new_line(bool p_split_current_line, bool p_above) { } begin_complex_operation(); - 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(text[get_caret_line(i)].length(), i == 0, i); - } - } - - insert_text_at_caret("\n", i); + begin_multicaret_edit(); - if (first_line) { - set_caret_line(0, i == 0, true, 0, i); + for (int i = 0; i < get_caret_count(); i++) { + if (multicaret_edit_ignore_caret(i)) { + continue; + } + if (p_split_current_line) { + insert_text_at_caret("\n", i); + } else { + int line = get_caret_line(i); + insert_text("\n", line, p_above ? 0 : text[line].length(), p_above, p_above); + deselect(i); + set_caret_line(p_above ? line : line + 1, false, true, -1, i); + set_caret_column(0, i == 0, i); } } + + end_multicaret_edit(); end_complex_operation(); } void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); 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_line(get_selection_from_line(i), false, true, -1, i); set_caret_column(get_selection_from_column(i), i == 0, i); deselect(i); continue; @@ -2368,7 +2343,7 @@ void TextEdit::_move_caret_left(bool p_select, bool 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_line(get_caret_line(i) - 1, false, true, -1, i); set_caret_column(text[get_caret_line(i)].length(), i == 0, i); } else { PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(i))->get_rid()); @@ -2389,7 +2364,8 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { // 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); + int new_caret_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_caret_line, false, true, -1, i); set_caret_column(text[get_caret_line(i)].length(), i == 0, i); } } else { @@ -2400,23 +2376,19 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { } } } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); 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_line(get_selection_to_line(i), false, true, -1, i); set_caret_column(get_selection_to_column(i), i == 0, i); deselect(i); continue; @@ -2428,7 +2400,7 @@ void TextEdit::_move_caret_right(bool p_select, bool 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_line(get_caret_line(i) + 1, false, true, -1, i); set_caret_column(0, i == 0, i); } else { PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(i))->get_rid()); @@ -2449,7 +2421,8 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { // 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); + int new_caret_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_caret_line, false, false, -1, i); set_caret_column(0, i == 0, i); } } else { @@ -2460,17 +2433,13 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { } } } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_up(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2490,17 +2459,13 @@ void TextEdit::_move_caret_up(bool p_select) { set_caret_line(new_line, i == 0, false, 0, i); } } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_down(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2516,17 +2481,13 @@ void TextEdit::_move_caret_down(bool p_select) { 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(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_to_line_start(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2551,17 +2512,13 @@ void TextEdit::_move_caret_to_line_start(bool p_select) { } else { set_caret_column(row_start_col, i == 0, i); } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_to_line_end(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2580,17 +2537,13 @@ void TextEdit::_move_caret_to_line_end(bool p_select) { } else { set_caret_column(row_end_col, i == 0, i); } - - if (p_select) { - _post_shift_selection(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_page_up(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2600,17 +2553,13 @@ void TextEdit::_move_caret_page_up(bool p_select) { 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(i); - } } merge_overlapping_carets(); } void TextEdit::_move_caret_page_down(bool p_select) { _push_current_op(); - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); i++) { if (p_select) { _pre_shift_selection(i); } else { @@ -2620,10 +2569,6 @@ void TextEdit::_move_caret_page_down(bool p_select) { 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(i); - } } merge_overlapping_carets(); } @@ -2634,58 +2579,47 @@ void TextEdit::_do_backspace(bool p_word, bool p_all_to_left) { } start_action(EditAction::ACTION_BACKSPACE); - Vector<int> carets_to_remove; + begin_multicaret_edit(); - 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)) { + Vector<int> sorted_carets = get_sorted_carets(); + sorted_carets.reverse(); + for (int i = 0; i < sorted_carets.size(); i++) { + int caret_index = sorted_carets[i]; + if (multicaret_edit_ignore_caret(caret_index)) { continue; } - if (has_selection(caret_idx) || (!p_all_to_left && !p_word) || get_caret_column(caret_idx) == 0) { - backspace(caret_idx); + if (get_caret_column(caret_index) == 0 && get_caret_line(caret_index) == 0 && !has_selection(caret_index)) { continue; } - 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; - } + if (has_selection(caret_index) || (!p_all_to_left && !p_word) || get_caret_column(caret_index) == 0) { + backspace(caret_index); + continue; + } - 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; - } + if (p_all_to_left) { + // Remove everything to left of caret to the start of the line. + int caret_current_column = get_caret_column(caret_index); + _remove_text(get_caret_line(caret_index), 0, get_caret_line(caret_index), caret_current_column); + collapse_carets(get_caret_line(caret_index), 0, get_caret_line(caret_index), caret_current_column); + set_caret_column(0, caret_index == 0, caret_index); + _offset_carets_after(get_caret_line(caret_index), caret_current_column, get_caret_line(caret_index), 0); continue; } 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); + // Remove text to the start of the word left of the caret. + int from_column = get_caret_column(caret_index); + int column = get_caret_column(caret_index); // 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])) { + if (is_whitespace(text[get_caret_line(caret_index)][get_caret_column(caret_index) - 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()); + const PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(caret_index))->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; @@ -2699,57 +2633,14 @@ void TextEdit::_do_backspace(bool p_word, bool p_all_to_left) { } } - // 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); - adjust_carets_after_edit(caret_idx, get_caret_line(caret_idx), column, get_caret_line(caret_idx), from_column); - - // Now we can clean up the overlapping 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; + _remove_text(get_caret_line(caret_index), column, get_caret_line(caret_index), from_column); + collapse_carets(get_caret_line(caret_index), column, get_caret_line(caret_index), from_column); + set_caret_column(column, caret_index == 0, caret_index); + _offset_carets_after(get_caret_line(caret_index), from_column, get_caret_line(caret_index), 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]); - } + end_multicaret_edit(); end_action(); } @@ -2759,61 +2650,40 @@ void TextEdit::_delete(bool p_word, bool p_all_to_right) { } start_action(EditAction::ACTION_DELETE); - Vector<int> carets_to_remove; + begin_multicaret_edit(); - 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); + Vector<int> sorted_carets = get_sorted_carets(); + for (int i = 0; i < sorted_carets.size(); i++) { + int caret_index = sorted_carets[i]; + if (multicaret_edit_ignore_caret(caret_index)) { continue; } - int curline_len = text[get_caret_line(caret_idx)].length(); - if (get_caret_line(caret_idx) == text.size() - 1 && get_caret_column(caret_idx) == curline_len) { + if (has_selection(caret_index)) { + delete_selection(caret_index); + continue; + } + + int curline_len = text[get_caret_line(caret_index)].length(); + if (get_caret_line(caret_index) == text.size() - 1 && get_caret_column(caret_index) == curline_len) { continue; // Last line, last column: Nothing to do. } - int next_line = get_caret_column(caret_idx) < curline_len ? get_caret_line(caret_idx) : get_caret_line(caret_idx) + 1; + int next_line = get_caret_column(caret_index) < curline_len ? get_caret_line(caret_index) : get_caret_line(caret_index) + 1; int next_column; 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]; - } - } - - if (get_caret_column(caret_idx) == curline_len) { + if (get_caret_column(caret_index) == 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) { + next_line = get_caret_line(caret_index); + } else if (p_word && get_caret_column(caret_index) < curline_len - 1) { // Delete next word to right of caret. - int line = get_caret_line(caret_idx); - int column = get_caret_column(caret_idx); + int line = get_caret_line(caret_index); + int column = get_caret_column(caret_index); 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) { @@ -2825,49 +2695,22 @@ void TextEdit::_delete(bool p_word, bool p_all_to_right) { 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 { // Delete one character. if (caret_mid_grapheme_enabled) { - next_column = get_caret_column(caret_idx) < curline_len ? (get_caret_column(caret_idx) + 1) : 0; + next_column = get_caret_column(caret_index) < curline_len ? (get_caret_column(caret_index) + 1) : 0; } else { - next_column = get_caret_column(caret_idx) < curline_len ? TS->shaped_text_next_character_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); - } + next_column = get_caret_column(caret_index) < curline_len ? TS->shaped_text_next_character_pos(text.get_line_data(get_caret_line(caret_index))->get_rid(), (get_caret_column(caret_index))) : 0; } } - _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(get_caret_line(caret_index), get_caret_column(caret_index), next_line, next_column); + collapse_carets(get_caret_line(caret_index), get_caret_column(caret_index), next_line, next_column); + _offset_carets_after(next_line, next_column, get_caret_line(caret_index), get_caret_column(caret_index)); } - // 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_multicaret_edit(); end_action(); - queue_redraw(); } void TextEdit::_move_caret_document_start(bool p_select) { @@ -2878,12 +2721,8 @@ void TextEdit::_move_caret_document_start(bool p_select) { deselect(); } - set_caret_line(0, false); + set_caret_line(0, false, true, -1); set_caret_column(0); - - if (p_select) { - _post_shift_selection(0); - } } void TextEdit::_move_caret_document_end(bool p_select) { @@ -2894,12 +2733,8 @@ void TextEdit::_move_caret_document_end(bool p_select) { deselect(); } - set_caret_line(get_last_unhidden_line(), true, false, 9999); + set_caret_line(get_last_unhidden_line(), true, false, -1); set_caret_column(text[get_caret_line()].length()); - - if (p_select) { - _post_shift_selection(0); - } } bool TextEdit::_clear_carets_and_selection() { @@ -2917,51 +2752,6 @@ bool TextEdit::_clear_carets_and_selection() { return false; } -void TextEdit::_get_above_below_caret_line_column(int p_old_line, int p_old_wrap_index, int p_old_column, bool p_below, int &p_new_line, int &p_new_column, int p_last_fit_x) const { - if (p_last_fit_x == -1) { - p_last_fit_x = _get_column_x_offset_for_line(p_old_column, p_old_line, p_old_column); - } - - // Calculate the new line and wrap index. - p_new_line = p_old_line; - int caret_wrap_index = p_old_wrap_index; - if (p_below) { - if (caret_wrap_index < get_line_wrap_count(p_new_line)) { - caret_wrap_index++; - } else { - p_new_line++; - caret_wrap_index = 0; - } - } else { - if (caret_wrap_index == 0) { - p_new_line--; - caret_wrap_index = get_line_wrap_count(p_new_line); - } else { - caret_wrap_index--; - } - } - - // Boundary checks. - if (p_new_line < 0) { - p_new_line = 0; - } - if (p_new_line >= text.size()) { - p_new_line = text.size() - 1; - } - - p_new_column = _get_char_pos_for_line(p_last_fit_x, p_new_line, caret_wrap_index); - if (p_new_column != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && caret_wrap_index < get_line_wrap_count(p_new_line)) { - Vector<String> rows = get_line_wrapped_text(p_new_line); - int row_end_col = 0; - for (int i = 0; i < caret_wrap_index + 1; i++) { - row_end_col += rows[i].length(); - } - if (p_new_column >= row_end_col) { - p_new_column -= 1; - } - } -} - void TextEdit::_update_placeholder() { if (theme_cache.font.is_null() || theme_cache.font_size <= 0) { return; // Not in tree? @@ -3127,53 +2917,48 @@ void TextEdit::drop_data(const Point2 &p_point, const Variant &p_data) { if (p_data.get_type() == Variant::STRING && is_editable()) { Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); - int caret_row_tmp = pos.y; - int caret_column_tmp = pos.x; + int drop_at_line = pos.y; + int drop_at_column = pos.x; + int selection_index = get_selection_at_line_column(drop_at_line, drop_at_column, !Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)); + + // Remove drag caret before the complex operation starts so it won't appear in undo. + remove_caret(drag_caret_index); + + if (selection_drag_attempt && selection_index >= 0 && selection_index == drag_and_drop_origin_caret_index) { + // Dropped onto original selection, do nothing. + selection_drag_attempt = false; + return; + } + + begin_complex_operation(); + begin_multicaret_edit(); if (selection_drag_attempt) { + // Drop from self. selection_drag_attempt = false; - if (!is_mouse_over_selection(!Input::get_singleton()->is_key_pressed(Key::CMD_OR_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::CMD_OR_CTRL)) { - 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(); - } + if (!Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL)) { + // Delete all selections. + int temp_caret = add_caret(drop_at_line, drop_at_column); - 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()) { - 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); - insert_text_at_caret(p_data); - grab_focus(); - } + delete_selection(); - 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()); + // Use a temporary caret to update the drop at position. + drop_at_line = get_caret_line(temp_caret); + drop_at_column = get_caret_column(temp_caret); + } } + remove_secondary_carets(); + deselect(); + + // Insert the dragged text. + set_caret_line(drop_at_line, true, false, -1); + set_caret_column(drop_at_column); + insert_text_at_caret(p_data); + + select(drop_at_line, drop_at_column, get_caret_line(), get_caret_column()); + grab_focus(); + adjust_viewport_to_caret(); + end_multicaret_edit(); + end_complex_operation(); } } @@ -3459,7 +3244,7 @@ void TextEdit::_clear() { clear_undo_history(); text.clear(); remove_secondary_carets(); - set_caret_line(0, false); + set_caret_line(0, false, true, -1); set_caret_column(0); first_visible_col = 0; first_visible_line = 0; @@ -3532,17 +3317,36 @@ void TextEdit::set_line(int p_line, const String &p_new_text) { return; } begin_complex_operation(); - _remove_text(p_line, 0, p_line, text[p_line].length()); - _insert_text(p_line, 0, p_new_text); - 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); + + int old_column = text[p_line].length(); + + // Set the affected carets column to update their last offset x. + for (int i = 0; i < get_caret_count(); i++) { + if (_is_line_col_in_range(get_caret_line(i), get_caret_column(i), p_line, 0, p_line, old_column)) { + set_caret_column(get_caret_column(i), false, i); } + if (has_selection(i) && _is_line_col_in_range(get_selection_origin_line(i), get_selection_origin_column(i), p_line, 0, p_line, old_column)) { + set_selection_origin_column(get_selection_origin_column(i), i); + } + } + + _remove_text(p_line, 0, p_line, old_column); + int new_line, new_column; + _insert_text(p_line, 0, p_new_text, &new_line, &new_column); - 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(); + // Don't offset carets that were on the old line. + _offset_carets_after(p_line, old_column, new_line, new_column, false, false); + + // Set the caret lines to update the column to match visually. + for (int i = 0; i < get_caret_count(); i++) { + if (_is_line_col_in_range(get_caret_line(i), get_caret_column(i), p_line, 0, p_line, old_column)) { + set_caret_line(get_caret_line(i), false, true, 0, i); + } + if (has_selection(i) && _is_line_col_in_range(get_selection_origin_line(i), get_selection_origin_column(i), p_line, 0, p_line, old_column)) { + set_selection_origin_line(get_selection_origin_line(i), true, 0, i); } } + merge_overlapping_carets(); end_complex_operation(); } @@ -3596,71 +3400,163 @@ void TextEdit::swap_lines(int p_from_line, int p_to_line) { ERR_FAIL_INDEX(p_from_line, text.size()); ERR_FAIL_INDEX(p_to_line, text.size()); - String tmp = get_line(p_from_line); - String tmp2 = get_line(p_to_line); + if (p_from_line == p_to_line) { + return; + } + + String from_line_text = get_line(p_from_line); + String to_line_text = get_line(p_to_line); begin_complex_operation(); - set_line(p_to_line, tmp); - set_line(p_from_line, tmp2); + begin_multicaret_edit(); + // Don't use set_line to avoid clamping and updating carets. + _remove_text(p_to_line, 0, p_to_line, text[p_to_line].length()); + _insert_text(p_to_line, 0, from_line_text); + _remove_text(p_from_line, 0, p_from_line, text[p_from_line].length()); + _insert_text(p_from_line, 0, to_line_text); + + // Swap carets. + for (int i = 0; i < get_caret_count(); i++) { + bool selected = has_selection(i); + if (get_caret_line(i) == p_from_line || get_caret_line(i) == p_to_line) { + int caret_new_line = get_caret_line(i) == p_from_line ? p_to_line : p_from_line; + int caret_column = get_caret_column(i); + set_caret_line(caret_new_line, false, true, -1, i); + set_caret_column(caret_column, false, i); + } + if (selected && (get_selection_origin_line(i) == p_from_line || get_selection_origin_line(i) == p_to_line)) { + int origin_new_line = get_selection_origin_line(i) == p_from_line ? p_to_line : p_from_line; + int origin_column = get_selection_origin_column(i); + select(origin_new_line, origin_column, get_caret_line(i), get_caret_column(i), i); + } + } + // If only part of a selection was changed, it may now overlap. + merge_overlapping_carets(); + + end_multicaret_edit(); end_complex_operation(); } -void TextEdit::insert_line_at(int p_at, const String &p_text) { - ERR_FAIL_INDEX(p_at, text.size()); +void TextEdit::insert_line_at(int p_line, const String &p_text) { + ERR_FAIL_INDEX(p_line, text.size()); - _insert_text(p_at, 0, p_text + "\n"); + // Use a complex operation so subsequent calls aren't merged together. + begin_complex_operation(); - 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); + int new_line, new_column; + _insert_text(p_line, 0, p_text + "\n", &new_line, &new_column); + _offset_carets_after(p_line, 0, new_line, new_column); + + end_complex_operation(); +} + +void TextEdit::remove_line_at(int p_line, bool p_move_carets_down) { + ERR_FAIL_INDEX(p_line, text.size()); + + if (get_line_count() == 1) { + // Only one line, just remove contents. + begin_complex_operation(); + int line_length = get_line(p_line).length(); + _remove_text(p_line, 0, p_line, line_length); + collapse_carets(p_line, 0, p_line, line_length, true); + end_complex_operation(); + return; + } + + begin_complex_operation(); + + bool is_last_line = p_line == get_line_count() - 1; + int from_line = is_last_line ? p_line - 1 : p_line; + int next_line = is_last_line ? p_line : p_line + 1; + int from_column = is_last_line ? get_line(from_line).length() : 0; + int next_column = is_last_line ? get_line(next_line).length() : 0; + + if ((!is_last_line && p_move_carets_down) || (p_line != 0 && !p_move_carets_down)) { + // Set the carets column to update their last offset x. + for (int i = 0; i < get_caret_count(); i++) { + if (get_caret_line(i) == p_line) { + set_caret_column(get_caret_column(i), false, i); + } + if (has_selection(i) && get_selection_origin_line(i) == p_line) { + set_selection_origin_column(get_selection_origin_column(i), i); } } } - // Need to apply the above adjustments to the undo / redo carets. - current_op.end_carets = carets; - queue_redraw(); + // Remove line. + _remove_text(from_line, from_column, next_line, next_column); + + begin_multicaret_edit(); + if ((is_last_line && p_move_carets_down) || (p_line == 0 && !p_move_carets_down)) { + // Collapse carets. + collapse_carets(from_line, from_column, next_line, next_column, true); + } else { + // Move carets to visually line up. + int target_line = p_move_carets_down ? p_line : p_line - 1; + for (int i = 0; i < get_caret_count(); i++) { + bool selected = has_selection(i); + if (get_caret_line(i) == p_line) { + set_caret_line(target_line, i == 0, true, 0, i); + } + if (selected && get_selection_origin_line(i) == p_line) { + set_selection_origin_line(target_line, true, 0, i); + select(get_selection_origin_line(i), get_selection_origin_column(i), get_caret_line(i), get_caret_column(i), i); + } + } + + merge_overlapping_carets(); + } + _offset_carets_after(next_line, next_column, from_line, from_column); + end_multicaret_edit(); + end_complex_operation(); } void TextEdit::insert_text_at_caret(const String &p_text, int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { if (p_caret != -1 && p_caret != i) { continue; } + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } delete_selection(i); int from_line = get_caret_line(i); int from_col = get_caret_column(i); - int new_column, new_line; + int new_line, new_column; _insert_text(from_line, from_col, p_text, &new_line, &new_column); _update_scrollbars(); + _offset_carets_after(from_line, from_col, new_line, new_column); - set_caret_line(new_line, false, true, 0, i); + set_caret_line(new_line, false, true, -1, i); set_caret_column(new_column, i == 0, i); - - adjust_carets_after_edit(i, new_line, new_column, from_line, from_col); } if (has_ime_text()) { _update_ime_text(); } + end_multicaret_edit(); + end_complex_operation(); +} + +void TextEdit::insert_text(const String &p_text, int p_line, int p_column, bool p_before_selection_begin, bool p_before_selection_end) { + ERR_FAIL_INDEX(p_line, text.size()); + ERR_FAIL_INDEX(p_column, text[p_line].length() + 1); + + begin_complex_operation(); + + int new_line, new_column; + _insert_text(p_line, p_column, p_text, &new_line, &new_column); + + _offset_carets_after(p_line, p_column, new_line, new_column, p_before_selection_begin, p_before_selection_end); + 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) { @@ -3671,7 +3567,13 @@ void TextEdit::remove_text(int p_from_line, int p_from_column, int p_to_line, in ERR_FAIL_COND(p_to_line < p_from_line); ERR_FAIL_COND(p_to_line == p_from_line && p_to_column < p_from_column); + begin_complex_operation(); + _remove_text(p_from_line, p_from_column, p_to_line, p_to_column); + collapse_carets(p_from_line, p_from_column, p_to_line, p_to_column); + _offset_carets_after(p_to_line, p_to_column, p_from_line, p_from_column); + + end_complex_operation(); } int TextEdit::get_last_unhidden_line() const { @@ -4040,7 +3942,7 @@ void TextEdit::undo() { _push_current_op(); if (undo_stack_pos == nullptr) { - if (!undo_stack.size()) { + if (undo_stack.is_empty()) { return; // Nothing to undo. } @@ -4059,6 +3961,7 @@ void TextEdit::undo() { current_op.version = op.prev_version; if (undo_stack_pos->get().chain_backward) { + // This was part of a complex operation, undo until the chain forward at the start of the complex operation. while (true) { ERR_BREAK(!undo_stack_pos->prev()); undo_stack_pos = undo_stack_pos->prev(); @@ -4072,9 +3975,9 @@ void TextEdit::undo() { } _update_scrollbars(); - bool dirty_carets = carets.size() != undo_stack_pos->get().start_carets.size(); + bool dirty_carets = get_caret_count() != undo_stack_pos->get().start_carets.size(); if (!dirty_carets) { - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); 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; @@ -4084,11 +3987,11 @@ void TextEdit::undo() { carets = undo_stack_pos->get().start_carets; - if (dirty_carets && !caret_pos_dirty) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); - } - caret_pos_dirty = true; + _unhide_carets(); + + if (dirty_carets) { + _caret_changed(); + _selection_changed(); } adjust_viewport_to_caret(); } @@ -4113,6 +4016,7 @@ void TextEdit::redo() { _do_text_op(op, false); current_op.version = op.version; if (undo_stack_pos->get().chain_forward) { + // This was part of a complex operation, redo until the chain backward at the end of the complex operation. while (true) { ERR_BREAK(!undo_stack_pos->next()); undo_stack_pos = undo_stack_pos->next(); @@ -4126,9 +4030,9 @@ void TextEdit::redo() { } _update_scrollbars(); - bool dirty_carets = carets.size() != undo_stack_pos->get().end_carets.size(); + bool dirty_carets = get_caret_count() != undo_stack_pos->get().end_carets.size(); if (!dirty_carets) { - for (int i = 0; i < carets.size(); i++) { + for (int i = 0; i < get_caret_count(); 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; @@ -4139,11 +4043,11 @@ void TextEdit::redo() { carets = undo_stack_pos->get().end_carets; undo_stack_pos = undo_stack_pos->next(); - if (dirty_carets && !caret_pos_dirty) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); - } - caret_pos_dirty = true; + _unhide_carets(); + + if (dirty_carets) { + _caret_changed(); + _selection_changed(); } adjust_viewport_to_caret(); } @@ -4358,13 +4262,7 @@ Point2i TextEdit::get_line_column_at_pos(const Point2i &p_pos, bool p_allow_out_ } } - if (row < 0) { - row = 0; - } - - if (row >= text.size()) { - row = text.size() - 1; - } + row = CLAMP(row, 0, text.size() - 1); int visible_lines = get_visible_line_count_in_range(first_vis_line, row); if (rows > visible_lines) { @@ -4510,29 +4408,13 @@ bool TextEdit::is_dragging_cursor() const { } 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; - } - } + Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); + int line = pos.y; + int column = pos.x; - 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; - } + if ((p_caret == -1 && get_selection_at_line_column(line, column, p_edges) != -1) || (p_caret != -1 && _selection_contains(p_caret, line, column, p_edges))) { + return true; } - return false; } @@ -4619,270 +4501,401 @@ bool TextEdit::is_multiple_carets_enabled() const { return multi_carets_enabled; } -int TextEdit::add_caret(int p_line, int p_col) { +int TextEdit::add_caret(int p_line, int p_column) { if (!multi_carets_enabled) { return -1; } + _cancel_drag_and_drop_text(); p_line = CLAMP(p_line, 0, text.size() - 1); - p_col = CLAMP(p_col, 0, get_line(p_line).length()); + p_column = CLAMP(p_column, 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) { + if (!is_in_mulitcaret_edit()) { + // Carets cannot overlap. + if (get_selection_at_line_column(p_line, p_column, true, false) != -1) { 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; + int new_index = carets.size() - 1; + set_caret_line(p_line, false, false, -1, new_index); + set_caret_column(p_column, false, new_index); + _caret_changed(new_index); + + if (is_in_mulitcaret_edit()) { + multicaret_edit_ignore_carets.insert(new_index); + merge_overlapping_carets(); + } + return new_index; } void TextEdit::remove_caret(int p_caret) { ERR_FAIL_COND_MSG(carets.size() <= 1, "The main caret should not be removed."); ERR_FAIL_INDEX(p_caret, carets.size()); + + _caret_changed(p_caret); carets.remove_at(p_caret); - caret_index_edit_dirty = true; + + if (drag_caret_index >= 0) { + if (p_caret == drag_caret_index) { + drag_caret_index = -1; + } else if (p_caret < drag_caret_index) { + drag_caret_index -= 1; + } + } } void TextEdit::remove_secondary_carets() { + if (carets.size() == 1) { + return; + } + + _caret_changed(); carets.resize(1); - caret_index_edit_dirty = true; - queue_redraw(); + + if (drag_caret_index >= 0) { + drag_caret_index = -1; + } } -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]; +int TextEdit::get_caret_count() const { + // Don't include drag caret. + if (drag_caret_index >= 0) { + return carets.size() - 1; + } + return carets.size(); +} - // 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; - } +void TextEdit::add_caret_at_carets(bool p_below) { + const int last_line_max_wrap = get_line_wrap_count(text.size() - 1); + + begin_multicaret_edit(); + int view_target_caret = -1; + int view_line = p_below ? -1 : INT_MAX; + int num_carets = get_caret_count(); + for (int i = 0; i < num_carets; i++) { + const int caret_line = get_caret_line(i); + const int caret_column = get_caret_column(i); + const bool is_selected = has_selection(i) || carets[i].last_fit_x != carets[i].selection.origin_last_fit_x; + const int selection_origin_line = get_selection_origin_line(i); + const int selection_origin_column = get_selection_origin_column(i); + const int caret_wrap_index = get_caret_wrap_index(i); + const int selection_origin_wrap_index = !is_selected ? -1 : get_line_wrap_index_at_column(selection_origin_line, selection_origin_column); + + if (caret_line == 0 && !p_below && (caret_wrap_index == 0 || selection_origin_wrap_index == 0)) { + // Can't add above the first line. + continue; + } + if (caret_line == text.size() - 1 && p_below && (caret_wrap_index == last_line_max_wrap || selection_origin_wrap_index == last_line_max_wrap)) { + // Can't add below the last line. + continue; + } - 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; - } + // Add a new caret. + int new_caret_index = add_caret(caret_line, caret_column); - if (!should_merge) { - continue; - } + // Copy the selection origin and last fit. + set_selection_origin_line(selection_origin_line, true, -1, new_caret_index); + set_selection_origin_column(selection_origin_column, new_caret_index); + carets.write[new_caret_index].last_fit_x = carets[i].last_fit_x; + carets.write[new_caret_index].selection.origin_last_fit_x = carets[i].selection.origin_last_fit_x; - // 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; + // Move the caret up or down one visible line. + if (!p_below) { + // Move caret up. + if (caret_wrap_index > 0) { + set_caret_line(caret_line, false, false, caret_wrap_index - 1, new_caret_index); + } 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, false, false, get_line_wrap_count(new_line), new_caret_index); + } else { + set_caret_line(new_line, false, false, 0, new_caret_index); + } } - - 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); + // Move selection origin up. + if (is_selected) { + if (selection_origin_wrap_index > 0) { + set_selection_origin_line(caret_line, false, selection_origin_wrap_index - 1, new_caret_index); + } else { + int new_line = selection_origin_line - get_next_visible_line_offset_from(selection_origin_line - 1, -1); + if (is_line_wrapped(new_line)) { + set_selection_origin_line(new_line, false, get_line_wrap_count(new_line), new_caret_index); + } else { + set_selection_origin_line(new_line, false, 0, new_caret_index); + } } - } 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); + if (get_caret_line(new_caret_index) < view_line) { + view_line = get_caret_line(new_caret_index); + view_target_caret = new_caret_index; + } + } else { + // Move caret down. + if (caret_wrap_index < get_line_wrap_count(caret_line)) { + set_caret_line(caret_line, false, false, caret_wrap_index + 1, new_caret_index); } else { - to_col = get_selection_to_column(caret_to_remove); + 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, false, false, 0, new_caret_index); + } + // Move selection origin down. + if (is_selected) { + if (selection_origin_wrap_index < get_line_wrap_count(selection_origin_line)) { + set_selection_origin_line(selection_origin_line, false, selection_origin_wrap_index + 1, new_caret_index); + } else { + int new_line = selection_origin_line + get_next_visible_line_offset_from(CLAMP(selection_origin_line + 1, 0, text.size() - 1), 1); + set_selection_origin_line(new_line, false, 0, new_caret_index); + } + } + if (get_caret_line(new_caret_index) > view_line) { + view_line = get_caret_line(new_caret_index); + view_target_caret = new_caret_index; } + } + if (is_selected) { + // Make sure selection is active. + select(get_selection_origin_line(new_caret_index), get_selection_origin_column(new_caret_index), get_caret_line(new_caret_index), get_caret_column(new_caret_index), new_caret_index); + carets.write[new_caret_index].last_fit_x = carets[i].last_fit_x; + carets.write[new_caret_index].selection.origin_last_fit_x = carets[i].selection.origin_last_fit_x; + } - 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; + bool check_edges = !has_selection(0) || !has_selection(new_caret_index); + bool will_merge_with_main_caret = _selection_contains(0, get_caret_line(new_caret_index), get_caret_column(new_caret_index), check_edges, false) || _selection_contains(new_caret_index, get_caret_line(0), get_caret_column(0), check_edges, false); + if (will_merge_with_main_caret) { + // Move next to the main caret so it stays the main caret after merging. + Caret new_caret = carets[new_caret_index]; + carets.remove_at(new_caret_index); + carets.insert(0, new_caret); + i++; } + } - // 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; + // Show the topmost caret if added above or bottommost caret if added below. + if (view_target_caret >= 0 && view_target_caret < get_caret_count()) { + adjust_viewport_to_caret(view_target_caret); + } + + merge_overlapping_carets(); + end_multicaret_edit(); +} + +struct _CaretSortComparator { + _FORCE_INLINE_ bool operator()(const Vector3i &a, const Vector3i &b) const { + // x is column, y is line, z is caret index. + if (a.y == b.y) { + return a.x < b.x; } + return a.y < b.y; + } +}; - // 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--; - } +Vector<int> TextEdit::get_sorted_carets(bool p_include_ignored_carets) const { + // Returns caret indexes sorted by selection start or caret position from top to bottom of text. + Vector<Vector3i> caret_line_col_indexes; + for (int i = 0; i < get_caret_count(); i++) { + if (!p_include_ignored_carets && multicaret_edit_ignore_caret(i)) { continue; } + caret_line_col_indexes.push_back(Vector3i(get_selection_from_column(i), get_selection_from_line(i), i)); + } + caret_line_col_indexes.sort_custom<_CaretSortComparator>(); + Vector<int> sorted; + sorted.resize(caret_line_col_indexes.size()); + for (int i = 0; i < caret_line_col_indexes.size(); i++) { + sorted.set(i, caret_line_col_indexes[i].z); + } + return sorted; +} - // 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); +void TextEdit::collapse_carets(int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_inclusive) { + // Collapse carets in the selected range to the from position. + + // Clamp the collapse target position. + int collapse_line = CLAMP(p_from_line, 0, text.size() - 1); + int collapse_column = CLAMP(p_from_column, 0, text[collapse_line].length()); + + // Swap the lines if they are in the wrong order. + if (p_from_line > p_to_line) { + SWAP(p_from_line, p_to_line); + SWAP(p_from_column, p_to_column); + } + if (p_from_line == p_to_line && p_from_column > p_to_column) { + SWAP(p_from_column, p_to_column); + } + bool any_collapsed = false; + + // Intentionally includes carets in the multicaret_edit_ignore list so that they are moved together. + for (int i = 0; i < get_caret_count(); i++) { + bool is_caret_in = _is_line_col_in_range(get_caret_line(i), get_caret_column(i), p_from_line, p_from_column, p_to_line, p_to_column, p_inclusive); + if (!has_selection(i)) { + if (is_caret_in) { + // Caret was in the collapsed area. + set_caret_line(collapse_line, false, true, -1, i); + set_caret_column(collapse_column, false, i); + if (is_in_mulitcaret_edit()) { + multicaret_edit_ignore_carets.insert(i); + } + any_collapsed = true; + } + } else { + bool is_origin_in = _is_line_col_in_range(get_selection_origin_line(i), get_selection_origin_column(i), p_from_line, p_from_column, p_to_line, p_to_column, p_inclusive); + + if (is_caret_in && is_origin_in) { + // Selection was completely encapsulated. + deselect(i); + set_caret_line(collapse_line, false, true, -1, i); + set_caret_column(collapse_column, false, i); + if (is_in_mulitcaret_edit()) { + multicaret_edit_ignore_carets.insert(i); + } + any_collapsed = true; + } else if (is_caret_in) { + // Only caret was inside. + set_caret_line(collapse_line, false, true, -1, i); + set_caret_column(collapse_column, false, i); + any_collapsed = true; + } else if (is_origin_in) { + // Only selection origin was inside. + set_selection_origin_line(collapse_line, true, -1, i); + set_selection_origin_column(collapse_column, i); + any_collapsed = true; } - i--; - caret_edit_order = get_caret_index_edit_order(); - continue; } + if (!p_inclusive && !any_collapsed) { + if ((get_caret_line(i) == collapse_line && get_caret_column(i) == collapse_column) || (get_selection_origin_line(i) == collapse_line && get_selection_origin_column(i) == collapse_column)) { + // Make sure to queue a merge, even if we didn't include it. + any_collapsed = true; + } + } + } + if (any_collapsed) { + merge_overlapping_carets(); } } -int TextEdit::get_caret_count() const { - return carets.size(); -} +void TextEdit::merge_overlapping_carets() { + if (is_in_mulitcaret_edit()) { + // Queue merge to be performed the end of the multicaret edit. + multicaret_edit_merge_queued = true; + return; + } -void TextEdit::add_caret_at_carets(bool p_below) { - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &caret_index : caret_edit_order) { - const int caret_line = get_caret_line(caret_index); - const int caret_column = get_caret_column(caret_index); - - // The last fit x will be cleared if the caret has a selection, - // but if it does not have a selection the last fit x will be - // transferred to the new caret. - int caret_from_column = 0, caret_to_column = 0, caret_last_fit_x = carets[caret_index].last_fit_x; - if (has_selection(caret_index)) { - // If the selection goes over multiple lines, deselect it. - if (get_selection_from_line(caret_index) != get_selection_to_line(caret_index)) { - deselect(caret_index); + multicaret_edit_merge_queued = false; + multicaret_edit_ignore_carets.clear(); + + if (get_caret_count() == 1) { + return; + } + + Vector<int> sorted_carets = get_sorted_carets(true); + for (int i = 0; i < sorted_carets.size() - 1; i++) { + int first_caret = sorted_carets[i]; + int second_caret = sorted_carets[i + 1]; + + bool merge_carets; + if (!has_selection(first_caret) || !has_selection(second_caret)) { + // Merge if touching. + merge_carets = get_selection_from_line(second_caret) < get_selection_to_line(first_caret) || (get_selection_from_line(second_caret) == get_selection_to_line(first_caret) && get_selection_from_column(second_caret) <= get_selection_to_column(first_caret)); + } else { + // Merge two selections if overlapping. + merge_carets = get_selection_from_line(second_caret) < get_selection_to_line(first_caret) || (get_selection_from_line(second_caret) == get_selection_to_line(first_caret) && get_selection_from_column(second_caret) < get_selection_to_column(first_caret)); + } + + if (!merge_carets) { + 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; + } + + if (get_selection_from_line(caret_to_save) != get_selection_from_line(caret_to_remove) || get_selection_to_line(caret_to_save) != get_selection_to_line(caret_to_remove) || get_selection_from_column(caret_to_save) != get_selection_from_column(caret_to_remove) || get_selection_to_column(caret_to_save) != get_selection_to_column(caret_to_remove)) { + // Selections are not the same, merge them into one bigger selection. + int new_from_line = MIN(get_selection_from_line(caret_to_remove), get_selection_from_line(caret_to_save)); + int new_to_line = MAX(get_selection_to_line(caret_to_remove), get_selection_to_line(caret_to_save)); + int new_from_col; + int new_to_col; + if (get_selection_from_line(caret_to_remove) < get_selection_from_line(caret_to_save)) { + new_from_col = get_selection_from_column(caret_to_remove); + } else if (get_selection_from_line(caret_to_remove) > get_selection_from_line(caret_to_save)) { + new_from_col = get_selection_from_column(caret_to_save); } else { - caret_from_column = get_selection_from_column(caret_index); - caret_to_column = get_selection_to_column(caret_index); - caret_last_fit_x = -1; - carets.write[caret_index].last_fit_x = _get_column_x_offset_for_line(caret_column, caret_line, caret_column); + new_from_col = MIN(get_selection_from_column(caret_to_remove), get_selection_from_column(caret_to_save)); + } + if (get_selection_to_line(caret_to_remove) < get_selection_to_line(caret_to_save)) { + new_to_col = get_selection_to_column(caret_to_save); + } else if (get_selection_to_line(caret_to_remove) > get_selection_to_line(caret_to_save)) { + new_to_col = get_selection_to_column(caret_to_remove); + } else { + new_to_col = MAX(get_selection_to_column(caret_to_remove), get_selection_to_column(caret_to_save)); } - } - // Get the line and column of the new caret as if you would move the caret by pressing the arrow keys. - int new_caret_line, new_caret_column, new_caret_from_column = 0, new_caret_to_column = 0; - _get_above_below_caret_line_column(caret_line, get_caret_wrap_index(caret_index), caret_column, p_below, new_caret_line, new_caret_column, caret_last_fit_x); + // Use the direction from the last caret or the saved one. + int caret_dir_to_copy; + if (has_selection(caret_to_remove) && has_selection(caret_to_save)) { + caret_dir_to_copy = caret_to_remove == get_caret_count() - 1 ? caret_to_remove : caret_to_save; + } else { + caret_dir_to_copy = !has_selection(caret_to_remove) ? caret_to_save : caret_to_remove; + } - // If the caret does have a selection calculate the new from and to columns. - if (caret_from_column != caret_to_column) { - // We only need to calculate the selection columns if the column of the caret changed. - if (caret_column != new_caret_column) { - int _; // Unused placeholder for p_new_line. - _get_above_below_caret_line_column(caret_line, get_caret_wrap_index(caret_index), caret_from_column, p_below, _, new_caret_from_column); - _get_above_below_caret_line_column(caret_line, get_caret_wrap_index(caret_index), caret_to_column, p_below, _, new_caret_to_column); + if (is_caret_after_selection_origin(caret_dir_to_copy)) { + select(new_from_line, new_from_col, new_to_line, new_to_col, caret_to_save); } else { - new_caret_from_column = caret_from_column; - new_caret_to_column = caret_to_column; + select(new_to_line, new_to_col, new_from_line, new_from_col, caret_to_save); } } - // Add the new caret. - const int new_caret_index = add_caret(new_caret_line, new_caret_column); - - if (new_caret_index == -1) { - continue; + if (caret_to_save == 0) { + adjust_viewport_to_caret(caret_to_save); } - // Also add the selection if there should be one. - if (new_caret_from_column != new_caret_to_column) { - select(new_caret_line, new_caret_from_column, new_caret_line, new_caret_to_column, new_caret_index); - // Necessary to properly modify the selection after adding the new caret. - carets.write[new_caret_index].selection.selecting_line = new_caret_line; - carets.write[new_caret_index].selection.selecting_column = new_caret_column == new_caret_from_column ? new_caret_to_column : new_caret_from_column; - continue; + remove_caret(caret_to_remove); + + // Update the rest of the sorted list. + for (int j = i; j < sorted_carets.size(); j++) { + if (sorted_carets[j] > caret_to_remove) { + // Shift the index since a caret before it was removed. + sorted_carets.write[j] -= 1; + } } + // Remove the caret from the sorted array. + sorted_carets.remove_at(caret_to_remove == first_caret ? i : i + 1); - // Copy the last fit x over. - carets.write[new_caret_index].last_fit_x = carets[caret_index].last_fit_x; + // Process the caret again, since it and the next caret might also overlap. + i--; } +} - merge_overlapping_carets(); - queue_redraw(); +// Starts a multicaret edit operation. Call this before iterating over the carets and call [end_multicaret_edit] afterwards. +void TextEdit::begin_multicaret_edit() { + multicaret_edit_count++; } -Vector<int> TextEdit::get_caret_index_edit_order() { - if (!caret_index_edit_dirty) { - return caret_index_edit_order; +void TextEdit::end_multicaret_edit() { + if (multicaret_edit_count > 0) { + multicaret_edit_count--; + } + if (multicaret_edit_count != 0) { + return; } - 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 = has_selection(i) ? get_selection_to_line(i) : carets[i].line; - int col = has_selection(i) ? get_selection_to_column(i) : carets[i].column; - - for (; j < caret_index_edit_order.size(); j++) { - int idx = caret_index_edit_order[j]; - int other_line = has_selection(idx) ? get_selection_to_line(idx) : carets[idx].line; - int other_col = has_selection(idx) ? get_selection_to_column(idx) : carets[idx].column; - if (line > other_line || (line == other_line && col > other_col)) { - break; - } - } - caret_index_edit_order.insert(j, i); + // This was the last multicaret edit operation. + if (multicaret_edit_merge_queued) { + merge_overlapping_carets(); } - caret_index_edit_dirty = false; - return caret_index_edit_order; + multicaret_edit_ignore_carets.clear(); } -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]); - } +bool TextEdit::is_in_mulitcaret_edit() const { + return multicaret_edit_count > 0; +} - // 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) == get_selection_from_line(caret_edit_order[j])) { - carets.write[caret_edit_order[j]].selection.from_column += edit_size; - } - } +bool TextEdit::multicaret_edit_ignore_caret(int p_caret) const { + return multicaret_edit_ignore_carets.has(p_caret); } bool TextEdit::is_caret_visible(int p_caret) const { @@ -4902,16 +4915,10 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_ } setting_caret_line = true; - if (p_line < 0) { - p_line = 0; - } - - if (p_line >= text.size()) { - p_line = text.size() - 1; - } + p_line = CLAMP(p_line, 0, text.size() - 1); if (!p_can_be_hidden) { - if (_is_line_hidden(CLAMP(p_line, 0, text.size() - 1))) { + if (_is_line_hidden(p_line)) { int move_down = get_next_visible_line_offset_from(p_line, 1) - 1; if (p_line + move_down <= text.size() - 1 && !_is_line_hidden(p_line + move_down)) { p_line += move_down; @@ -4920,7 +4927,7 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_ if (p_line - move_up > 0 && !_is_line_hidden(p_line - move_up)) { p_line -= move_up; } else { - WARN_PRINT(("Caret set to hidden line " + itos(p_line) + " and there are no nonhidden lines.")); + WARN_PRINT("Caret set to hidden line " + itos(p_line) + " and there are no nonhidden lines."); } } } @@ -4928,31 +4935,36 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_ 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(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; - for (int i = 0; i < p_wrap_index + 1; i++) { - row_end_col += rows[i].length(); - } - if (n_col >= row_end_col) { - n_col -= 1; + int n_col; + if (p_wrap_index >= 0) { + // Keep caret in same visual x position it was at previously. + 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)) { + // Offset by one to not go past the end of the wrapped line. + if (n_col >= text.get_line_wrap_ranges(p_line)[p_wrap_index].y) { + n_col -= 1; + } } + } else { + // Clamp the column. + n_col = MIN(get_caret_column(p_caret), get_line(p_line).length()); } caret_moved = (caret_moved || get_caret_column(p_caret) != n_col); carets.write[p_caret].column = n_col; + // Unselect if the caret moved to the selection origin. + if (p_wrap_index >= 0 && has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) { + deselect(p_caret); + } + if (is_inside_tree() && p_adjust_viewport) { adjust_viewport_to_caret(p_caret); } setting_caret_line = false; - if (caret_moved && !caret_pos_dirty) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); - } - caret_pos_dirty = true; + if (caret_moved) { + _caret_changed(p_caret); } } @@ -4961,29 +4973,32 @@ int TextEdit::get_caret_line(int p_caret) const { return carets[p_caret].line; } -void TextEdit::set_caret_column(int p_col, bool p_adjust_viewport, int p_caret) { +void TextEdit::set_caret_column(int p_column, 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(get_caret_line(p_caret)).length()) { - p_col = get_line(get_caret_line(p_caret)).length(); - } - bool caret_moved = get_caret_column(p_caret) != p_col; - carets.write[p_caret].column = p_col; + p_column = CLAMP(p_column, 0, get_line(get_caret_line(p_caret)).length()); + + bool caret_moved = get_caret_column(p_caret) != p_column; + carets.write[p_caret].column = p_column; 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 (!has_selection(p_caret)) { + // Set the selection origin last fit x to be the same, so we can tell if there was a selection. + carets.write[p_caret].selection.origin_last_fit_x = carets[p_caret].last_fit_x; + } + + // Unselect if the caret moved to the selection origin. + if (has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) { + deselect(p_caret); + } + if (is_inside_tree() && p_adjust_viewport) { adjust_viewport_to_caret(p_caret); } - if (caret_moved && !caret_pos_dirty) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); - } - caret_pos_dirty = true; + if (caret_moved) { + _caret_changed(p_caret); } } @@ -4998,7 +5013,7 @@ int TextEdit::get_caret_wrap_index(int p_caret) const { } String TextEdit::get_word_under_caret(int p_caret) const { - ERR_FAIL_COND_V(p_caret > carets.size(), ""); + ERR_FAIL_COND_V(p_caret >= carets.size() || p_caret < -1, ""); StringBuilder selected_text; for (int c = 0; c < carets.size(); c++) { @@ -5059,20 +5074,8 @@ bool TextEdit::is_drag_and_drop_selection_enabled() const { return drag_and_drop_selection_enabled; } -void TextEdit::set_selection_mode(SelectionMode p_mode, int p_line, int p_column, int p_caret) { - ERR_FAIL_INDEX(p_caret, carets.size()); - +void TextEdit::set_selection_mode(SelectionMode p_mode) { selecting_mode = p_mode; - if (p_line >= 0) { - ERR_FAIL_INDEX(p_line, text.size()); - 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(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 { @@ -5090,16 +5093,12 @@ void TextEdit::select_all() { } remove_secondary_carets(); + set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT); 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(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= carets.size() || p_caret < -1); _push_current_op(); if (!selecting_enabled) { @@ -5140,8 +5139,6 @@ void TextEdit::select_word_under_caret(int p_caret) { } select(get_caret_line(c), begin, get_caret_line(c), end, c); - // Move the caret to the end of the word for easier editing. - set_caret_column(end, false, c); } merge_overlapping_carets(); } @@ -5234,53 +5231,37 @@ void TextEdit::skip_selection_for_next_occurrence() { } } -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()); +void TextEdit::select(int p_origin_line, int p_origin_column, int p_caret_line, int p_caret_column, int p_caret) { + ERR_FAIL_INDEX(p_caret, get_caret_count()); + + p_caret_line = CLAMP(p_caret_line, 0, text.size() - 1); + p_caret_column = CLAMP(p_caret_column, 0, text[p_caret_line].length()); + set_caret_line(p_caret_line, false, true, -1, p_caret); + set_caret_column(p_caret_column, false, p_caret); + if (!selecting_enabled) { return; } - p_from_line = CLAMP(p_from_line, 0, text.size() - 1); - p_from_column = CLAMP(p_from_column, 0, text[p_from_line].length()); - p_to_line = CLAMP(p_to_line, 0, text.size() - 1); - p_to_column = CLAMP(p_to_column, 0, text[p_to_line].length()); - - 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; + p_origin_line = CLAMP(p_origin_line, 0, text.size() - 1); + p_origin_column = CLAMP(p_origin_column, 0, text[p_origin_line].length()); + set_selection_origin_line(p_origin_line, true, -1, p_caret); + set_selection_origin_column(p_origin_column, p_caret); - carets.write[p_caret].selection.active = true; - - if (get_selection_from_line(p_caret) == get_selection_to_line(p_caret)) { - if (get_selection_from_column(p_caret) == get_selection_to_column(p_caret)) { - carets.write[p_caret].selection.active = false; - - } else if (get_selection_from_column(p_caret) > get_selection_to_column(p_caret)) { - carets.write[p_caret].selection.shiftclick_left = false; - SWAP(carets.write[p_caret].selection.from_column, carets.write[p_caret].selection.to_column); - } else { - carets.write[p_caret].selection.shiftclick_left = true; - } - } else if (get_selection_from_line(p_caret) > get_selection_to_line(p_caret)) { - 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 { - carets.write[p_caret].selection.shiftclick_left = true; + bool had_selection = has_selection(p_caret); + bool activate = p_origin_line != p_caret_line || p_origin_column != p_caret_column; + carets.write[p_caret].selection.active = activate; + if (had_selection != activate) { + _selection_changed(p_caret); } - - caret_index_edit_dirty = true; - queue_redraw(); } bool TextEdit::has_selection(int p_caret) const { - ERR_FAIL_COND_V(p_caret > carets.size(), false); + ERR_FAIL_COND_V(p_caret >= carets.size() || p_caret < -1, false); + if (p_caret >= 0) { + return carets[p_caret].selection.active; + } for (int i = 0; i < carets.size(); i++) { - if (p_caret != -1 && p_caret != i) { - continue; - } - if (carets[i].selection.active) { return true; } @@ -5289,100 +5270,268 @@ bool TextEdit::has_selection(int p_caret) const { } String TextEdit::get_selected_text(int p_caret) { - ERR_FAIL_COND_V(p_caret > carets.size(), ""); + ERR_FAIL_COND_V(p_caret >= carets.size() || p_caret < -1, ""); - 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 (p_caret >= 0) { + if (!has_selection(p_caret)) { + return ""; } + return _base_get_text(get_selection_from_line(p_caret), get_selection_from_column(p_caret), get_selection_to_line(p_caret), get_selection_to_column(p_caret)); + } + + StringBuilder selected_text; + Vector<int> sorted_carets = get_sorted_carets(); + for (int i = 0; i < sorted_carets.size(); i++) { + int caret_index = sorted_carets[i]; - if (!has_selection(caret_idx)) { + if (!has_selection(caret_index)) { 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) { + if (selected_text.get_string_length() != 0) { selected_text += "\n"; } + selected_text += _base_get_text(get_selection_from_line(caret_index), get_selection_from_column(caret_index), get_selection_to_line(caret_index), get_selection_to_column(caret_index)); } return selected_text.as_string(); } -int TextEdit::get_selection_line(int p_caret) const { +int TextEdit::get_selection_at_line_column(int p_line, int p_column, bool p_include_edges, bool p_only_selections) const { + // Return the caret index of the found selection, or -1. + for (int i = 0; i < get_caret_count(); i++) { + if (_selection_contains(i, p_line, p_column, p_include_edges, p_only_selections)) { + return i; + } + } + return -1; +} + +Vector<Point2i> TextEdit::get_line_ranges_from_carets(bool p_only_selections, bool p_merge_adjacent) const { + // Get a series of line ranges that cover all lines that have a caret or selection. + // For each Point2i range, x is the first line and y is the last line. + Vector<Point2i> ret; + int last_to_line = INT_MIN; + + Vector<int> sorted_carets = get_sorted_carets(); + for (int i = 0; i < sorted_carets.size(); i++) { + int caret_index = sorted_carets[i]; + if (p_only_selections && !has_selection(caret_index)) { + continue; + } + Point2i range = Point2i(get_selection_from_line(caret_index), get_selection_to_line(caret_index)); + if (has_selection(caret_index) && get_selection_to_column(caret_index) == 0) { + // Dont include selection end line if it ends at column 0. + range.y--; + } + if (range.x == last_to_line || (p_merge_adjacent && range.x - 1 == last_to_line)) { + // Merge if starts on the same line or adjacent line. + ret.write[ret.size() - 1].y = range.y; + } else { + ret.append(range); + } + last_to_line = range.y; + } + return ret; +} + +TypedArray<Vector2i> TextEdit::get_line_ranges_from_carets_typed_array(bool p_only_selections, bool p_merge_adjacent) const { + // Wrapper for `get_line_ranges_from_carets` to return a datatype that can be exposed. + TypedArray<Vector2i> ret; + Vector<Point2i> ranges = get_line_ranges_from_carets(p_only_selections, p_merge_adjacent); + for (const Point2i &range : ranges) { + ret.push_back(range); + } + return ret; +} + +void TextEdit::set_selection_origin_line(int p_line, bool p_can_be_hidden, int p_wrap_index, int p_caret) { + if (!selecting_enabled) { + return; + } + ERR_FAIL_INDEX(p_caret, carets.size()); + p_line = CLAMP(p_line, 0, text.size() - 1); + + if (!p_can_be_hidden) { + if (_is_line_hidden(p_line)) { + int move_down = get_next_visible_line_offset_from(p_line, 1) - 1; + if (p_line + move_down <= text.size() - 1 && !_is_line_hidden(p_line + move_down)) { + p_line += move_down; + } else { + int move_up = get_next_visible_line_offset_from(p_line, -1) - 1; + if (p_line - move_up > 0 && !_is_line_hidden(p_line - move_up)) { + p_line -= move_up; + } else { + WARN_PRINT("Selection origin set to hidden line " + itos(p_line) + " and there are no nonhidden lines."); + } + } + } + } + + bool selection_moved = get_selection_origin_line(p_caret) != p_line; + carets.write[p_caret].selection.origin_line = p_line; + + int n_col; + if (p_wrap_index >= 0) { + // Keep selection origin in same visual x position it was at previously. + n_col = _get_char_pos_for_line(carets[p_caret].selection.origin_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)) { + // Offset by one to not go past the end of the wrapped line. + if (n_col >= text.get_line_wrap_ranges(p_line)[p_wrap_index].y) { + n_col -= 1; + } + } + } else { + // Clamp the column. + n_col = MIN(get_selection_origin_column(p_caret), get_line(p_line).length()); + } + selection_moved = (selection_moved || get_selection_origin_column(p_caret) != n_col); + carets.write[p_caret].selection.origin_column = n_col; + + // Unselect if the selection origin moved to the caret. + if (p_wrap_index >= 0 && has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) { + deselect(p_caret); + } + + if (selection_moved && has_selection(p_caret)) { + _selection_changed(p_caret); + } +} + +void TextEdit::set_selection_origin_column(int p_column, int p_caret) { + if (!selecting_enabled) { + return; + } + ERR_FAIL_INDEX(p_caret, carets.size()); + + p_column = CLAMP(p_column, 0, get_line(get_selection_origin_line(p_caret)).length()); + + bool selection_moved = get_selection_origin_column(p_caret) != p_column; + + carets.write[p_caret].selection.origin_column = p_column; + + carets.write[p_caret].selection.origin_last_fit_x = _get_column_x_offset_for_line(get_selection_origin_column(p_caret), get_selection_origin_line(p_caret), get_selection_origin_column(p_caret)); + + // Unselect if the selection origin moved to the caret. + if (has_selection(p_caret) && get_caret_line(p_caret) == get_selection_origin_line(p_caret) && get_caret_column(p_caret) == get_selection_origin_column(p_caret)) { + deselect(p_caret); + } + + if (selection_moved && has_selection(p_caret)) { + _selection_changed(p_caret); + } +} + +int TextEdit::get_selection_origin_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; + return carets[p_caret].selection.origin_line; } -int TextEdit::get_selection_column(int p_caret) const { +int TextEdit::get_selection_origin_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; + return carets[p_caret].selection.origin_column; } 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; + if (!has_selection(p_caret)) { + return carets[p_caret].line; + } + return MIN(carets[p_caret].selection.origin_line, carets[p_caret].line); } 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; + if (!has_selection(p_caret)) { + return carets[p_caret].column; + } + if (carets[p_caret].selection.origin_line < carets[p_caret].line) { + return carets[p_caret].selection.origin_column; + } else if (carets[p_caret].selection.origin_line > carets[p_caret].line) { + return carets[p_caret].column; + } else { + return MIN(carets[p_caret].selection.origin_column, carets[p_caret].column); + } } 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; + if (!has_selection(p_caret)) { + return carets[p_caret].line; + } + return MAX(carets[p_caret].selection.origin_line, carets[p_caret].line); } 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; + if (!has_selection(p_caret)) { + return carets[p_caret].column; + } + if (carets[p_caret].selection.origin_line < carets[p_caret].line) { + return carets[p_caret].column; + } else if (carets[p_caret].selection.origin_line > carets[p_caret].line) { + return carets[p_caret].selection.origin_column; + } else { + return MAX(carets[p_caret].selection.origin_column, carets[p_caret].column); + } +} + +bool TextEdit::is_caret_after_selection_origin(int p_caret) const { + ERR_FAIL_INDEX_V(p_caret, carets.size(), false); + if (!has_selection(p_caret)) { + return true; + } + return carets[p_caret].line > carets[p_caret].selection.origin_line || (carets[p_caret].line == carets[p_caret].selection.origin_line && carets[p_caret].column >= carets[p_caret].selection.origin_column); } 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; + ERR_FAIL_COND(p_caret >= carets.size() || p_caret < -1); + bool selection_changed = false; + if (p_caret >= 0) { + selection_changed = carets.write[p_caret].selection.active; + carets.write[p_caret].selection.active = false; + } else { + for (int i = 0; i < carets.size(); i++) { + selection_changed |= carets.write[i].selection.active; + carets.write[i].selection.active = false; } - carets.write[i].selection.active = false; } - caret_index_edit_dirty = true; - queue_redraw(); + if (selection_changed) { + _selection_changed(p_caret); + } } void TextEdit::delete_selection(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { if (p_caret != -1 && p_caret != i) { continue; } + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } if (!has_selection(i)) { continue; } - selecting_mode = SelectionMode::SELECTION_MODE_NONE; - _remove_text(get_selection_from_line(i), get_selection_from_column(i), get_selection_to_line(i), get_selection_to_column(i)); - set_caret_line(get_selection_from_line(i), false, false, 0, i); - set_caret_column(get_selection_from_column(i), i == 0, i); - carets.write[i].selection.active = false; + int selection_from_line = get_selection_from_line(i); + int selection_from_column = get_selection_from_column(i); + int selection_to_line = get_selection_to_line(i); + int selection_to_column = get_selection_to_column(i); + + _remove_text(selection_from_line, selection_from_column, selection_to_line, selection_to_column); + _offset_carets_after(selection_to_line, selection_to_column, selection_from_line, selection_from_column); + merge_overlapping_carets(); - 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); + deselect(i); + set_caret_line(selection_from_line, false, false, -1, i); + set_caret_column(selection_from_column, i == 0, i); } + end_multicaret_edit(); end_complex_operation(); - queue_redraw(); } /* Line wrapping. */ @@ -6224,8 +6373,10 @@ 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("remove_line_at", "line", "move_carets_down"), &TextEdit::remove_line_at, DEFVAL(true)); ClassDB::bind_method(D_METHOD("insert_text_at_caret", "text", "caret_index"), &TextEdit::insert_text_at_caret, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("insert_text", "text", "line", "column", "before_selection_begin", "before_selection_end"), &TextEdit::insert_text, DEFVAL(true), DEFVAL(false)); ClassDB::bind_method(D_METHOD("remove_text", "from_line", "from_column", "to_line", "to_column"), &TextEdit::remove_text); ClassDB::bind_method(D_METHOD("get_last_unhidden_line"), &TextEdit::get_last_unhidden_line); @@ -6311,7 +6462,7 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_search_text", "search_text"), &TextEdit::set_search_text); ClassDB::bind_method(D_METHOD("set_search_flags", "flags"), &TextEdit::set_search_flags); - ClassDB::bind_method(D_METHOD("search", "text", "flags", "from_line", "from_colum"), &TextEdit::search); + ClassDB::bind_method(D_METHOD("search", "text", "flags", "from_line", "from_column"), &TextEdit::search); /* Tooltip */ ClassDB::bind_method(D_METHOD("set_tooltip_request_func", "callback"), &TextEdit::set_tooltip_request_func); @@ -6355,15 +6506,20 @@ void TextEdit::_bind_methods() { 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("add_caret", "line", "column"), &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("add_caret_at_carets", "below"), &TextEdit::add_caret_at_carets); - 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("get_sorted_carets", "include_ignored_carets"), &TextEdit::get_sorted_carets, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("collapse_carets", "from_line", "from_column", "to_line", "to_column", "inclusive"), &TextEdit::collapse_carets, DEFVAL(false)); + + ClassDB::bind_method(D_METHOD("merge_overlapping_carets"), &TextEdit::merge_overlapping_carets); + ClassDB::bind_method(D_METHOD("begin_multicaret_edit"), &TextEdit::begin_multicaret_edit); + ClassDB::bind_method(D_METHOD("end_multicaret_edit"), &TextEdit::end_multicaret_edit); + ClassDB::bind_method(D_METHOD("is_in_mulitcaret_edit"), &TextEdit::is_in_mulitcaret_edit); + ClassDB::bind_method(D_METHOD("multicaret_edit_ignore_caret", "caret_index"), &TextEdit::multicaret_edit_ignore_caret); 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)); @@ -6394,27 +6550,33 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_drag_and_drop_selection_enabled", "enable"), &TextEdit::set_drag_and_drop_selection_enabled); ClassDB::bind_method(D_METHOD("is_drag_and_drop_selection_enabled"), &TextEdit::is_drag_and_drop_selection_enabled); - 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("set_selection_mode", "mode"), &TextEdit::set_selection_mode); 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", "caret_index"), &TextEdit::select_word_under_caret, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("add_selection_for_next_occurrence"), &TextEdit::add_selection_for_next_occurrence); ClassDB::bind_method(D_METHOD("skip_selection_for_next_occurrence"), &TextEdit::skip_selection_for_next_occurrence); - 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("select", "origin_line", "origin_column", "caret_line", "caret_column", "caret_index"), &TextEdit::select, DEFVAL(0)); ClassDB::bind_method(D_METHOD("has_selection", "caret_index"), &TextEdit::has_selection, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("get_selected_text", "caret_index"), &TextEdit::get_selected_text, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("get_selection_at_line_column", "line", "column", "include_edges", "only_selections"), &TextEdit::get_selection_at_line_column, DEFVAL(true), DEFVAL(true)); + ClassDB::bind_method(D_METHOD("get_line_ranges_from_carets", "only_selections", "merge_adjacent"), &TextEdit::get_line_ranges_from_carets_typed_array, DEFVAL(false), DEFVAL(true)); - 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_origin_line", "caret_index"), &TextEdit::get_selection_origin_line, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_selection_origin_column", "caret_index"), &TextEdit::get_selection_origin_column, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("set_selection_origin_line", "line", "can_be_hidden", "wrap_index", "caret_index"), &TextEdit::set_selection_origin_line, DEFVAL(true), DEFVAL(-1), DEFVAL(0)); + ClassDB::bind_method(D_METHOD("set_selection_origin_column", "column", "caret_index"), &TextEdit::set_selection_origin_column, DEFVAL(0)); 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("is_caret_after_selection_origin", "caret_index"), &TextEdit::is_caret_after_selection_origin, DEFVAL(0)); + 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)); @@ -6550,6 +6712,14 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("is_menu_visible"), &TextEdit::is_menu_visible); ClassDB::bind_method(D_METHOD("menu_option", "option"), &TextEdit::menu_option); + /* Deprecated */ +#ifndef DISABLE_DEPRECATED + 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("get_caret_index_edit_order"), &TextEdit::get_caret_index_edit_order); + 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)); +#endif + /* Inspector */ ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT), "set_text", "get_text"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "placeholder_text", PROPERTY_HINT_MULTILINE_TEXT), "set_placeholder", "get_placeholder"); @@ -6617,7 +6787,7 @@ void TextEdit::_bind_methods() { ADD_SIGNAL(MethodInfo("gutter_added")); ADD_SIGNAL(MethodInfo("gutter_removed")); - /* Theme items */ + // Theme items /* Search */ BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TextEdit, search_result_color); BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TextEdit, search_result_border_color); @@ -6690,6 +6860,10 @@ void TextEdit::_unhide_all_lines() { queue_redraw(); } +void TextEdit::_unhide_carets() { + // Override for functionality. +} + void TextEdit::_set_line_as_hidden(int p_line, bool p_hidden) { ERR_FAIL_INDEX(p_line, text.size()); @@ -6717,14 +6891,17 @@ void TextEdit::_set_symbol_lookup_word(const String &p_symbol) { // Overridable actions void TextEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); if (!editable) { return; } start_action(EditAction::ACTION_TYPING); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } if (p_caret != -1 && p_caret != i) { continue; } @@ -6742,11 +6919,12 @@ void TextEdit::_handle_unicode_input_internal(const uint32_t p_unicode, int p_ca const char32_t chr[2] = { (char32_t)p_unicode, 0 }; insert_text_at_caret(chr, i); } + end_multicaret_edit(); end_action(); } void TextEdit::_backspace_internal(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); if (!editable) { return; } @@ -6757,194 +6935,163 @@ void TextEdit::_backspace_internal(int p_caret) { } begin_complex_operation(); - Vector<int> caret_edit_order = get_caret_index_edit_order(); - for (const int &i : caret_edit_order) { + begin_multicaret_edit(); + for (int i = 0; i < get_caret_count(); i++) { + if (p_caret == -1 && multicaret_edit_ignore_caret(i)) { + continue; + } if (p_caret != -1 && p_caret != i) { continue; } - int cc = get_caret_column(i); - int cl = get_caret_line(i); + int to_line = get_caret_line(i); + int to_column = get_caret_column(i); - if (cc == 0 && cl == 0) { + if (to_column == 0 && to_line == 0) { continue; } - int prev_line = cc ? cl : cl - 1; - int prev_column = cc ? (cc - 1) : (text[cl - 1].length()); + int from_line = to_column > 0 ? to_line : to_line - 1; + int from_column = to_column > 0 ? (to_column - 1) : (text[to_line - 1].length()); - merge_gutters(prev_line, cl); + merge_gutters(from_line, to_line); - 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); + _remove_text(from_line, from_column, to_line, to_column); + collapse_carets(from_line, from_column, to_line, to_column); + _offset_carets_after(to_line, to_column, from_line, from_column); - adjust_carets_after_edit(i, prev_line, prev_column, cl, cc); + set_caret_line(from_line, false, true, -1, i); + set_caret_column(from_column, i == 0, i); } - merge_overlapping_carets(); + end_multicaret_edit(); end_complex_operation(); } void TextEdit::_cut_internal(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); + + _copy_internal(p_caret); + if (!editable) { return; } if (has_selection(p_caret)) { - DisplayServer::get_singleton()->clipboard_set(get_selected_text(p_caret)); delete_selection(p_caret); - cut_copy_line = ""; return; } + // Remove full lines. 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(); - - // Check for overlapping 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; - } - } - - 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(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); + begin_multicaret_edit(); + Vector<Point2i> line_ranges; + if (p_caret == -1) { + line_ranges = get_line_ranges_from_carets(); + } else { + line_ranges.push_back(Point2i(get_caret_line(p_caret), get_caret_line(p_caret))); } - - // 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]); + int line_offset = 0; + for (Point2i line_range : line_ranges) { + // Preserve carets on the last line. + remove_line_at(line_range.y + line_offset); + if (line_range.x != line_range.y) { + remove_text(line_range.x + line_offset, 0, line_range.y + line_offset, 0); + } + line_offset += line_range.x - line_range.y - 1; } + end_multicaret_edit(); end_complex_operation(); - - String clipboard_string = clipboard.as_string(); - DisplayServer::get_singleton()->clipboard_set(clipboard_string); - cut_copy_line = clipboard_string; } void TextEdit::_copy_internal(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); if (has_selection(p_caret)) { DisplayServer::get_singleton()->clipboard_set(get_selected_text(p_caret)); cut_copy_line = ""; return; } + // Copy full lines. 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"; + Vector<Point2i> line_ranges; + if (p_caret == -1) { + // When there are multiple carets on a line, only copy it once. + line_ranges = get_line_ranges_from_carets(false, true); + } else { + line_ranges.push_back(Point2i(get_caret_line(p_caret), get_caret_line(p_caret))); + } + for (Point2i line_range : line_ranges) { + for (int i = line_range.x; i <= line_range.y; i++) { + if (text[i].length() != 0) { + clipboard += _base_get_text(i, 0, i, text[i].length()); } + clipboard += "\n"; } } String clipboard_string = clipboard.as_string(); DisplayServer::get_singleton()->clipboard_set(clipboard_string); - cut_copy_line = clipboard_string; + // Set the cut copy line so we know to paste as a line. + if (get_caret_count() == 1) { + cut_copy_line = clipboard_string; + } else { + cut_copy_line = ""; + } } void TextEdit::_paste_internal(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); if (!editable) { return; } String clipboard = DisplayServer::get_singleton()->clipboard_get(); + + // Paste a full line. Ignore '\r' characters that may have been added to the clipboard by the OS. + if (get_caret_count() == 1 && !has_selection(0) && !cut_copy_line.is_empty() && cut_copy_line == clipboard.replace("\r", "")) { + insert_text(clipboard, get_caret_line(), 0); + return; + } + + // Paste text at each caret or one line per caret. Vector<String> clipboad_lines = clipboard.split("\n"); - bool insert_line_per_caret = p_caret == -1 && carets.size() > 1 && clipboad_lines.size() == carets.size(); + bool insert_line_per_caret = p_caret == -1 && get_caret_count() > 1 && clipboad_lines.size() == get_caret_count(); begin_complex_operation(); - 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) { + begin_multicaret_edit(); + Vector<int> sorted_carets = get_sorted_carets(); + for (int i = 0; i < get_caret_count(); i++) { + int caret_index = sorted_carets[i]; + if (p_caret != -1 && p_caret != caret_index) { 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 (has_selection(caret_index)) { + delete_selection(caret_index); } if (insert_line_per_caret) { - clipboard = clipboad_lines[clipboad_line]; + clipboard = clipboad_lines[i]; } - insert_text_at_caret(clipboard, i); - clipboad_line--; + insert_text_at_caret(clipboard, caret_index); } + end_multicaret_edit(); end_complex_operation(); } void TextEdit::_paste_primary_clipboard_internal(int p_caret) { - ERR_FAIL_COND(p_caret > carets.size()); + ERR_FAIL_COND(p_caret >= get_caret_count() || p_caret < -1); if (!is_editable() || !DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { return; } String paste_buffer = DisplayServer::get_singleton()->clipboard_get_primary(); - if (carets.size() == 1) { + if (get_caret_count() == 1) { Point2i pos = get_line_column_at_pos(get_local_mouse_pos()); deselect(); - set_caret_line(pos.y, true, false); + set_caret_line(pos.y, true, false, -1); set_caret_column(pos.x); } @@ -7203,10 +7350,26 @@ int TextEdit::_get_char_pos_for_line(int p_px, int p_line, int p_wrap_index) con } /* Caret */ +void TextEdit::_caret_changed(int p_caret) { + queue_redraw(); + + if (has_selection(p_caret)) { + _selection_changed(p_caret); + } + + if (caret_pos_dirty) { + return; + } + + if (is_inside_tree()) { + callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); + } + caret_pos_dirty = true; +} + 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() { @@ -7251,60 +7414,152 @@ int TextEdit::_get_column_x_offset_for_line(int p_char, int p_line, int p_column } } -/* Selection */ -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) && get_selection_mode() != SelectionMode::SELECTION_MODE_NONE) { - switch (get_selection_mode()) { - case SelectionMode::SELECTION_MODE_POINTER: { - _update_selection_mode_pointer(); - } break; - case SelectionMode::SELECTION_MODE_WORD: { - _update_selection_mode_word(); - } break; - case SelectionMode::SELECTION_MODE_LINE: { - _update_selection_mode_line(); - } break; - default: { - break; +bool TextEdit::_is_line_col_in_range(int p_line, int p_column, int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_include_edges) const { + if (p_line >= p_from_line && p_line <= p_to_line && (p_line > p_from_line || p_column > p_from_column) && (p_line < p_to_line || p_column < p_to_column)) { + return true; + } + if (p_include_edges) { + if ((p_line == p_from_line && p_column == p_from_column) || (p_line == p_to_line && p_column == p_to_column)) { + return true; + } + } + return false; +} + +void TextEdit::_offset_carets_after(int p_old_line, int p_old_column, int p_new_line, int p_new_column, bool p_include_selection_begin, bool p_include_selection_end) { + // Moves all carets at or after old_line and old_column. + // Called after deleting or inserting text so that the carets stay with the text they are at. + + int edit_height = p_new_line - p_old_line; + int edit_size = p_new_column - p_old_column; + if (edit_height == 0 && edit_size == 0) { + return; + } + + // Intentionally includes carets in the multicaret_edit_ignore list so that they are moved together. + for (int i = 0; i < get_caret_count(); i++) { + bool selected = has_selection(i); + bool caret_at_end = selected && is_caret_after_selection_origin(i); + bool include_caret_at = caret_at_end ? p_include_selection_end : p_include_selection_begin; + + // Move caret. + int caret_line = get_caret_line(i); + int caret_column = get_caret_column(i); + bool caret_after = caret_line > p_old_line || (caret_line == p_old_line && caret_column > p_old_column); + bool caret_at = caret_line == p_old_line && caret_column == p_old_column; + if (caret_after || (caret_at && include_caret_at)) { + caret_line += edit_height; + if (caret_line == p_new_line) { + caret_column += edit_size; } + + if (edit_height != 0) { + set_caret_line(caret_line, false, true, -1, i); + } + set_caret_column(caret_column, false, i); } - } else { + + // Move selection origin. + if (!selected) { + continue; + } + bool include_selection_origin_at = !caret_at_end ? p_include_selection_end : p_include_selection_begin; + + int selection_origin_line = get_selection_origin_line(i); + int selection_origin_column = get_selection_origin_column(i); + bool selection_origin_after = selection_origin_line > p_old_line || (selection_origin_line == p_old_line && selection_origin_column > p_old_column); + bool selection_origin_at = selection_origin_line == p_old_line && selection_origin_column == p_old_column; + if (selection_origin_after || (selection_origin_at && include_selection_origin_at)) { + selection_origin_line += edit_height; + if (selection_origin_line == p_new_line) { + selection_origin_column += edit_size; + } + select(selection_origin_line, selection_origin_column, caret_line, caret_column, i); + } + } + if (!p_include_selection_begin && p_include_selection_end && has_selection()) { + // It is possible that two adjacent selections now overlap. + merge_overlapping_carets(); + } +} + +void TextEdit::_cancel_drag_and_drop_text() { + // Cancel the drag operation if drag originated from here. + if (selection_drag_attempt && get_viewport()) { + get_viewport()->gui_cancel_drag(); + } +} + +/* Selection */ +void TextEdit::_selection_changed(int p_caret) { + if (!selecting_enabled) { + return; + } + + _cancel_drag_and_drop_text(); + queue_redraw(); +} + +void TextEdit::_click_selection_held() { + // Update the selection mode on a timer so it is updated when the view scrolls even if the mouse isn't moving. + if (!Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT) || get_selection_mode() == SelectionMode::SELECTION_MODE_NONE) { click_select_held->stop(); + return; + } + switch (get_selection_mode()) { + case SelectionMode::SELECTION_MODE_POINTER: { + _update_selection_mode_pointer(); + } break; + case SelectionMode::SELECTION_MODE_WORD: { + _update_selection_mode_word(); + } break; + case SelectionMode::SELECTION_MODE_LINE: { + _update_selection_mode_line(); + } break; + default: { + break; + } } } -void TextEdit::_update_selection_mode_pointer() { - dragging_selection = true; +void TextEdit::_update_selection_mode_pointer(bool p_initial) { Point2 mp = get_local_mouse_pos(); Point2i pos = get_line_column_at_pos(mp); int line = pos.y; - int col = pos.x; - int caret_idx = carets.size() - 1; - - select(carets[caret_idx].selection.selecting_line, carets[caret_idx].selection.selecting_column, line, col, caret_idx); + int column = pos.x; + int caret_index = get_caret_count() - 1; + + if (p_initial && !has_selection(caret_index)) { + set_selection_origin_line(line, true, -1, caret_index); + set_selection_origin_column(column, caret_index); + // Set the word begin and end to the column in case the mode changes later. + carets.write[caret_index].selection.word_begin_column = column; + carets.write[caret_index].selection.word_end_column = column; + } else { + select(get_selection_origin_line(caret_index), get_selection_origin_column(caret_index), line, column, caret_index); + } + adjust_viewport_to_caret(caret_index); - set_caret_line(line, false, true, 0, caret_idx); - set_caret_column(col, true, caret_idx); - queue_redraw(); + if (has_selection(caret_index)) { + // Only set to true if any selection has been made. + dragging_selection = true; + } click_select_held->start(); merge_overlapping_carets(); } -void TextEdit::_update_selection_mode_word() { +void TextEdit::_update_selection_mode_word(bool p_initial) { dragging_selection = true; Point2 mp = get_local_mouse_pos(); Point2i pos = get_line_column_at_pos(mp); int line = pos.y; - int col = pos.x; - int caret_idx = carets.size() - 1; + int column = pos.x; + int caret_index = get_caret_count() - 1; - int caret_pos = CLAMP(col, 0, text[line].length()); + int caret_pos = CLAMP(column, 0, text[line].length()); int beg = caret_pos; int end = beg; PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid()); @@ -7316,70 +7571,57 @@ void TextEdit::_update_selection_mode_word() { } } - /* Initial selection. */ - 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); + if (p_initial && !has_selection(caret_index)) { + // Set the selection origin if there is no existing selection. + select(line, beg, line, end, caret_index); + carets.write[caret_index].selection.word_begin_column = beg; + carets.write[caret_index].selection.word_end_column = end; } else { - 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(line, false, true, 0, caret_idx); - set_caret_column(beg, true, caret_idx); - } else { - 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); - } + // Expand the word selection to the mouse. + int origin_line = get_selection_origin_line(caret_index); + bool is_new_selection_dir_right = line > origin_line || (line == origin_line && column >= carets[caret_index].selection.word_begin_column); + int origin_col = is_new_selection_dir_right ? carets[caret_index].selection.word_begin_column : carets[caret_index].selection.word_end_column; + int caret_col = is_new_selection_dir_right ? end : beg; + + select(origin_line, origin_col, line, caret_col, caret_index); } + adjust_viewport_to_caret(caret_index); if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text()); } - queue_redraw(); - click_select_held->start(); merge_overlapping_carets(); } -void TextEdit::_update_selection_mode_line() { +void TextEdit::_update_selection_mode_line(bool p_initial) { dragging_selection = true; Point2 mp = get_local_mouse_pos(); 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 < carets[caret_idx].selection.selecting_line) { - // Caret is above us. - set_caret_line(line - 1, false, true, 0, caret_idx); - carets.write[caret_idx].selection.selecting_column = has_selection(caret_idx) - ? text[get_selection_line(caret_idx)].length() - : 0; - } else { - // Caret is below us. - set_caret_line(line + 1, false, true, 0, caret_idx); - carets.write[caret_idx].selection.selecting_column = 0; - col = text[line].length(); + int caret_index = get_caret_count() - 1; + + int origin_line = p_initial && !has_selection(caret_index) ? line : get_selection_origin_line(); + bool line_below = line >= origin_line; + int origin_col = line_below ? 0 : get_line(origin_line).length(); + int caret_line = line_below ? line + 1 : line; + int caret_col = caret_line < text.size() ? 0 : get_line(text.size() - 1).length(); + + select(origin_line, origin_col, caret_line, caret_col, caret_index); + adjust_viewport_to_caret(caret_index); + + if (p_initial) { + // Set the word begin and end to the start and end of the origin line in case the mode changes later. + carets.write[caret_index].selection.word_begin_column = 0; + carets.write[caret_index].selection.word_end_column = get_line(origin_line).length(); } - set_caret_column(0, false, caret_idx); - 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()); } - queue_redraw(); - click_select_held->start(); merge_overlapping_carets(); } @@ -7389,23 +7631,23 @@ void TextEdit::_pre_shift_selection(int p_caret) { return; } - 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); + set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT); + if (has_selection(p_caret)) { return; } - - set_selection_mode(SelectionMode::SELECTION_MODE_SHIFT, get_selection_line(p_caret), get_selection_column(p_caret), p_caret); + // Prepare selection to start at current caret position. + set_selection_origin_line(get_caret_line(p_caret), true, -1, p_caret); + set_selection_origin_column(get_caret_column(p_caret), p_caret); + carets.write[p_caret].selection.active = true; + carets.write[p_caret].selection.word_begin_column = get_caret_column(p_caret); + carets.write[p_caret].selection.word_end_column = get_caret_column(p_caret); } -void TextEdit::_post_shift_selection(int p_caret) { - if (!selecting_enabled) { - return; - } - - 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); +bool TextEdit::_selection_contains(int p_caret, int p_line, int p_column, bool p_include_edges, bool p_only_selections) const { + if (!has_selection(p_caret)) { + return !p_only_selections && p_line == get_caret_line(p_caret) && p_column == get_caret_column(p_caret); } + return _is_line_col_in_range(p_line, p_column, get_selection_from_line(p_caret), get_selection_from_column(p_caret), get_selection_to_line(p_caret), get_selection_to_column(p_caret), p_include_edges); } /* Line Wrapping */ @@ -7780,9 +8022,43 @@ Dictionary TextEdit::_get_line_syntax_highlighting(int p_line) { return syntax_highlighter.is_null() && !setting_text ? Dictionary() : syntax_highlighter->get_line_syntax_highlighting(p_line); } +/* Deprecated. */ +#ifndef DISABLE_DEPRECATED +Vector<int> TextEdit::get_caret_index_edit_order() { + Vector<int> carets_order = get_sorted_carets(); + carets_order.reverse(); + return carets_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 TextEdit::get_selection_line(int p_caret) const { + return get_selection_origin_line(p_caret); +} + +int TextEdit::get_selection_column(int p_caret) const { + return get_selection_origin_column(p_caret); +} +#endif + /*** Super internal Core API. Everything builds on it. ***/ -void TextEdit::_text_changed_emit() { +void TextEdit::_text_changed() { + _cancel_drag_and_drop_text(); + queue_redraw(); + + if (text_changed_dirty || setting_text) { + return; + } + + if (is_inside_tree()) { + callable_mp(this, &TextEdit::_emit_text_changed).call_deferred(); + } + text_changed_dirty = true; +} + +void TextEdit::_emit_text_changed() { emit_signal(SNAME("text_changed")); text_changed_dirty = false; } @@ -7918,12 +8194,7 @@ void TextEdit::_base_insert_text(int p_line, int p_char, const String &p_text, i input_direction = (TextDirection)dir; } - if (!text_changed_dirty && !setting_text) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_text_changed_emit).call_deferred(); - } - text_changed_dirty = true; - } + _text_changed(); emit_signal(SNAME("lines_edited_from"), p_line, r_end_line); } @@ -7964,12 +8235,7 @@ void TextEdit::_base_remove_text(int p_from_line, int p_from_column, int p_to_li text.remove_range(p_from_line, p_to_line); text.set(p_from_line, pre_text + post_text, structured_text_parser(st_parser, st_args, pre_text + post_text)); - if (!text_changed_dirty && !setting_text) { - if (is_inside_tree()) { - callable_mp(this, &TextEdit::_text_changed_emit).call_deferred(); - } - text_changed_dirty = true; - } + _text_changed(); emit_signal(SNAME("lines_edited_from"), p_to_line, p_from_line); } diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index 1099295d3b..efade39876 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -389,18 +389,12 @@ private: /* 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; + int origin_line = 0; + int origin_column = 0; + int origin_last_fit_x = 0; + int word_begin_column = 0; + int word_end_column = 0; }; struct Caret { @@ -415,11 +409,13 @@ private: // 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; + + int multicaret_edit_count = 0; + bool multicaret_edit_merge_queued = false; + HashSet<int> multicaret_edit_ignore_carets; CaretType caret_type = CaretType::CARET_TYPE_LINE; @@ -438,12 +434,18 @@ private: bool drag_action = false; bool drag_caret_force_displayed = false; + void _caret_changed(int p_caret = -1); void _emit_caret_changed(); void _reset_caret_blink_timer(); void _toggle_draw_caret(); int _get_column_x_offset_for_line(int p_char, int p_line, int p_column) const; + bool _is_line_col_in_range(int p_line, int p_column, int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_include_edges = true) const; + + void _offset_carets_after(int p_old_line, int p_old_column, int p_new_line, int p_new_column, bool p_include_selection_begin = true, bool p_include_selection_end = true); + + void _cancel_drag_and_drop_text(); /* Selection. */ SelectionMode selecting_mode = SelectionMode::SELECTION_MODE_NONE; @@ -456,18 +458,23 @@ private: bool selection_drag_attempt = false; bool dragging_selection = false; + int drag_and_drop_origin_caret_index = -1; + int drag_caret_index = -1; Timer *click_select_held = nullptr; uint64_t last_dblclk = 0; Vector2 last_dblclk_pos; + + void _selection_changed(int p_caret = -1); void _click_selection_held(); - void _update_selection_mode_pointer(); - void _update_selection_mode_word(); - void _update_selection_mode_line(); + void _update_selection_mode_pointer(bool p_initial = false); + void _update_selection_mode_word(bool p_initial = false); + void _update_selection_mode_line(bool p_initial = false); void _pre_shift_selection(int p_caret); - void _post_shift_selection(int p_caret); + + bool _selection_contains(int p_caret, int p_line, int p_column, bool p_include_edges = true, bool p_only_selections = true) const; /* Line wrapping. */ LineWrappingMode line_wrapping_mode = LineWrappingMode::LINE_WRAPPING_NONE; @@ -599,7 +606,8 @@ private: /*** Super internal Core API. Everything builds on it. ***/ bool text_changed_dirty = false; - void _text_changed_emit(); + void _text_changed(); + void _emit_text_changed(); void _insert_text(int p_line, int p_char, const String &p_text, int *r_end_line = nullptr, int *r_end_char = nullptr); void _remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column); @@ -625,13 +633,15 @@ private: void _move_caret_document_end(bool p_select); bool _clear_carets_and_selection(); - // Used in add_caret_at_carets - void _get_above_below_caret_line_column(int p_old_line, int p_old_wrap_index, int p_old_column, bool p_below, int &p_new_line, int &p_new_column, int p_last_fit_x = -1) const; - protected: void _notification(int p_what); static void _bind_methods(); +#ifndef DISABLE_DEPRECATED + void _set_selection_mode_compat_86978(SelectionMode p_mode, int p_line = -1, int p_column = -1, int p_caret = 0); + static void _bind_compatibility_methods(); +#endif // DISABLE_DEPRECATED + virtual void _update_theme_item_cache() override; /* Internal API for CodeEdit, pending public API. */ @@ -659,6 +669,7 @@ protected: bool _is_line_hidden(int p_line) const; void _unhide_all_lines(); + virtual void _unhide_carets(); // Symbol lookup. String lookup_symbol_word; @@ -765,9 +776,11 @@ 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, int p_caret = -1); + void insert_line_at(int p_line, const String &p_text); + void remove_line_at(int p_line, bool p_move_carets_down = true); + void insert_text_at_caret(const String &p_text, int p_caret = -1); + void insert_text(const String &p_text, int p_line, int p_column, bool p_before_selection_begin = true, bool p_before_selection_end = false); void remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column); int get_last_unhidden_line() const; @@ -851,15 +864,20 @@ public: void set_multiple_carets_enabled(bool p_enabled); bool is_multiple_carets_enabled() const; - int add_caret(int p_line, int p_col); + int add_caret(int p_line, int p_column); void remove_caret(int p_caret); void remove_secondary_carets(); - void merge_overlapping_carets(); int get_caret_count() const; void add_caret_at_carets(bool p_below); - 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); + Vector<int> get_sorted_carets(bool p_include_ignored_carets = false) const; + void collapse_carets(int p_from_line, int p_from_column, int p_to_line, int p_to_column, bool p_inclusive = false); + + void merge_overlapping_carets(); + void begin_multicaret_edit(); + void end_multicaret_edit(); + bool is_in_mulitcaret_edit() const; + bool multicaret_edit_ignore_caret(int p_caret) const; bool is_caret_visible(int p_caret = 0) const; Point2 get_caret_draw_pos(int p_caret = 0) const; @@ -867,7 +885,7 @@ public: 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 p_caret = 0); + void set_caret_column(int p_column, bool p_adjust_viewport = true, int p_caret = 0); int get_caret_column(int p_caret = 0) const; int get_caret_wrap_index(int p_caret = 0) const; @@ -884,27 +902,34 @@ public: void set_drag_and_drop_selection_enabled(const bool p_enabled); bool is_drag_and_drop_selection_enabled() const; - void set_selection_mode(SelectionMode p_mode, int p_line = -1, int p_column = -1, int p_caret = 0); + void set_selection_mode(SelectionMode p_mode); SelectionMode get_selection_mode() const; void select_all(); void select_word_under_caret(int p_caret = -1); void add_selection_for_next_occurrence(); void skip_selection_for_next_occurrence(); - void select(int p_from_line, int p_from_column, int p_to_line, int p_to_column, int p_caret = 0); + void select(int p_origin_line, int p_origin_column, int p_caret_line, int p_caret_column, int p_caret = 0); bool has_selection(int p_caret = -1) const; String get_selected_text(int p_caret = -1); + int get_selection_at_line_column(int p_line, int p_column, bool p_include_edges = true, bool p_only_selections = true) const; + Vector<Point2i> get_line_ranges_from_carets(bool p_only_selections = false, bool p_merge_adjacent = true) const; + TypedArray<Vector2i> get_line_ranges_from_carets_typed_array(bool p_only_selections = false, bool p_merge_adjacent = true) const; - int get_selection_line(int p_caret = 0) const; - int get_selection_column(int p_caret = 0) const; + void set_selection_origin_line(int p_line, bool p_can_be_hidden = true, int p_wrap_index = -1, int p_caret = 0); + void set_selection_origin_column(int p_column, int p_caret = 0); + int get_selection_origin_line(int p_caret = 0) const; + int get_selection_origin_column(int p_caret = 0) 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; + bool is_caret_after_selection_origin(int p_caret = 0) const; + void deselect(int p_caret = -1); void delete_selection(int p_caret = -1); @@ -1043,6 +1068,15 @@ public: Color get_font_color() const; + /* Deprecated. */ +#ifndef DISABLE_DEPRECATED + 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); + + int get_selection_line(int p_caret = 0) const; + int get_selection_column(int p_caret = 0) const; +#endif + TextEdit(const String &p_placeholder = String()); }; diff --git a/scene/main/status_indicator.cpp b/scene/main/status_indicator.cpp index 54b2ff75ca..22aa051c70 100644 --- a/scene/main/status_indicator.cpp +++ b/scene/main/status_indicator.cpp @@ -30,6 +30,8 @@ #include "status_indicator.h" +#include "scene/gui/popup_menu.h" + void StatusIndicator::_notification(int p_what) { ERR_MAIN_THREAD_GUARD; #ifdef TOOLS_ENABLED @@ -43,12 +45,22 @@ void StatusIndicator::_notification(int p_what) { if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_STATUS_INDICATOR)) { if (visible && iid == DisplayServer::INVALID_INDICATOR_ID) { iid = DisplayServer::get_singleton()->create_status_indicator(icon, tooltip, callable_mp(this, &StatusIndicator::_callback)); + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(menu)); + if (pm) { + RID menu_rid = pm->bind_global_menu(); + DisplayServer::get_singleton()->status_indicator_set_menu(iid, menu_rid); + } } } } break; case NOTIFICATION_EXIT_TREE: { if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_STATUS_INDICATOR)) { if (iid != DisplayServer::INVALID_INDICATOR_ID) { + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(menu)); + if (pm) { + pm->unbind_global_menu(); + DisplayServer::get_singleton()->status_indicator_set_menu(iid, RID()); + } DisplayServer::get_singleton()->delete_status_indicator(iid); iid = DisplayServer::INVALID_INDICATOR_ID; } @@ -66,11 +78,14 @@ void StatusIndicator::_bind_methods() { ClassDB::bind_method(D_METHOD("get_icon"), &StatusIndicator::get_icon); ClassDB::bind_method(D_METHOD("set_visible", "visible"), &StatusIndicator::set_visible); ClassDB::bind_method(D_METHOD("is_visible"), &StatusIndicator::is_visible); + ClassDB::bind_method(D_METHOD("set_menu", "menu"), &StatusIndicator::set_menu); + ClassDB::bind_method(D_METHOD("get_menu"), &StatusIndicator::get_menu); ADD_SIGNAL(MethodInfo("pressed", PropertyInfo(Variant::INT, "mouse_button"), PropertyInfo(Variant::VECTOR2I, "mouse_position"))); ADD_PROPERTY(PropertyInfo(Variant::STRING, "tooltip", PROPERTY_HINT_MULTILINE_TEXT), "set_tooltip", "get_tooltip"); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "icon", PROPERTY_HINT_RESOURCE_TYPE, "Image"), "set_icon", "get_icon"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_icon", "get_icon"); + ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "menu", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "PopupMenu"), "set_menu", "get_menu"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "visible"), "set_visible", "is_visible"); } @@ -78,7 +93,7 @@ void StatusIndicator::_callback(MouseButton p_index, const Point2i &p_pos) { emit_signal(SNAME("pressed"), p_index, p_pos); } -void StatusIndicator::set_icon(const Ref<Image> &p_icon) { +void StatusIndicator::set_icon(const Ref<Texture2D> &p_icon) { ERR_MAIN_THREAD_GUARD; icon = p_icon; if (iid != DisplayServer::INVALID_INDICATOR_ID) { @@ -86,7 +101,7 @@ void StatusIndicator::set_icon(const Ref<Image> &p_icon) { } } -Ref<Image> StatusIndicator::get_icon() const { +Ref<Texture2D> StatusIndicator::get_icon() const { return icon; } @@ -102,6 +117,30 @@ String StatusIndicator::get_tooltip() const { return tooltip; } +void StatusIndicator::set_menu(const NodePath &p_menu) { + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(menu)); + if (pm) { + pm->unbind_global_menu(); + if (iid != DisplayServer::INVALID_INDICATOR_ID) { + DisplayServer::get_singleton()->status_indicator_set_menu(iid, RID()); + } + } + + menu = p_menu; + + pm = Object::cast_to<PopupMenu>(get_node_or_null(menu)); + if (pm) { + if (iid != DisplayServer::INVALID_INDICATOR_ID) { + RID menu_rid = pm->bind_global_menu(); + DisplayServer::get_singleton()->status_indicator_set_menu(iid, menu_rid); + } + } +} + +NodePath StatusIndicator::get_menu() const { + return menu; +} + void StatusIndicator::set_visible(bool p_visible) { ERR_MAIN_THREAD_GUARD; if (visible == p_visible) { @@ -122,8 +161,18 @@ void StatusIndicator::set_visible(bool p_visible) { if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_STATUS_INDICATOR)) { if (visible && iid == DisplayServer::INVALID_INDICATOR_ID) { iid = DisplayServer::get_singleton()->create_status_indicator(icon, tooltip, callable_mp(this, &StatusIndicator::_callback)); + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(menu)); + if (pm) { + RID menu_rid = pm->bind_global_menu(); + DisplayServer::get_singleton()->status_indicator_set_menu(iid, menu_rid); + } } if (!visible && iid != DisplayServer::INVALID_INDICATOR_ID) { + PopupMenu *pm = Object::cast_to<PopupMenu>(get_node_or_null(menu)); + if (pm) { + pm->unbind_global_menu(); + DisplayServer::get_singleton()->status_indicator_set_menu(iid, RID()); + } DisplayServer::get_singleton()->delete_status_indicator(iid); iid = DisplayServer::INVALID_INDICATOR_ID; } diff --git a/scene/main/status_indicator.h b/scene/main/status_indicator.h index aa3aa68d78..cc137391a9 100644 --- a/scene/main/status_indicator.h +++ b/scene/main/status_indicator.h @@ -37,10 +37,11 @@ class StatusIndicator : public Node { GDCLASS(StatusIndicator, Node); - Ref<Image> icon; + Ref<Texture2D> icon; String tooltip; bool visible = true; DisplayServer::IndicatorID iid = DisplayServer::INVALID_INDICATOR_ID; + NodePath menu; protected: void _notification(int p_what); @@ -49,12 +50,15 @@ protected: void _callback(MouseButton p_index, const Point2i &p_pos); public: - void set_icon(const Ref<Image> &p_icon); - Ref<Image> get_icon() const; + void set_icon(const Ref<Texture2D> &p_icon); + Ref<Texture2D> get_icon() const; void set_tooltip(const String &p_tooltip); String get_tooltip() const; + void set_menu(const NodePath &p_menu); + NodePath get_menu() const; + void set_visible(bool p_visible); bool is_visible() const; }; diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp index 73ce166123..07e7273343 100644 --- a/scene/main/viewport.cpp +++ b/scene/main/viewport.cpp @@ -688,6 +688,18 @@ void Viewport::_process_picking() { physics_picking_events.clear(); return; } +#ifndef _3D_DISABLED + if (use_xr) { + if (XRServer::get_singleton() != nullptr) { + Ref<XRInterface> xr_interface = XRServer::get_singleton()->get_primary_interface(); + if (xr_interface.is_valid() && xr_interface->is_initialized() && xr_interface->get_view_count() > 1) { + WARN_PRINT_ONCE("Object picking can't be used when stereo rendering, this will be turned off!"); + physics_object_picking = false; // don't try again. + return; + } + } + } +#endif _drop_physics_mouseover(true); @@ -856,9 +868,10 @@ void Viewport::_process_picking() { if (send_event) { co->_input_event_call(this, ev, res[i].shape); - if (physics_object_picking_first_only) { - break; - } + } + + if (physics_object_picking_first_only) { + break; } } } @@ -3578,6 +3591,13 @@ bool Viewport::gui_is_drag_successful() const { return gui.drag_successful; } +void Viewport::gui_cancel_drag() { + ERR_MAIN_THREAD_GUARD; + if (gui_is_dragging()) { + _perform_drop(); + } +} + void Viewport::set_input_as_handled() { ERR_MAIN_THREAD_GUARD; if (!handle_input_locally) { diff --git a/scene/main/viewport.h b/scene/main/viewport.h index 21832a454c..c6a757acd0 100644 --- a/scene/main/viewport.h +++ b/scene/main/viewport.h @@ -616,6 +616,7 @@ public: bool gui_is_dragging() const; bool gui_is_drag_successful() const; + void gui_cancel_drag(); Control *gui_find_control(const Point2 &p_global); diff --git a/scene/resources/material.cpp b/scene/resources/material.cpp index b381096df8..15b40e776c 100644 --- a/scene/resources/material.cpp +++ b/scene/resources/material.cpp @@ -688,6 +688,9 @@ void BaseMaterial3D::_update_shader() { case BLEND_MODE_MUL: code += "blend_mul"; break; + case BLEND_MODE_PREMULT_ALPHA: + code += "blend_premul_alpha"; + break; case BLEND_MODE_MAX: break; // Internal value, skip. } @@ -1819,6 +1822,11 @@ void fragment() {)"; vec3 detail = mix(ALBEDO.rgb, ALBEDO.rgb * detail_tex.rgb, detail_tex.a); )"; } break; + case BLEND_MODE_PREMULT_ALPHA: { + // This is unlikely to ever be used for detail textures, and in order for it to function in the editor, another bit must be used in MaterialKey, + // but there are only 5 bits left, so I'm going to leave this disabled unless it's actually requested. + //code += "\tvec3 detail = (1.0-detail_tex.a)*ALBEDO.rgb+detail_tex.rgb;\n"; + } break; case BLEND_MODE_MAX: break; // Internal value, skip. } @@ -3040,7 +3048,7 @@ void BaseMaterial3D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "alpha_hash_scale", PROPERTY_HINT_RANGE, "0,2,0.01"), "set_alpha_hash_scale", "get_alpha_hash_scale"); ADD_PROPERTY(PropertyInfo(Variant::INT, "alpha_antialiasing_mode", PROPERTY_HINT_ENUM, "Disabled,Alpha Edge Blend,Alpha Edge Clip"), "set_alpha_antialiasing", "get_alpha_antialiasing"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "alpha_antialiasing_edge", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_alpha_antialiasing_edge", "get_alpha_antialiasing_edge"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "blend_mode", PROPERTY_HINT_ENUM, "Mix,Add,Subtract,Multiply"), "set_blend_mode", "get_blend_mode"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "blend_mode", PROPERTY_HINT_ENUM, "Mix,Add,Subtract,Multiply,Premultiplied Alpha"), "set_blend_mode", "get_blend_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "cull_mode", PROPERTY_HINT_ENUM, "Back,Front,Disabled"), "set_cull_mode", "get_cull_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "depth_draw_mode", PROPERTY_HINT_ENUM, "Opaque Only,Always,Never"), "set_depth_draw_mode", "get_depth_draw_mode"); ADD_PROPERTYI(PropertyInfo(Variant::BOOL, "no_depth_test"), "set_flag", "get_flag", FLAG_DISABLE_DEPTH_TEST); @@ -3269,6 +3277,7 @@ void BaseMaterial3D::_bind_methods() { BIND_ENUM_CONSTANT(BLEND_MODE_ADD); BIND_ENUM_CONSTANT(BLEND_MODE_SUB); BIND_ENUM_CONSTANT(BLEND_MODE_MUL); + BIND_ENUM_CONSTANT(BLEND_MODE_PREMULT_ALPHA); BIND_ENUM_CONSTANT(ALPHA_ANTIALIASING_OFF); BIND_ENUM_CONSTANT(ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE); diff --git a/scene/resources/material.h b/scene/resources/material.h index 073403f71e..ecf79c581b 100644 --- a/scene/resources/material.h +++ b/scene/resources/material.h @@ -219,6 +219,7 @@ public: BLEND_MODE_ADD, BLEND_MODE_SUB, BLEND_MODE_MUL, + BLEND_MODE_PREMULT_ALPHA, BLEND_MODE_MAX }; diff --git a/scene/resources/particle_process_material.cpp b/scene/resources/particle_process_material.cpp index 685625ab72..0b65b33240 100644 --- a/scene/resources/particle_process_material.cpp +++ b/scene/resources/particle_process_material.cpp @@ -634,7 +634,7 @@ void ParticleProcessMaterial::_update_shader() { if (emission_shape == EMISSION_SHAPE_RING) { code += " \n"; code += " float ring_spawn_angle = rand_from_seed(alt_seed) * 2.0 * pi;\n"; - code += " float ring_random_radius = rand_from_seed(alt_seed) * (emission_ring_radius - emission_ring_inner_radius) + emission_ring_inner_radius;\n"; + code += " float ring_random_radius = sqrt(rand_from_seed(alt_seed) * (emission_ring_radius - emission_ring_inner_radius * emission_ring_inner_radius) + emission_ring_inner_radius * emission_ring_inner_radius);\n"; code += " vec3 axis = emission_ring_axis == vec3(0.0) ? vec3(0.0, 0.0, 1.0) : normalize(emission_ring_axis);\n"; code += " vec3 ortho_axis = vec3(0.0);\n"; code += " if (abs(axis) == vec3(1.0, 0.0, 0.0)) {\n"; @@ -1136,9 +1136,9 @@ void ParticleProcessMaterial::_update_shader() { code += " if (COLLIDED) emit_count = sub_emitter_amount_at_collision;\n"; } break; case SUB_EMITTER_AT_END: { - code += " float unit_delta = DELTA/LIFETIME;\n"; - code += " float end_time = CUSTOM.w * 0.95;\n"; // if we do at the end we might miss it, as it can just get deactivated by emitter - code += " if (CUSTOM.y < end_time && (CUSTOM.y + unit_delta) >= end_time) emit_count = sub_emitter_amount_at_end;\n"; + code += " if ((CUSTOM.y / CUSTOM.w * LIFETIME) > (LIFETIME - DELTA)) {\n"; + code += " emit_count = sub_emitter_amount_at_end;\n"; + code += " }\n"; } break; default: { } diff --git a/scene/resources/visual_shader.cpp b/scene/resources/visual_shader.cpp index 6f1aa5c850..4b51f6c471 100644 --- a/scene/resources/visual_shader.cpp +++ b/scene/resources/visual_shader.cpp @@ -4928,6 +4928,10 @@ String VisualShaderNodeExpression::generate_code(Shader::Mode p_mode, VisualShad return code; } +bool VisualShaderNodeExpression::is_output_port_expandable(int p_port) const { + return false; +} + void VisualShaderNodeExpression::_bind_methods() { ClassDB::bind_method(D_METHOD("set_expression", "expression"), &VisualShaderNodeExpression::set_expression); ClassDB::bind_method(D_METHOD("get_expression"), &VisualShaderNodeExpression::get_expression); diff --git a/scene/resources/visual_shader.h b/scene/resources/visual_shader.h index d7270f3ac6..d32e2465b9 100644 --- a/scene/resources/visual_shader.h +++ b/scene/resources/visual_shader.h @@ -878,6 +878,7 @@ public: String get_expression() const; virtual String generate_code(Shader::Mode p_mode, VisualShader::Type p_type, int p_id, const String *p_input_vars, const String *p_output_vars, bool p_for_preview = false) const override; + virtual bool is_output_port_expandable(int p_port) const override; VisualShaderNodeExpression(); }; diff --git a/servers/display_server.cpp b/servers/display_server.cpp index 9600caa214..f1e3479eae 100644 --- a/servers/display_server.cpp +++ b/servers/display_server.cpp @@ -709,12 +709,12 @@ void DisplayServer::set_icon(const Ref<Image> &p_icon) { WARN_PRINT("Icon not supported by this display server."); } -DisplayServer::IndicatorID DisplayServer::create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) { +DisplayServer::IndicatorID DisplayServer::create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback) { WARN_PRINT("Status indicator not supported by this display server."); return INVALID_INDICATOR_ID; } -void DisplayServer::status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) { +void DisplayServer::status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon) { WARN_PRINT("Status indicator not supported by this display server."); } @@ -722,6 +722,10 @@ void DisplayServer::status_indicator_set_tooltip(IndicatorID p_id, const String WARN_PRINT("Status indicator not supported by this display server."); } +void DisplayServer::status_indicator_set_menu(IndicatorID p_id, const RID &p_menu_rid) { + WARN_PRINT("Status indicator not supported by this display server."); +} + void DisplayServer::status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) { WARN_PRINT("Status indicator not supported by this display server."); } @@ -977,6 +981,7 @@ void DisplayServer::_bind_methods() { ClassDB::bind_method(D_METHOD("create_status_indicator", "icon", "tooltip", "callback"), &DisplayServer::create_status_indicator); ClassDB::bind_method(D_METHOD("status_indicator_set_icon", "id", "icon"), &DisplayServer::status_indicator_set_icon); ClassDB::bind_method(D_METHOD("status_indicator_set_tooltip", "id", "tooltip"), &DisplayServer::status_indicator_set_tooltip); + ClassDB::bind_method(D_METHOD("status_indicator_set_menu", "id", "menu_rid"), &DisplayServer::status_indicator_set_menu); ClassDB::bind_method(D_METHOD("status_indicator_set_callback", "id", "callback"), &DisplayServer::status_indicator_set_callback); ClassDB::bind_method(D_METHOD("delete_status_indicator", "id"), &DisplayServer::delete_status_indicator); diff --git a/servers/display_server.h b/servers/display_server.h index aab51644c0..0391edecd4 100644 --- a/servers/display_server.h +++ b/servers/display_server.h @@ -564,9 +564,10 @@ public: virtual void set_native_icon(const String &p_filename); virtual void set_icon(const Ref<Image> &p_icon); - virtual IndicatorID create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback); - virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon); + virtual IndicatorID create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback); + virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon); virtual void status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip); + virtual void status_indicator_set_menu(IndicatorID p_id, const RID &p_menu_rid); virtual void status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback); virtual void delete_status_indicator(IndicatorID p_id); 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 8d865ba440..9a855adaa2 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 @@ -90,6 +90,7 @@ void SceneShaderForwardClustered::ShaderData::set_code(const String &p_code) { actions.render_mode_values["blend_mix"] = Pair<int *, int>(&blend_mode, BLEND_MODE_MIX); actions.render_mode_values["blend_sub"] = Pair<int *, int>(&blend_mode, BLEND_MODE_SUB); actions.render_mode_values["blend_mul"] = Pair<int *, int>(&blend_mode, BLEND_MODE_MUL); + actions.render_mode_values["blend_premult_alpha"] = Pair<int *, int>(&blend_mode, BLEND_MODE_PREMULT_ALPHA); actions.render_mode_values["alpha_to_coverage"] = Pair<int *, int>(&alpha_antialiasing_mode, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE); actions.render_mode_values["alpha_to_coverage_and_one"] = Pair<int *, int>(&alpha_antialiasing_mode, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE_AND_TO_ONE); @@ -244,7 +245,17 @@ void SceneShaderForwardClustered::ShaderData::set_code(const String &p_code) { blend_attachment.dst_color_blend_factor = RD::BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; blend_attachment.src_alpha_blend_factor = RD::BLEND_FACTOR_ONE; blend_attachment.dst_alpha_blend_factor = RD::BLEND_FACTOR_ZERO; - } + } break; + case BLEND_MODE_PREMULT_ALPHA: { + blend_attachment.enable_blend = true; + blend_attachment.alpha_blend_op = RD::BLEND_OP_ADD; + blend_attachment.color_blend_op = RD::BLEND_OP_ADD; + blend_attachment.src_color_blend_factor = RD::BLEND_FACTOR_ONE; + blend_attachment.dst_color_blend_factor = RD::BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blend_attachment.src_alpha_blend_factor = RD::BLEND_FACTOR_ONE; + blend_attachment.dst_alpha_blend_factor = RD::BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + uses_blend_alpha = true; // Force alpha used because of blend. + } break; } // Color pass -> attachment 0: Color/Diffuse, attachment 1: Separate Specular, attachment 2: Motion Vectors @@ -593,6 +604,7 @@ void SceneShaderForwardClustered::init(const String p_defines) { actions.renames["NORMAL_MAP_DEPTH"] = "normal_map_depth"; actions.renames["ALBEDO"] = "albedo"; actions.renames["ALPHA"] = "alpha"; + actions.renames["PREMULT_ALPHA_FACTOR"] = "premult_alpha"; actions.renames["METALLIC"] = "metallic"; actions.renames["SPECULAR"] = "specular"; actions.renames["ROUGHNESS"] = "roughness"; @@ -672,6 +684,7 @@ void SceneShaderForwardClustered::init(const String p_defines) { actions.usage_defines["INSTANCE_CUSTOM"] = "#define ENABLE_INSTANCE_CUSTOM\n"; actions.usage_defines["POSITION"] = "#define OVERRIDE_POSITION\n"; actions.usage_defines["LIGHT_VERTEX"] = "#define LIGHT_VERTEX_USED\n"; + actions.usage_defines["PREMULT_ALPHA_FACTOR"] = "#define PREMULT_ALPHA_USED\n"; actions.usage_defines["ALPHA_SCISSOR_THRESHOLD"] = "#define ALPHA_SCISSOR_USED\n"; actions.usage_defines["ALPHA_HASH_SCALE"] = "#define ALPHA_HASH_USED\n"; diff --git a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.h b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.h index 3b83b2b582..d5332032f9 100644 --- a/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.h +++ b/servers/rendering/renderer_rd/forward_clustered/scene_shader_forward_clustered.h @@ -106,7 +106,8 @@ public: BLEND_MODE_ADD, BLEND_MODE_SUB, BLEND_MODE_MUL, - BLEND_MODE_ALPHA_TO_COVERAGE + BLEND_MODE_ALPHA_TO_COVERAGE, + BLEND_MODE_PREMULT_ALPHA, }; enum DepthDraw { 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 0810f567cb..181a84b670 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 @@ -91,6 +91,7 @@ void SceneShaderForwardMobile::ShaderData::set_code(const String &p_code) { actions.render_mode_values["blend_mix"] = Pair<int *, int>(&blend_mode, BLEND_MODE_MIX); actions.render_mode_values["blend_sub"] = Pair<int *, int>(&blend_mode, BLEND_MODE_SUB); actions.render_mode_values["blend_mul"] = Pair<int *, int>(&blend_mode, BLEND_MODE_MUL); + actions.render_mode_values["blend_premult_alpha"] = Pair<int *, int>(&blend_mode, BLEND_MODE_PREMULT_ALPHA); actions.render_mode_values["alpha_to_coverage"] = Pair<int *, int>(&alpha_antialiasing_mode, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE); actions.render_mode_values["alpha_to_coverage_and_one"] = Pair<int *, int>(&alpha_antialiasing_mode, ALPHA_ANTIALIASING_ALPHA_TO_COVERAGE_AND_TO_ONE); @@ -255,7 +256,17 @@ void SceneShaderForwardMobile::ShaderData::set_code(const String &p_code) { blend_attachment.dst_color_blend_factor = RD::BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; blend_attachment.src_alpha_blend_factor = RD::BLEND_FACTOR_ONE; blend_attachment.dst_alpha_blend_factor = RD::BLEND_FACTOR_ZERO; - } + } break; + case BLEND_MODE_PREMULT_ALPHA: { + blend_attachment.enable_blend = true; + blend_attachment.alpha_blend_op = RD::BLEND_OP_ADD; + blend_attachment.color_blend_op = RD::BLEND_OP_ADD; + blend_attachment.src_color_blend_factor = RD::BLEND_FACTOR_ONE; + blend_attachment.dst_color_blend_factor = RD::BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blend_attachment.src_alpha_blend_factor = RD::BLEND_FACTOR_ONE; + blend_attachment.dst_alpha_blend_factor = RD::BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + uses_blend_alpha = true; // Force alpha used because of blend. + } break; } RD::PipelineColorBlendState blend_state_blend; @@ -497,6 +508,7 @@ void SceneShaderForwardMobile::init(const String p_defines) { actions.renames["NORMAL_MAP_DEPTH"] = "normal_map_depth"; actions.renames["ALBEDO"] = "albedo"; actions.renames["ALPHA"] = "alpha"; + actions.renames["PREMULT_ALPHA_FACTOR"] = "premult_alpha"; actions.renames["METALLIC"] = "metallic"; actions.renames["SPECULAR"] = "specular"; actions.renames["ROUGHNESS"] = "roughness"; @@ -581,6 +593,7 @@ void SceneShaderForwardMobile::init(const String p_defines) { actions.usage_defines["ALPHA_HASH_SCALE"] = "#define ALPHA_HASH_USED\n"; actions.usage_defines["ALPHA_ANTIALIASING_EDGE"] = "#define ALPHA_ANTIALIASING_EDGE_USED\n"; actions.usage_defines["ALPHA_TEXTURE_COORDINATE"] = "@ALPHA_ANTIALIASING_EDGE"; + actions.usage_defines["PREMULT_ALPHA_FACTOR"] = "#define PREMULT_ALPHA_USED"; actions.usage_defines["SSS_STRENGTH"] = "#define ENABLE_SSS\n"; actions.usage_defines["SSS_TRANSMITTANCE_DEPTH"] = "#define ENABLE_TRANSMITTANCE\n"; diff --git a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.h b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.h index da189c6f67..833b06c1e3 100644 --- a/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.h +++ b/servers/rendering/renderer_rd/forward_mobile/scene_shader_forward_mobile.h @@ -61,6 +61,7 @@ public: BLEND_MODE_ADD, BLEND_MODE_SUB, BLEND_MODE_MUL, + BLEND_MODE_PREMULT_ALPHA, BLEND_MODE_ALPHA_TO_COVERAGE }; diff --git a/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl b/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl index cb07579c4b..65669952be 100644 --- a/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl +++ b/servers/rendering/renderer_rd/shaders/forward_clustered/scene_forward_clustered.glsl @@ -914,6 +914,9 @@ vec3 encode24(vec3 v) { void fragment_shader(in SceneData scene_data) { uint instance_index = instance_index_interp; +#ifdef PREMULT_ALPHA_USED + float premult_alpha = 1.0; +#endif // PREMULT_ALPHA_USED //lay out everything, whatever is unused is optimized away anyway vec3 vertex = vertex_interp; #ifdef USE_MULTIVIEW @@ -2458,6 +2461,10 @@ void fragment_shader(in SceneData scene_data) { motion_vector = prev_position_uv - position_uv; #endif + +#if defined(PREMULT_ALPHA_USED) && !defined(MODE_RENDER_DEPTH) + frag_color.rgb *= premult_alpha; +#endif //PREMULT_ALPHA_USED } void main() { diff --git a/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl b/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl index b98ea5a27f..b82221dd63 100644 --- a/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl +++ b/servers/rendering/renderer_rd/shaders/forward_mobile/scene_forward_mobile.glsl @@ -749,6 +749,9 @@ void main() { float clearcoat_roughness = 0.0; float anisotropy = 0.0; vec2 anisotropy_flow = vec2(1.0, 0.0); +#ifdef PREMULT_ALPHA_USED + float premult_alpha = 1.0; +#endif #ifndef FOG_DISABLED vec4 fog = vec4(0.0); #endif // !FOG_DISABLED @@ -1846,6 +1849,9 @@ void main() { // On mobile we use a UNORM buffer with 10bpp which results in a range from 0.0 - 1.0 resulting in HDR breaking // We divide by sc_luminance_multiplier to support a range from 0.0 - 2.0 both increasing precision on bright and darker images frag_color.rgb = frag_color.rgb / sc_luminance_multiplier; +#ifdef PREMULT_ALPHA_USED + frag_color.rgb *= premult_alpha; +#endif #endif //MODE_MULTIPLE_RENDER_TARGETS diff --git a/servers/rendering/rendering_device_graph.cpp b/servers/rendering/rendering_device_graph.cpp index adac7ee3eb..b04f2ebbaa 100644 --- a/servers/rendering/rendering_device_graph.cpp +++ b/servers/rendering/rendering_device_graph.cpp @@ -495,18 +495,19 @@ void RenderingDeviceGraph::_add_command_to_graph(ResourceTracker **p_resource_tr // We add this command to the adjacency list of all commands that were reading from the entire resource. int32_t read_full_command_list_index = search_tracker->read_full_command_list_index; while (read_full_command_list_index >= 0) { - const RecordedCommandListNode &command_list_node = command_list_nodes[read_full_command_list_index]; - if (command_list_node.command_index == p_command_index) { + int32_t read_full_command_index = command_list_nodes[read_full_command_list_index].command_index; + int32_t read_full_next_index = command_list_nodes[read_full_command_list_index].next_list_index; + if (read_full_command_index == p_command_index) { if (!resource_has_parent) { // Only slices are allowed to be in different usages in the same command as they are guaranteed to have no overlap in the same command. ERR_FAIL_MSG("Command can't have itself as a dependency."); } } else { // Add this command to the adjacency list of each command that was reading this resource. - _add_adjacent_command(command_list_node.command_index, p_command_index, r_command); + _add_adjacent_command(read_full_command_index, p_command_index, r_command); } - read_full_command_list_index = command_list_node.next_list_index; + read_full_command_list_index = read_full_next_index; } if (!resource_has_parent) { diff --git a/servers/rendering/rendering_server_default.h b/servers/rendering/rendering_server_default.h index 8de76c7dbc..e0049e3fa4 100644 --- a/servers/rendering/rendering_server_default.h +++ b/servers/rendering/rendering_server_default.h @@ -1048,6 +1048,10 @@ public: virtual void init() override; virtual void finish() override; + virtual bool is_on_render_thread() override { + return Thread::get_caller_id() == server_thread; + } + virtual void call_on_render_thread(const Callable &p_callable) override { if (Thread::get_caller_id() == server_thread) { command_queue.flush_if_pending(); diff --git a/servers/rendering/shader_types.cpp b/servers/rendering/shader_types.cpp index af51083dc3..a363055a99 100644 --- a/servers/rendering/shader_types.cpp +++ b/servers/rendering/shader_types.cpp @@ -124,6 +124,7 @@ ShaderTypes::ShaderTypes() { shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["UV2"] = constt(ShaderLanguage::TYPE_VEC2); shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["COLOR"] = constt(ShaderLanguage::TYPE_VEC4); shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["ALBEDO"] = ShaderLanguage::TYPE_VEC3; + shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["PREMULT_ALPHA_FACTOR"] = ShaderLanguage::TYPE_FLOAT; shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["ALPHA"] = ShaderLanguage::TYPE_FLOAT; shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["METALLIC"] = ShaderLanguage::TYPE_FLOAT; shader_modes[RS::SHADER_SPATIAL].functions["fragment"].built_ins["SPECULAR"] = ShaderLanguage::TYPE_FLOAT; @@ -208,7 +209,7 @@ ShaderTypes::ShaderTypes() { // spatial render modes { - shader_modes[RS::SHADER_SPATIAL].modes.push_back({ PNAME("blend"), "mix", "add", "sub", "mul" }); + shader_modes[RS::SHADER_SPATIAL].modes.push_back({ PNAME("blend"), "mix", "add", "sub", "mul", "premult_alpha" }); shader_modes[RS::SHADER_SPATIAL].modes.push_back({ PNAME("depth_draw"), "opaque", "always", "never" }); shader_modes[RS::SHADER_SPATIAL].modes.push_back({ PNAME("depth_prepass_alpha") }); shader_modes[RS::SHADER_SPATIAL].modes.push_back({ PNAME("depth_test_disabled") }); diff --git a/servers/rendering_server.cpp b/servers/rendering_server.cpp index bbe6b1ad0d..48b6632364 100644 --- a/servers/rendering_server.cpp +++ b/servers/rendering_server.cpp @@ -3426,6 +3426,7 @@ void RenderingServer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_rendering_device"), &RenderingServer::get_rendering_device); ClassDB::bind_method(D_METHOD("create_local_rendering_device"), &RenderingServer::create_local_rendering_device); + ClassDB::bind_method(D_METHOD("is_on_render_thread"), &RenderingServer::is_on_render_thread); ClassDB::bind_method(D_METHOD("call_on_render_thread", "callable"), &RenderingServer::call_on_render_thread); #ifndef DISABLE_DEPRECATED diff --git a/servers/rendering_server.h b/servers/rendering_server.h index 8f0150f180..240d82c90b 100644 --- a/servers/rendering_server.h +++ b/servers/rendering_server.h @@ -41,6 +41,32 @@ #include "servers/display_server.h" #include "servers/rendering/rendering_device.h" +// Helper macros for code outside of the rendering server, but that is +// called by the rendering server. +#ifdef DEBUG_ENABLED +#define ERR_ON_RENDER_THREAD \ + RenderingServer *rendering_server = RenderingServer::get_singleton(); \ + ERR_FAIL_NULL(rendering_server); \ + ERR_FAIL_COND(rendering_server->is_on_render_thread()); +#define ERR_ON_RENDER_THREAD_V(m_ret) \ + RenderingServer *rendering_server = RenderingServer::get_singleton(); \ + ERR_FAIL_NULL_V(rendering_server, m_ret); \ + ERR_FAIL_COND_V(rendering_server->is_on_render_thread(), m_ret); +#define ERR_NOT_ON_RENDER_THREAD \ + RenderingServer *rendering_server = RenderingServer::get_singleton(); \ + ERR_FAIL_NULL(rendering_server); \ + ERR_FAIL_COND(!rendering_server->is_on_render_thread()); +#define ERR_NOT_ON_RENDER_THREAD_V(m_ret) \ + RenderingServer *rendering_server = RenderingServer::get_singleton(); \ + ERR_FAIL_NULL_V(rendering_server, m_ret); \ + ERR_FAIL_COND_V(!rendering_server->is_on_render_thread(), m_ret); +#else +#define ERR_ON_RENDER_THREAD +#define ERR_ON_RENDER_THREAD_V(m_ret) +#define ERR_NOT_ON_RENDER_THREAD +#define ERR_NOT_ON_RENDER_THREAD_V(m_ret) +#endif + template <typename T> class TypedArray; @@ -1684,7 +1710,7 @@ public: #ifndef DISABLE_DEPRECATED // Never actually used, should be removed when we can break compatibility. - enum Features { + enum Features{ FEATURE_SHADERS, FEATURE_MULTITHREADED, }; @@ -1708,6 +1734,7 @@ public: bool is_render_loop_enabled() const; void set_render_loop_enabled(bool p_enabled); + virtual bool is_on_render_thread() = 0; virtual void call_on_render_thread(const Callable &p_callable) = 0; #ifdef TOOLS_ENABLED diff --git a/servers/xr/xr_interface.h b/servers/xr/xr_interface.h index d7bd212449..809800d8b9 100644 --- a/servers/xr/xr_interface.h +++ b/servers/xr/xr_interface.h @@ -122,17 +122,21 @@ public: /** rendering and internal **/ + // These methods are called from the main thread. + virtual Transform3D get_camera_transform() = 0; /* returns the position of our camera, only used for updating reference frame. For monoscopic this is equal to the views transform, for stereoscopic this should be an average */ + virtual void process() = 0; + + // These methods can be called from both main and render thread. virtual Size2 get_render_target_size() = 0; /* returns the recommended render target size per eye for this device */ virtual uint32_t get_view_count() = 0; /* returns the view count we need (1 is monoscopic, 2 is stereoscopic but can be more) */ - virtual Transform3D get_camera_transform() = 0; /* returns the position of our camera for updating our camera node. For monoscopic this is equal to the views transform, for stereoscopic this should be an average */ + + // These methods are called from the rendering thread. virtual Transform3D get_transform_for_view(uint32_t p_view, const Transform3D &p_cam_transform) = 0; /* get each views transform */ virtual Projection get_projection_for_view(uint32_t p_view, double p_aspect, double p_z_near, double p_z_far) = 0; /* get each view projection matrix */ virtual RID get_vrs_texture(); /* obtain VRS texture */ virtual RID get_color_texture(); /* obtain color output texture (if applicable) */ virtual RID get_depth_texture(); /* obtain depth output texture (if applicable, used for reprojection) */ virtual RID get_velocity_texture(); /* obtain velocity output texture (if applicable, used for spacewarp) */ - - virtual void process() = 0; virtual void pre_render(){}; virtual bool pre_draw_viewport(RID p_render_target) { return true; }; /* inform XR interface we are about to start our viewport draw process */ virtual Vector<BlitToScreen> post_draw_viewport(RID p_render_target, const Rect2 &p_screen_rect) = 0; /* inform XR interface we finished our viewport draw process */ diff --git a/servers/xr_server.cpp b/servers/xr_server.cpp index f1105a650d..2cfe98ea1e 100644 --- a/servers/xr_server.cpp +++ b/servers/xr_server.cpp @@ -51,7 +51,7 @@ XRServer *XRServer::singleton = nullptr; XRServer *XRServer::get_singleton() { return singleton; -}; +} void XRServer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_world_scale"), &XRServer::get_world_scale); @@ -59,7 +59,7 @@ void XRServer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_world_origin"), &XRServer::get_world_origin); ClassDB::bind_method(D_METHOD("set_world_origin", "world_origin"), &XRServer::set_world_origin); ClassDB::bind_method(D_METHOD("get_reference_frame"), &XRServer::get_reference_frame); - ClassDB::bind_method(D_METHOD("clear_reference_frame"), &XRServer::get_reference_frame); + ClassDB::bind_method(D_METHOD("clear_reference_frame"), &XRServer::clear_reference_frame); ClassDB::bind_method(D_METHOD("center_on_hmd", "rotation_mode", "keep_height"), &XRServer::center_on_hmd); ClassDB::bind_method(D_METHOD("get_hmd_transform"), &XRServer::get_hmd_transform); @@ -104,11 +104,20 @@ void XRServer::_bind_methods() { ADD_SIGNAL(MethodInfo("tracker_added", PropertyInfo(Variant::STRING_NAME, "tracker_name"), PropertyInfo(Variant::INT, "type"))); ADD_SIGNAL(MethodInfo("tracker_updated", PropertyInfo(Variant::STRING_NAME, "tracker_name"), PropertyInfo(Variant::INT, "type"))); ADD_SIGNAL(MethodInfo("tracker_removed", PropertyInfo(Variant::STRING_NAME, "tracker_name"), PropertyInfo(Variant::INT, "type"))); -}; +} double XRServer::get_world_scale() const { - return world_scale; -}; + RenderingServer *rendering_server = RenderingServer::get_singleton(); + + if (rendering_server && rendering_server->is_on_render_thread()) { + // Return the value with which we're currently rendering, + // if we're on the render thread + return render_state.world_scale; + } else { + // Return our current value + return world_scale; + } +} void XRServer::set_world_scale(double p_world_scale) { if (p_world_scale < 0.01) { @@ -118,19 +127,58 @@ void XRServer::set_world_scale(double p_world_scale) { } world_scale = p_world_scale; -}; + set_render_world_scale(world_scale); +} + +void XRServer::_set_render_world_scale(double p_world_scale) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; + + XRServer *xr_server = XRServer::get_singleton(); + ERR_FAIL_NULL(xr_server); + xr_server->render_state.world_scale = p_world_scale; +} Transform3D XRServer::get_world_origin() const { - return world_origin; -}; + RenderingServer *rendering_server = RenderingServer::get_singleton(); + + if (rendering_server && rendering_server->is_on_render_thread()) { + // Return the value with which we're currently rendering, + // if we're on the render thread + return render_state.world_origin; + } else { + // Return our current value + return world_origin; + } +} void XRServer::set_world_origin(const Transform3D &p_world_origin) { world_origin = p_world_origin; -}; + set_render_world_origin(world_origin); +} + +void XRServer::_set_render_world_origin(const Transform3D &p_world_origin) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; + + XRServer *xr_server = XRServer::get_singleton(); + ERR_FAIL_NULL(xr_server); + xr_server->render_state.world_origin = p_world_origin; +} Transform3D XRServer::get_reference_frame() const { - return reference_frame; -}; + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL_V(rendering_server, reference_frame); + + if (rendering_server->is_on_render_thread()) { + // Return the value with which we're currently rendering, + // if we're on the render thread + return render_state.reference_frame; + } else { + // Return our current value + return reference_frame; + } +} void XRServer::center_on_hmd(RotationMode p_rotation_mode, bool p_keep_height) { if (primary_interface == nullptr) { @@ -156,27 +204,38 @@ void XRServer::center_on_hmd(RotationMode p_rotation_mode, bool p_keep_height) { } else if (p_rotation_mode == 2) { // remove our rotation, we're only interesting in centering on position new_reference_frame.basis = Basis(); - }; + } // don't negate our height if (p_keep_height) { new_reference_frame.origin.y = 0.0; - }; + } reference_frame = new_reference_frame.inverse(); -}; + set_render_reference_frame(reference_frame); +} void XRServer::clear_reference_frame() { reference_frame = Transform3D(); + set_render_reference_frame(reference_frame); +} + +void XRServer::_set_render_reference_frame(const Transform3D &p_reference_frame) { + // Must be called from rendering thread! + ERR_NOT_ON_RENDER_THREAD; + + XRServer *xr_server = XRServer::get_singleton(); + ERR_FAIL_NULL(xr_server); + xr_server->render_state.reference_frame = p_reference_frame; } Transform3D XRServer::get_hmd_transform() { Transform3D hmd_transform; if (primary_interface != nullptr) { hmd_transform = primary_interface->get_camera_transform(); - }; + } return hmd_transform; -}; +} void XRServer::add_interface(const Ref<XRInterface> &p_interface) { ERR_FAIL_COND(p_interface.is_null()); @@ -185,12 +244,12 @@ void XRServer::add_interface(const Ref<XRInterface> &p_interface) { if (interfaces[i] == p_interface) { ERR_PRINT("Interface was already added"); return; - }; - }; + } + } interfaces.push_back(p_interface); emit_signal(SNAME("interface_added"), p_interface->get_name()); -}; +} void XRServer::remove_interface(const Ref<XRInterface> &p_interface) { ERR_FAIL_COND(p_interface.is_null()); @@ -200,33 +259,33 @@ void XRServer::remove_interface(const Ref<XRInterface> &p_interface) { if (interfaces[i] == p_interface) { idx = i; break; - }; - }; + } + } ERR_FAIL_COND_MSG(idx == -1, "Interface not found."); print_verbose("XR: Removed interface \"" + p_interface->get_name() + "\""); emit_signal(SNAME("interface_removed"), p_interface->get_name()); interfaces.remove_at(idx); -}; +} int XRServer::get_interface_count() const { return interfaces.size(); -}; +} Ref<XRInterface> XRServer::get_interface(int p_index) const { ERR_FAIL_INDEX_V(p_index, interfaces.size(), nullptr); return interfaces[p_index]; -}; +} Ref<XRInterface> XRServer::find_interface(const String &p_name) const { for (int i = 0; i < interfaces.size(); i++) { if (interfaces[i]->get_name() == p_name) { return interfaces[i]; - }; - }; + } + } return Ref<XRInterface>(); -}; +} TypedArray<Dictionary> XRServer::get_interfaces() const { Array ret; @@ -238,14 +297,14 @@ TypedArray<Dictionary> XRServer::get_interfaces() const { iface_info["name"] = interfaces[i]->get_name(); ret.push_back(iface_info); - }; + } return ret; -}; +} Ref<XRInterface> XRServer::get_primary_interface() const { return primary_interface; -}; +} void XRServer::set_primary_interface(const Ref<XRInterface> &p_primary_interface) { if (p_primary_interface.is_null()) { @@ -256,7 +315,7 @@ void XRServer::set_primary_interface(const Ref<XRInterface> &p_primary_interface print_verbose("XR: Primary interface set to: " + primary_interface->get_name()); } -}; +} void XRServer::add_tracker(const Ref<XRTracker> &p_tracker) { ERR_FAIL_COND(p_tracker.is_null()); @@ -272,7 +331,7 @@ void XRServer::add_tracker(const Ref<XRTracker> &p_tracker) { trackers[tracker_name] = p_tracker; emit_signal(SNAME("tracker_added"), tracker_name, p_tracker->get_tracker_type()); } -}; +} void XRServer::remove_tracker(const Ref<XRTracker> &p_tracker) { ERR_FAIL_COND(p_tracker.is_null()); @@ -285,7 +344,7 @@ void XRServer::remove_tracker(const Ref<XRTracker> &p_tracker) { // and remove it trackers.erase(tracker_name); } -}; +} Dictionary XRServer::get_trackers(int p_tracker_types) { Dictionary res; @@ -307,7 +366,7 @@ Ref<XRTracker> XRServer::get_tracker(const StringName &p_name) const { // tracker hasn't been registered yet, which is fine, no need to spam the error log... return Ref<XRTracker>(); } -}; +} PackedStringArray XRServer::get_suggested_tracker_names() const { PackedStringArray arr; @@ -369,9 +428,9 @@ void XRServer::_process() { // ignore, not a valid reference } else if (interfaces[i]->is_initialized()) { interfaces.write[i]->process(); - }; - }; -}; + } + } +} void XRServer::pre_render() { // called from RendererViewport.draw_viewports right before we start drawing our viewports @@ -383,8 +442,8 @@ void XRServer::pre_render() { // ignore, not a valid reference } else if (interfaces[i]->is_initialized()) { interfaces.write[i]->pre_render(); - }; - }; + } + } } void XRServer::end_frame() { @@ -396,14 +455,13 @@ void XRServer::end_frame() { // ignore, not a valid reference } else if (interfaces[i]->is_initialized()) { interfaces.write[i]->end_frame(); - }; - }; + } + } } XRServer::XRServer() { singleton = this; - world_scale = 1.0; -}; +} XRServer::~XRServer() { primary_interface.unref(); @@ -412,4 +470,4 @@ XRServer::~XRServer() { trackers.clear(); singleton = nullptr; -}; +} diff --git a/servers/xr_server.h b/servers/xr_server.h index 717728171a..cd9c241bb0 100644 --- a/servers/xr_server.h +++ b/servers/xr_server.h @@ -36,6 +36,7 @@ #include "core/os/thread_safe.h" #include "core/templates/rid.h" #include "core/variant/variant.h" +#include "rendering_server.h" class XRInterface; class XRTracker; @@ -92,10 +93,46 @@ private: Ref<XRInterface> primary_interface; /* we'll identify one interface as primary, this will be used by our viewports */ - double world_scale; /* scale by which we multiply our tracker positions */ + double world_scale = 1.0; /* scale by which we multiply our tracker positions */ Transform3D world_origin; /* our world origin point, maps a location in our virtual world to the origin point in our real world tracking volume */ Transform3D reference_frame; /* our reference frame */ + // As we may be updating our main state for our next frame while we're still rendering our previous frame, + // we need to keep copies around. + struct RenderState { + double world_scale = 1.0; /* scale by which we multiply our tracker positions */ + Transform3D world_origin; /* our world origin point, maps a location in our virtual world to the origin point in our real world tracking volume */ + Transform3D reference_frame; /* our reference frame */ + } render_state; + + static void _set_render_world_scale(double p_world_scale); + static void _set_render_world_origin(const Transform3D &p_world_origin); + static void _set_render_reference_frame(const Transform3D &p_reference_frame); + + _FORCE_INLINE_ void set_render_world_scale(double p_world_scale) { + // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready... + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp_static(&XRServer::_set_render_world_scale).bind(p_world_scale)); + } + + _FORCE_INLINE_ void set_render_world_origin(const Transform3D &p_world_origin) { + // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready... + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp_static(&XRServer::_set_render_world_origin).bind(p_world_origin)); + } + + _FORCE_INLINE_ void set_render_reference_frame(const Transform3D &p_reference_frame) { + // If we're rendering on a separate thread, we may still be processing the last frame, don't communicate this till we're ready... + RenderingServer *rendering_server = RenderingServer::get_singleton(); + ERR_FAIL_NULL(rendering_server); + + rendering_server->call_on_render_thread(callable_mp_static(&XRServer::_set_render_reference_frame).bind(p_reference_frame)); + } + protected: static XRServer *singleton; diff --git a/tests/display_server_mock.h b/tests/display_server_mock.h index ee7433fcbd..fd79a46c5c 100644 --- a/tests/display_server_mock.h +++ b/tests/display_server_mock.h @@ -47,6 +47,9 @@ private: Callable event_callback; Callable input_event_callback; + String clipboard_text; + String primary_clipboard_text; + static Vector<String> get_rendering_drivers_func() { Vector<String> drivers; drivers.push_back("dummy"); @@ -97,6 +100,8 @@ public: switch (p_feature) { case FEATURE_MOUSE: case FEATURE_CURSOR_SHAPE: + case FEATURE_CLIPBOARD: + case FEATURE_CLIPBOARD_PRIMARY: return true; default: { } @@ -131,6 +136,11 @@ public: virtual Point2i mouse_get_position() const override { return mouse_position; } + virtual void clipboard_set(const String &p_text) override { clipboard_text = p_text; } + virtual String clipboard_get() const override { return clipboard_text; } + virtual void clipboard_set_primary(const String &p_text) override { primary_clipboard_text = p_text; } + virtual String clipboard_get_primary() const override { return primary_clipboard_text; } + virtual Size2i window_get_size(WindowID p_window = MAIN_WINDOW_ID) const override { return Size2i(1920, 1080); } diff --git a/tests/scene/test_code_edit.h b/tests/scene/test_code_edit.h index b0a46b8107..c02830b6df 100644 --- a/tests/scene/test_code_edit.h +++ b/tests/scene/test_code_edit.h @@ -36,6 +36,15 @@ #include "tests/test_macros.h" namespace TestCodeEdit { +static inline Array build_array() { + return Array(); +} +template <typename... Targs> +static inline Array build_array(Variant item, Targs... Fargs) { + Array a = build_array(Fargs...); + a.push_front(item); + return a; +} TEST_CASE("[SceneTree][CodeEdit] line gutters") { CodeEdit *code_edit = memnew(CodeEdit); @@ -67,10 +76,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { ERR_PRINT_ON; - Array arg1; - arg1.push_back(0); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(0)); code_edit->set_line_as_breakpoint(0, true); CHECK(code_edit->is_line_breakpointed(0)); @@ -86,10 +92,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { code_edit->clear_breakpointed_lines(); SIGNAL_CHECK_FALSE("breakpoint_toggled"); - Array arg1; - arg1.push_back(0); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(0)); code_edit->set_line_as_breakpoint(0, true); CHECK(code_edit->is_line_breakpointed(0)); @@ -101,10 +104,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { } SUBCASE("[CodeEdit] breakpoints and set text") { - Array arg1; - arg1.push_back(0); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(0)); code_edit->set_text("test\nline"); code_edit->set_line_as_breakpoint(0, true); @@ -121,7 +121,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { code_edit->clear_breakpointed_lines(); SIGNAL_DISCARD("breakpoint_toggled") - ((Array)args[0])[0] = 1; + args = build_array(build_array(1)); code_edit->set_text("test\nline"); code_edit->set_line_as_breakpoint(1, true); CHECK(code_edit->is_line_breakpointed(1)); @@ -137,10 +137,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { } SUBCASE("[CodeEdit] breakpoints and clear") { - Array arg1; - arg1.push_back(0); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(0)); code_edit->set_text("test\nline"); code_edit->set_line_as_breakpoint(0, true); @@ -157,7 +154,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { code_edit->clear_breakpointed_lines(); SIGNAL_DISCARD("breakpoint_toggled") - ((Array)args[0])[0] = 1; + args = build_array(build_array(1)); code_edit->set_text("test\nline"); code_edit->set_line_as_breakpoint(1, true); CHECK(code_edit->is_line_breakpointed(1)); @@ -173,21 +170,15 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { } SUBCASE("[CodeEdit] breakpoints and new lines no text") { - Array arg1; - arg1.push_back(0); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(0)); /* No text moves breakpoint. */ code_edit->set_line_as_breakpoint(0, true); CHECK(code_edit->is_line_breakpointed(0)); SIGNAL_CHECK("breakpoint_toggled", args); - /* Normal. */ - ((Array)args[0])[0] = 0; - Array arg2; - arg2.push_back(1); - args.push_back(arg2); + // Normal. + args = build_array(build_array(0), build_array(1)); SEND_GUI_ACTION("ui_text_newline"); CHECK(code_edit->get_line_count() == 2); @@ -195,18 +186,16 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { CHECK(code_edit->is_line_breakpointed(1)); SIGNAL_CHECK("breakpoint_toggled", args); - /* Non-Breaking. */ - ((Array)args[0])[0] = 1; - ((Array)args[1])[0] = 2; + // Non-Breaking. + args = build_array(build_array(1), build_array(2)); SEND_GUI_ACTION("ui_text_newline_blank"); CHECK(code_edit->get_line_count() == 3); CHECK_FALSE(code_edit->is_line_breakpointed(1)); CHECK(code_edit->is_line_breakpointed(2)); SIGNAL_CHECK("breakpoint_toggled", args); - /* Above. */ - ((Array)args[0])[0] = 2; - ((Array)args[1])[0] = 3; + // Above. + args = build_array(build_array(2), build_array(3)); SEND_GUI_ACTION("ui_text_newline_above"); CHECK(code_edit->get_line_count() == 4); CHECK_FALSE(code_edit->is_line_breakpointed(2)); @@ -215,10 +204,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { } SUBCASE("[CodeEdit] breakpoints and new lines with text") { - Array arg1; - arg1.push_back(0); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(0)); /* Having text does not move breakpoint. */ code_edit->insert_text_at_caret("text"); @@ -241,11 +227,8 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { CHECK_FALSE(code_edit->is_line_breakpointed(1)); SIGNAL_CHECK_FALSE("breakpoint_toggled"); - /* Above does move. */ - ((Array)args[0])[0] = 0; - Array arg2; - arg2.push_back(1); - args.push_back(arg2); + // Above does move. + args = build_array(build_array(0), build_array(1)); code_edit->set_caret_line(0); SEND_GUI_ACTION("ui_text_newline_above"); @@ -256,10 +239,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { } SUBCASE("[CodeEdit] breakpoints and backspace") { - Array arg1; - arg1.push_back(1); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(1)); code_edit->set_text("\n\n"); code_edit->set_line_as_breakpoint(1, true); @@ -281,8 +261,8 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { ERR_PRINT_ON; SIGNAL_CHECK("breakpoint_toggled", args); - /* Backspace above breakpointed line moves it. */ - ((Array)args[0])[0] = 2; + // Backspace above breakpointed line moves it. + args = build_array(build_array(2)); code_edit->set_text("\n\n"); code_edit->set_line_as_breakpoint(2, true); @@ -291,9 +271,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { code_edit->set_caret_line(1); - Array arg2; - arg2.push_back(1); - args.push_back(arg2); + args = build_array(build_array(2), build_array(1)); SEND_GUI_ACTION("ui_text_backspace"); ERR_PRINT_OFF; CHECK_FALSE(code_edit->is_line_breakpointed(2)); @@ -303,10 +281,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { } SUBCASE("[CodeEdit] breakpoints and delete") { - Array arg1; - arg1.push_back(1); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(1)); code_edit->set_text("\n\n"); code_edit->set_line_as_breakpoint(1, true); @@ -329,8 +304,8 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { ERR_PRINT_ON; SIGNAL_CHECK("breakpoint_toggled", args); - /* Delete above breakpointed line moves it. */ - ((Array)args[0])[0] = 2; + // Delete above breakpointed line moves it. + args = build_array(build_array(2)); code_edit->set_text("\n\n"); code_edit->set_line_as_breakpoint(2, true); @@ -339,9 +314,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { code_edit->set_caret_line(0); - Array arg2; - arg2.push_back(1); - args.push_back(arg2); + args = build_array(build_array(2), build_array(1)); SEND_GUI_ACTION("ui_text_delete"); ERR_PRINT_OFF; CHECK_FALSE(code_edit->is_line_breakpointed(2)); @@ -351,10 +324,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { } SUBCASE("[CodeEdit] breakpoints and delete selection") { - Array arg1; - arg1.push_back(1); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(1)); code_edit->set_text("\n\n"); code_edit->set_line_as_breakpoint(1, true); @@ -367,8 +337,8 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { CHECK_FALSE(code_edit->is_line_breakpointed(0)); SIGNAL_CHECK("breakpoint_toggled", args); - /* Should handle breakpoint move when deleting selection by adding less text then removed. */ - ((Array)args[0])[0] = 9; + // Should handle breakpoint move when deleting selection by adding less text then removed. + args = build_array(build_array(9)); code_edit->set_text("\n\n\n\n\n\n\n\n\n"); code_edit->set_line_as_breakpoint(9, true); @@ -377,9 +347,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { code_edit->select(0, 0, 6, 0); - Array arg2; - arg2.push_back(4); - args.push_back(arg2); + args = build_array(build_array(9), build_array(4)); SEND_GUI_ACTION("ui_text_newline"); ERR_PRINT_OFF; CHECK_FALSE(code_edit->is_line_breakpointed(9)); @@ -387,9 +355,8 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { CHECK(code_edit->is_line_breakpointed(4)); SIGNAL_CHECK("breakpoint_toggled", args); - /* Should handle breakpoint move when deleting selection by adding more text then removed. */ - ((Array)args[0])[0] = 9; - ((Array)args[1])[0] = 14; + // Should handle breakpoint move when deleting selection by adding more text then removed. + args = build_array(build_array(9), build_array(14)); code_edit->insert_text_at_caret("\n\n\n\n\n"); MessageQueue::get_singleton()->flush(); @@ -404,10 +371,7 @@ TEST_CASE("[SceneTree][CodeEdit] line gutters") { } SUBCASE("[CodeEdit] breakpoints and undo") { - Array arg1; - arg1.push_back(1); - Array args; - args.push_back(arg1); + Array args = build_array(build_array(1)); code_edit->set_text("\n\n"); code_edit->set_line_as_breakpoint(1, true); @@ -1849,17 +1813,47 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->do_indent(); CHECK(code_edit->get_line(0) == "test\t"); - /* Indent lines does entire line and works without selection. */ + // Insert in place with multiple carets. + code_edit->set_text("test text"); + code_edit->set_caret_column(5); + code_edit->add_caret(0, 7); + code_edit->add_caret(0, 2); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "te\tst \tte\txt"); + CHECK(code_edit->get_caret_count() == 3); + CHECK(code_edit->get_caret_column(0) == 7); + CHECK(code_edit->get_caret_column(1) == 10); + CHECK(code_edit->get_caret_column(2) == 3); + code_edit->remove_secondary_carets(); + + // Indent lines does entire line and works without selection. code_edit->set_text(""); code_edit->insert_text_at_caret("test"); code_edit->indent_lines(); CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_caret_column() == 5); /* Selection does entire line. */ code_edit->set_text("test"); code_edit->select_all(); code_edit->do_indent(); CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 5); + + // Selection does entire line, right to left selection. + code_edit->set_text("test"); + code_edit->select(0, 4, 0, 0); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 5); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); /* Handles multiple lines. */ code_edit->set_text("test\ntext"); @@ -1867,6 +1861,11 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->do_indent(); CHECK(code_edit->get_line(0) == "\ttest"); CHECK(code_edit->get_line(1) == "\ttext"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 5); /* Do not indent line if last col is zero. */ code_edit->set_text("test\ntext"); @@ -1874,6 +1873,11 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->do_indent(); CHECK(code_edit->get_line(0) == "\ttest"); CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); /* Indent even if last column of first line. */ code_edit->set_text("test\ntext"); @@ -1881,15 +1885,53 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->do_indent(); CHECK(code_edit->get_line(0) == "\ttest"); CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 5); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Indent even if last column of first line, reversed. + code_edit->set_text("test\ntext"); + code_edit->select(1, 0, 0, 4); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 5); /* Check selection is adjusted. */ code_edit->set_text("test"); code_edit->select(0, 1, 0, 2); code_edit->do_indent(); - CHECK(code_edit->get_selection_from_column() == 2); - CHECK(code_edit->get_selection_to_column() == 3); CHECK(code_edit->get_line(0) == "\ttest"); - code_edit->undo(); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 3); + + // Indent once with multiple selections. + code_edit->set_text("test"); + code_edit->select(0, 1, 0, 2); + code_edit->add_caret(0, 4); + code_edit->select(0, 4, 0, 3, 1); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 0); + CHECK(code_edit->get_selection_origin_column(0) == 2); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 3); + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 0); + CHECK(code_edit->get_selection_origin_column(1) == 5); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 4); } SUBCASE("[CodeEdit] indent spaces") { @@ -1922,23 +1964,58 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->do_indent(); CHECK(code_edit->get_line(0) == "test "); - /* Indent lines does entire line and works without selection. */ + // Insert in place with multiple carets. + code_edit->set_text("test text"); + code_edit->set_caret_column(5); + code_edit->add_caret(0, 7); + code_edit->add_caret(0, 2); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == "te st te xt"); + CHECK(code_edit->get_caret_count() == 3); + CHECK(code_edit->get_caret_column(0) == 10); + CHECK(code_edit->get_caret_column(1) == 14); + CHECK(code_edit->get_caret_column(2) == 4); + code_edit->remove_secondary_carets(); + + // Indent lines does entire line and works without selection. code_edit->set_text(""); code_edit->insert_text_at_caret("test"); code_edit->indent_lines(); CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_caret_column() == 8); /* Selection does entire line. */ code_edit->set_text("test"); code_edit->select_all(); code_edit->do_indent(); CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 8); + + // Selection does entire line, right to left selection. + code_edit->set_text("test"); + code_edit->select(0, 4, 0, 0); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); /* single indent only add required spaces. */ code_edit->set_text(" test"); code_edit->select_all(); code_edit->do_indent(); CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 8); /* Handles multiple lines. */ code_edit->set_text("test\ntext"); @@ -1946,6 +2023,11 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->do_indent(); CHECK(code_edit->get_line(0) == " test"); CHECK(code_edit->get_line(1) == " text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 8); /* Do not indent line if last col is zero. */ code_edit->set_text("test\ntext"); @@ -1953,6 +2035,11 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->do_indent(); CHECK(code_edit->get_line(0) == " test"); CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); /* Indent even if last column of first line. */ code_edit->set_text("test\ntext"); @@ -1960,14 +2047,53 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->do_indent(); CHECK(code_edit->get_line(0) == " test"); CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Indent even if last column of first line, right to left selection. + code_edit->set_text("test\ntext"); + code_edit->select(1, 0, 0, 4); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 8); /* Check selection is adjusted. */ code_edit->set_text("test"); code_edit->select(0, 1, 0, 2); code_edit->do_indent(); - CHECK(code_edit->get_selection_from_column() == 5); - CHECK(code_edit->get_selection_to_column() == 6); CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 5); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 6); + + // Indent once with multiple selections. + code_edit->set_text("test"); + code_edit->select(0, 1, 0, 2); + code_edit->add_caret(0, 4); + code_edit->select(0, 4, 0, 3, 1); + code_edit->do_indent(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 0); + CHECK(code_edit->get_selection_origin_column(0) == 5); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 6); + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 0); + CHECK(code_edit->get_selection_origin_column(1) == 8); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 7); } SUBCASE("[CodeEdit] unindent tabs") { @@ -2003,11 +2129,28 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->insert_text_at_caret("\ttest"); code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_caret_column() == 4); + + // Unindent lines once with multiple carets. + code_edit->set_text("\t\ttest"); + code_edit->set_caret_column(1); + code_edit->add_caret(0, 3); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_caret_count() == 2); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 2); + code_edit->remove_secondary_carets(); /* Caret on col zero unindent line. */ code_edit->set_text("\t\ttest"); + code_edit->set_caret_column(0); code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_caret_column() == 0); /* Check input action. */ code_edit->set_text("\t\ttest"); @@ -2019,13 +2162,34 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->select_all(); code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 5); - /* Handles multiple lines. */ - code_edit->set_text("\ttest\n\ttext"); + // Selection does entire line, right to left selection. + code_edit->set_text("\t\ttest"); + code_edit->select(0, 6, 0, 0); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 5); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + + // Handles multiple lines. + code_edit->set_text("\t\ttest\n\t\ttext"); code_edit->select_all(); code_edit->unindent_lines(); - CHECK(code_edit->get_line(0) == "test"); - CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_line(1) == "\ttext"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 5); /* Do not unindent line if last col is zero. */ code_edit->set_text("\ttest\n\ttext"); @@ -2033,6 +2197,23 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == "test"); CHECK(code_edit->get_line(1) == "\ttext"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Do not unindent line if last col is zero, right to left selection. + code_edit->set_text("\ttest\n\ttext"); + code_edit->select(1, 0, 0, 0); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == "\ttext"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); /* Unindent even if last column of first line. */ code_edit->set_text("\ttest\n\ttext"); @@ -2040,14 +2221,50 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == "test"); CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 4); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); /* Check selection is adjusted. */ code_edit->set_text("\ttest"); code_edit->select(0, 1, 0, 2); code_edit->unindent_lines(); - CHECK(code_edit->get_selection_from_column() == 0); - CHECK(code_edit->get_selection_to_column() == 1); CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 1); + + // Deselect if only the tab was selected. + code_edit->set_text("\ttest"); + code_edit->select(0, 0, 0, 1); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "test"); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + + // Unindent once with multiple selections. + code_edit->set_text("\t\ttest"); + code_edit->select(0, 1, 0, 2); + code_edit->add_caret(0, 4); + code_edit->select(0, 4, 0, 3, 1); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 0); + CHECK(code_edit->get_selection_origin_column(0) == 0); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 1); + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 0); + CHECK(code_edit->get_selection_origin_column(1) == 3); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 2); } SUBCASE("[CodeEdit] unindent spaces") { @@ -2089,11 +2306,28 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->insert_text_at_caret(" test"); code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_caret_column() == 4); + + // Unindent lines once with multiple carets. + code_edit->set_text(" test"); + code_edit->set_caret_column(1); + code_edit->add_caret(0, 9); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_caret_count() == 2); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 5); + code_edit->remove_secondary_carets(); /* Caret on col zero unindent line. */ code_edit->set_text(" test"); + code_edit->set_caret_column(0); code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_caret_column() == 0); /* Only as far as needed */ code_edit->set_text(" test"); @@ -2110,13 +2344,34 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->select_all(); code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 8); - /* Handles multiple lines. */ - code_edit->set_text(" test\n text"); + // Selection does entire line, right to left selection. + code_edit->set_text(" test"); + code_edit->select(0, 12, 0, 0); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + + // Handles multiple lines. + code_edit->set_text(" test\n text"); code_edit->select_all(); code_edit->unindent_lines(); - CHECK(code_edit->get_line(0) == "test"); - CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_line(1) == " text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 8); /* Do not unindent line if last col is zero. */ code_edit->set_text(" test\n text"); @@ -2124,6 +2379,23 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == "test"); CHECK(code_edit->get_line(1) == " text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Do not unindent line if last col is zero, right to left selection. + code_edit->set_text(" test\n text"); + code_edit->select(1, 0, 0, 0); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->get_line(1) == " text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); /* Unindent even if last column of first line. */ code_edit->set_text(" test\n text"); @@ -2131,14 +2403,48 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->unindent_lines(); CHECK(code_edit->get_line(0) == "test"); CHECK(code_edit->get_line(1) == "text"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 1); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); /* Check selection is adjusted. */ code_edit->set_text(" test"); code_edit->select(0, 4, 0, 5); code_edit->unindent_lines(); - CHECK(code_edit->get_selection_from_column() == 0); - CHECK(code_edit->get_selection_to_column() == 1); CHECK(code_edit->get_line(0) == "test"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 1); + + // Deselect if only the tab was selected. + code_edit->set_text(" test"); + code_edit->select(0, 0, 0, 4); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == "test"); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + + // Unindent once with multiple selections. + code_edit->set_text(" test"); + code_edit->select(0, 1, 0, 2); + code_edit->add_caret(0, 4); + code_edit->select(0, 12, 0, 10, 1); + code_edit->unindent_lines(); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_caret_count() == 2); + CHECK_FALSE(code_edit->has_selection(0)); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 0); + CHECK(code_edit->get_selection_origin_column(1) == 8); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 6); } SUBCASE("[CodeEdit] auto indent") { @@ -2153,6 +2459,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline"); CHECK(code_edit->get_line(0) == "test:"); CHECK(code_edit->get_line(1) == "\t"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); /* new blank line should still indent. */ code_edit->set_text(""); @@ -2160,6 +2468,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline_blank"); CHECK(code_edit->get_line(0) == "test:"); CHECK(code_edit->get_line(1) == "\t"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); /* new line above should not indent. */ code_edit->set_text(""); @@ -2167,6 +2477,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline_above"); CHECK(code_edit->get_line(0) == ""); CHECK(code_edit->get_line(1) == "test:"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); /* Whitespace between symbol and caret is okay. */ code_edit->set_text(""); @@ -2174,6 +2486,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline"); CHECK(code_edit->get_line(0) == "test: "); CHECK(code_edit->get_line(1) == "\t"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); /* Comment between symbol and caret is okay. */ code_edit->add_comment_delimiter("#", ""); @@ -2183,6 +2497,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { CHECK(code_edit->get_line(0) == "test: # comment"); CHECK(code_edit->get_line(1) == "\t"); code_edit->remove_comment_delimiter("#"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); /* Strings between symbol and caret are not okay. */ code_edit->add_string_delimiter("#", ""); @@ -2192,6 +2508,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { CHECK(code_edit->get_line(0) == "test: # string"); CHECK(code_edit->get_line(1) == ""); code_edit->remove_string_delimiter("#"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); /* Non-whitespace prevents auto-indentation. */ code_edit->add_comment_delimiter("#", ""); @@ -2201,6 +2519,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { CHECK(code_edit->get_line(0) == "test := 0 # comment"); CHECK(code_edit->get_line(1) == ""); code_edit->remove_comment_delimiter("#"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); /* Even when there's no comments. */ code_edit->set_text(""); @@ -2208,6 +2528,53 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline"); CHECK(code_edit->get_line(0) == "test := 0"); CHECK(code_edit->get_line(1) == ""); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Preserve current indentation. + code_edit->set_text("\ttest"); + code_edit->set_caret_column(3); + SEND_GUI_ACTION("ui_text_newline"); + CHECK(code_edit->get_line(0) == "\tte"); + CHECK(code_edit->get_line(1) == "\tst"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); + + // Preserve current indentation blank. + code_edit->set_text("\ttest"); + code_edit->set_caret_column(3); + SEND_GUI_ACTION("ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "\ttest"); + CHECK(code_edit->get_line(1) == "\t"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); + + // Preserve current indentation above. + code_edit->set_text("\ttest"); + code_edit->set_caret_column(3); + SEND_GUI_ACTION("ui_text_newline_above"); + CHECK(code_edit->get_line(0) == "\t"); + CHECK(code_edit->get_line(1) == "\ttest"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 1); + + // Increase existing indentation. + code_edit->set_text("\ttest:"); + code_edit->set_caret_column(6); + SEND_GUI_ACTION("ui_text_newline"); + CHECK(code_edit->get_line(0) == "\ttest:"); + CHECK(code_edit->get_line(1) == "\t\t"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 2); + + // Increase existing indentation blank. + code_edit->set_text("\ttest:"); + code_edit->set_caret_column(3); + SEND_GUI_ACTION("ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "\ttest:"); + CHECK(code_edit->get_line(1) == "\t\t"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 2); /* If between brace pairs an extra line is added. */ code_edit->set_text(""); @@ -2217,6 +2584,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { CHECK(code_edit->get_line(0) == "test{"); CHECK(code_edit->get_line(1) == "\t"); CHECK(code_edit->get_line(2) == "}"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); /* Except when we are going above. */ code_edit->set_text(""); @@ -2225,6 +2594,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline_above"); CHECK(code_edit->get_line(0) == ""); CHECK(code_edit->get_line(1) == "test{}"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); /* or below. */ code_edit->set_text(""); @@ -2233,6 +2604,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline_blank"); CHECK(code_edit->get_line(0) == "test{}"); CHECK(code_edit->get_line(1) == ""); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); } SUBCASE("[CodeEdit] auto indent spaces") { @@ -2246,6 +2619,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline"); CHECK(code_edit->get_line(0) == "test:"); CHECK(code_edit->get_line(1) == " "); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 4); /* new blank line should still indent. */ code_edit->set_text(""); @@ -2253,6 +2628,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline_blank"); CHECK(code_edit->get_line(0) == "test:"); CHECK(code_edit->get_line(1) == " "); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 4); /* new line above should not indent. */ code_edit->set_text(""); @@ -2260,6 +2637,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline_above"); CHECK(code_edit->get_line(0) == ""); CHECK(code_edit->get_line(1) == "test:"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); /* Whitespace between symbol and caret is okay. */ code_edit->set_text(""); @@ -2267,6 +2646,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline"); CHECK(code_edit->get_line(0) == "test: "); CHECK(code_edit->get_line(1) == " "); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 4); /* Comment between symbol and caret is okay. */ code_edit->add_comment_delimiter("#", ""); @@ -2276,6 +2657,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { CHECK(code_edit->get_line(0) == "test: # comment"); CHECK(code_edit->get_line(1) == " "); code_edit->remove_comment_delimiter("#"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 4); /* Strings between symbol and caret are not okay. */ code_edit->add_string_delimiter("#", ""); @@ -2285,6 +2668,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { CHECK(code_edit->get_line(0) == "test: # string"); CHECK(code_edit->get_line(1) == ""); code_edit->remove_string_delimiter("#"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); /* Non-whitespace prevents auto-indentation. */ code_edit->add_comment_delimiter("#", ""); @@ -2294,6 +2679,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { CHECK(code_edit->get_line(0) == "test := 0 # comment"); CHECK(code_edit->get_line(1) == ""); code_edit->remove_comment_delimiter("#"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); /* Even when there's no comments. */ code_edit->set_text(""); @@ -2301,6 +2688,53 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline"); CHECK(code_edit->get_line(0) == "test := 0"); CHECK(code_edit->get_line(1) == ""); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Preserve current indentation. + code_edit->set_text(" test"); + code_edit->set_caret_column(6); + SEND_GUI_ACTION("ui_text_newline"); + CHECK(code_edit->get_line(0) == " te"); + CHECK(code_edit->get_line(1) == " st"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 4); + + // Preserve current indentation blank. + code_edit->set_text(" test"); + code_edit->set_caret_column(6); + SEND_GUI_ACTION("ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->get_line(1) == " "); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 4); + + // Preserve current indentation above. + code_edit->set_text(" test"); + code_edit->set_caret_column(6); + SEND_GUI_ACTION("ui_text_newline_above"); + CHECK(code_edit->get_line(0) == " "); + CHECK(code_edit->get_line(1) == " test"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 4); + + // Increase existing indentation. + code_edit->set_text(" test:"); + code_edit->set_caret_column(9); + SEND_GUI_ACTION("ui_text_newline"); + CHECK(code_edit->get_line(0) == " test:"); + CHECK(code_edit->get_line(1) == " "); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 8); + + // Increase existing indentation blank. + code_edit->set_text(" test:"); + code_edit->set_caret_column(9); + SEND_GUI_ACTION("ui_text_newline"); + CHECK(code_edit->get_line(0) == " test:"); + CHECK(code_edit->get_line(1) == " "); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 8); /* If between brace pairs an extra line is added. */ code_edit->set_text(""); @@ -2310,6 +2744,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { CHECK(code_edit->get_line(0) == "test{"); CHECK(code_edit->get_line(1) == " "); CHECK(code_edit->get_line(2) == "}"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 4); /* Except when we are going above. */ code_edit->set_text(""); @@ -2318,6 +2754,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline_above"); CHECK(code_edit->get_line(0) == ""); CHECK(code_edit->get_line(1) == "test{}"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); /* or below. */ code_edit->set_text(""); @@ -2326,6 +2764,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { SEND_GUI_ACTION("ui_text_newline_blank"); CHECK(code_edit->get_line(0) == "test{}"); CHECK(code_edit->get_line(1) == ""); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); /* If there is something after a colon and there is a colon in the comment it @@ -2337,6 +2777,8 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { CHECK(code_edit->get_line(0) == "test:test#:"); CHECK(code_edit->get_line(1) == ""); code_edit->remove_comment_delimiter("#"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); } } @@ -2345,64 +2787,50 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->set_indent_using_spaces(false); // Only line. - code_edit->insert_text_at_caret(" test"); - code_edit->set_caret_line(0); - code_edit->set_caret_column(8); - code_edit->select(0, 8, 0, 9); + code_edit->set_text(" test"); + code_edit->select(0, 9, 0, 8); code_edit->convert_indent(); CHECK(code_edit->get_line(0) == "\t\ttest"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 3); CHECK(code_edit->get_caret_column() == 2); - CHECK(code_edit->get_selection_from_column() == 2); - CHECK(code_edit->get_selection_to_column() == 3); // First line. - code_edit->set_text(""); - code_edit->insert_text_at_caret(" test\n"); - code_edit->set_caret_line(0); - code_edit->set_caret_column(8); + code_edit->set_text(" test\n"); code_edit->select(0, 8, 0, 9); code_edit->convert_indent(); CHECK(code_edit->get_line(0) == "\t\ttest"); - CHECK(code_edit->get_caret_column() == 2); - CHECK(code_edit->get_selection_from_column() == 2); - CHECK(code_edit->get_selection_to_column() == 3); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK(code_edit->get_caret_column() == 3); // Middle line. - code_edit->set_text(""); - code_edit->insert_text_at_caret("\n test\n"); - code_edit->set_caret_line(1); - code_edit->set_caret_column(8); + code_edit->set_text("\n test\n"); code_edit->select(1, 8, 1, 9); code_edit->convert_indent(); CHECK(code_edit->get_line(1) == "\t\ttest"); - CHECK(code_edit->get_caret_column() == 2); - CHECK(code_edit->get_selection_from_column() == 2); - CHECK(code_edit->get_selection_to_column() == 3); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK(code_edit->get_caret_column() == 3); // End line. - code_edit->set_text(""); - code_edit->insert_text_at_caret("\n test"); - code_edit->set_caret_line(1); - code_edit->set_caret_column(8); + code_edit->set_text("\n test"); code_edit->select(1, 8, 1, 9); code_edit->convert_indent(); CHECK(code_edit->get_line(1) == "\t\ttest"); - CHECK(code_edit->get_caret_column() == 2); - CHECK(code_edit->get_selection_from_column() == 2); - CHECK(code_edit->get_selection_to_column() == 3); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK(code_edit->get_caret_column() == 3); // Within provided range. - code_edit->set_text(""); - code_edit->insert_text_at_caret(" test\n test\n"); - code_edit->set_caret_line(1); - code_edit->set_caret_column(8); + code_edit->set_text(" test\n test\n"); code_edit->select(1, 8, 1, 9); code_edit->convert_indent(1, 1); CHECK(code_edit->get_line(0) == " test"); CHECK(code_edit->get_line(1) == "\t\ttest"); - CHECK(code_edit->get_caret_column() == 2); - CHECK(code_edit->get_selection_from_column() == 2); - CHECK(code_edit->get_selection_to_column() == 3); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK(code_edit->get_caret_column() == 3); } SUBCASE("[CodeEdit] convert indent to spaces") { @@ -2410,64 +2838,50 @@ TEST_CASE("[SceneTree][CodeEdit] indent") { code_edit->set_indent_using_spaces(true); // Only line. - code_edit->insert_text_at_caret("\t\ttest"); - code_edit->set_caret_line(0); - code_edit->set_caret_column(2); - code_edit->select(0, 2, 0, 3); + code_edit->set_text("\t\ttest"); + code_edit->select(0, 3, 0, 2); code_edit->convert_indent(); CHECK(code_edit->get_line(0) == " test"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 9); CHECK(code_edit->get_caret_column() == 8); - CHECK(code_edit->get_selection_from_column() == 8); - CHECK(code_edit->get_selection_to_column() == 9); // First line. - code_edit->set_text(""); - code_edit->insert_text_at_caret("\t\ttest\n"); - code_edit->set_caret_line(0); - code_edit->set_caret_column(2); + code_edit->set_text("\t\ttest\n"); code_edit->select(0, 2, 0, 3); code_edit->convert_indent(); CHECK(code_edit->get_line(0) == " test"); - CHECK(code_edit->get_caret_column() == 8); - CHECK(code_edit->get_selection_from_column() == 8); - CHECK(code_edit->get_selection_to_column() == 9); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK(code_edit->get_caret_column() == 9); // Middle line. - code_edit->set_text(""); - code_edit->insert_text_at_caret("\n\t\ttest\n"); - code_edit->set_caret_line(1); - code_edit->set_caret_column(2); + code_edit->set_text("\n\t\ttest\n"); code_edit->select(1, 2, 1, 3); code_edit->convert_indent(); CHECK(code_edit->get_line(1) == " test"); - CHECK(code_edit->get_caret_column() == 8); - CHECK(code_edit->get_selection_from_column() == 8); - CHECK(code_edit->get_selection_to_column() == 9); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK(code_edit->get_caret_column() == 9); // End line. - code_edit->set_text(""); - code_edit->insert_text_at_caret("\n\t\ttest"); - code_edit->set_caret_line(1); - code_edit->set_caret_column(2); + code_edit->set_text("\n\t\ttest"); code_edit->select(1, 2, 1, 3); code_edit->convert_indent(); CHECK(code_edit->get_line(1) == " test"); - CHECK(code_edit->get_caret_column() == 8); - CHECK(code_edit->get_selection_from_column() == 8); - CHECK(code_edit->get_selection_to_column() == 9); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK(code_edit->get_caret_column() == 9); // Within provided range. - code_edit->set_text(""); - code_edit->insert_text_at_caret("\ttest\n\t\ttest\n"); - code_edit->set_caret_line(1); - code_edit->set_caret_column(2); + code_edit->set_text("\ttest\n\t\ttest\n"); code_edit->select(1, 2, 1, 3); code_edit->convert_indent(1, 1); CHECK(code_edit->get_line(0) == "\ttest"); CHECK(code_edit->get_line(1) == " test"); - CHECK(code_edit->get_caret_column() == 8); - CHECK(code_edit->get_selection_from_column() == 8); - CHECK(code_edit->get_selection_to_column() == 9); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK(code_edit->get_caret_column() == 9); // Outside of range. ERR_PRINT_OFF; @@ -2484,6 +2898,7 @@ TEST_CASE("[SceneTree][CodeEdit] folding") { CodeEdit *code_edit = memnew(CodeEdit); SceneTree::get_singleton()->get_root()->add_child(code_edit); code_edit->grab_focus(); + code_edit->set_line_folding_enabled(true); SUBCASE("[CodeEdit] folding settings") { code_edit->set_line_folding_enabled(true); @@ -2494,8 +2909,6 @@ TEST_CASE("[SceneTree][CodeEdit] folding") { } SUBCASE("[CodeEdit] folding") { - code_edit->set_line_folding_enabled(true); - // No indent. code_edit->set_text("line1\nline2\nline3"); for (int i = 0; i < 2; i++) { @@ -2862,6 +3275,100 @@ TEST_CASE("[SceneTree][CodeEdit] folding") { CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); } + SUBCASE("[CodeEdit] folding carets") { + // Folding a line moves all carets that would be hidden. + code_edit->set_text("test\n\tline1\n\t\tline 2\n"); + code_edit->set_caret_line(1); + code_edit->set_caret_column(0); + code_edit->add_caret(1, 3); + code_edit->add_caret(2, 8); + code_edit->add_caret(2, 1); + code_edit->select(2, 0, 2, 1, 3); + + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK(code_edit->get_caret_count() == 1); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 4); + + // Undoing an action that puts the caret on a folded line unfolds it. + code_edit->set_text("test\n\tline1"); + code_edit->select(1, 1, 1, 2); + code_edit->duplicate_selection(); + CHECK(code_edit->get_text() == "test\n\tlline1"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 3); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 2); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 4); + + code_edit->undo(); + CHECK(code_edit->get_text() == "test\n\tline1"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 2); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 1); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + + // Redoing doesn't refold. + code_edit->redo(); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 3); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(1)); + } + + SUBCASE("[CodeEdit] toggle folding carets") { + code_edit->set_text("test\n\tline1\ntest2\n\tline2"); + + // Fold lines with carets on them. + code_edit->set_caret_line(0); + code_edit->set_caret_column(1); + code_edit->toggle_foldable_lines_at_carets(); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(2)); + + // Toggle fold on lines with carets. + code_edit->add_caret(2, 0); + code_edit->toggle_foldable_lines_at_carets(); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK(code_edit->is_line_folded(2)); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 1); + CHECK(code_edit->get_caret_line(1) == 2); + CHECK(code_edit->get_caret_column(1) == 0); + + // Multiple carets as part of one fold. + code_edit->unfold_all_lines(); + code_edit->remove_secondary_carets(); + code_edit->set_caret_line(0); + code_edit->set_caret_column(1); + code_edit->add_caret(0, 4); + code_edit->add_caret(1, 2); + code_edit->toggle_foldable_lines_at_carets(); + CHECK(code_edit->is_line_folded(0)); + CHECK_FALSE(code_edit->is_line_folded(2)); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 1); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 4); + } + memdelete(code_edit); } @@ -2870,7 +3377,7 @@ TEST_CASE("[SceneTree][CodeEdit] region folding") { SceneTree::get_singleton()->get_root()->add_child(code_edit); code_edit->grab_focus(); - SUBCASE("[CodeEdit] region folding") { + SUBCASE("[CodeEdit] region tags") { code_edit->set_line_folding_enabled(true); // Region tag detection. @@ -2907,16 +3414,51 @@ TEST_CASE("[SceneTree][CodeEdit] region folding") { ERR_PRINT_ON; CHECK(code_edit->get_code_region_start_tag() == "region"); CHECK(code_edit->get_code_region_end_tag() == "endregion"); + } - // Region creation with selection adds start / close region lines. + SUBCASE("[CodeEdit] create code region") { + code_edit->set_line_folding_enabled(true); + + // Region creation with selection adds start and close region lines. Region name is selected and the region is folded. code_edit->set_text("line1\nline2\nline3"); code_edit->clear_comment_delimiters(); code_edit->add_comment_delimiter("#", ""); code_edit->select(1, 0, 1, 4); code_edit->create_code_region(); CHECK(code_edit->is_line_code_region_start(1)); - CHECK(code_edit->get_line(2).contains("line2")); CHECK(code_edit->is_line_code_region_end(3)); + CHECK(code_edit->get_text() == "line1\n#region New Code Region\nline2\n#endregion\nline3"); + CHECK(code_edit->get_caret_count() == 1); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selected_text() == "New Code Region"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 23); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK(code_edit->is_line_folded(1)); + + // Undo region creation. Line get unfolded. + code_edit->undo(); + CHECK(code_edit->get_text() == "line1\nline2\nline3"); + CHECK(code_edit->get_caret_count() == 1); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 4); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK_FALSE(code_edit->is_line_folded(1)); + + // Redo region creation. + code_edit->redo(); + CHECK(code_edit->get_text() == "line1\n#region New Code Region\nline2\n#endregion\nline3"); + CHECK(code_edit->get_caret_count() == 1); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selected_text() == "New Code Region"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 23); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK_FALSE(code_edit->is_line_folded(1)); // Region creation without any selection has no effect. code_edit->set_text("line1\nline2\nline3"); @@ -2925,7 +3467,7 @@ TEST_CASE("[SceneTree][CodeEdit] region folding") { code_edit->create_code_region(); CHECK(code_edit->get_text() == "line1\nline2\nline3"); - // Region creation with multiple selections. + // Region creation with multiple selections. Secondary carets are removed and the first region name is selected. code_edit->set_text("line1\nline2\nline3"); code_edit->clear_comment_delimiters(); code_edit->add_comment_delimiter("#", ""); @@ -2934,6 +3476,25 @@ TEST_CASE("[SceneTree][CodeEdit] region folding") { code_edit->select(2, 0, 2, 5, 1); code_edit->create_code_region(); CHECK(code_edit->get_text() == "#region New Code Region\nline1\n#endregion\nline2\n#region New Code Region\nline3\n#endregion"); + CHECK(code_edit->get_caret_count() == 1); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selected_text() == "New Code Region"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 23); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 8); + + // Region creation with mixed selection and non-selection carets. Regular carets are ignored. + code_edit->set_text("line1\nline2\nline3"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->select(0, 0, 0, 4, 0); + code_edit->add_caret(2, 5); + code_edit->create_code_region(); + CHECK(code_edit->get_text() == "#region New Code Region\nline1\n#endregion\nline2\nline3"); + CHECK(code_edit->get_caret_count() == 1); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selected_text() == "New Code Region"); // Two selections on the same line create only one region. code_edit->set_text("test line1\ntest line2\ntest line3"); @@ -2960,6 +3521,10 @@ TEST_CASE("[SceneTree][CodeEdit] region folding") { code_edit->add_comment_delimiter("/*", "*/"); code_edit->create_code_region(); CHECK(code_edit->get_text() == "line1\nline2\nline3"); + } + + SUBCASE("[CodeEdit] region comment delimiters") { + code_edit->set_line_folding_enabled(true); // Choose one line comment delimiter. code_edit->set_text("//region region_name\nline2\n//endregion"); @@ -2993,6 +3558,10 @@ TEST_CASE("[SceneTree][CodeEdit] region folding") { code_edit->clear_comment_delimiters(); CHECK_FALSE(code_edit->is_line_code_region_start(0)); CHECK_FALSE(code_edit->is_line_code_region_end(2)); + } + + SUBCASE("[CodeEdit] fold region") { + code_edit->set_line_folding_enabled(true); // Fold region. code_edit->clear_comment_delimiters(); @@ -3895,10 +4464,7 @@ TEST_CASE("[SceneTree][CodeEdit] symbol lookup") { SEND_GUI_KEY_EVENT(Key::CTRL); #endif - Array signal_args; - Array arg; - arg.push_back("some"); - signal_args.push_back(arg); + Array signal_args = build_array(build_array("some")); SIGNAL_CHECK("symbol_validate", signal_args); SIGNAL_UNWATCH(code_edit, "symbol_validate"); @@ -3928,178 +4494,980 @@ TEST_CASE("[SceneTree][CodeEdit] line length guidelines") { memdelete(code_edit); } -TEST_CASE("[SceneTree][CodeEdit] Backspace delete") { +TEST_CASE("[SceneTree][CodeEdit] text manipulation") { CodeEdit *code_edit = memnew(CodeEdit); SceneTree::get_singleton()->get_root()->add_child(code_edit); code_edit->grab_focus(); - /* Backspace with selection on first line. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("test backspace"); - code_edit->select(0, 0, 0, 5); - code_edit->backspace(); - CHECK(code_edit->get_line(0) == "backspace"); - - /* Backspace with selection on first line and caret at the beginning of file. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("test backspace"); - code_edit->select(0, 0, 0, 5); - code_edit->set_caret_column(0); - code_edit->backspace(); - CHECK(code_edit->get_line(0) == "backspace"); - - /* Move caret up to the previous line on backspace if caret is at the first column. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("line 1\nline 2"); - code_edit->set_caret_line(1); - code_edit->set_caret_column(0); - code_edit->backspace(); - CHECK(code_edit->get_line(0) == "line 1line 2"); - CHECK(code_edit->get_caret_line() == 0); - CHECK(code_edit->get_caret_column() == 6); - - /* Backspace delete all text if all text is selected. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("line 1\nline 2\nline 3"); - code_edit->select_all(); - code_edit->backspace(); - CHECK(code_edit->get_text().is_empty()); - - /* Backspace at the beginning without selection has no effect. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("line 1\nline 2\nline 3"); - code_edit->set_caret_line(0); - code_edit->set_caret_column(0); - code_edit->backspace(); - CHECK(code_edit->get_text() == "line 1\nline 2\nline 3"); + SUBCASE("[SceneTree][CodeEdit] backspace") { + // Backspace with selection on first line. + code_edit->set_text("test backspace"); + code_edit->select(0, 0, 0, 5); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == "backspace"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); - memdelete(code_edit); -} + // Backspace with selection on first line and caret at the beginning of file. + code_edit->set_text("test backspace"); + code_edit->select(0, 5, 0, 0); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == "backspace"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); -TEST_CASE("[SceneTree][CodeEdit] New Line") { - CodeEdit *code_edit = memnew(CodeEdit); - SceneTree::get_singleton()->get_root()->add_child(code_edit); - code_edit->grab_focus(); + // Move caret up to the previous line on backspace if caret is at the first column. + code_edit->set_text("line 1\nline 2"); + code_edit->set_caret_line(1); + code_edit->set_caret_column(0); + code_edit->backspace(); + CHECK(code_edit->get_line(0) == "line 1line 2"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 6); - /* Add a new line. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("test new line"); - code_edit->set_caret_line(0); - code_edit->set_caret_column(13); - SEND_GUI_ACTION("ui_text_newline"); - CHECK(code_edit->get_line(0) == "test new line"); - CHECK(code_edit->get_line(1) == ""); - - /* Split line with new line. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("test new line"); - code_edit->set_caret_line(0); - code_edit->set_caret_column(5); - SEND_GUI_ACTION("ui_text_newline"); - CHECK(code_edit->get_line(0) == "test "); - CHECK(code_edit->get_line(1) == "new line"); - - /* Delete selection and split with new line. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("test new line"); - code_edit->select(0, 0, 0, 5); - SEND_GUI_ACTION("ui_text_newline"); - CHECK(code_edit->get_line(0) == ""); - CHECK(code_edit->get_line(1) == "new line"); - - /* Blank new line below with selection should not split. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("test new line"); - code_edit->select(0, 0, 0, 5); - SEND_GUI_ACTION("ui_text_newline_blank"); - CHECK(code_edit->get_line(0) == "test new line"); - CHECK(code_edit->get_line(1) == ""); - - /* Blank new line above with selection should not split. */ - code_edit->set_text(""); - code_edit->insert_text_at_caret("test new line"); - code_edit->select(0, 0, 0, 5); - SEND_GUI_ACTION("ui_text_newline_above"); - CHECK(code_edit->get_line(0) == ""); - CHECK(code_edit->get_line(1) == "test new line"); + // Multiple carets with a caret at the first column. + code_edit->set_text("line 1\nline 2"); + code_edit->set_caret_line(1); + code_edit->set_caret_column(2); + code_edit->add_caret(1, 0); + code_edit->add_caret(1, 5); + code_edit->backspace(); + CHECK(code_edit->get_text() == "line 1lne2"); + CHECK(code_edit->get_caret_count() == 3); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 7); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 6); + CHECK(code_edit->get_caret_line(2) == 0); + CHECK(code_edit->get_caret_column(2) == 9); + code_edit->remove_secondary_carets(); + + // Multiple carets close together. + code_edit->set_text("line 1\nline 2"); + code_edit->set_caret_line(1); + code_edit->set_caret_column(2); + code_edit->add_caret(1, 1); + code_edit->backspace(); + CHECK(code_edit->get_text() == "line 1\nne 2"); + CHECK(code_edit->get_caret_count() == 1); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); - memdelete(code_edit); -} + // Backspace delete all text if all text is selected. + code_edit->set_text("line 1\nline 2\nline 3"); + code_edit->select_all(); + code_edit->backspace(); + CHECK(code_edit->get_text().is_empty()); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); -TEST_CASE("[SceneTree][CodeEdit] Duplicate Lines") { - CodeEdit *code_edit = memnew(CodeEdit); - SceneTree::get_singleton()->get_root()->add_child(code_edit); - code_edit->grab_focus(); + // Backspace at the beginning without selection has no effect. + code_edit->set_text("line 1\nline 2\nline 3"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(0); + code_edit->backspace(); + CHECK(code_edit->get_text() == "line 1\nline 2\nline 3"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + } + + SUBCASE("[TextEdit] cut") { + DisplayServerMock *DS = (DisplayServerMock *)(DisplayServer::get_singleton()); + code_edit->set_line_folding_enabled(true); + + // Cut without a selection removes the entire line. + code_edit->set_text("this is\nsome\n"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(6); + + code_edit->cut(); + CHECK(DS->clipboard_get() == "this is\n"); + CHECK(code_edit->get_text() == "some\n"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 3); // In the default font, this is the same position. + + // Undo restores the cut text. + code_edit->undo(); + CHECK(DS->clipboard_get() == "this is\n"); + CHECK(code_edit->get_text() == "this is\nsome\n"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 6); + + // Redo. + code_edit->redo(); + CHECK(DS->clipboard_get() == "this is\n"); + CHECK(code_edit->get_text() == "some\n"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 3); + + // Cut unfolds the line. + code_edit->set_text("this is\n\tsome\n"); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + + code_edit->cut(); + CHECK_FALSE(code_edit->is_line_folded(0)); + CHECK(DS->clipboard_get() == "this is\n"); + CHECK(code_edit->get_text() == "\tsome\n"); + CHECK(code_edit->get_caret_line() == 0); + + // Cut with a selection removes just the selection. + code_edit->set_text("this is\nsome\n"); + code_edit->select(0, 5, 0, 7); + + SEND_GUI_ACTION("ui_cut"); + CHECK(code_edit->get_viewport()->is_input_handled()); + CHECK(DS->clipboard_get() == "is"); + CHECK(code_edit->get_text() == "this \nsome\n"); + CHECK_FALSE(code_edit->get_caret_line()); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 5); + + // Cut does not change the text if not editable. Text is still added to clipboard. + code_edit->set_text("this is\nsome\n"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(5); + + code_edit->set_editable(false); + code_edit->cut(); + code_edit->set_editable(true); + CHECK(DS->clipboard_get() == "this is\n"); + CHECK(code_edit->get_text() == "this is\nsome\n"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 5); + + // Cut line with multiple carets. + code_edit->set_text("this is\nsome\n"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(3); + code_edit->add_caret(0, 2); + code_edit->add_caret(0, 4); + code_edit->add_caret(2, 0); + + code_edit->cut(); + CHECK(DS->clipboard_get() == "this is\n\n"); + CHECK(code_edit->get_text() == "some"); + CHECK(code_edit->get_caret_count() == 3); + CHECK_FALSE(code_edit->has_selection(0)); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 2); // In the default font, this is the same position. + // The previous caret at index 1 was merged. + CHECK_FALSE(code_edit->has_selection(1)); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 3); // In the default font, this is the same position. + CHECK_FALSE(code_edit->has_selection(2)); + CHECK(code_edit->get_caret_line(2) == 0); + CHECK(code_edit->get_caret_column(2) == 4); + code_edit->remove_secondary_carets(); + + // Cut on the only line removes the contents. + code_edit->set_caret_line(0); + code_edit->set_caret_column(2); + + code_edit->cut(); + CHECK(DS->clipboard_get() == "some\n"); + CHECK(code_edit->get_text() == ""); + CHECK(code_edit->get_line_count() == 1); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + + // Cut empty line. + code_edit->cut(); + CHECK(DS->clipboard_get() == "\n"); + CHECK(code_edit->get_text() == ""); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + + // Cut multiple lines, in order. + code_edit->set_text("this is\nsome\ntext to\nbe\n\ncut"); + code_edit->set_caret_line(2); + code_edit->set_caret_column(7); + code_edit->add_caret(3, 0); + code_edit->add_caret(0, 2); + + code_edit->cut(); + CHECK(DS->clipboard_get() == "this is\ntext to\nbe\n"); + CHECK(code_edit->get_text() == "some\n\ncut"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->get_caret_line(0) == 1); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 2); + code_edit->remove_secondary_carets(); + + // Cut multiple selections, in order. Ignores regular carets. + code_edit->set_text("this is\nsome\ntext to\nbe\n\ncut"); + code_edit->add_caret(3, 0); + code_edit->add_caret(0, 2); + code_edit->add_caret(2, 0); + code_edit->select(1, 0, 1, 2, 0); + code_edit->select(3, 0, 4, 0, 1); + code_edit->select(0, 5, 0, 3, 2); + + code_edit->cut(); + CHECK(DS->clipboard_get() == "s \nso\nbe\n"); + CHECK(code_edit->get_text() == "thiis\nme\ntext to\n\ncut"); + CHECK(code_edit->get_caret_count() == 4); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line(0) == 1); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->get_caret_line(1) == 3); + CHECK(code_edit->get_caret_column(1) == 0); + CHECK(code_edit->get_caret_line(2) == 0); + CHECK(code_edit->get_caret_column(2) == 3); + CHECK(code_edit->get_caret_line(3) == 2); + CHECK(code_edit->get_caret_column(3) == 0); + } + + SUBCASE("[SceneTree][CodeEdit] new line") { + // Add a new line. + code_edit->set_text("test new line"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(13); + SEND_GUI_ACTION("ui_text_newline"); + CHECK(code_edit->get_line(0) == "test new line"); + CHECK(code_edit->get_line(1) == ""); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Split line with new line. + code_edit->set_text("test new line"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(5); + SEND_GUI_ACTION("ui_text_newline"); + CHECK(code_edit->get_line(0) == "test "); + CHECK(code_edit->get_line(1) == "new line"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Delete selection and split with new line. + code_edit->set_text("test new line"); + code_edit->select(0, 0, 0, 5); + SEND_GUI_ACTION("ui_text_newline"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == "new line"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Blank new line below with selection should not split. + code_edit->set_text("test new line"); + code_edit->select(0, 0, 0, 5); + SEND_GUI_ACTION("ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "test new line"); + CHECK(code_edit->get_line(1) == ""); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Blank new line above with selection should not split. + code_edit->set_text("test new line"); + code_edit->select(0, 0, 0, 5); + SEND_GUI_ACTION("ui_text_newline_above"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == "test new line"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + + // Multiple new lines with multiple carets. + code_edit->set_text("test new line"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(5); + code_edit->add_caret(0, 8); + SEND_GUI_ACTION("ui_text_newline"); + CHECK(code_edit->get_line(0) == "test "); + CHECK(code_edit->get_line(1) == "new"); + CHECK(code_edit->get_line(2) == " line"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->get_caret_line(0) == 1); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->get_caret_line(1) == 2); + CHECK(code_edit->get_caret_column(1) == 0); + + // Multiple blank new lines with multiple carets. + code_edit->set_text("test new line"); + code_edit->remove_secondary_carets(); + code_edit->set_caret_line(0); + code_edit->set_caret_column(5); + code_edit->add_caret(0, 8); + SEND_GUI_ACTION("ui_text_newline_blank"); + CHECK(code_edit->get_line(0) == "test new line"); + CHECK(code_edit->get_line(1) == ""); + CHECK(code_edit->get_line(2) == ""); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->get_caret_line(0) == 2); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->get_caret_line(1) == 1); + CHECK(code_edit->get_caret_column(1) == 0); + + // Multiple new lines above with multiple carets. + code_edit->set_text("test new line"); + code_edit->remove_secondary_carets(); + code_edit->set_caret_line(0); + code_edit->set_caret_column(5); + code_edit->add_caret(0, 8); + SEND_GUI_ACTION("ui_text_newline_above"); + CHECK(code_edit->get_line(0) == ""); + CHECK(code_edit->get_line(1) == ""); + CHECK(code_edit->get_line(2) == "test new line"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->get_caret_line(1) == 1); + CHECK(code_edit->get_caret_column(1) == 0); + + // See '[CodeEdit] auto indent' tests for tests about new line with indentation. + } + + SUBCASE("[SceneTree][CodeEdit] move lines up") { + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + + // Move line up with caret on it. + code_edit->set_caret_line(2); + code_edit->set_caret_column(1); + code_edit->move_lines_up(); + CHECK(code_edit->get_text() == "test\nto\nlines\n\nmove\naround"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); + + // Undo. + code_edit->undo(); + CHECK(code_edit->get_text() == "test\nlines\nto\n\nmove\naround"); + CHECK(code_edit->get_caret_line() == 2); + CHECK(code_edit->get_caret_column() == 1); + + // Redo. + code_edit->redo(); + CHECK(code_edit->get_text() == "test\nto\nlines\n\nmove\naround"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); + + // Does nothing at the first line. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(1); + code_edit->move_lines_up(); + CHECK(code_edit->get_text() == "test\nlines\nto\n\nmove\naround"); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 1); + + // Works on empty line. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->set_caret_line(3); + code_edit->set_caret_column(0); + code_edit->move_lines_up(); + CHECK(code_edit->get_text() == "test\nlines\n\nto\nmove\naround"); + CHECK(code_edit->get_caret_line() == 2); + CHECK(code_edit->get_caret_column() == 0); + + // Move multiple lines up with selection. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(4, 0, 5, 1); + code_edit->move_lines_up(); + CHECK(code_edit->get_text() == "test\nlines\nto\nmove\naround\n"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 3); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 4); + CHECK(code_edit->get_caret_column() == 1); + + // Does not affect line with selection end at column 0. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(4, 0, 5, 0); + code_edit->move_lines_up(); + CHECK(code_edit->get_text() == "test\nlines\nto\nmove\n\naround"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 3); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 4); + CHECK(code_edit->get_caret_column() == 0); + + // Move multiple lines up with selection, right to left selection. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(5, 2, 4, 1); + code_edit->move_lines_up(); + CHECK(code_edit->get_text() == "test\nlines\nto\nmove\naround\n"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 4); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK(code_edit->get_caret_line() == 3); + CHECK(code_edit->get_caret_column() == 1); + + // Move multiple lines with multiple carets. A line with multiple carets is only moved once. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(5, 2, 5, 4); + code_edit->add_caret(4, 0); + code_edit->add_caret(4, 4); + code_edit->move_lines_up(); + CHECK(code_edit->get_text() == "test\nlines\nto\nmove\naround\n"); + CHECK(code_edit->get_caret_count() == 3); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 4); + CHECK(code_edit->get_selection_origin_column(0) == 2); + CHECK(code_edit->get_caret_line(0) == 4); + CHECK(code_edit->get_caret_column(0) == 4); + CHECK_FALSE(code_edit->has_selection(1)); + CHECK(code_edit->get_caret_line(1) == 3); + CHECK(code_edit->get_caret_column(1) == 0); + CHECK_FALSE(code_edit->has_selection(2)); + CHECK(code_edit->get_caret_line(2) == 3); + CHECK(code_edit->get_caret_column(2) == 4); + + // Move multiple separate lines with multiple selections. + code_edit->remove_secondary_carets(); + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(2, 2, 1, 4); + code_edit->add_caret(5, 0); + code_edit->select(5, 0, 5, 1, 1); + code_edit->move_lines_up(); + CHECK(code_edit->get_text() == "lines\nto\ntest\n\naround\nmove"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 1); + CHECK(code_edit->get_selection_origin_column(0) == 2); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 4); + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 4); + CHECK(code_edit->get_selection_origin_column(1) == 0); + CHECK(code_edit->get_caret_line(1) == 4); + CHECK(code_edit->get_caret_column(1) == 1); + } + + SUBCASE("[SceneTree][CodeEdit] move lines down") { + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + + // Move line down with caret on it. + code_edit->set_caret_line(1); + code_edit->set_caret_column(1); + code_edit->move_lines_down(); + CHECK(code_edit->get_text() == "test\nto\nlines\n\nmove\naround"); + CHECK(code_edit->get_caret_line() == 2); + CHECK(code_edit->get_caret_column() == 1); + + // Undo. + code_edit->undo(); + CHECK(code_edit->get_text() == "test\nlines\nto\n\nmove\naround"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); + + // Redo. + code_edit->redo(); + CHECK(code_edit->get_text() == "test\nto\nlines\n\nmove\naround"); + CHECK(code_edit->get_caret_line() == 2); + CHECK(code_edit->get_caret_column() == 1); + + // Does nothing at the last line. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->set_caret_line(5); + code_edit->set_caret_column(1); + code_edit->move_lines_down(); + CHECK(code_edit->get_text() == "test\nlines\nto\n\nmove\naround"); + CHECK(code_edit->get_caret_line() == 5); + CHECK(code_edit->get_caret_column() == 1); + + // Works on empty line. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->set_caret_line(3); + code_edit->set_caret_column(0); + code_edit->move_lines_down(); + CHECK(code_edit->get_text() == "test\nlines\nto\nmove\n\naround"); + CHECK(code_edit->get_caret_line() == 4); + CHECK(code_edit->get_caret_column() == 0); + + // Move multiple lines down with selection. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(1, 0, 2, 1); + code_edit->move_lines_down(); + CHECK(code_edit->get_text() == "test\n\nlines\nto\nmove\naround"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 2); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 3); + CHECK(code_edit->get_caret_column() == 1); + + // Does not affect line with selection end at column 0. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(1, 0, 2, 0); + code_edit->move_lines_down(); + CHECK(code_edit->get_text() == "test\nto\nlines\n\nmove\naround"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 2); + CHECK(code_edit->get_selection_origin_column() == 0); + CHECK(code_edit->get_caret_line() == 3); + CHECK(code_edit->get_caret_column() == 0); + + // Move multiple lines down with selection, right to left selection. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(2, 2, 1, 1); + code_edit->move_lines_down(); + CHECK(code_edit->get_text() == "test\n\nlines\nto\nmove\naround"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 3); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK(code_edit->get_caret_line() == 2); + CHECK(code_edit->get_caret_column() == 1); + + // Move multiple lines with multiple carets. A line with multiple carets is only moved once. + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(1, 2, 1, 4); + code_edit->add_caret(0, 0); + code_edit->add_caret(0, 1); + code_edit->move_lines_down(); + CHECK(code_edit->get_text() == "to\ntest\nlines\n\nmove\naround"); + CHECK(code_edit->get_caret_count() == 3); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 2); + CHECK(code_edit->get_selection_origin_column(0) == 2); + CHECK(code_edit->get_caret_line(0) == 2); + CHECK(code_edit->get_caret_column(0) == 4); + CHECK_FALSE(code_edit->has_selection(1)); + CHECK(code_edit->get_caret_line(1) == 1); + CHECK(code_edit->get_caret_column(1) == 0); + CHECK_FALSE(code_edit->has_selection(2)); + CHECK(code_edit->get_caret_line(2) == 1); + CHECK(code_edit->get_caret_column(2) == 1); + + // Move multiple separate lines with multiple selections. + code_edit->remove_secondary_carets(); + code_edit->set_text("test\nlines\nto\n\nmove\naround"); + code_edit->select(0, 2, 1, 4); + code_edit->add_caret(4, 0); + code_edit->select(4, 0, 4, 2, 1); + code_edit->move_lines_down(); + CHECK(code_edit->get_text() == "to\ntest\nlines\n\naround\nmove"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 1); + CHECK(code_edit->get_selection_origin_column(0) == 2); + CHECK(code_edit->get_caret_line(0) == 2); + CHECK(code_edit->get_caret_column(0) == 4); + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 5); + CHECK(code_edit->get_selection_origin_column(1) == 0); + CHECK(code_edit->get_caret_line(1) == 5); + CHECK(code_edit->get_caret_column(1) == 2); + } + + SUBCASE("[SceneTree][CodeEdit] delete lines") { + code_edit->set_text("test\nlines\nto\n\ndelete"); + + // Delete line with caret on it. + code_edit->set_caret_line(1); + code_edit->set_caret_column(1); + code_edit->delete_lines(); + CHECK(code_edit->get_text() == "test\nto\n\ndelete"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); + + // Undo. + code_edit->undo(); + CHECK(code_edit->get_text() == "test\nlines\nto\n\ndelete"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); + + // Redo. + code_edit->redo(); + CHECK(code_edit->get_text() == "test\nto\n\ndelete"); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); + + // Delete empty line. + code_edit->set_caret_line(2); + code_edit->set_caret_column(0); + code_edit->delete_lines(); + CHECK(code_edit->get_text() == "test\nto\ndelete"); + CHECK(code_edit->get_caret_line() == 2); + CHECK(code_edit->get_caret_column() == 0); + + // Deletes only one line when there are multiple carets on it. Carets move down and the column gets clamped. + code_edit->set_caret_line(0); + code_edit->set_caret_column(0); + code_edit->add_caret(0, 1); + code_edit->add_caret(0, 4); + code_edit->delete_lines(); + CHECK(code_edit->get_text() == "to\ndelete"); + CHECK(code_edit->get_caret_count() == 3); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->get_caret_line(1) == 0); + CHECK(code_edit->get_caret_column(1) == 1); + CHECK(code_edit->get_caret_line(2) == 0); + CHECK(code_edit->get_caret_column(2) == 2); + + // Delete multiple lines with selection. + code_edit->remove_secondary_carets(); + code_edit->set_text("test\nlines\nto\n\ndelete"); + code_edit->select(0, 1, 2, 1); + code_edit->delete_lines(); + CHECK(code_edit->get_text() == "\ndelete"); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + + // Does not affect line with selection end at column 0. + code_edit->set_text("test\nlines\nto\n\ndelete"); + code_edit->select(0, 1, 1, 0); + code_edit->delete_lines(); + CHECK(code_edit->get_text() == "lines\nto\n\ndelete"); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + + // Delete multiple lines with multiple carets. + code_edit->set_text("test\nlines\nto\n\ndelete"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(2); + code_edit->add_caret(1, 0); + code_edit->add_caret(4, 5); + code_edit->delete_lines(); + CHECK(code_edit->get_text() == "to\n"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 0); + CHECK(code_edit->get_caret_line(1) == 1); + CHECK(code_edit->get_caret_column(1) == 0); + + // Delete multiple separate lines with multiple selections. + code_edit->remove_secondary_carets(); + code_edit->set_text("test\nlines\nto\n\ndelete"); + code_edit->add_caret(4, 5); + code_edit->select(0, 1, 1, 1); + code_edit->select(5, 5, 4, 0, 1); + code_edit->delete_lines(); + CHECK(code_edit->get_text() == "to\n"); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->get_caret_line(0) == 0); + CHECK(code_edit->get_caret_column(0) == 1); + CHECK(code_edit->get_caret_line(1) == 1); + CHECK(code_edit->get_caret_column(1) == 0); + + // Deletes contents when there is only one line. + code_edit->remove_secondary_carets(); + code_edit->set_text("test"); + code_edit->set_caret_line(0); + code_edit->set_caret_column(4); + code_edit->delete_lines(); + CHECK(code_edit->get_text() == ""); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 0); + CHECK(code_edit->get_caret_column() == 0); + } + + SUBCASE("[SceneTree][CodeEdit] duplicate selection") { + code_edit->set_text("test\nlines\nto\n\nduplicate"); + + // Duplicate selected text. + code_edit->select(0, 1, 1, 2); + code_edit->duplicate_selection(); + CHECK(code_edit->get_text() == "test\nliest\nlines\nto\n\nduplicate"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK(code_edit->get_caret_line() == 2); + CHECK(code_edit->get_caret_column() == 2); + + // Undo. + code_edit->undo(); + CHECK(code_edit->get_text() == "test\nlines\nto\n\nduplicate"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 0); + CHECK(code_edit->get_selection_origin_column() == 1); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 2); + + // Redo. + code_edit->redo(); + CHECK(code_edit->get_text() == "test\nliest\nlines\nto\n\nduplicate"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 1); + CHECK(code_edit->get_selection_origin_column() == 2); + CHECK(code_edit->get_caret_line() == 2); + CHECK(code_edit->get_caret_column() == 2); + + // Duplicate selected text, right to left selection. + code_edit->set_text("test\nlines\nto\n\nduplicate"); + code_edit->select(1, 1, 0, 2); + code_edit->duplicate_selection(); + CHECK(code_edit->get_text() == "test\nlst\nlines\nto\n\nduplicate"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 2); + CHECK(code_edit->get_selection_origin_column() == 1); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 1); + + // Duplicate line if there is no selection. + code_edit->deselect(); + code_edit->set_text("test\nlines\nto\n\nduplicate"); + code_edit->set_caret_line(1); + code_edit->set_caret_column(2); + code_edit->duplicate_selection(); + CHECK(code_edit->get_text() == "test\nlines\nlines\nto\n\nduplicate"); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line() == 2); + CHECK(code_edit->get_caret_column() == 2); + + // Duplicate multiple lines. + code_edit->deselect(); + code_edit->set_text("test\nlines\nto\n\nduplicate"); + code_edit->set_caret_line(1); + code_edit->set_caret_column(2); + code_edit->add_caret(5, 0); + code_edit->add_caret(0, 4); + code_edit->duplicate_selection(); + CHECK(code_edit->get_text() == "test\ntest\nlines\nlines\nto\n\nduplicate\nduplicate"); + CHECK(code_edit->get_caret_count() == 3); + CHECK_FALSE(code_edit->has_selection()); + CHECK(code_edit->get_caret_line(0) == 3); + CHECK(code_edit->get_caret_column(0) == 2); + CHECK(code_edit->get_caret_line(1) == 7); + CHECK(code_edit->get_caret_column(1) == 0); + CHECK(code_edit->get_caret_line(2) == 1); + CHECK(code_edit->get_caret_column(2) == 4); + + // Duplicate multiple separate selections. + code_edit->remove_secondary_carets(); + code_edit->set_text("test\nlines\nto\n\nduplicate"); + code_edit->add_caret(4, 4); + code_edit->add_caret(0, 1); + code_edit->add_caret(0, 4); + code_edit->select(2, 0, 2, 1, 0); + code_edit->select(3, 0, 4, 4, 1); + code_edit->select(0, 1, 0, 0, 2); + code_edit->select(0, 2, 0, 4, 3); + code_edit->duplicate_selection(); + CHECK(code_edit->get_text() == "ttestst\nlines\ntto\n\ndupl\nduplicate"); + CHECK(code_edit->get_caret_count() == 4); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 2); + CHECK(code_edit->get_selection_origin_column(0) == 1); + CHECK(code_edit->get_caret_line(0) == 2); + CHECK(code_edit->get_caret_column(0) == 2); + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 4); + CHECK(code_edit->get_selection_origin_column(1) == 4); + CHECK(code_edit->get_caret_line(1) == 5); + CHECK(code_edit->get_caret_column(1) == 4); + CHECK(code_edit->has_selection(2)); + CHECK(code_edit->get_selection_origin_line(2) == 0); + CHECK(code_edit->get_selection_origin_column(2) == 2); + CHECK(code_edit->get_caret_line(2) == 0); + CHECK(code_edit->get_caret_column(2) == 1); + CHECK(code_edit->has_selection(3)); + CHECK(code_edit->get_selection_origin_line(3) == 0); + CHECK(code_edit->get_selection_origin_column(3) == 5); + CHECK(code_edit->get_caret_line(3) == 0); + CHECK(code_edit->get_caret_column(3) == 7); + + // Duplicate adjacent selections. + code_edit->remove_secondary_carets(); + code_edit->set_text("test\nlines\nto\n\nduplicate"); + code_edit->add_caret(1, 2); + code_edit->select(1, 0, 1, 1, 0); + code_edit->select(1, 1, 1, 4, 1); + code_edit->duplicate_selection(); + CHECK(code_edit->get_text() == "test\nllineines\nto\n\nduplicate"); + CHECK(code_edit->get_caret_count() == 2); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 1); + CHECK(code_edit->get_selection_origin_column(0) == 1); + CHECK(code_edit->get_caret_line(0) == 1); + CHECK(code_edit->get_caret_column(0) == 2); + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 1); + CHECK(code_edit->get_selection_origin_column(1) == 5); + CHECK(code_edit->get_caret_line(1) == 1); + CHECK(code_edit->get_caret_column(1) == 8); + + // Duplicate lines then duplicate selections when there are both selections and non-selections. + code_edit->remove_secondary_carets(); + code_edit->set_text("test duplicate"); + code_edit->select(0, 14, 0, 13, 0); + code_edit->add_caret(0, 8); + code_edit->add_caret(0, 4); + code_edit->select(0, 2, 0, 4, 2); + code_edit->duplicate_selection(); + CHECK(code_edit->get_text() == "test duplicate\ntestst duplicatee"); + CHECK(code_edit->get_caret_count() == 3); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 1); + CHECK(code_edit->get_selection_origin_column(0) == 17); + CHECK(code_edit->get_caret_line(0) == 1); + CHECK(code_edit->get_caret_column(0) == 16); + CHECK_FALSE(code_edit->has_selection(1)); + CHECK(code_edit->get_caret_line(1) == 1); + CHECK(code_edit->get_caret_column(1) == 10); + CHECK(code_edit->has_selection(2)); + CHECK(code_edit->get_selection_origin_line(2) == 1); + CHECK(code_edit->get_selection_origin_column(2) == 4); + CHECK(code_edit->get_caret_line(2) == 1); + CHECK(code_edit->get_caret_column(2) == 6); + } + + SUBCASE("[SceneTree][CodeEdit] duplicate lines") { + String reset_text = R"(extends Node + +func _ready(): + var a := len(OS.get_cmdline_args()) + var b := get_child_count() + var c := a + b + for i in range(c): + print("This is the solution: ", sin(i)) + var pos = get_index() - 1 + print("Make sure this exits: %b" % pos) +)"; + + code_edit->set_text(reset_text); - code_edit->set_text(R"(extends Node + // Duplicate a single line without selection. + code_edit->set_caret_line(0); + code_edit->duplicate_lines(); + CHECK(code_edit->get_line(0) == "extends Node"); + CHECK(code_edit->get_line(1) == "extends Node"); + CHECK(code_edit->get_line(2) == ""); + CHECK(code_edit->get_caret_line() == 1); + CHECK(code_edit->get_caret_column() == 0); + + // Duplicate multiple lines with selection. + code_edit->set_text(reset_text); + code_edit->select(4, 8, 6, 15); + code_edit->duplicate_lines(); + CHECK(code_edit->get_text() == R"(extends Node + +func _ready(): + var a := len(OS.get_cmdline_args()) + var b := get_child_count() + var c := a + b + for i in range(c): + var b := get_child_count() + var c := a + b + for i in range(c): + print("This is the solution: ", sin(i)) + var pos = get_index() - 1 + print("Make sure this exits: %b" % pos) +)"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 7); + CHECK(code_edit->get_selection_origin_column() == 8); + CHECK(code_edit->get_caret_line() == 9); + CHECK(code_edit->get_caret_column() == 15); + + // Duplicate multiple lines with right to left selection. + code_edit->set_text(reset_text); + code_edit->select(6, 15, 4, 8); + code_edit->duplicate_lines(); + CHECK(code_edit->get_text() == R"(extends Node func _ready(): var a := len(OS.get_cmdline_args()) var b := get_child_count() var c := a + b for i in range(c): + var b := get_child_count() + var c := a + b + for i in range(c): print("This is the solution: ", sin(i)) var pos = get_index() - 1 print("Make sure this exits: %b" % pos) )"); + CHECK(code_edit->has_selection()); + CHECK(code_edit->get_selection_origin_line() == 9); + CHECK(code_edit->get_selection_origin_column() == 15); + CHECK(code_edit->get_caret_line() == 7); + CHECK(code_edit->get_caret_column() == 8); + + // Duplicate single lines with multiple carets. Multiple carets on a single line only duplicate once. + code_edit->remove_secondary_carets(); + code_edit->deselect(); + code_edit->set_text(reset_text); + code_edit->set_caret_line(3); + code_edit->set_caret_column(1); + code_edit->add_caret(5, 1); + code_edit->add_caret(5, 5); + code_edit->add_caret(4, 2); + code_edit->duplicate_lines(); + CHECK(code_edit->get_text() == R"(extends Node - /* Duplicate a single line without selection. */ - code_edit->set_caret_line(0); - code_edit->duplicate_lines(); - CHECK(code_edit->get_line(0) == "extends Node"); - CHECK(code_edit->get_line(1) == "extends Node"); - CHECK(code_edit->get_line(2) == ""); - - /* Duplicate multiple lines with selection. */ - code_edit->set_caret_line(6); - code_edit->set_caret_column(15); - code_edit->select(4, 8, 6, 15); - code_edit->duplicate_lines(); - CHECK(code_edit->get_line(6) == "\tvar c := a + b"); - CHECK(code_edit->get_line(7) == "\tvar a := len(OS.get_cmdline_args())"); - CHECK(code_edit->get_line(8) == "\tvar b := get_child_count()"); - CHECK(code_edit->get_line(9) == "\tvar c := a + b"); - CHECK(code_edit->get_line(10) == "\tfor i in range(c):"); - - /* Duplicate single lines with multiple carets. */ - code_edit->deselect(); - code_edit->set_caret_line(10); - code_edit->set_caret_column(1); - code_edit->add_caret(11, 2); - code_edit->add_caret(12, 1); - code_edit->duplicate_lines(); - CHECK(code_edit->get_line(9) == "\tvar c := a + b"); - CHECK(code_edit->get_line(10) == "\tfor i in range(c):"); - CHECK(code_edit->get_line(11) == "\tfor i in range(c):"); - CHECK(code_edit->get_line(12) == "\t\tprint(\"This is the solution: \", sin(i))"); - CHECK(code_edit->get_line(13) == "\t\tprint(\"This is the solution: \", sin(i))"); - CHECK(code_edit->get_line(14) == "\tvar pos = get_index() - 1"); - CHECK(code_edit->get_line(15) == "\tvar pos = get_index() - 1"); - CHECK(code_edit->get_line(16) == "\tprint(\"Make sure this exits: %b\" % pos)"); - - /* Duplicate multiple lines with multiple carets. */ - code_edit->select(0, 0, 1, 2, 0); - code_edit->select(3, 0, 4, 2, 1); - code_edit->select(16, 0, 17, 0, 2); - code_edit->set_caret_line(1, false, true, 0, 0); - code_edit->set_caret_column(2, false, 0); - code_edit->set_caret_line(4, false, true, 0, 1); - code_edit->set_caret_column(2, false, 1); - code_edit->set_caret_line(17, false, true, 0, 2); - code_edit->set_caret_column(0, false, 2); - code_edit->duplicate_lines(); - CHECK(code_edit->get_line(1) == "extends Node"); - CHECK(code_edit->get_line(2) == "extends Node"); - CHECK(code_edit->get_line(3) == "extends Node"); - CHECK(code_edit->get_line(4) == ""); - CHECK(code_edit->get_line(6) == "\tvar a := len(OS.get_cmdline_args())"); - CHECK(code_edit->get_line(7) == "func _ready():"); - CHECK(code_edit->get_line(8) == "\tvar a := len(OS.get_cmdline_args())"); - CHECK(code_edit->get_line(9) == "\tvar b := get_child_count()"); - CHECK(code_edit->get_line(20) == "\tprint(\"Make sure this exits: %b\" % pos)"); - CHECK(code_edit->get_line(21) == ""); - CHECK(code_edit->get_line(22) == "\tprint(\"Make sure this exits: %b\" % pos)"); - CHECK(code_edit->get_line(23) == ""); +func _ready(): + var a := len(OS.get_cmdline_args()) + var a := len(OS.get_cmdline_args()) + var b := get_child_count() + var b := get_child_count() + var c := a + b + var c := a + b + for i in range(c): + print("This is the solution: ", sin(i)) + var pos = get_index() - 1 + print("Make sure this exits: %b" % pos) +)"); + CHECK(code_edit->get_caret_count() == 4); + CHECK_FALSE(code_edit->has_selection(0)); + CHECK(code_edit->get_caret_line(0) == 4); + CHECK(code_edit->get_caret_column(0) == 1); + CHECK_FALSE(code_edit->has_selection(1)); + CHECK(code_edit->get_caret_line(1) == 8); + CHECK(code_edit->get_caret_column(1) == 1); + CHECK_FALSE(code_edit->has_selection(2)); + CHECK(code_edit->get_caret_line(2) == 8); + CHECK(code_edit->get_caret_column(2) == 5); + CHECK_FALSE(code_edit->has_selection(3)); + CHECK(code_edit->get_caret_line(3) == 6); + CHECK(code_edit->get_caret_column(3) == 2); + + // Duplicate multiple lines with multiple selections. + code_edit->remove_secondary_carets(); + code_edit->set_text(reset_text); + code_edit->add_caret(4, 2); + code_edit->add_caret(6, 0); + code_edit->add_caret(7, 8); + code_edit->select(0, 0, 2, 5, 0); + code_edit->select(3, 0, 4, 2, 1); + code_edit->select(7, 1, 6, 0, 2); + code_edit->select(7, 3, 7, 8, 3); + code_edit->duplicate_lines(); + CHECK(code_edit->get_text() == R"(extends Node + +func _ready(): +extends Node + +func _ready(): + var a := len(OS.get_cmdline_args()) + var b := get_child_count() + var a := len(OS.get_cmdline_args()) + var b := get_child_count() + var c := a + b + for i in range(c): + print("This is the solution: ", sin(i)) + for i in range(c): + print("This is the solution: ", sin(i)) + var pos = get_index() - 1 + print("Make sure this exits: %b" % pos) +)"); + CHECK(code_edit->get_caret_count() == 4); + CHECK(code_edit->has_selection(0)); + CHECK(code_edit->get_selection_origin_line(0) == 3); + CHECK(code_edit->get_selection_origin_column(0) == 0); + CHECK(code_edit->get_caret_line(0) == 5); + CHECK(code_edit->get_caret_column(0) == 5); + + CHECK(code_edit->has_selection(1)); + CHECK(code_edit->get_selection_origin_line(1) == 8); + CHECK(code_edit->get_selection_origin_column(1) == 0); + CHECK(code_edit->get_caret_line(1) == 9); + CHECK(code_edit->get_caret_column(1) == 2); + + CHECK(code_edit->has_selection(2)); + CHECK(code_edit->get_selection_origin_line(2) == 14); + CHECK(code_edit->get_selection_origin_column(2) == 1); + CHECK(code_edit->get_caret_line(2) == 13); + CHECK(code_edit->get_caret_column(2) == 0); + + CHECK(code_edit->has_selection(3)); + CHECK(code_edit->get_selection_origin_line(3) == 14); + CHECK(code_edit->get_selection_origin_column(3) == 3); + CHECK(code_edit->get_caret_line(3) == 14); + CHECK(code_edit->get_caret_column(3) == 8); + } memdelete(code_edit); } diff --git a/tests/scene/test_text_edit.h b/tests/scene/test_text_edit.h index 8577dd7148..246d869687 100644 --- a/tests/scene/test_text_edit.h +++ b/tests/scene/test_text_edit.h @@ -36,6 +36,23 @@ #include "tests/test_macros.h" namespace TestTextEdit { +static inline Array build_array() { + return Array(); +} +template <typename... Targs> +static inline Array build_array(Variant item, Targs... Fargs) { + Array a = build_array(Fargs...); + a.push_front(item); + return a; +} +static inline Array reverse_nested(Array array) { + Array reversed_array = array.duplicate(true); + reversed_array.reverse(); + for (int i = 0; i < reversed_array.size(); i++) { + ((Array)reversed_array[i]).reverse(); + } + return reversed_array; +} TEST_CASE("[SceneTree][TextEdit] text entry") { SceneTree::get_singleton()->get_root()->set_physics_object_picking(false); @@ -52,12 +69,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_WATCH(text_edit, "lines_edited_from"); SIGNAL_WATCH(text_edit, "caret_changed"); - Array args1; - args1.push_back(0); - args1.push_back(0); - Array lines_edited_args; - lines_edited_args.push_back(args1); - lines_edited_args.push_back(args1.duplicate()); + Array lines_edited_args = build_array(build_array(0, 0), build_array(0, 0)); SUBCASE("[TextEdit] clear and set text") { // "text_changed" should not be emitted on clear / set. @@ -119,13 +131,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_set"); - // Clear. + // Can clear even if not editable. text_edit->set_editable(false); - Array lines_edited_clear_args; - Array new_args = args1.duplicate(); - new_args[0] = 1; - lines_edited_clear_args.push_back(new_args); + Array lines_edited_clear_args = build_array(build_array(1, 0)); text_edit->clear(); MessageQueue::get_singleton()->flush(); @@ -210,6 +219,321 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK_FALSE("text_changed"); } + SUBCASE("[TextEdit] insert text") { + // insert_text is 0 indexed. + ERR_PRINT_OFF; + text_edit->insert_text("test", 1, 0); + ERR_PRINT_ON; + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == ""); + SIGNAL_CHECK_FALSE("lines_edited_from"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_set"); + + // Insert text when there is no text. + lines_edited_args = build_array(build_array(0, 0)); + + text_edit->insert_text("tes", 0, 0); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "tes"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 3); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Insert multiple lines. + lines_edited_args = build_array(build_array(0, 1)); + + text_edit->insert_text("t\ninserting text", 0, 3); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\ninserting text"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 14); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Can insert even if not editable. + lines_edited_args = build_array(build_array(1, 1)); + + text_edit->set_editable(false); + text_edit->insert_text("mid", 1, 2); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\ninmidserting text"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 17); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->set_editable(true); + + // Undo insert. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\ninserting text"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 14); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Redo insert. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\ninmidserting text"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 17); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Insert offsets carets after the edit. + text_edit->add_caret(1, 1); + text_edit->add_caret(1, 4); + text_edit->select(1, 4, 1, 6, 2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 2)); + + text_edit->insert_text("\n ", 1, 2); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\nin\n midserting text"); + CHECK(text_edit->get_caret_count() == 3); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 2); + CHECK(text_edit->get_caret_column(0) == 16); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 1); + CHECK(text_edit->has_selection(2)); + CHECK(text_edit->get_caret_line(2) == 2); + CHECK(text_edit->get_caret_column(2) == 5); + CHECK(text_edit->get_selection_origin_line(2) == 2); + CHECK(text_edit->get_selection_origin_column(2) == 3); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->remove_secondary_carets(); + text_edit->deselect(); + + // Insert text outside of selections. + text_edit->set_text("test text"); + text_edit->add_caret(0, 8); + text_edit->select(0, 1, 0, 4, 0); + text_edit->select(0, 4, 0, 8, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0)); + + text_edit->insert_text("a", 0, 4, true, false); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testa text"); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 1); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 9); + CHECK(text_edit->get_selection_origin_line(1) == 0); + CHECK(text_edit->get_selection_origin_column(1) == 5); + + // Insert text to beginning of selections. + text_edit->set_text("test text"); + text_edit->add_caret(0, 8); + text_edit->select(0, 1, 0, 4, 0); + text_edit->select(0, 4, 0, 8, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0)); + + text_edit->insert_text("a", 0, 4, false, false); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testa text"); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 1); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 9); + CHECK(text_edit->get_selection_origin_line(1) == 0); + CHECK(text_edit->get_selection_origin_column(1) == 4); + + // Insert text to end of selections. + text_edit->set_text("test text"); + text_edit->add_caret(0, 8); + text_edit->select(0, 1, 0, 4, 0); + text_edit->select(0, 4, 0, 8, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0)); + + text_edit->insert_text("a", 0, 4, true, true); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testa text"); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 1); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 9); + CHECK(text_edit->get_selection_origin_line(1) == 0); + CHECK(text_edit->get_selection_origin_column(1) == 5); + + // Insert text inside of selections. + text_edit->set_text("test text"); + text_edit->add_caret(0, 8); + text_edit->select(0, 1, 0, 4, 0); + text_edit->select(0, 4, 0, 8, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0)); + + text_edit->insert_text("a", 0, 4, false, true); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testa text"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 9); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 1); + } + + SUBCASE("[TextEdit] remove text") { + lines_edited_args = build_array(build_array(0, 0), build_array(0, 2)); + + text_edit->set_text("test\nremoveing text\nthird line"); + MessageQueue::get_singleton()->flush(); + SIGNAL_CHECK("text_set", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + + // remove_text is 0 indexed. + ERR_PRINT_OFF; + text_edit->remove_text(3, 0, 3, 4); + ERR_PRINT_ON; + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\nremoveing text\nthird line"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_set"); + + // Remove multiple lines. + text_edit->set_caret_line(2); + text_edit->set_caret_column(10); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(2, 1)); + + text_edit->remove_text(1, 9, 2, 2); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\nremoveingird line"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 17); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Can remove even if not editable. + lines_edited_args = build_array(build_array(1, 1)); + + text_edit->set_editable(false); + text_edit->remove_text(1, 5, 1, 6); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\nremovingird line"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 16); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->set_editable(true); + + // Undo remove. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\nremoveingird line"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 17); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Redo remove. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\nremovingird line"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 16); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Remove collapses carets and offsets carets after the edit. + text_edit->set_caret_line(1); + text_edit->set_caret_column(9); + text_edit->add_caret(1, 10); + text_edit->select(1, 10, 1, 13, 1); + text_edit->add_caret(1, 14); + text_edit->add_caret(1, 2); + MessageQueue::get_singleton()->flush(); + SIGNAL_CHECK("caret_changed", empty_signal_args); + + text_edit->remove_text(1, 8, 1, 11); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test\nremoving line"); + // Caret 0 was merged into the selection. + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 10); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 8); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 11); + CHECK(text_edit->get_caret_line(2) == 1); + CHECK(text_edit->get_caret_column(2) == 2); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->remove_secondary_carets(); + } + SUBCASE("[TextEdit] set and get line") { // Set / Get line is 0 indexed. text_edit->set_line(1, "test"); @@ -225,6 +549,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_text() == "test"); CHECK(text_edit->get_line(0) == "test"); CHECK(text_edit->get_line(1) == ""); + CHECK(text_edit->get_line_count() == 1); SIGNAL_CHECK("lines_edited_from", lines_edited_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_set"); @@ -233,14 +558,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { // Setting to a longer line, caret and selections should be preserved. text_edit->select_all(); MessageQueue::get_singleton()->flush(); - CHECK(text_edit->has_selection()); - SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_DISCARD("caret_changed"); text_edit->set_line(0, "test text"); 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_selection_origin_column() == 0); + CHECK(text_edit->get_caret_column() == 4); SIGNAL_CHECK("lines_edited_from", lines_edited_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK_FALSE("caret_changed"); @@ -299,12 +625,84 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("text_set"); - ERR_PRINT_ON; + + // Both ends of selection are adjusted and deselects. + text_edit->set_text("test text"); + text_edit->select(0, 8, 0, 6); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("caret_changed"); + + text_edit->set_line(0, "test"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_line(0) == "test"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_column() == 4); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Multiple carets adjust to keep visual position. + text_edit->set_text("test text"); + text_edit->set_caret_column(2); + text_edit->add_caret(0, 0); + text_edit->add_caret(0, 1); + text_edit->add_caret(0, 6); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("caret_changed"); + + text_edit->set_line(0, "\tset line"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_line(0) == "\tset line"); + CHECK(text_edit->get_caret_count() == 3); + CHECK_FALSE(text_edit->has_selection()); + // In the default font, these are the same positions. + CHECK(text_edit->get_caret_column(0) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + // The previous caret at index 2 was merged. + CHECK(text_edit->get_caret_column(2) == 4); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->remove_secondary_carets(); + + // Insert multiple lines. + text_edit->set_text("test text\nsecond line"); + text_edit->set_caret_column(5); + text_edit->add_caret(1, 6); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0), build_array(0, 1)); + + text_edit->set_line(0, "multiple\nlines"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "multiple\nlines\nsecond line"); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 3); // In the default font, this is the same position. + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 6); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + text_edit->remove_secondary_carets(); } SUBCASE("[TextEdit] swap lines") { - ((Array)lines_edited_args[1])[1] = 1; + lines_edited_args = build_array(build_array(0, 0), build_array(0, 1)); text_edit->set_text("testing\nswap"); MessageQueue::get_singleton()->flush(); @@ -317,15 +715,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_caret_column(text_edit->get_line(0).length()); MessageQueue::get_singleton()->flush(); SIGNAL_CHECK("caret_changed", empty_signal_args); + // Emitted twice for each line. + lines_edited_args = build_array(build_array(0, 0), build_array(0, 0), build_array(1, 1), build_array(1, 1)); - ((Array)lines_edited_args[1])[1] = 0; - Array swap_args; - swap_args.push_back(1); - swap_args.push_back(1); - lines_edited_args.push_back(swap_args); - lines_edited_args.push_back(swap_args); - - // Order does not matter. Should also work if not editable. + // Order does not matter. Works when not editable. text_edit->set_editable(false); text_edit->swap_lines(1, 0); MessageQueue::get_singleton()->flush(); @@ -336,19 +729,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK_FALSE("text_set"); text_edit->set_editable(true); - lines_edited_args.reverse(); - - // Single undo/redo action + // Single undo/redo action. text_edit->undo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "testing\nswap"); - SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_set"); - lines_edited_args.reverse(); - text_edit->redo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "swap\ntesting"); @@ -361,36 +750,70 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { ERR_PRINT_OFF; text_edit->swap_lines(-1, 0); CHECK(text_edit->get_text() == "swap\ntesting"); - SIGNAL_CHECK_FALSE("lines_edited_from"); - SIGNAL_CHECK_FALSE("caret_changed"); - SIGNAL_CHECK_FALSE("text_changed"); - SIGNAL_CHECK_FALSE("text_set"); - text_edit->swap_lines(0, -1); CHECK(text_edit->get_text() == "swap\ntesting"); - SIGNAL_CHECK_FALSE("lines_edited_from"); - SIGNAL_CHECK_FALSE("caret_changed"); - SIGNAL_CHECK_FALSE("text_changed"); - SIGNAL_CHECK_FALSE("text_set"); - text_edit->swap_lines(2, 0); CHECK(text_edit->get_text() == "swap\ntesting"); + text_edit->swap_lines(0, 2); + CHECK(text_edit->get_text() == "swap\ntesting"); + MessageQueue::get_singleton()->flush(); SIGNAL_CHECK_FALSE("lines_edited_from"); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("text_set"); + ERR_PRINT_ON; + + // Carets are also swapped. + text_edit->set_caret_line(0); + text_edit->set_caret_column(2); + text_edit->select(0, 0, 0, 2); + text_edit->add_caret(1, 6); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 1), build_array(1, 1), build_array(0, 0), build_array(0, 0)); + + text_edit->swap_lines(0, 1); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testing\nswap"); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 2); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 6); + 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("text_set"); + text_edit->remove_secondary_carets(); + + // Swap non adjacent lines. + text_edit->insert_line_at(1, "new line"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(5); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testing\nnew line\nswap"); + SIGNAL_DISCARD("caret_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("text_changed"); + lines_edited_args = build_array(build_array(2, 2), build_array(2, 2), build_array(0, 0), build_array(0, 0)); text_edit->swap_lines(0, 2); - CHECK(text_edit->get_text() == "swap\ntesting"); - SIGNAL_CHECK_FALSE("lines_edited_from"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "swap\nnew line\ntesting"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 5); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); SIGNAL_CHECK_FALSE("caret_changed"); - SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_set"); - ERR_PRINT_ON; } SUBCASE("[TextEdit] insert line at") { - ((Array)lines_edited_args[1])[1] = 1; + lines_edited_args = build_array(build_array(0, 0), build_array(0, 1)); text_edit->set_text("testing\nswap"); MessageQueue::get_singleton()->flush(); @@ -407,9 +830,9 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_selection_to_line() == 1); SIGNAL_CHECK("caret_changed", empty_signal_args); - // Insert before should move caret and selection, and works when not editable. + // Insert line at inserts a line before and moves caret and selection. Works when not editable. text_edit->set_editable(false); - lines_edited_args.remove_at(0); + lines_edited_args = build_array(build_array(0, 1)); text_edit->insert_line_at(0, "new"); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "new\ntesting\nswap"); @@ -417,7 +840,9 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_caret_column() == text_edit->get_line(2).size() - 1); CHECK(text_edit->has_selection()); CHECK(text_edit->get_selection_from_line() == 1); + CHECK(text_edit->get_selection_from_column() == 0); CHECK(text_edit->get_selection_to_line() == 2); + CHECK(text_edit->get_selection_to_column() == 4); SIGNAL_CHECK("lines_edited_from", lines_edited_args); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); @@ -425,19 +850,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_editable(true); // Can undo/redo as single action. - ((Array)lines_edited_args[0])[0] = 1; - ((Array)lines_edited_args[0])[1] = 0; text_edit->undo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "testing\nswap"); CHECK(text_edit->has_selection()); - SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_set"); - ((Array)lines_edited_args[0])[0] = 0; - ((Array)lines_edited_args[0])[1] = 1; text_edit->redo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "new\ntesting\nswap"); @@ -454,9 +875,8 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_selection_from_line() == 0); CHECK(text_edit->get_selection_to_line() == 2); SIGNAL_CHECK_FALSE("caret_changed"); + lines_edited_args = build_array(build_array(2, 3)); - ((Array)lines_edited_args[0])[0] = 2; - ((Array)lines_edited_args[0])[1] = 3; text_edit->insert_line_at(2, "after"); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "new\ntesting\nafter\nswap"); @@ -474,24 +894,222 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { ERR_PRINT_OFF; text_edit->insert_line_at(-1, "after"); CHECK(text_edit->get_text() == "new\ntesting\nafter\nswap"); + text_edit->insert_line_at(4, "after"); + CHECK(text_edit->get_text() == "new\ntesting\nafter\nswap"); + MessageQueue::get_singleton()->flush(); SIGNAL_CHECK_FALSE("lines_edited_from"); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("text_set"); + ERR_PRINT_ON; - text_edit->insert_line_at(4, "after"); - CHECK(text_edit->get_text() == "new\ntesting\nafter\nswap"); + // Can insert multiple lines. + text_edit->select(0, 1, 2, 2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(2, 4)); + + text_edit->insert_line_at(2, "multiple\nlines"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "new\ntesting\nmultiple\nlines\nafter\nswap"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 4); + CHECK(text_edit->get_caret_column() == 2); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 1); + 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("text_set"); + } + + SUBCASE("[TextEdit] remove line at") { + lines_edited_args = build_array(build_array(0, 0), build_array(0, 5)); + text_edit->set_text("testing\nremove line at\n\tremove\nlines\n\ntest"); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testing\nremove line at\n\tremove\nlines\n\ntest"); + SIGNAL_CHECK("text_set", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + + // Remove line handles multiple carets. + text_edit->set_caret_line(2); + text_edit->set_caret_column(0); + text_edit->add_caret(2, 7); + text_edit->select(2, 1, 2, 7, 1); + text_edit->add_caret(3, 1); + text_edit->add_caret(4, 5); + text_edit->add_caret(1, 5); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(3, 2)); + + text_edit->remove_line_at(2, true); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "testing\nremove line at\nlines\n\ntest"); + CHECK(text_edit->get_caret_count() == 5); + CHECK_FALSE(text_edit->has_selection(0)); // Same line. + CHECK(text_edit->get_caret_line(0) == 2); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK(text_edit->has_selection(1)); // Same line, clamped. + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 5); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 3); // In the default font, this is the same position. + CHECK_FALSE(text_edit->has_selection(2)); // Moved up. + CHECK(text_edit->get_caret_line(2) == 2); + CHECK(text_edit->get_caret_column(2) == 1); + CHECK_FALSE(text_edit->has_selection(3)); // Moved up. + CHECK(text_edit->get_caret_line(3) == 3); + CHECK(text_edit->get_caret_column(3) == 0); + CHECK_FALSE(text_edit->has_selection(4)); // Didn't move. + CHECK(text_edit->get_caret_line(4) == 1); + CHECK(text_edit->get_caret_column(4) == 5); + 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("text_set"); + text_edit->remove_secondary_carets(); + + // Remove first line. + text_edit->set_caret_line(0); + text_edit->set_caret_column(5); + text_edit->add_caret(4, 4); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 0)); + + text_edit->remove_line_at(0, false); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "remove line at\nlines\n\ntest"); + 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) == 3); + CHECK(text_edit->get_caret_column(1) == 4); + 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("text_set"); + text_edit->remove_secondary_carets(); + + // Remove empty line. + text_edit->set_caret_line(2); + text_edit->set_caret_column(0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(3, 2)); + + text_edit->remove_line_at(2, false); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "remove line at\nlines\ntest"); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 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("text_set"); + + // Remove last line. + text_edit->set_caret_line(2); + text_edit->set_caret_column(2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(2, 1)); + + text_edit->remove_line_at(2, true); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "remove line at\nlines"); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 5); + 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("text_set"); + + // Out of bounds. + text_edit->set_caret_line(0); + text_edit->set_caret_column(2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + ERR_PRINT_OFF + text_edit->remove_line_at(2, true); + ERR_PRINT_ON + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "remove line at\nlines"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 2); SIGNAL_CHECK_FALSE("lines_edited_from"); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("text_set"); - ERR_PRINT_ON; + + // Remove regular line with move caret up and not editable. + text_edit->set_editable(false); + text_edit->set_caret_line(1); + text_edit->set_caret_column(2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 0)); + + text_edit->remove_line_at(1, false); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "remove line at"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 1); // In the default font, this is the same position. + 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("text_set"); + text_edit->set_editable(true); + + // Undo. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "remove line at\nlines"); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 2); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_set"); + + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "remove line at"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 1); + 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("text_set"); + + // Remove only line removes line content. + text_edit->set_caret_line(0); + text_edit->set_caret_column(10); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0)); + + text_edit->remove_line_at(0); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_line_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 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("text_set"); } - SUBCASE("[TextEdit] insert line at caret") { - lines_edited_args.pop_back(); - ((Array)lines_edited_args[0])[1] = 1; + SUBCASE("[TextEdit] insert text at caret") { + lines_edited_args = build_array(build_array(0, 1)); + // Insert text at caret can insert multiple lines. text_edit->insert_text_at_caret("testing\nswap"); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "testing\nswap"); @@ -502,11 +1120,13 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_set"); + // Text is inserted at caret. text_edit->set_caret_line(0, false); text_edit->set_caret_column(2); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("caret_changed"); - ((Array)lines_edited_args[0])[1] = 0; + lines_edited_args = build_array(build_array(0, 0)); text_edit->insert_text_at_caret("mid"); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "temidsting\nswap"); @@ -517,9 +1137,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_set"); + // Selections are deleted then text is inserted. It also works even if not editable. text_edit->select(0, 0, 0, text_edit->get_line(0).length()); CHECK(text_edit->has_selection()); - lines_edited_args.push_back(args1.duplicate()); + lines_edited_args = build_array(build_array(0, 0), build_array(0, 0)); text_edit->set_editable(false); text_edit->insert_text_at_caret("new line"); @@ -534,12 +1155,15 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK_FALSE("text_set"); text_edit->set_editable(true); + // Undo restores text and selection. text_edit->undo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "temidsting\nswap"); CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 5); + CHECK(text_edit->get_caret_column() == text_edit->get_line(0).length()); CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 0); SIGNAL_CHECK("lines_edited_from", lines_edited_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("caret_changed", empty_signal_args); @@ -589,24 +1213,19 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_WATCH(text_edit, "lines_edited_from"); SIGNAL_WATCH(text_edit, "caret_changed"); - Array args1; - args1.push_back(0); - args1.push_back(0); - Array lines_edited_args; - lines_edited_args.push_back(args1); - lines_edited_args.push_back(args1.duplicate()); + Array lines_edited_args = build_array(build_array(0, 0), build_array(0, 0)); SUBCASE("[TextEdit] select all") { + // Select when there is no text does not select. text_edit->select_all(); CHECK_FALSE(text_edit->has_selection()); - ERR_PRINT_OFF; - CHECK(text_edit->get_selection_from_line() == -1); - CHECK(text_edit->get_selection_from_column() == -1); - CHECK(text_edit->get_selection_to_line() == -1); - CHECK(text_edit->get_selection_to_column() == -1); + CHECK(text_edit->get_selection_from_line() == 0); + CHECK(text_edit->get_selection_from_column() == 0); + CHECK(text_edit->get_selection_to_line() == 0); + CHECK(text_edit->get_selection_to_column() == 0); CHECK(text_edit->get_selected_text() == ""); - ERR_PRINT_ON; + // Select all selects all text. text_edit->set_text("test\nselection"); SEND_GUI_ACTION("ui_text_select_all"); CHECK(text_edit->get_viewport()->is_input_handled()); @@ -618,10 +1237,12 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_selection_to_line() == 1); CHECK(text_edit->get_selection_to_column() == 9); CHECK(text_edit->get_selection_mode() == TextEdit::SelectionMode::SELECTION_MODE_SHIFT); + CHECK(text_edit->is_caret_after_selection_origin()); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 9); SIGNAL_CHECK("caret_changed", empty_signal_args); + // Cannot select when disabled. text_edit->set_caret_line(0); text_edit->set_caret_column(0); text_edit->set_selecting_enabled(false); @@ -654,6 +1275,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // Select word under caret with multiple carets. text_edit->select_word_under_caret(); CHECK(text_edit->has_selection(0)); CHECK(text_edit->get_selected_text(0) == "test"); @@ -675,6 +1297,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_caret_count() == 2); + // Select word under caret disables selection if there is already a selection. text_edit->select_word_under_caret(); CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == ""); @@ -703,6 +1326,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_selected_text() == "test\ntest"); SIGNAL_CHECK("caret_changed", empty_signal_args); + // Cannot select when disabled. text_edit->set_selecting_enabled(false); text_edit->select_word_under_caret(); CHECK_FALSE(text_edit->has_selection()); @@ -714,10 +1338,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK_FALSE("caret_changed"); text_edit->set_selecting_enabled(true); - text_edit->set_caret_line(1, false, true, 0, 0); + // Select word under caret when there is no word does not select. + text_edit->set_caret_line(1, false, true, -1, 0); text_edit->set_caret_column(5, false, 0); - - text_edit->set_caret_line(2, false, true, 0, 1); + text_edit->set_caret_line(2, false, true, -1, 1); text_edit->set_caret_column(5, false, 1); text_edit->select_word_under_caret(); @@ -739,7 +1363,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_caret_column(0); text_edit->set_caret_line(1); - // First selection made by the implicit select_word_under_caret call + // First selection made by the implicit select_word_under_caret call. text_edit->add_selection_for_next_occurrence(); CHECK(text_edit->get_caret_count() == 1); CHECK(text_edit->get_selected_text(0) == "test"); @@ -780,7 +1404,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_caret_line(3) == 3); CHECK(text_edit->get_caret_column(3) == 9); - // A different word with a new manually added caret + // A different word with a new manually added caret. text_edit->add_caret(2, 1); text_edit->select(2, 0, 2, 4, 4); CHECK(text_edit->get_selected_text(4) == "rand"); @@ -795,7 +1419,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_caret_line(5) == 3); CHECK(text_edit->get_caret_column(5) == 22); - // Make sure the previous selections are still active + // Make sure the previous selections are still active. CHECK(text_edit->get_selected_text(0) == "test"); CHECK(text_edit->get_selected_text(1) == "test"); CHECK(text_edit->get_selected_text(2) == "test"); @@ -987,6 +1611,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT) CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "t"); + CHECK(text_edit->is_caret_after_selection_origin()); #ifdef MACOS_ENABLED SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT | KeyModifierMask::ALT) @@ -995,10 +1620,12 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { #endif CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "test"); + CHECK(text_edit->is_caret_after_selection_origin()); SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT) CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "tes"); + CHECK(text_edit->is_caret_after_selection_origin()); #ifdef MACOS_ENABLED SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT | KeyModifierMask::ALT) @@ -1019,11 +1646,13 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT) CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "t"); + CHECK_FALSE(text_edit->is_caret_after_selection_origin()); SEND_GUI_KEY_EVENT(Key::LEFT) CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == ""); + // Cannot select when disabled. text_edit->set_selecting_enabled(false); SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT) CHECK_FALSE(text_edit->has_selection()); @@ -1032,46 +1661,120 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { } SUBCASE("[TextEdit] mouse drag select") { - /* Set size for mouse input. */ + // Set size for mouse input. text_edit->set_size(Size2(200, 200)); text_edit->set_text("this is some text\nfor selection"); text_edit->grab_focus(); MessageQueue::get_singleton()->flush(); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 1), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); - SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_pos_at_line_column(0, 7), MouseButtonMask::LEFT, Key::NONE); + // Click and drag to make a selection. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + // Add (2,0) to bring it past the center point of the grapheme and account for integer division flooring. + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "for s"); CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER); - CHECK(text_edit->get_selection_from_line() == 1); - CHECK(text_edit->get_selection_from_column() == 0); - CHECK(text_edit->get_selection_to_line() == 1); - CHECK(text_edit->get_selection_to_column() == 5); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 0); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 5); + CHECK(text_edit->is_caret_after_selection_origin()); + CHECK(text_edit->is_dragging_cursor()); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 9), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + // Releasing finishes. + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "for s"); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 9).get_center() + Point2i(2, 0), MouseButtonMask::NONE, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "for s"); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 0); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 5); + CHECK(text_edit->is_caret_after_selection_origin()); + + // Clicking clears selection. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 7).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 7); + // Cannot select when disabled, but caret still moves. text_edit->set_selecting_enabled(false); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 1), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); - SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_pos_at_line_column(0, 7), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 5); text_edit->set_selecting_enabled(true); + + // Only last caret is moved when adding a selection. + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + text_edit->add_caret(0, 15); + text_edit->select(0, 11, 0, 15, 1); + MessageQueue::get_singleton()->flush(); + + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::ALT); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 0).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selection_origin_line(1) == 0); + CHECK(text_edit->get_selection_origin_column(1) == 11); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 15); + + CHECK(text_edit->has_selection(2)); + CHECK(text_edit->get_selected_text(2) == "for s"); + CHECK(text_edit->get_selection_origin_line(2) == 1); + CHECK(text_edit->get_selection_origin_column(2) == 5); + CHECK(text_edit->get_caret_line(2) == 1); + CHECK(text_edit->get_caret_column(2) == 0); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(2)); + + // Overlapping carets and selections merges them. + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 3).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "s is some text\nfor s"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 5); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 3); + CHECK_FALSE(text_edit->is_caret_after_selection_origin()); + + // Entering text stops selecting. + text_edit->insert_text_at_caret("a"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == "thiaelection"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 10).get_center() + Point2i(2, 0), MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); } SUBCASE("[TextEdit] mouse word select") { - /* Set size for mouse input. */ + // Set size for mouse input. text_edit->set_size(Size2(200, 200)); - text_edit->set_text("this is some text\nfor selection"); + text_edit->set_text("this is some text\nfor selection\n"); MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("caret_changed"); - SEND_GUI_DOUBLE_CLICK(text_edit->get_pos_at_line_column(0, 2), Key::NONE); + // Double click to select word. + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(1, 2).get_center() + Point2i(2, 0), Key::NONE); CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "for"); CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); @@ -1081,9 +1784,11 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_selection_to_column() == 3); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 3); + CHECK(text_edit->is_caret_after_selection_origin()); SIGNAL_CHECK("caret_changed", empty_signal_args); - SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_pos_at_line_column(0, 7), MouseButtonMask::LEFT, Key::NONE); + // Moving mouse selects entire words at a time. + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 6).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "for selection"); CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); @@ -1093,15 +1798,116 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_selection_to_column() == 13); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 13); + CHECK(text_edit->is_caret_after_selection_origin()); + CHECK(text_edit->is_dragging_cursor()); + SIGNAL_CHECK("caret_changed", empty_signal_args); + + // Moving to a word before the initial selected word reverses selection direction and keeps the initial word selected. + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 10).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "some text\nfor"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); + CHECK(text_edit->get_selection_from_line() == 0); + CHECK(text_edit->get_selection_from_column() == 8); + CHECK(text_edit->get_selection_to_line() == 1); + CHECK(text_edit->get_selection_to_column() == 3); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 8); + CHECK_FALSE(text_edit->is_caret_after_selection_origin()); SIGNAL_CHECK("caret_changed", empty_signal_args); - Point2i line_0 = text_edit->get_pos_at_line_column(0, 0); - line_0.y /= 2; - SEND_GUI_MOUSE_BUTTON_EVENT(line_0, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + // Releasing finishes. + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(0, 10).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "some text\nfor"); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 2).get_center(), MouseButtonMask::NONE, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "some text\nfor"); + text_edit->deselect(); + + // Can start word select mode on an empty line. + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(2, 0).get_center() + Point2i(2, 0), Key::NONE); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_caret_column() == 0); + + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 9).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "selection\n"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 4); + CHECK(text_edit->get_selection_origin_line() == 2); + CHECK(text_edit->get_selection_origin_column() == 0); + + // Clicking clears selection. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + + // Can start word select mode when not on a word. + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(0, 12).get_center() + Point2i(2, 0), Key::NONE); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 12); + + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 9).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == " text\nfor selection"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 13); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 12); + + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 10).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "some"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 8); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 12); + + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(0, 15).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + + // Add a new selection without affecting the old one. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::ALT); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 8).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::ALT); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "some"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 8); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 12); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "ele"); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 8); + CHECK(text_edit->get_selection_origin_line(1) == 1); + CHECK(text_edit->get_selection_origin_column(1) == 5); + + // Shift + double click to extend selection and start word select mode. + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 8).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + text_edit->remove_secondary_carets(); + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), Key::NONE | KeyModifierMask::SHIFT); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_WORD); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == " text\nfor selection"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 13); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 12); + + // Cannot select when disabled, but caret still moves to end of word. text_edit->set_selecting_enabled(false); - SEND_GUI_DOUBLE_CLICK(text_edit->get_pos_at_line_column(0, 2), Key::NONE); + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(1, 1).get_center() + Point2i(2, 0), Key::NONE); CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 3); @@ -1109,32 +1915,149 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { } SUBCASE("[TextEdit] mouse line select") { - /* Set size for mouse input. */ + // Set size for mouse input. text_edit->set_size(Size2(200, 200)); - text_edit->set_text("this is some text\nfor selection"); + text_edit->set_text("this is some text\nfor selection\nwith 3 lines"); MessageQueue::get_singleton()->flush(); - SEND_GUI_DOUBLE_CLICK(text_edit->get_pos_at_line_column(0, 2), Key::NONE); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 2), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + // Triple click to select line. + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(1, 2).get_center(), Key::NONE); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 2).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); CHECK(text_edit->has_selection()); - CHECK(text_edit->get_selected_text() == "for selection"); + CHECK(text_edit->get_selected_text() == "for selection\n"); CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE); CHECK(text_edit->get_selection_from_line() == 1); CHECK(text_edit->get_selection_from_column() == 0); + CHECK(text_edit->get_selection_to_line() == 2); + CHECK(text_edit->get_selection_to_column() == 0); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->is_caret_after_selection_origin()); + + // Moving mouse selects entire lines at a time. Selecting above reverses the selection direction. + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 10).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "this is some text\nfor selection"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE); + CHECK(text_edit->get_selection_from_line() == 0); + CHECK(text_edit->get_selection_from_column() == 0); CHECK(text_edit->get_selection_to_line() == 1); CHECK(text_edit->get_selection_to_column() == 13); - CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 0); + CHECK_FALSE(text_edit->is_caret_after_selection_origin()); + CHECK(text_edit->is_dragging_cursor()); - Point2i line_0 = text_edit->get_pos_at_line_column(0, 0); - line_0.y /= 2; - SEND_GUI_MOUSE_BUTTON_EVENT(line_0, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + // Selecting to the last line puts the caret at end of the line. + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(2, 10).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "for selection\nwith 3 lines"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE); + CHECK(text_edit->get_selection_from_line() == 1); + CHECK(text_edit->get_selection_from_column() == 0); + CHECK(text_edit->get_selection_to_line() == 2); + CHECK(text_edit->get_selection_to_column() == 12); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_caret_column() == 12); + CHECK(text_edit->is_caret_after_selection_origin()); + + // Releasing finishes. + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(2, 10).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "for selection\nwith 3 lines"); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 2).get_center(), MouseButtonMask::NONE, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "for selection\nwith 3 lines"); + + // Clicking clears selection. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(0, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + + // Can start line select mode on an empty line. + text_edit->set_text("this is some text\n\nfor selection\nwith 4 lines"); + MessageQueue::get_singleton()->flush(); + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(1, 0).get_center() + Point2i(2, 0), Key::NONE); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "\n"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 0); + + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(2, 9).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "\nfor selection\n"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE); + CHECK(text_edit->get_caret_line() == 3); + CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 0); + + // Add a new selection without affecting the old one. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 3).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::ALT); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 4).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::ALT); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "\nfor selection\n"); + CHECK(text_edit->get_caret_line(0) == 3); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 0); + + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "is"); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK(text_edit->get_selection_origin_line(1) == 0); + CHECK(text_edit->get_selection_origin_column(1) == 2); + text_edit->remove_secondary_carets(); + text_edit->deselect(); + + // Selecting the last line puts caret at the end. + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(3, 3).get_center(), Key::NONE); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(3, 3).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "with 4 lines"); + CHECK(text_edit->get_caret_line() == 3); + CHECK(text_edit->get_caret_column() == 12); + CHECK(text_edit->get_selection_origin_line() == 3); + CHECK(text_edit->get_selection_origin_column() == 0); + + // Selecting above reverses direction. + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(2, 10).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "for selection\nwith 4 lines"); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_selection_origin_line() == 3); + CHECK(text_edit->get_selection_origin_column() == 12); + + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(2, 10).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + // Shift + triple click to extend selection and restart line select mode. + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(0, 9).get_center() + Point2i(2, 0), Key::NONE | KeyModifierMask::SHIFT); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 9).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::SHIFT); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_LINE); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "this is some text\n\nfor selection\nwith 4 lines"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_selection_origin_line() == 3); + CHECK(text_edit->get_selection_origin_column() == 12); + + // Cannot select when disabled, but caret still moves to the start of the next line. text_edit->set_selecting_enabled(false); - SEND_GUI_DOUBLE_CLICK(text_edit->get_pos_at_line_column(0, 2), Key::NONE); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 2), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_DOUBLE_CLICK(text_edit->get_rect_at_line_column(0, 2).get_center(), Key::NONE); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 2).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); @@ -1142,30 +2065,47 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { } SUBCASE("[TextEdit] mouse shift click select") { - /* Set size for mouse input. */ + // Set size for mouse input. text_edit->set_size(Size2(200, 200)); text_edit->set_text("this is some text\nfor selection"); MessageQueue::get_singleton()->flush(); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 7), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::SHIFT); + // Shift click to make a selection from the previous caret position. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 1).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::SHIFT); CHECK(text_edit->has_selection()); - CHECK(text_edit->get_selected_text() == "for s"); + CHECK(text_edit->get_selected_text() == "or s"); CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER); - CHECK(text_edit->get_selection_from_line() == 1); - CHECK(text_edit->get_selection_from_column() == 0); - CHECK(text_edit->get_selection_to_line() == 1); - CHECK(text_edit->get_selection_to_column() == 5); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 1); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 5); + CHECK(text_edit->is_caret_after_selection_origin()); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 9), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + // Shift click above to switch selection direction. Uses original selection position. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 6).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::SHIFT); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "s some text\nf"); + CHECK(text_edit->get_selection_mode() == TextEdit::SELECTION_MODE_POINTER); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 1); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 6); + CHECK_FALSE(text_edit->is_caret_after_selection_origin()); + + // Clicking clears selection. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 7); + // Cannot select when disabled, but caret still moves. text_edit->set_selecting_enabled(false); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); - SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_pos_at_line_column(0, 7), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::SHIFT); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 5).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE | KeyModifierMask::SHIFT); CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 5); @@ -1175,89 +2115,166 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SUBCASE("[TextEdit] select and deselect") { text_edit->set_text("this is some text\nfor selection"); MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + // Select clamps input to full text. text_edit->select(-1, -1, 500, 500); + MessageQueue::get_singleton()->flush(); CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "this is some text\nfor selection"); + CHECK(text_edit->is_caret_after_selection_origin(0)); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 0); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 13); + CHECK(text_edit->get_selection_from_line(0) == text_edit->get_selection_origin_line(0)); + CHECK(text_edit->get_selection_from_column(0) == text_edit->get_selection_origin_column(0)); + CHECK(text_edit->get_selection_to_line(0) == text_edit->get_caret_line(0)); + CHECK(text_edit->get_selection_to_column(0) == text_edit->get_caret_column(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); text_edit->deselect(); + MessageQueue::get_singleton()->flush(); CHECK_FALSE(text_edit->has_selection()); + SIGNAL_CHECK_FALSE("caret_changed"); + // Select works in the other direction. text_edit->select(500, 500, -1, -1); + MessageQueue::get_singleton()->flush(); CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == "this is some text\nfor selection"); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(0)); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 13); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK(text_edit->get_selection_from_line(0) == text_edit->get_caret_line(0)); + CHECK(text_edit->get_selection_from_column(0) == text_edit->get_caret_column(0)); + CHECK(text_edit->get_selection_to_line(0) == text_edit->get_selection_origin_line(0)); + CHECK(text_edit->get_selection_to_column(0) == text_edit->get_selection_origin_column(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); text_edit->deselect(); + MessageQueue::get_singleton()->flush(); CHECK_FALSE(text_edit->has_selection()); + SIGNAL_CHECK_FALSE("caret_changed"); + // Select part of a line. text_edit->select(0, 4, 0, 8); + MessageQueue::get_singleton()->flush(); CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == " is "); + CHECK(text_edit->is_caret_after_selection_origin(0)); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 4); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 8); + CHECK(text_edit->get_selection_from_line(0) == text_edit->get_selection_origin_line(0)); + CHECK(text_edit->get_selection_from_column(0) == text_edit->get_selection_origin_column(0)); + CHECK(text_edit->get_selection_to_line(0) == text_edit->get_caret_line(0)); + CHECK(text_edit->get_selection_to_column(0) == text_edit->get_caret_column(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); text_edit->deselect(); + MessageQueue::get_singleton()->flush(); CHECK_FALSE(text_edit->has_selection()); + SIGNAL_CHECK_FALSE("caret_changed"); + // Select part of a line in the other direction. text_edit->select(0, 8, 0, 4); + MessageQueue::get_singleton()->flush(); CHECK(text_edit->has_selection()); CHECK(text_edit->get_selected_text() == " is "); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(0)); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 8); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_selection_from_line(0) == text_edit->get_caret_line(0)); + CHECK(text_edit->get_selection_from_column(0) == text_edit->get_caret_column(0)); + CHECK(text_edit->get_selection_to_line(0) == text_edit->get_selection_origin_line(0)); + CHECK(text_edit->get_selection_to_column(0) == text_edit->get_selection_origin_column(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + // Cannot select when disabled. text_edit->set_selecting_enabled(false); CHECK_FALSE(text_edit->has_selection()); text_edit->select(0, 8, 0, 4); + MessageQueue::get_singleton()->flush(); CHECK_FALSE(text_edit->has_selection()); + SIGNAL_CHECK_FALSE("caret_changed"); text_edit->set_selecting_enabled(true); + } - text_edit->select(0, 8, 0, 4); - CHECK(text_edit->has_selection()); - SEND_GUI_ACTION("ui_text_caret_right"); + SUBCASE("[TextEdit] delete selection") { + text_edit->set_text("this is some text\nfor selection"); + MessageQueue::get_singleton()->flush(); + + // Delete selection does nothing if there is no selection. + text_edit->set_caret_line(0); + text_edit->set_caret_column(8); CHECK_FALSE(text_edit->has_selection()); text_edit->delete_selection(); CHECK(text_edit->get_text() == "this is some text\nfor selection"); + CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 8); - text_edit->select(0, 8, 0, 4); + // Backspace removes selection. + text_edit->select(0, 4, 0, 8); CHECK(text_edit->has_selection()); SEND_GUI_ACTION("ui_text_backspace"); + 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() == 4); + // Undo restores previous selection. text_edit->undo(); - CHECK(text_edit->has_selection()); CHECK(text_edit->get_text() == "this is some text\nfor selection"); + CHECK(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 8); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 4); + // Redo restores caret. text_edit->redo(); - CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_text() == "thissome text\nfor selection"); + CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 4); text_edit->undo(); - CHECK(text_edit->has_selection()); CHECK(text_edit->get_text() == "this is some text\nfor selection"); + CHECK(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 8); - text_edit->select(0, 8, 0, 4); + text_edit->select(0, 4, 0, 8); CHECK(text_edit->has_selection()); + // Delete selection removes text, deselects, and moves caret. text_edit->delete_selection(); - CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_text() == "thissome text\nfor selection"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + // Undo delete works. text_edit->undo(); - CHECK(text_edit->has_selection()); CHECK(text_edit->get_text() == "this is some text\nfor selection"); + CHECK(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 8); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 4); + // Redo delete works. text_edit->redo(); - CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_text() == "thissome text\nfor selection"); + CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 4); @@ -1267,19 +2284,227 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 8); + // Can still delete if not editable. text_edit->set_editable(false); text_edit->delete_selection(); text_edit->set_editable(false); CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_text() == "thissome text\nfor selection"); + // Cannot undo since it was not editable. text_edit->undo(); CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_text() == "thissome text\nfor selection"); + + // Delete multiple adjacent selections on the same line. + text_edit->select(0, 0, 0, 5); + text_edit->add_caret(0, 8); + text_edit->select(0, 5, 0, 8, 1); + CHECK(text_edit->get_caret_count() == 2); + text_edit->delete_selection(); + CHECK(text_edit->get_text() == " text\nfor selection"); + CHECK(text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + + // Delete mulitline selection. Ignore non selections. + text_edit->remove_secondary_carets(); + text_edit->select(1, 3, 0, 2); + text_edit->add_caret(1, 7); + text_edit->delete_selection(); + CHECK(text_edit->get_text() == " t selection"); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 2); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 6); } - // Add readonly test? SUBCASE("[TextEdit] text drag") { + text_edit->set_size(Size2(200, 200)); + text_edit->set_text("drag test\ndrop here ''"); + text_edit->grab_click_focus(); + MessageQueue::get_singleton()->flush(); + + // Drag and drop selected text to mouse position. + text_edit->select(0, 0, 0, 4); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 0); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 11).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 11).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == " test\ndrop here 'drag'"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 15); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 11); + + // Undo. + text_edit->undo(); + CHECK(text_edit->get_text() == "drag test\ndrop here ''"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 0); + + // Redo. + text_edit->redo(); + CHECK(text_edit->get_text() == " test\ndrop here 'drag'"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 15); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 11); + + // Hold control when dropping to not delete selected text. + text_edit->select(1, 10, 1, 16); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 12).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "'drag'"); + CHECK(text_edit->has_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 0).get_center(), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_KEY_EVENT(Key::CMD_OR_CTRL); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(0, 0).get_center(), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + SEND_GUI_KEY_UP_EVENT(Key::CMD_OR_CTRL); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "'drag' test\ndrop here 'drag'"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 6); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 0); + + // Multiple caret drags entire selection. + text_edit->select(0, 11, 0, 7, 0); + text_edit->add_caret(1, 2); + text_edit->select(1, 2, 1, 4, 1); + text_edit->add_caret(1, 12); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 3).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection(true, 1)); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 12).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "test\nop"); + // Carets aren't removed from dragging, only dropping. + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 7); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 11); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK(text_edit->get_selection_origin_line(1) == 1); + CHECK(text_edit->get_selection_origin_column(1) == 2); + CHECK_FALSE(text_edit->has_selection(2)); + CHECK(text_edit->get_caret_line(2) == 1); + CHECK(text_edit->get_caret_column(2) == 12); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 9).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 9).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "'drag' \ndr heretest\nop 'drag'"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_caret_column() == 2); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 7); + + // Drop onto same selection should do effectively nothing. + text_edit->select(1, 3, 1, 7); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(1, 6).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 1).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "here"); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "'drag' \ndr heretest\nop 'drag'"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 7); + CHECK(text_edit->get_selection_origin_line() == 1); + CHECK(text_edit->get_selection_origin_column() == 3); + + // Cannot drag when drag and drop selection is disabled. It becomes regular drag to select. + text_edit->set_drag_and_drop_selection_enabled(false); + text_edit->select(0, 1, 0, 5); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK_FALSE(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "'drag' \ndr heretest\nop 'drag'"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 7); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 2); + text_edit->set_drag_and_drop_selection_enabled(true); + + // Cancel drag and drop from Escape key. + text_edit->select(0, 1, 0, 5); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 3).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 1).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag"); + SEND_GUI_KEY_EVENT(Key::ESCAPE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "'drag' \ndr heretest\nop 'drag'"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 5); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 1); + + // Cancel drag and drop from caret move key input. + text_edit->select(0, 1, 0, 5); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 3).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 1).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag"); + SEND_GUI_KEY_EVENT(Key::RIGHT); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "'drag' \ndr heretest\nop 'drag'"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 5); + + // Cancel drag and drop from text key input. + text_edit->select(0, 1, 0, 5); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 3).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 1).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag"); + SEND_GUI_KEY_EVENT(Key::A); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "'A' \ndr heretest\nop 'drag'"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 2); + } + + SUBCASE("[TextEdit] text drag to another text edit") { TextEdit *target_text_edit = memnew(TextEdit); SceneTree::get_singleton()->get_root()->add_child(target_text_edit); @@ -1292,27 +2517,223 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_text("drag me"); text_edit->select_all(); text_edit->grab_click_focus(); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 7); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 0); MessageQueue::get_singleton()->flush(); - Point2i line_0 = text_edit->get_pos_at_line_column(0, 0); - line_0.y /= 2; - SEND_GUI_MOUSE_BUTTON_EVENT(line_0, MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + // Drag text between text edits. + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 0).get_center(), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); CHECK(text_edit->is_mouse_over_selection()); - SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_pos_at_line_column(0, 7), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 7).get_center(), MouseButtonMask::LEFT, Key::NONE); CHECK(text_edit->get_viewport()->gui_is_dragging()); CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag me"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 7); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 0); - line_0 = target_text_edit->get_pos_at_line_column(0, 0); - line_0.y /= 2; - line_0.x += 401; // As empty add one. - SEND_GUI_MOUSE_MOTION_EVENT(line_0, MouseButtonMask::LEFT, Key::NONE); + Point2i target_line0 = target_text_edit->get_position() + Point2i(1, target_text_edit->get_line_height() / 2); + SEND_GUI_MOUSE_MOTION_EVENT(target_line0, MouseButtonMask::LEFT, Key::NONE); CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(target_text_edit->get_caret_line() == 0); + CHECK(target_text_edit->get_caret_column() == 0); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_line0, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == ""); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK(target_text_edit->get_text() == "drag me"); + CHECK(target_text_edit->has_selection()); + CHECK(target_text_edit->get_caret_line() == 0); + CHECK(target_text_edit->get_caret_column() == 7); + CHECK(target_text_edit->get_selection_origin_line() == 0); + CHECK(target_text_edit->get_selection_origin_column() == 0); - SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(line_0, MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + // Undo is separate per TextEdit. + text_edit->undo(); + CHECK(text_edit->get_text() == "drag me"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 7); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 0); + CHECK(target_text_edit->get_text() == "drag me"); + CHECK(target_text_edit->has_selection()); + CHECK(target_text_edit->get_caret_line() == 0); + CHECK(target_text_edit->get_caret_column() == 7); + CHECK(target_text_edit->get_selection_origin_line() == 0); + CHECK(target_text_edit->get_selection_origin_column() == 0); + + target_text_edit->undo(); + CHECK(text_edit->get_text() == "drag me"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 7); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 0); + CHECK(target_text_edit->get_text() == ""); + CHECK_FALSE(target_text_edit->has_selection()); + CHECK(target_text_edit->get_caret_line() == 0); + CHECK(target_text_edit->get_caret_column() == 0); + + // Redo is also separate. + text_edit->redo(); + CHECK(text_edit->get_text() == ""); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + CHECK(target_text_edit->get_text() == ""); + CHECK_FALSE(target_text_edit->has_selection()); + CHECK(target_text_edit->get_caret_line() == 0); + CHECK(target_text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + target_text_edit->redo(); CHECK(text_edit->get_text() == ""); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); CHECK(target_text_edit->get_text() == "drag me"); + CHECK(target_text_edit->has_selection()); + CHECK(target_text_edit->get_caret_line() == 0); + CHECK(target_text_edit->get_caret_column() == 7); + CHECK(target_text_edit->get_selection_origin_line() == 0); + CHECK(target_text_edit->get_selection_origin_column() == 0); + + // Hold control to not remove selected text. + text_edit->set_text("drag test\ndrop test"); + MessageQueue::get_singleton()->flush(); + target_text_edit->select(0, 0, 0, 3, 0); + target_text_edit->add_caret(0, 5); + text_edit->select(0, 5, 0, 7, 0); + text_edit->add_caret(0, 1); + text_edit->select(0, 1, 0, 0, 1); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 5).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection(true, 0)); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 6).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "d\nte"); + CHECK(text_edit->has_selection()); + SEND_GUI_KEY_EVENT(Key::CMD_OR_CTRL); + SEND_GUI_MOUSE_MOTION_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 6).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 6).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + SEND_GUI_KEY_UP_EVENT(Key::CMD_OR_CTRL); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "drag test\ndrop test"); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 7); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK(target_text_edit->get_text() == "drag md\ntee"); + CHECK(target_text_edit->get_caret_count() == 1); + CHECK(target_text_edit->has_selection()); + CHECK(target_text_edit->get_caret_line() == 1); + CHECK(target_text_edit->get_caret_column() == 2); + CHECK(target_text_edit->get_selection_origin_line() == 0); + CHECK(target_text_edit->get_selection_origin_column() == 6); + + // Drop onto selected text deletes the selected text first. + text_edit->set_deselect_on_focus_loss_enabled(false); + target_text_edit->set_deselect_on_focus_loss_enabled(false); + text_edit->remove_secondary_carets(); + text_edit->select(0, 5, 0, 9); + target_text_edit->select(0, 6, 0, 8); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 6).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection(true, 0)); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center(), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "test"); + CHECK(text_edit->has_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 7).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "drag \ndrop test"); + CHECK(target_text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 5); + CHECK(target_text_edit->get_text() == "drag mdtest\ntee"); + CHECK(target_text_edit->has_selection()); + CHECK(target_text_edit->get_caret_line() == 0); + CHECK(target_text_edit->get_caret_column() == 11); + CHECK(target_text_edit->get_selection_origin_line() == 0); + CHECK(target_text_edit->get_selection_origin_column() == 7); + text_edit->set_deselect_on_focus_loss_enabled(true); + target_text_edit->set_deselect_on_focus_loss_enabled(true); + + // Can drop even when drag and drop selection is disabled. + target_text_edit->set_drag_and_drop_selection_enabled(false); + text_edit->select(0, 4, 0, 5); + target_text_edit->deselect(); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 4).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == " "); + CHECK(text_edit->has_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 7).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "drag\ndrop test"); + CHECK(target_text_edit->get_text() == "drag md test\ntee"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + CHECK(target_text_edit->has_selection()); + CHECK(target_text_edit->get_caret_line() == 0); + CHECK(target_text_edit->get_caret_column() == 8); + CHECK(target_text_edit->get_selection_origin_line() == 0); + CHECK(target_text_edit->get_selection_origin_column() == 7); + target_text_edit->set_drag_and_drop_selection_enabled(true); + + // Cannot drop when not editable. + target_text_edit->set_editable(false); + text_edit->select(0, 1, 0, 4); + target_text_edit->deselect(); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "rag"); + CHECK(text_edit->has_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "drag\ndrop test"); + CHECK(target_text_edit->get_text() == "drag md test\ntee"); + CHECK(text_edit->has_selection()); + CHECK_FALSE(target_text_edit->has_selection()); + target_text_edit->set_editable(true); + + // Can drag when not editable, but text will not be removed. + text_edit->set_editable(false); + text_edit->select(0, 0, 0, 4); + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->is_mouse_over_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 7).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_viewport()->gui_get_drag_data() == "drag"); + CHECK(text_edit->has_selection()); + SEND_GUI_MOUSE_MOTION_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 4).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(target_text_edit->get_position() + target_text_edit->get_rect_at_line_column(0, 4).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::NONE, Key::NONE); + CHECK_FALSE(text_edit->get_viewport()->gui_is_dragging()); + CHECK(text_edit->get_text() == "drag\ndrop test"); + CHECK(target_text_edit->get_text() == "dragdrag md test\ntee"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + CHECK(target_text_edit->has_selection()); + CHECK(target_text_edit->get_caret_line() == 0); + CHECK(target_text_edit->get_caret_column() == 8); + CHECK(target_text_edit->get_selection_origin_line() == 0); + CHECK(target_text_edit->get_selection_origin_column() == 4); + text_edit->set_editable(true); memdelete(target_text_edit); } @@ -1324,44 +2745,41 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { } SUBCASE("[TextEdit] overridable actions") { + DisplayServerMock *DS = (DisplayServerMock *)(DisplayServer::get_singleton()); + SIGNAL_WATCH(text_edit, "text_set"); SIGNAL_WATCH(text_edit, "text_changed"); SIGNAL_WATCH(text_edit, "lines_edited_from"); SIGNAL_WATCH(text_edit, "caret_changed"); - Array args1; - args1.push_back(0); - args1.push_back(0); - Array lines_edited_args; - lines_edited_args.push_back(args1); + Array lines_edited_args = build_array(build_array(0, 0)); SUBCASE("[TextEdit] backspace") { text_edit->set_text("this is\nsome\n"); text_edit->set_caret_line(0); text_edit->set_caret_column(0); MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // Cannot backspace at start of text. text_edit->backspace(); MessageQueue::get_singleton()->flush(); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // Backspace at start of line removes the line. text_edit->set_caret_line(2); text_edit->set_caret_column(0); MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(2, 1)); - ((Array)lines_edited_args[0])[0] = 2; - ((Array)lines_edited_args[0])[1] = 1; text_edit->backspace(); MessageQueue::get_singleton()->flush(); - CHECK(text_edit->get_text() == "this is\nsome"); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 4); @@ -1369,10 +2787,10 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - ((Array)lines_edited_args[0])[0] = 1; + // Backspace removes a character. + lines_edited_args = build_array(build_array(1, 1)); text_edit->backspace(); MessageQueue::get_singleton()->flush(); - CHECK(text_edit->get_text() == "this is\nsom"); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 3); @@ -1380,11 +2798,11 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Backspace when text is selected removes the selection. text_edit->end_complex_operation(); text_edit->select(1, 0, 1, 3); text_edit->backspace(); MessageQueue::get_singleton()->flush(); - CHECK(text_edit->get_text() == "this is\n"); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); @@ -1392,11 +2810,11 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Cannot backspace if not editable. text_edit->set_editable(false); text_edit->backspace(); text_edit->set_editable(true); MessageQueue::get_singleton()->flush(); - CHECK(text_edit->get_text() == "this is\n"); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); @@ -1404,6 +2822,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + // Undo restores text to the previous end of complex operation. text_edit->undo(); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_text() == "this is\nsom"); @@ -1412,98 +2831,736 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is\n"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // See ui_text_backspace for more backspace tests. } SUBCASE("[TextEdit] cut") { + // Cut without a selection removes the entire line. text_edit->set_text("this is\nsome\n"); text_edit->set_caret_line(0); text_edit->set_caret_column(6); MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 0)); - ERR_PRINT_OFF; text_edit->cut(); MessageQueue::get_singleton()->flush(); - ERR_PRINT_ON; // Can't check display server content. - - ((Array)lines_edited_args[0])[0] = 1; + CHECK(DS->clipboard_get() == "this is\n"); CHECK(text_edit->get_text() == "some\n"); CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 4); + CHECK(text_edit->get_caret_column() == 3); // In the default font, this is the same position. 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[0])[1] = 1; + // Undo restores the cut text. text_edit->undo(); MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "this is\n"); CHECK(text_edit->get_text() == "this is\nsome\n"); 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); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); - ((Array)lines_edited_args[0])[0] = 1; - ((Array)lines_edited_args[0])[1] = 0; + // Redo. text_edit->redo(); MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "this is\n"); CHECK(text_edit->get_text() == "some\n"); CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 4); + CHECK(text_edit->get_caret_column() == 3); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Cut with a selection removes just the selection. text_edit->set_text("this is\nsome\n"); + text_edit->select(0, 5, 0, 7); MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0)); - ((Array)lines_edited_args[0])[0] = 0; - text_edit->select(0, 5, 0, 7); - ERR_PRINT_OFF; SEND_GUI_ACTION("ui_cut"); CHECK(text_edit->get_viewport()->is_input_handled()); MessageQueue::get_singleton()->flush(); - ERR_PRINT_ON; // Can't check display server content. + CHECK(DS->clipboard_get() == "is"); CHECK(text_edit->get_text() == "this \nsome\n"); + CHECK_FALSE(text_edit->get_caret_line()); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 5); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Cut does not change the text if not editable. Text is still added to clipboard. + text_edit->set_text("this is\nsome\n"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(5); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + text_edit->set_editable(false); text_edit->cut(); MessageQueue::get_singleton()->flush(); text_edit->set_editable(true); - CHECK(text_edit->get_text() == "this \nsome\n"); + CHECK(DS->clipboard_get() == "this is\n"); + CHECK(text_edit->get_text() == "this is\nsome\n"); CHECK(text_edit->get_caret_line() == 0); CHECK(text_edit->get_caret_column() == 5); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Cut line with multiple carets. + text_edit->set_text("this is\nsome\n"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(3); + text_edit->add_caret(0, 2); + text_edit->add_caret(0, 4); + text_edit->add_caret(2, 0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 0), build_array(1, 0)); + + text_edit->cut(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "this is\n\n"); + CHECK(text_edit->get_text() == "some"); + CHECK(text_edit->get_caret_count() == 3); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 2); // In the default font, this is the same position. + // The previous caret at index 1 was merged. + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 3); // In the default font, this is the same position. + CHECK_FALSE(text_edit->has_selection(2)); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 4); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + text_edit->remove_secondary_carets(); + + // Cut on the only line removes the contents. + text_edit->set_caret_line(0); + text_edit->set_caret_column(2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0)); + + text_edit->cut(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "some\n"); + CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_line_count() == 1); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Cut empty line. + text_edit->cut(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "\n"); + CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK_FALSE("caret_changed"); + // These signals are emitted even if there is no change. + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Cut multiple lines, in order. + text_edit->set_text("this is\nsome\ntext to\nbe\n\ncut"); + text_edit->set_caret_line(2); + text_edit->set_caret_column(7); + text_edit->add_caret(3, 0); + text_edit->add_caret(0, 2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 0), build_array(3, 2), build_array(2, 1)); + + text_edit->cut(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "this is\ntext to\nbe\n"); + CHECK(text_edit->get_text() == "some\n\ncut"); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK(text_edit->get_caret_line(1) == 0); + 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); + text_edit->remove_secondary_carets(); + + // Cut multiple selections, in order. Ignores regular carets. + text_edit->set_text("this is\nsome\ntext to\nbe\n\ncut"); + text_edit->add_caret(3, 0); + text_edit->add_caret(0, 2); + text_edit->add_caret(2, 0); + text_edit->select(1, 0, 1, 2, 0); + text_edit->select(3, 0, 4, 0, 1); + text_edit->select(0, 5, 0, 3, 2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 1), build_array(4, 3), build_array(0, 0)); + + text_edit->cut(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "s \nso\nbe\n"); + CHECK(text_edit->get_text() == "thiis\nme\ntext to\n\ncut"); + CHECK(text_edit->get_caret_count() == 4); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 3); + CHECK(text_edit->get_caret_line(3) == 2); + CHECK(text_edit->get_caret_column(3) == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); } SUBCASE("[TextEdit] copy") { - // TODO: Cannot test need display server support. + text_edit->set_text("this is\nsome\ntest\n\ntext"); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + // Copy selected text. + text_edit->select(0, 0, 1, 2, 0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + DS->clipboard_set_primary(""); + + text_edit->copy(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "this is\nso"); + CHECK(DS->clipboard_get_primary() == ""); + CHECK(text_edit->get_text() == "this is\nsome\ntest\n\ntext"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 0); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 2); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Copy with GUI action. + text_edit->select(0, 0, 0, 2, 0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + SEND_GUI_ACTION("ui_copy"); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "th"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Can copy even if not editable. + text_edit->select(2, 4, 1, 2, 0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + text_edit->set_editable(false); + text_edit->copy(); + text_edit->set_editable(true); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "me\ntest"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->deselect(); + + // Copy full line when there is no selection. + text_edit->set_caret_line(0); + text_edit->set_caret_column(2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + text_edit->copy(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "this is\n"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Copy empty line. + text_edit->set_caret_line(3); + text_edit->set_caret_column(0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + text_edit->copy(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "\n"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->deselect(); + + // Copy full line with multiple carets on that line only copies once. + text_edit->set_caret_line(1); + text_edit->set_caret_column(2); + text_edit->add_caret(1, 0); + text_edit->add_caret(1, 4); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + text_edit->copy(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "some\n"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->remove_secondary_carets(); + + // Copy selected text from all selections with `\n` in between, in order. Ignore regular carets. + text_edit->set_caret_line(2); + text_edit->set_caret_column(4); + text_edit->add_caret(4, 0); + text_edit->add_caret(0, 4); + text_edit->add_caret(1, 0); + text_edit->select(1, 3, 2, 4, 0); + text_edit->select(4, 4, 4, 0, 1); + text_edit->select(0, 5, 0, 4, 2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + text_edit->copy(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == " \ne\ntest\ntext"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + text_edit->remove_secondary_carets(); + text_edit->deselect(); + + // Copy multiple lines with multiple carets, in order. + text_edit->set_caret_line(3); + text_edit->set_caret_column(0); + text_edit->add_caret(4, 2); + text_edit->add_caret(0, 4); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + text_edit->copy(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "this is\n\ntext\n"); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); } SUBCASE("[TextEdit] paste") { - // TODO: Cannot test need display server support. + // Paste text from clipboard at caret. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 1)); + DS->clipboard_set("paste"); + + text_edit->paste(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "paste"); + CHECK(text_edit->get_text() == "this is\nsopasteme\n\ntext"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 7); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Undo. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "paste"); + CHECK(text_edit->get_text() == "this is\nsome\n\ntext"); + CHECK(text_edit->get_caret_line() == 1); + 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); + + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "paste"); + CHECK(text_edit->get_text() == "this is\nsopasteme\n\ntext"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 7); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Paste on empty line. Use GUI action. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(2); + text_edit->set_caret_column(0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(2, 2)); + DS->clipboard_set("paste2"); + + SEND_GUI_ACTION("ui_paste"); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "paste2"); + CHECK(text_edit->get_text() == "this is\nsome\npaste2\ntext"); + CHECK(text_edit->get_caret_line() == 2); + 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); + + // Paste removes selection before pasting. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->select(0, 5, 1, 3); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 0), build_array(0, 0)); + DS->clipboard_set("paste"); + + text_edit->paste(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "paste"); + CHECK(text_edit->get_text() == "this pastee\n\ntext"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 10); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Paste multiple lines. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 3)); + DS->clipboard_set("multi\n\nline\npaste"); + + text_edit->paste(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "multi\n\nline\npaste"); + CHECK(text_edit->get_text() == "tmulti\n\nline\npastehis is\nsome\n\ntext"); + CHECK(text_edit->get_caret_line() == 3); + CHECK(text_edit->get_caret_column() == 5); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Paste full line after copying it. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 2)); + DS->clipboard_set(""); + text_edit->copy(); + text_edit->set_caret_column(3); + CHECK(DS->clipboard_get() == "some\n"); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + text_edit->paste(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "some\n"); + CHECK(text_edit->get_text() == "this is\nsome\nsome\n\ntext"); + CHECK(text_edit->get_caret_line() == 2); + CHECK(text_edit->get_caret_column() == 3); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Do not paste as line since it wasn't copied. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 1)); + DS->clipboard_set("paste\n"); + + text_edit->paste(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "paste\n"); + CHECK(text_edit->get_text() == "thispaste\n is\nsome\n\ntext"); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Paste text at each caret. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(2); + text_edit->add_caret(3, 4); + text_edit->add_caret(0, 4); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 1), build_array(2, 3), build_array(5, 6)); + DS->clipboard_set("paste\ntest"); + + text_edit->paste(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "paste\ntest"); + CHECK(text_edit->get_text() == "thispaste\ntest is\nsopaste\ntestme\n\ntextpaste\ntest"); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->get_caret_line(0) == 3); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_caret_line(1) == 6); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK(text_edit->get_caret_line(2) == 1); + CHECK(text_edit->get_caret_column(2) == 4); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + text_edit->remove_secondary_carets(); + + // Paste line per caret when the amount of lines is equal to the number of carets. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(2); + text_edit->add_caret(3, 4); + text_edit->add_caret(0, 4); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0), build_array(1, 1), build_array(3, 3)); + DS->clipboard_set("paste\ntest\n1"); + + text_edit->paste(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "paste\ntest\n1"); + CHECK(text_edit->get_text() == "thispaste is\nsotestme\n\ntext1"); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 6); + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 5); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 9); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + text_edit->remove_secondary_carets(); + + // Cannot paste when not editable. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + DS->clipboard_set("no paste"); + + text_edit->set_editable(false); + text_edit->paste(); + text_edit->set_editable(true); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "no paste"); + CHECK(text_edit->get_text() == "this is\nsome\n\ntext"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); } SUBCASE("[TextEdit] paste primary") { - // TODO: Cannot test need display server support. + // Set size for mouse input. + text_edit->set_size(Size2(200, 200)); + + text_edit->grab_focus(); + DS->clipboard_set(""); + DS->clipboard_set_primary(""); + CHECK(DS->clipboard_get_primary() == ""); + + // Select text with mouse to put into primary clipboard. + text_edit->set_text("this is\nsome\n\ntext"); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(0, 2).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 3).get_center() + Point2i(2, 0), MouseButtonMask::LEFT, Key::NONE); + SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(text_edit->get_rect_at_line_column(1, 3).get_center() + Point2i(2, 0), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(DS->clipboard_get() == ""); + CHECK(DS->clipboard_get_primary() == "is is\nsom"); + CHECK(text_edit->get_text() == "this is\nsome\n\ntext"); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_selected_text() == "is is\nsom"); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 2); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 3); + SIGNAL_CHECK_FALSE("text_set"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Middle click to paste at mouse. + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(3, 4)); + + SEND_GUI_MOUSE_BUTTON_EVENT(text_edit->get_rect_at_line_column(3, 2).get_center() + Point2i(2, 0), MouseButton::MIDDLE, MouseButtonMask::MIDDLE, Key::NONE); + CHECK(DS->clipboard_get_primary() == "is is\nsom"); + CHECK(text_edit->get_text() == "this is\nsome\n\nteis is\nsomxt"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 4); + CHECK(text_edit->get_caret_column() == 3); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Paste at mouse position if there is only one caret. + text_edit->set_text("this is\nsome\n\ntext"); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 1).get_center() + Point2i(2, 0), MouseButtonMask::NONE, Key::NONE); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + DS->clipboard_set_primary("paste"); + lines_edited_args = build_array(build_array(0, 0)); + + text_edit->paste_primary_clipboard(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get_primary() == "paste"); + CHECK(text_edit->get_text() == "tpastehis is\nsome\n\ntext"); + CHECK_FALSE(text_edit->has_selection()); + 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); + + // Paste at all carets if there are multiple carets. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(0); + text_edit->add_caret(2, 0); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(0, 1).get_center() + Point2i(2, 0), MouseButtonMask::NONE, Key::NONE); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + DS->clipboard_set_primary("paste"); + lines_edited_args = build_array(build_array(1, 1), build_array(2, 2)); + + text_edit->paste_primary_clipboard(); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get_primary() == "paste"); + CHECK(text_edit->get_text() == "this is\npastesome\npaste\ntext"); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 5); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Cannot paste if not editable. + text_edit->set_text("this is\nsome\n\ntext"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + SEND_GUI_MOUSE_MOTION_EVENT(text_edit->get_rect_at_line_column(1, 3).get_center() + Point2i(2, 0), MouseButtonMask::NONE, Key::NONE); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + DS->clipboard_set("no paste"); + + text_edit->set_editable(false); + text_edit->paste_primary_clipboard(); + text_edit->set_editable(true); + MessageQueue::get_singleton()->flush(); + CHECK(DS->clipboard_get() == "no paste"); + CHECK(text_edit->get_text() == "this is\nsome\n\ntext"); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 4); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); } SIGNAL_UNWATCH(text_edit, "text_set"); @@ -1512,60 +3569,77 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_UNWATCH(text_edit, "caret_changed"); } - // Add undo / redo tests? SUBCASE("[TextEdit] input") { SIGNAL_WATCH(text_edit, "text_set"); SIGNAL_WATCH(text_edit, "text_changed"); SIGNAL_WATCH(text_edit, "lines_edited_from"); SIGNAL_WATCH(text_edit, "caret_changed"); - Array args1; - args1.push_back(0); - args1.push_back(0); - Array lines_edited_args; - lines_edited_args.push_back(args1); + Array lines_edited_args = build_array(build_array(0, 0)); SUBCASE("[TextEdit] ui_text_newline_above") { 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"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - // For the second caret. - Array args2; - args2.push_back(0); - args2.push_back(1); - lines_edited_args.push_front(args2); + // Insert new line above. + text_edit->select(0, 0, 0, 4); + text_edit->add_caret(1, 4); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 1), build_array(2, 3)); - ((Array)lines_edited_args[1])[1] = 1; SEND_GUI_ACTION("ui_text_newline_above"); CHECK(text_edit->get_viewport()->is_input_handled()); 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(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 2); CHECK(text_edit->get_caret_column(1) == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Undo. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is some test text.\nthis is some test text."); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "\nthis is some test text.\n\nthis is some test text."); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 0); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Does not work if not editable. text_edit->set_caret_line(1); text_edit->set_caret_column(4); - - text_edit->set_caret_line(3, false, true, 0, 1); + text_edit->set_caret_line(3, false, true, -1, 1); text_edit->set_caret_column(4, false, 1); MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("caret_changed"); @@ -1574,31 +3648,57 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SEND_GUI_ACTION("ui_text_newline_above"); CHECK(text_edit->get_viewport()->is_input_handled()); 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(0)); - + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK_FALSE(text_edit->has_selection(1)); 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; + // Works on first line, empty lines, and only happens at caret for selections. + text_edit->select(1, 10, 0, 0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 1), build_array(4, 5)); SEND_GUI_ACTION("ui_text_newline_above"); CHECK(text_edit->get_viewport()->is_input_handled()); 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(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 4); CHECK(text_edit->get_caret_column(1) == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Insert multiple new lines above from one line. + text_edit->set_text("test"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(1); + text_edit->add_caret(0, 3); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 1), build_array(1, 2)); + + SEND_GUI_ACTION("ui_text_newline_above"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "\n\ntest"); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); @@ -1606,34 +3706,55 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SUBCASE("[TextEdit] ui_text_newline_blank") { 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"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); - SIGNAL_DISCARD("caret_changed"); - // For the second caret. - Array args2; - args2.push_back(1); - args2.push_back(2); - lines_edited_args.push_front(args2); + // Insert new line below. + text_edit->select(0, 0, 0, 4); + text_edit->add_caret(1, 4); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 1), build_array(2, 3)); - ((Array)lines_edited_args[1])[1] = 1; SEND_GUI_ACTION("ui_text_newline_blank"); CHECK(text_edit->get_viewport()->is_input_handled()); 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(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Undo. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is some test text.\nthis is some test text."); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is some test text.\n\nthis is some test text.\n"); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + 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)); @@ -1641,75 +3762,119 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Does not work if not editable. text_edit->set_editable(false); SEND_GUI_ACTION("ui_text_newline_blank"); CHECK(text_edit->get_viewport()->is_input_handled()); 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(0)); - + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); 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"); text_edit->set_editable(true); + + // Insert multiple new lines below from one line. + text_edit->set_text("test"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(1); + text_edit->add_caret(0, 3); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 1), build_array(0, 1)); + + SEND_GUI_ACTION("ui_text_newline_blank"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "test\n\n"); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 2); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); + 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") { 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"); SIGNAL_DISCARD("text_changed"); 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); - 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; + // Insert new line at caret. + text_edit->select(0, 0, 0, 4); + text_edit->add_caret(1, 4); + CHECK(text_edit->get_caret_count() == 2); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + // Lines edited: deletion, insert line, insert line. + lines_edited_args = build_array(build_array(0, 0), build_array(0, 1), build_array(2, 3)); SEND_GUI_ACTION("ui_text_newline"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == "\n is some test text.\nthis\n is some test text."); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 3); CHECK(text_edit->get_caret_column(1) == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Undo. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is some test text.\nthis is some test text."); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "\n is some test text.\nthis\n is some test text."); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Does not work if not editable. text_edit->set_editable(false); SEND_GUI_ACTION("ui_text_newline"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == "\n is some test text.\nthis\n is some test text."); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); 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"); @@ -1717,255 +3882,399 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { } SUBCASE("[TextEdit] ui_text_backspace_all_to_left") { - 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); InputMap::get_singleton()->action_add_event("ui_text_backspace_all_to_left", tmpevent); + text_edit->set_text("\nthis is some test text.\n\nthis is some test text."); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); 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[1])[0] = 1; - ((Array)lines_edited_args[1])[1] = 1; + // Remove all text to the left. + text_edit->set_caret_line(1); + text_edit->set_caret_column(5); + text_edit->add_caret(1, 2); + text_edit->add_caret(1, 8); + lines_edited_args = build_array(build_array(1, 1)); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); SEND_GUI_ACTION("ui_text_backspace_all_to_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == "\n is some test text.\n\n is some test text."); + CHECK(text_edit->get_text() == "\nsome test text.\n\nthis is some test text."); + CHECK(text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_line() == 1); CHECK(text_edit->get_caret_column() == 0); - 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); - CHECK(text_edit->get_caret_line(1) == 3); - CHECK(text_edit->get_caret_column(1) == 0); + // Undo. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "\nthis is some test text.\n\nthis is some test text."); + CHECK(text_edit->get_caret_count() == 3); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 5); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 2); + CHECK_FALSE(text_edit->has_selection(2)); + CHECK(text_edit->get_caret_line(2) == 1); + CHECK(text_edit->get_caret_column(2) == 8); 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] = 2; - ((Array)lines_edited_args[1])[1] = 0; + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "\nsome test text.\n\nthis is some test text."); + CHECK(text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 1); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Acts as a normal backspace with selections. + text_edit->select(1, 5, 1, 9, 0); + text_edit->add_caret(3, 4); + text_edit->select(3, 7, 3, 4, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(3, 3), build_array(1, 1)); - // Start of line should also be a normal backspace. SEND_GUI_ACTION("ui_text_backspace_all_to_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == "\nsome text.\n\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 4); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Acts as a normal backspace when at the start of a line. + text_edit->set_caret_column(0); + text_edit->set_caret_column(0, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(3, 2), build_array(1, 0)); + SEND_GUI_ACTION("ui_text_backspace_all_to_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "some text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); 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); + // Does not work if not editable. 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"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); text_edit->set_editable(false); SEND_GUI_ACTION("ui_text_backspace_all_to_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == "some text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == text_edit->get_line(0).length()); + CHECK_FALSE(text_edit->has_selection(1)); 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] = 1; - ((Array)lines_edited_args[0])[1] = 1; - ((Array)lines_edited_args[1])[0] = 0; + // Remove entire line content when at the end of the line. + lines_edited_args = build_array(build_array(1, 1), build_array(0, 0)); SEND_GUI_ACTION("ui_text_backspace_all_to_left"); CHECK(text_edit->get_viewport()->is_input_handled()); 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_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); 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->remove_secondary_carets(); + + // Removing newline effectively happens after removing text. + text_edit->set_text("test\nlines"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(0); + text_edit->add_caret(1, 4); + + SEND_GUI_ACTION("ui_text_backspace_all_to_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == "tests"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + text_edit->remove_secondary_carets(); + + // Removing newline effectively happens after removing text, reverse caret order. + text_edit->set_text("test\nlines"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(4); + text_edit->add_caret(1, 0); + + SEND_GUI_ACTION("ui_text_backspace_all_to_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == "tests"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + text_edit->remove_secondary_carets(); InputMap::get_singleton()->action_erase_event("ui_text_backspace_all_to_left", tmpevent); } SUBCASE("[TextEdit] ui_text_backspace_word") { 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"); SIGNAL_DISCARD("text_changed"); 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[1])[0] = 1; - ((Array)lines_edited_args[1])[1] = 1; + // Acts as a normal backspace with selections. + text_edit->select(1, 8, 1, 15); + text_edit->add_caret(3, 6); + text_edit->select(3, 10, 3, 6, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(3, 3), build_array(1, 1)); SEND_GUI_ACTION("ui_text_backspace_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == "\nthis is st text.\n\nthis ime test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - - CHECK(text_edit->get_caret_line(1) == 3); - CHECK(text_edit->get_caret_column(1) == 0); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 8); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 6); 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] = 2; - ((Array)lines_edited_args[1])[1] = 0; + lines_edited_args = build_array(build_array(3, 2), build_array(1, 0)); // Start of line should also be a normal backspace. + text_edit->set_caret_column(0); + text_edit->set_caret_column(0, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + SEND_GUI_ACTION("ui_text_backspace_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == "this is st text.\nthis ime test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); 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); + // Does not work if not editable. text_edit->set_editable(false); SEND_GUI_ACTION("ui_text_backspace_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == "this is st text.\nthis ime test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); 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); + // FIXME: Remove after GH-77101 is fixed. + text_edit->start_action(TextEdit::ACTION_NONE); + + // Remove text to the start of the word to the left of the caret. text_edit->set_caret_column(text_edit->get_line(0).length()); - text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); + text_edit->set_caret_column(12, false, 1); MessageQueue::get_singleton()->flush(); - - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - - ((Array)lines_edited_args[0])[0] = 1; - ((Array)lines_edited_args[0])[1] = 1; - ((Array)lines_edited_args[1])[0] = 0; + lines_edited_args = build_array(build_array(1, 1), build_array(0, 0)); SEND_GUI_ACTION("ui_text_backspace_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == "this is st \nthis ime t text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 11); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 9); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Undo. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is st text.\nthis ime test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 16); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); - CHECK(text_edit->get_caret_column(1) == 14); + CHECK(text_edit->get_caret_column(1) == 12); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is st \nthis ime t text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 11); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 9); 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_word same line") { - text_edit->set_text("test test test"); - text_edit->set_caret_column(4); - text_edit->add_caret(0, 9); - text_edit->add_caret(0, 15); + // Removing newline effectively happens after removing text. + text_edit->set_text("test\nlines"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(0); + text_edit->add_caret(1, 4); - // For the second caret. - Array args2; - args2.push_back(0); - lines_edited_args.push_front(args2); + SEND_GUI_ACTION("ui_text_backspace_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == "tests"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + text_edit->remove_secondary_carets(); - // For the third caret. - Array args3; - args2.push_back(0); - lines_edited_args.push_front(args2); + // Removing newline effectively happens after removing text, reverse caret order. + text_edit->set_text("test\nlines"); + text_edit->set_caret_line(1); + text_edit->set_caret_column(4); + text_edit->add_caret(1, 0); - CHECK(text_edit->get_caret_count() == 3); - MessageQueue::get_singleton()->flush(); + SEND_GUI_ACTION("ui_text_backspace_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == "tests"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + text_edit->remove_secondary_carets(); + } + SUBCASE("[TextEdit] ui_text_backspace_word same line") { + text_edit->set_text("test longwordtest test"); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // Multiple carets on the same line is handled. + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + text_edit->add_caret(0, 11); + text_edit->add_caret(0, 15); + text_edit->add_caret(0, 9); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + lines_edited_args = build_array(build_array(0, 0), build_array(0, 0)); + SEND_GUI_ACTION("ui_text_backspace_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " "); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_text() == " st test"); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 0); CHECK(text_edit->get_caret_column(1) == 1); - 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); - CHECK(text_edit->get_caret_line(2) == 0); - CHECK(text_edit->get_caret_column(2) == 2); + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test longwordtest test"); + CHECK(text_edit->get_caret_count() == 4); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 11); + CHECK_FALSE(text_edit->has_selection(2)); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 15); + CHECK_FALSE(text_edit->has_selection(3)); + CHECK(text_edit->get_caret_line(3) == 0); + CHECK(text_edit->get_caret_column(3) == 9); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " st test"); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + 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); @@ -1973,130 +4282,267 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SUBCASE("[TextEdit] ui_text_backspace") { 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"); SIGNAL_DISCARD("text_changed"); 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[1])[0] = 1; - ((Array)lines_edited_args[1])[1] = 1; + // Remove selected text when there are selections. + text_edit->select(1, 0, 1, 4); + text_edit->add_caret(3, 4); + text_edit->select(3, 5, 3, 2, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(3, 3), build_array(1, 1)); SEND_GUI_ACTION("ui_text_backspace"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == "\n is some test text.\n\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 3); + 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); + // Undo remove selection. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_text() == "\nthis is some test text.\n\nthis is some test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 0); + CHECK(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 3); - CHECK(text_edit->get_caret_column(1) == 0); + CHECK(text_edit->get_caret_column(1) == 2); + CHECK(text_edit->get_selection_origin_line(1) == 3); + CHECK(text_edit->get_selection_origin_column(1) == 5); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + + // Redo remove selection. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "\n is some test text.\n\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 3); + 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); - ((Array)lines_edited_args[0])[1] = 2; - ((Array)lines_edited_args[1])[1] = 0; + // Remove the newline when at start of line. + text_edit->set_caret_column(0, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(3, 2), build_array(1, 0)); - // Start of line should also be a normal backspace. SEND_GUI_ACTION("ui_text_backspace"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == " is some test text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); CHECK(text_edit->get_caret_column(1) == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Undo remove newline. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "\n is some test text.\n\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 3); + CHECK(text_edit->get_caret_column(1) == 0); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + + // Redo remove newline. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " is some test text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 0); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Does not work if not editable. text_edit->set_caret_column(text_edit->get_line(0).length()); - text_edit->set_caret_column(text_edit->get_line(1).length(), false, 1); + text_edit->set_caret_column(15, false, 1); MessageQueue::get_singleton()->flush(); - - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); text_edit->set_editable(false); SEND_GUI_ACTION("ui_text_backspace"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == " is some test text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); 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(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == text_edit->get_line(0).length()); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 15); 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] = 1; - ((Array)lines_edited_args[0])[1] = 1; - ((Array)lines_edited_args[1])[0] = 0; + // FIXME: Remove after GH-77101 is fixed. + text_edit->start_action(TextEdit::ACTION_NONE); + + // Backspace removes character to the left. + lines_edited_args = build_array(build_array(1, 1), build_array(0, 0)); SEND_GUI_ACTION("ui_text_backspace"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == " is some test text\nthis some testtext."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 18); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 14); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Backspace another character without changing caret. + SEND_GUI_ACTION("ui_text_backspace"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " is some test tex\nthis some testext."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 17); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 13); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Undo both backspaces. + lines_edited_args = build_array(build_array(1, 1), build_array(0, 0), build_array(1, 1), build_array(0, 0)); + + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " is some test text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 19); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); - CHECK(text_edit->get_caret_column(1) == 18); + CHECK(text_edit->get_caret_column(1) == 15); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + + // Redo both backspaces. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " is some test tex\nthis some testext."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 17); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 13); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - // Select the entire text, from right to left - text_edit->select(0, 18, 0, 0); + // Backspace with multiple carets that will overlap. + text_edit->remove_secondary_carets(); 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); + text_edit->set_caret_column(8); + text_edit->add_caret(0, 7); + text_edit->add_caret(0, 9); MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0), build_array(0, 0), build_array(0, 0)); - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); + SEND_GUI_ACTION("ui_text_backspace"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " is sotest tex\nthis some testext."); + CHECK(text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection()); + 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); + + // Select each line of text, from right to left. Remove selection to column 0. + text_edit->select(0, text_edit->get_line(0).length(), 0, 0); + text_edit->add_caret(1, 0); + text_edit->select(1, text_edit->get_line(1).length(), 1, 0, 1); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 1), build_array(0, 0)); SEND_GUI_ACTION("ui_text_backspace"); 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_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); 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); + + // Backspace at start of first line does nothing. + text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + SEND_GUI_ACTION("ui_text_backspace"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "\n"); + CHECK(text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 0); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); } SUBCASE("[TextEdit] ui_text_delete_all_to_right") { @@ -2104,101 +4550,138 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { InputMap::get_singleton()->action_add_event("ui_text_delete_all_to_right", tmpevent); 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"); SIGNAL_DISCARD("text_changed"); 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); + // Remove all text to right of caret. + text_edit->set_caret_line(0); + text_edit->set_caret_column(18); + text_edit->add_caret(0, 16); + text_edit->add_caret(0, 20); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0)); - // With selection should be a normal delete. SEND_GUI_ACTION("ui_text_delete_all_to_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == "this is some tes\nthis is some test text.\n"); + CHECK(text_edit->get_caret_count() == 1); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 16); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); - CHECK(text_edit->get_caret_line(1) == 1); - CHECK(text_edit->get_caret_column(1) == 0); + // Undo. + lines_edited_args = build_array(build_array(0, 0)); + + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is some test text.\nthis is some test text.\n"); + CHECK(text_edit->get_caret_count() == 3); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 18); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 16); + CHECK_FALSE(text_edit->has_selection(2)); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 20); 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); + // Redo. + text_edit->redo(); MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is some tes\nthis is some test text.\n"); + CHECK(text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 16); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); + // Acts as a normal delete with selections. + text_edit->select(0, 0, 0, 4); + text_edit->add_caret(1, 4); + text_edit->select(1, 8, 1, 4, 1); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0), build_array(1, 1)); SEND_GUI_ACTION("ui_text_delete_all_to_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == " is some tes\nthissome test text.\n"); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Does nothing when caret is at end of line. + 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("caret_changed"); + + SEND_GUI_ACTION("ui_text_delete_all_to_right"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " is some tes\nthissome test text.\n"); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == text_edit->get_line(0).length()); + CHECK_FALSE(text_edit->has_selection(1)); 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"); + // Does not work if not editable. text_edit->set_caret_column(0); text_edit->set_caret_column(0, false, 1); MessageQueue::get_singleton()->flush(); - - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); text_edit->set_editable(false); SEND_GUI_ACTION("ui_text_delete_all_to_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == " is some tes\nthissome test text.\n"); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); 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); + // Delete entire line. SEND_GUI_ACTION("ui_text_delete_all_to_right"); CHECK(text_edit->get_viewport()->is_input_handled()); CHECK(text_edit->get_text() == "\n\n"); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); 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); @@ -2210,302 +4693,589 @@ 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\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); - + text_edit->set_text("this is some test text.\n\nthis is some test text.\n"); MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); 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); + // Acts as a normal delete with selections. + text_edit->select(0, 8, 0, 15); + text_edit->add_caret(2, 6); + text_edit->select(2, 10, 2, 6, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0), build_array(2, 2)); - // With selection should be a normal delete. SEND_GUI_ACTION("ui_text_delete_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == "this is st text.\n\nthis ime test text.\n"); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - - CHECK(text_edit->get_caret_line(1) == 2); - CHECK(text_edit->get_caret_column(1) == 0); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 8); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 6); 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] = 3; - ((Array)lines_edited_args[1])[0] = 1; + // Removes newlines when at end of line. 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"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(1, 0), build_array(2, 1)); SEND_GUI_ACTION("ui_text_delete_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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()); - + CHECK(text_edit->get_text() == "this is st text.\nthis ime test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == text_edit->get_line(0).length()); + CHECK_FALSE(text_edit->has_selection(1)); 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[1])[0] = 0; - ((Array)lines_edited_args[0])[0] = 1; - ((Array)lines_edited_args[0])[1] = 1; + // Does not work if not editable. text_edit->set_caret_column(0); - text_edit->set_caret_column(0, false, 1); + text_edit->set_caret_column(10, false, 1); MessageQueue::get_singleton()->flush(); - - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); text_edit->set_editable(false); SEND_GUI_ACTION("ui_text_delete_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == "this is st text.\nthis ime test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - - CHECK(text_edit->get_caret_line(1) == 1); - CHECK(text_edit->get_caret_column(1) == 0); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 10); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_editable(true); + // FIXME: Remove after GH-77101 is fixed. + text_edit->start_action(TextEdit::ACTION_NONE); + + // Delete to the end of the word right of the caret. + lines_edited_args = build_array(build_array(0, 0), build_array(1, 1)); + SEND_GUI_ACTION("ui_text_delete_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == " is st text.\nthis ime t text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 10); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Undo. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "this is st text.\nthis ime test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); CHECK(text_edit->get_caret_line(1) == 1); - CHECK(text_edit->get_caret_column(1) == 0); + CHECK(text_edit->get_caret_column(1) == 10); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " is st text.\nthis ime t text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 10); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - } - SUBCASE("[TextEdit] ui_text_delete") { - text_edit->set_caret_mid_grapheme_enabled(true); - CHECK(text_edit->is_caret_mid_grapheme_enabled()); + // Delete one word with multiple carets. + text_edit->remove_secondary_carets(); + text_edit->set_text("onelongword test"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(6); + text_edit->add_caret(0, 9); + text_edit->add_caret(0, 3); + lines_edited_args = build_array(build_array(0, 0)); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("text_set"); + SIGNAL_DISCARD("text_changed"); + SIGNAL_DISCARD("lines_edited_from"); + SIGNAL_DISCARD("caret_changed"); - text_edit->set_text("this ffi some test text.\nthis ffi some test text."); - text_edit->select(0, 0, 0, 4); + SEND_GUI_ACTION("ui_text_delete_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "one test"); + CHECK(text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 3); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Removing newline effectively happens after removing text. + text_edit->set_text("test\nlines"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(2); + text_edit->add_caret(0, 4); + + SEND_GUI_ACTION("ui_text_delete_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == "telines"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 2); + text_edit->remove_secondary_carets(); + + // Removing newline effectively happens after removing text, reverse caret order. + text_edit->set_text("test\nlines"); text_edit->set_caret_line(0); text_edit->set_caret_column(4); + text_edit->add_caret(0, 2); - text_edit->add_caret(1, 4); - text_edit->select(1, 0, 1, 4, 1); - CHECK(text_edit->get_caret_count() == 2); + SEND_GUI_ACTION("ui_text_delete_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == "telines"); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 2); + text_edit->remove_secondary_carets(); + } + SUBCASE("[TextEdit] ui_text_delete_word same line") { + text_edit->set_text("test longwordtest test"); MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); 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); + // Multiple carets on the same line is handled. + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + text_edit->add_caret(0, 11); + text_edit->add_caret(0, 15); + text_edit->add_caret(0, 9); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + lines_edited_args = build_array(build_array(0, 0), build_array(0, 0)); - // With selection should be a normal delete. - SEND_GUI_ACTION("ui_text_delete"); + SEND_GUI_ACTION("ui_text_delete_word"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == " long test"); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - - CHECK(text_edit->get_caret_line(1) == 1); - CHECK(text_edit->get_caret_column(1) == 0); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 5); 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()); + lines_edited_args = build_array(build_array(0, 0), build_array(0, 0)); + + text_edit->undo(); MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "test longwordtest test"); + CHECK(text_edit->get_caret_count() == 4); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 11); + CHECK_FALSE(text_edit->has_selection(2)); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 15); + CHECK_FALSE(text_edit->has_selection(3)); + CHECK(text_edit->get_caret_line(3) == 0); + CHECK(text_edit->get_caret_column(3) == 9); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " long test"); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 5); + 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_delete") { + text_edit->set_caret_mid_grapheme_enabled(true); + CHECK(text_edit->is_caret_mid_grapheme_enabled()); + + text_edit->set_text("this is some test text.\n\nthis is some test text.\n"); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); + // Remove selected text when there are selections. + text_edit->select(0, 0, 0, 4); + text_edit->add_caret(2, 2); + text_edit->select(2, 5, 2, 2, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + lines_edited_args = build_array(build_array(0, 0), build_array(2, 2)); + SEND_GUI_ACTION("ui_text_delete"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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() == 20); + CHECK(text_edit->get_text() == " is some test text.\n\nthis some test text.\n"); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 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); - // 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); + // Undo remove selection. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_text() == "this is some test text.\n\nthis is some test text.\n"); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 0); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 2); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 5); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); - ((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; + // Redo remove selection. + text_edit->redo(); MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " is some test text.\n\nthis some test text.\n"); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 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); - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); + // Remove newline when at end of line. + 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("caret_changed"); + lines_edited_args = build_array(build_array(1, 0), build_array(2, 1)); - text_edit->set_editable(false); SEND_GUI_ACTION("ui_text_delete"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == " is some test text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 19); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 20); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); - CHECK(text_edit->get_caret_line(1) == 0); + // Undo remove newline. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " is some test text.\n\nthis some test text.\n"); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 19); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 2); CHECK(text_edit->get_caret_column(1) == 20); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); + + // Redo remove newline. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " is some test text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 19); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 20); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Does not work if not editable. + text_edit->set_caret_column(0); + text_edit->set_caret_column(15, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + text_edit->set_editable(false); + SEND_GUI_ACTION("ui_text_delete"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == " is some test text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 15); SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_editable(true); + // FIXME: Remove after GH-77101 is fixed. text_edit->start_action(TextEdit::EditAction::ACTION_NONE); + // Delete removes character to the right. + lines_edited_args = build_array(build_array(0, 0), build_array(1, 1)); + SEND_GUI_ACTION("ui_text_delete"); CHECK(text_edit->get_viewport()->is_input_handled()); - 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(text_edit->get_text() == "is some test text.\nthis some test ext."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 15); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", lines_edited_args); - CHECK(text_edit->get_caret_line(1) == 0); - CHECK(text_edit->get_caret_column(1) == 19); + // Delete another character without changing caret. + SEND_GUI_ACTION("ui_text_delete"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "s some test text.\nthis some test xt."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - SIGNAL_CHECK("caret_changed", empty_signal_args); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 15); + SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - text_edit->start_action(TextEdit::EditAction::ACTION_NONE); + // Undo both deletes. + lines_edited_args = build_array(build_array(0, 0), build_array(1, 1), build_array(0, 0), build_array(1, 1)); - SEND_GUI_ACTION("ui_text_delete"); - CHECK(text_edit->get_viewport()->is_input_handled()); - 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); + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == " is some test text.\nthis some test text."); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 15); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK("text_changed", empty_signal_args); + SIGNAL_CHECK("lines_edited_from", reverse_nested(lines_edited_args)); - CHECK(text_edit->get_caret_line(1) == 0); - CHECK(text_edit->get_caret_column(1) == 18); + // Redo both deletes. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "s some test text.\nthis some test xt."); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); - SIGNAL_CHECK("caret_changed", empty_signal_args); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 15); + SIGNAL_CHECK_FALSE("caret_changed"); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + + // Delete at end of last line does nothing. + text_edit->remove_secondary_carets(); + text_edit->set_caret_line(1); + text_edit->set_caret_column(18); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + SEND_GUI_ACTION("ui_text_delete"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_text() == "s some test text.\nthis some test xt."); + CHECK(text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 18); + SIGNAL_CHECK_FALSE("caret_changed"); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); } SUBCASE("[TextEdit] ui_text_caret_word_left") { 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); + text_edit->set_caret_column(15); + text_edit->add_caret(2, 10); + text_edit->select(1, 10, 1, 15); + text_edit->select(2, 15, 2, 10, 1); MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - // Shift should select. + // Deselect to start of previous word when selection is right to left. + // Select to start of next word when selection is left to right. #ifdef MACOS_ENABLED SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::ALT | KeyModifierMask::SHIFT); #else SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT); #endif CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 5); - CHECK(text_edit->get_selected_text(0) == "is"); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "me "); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 13); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 10); + CHECK(text_edit->is_caret_after_selection_origin(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)); + CHECK(text_edit->get_selected_text(1) == "some te"); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 8); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 15); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(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("ui_text_caret_word_left"); + // Select to start of word with shift. + text_edit->deselect(); + text_edit->set_caret_column(7); + text_edit->set_caret_column(16, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + +#ifdef MACOS_ENABLED + SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::ALT | KeyModifierMask::SHIFT); +#else + SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT); +#endif CHECK(text_edit->get_viewport()->is_input_handled()); - 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_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "is"); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 7); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(0)); + + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "tes"); CHECK(text_edit->get_caret_line(1) == 2); - CHECK(text_edit->get_caret_column(1) == 0); - CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_column(1) == 13); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 16); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(1)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); - // Normal word left. + // Deselect and move caret to start of next word without shift. SEND_GUI_ACTION("ui_text_caret_word_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 8); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + // Moves to end of previous line when at start of line. Does nothing at start of text. + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + text_edit->set_caret_column(0, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + SEND_GUI_ACTION("ui_text_caret_word_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); 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"); @@ -2515,249 +5285,417 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { 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); - + text_edit->select(1, 3, 1, 7); + text_edit->add_caret(2, 3); + text_edit->select(2, 7, 2, 3, 1); MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - // Normal left should deselect and place at selection start. - SEND_GUI_ACTION("ui_text_caret_left"); + // Remove one character from selection when selection is left to right. + // Add one character to selection when selection is right to left. + SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT); CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "s i"); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 6); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 3); + CHECK(text_edit->is_caret_after_selection_origin(0)); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 2); - CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "is is"); CHECK(text_edit->get_caret_line(1) == 2); CHECK(text_edit->get_caret_column(1) == 2); - CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 7); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(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(Key::LEFT | KeyModifierMask::SHIFT); + // Deselect and put caret at selection start without shift. + SEND_GUI_ACTION("ui_text_caret_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 1); - CHECK(text_edit->get_selected_text(0) == "h"); - CHECK(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 3); + CHECK_FALSE(text_edit->has_selection(1)); 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)); - + CHECK(text_edit->get_caret_column(1) == 2); 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. + // Move caret one character to the left. SEND_GUI_ACTION("ui_text_caret_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 1); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 2); + CHECK_FALSE(text_edit->has_selection(1)); 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("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); - // Normal left. - SEND_GUI_ACTION("ui_text_caret_left"); + // Select one character to the left with shift and no existing selection. + SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT); CHECK(text_edit->get_viewport()->is_input_handled()); - 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_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "h"); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 1); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 2); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(0)); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "t"); CHECK(text_edit->get_caret_line(1) == 2); CHECK(text_edit->get_caret_column(1) == 0); - CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 1); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(1)); + 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. + // Moves to end of previous line when at start of line. Does nothing at start of text. + text_edit->deselect(); + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + text_edit->set_caret_column(0, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + SEND_GUI_ACTION("ui_text_caret_left"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); 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\nthis is some test text\n"); - text_edit->set_caret_line(0); - text_edit->set_caret_column(13); + // Selects to end of previous line when at start of line. + text_edit->remove_secondary_carets(); + text_edit->set_caret_line(1); + text_edit->set_caret_column(0); + text_edit->select(1, 1, 1, 0); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); - text_edit->add_caret(2, 13); - CHECK(text_edit->get_caret_count() == 2); + SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "\nt"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 1); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + // Merge selections when they overlap. + text_edit->set_caret_line(1); + text_edit->set_caret_column(4); + text_edit->select(1, 6, 1, 4); + text_edit->add_caret(1, 8); + text_edit->select(1, 8, 1, 6, 1); MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + CHECK(text_edit->get_caret_count() == 2); + SEND_GUI_KEY_EVENT(Key::LEFT | KeyModifierMask::SHIFT); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "s is "); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 3); + CHECK(text_edit->get_selection_origin_line(0) == 1); + CHECK(text_edit->get_selection_origin_column(0) == 8); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(0)); + 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\nthis is some test text"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(15); + text_edit->add_caret(2, 10); + text_edit->select(0, 10, 0, 15); + text_edit->select(2, 15, 2, 10, 1); + MessageQueue::get_singleton()->flush(); SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - // Shift should select. + // Select to end of next word when selection is right to left. + // Deselect to end of previous word when selection is left to right. #ifdef MACOS_ENABLED SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::ALT | KeyModifierMask::SHIFT); #else SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT); #endif CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 17); - CHECK(text_edit->get_selected_text(0) == "test"); + CHECK(text_edit->get_caret_count() == 2); CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "me test"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 17); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 10); + CHECK(text_edit->is_caret_after_selection_origin(0)); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == " te"); 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->get_caret_column(1) == 12); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 15); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(1)); + + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Select to end of word with shift. + text_edit->deselect(); + text_edit->set_caret_column(13); + text_edit->set_caret_column(15, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); +#ifdef MACOS_ENABLED + SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::ALT | KeyModifierMask::SHIFT); +#else + SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT); +#endif + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "test"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 17); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 13); + CHECK(text_edit->is_caret_after_selection_origin(0)); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "st"); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 17); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 15); + CHECK(text_edit->is_caret_after_selection_origin(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. + // Deselect and move caret to end of next word without shift. SEND_GUI_ACTION("ui_text_caret_word_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 22); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 22); + CHECK_FALSE(text_edit->has_selection(1)); 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. + // Moves to start of next line when at end of line. Does nothing at end of text. SEND_GUI_ACTION("ui_text_caret_word_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 1); - CHECK(text_edit->get_caret_column() == 0); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - - CHECK(text_edit->get_caret_line(1) == 3); - CHECK(text_edit->get_caret_column(1) == 0); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 22); 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\nthis is some test text\n"); + text_edit->set_text("this is some test text\n\nthis is some test text"); 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); - + text_edit->set_caret_column(19); + text_edit->select(0, 15, 0, 19); + text_edit->add_caret(2, 15); + text_edit->select(2, 19, 2, 15, 1); MessageQueue::get_singleton()->flush(); - SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); SIGNAL_DISCARD("lines_edited_from"); SIGNAL_DISCARD("caret_changed"); - // Normal right should deselect and place at selection start. - SEND_GUI_ACTION("ui_text_caret_right"); + // Remove one character from selection when selection is right to left. + // Add one character to selection when selection is left to right. + SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 20); - CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "st te"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 20); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 15); + CHECK(text_edit->is_caret_after_selection_origin(0)); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "t t"); CHECK(text_edit->get_caret_line(1) == 2); - CHECK(text_edit->get_caret_column(1) == 20); - CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_column(1) == 16); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 19); + CHECK_FALSE(text_edit->is_caret_after_selection_origin(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(Key::RIGHT | KeyModifierMask::SHIFT); + // Deselect and put caret at selection end without shift. + SEND_GUI_ACTION("ui_text_caret_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 21); - CHECK(text_edit->get_selected_text(0) == "x"); - CHECK(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 20); + CHECK_FALSE(text_edit->has_selection(1)); 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)); - + CHECK(text_edit->get_caret_column(1) == 19); 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. + // Move caret one character to the right. SEND_GUI_ACTION("ui_text_caret_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 21); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 21); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 20); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); + // Select one character to the right with shift and no existing selection. + SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "t"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 22); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 21); + CHECK(text_edit->is_caret_after_selection_origin(0)); + + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selected_text(1) == "x"); 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"); + CHECK(text_edit->get_selection_origin_line(1) == 2); + CHECK(text_edit->get_selection_origin_column(1) == 20); + CHECK(text_edit->is_caret_after_selection_origin(1)); + + SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); - // Normal right. + // Moves to start of next line when at end of line. Does nothing at end of text. + text_edit->deselect(); + text_edit->set_caret_line(0); + text_edit->set_caret_column(22); + text_edit->set_caret_column(22, false, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + SEND_GUI_ACTION("ui_text_caret_right"); CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 22); + CHECK(text_edit->get_caret_count() == 2); CHECK_FALSE(text_edit->has_selection(0)); - + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK_FALSE(text_edit->has_selection(1)); 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("ui_text_caret_right"); + // Selects to start of next line when at end of line. + text_edit->remove_secondary_carets(); + text_edit->set_caret_line(0); + text_edit->set_caret_column(22); + text_edit->select(0, 21, 0, 22); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + + SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT); CHECK(text_edit->get_viewport()->is_input_handled()); - 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_count() == 1); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == "t\n"); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 0); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 21); + CHECK(text_edit->is_caret_after_selection_origin(0)); + SIGNAL_CHECK("caret_changed", empty_signal_args); + SIGNAL_CHECK_FALSE("text_changed"); + SIGNAL_CHECK_FALSE("lines_edited_from"); - CHECK(text_edit->get_caret_line(1) == 3); - CHECK(text_edit->get_caret_column(1) == 0); - CHECK_FALSE(text_edit->has_selection(1)); + // Merge selections when they overlap. + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + text_edit->select(0, 4, 0, 6); + text_edit->add_caret(0, 8); + text_edit->select(0, 6, 0, 8, 1); + MessageQueue::get_singleton()->flush(); + SIGNAL_DISCARD("caret_changed"); + CHECK(text_edit->get_caret_count() == 2); + + SEND_GUI_KEY_EVENT(Key::RIGHT | KeyModifierMask::SHIFT); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selected_text(0) == " is s"); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 9); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 4); + CHECK(text_edit->is_caret_after_selection_origin(0)); SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); @@ -2775,7 +5713,6 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_caret_count() == 2); MessageQueue::get_singleton()->flush(); - CHECK(text_edit->is_line_wrapped(0)); SIGNAL_DISCARD("text_set"); SIGNAL_DISCARD("text_changed"); @@ -3156,11 +6093,7 @@ 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); + lines_edited_args = build_array(build_array(0, 0), build_array(1, 1)); SEND_GUI_KEY_EVENT(Key::A); CHECK(text_edit->get_viewport()->is_input_handled()); @@ -3171,6 +6104,27 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); + // Undo reverts both carets. + text_edit->undo(); + MessageQueue::get_singleton()->flush(); + CHECK(text_edit->get_text() == "a\na"); + 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", reverse_nested(lines_edited_args)); + + // Redo. + text_edit->redo(); + MessageQueue::get_singleton()->flush(); + 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); + + // Does not work if not editable. text_edit->set_editable(false); SEND_GUI_KEY_EVENT(Key::A); CHECK_FALSE(text_edit->get_viewport()->is_input_handled()); // Should this be handled? @@ -3182,8 +6136,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK_FALSE("lines_edited_from"); text_edit->set_editable(true); - lines_edited_args.push_back(lines_edited_args[1].duplicate()); - lines_edited_args.push_front(args2.duplicate()); + lines_edited_args = build_array(build_array(0, 0), build_array(0, 0), build_array(1, 1), build_array(1, 1)); text_edit->select(0, 0, 0, 1); text_edit->select(1, 0, 1, 1, 1); @@ -3220,8 +6173,7 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { text_edit->set_overtype_mode_enabled(false); CHECK_FALSE(text_edit->is_overtype_mode_enabled()); - lines_edited_args.remove_at(0); - lines_edited_args.remove_at(1); + lines_edited_args = build_array(build_array(0, 0), build_array(1, 1)); SEND_GUI_KEY_EVENT(Key::TAB); CHECK(text_edit->get_viewport()->is_input_handled()); @@ -3576,6 +6528,11 @@ TEST_CASE("[SceneTree][TextEdit] caret") { text_edit->set_caret_column(4); CHECK(text_edit->get_word_under_caret() == "Lorem"); + text_edit->set_caret_column(1); + text_edit->add_caret(0, 15); + CHECK(text_edit->get_word_under_caret() == "Lorem\ndolor"); + text_edit->remove_secondary_carets(); + // Should this work? text_edit->set_caret_column(5); CHECK(text_edit->get_word_under_caret() == ""); @@ -3616,18 +6573,20 @@ TEST_CASE("[SceneTree][TextEdit] multicaret") { SIGNAL_DISCARD("caret_changed"); SUBCASE("[TextEdit] add remove caret") { - // Overlapping + // 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); + // Select. + text_edit->select(2, 4, 0, 0); + + // Cannot add in selection. 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 + // Cannot add when 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); @@ -3670,23 +6629,276 @@ TEST_CASE("[SceneTree][TextEdit] multicaret") { ERR_PRINT_ON; } - 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); + SUBCASE("[TextEdit] sort carets") { + Vector<int> sorted_carets = { 0, 1, 2 }; - 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); + // Ascending order. + text_edit->remove_secondary_carets(); + text_edit->add_caret(0, 1); + text_edit->add_caret(1, 0); + CHECK(text_edit->get_sorted_carets() == sorted_carets); + // Descending order. + sorted_carets = { 2, 1, 0 }; text_edit->remove_secondary_carets(); text_edit->set_caret_line(1); - CHECK(text_edit->add_caret(0, 0)); + text_edit->add_caret(0, 1); + text_edit->add_caret(0, 0); + CHECK(text_edit->get_sorted_carets() == sorted_carets); + + // Mixed order. + sorted_carets = { 0, 2, 1, 3 }; + text_edit->remove_secondary_carets(); + text_edit->set_caret_line(0); + text_edit->add_caret(1, 0); + text_edit->add_caret(0, 1); + text_edit->add_caret(1, 1); + CHECK(text_edit->get_sorted_carets() == sorted_carets); + + // Overlapping carets. + sorted_carets = { 0, 1, 3, 2 }; + text_edit->remove_secondary_carets(); + text_edit->add_caret(0, 1); + text_edit->add_caret(1, 2); + text_edit->add_caret(0, 2); + text_edit->set_caret_column(1, false, 3); + CHECK(text_edit->get_sorted_carets() == sorted_carets); + + // Sorted by selection start. + sorted_carets = { 1, 0 }; + text_edit->remove_secondary_carets(); + text_edit->select(1, 3, 1, 5); + text_edit->add_caret(2, 0); + text_edit->select(1, 0, 2, 0, 1); + CHECK(text_edit->get_sorted_carets() == sorted_carets); + } + + SUBCASE("[TextEdit] merge carets") { + text_edit->set_text("this is some text\nfor selection"); + MessageQueue::get_singleton()->flush(); + + // Don't merge carets that are not overlapping. + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + text_edit->add_caret(0, 6); + text_edit->add_caret(1, 6); + text_edit->merge_overlapping_carets(); + CHECK(text_edit->get_caret_count() == 3); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 6); + CHECK(text_edit->get_caret_line(2) == 1); + CHECK(text_edit->get_caret_column(2) == 6); + text_edit->remove_secondary_carets(); + + // Don't merge when in a multicaret edit. + text_edit->begin_multicaret_edit(); + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + text_edit->add_caret(0, 4); + text_edit->merge_overlapping_carets(); + CHECK(text_edit->is_in_mulitcaret_edit()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 4); + + // Merge overlapping carets. Merge at the end of the multicaret edit. + text_edit->end_multicaret_edit(); + CHECK_FALSE(text_edit->is_in_mulitcaret_edit()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + + // Don't merge selections that are not overlapping. + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + text_edit->add_caret(0, 2); + text_edit->add_caret(1, 4); + text_edit->select(0, 4, 1, 2, 0); + text_edit->select(0, 2, 0, 3, 1); + text_edit->select(1, 4, 1, 8, 2); + text_edit->merge_overlapping_carets(); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->has_selection(2)); + text_edit->remove_secondary_carets(); + text_edit->deselect(); + + // Don't merge selections that are only touching. + text_edit->set_caret_line(0); + text_edit->set_caret_column(4); + text_edit->add_caret(1, 2); + text_edit->select(0, 4, 1, 2, 0); + text_edit->select(1, 2, 1, 5, 1); + text_edit->merge_overlapping_carets(); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->has_selection(1)); + text_edit->remove_secondary_carets(); + text_edit->deselect(); + + // Merge carets into selection. + text_edit->set_caret_line(0); + text_edit->set_caret_column(3); + text_edit->add_caret(0, 2); + text_edit->add_caret(1, 4); + text_edit->add_caret(1, 8); + text_edit->add_caret(1, 10); + text_edit->select(0, 2, 1, 8, 0); + text_edit->merge_overlapping_carets(); CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_from_line(0) == 0); + CHECK(text_edit->get_selection_from_column(0) == 2); + CHECK(text_edit->get_selection_to_line(0) == 1); + CHECK(text_edit->get_selection_to_column(0) == 8); + CHECK(text_edit->is_caret_after_selection_origin(0)); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 10); + text_edit->remove_secondary_carets(); + text_edit->deselect(); - 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); + // Merge partially overlapping selections. + text_edit->set_caret_line(0); + text_edit->set_caret_column(1); + text_edit->add_caret(0, 2); + text_edit->add_caret(0, 3); + text_edit->select(0, 2, 0, 6, 0); + text_edit->select(0, 4, 1, 3, 1); + text_edit->select(1, 0, 1, 5, 2); + text_edit->merge_overlapping_carets(); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_from_line(0) == 0); + CHECK(text_edit->get_selection_from_column(0) == 2); + CHECK(text_edit->get_selection_to_line(0) == 1); + CHECK(text_edit->get_selection_to_column(0) == 5); + CHECK(text_edit->is_caret_after_selection_origin(0)); + text_edit->remove_secondary_carets(); + text_edit->deselect(); + + // Merge smaller overlapping selection into a bigger one. + text_edit->set_caret_line(0); + text_edit->set_caret_column(1); + text_edit->add_caret(0, 2); + text_edit->add_caret(0, 3); + text_edit->select(0, 2, 0, 6, 0); + text_edit->select(0, 8, 1, 3, 1); + text_edit->select(0, 2, 1, 5, 2); + text_edit->merge_overlapping_carets(); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_from_line(0) == 0); + CHECK(text_edit->get_selection_from_column(0) == 2); + CHECK(text_edit->get_selection_to_line(0) == 1); + CHECK(text_edit->get_selection_to_column(0) == 5); + CHECK(text_edit->is_caret_after_selection_origin(0)); + text_edit->remove_secondary_carets(); + text_edit->deselect(); + + // Merge equal overlapping selections. + text_edit->set_caret_line(0); + text_edit->set_caret_column(1); + text_edit->add_caret(0, 2); + text_edit->select(0, 2, 1, 6, 0); + text_edit->select(0, 2, 1, 6, 1); + text_edit->merge_overlapping_carets(); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_from_line(0) == 0); + CHECK(text_edit->get_selection_from_column(0) == 2); + CHECK(text_edit->get_selection_to_line(0) == 1); + CHECK(text_edit->get_selection_to_column(0) == 6); + CHECK(text_edit->is_caret_after_selection_origin(0)); + } + + SUBCASE("[TextEdit] collapse carets") { + text_edit->set_text("this is some text\nfor selection"); + + // Collapse carets in range, dont affect other carets. + text_edit->add_caret(0, 9); + text_edit->add_caret(1, 0); + text_edit->add_caret(1, 2); + text_edit->add_caret(1, 6); + text_edit->begin_multicaret_edit(); + + text_edit->collapse_carets(0, 8, 1, 2); + CHECK(text_edit->get_caret_count() == 5); + CHECK_FALSE(text_edit->has_selection()); + 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) == 8); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 8); + CHECK(text_edit->get_caret_line(3) == 1); + CHECK(text_edit->get_caret_column(3) == 2); + CHECK(text_edit->get_caret_line(4) == 1); + CHECK(text_edit->get_caret_column(4) == 6); + CHECK_FALSE(text_edit->multicaret_edit_ignore_caret(0)); + CHECK(text_edit->multicaret_edit_ignore_caret(1)); + CHECK(text_edit->multicaret_edit_ignore_caret(2)); + CHECK_FALSE(text_edit->multicaret_edit_ignore_caret(3)); + CHECK_FALSE(text_edit->multicaret_edit_ignore_caret(4)); + + // Collapsed carets get merged at the end of the edit. + text_edit->end_multicaret_edit(); + CHECK(text_edit->get_caret_count() == 4); + CHECK_FALSE(text_edit->has_selection()); + 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) == 8); + CHECK(text_edit->get_caret_line(2) == 1); + CHECK(text_edit->get_caret_column(2) == 2); + CHECK(text_edit->get_caret_line(3) == 1); + CHECK(text_edit->get_caret_column(3) == 6); + text_edit->remove_secondary_carets(); + + // Collapse inclusive. + text_edit->set_caret_line(0); + text_edit->set_caret_column(3); + text_edit->add_caret(1, 2); + text_edit->collapse_carets(0, 3, 1, 2, true); + CHECK(text_edit->get_caret_count() == 1); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 3); + text_edit->remove_secondary_carets(); + + // Deselect if selection was encompassed. + text_edit->select(0, 5, 0, 7); + text_edit->collapse_carets(0, 3, 1, 2); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 3); + + // Clamp only caret end of selection. + text_edit->select(0, 1, 0, 7); + text_edit->collapse_carets(0, 3, 1, 2); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 3); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 1); + text_edit->deselect(); + + // Clamp only selection origin end of selection. + text_edit->select(0, 7, 0, 1); + text_edit->collapse_carets(0, 3, 1, 2); + CHECK(text_edit->has_selection()); + CHECK(text_edit->get_caret_line() == 0); + CHECK(text_edit->get_caret_column() == 1); + CHECK(text_edit->get_selection_origin_line() == 0); + CHECK(text_edit->get_selection_origin_column() == 3); + text_edit->deselect(); } SUBCASE("[TextEdit] add caret at carets") { @@ -3694,36 +6906,320 @@ TEST_CASE("[SceneTree][TextEdit] multicaret") { text_edit->set_caret_line(1); text_edit->set_caret_column(9); + // Add caret below. Column will clamp. text_edit->add_caret_at_carets(true); CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 9); CHECK(text_edit->get_caret_line(1) == 2); CHECK(text_edit->get_caret_column(1) == 4); + // Cannot add below when at last line. text_edit->add_caret_at_carets(true); CHECK(text_edit->get_caret_count() == 2); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 9); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 4); + // Add caret above. Column will clamp. text_edit->add_caret_at_carets(false); CHECK(text_edit->get_caret_count() == 3); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 9); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 4); CHECK(text_edit->get_caret_line(2) == 0); CHECK(text_edit->get_caret_column(2) == 7); + // Cannot add above when at first line. + text_edit->add_caret_at_carets(false); + CHECK(text_edit->get_caret_count() == 3); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 9); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 7); + + // Cannot add below when at the last line for selection. + text_edit->remove_secondary_carets(); + text_edit->select(2, 1, 2, 4); + text_edit->add_caret_at_carets(true); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_origin_line(0) == 2); + CHECK(text_edit->get_selection_origin_column(0) == 1); + CHECK(text_edit->get_caret_line(0) == 2); + CHECK(text_edit->get_caret_column(0) == 4); + + // Cannot add above when at the first line for selection. + text_edit->select(0, 1, 0, 4); + text_edit->add_caret_at_carets(false); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + + // Add selection below. + text_edit->select(0, 0, 0, 4); + text_edit->add_caret_at_carets(true); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 0); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selection_origin_line(1) == 1); + CHECK(text_edit->get_selection_origin_column(1) == 0); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 3); // In the default font, this is the same position. + + // Add selection below again. + text_edit->add_caret_at_carets(true); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 0); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selection_origin_line(1) == 1); + CHECK(text_edit->get_selection_origin_column(1) == 0); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 3); + CHECK(text_edit->has_selection(2)); + CHECK(text_edit->get_selection_origin_line(2) == 2); + CHECK(text_edit->get_selection_origin_column(2) == 0); + CHECK(text_edit->get_caret_line(2) == 2); + CHECK(text_edit->get_caret_column(2) == 4); + + text_edit->set_text("\tthis is\nsome\n\ttest text"); + MessageQueue::get_singleton()->flush(); + + // Last fit x is preserved when adding below. + text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->set_caret_line(0); + text_edit->set_caret_column(6); + text_edit->add_caret_at_carets(true); + text_edit->add_caret_at_carets(true); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 6); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK(text_edit->get_caret_line(2) == 2); + CHECK(text_edit->get_caret_column(2) == 6); + + // Last fit x is preserved when adding above. + text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->set_caret_line(2); + text_edit->set_caret_column(9); + text_edit->add_caret_at_carets(false); + text_edit->add_caret_at_carets(false); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->get_caret_line(0) == 2); + CHECK(text_edit->get_caret_column(0) == 9); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 8); + + // Last fit x is preserved when selection adding below. + text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->select(0, 8, 0, 5); + text_edit->add_caret_at_carets(true); + text_edit->add_caret_at_carets(true); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 8); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK(text_edit->has_selection(2)); + CHECK(text_edit->get_selection_origin_line(2) == 2); + CHECK(text_edit->get_selection_origin_column(2) == 7); + CHECK(text_edit->get_caret_line(2) == 2); + CHECK(text_edit->get_caret_column(2) == 5); + + // Last fit x is preserved when selection adding above. text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->select(2, 9, 2, 5); + text_edit->add_caret_at_carets(false); + text_edit->add_caret_at_carets(false); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_origin_line(0) == 2); + CHECK(text_edit->get_selection_origin_column(0) == 9); + CHECK(text_edit->get_caret_line(0) == 2); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK_FALSE(text_edit->has_selection(1)); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK(text_edit->has_selection(2)); + CHECK(text_edit->get_selection_origin_line(2) == 0); + CHECK(text_edit->get_selection_origin_column(2) == 8); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 5); + + // Selections are merged when they overlap. + text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->select(0, 1, 0, 5); + text_edit->add_caret(1, 0); + text_edit->select(1, 1, 1, 3, 1); + text_edit->add_caret_at_carets(true); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selection_origin_line(1) == 1); + CHECK(text_edit->get_selection_origin_column(1) == 1); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 4); + CHECK(text_edit->has_selection(2)); + CHECK(text_edit->get_selection_origin_line(2) == 2); + CHECK(text_edit->get_selection_origin_column(2) == 0); + CHECK(text_edit->get_caret_line(2) == 2); + CHECK(text_edit->get_caret_column(2) == 3); + + // Multiline selection. + text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->set_caret_line(0); + text_edit->set_caret_column(1); + text_edit->select(0, 3, 1, 1); + text_edit->add_caret_at_carets(true); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->has_selection(0)); + CHECK(text_edit->get_selection_origin_line(0) == 0); + CHECK(text_edit->get_selection_origin_column(0) == 3); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 1); + CHECK(text_edit->has_selection(1)); + CHECK(text_edit->get_selection_origin_line(1) == 1); + CHECK(text_edit->get_selection_origin_column(1) == 3); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 0); + + text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY); + text_edit->set_size(Size2(50, 100)); + // Line wraps: `\t,this, is\nso,me\n\t,test, ,text`. + CHECK(text_edit->is_line_wrapped(0)); + MessageQueue::get_singleton()->flush(); + + // Add caret below on next line wrap. + text_edit->remove_secondary_carets(); + text_edit->deselect(); text_edit->set_caret_line(0); text_edit->set_caret_column(4); - text_edit->select(0, 0, 0, 4); text_edit->add_caret_at_carets(true); + CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_count() == 2); - CHECK(text_edit->get_selection_from_line(1) == 1); - CHECK(text_edit->get_selection_to_line(1) == 1); - CHECK(text_edit->get_selection_from_column(1) == 0); - CHECK(text_edit->get_selection_to_column(1) == 3); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 8); + // Add caret below from end of line wrap. text_edit->add_caret_at_carets(true); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 3); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_caret_line(1) == 0); + CHECK(text_edit->get_caret_column(1) == 8); + CHECK(text_edit->get_caret_line(2) == 1); + CHECK(text_edit->get_caret_column(2) == 1); + + // Add caret below from last line and not last line wrap. + text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->set_caret_line(2); + text_edit->set_caret_column(5); + text_edit->add_caret_at_carets(true); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_caret_line(0) == 2); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 10); + + // Cannot add caret below from last line last line wrap. + text_edit->add_caret_at_carets(true); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_caret_line(0) == 2); + CHECK(text_edit->get_caret_column(0) == 5); + CHECK(text_edit->get_caret_line(1) == 2); + CHECK(text_edit->get_caret_column(1) == 10); + + // Add caret above from not first line wrap. + text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->set_caret_line(1); + text_edit->set_caret_column(4); + text_edit->add_caret_at_carets(false); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 2); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 1); + + // Add caret above from first line wrap. + text_edit->add_caret_at_carets(false); + CHECK_FALSE(text_edit->has_selection()); CHECK(text_edit->get_caret_count() == 3); - CHECK(text_edit->get_selection_from_line(2) == 2); - CHECK(text_edit->get_selection_to_line(2) == 2); - CHECK(text_edit->get_selection_from_column(2) == 0); - CHECK(text_edit->get_selection_to_column(2) == 4); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 1); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 8); + + // Add caret above from first line and not first line wrap. + text_edit->add_caret_at_carets(false); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 4); + CHECK(text_edit->get_caret_line(0) == 1); + CHECK(text_edit->get_caret_column(0) == 4); + CHECK(text_edit->get_caret_line(1) == 1); + CHECK(text_edit->get_caret_column(1) == 1); + CHECK(text_edit->get_caret_line(2) == 0); + CHECK(text_edit->get_caret_column(2) == 8); + CHECK(text_edit->get_caret_line(3) == 0); + CHECK(text_edit->get_caret_column(3) == 4); + + // Cannot add caret above from first line first line wrap. + text_edit->remove_secondary_carets(); + text_edit->deselect(); + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + text_edit->add_caret_at_carets(false); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_caret_count() == 1); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); } memdelete(text_edit); @@ -3992,7 +7488,7 @@ TEST_CASE("[SceneTree][TextEdit] viewport") { CHECK(text_edit->get_last_full_visible_line() == visible_lines - 1); CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); - // Wrap + // Wrap. text_edit->set_line_wrapping_mode(TextEdit::LineWrappingMode::LINE_WRAPPING_BOUNDARY); MessageQueue::get_singleton()->flush(); CHECK(text_edit->get_total_visible_line_count() > total_visible_lines); @@ -4242,7 +7738,7 @@ TEST_CASE("[SceneTree][TextEdit] viewport") { CHECK(text_edit->get_last_full_visible_line_wrap_index() == 0); CHECK(text_edit->get_caret_wrap_index() == 0); - // Typing and undo / redo should adjust viewport + // Typing and undo / redo should adjust viewport. text_edit->set_caret_line(0); text_edit->set_caret_column(0); text_edit->set_line_as_first_visible(5); diff --git a/tests/test_macros.h b/tests/test_macros.h index a173b37a2d..927884dced 100644 --- a/tests/test_macros.h +++ b/tests/test_macros.h @@ -136,6 +136,7 @@ int register_test_command(String p_command, TestFunc p_function); // Requires Message Queue and InputMap to be setup. // SEND_GUI_ACTION - takes an input map key. e.g SEND_GUI_ACTION("ui_text_newline"). // SEND_GUI_KEY_EVENT - takes a keycode set. e.g SEND_GUI_KEY_EVENT(Key::A | KeyModifierMask::META). +// SEND_GUI_KEY_UP_EVENT - takes a keycode set. e.g SEND_GUI_KEY_UP_EVENT(Key::A | KeyModifierMask::META). // SEND_GUI_MOUSE_BUTTON_EVENT - takes a position, mouse button, mouse mask and modifiers e.g SEND_GUI_MOUSE_BUTTON_EVENT(Vector2(50, 50), MOUSE_BUTTON_NONE, MOUSE_BUTTON_NONE, Key::None); // SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT - takes a position, mouse button, mouse mask and modifiers e.g SEND_GUI_MOUSE_BUTTON_RELEASED_EVENT(Vector2(50, 50), MOUSE_BUTTON_NONE, MOUSE_BUTTON_NONE, Key::None); // SEND_GUI_MOUSE_MOTION_EVENT - takes a position, mouse mask and modifiers e.g SEND_GUI_MOUSE_MOTION_EVENT(Vector2(50, 50), MouseButtonMask::LEFT, KeyModifierMask::META); @@ -161,6 +162,14 @@ int register_test_command(String p_command, TestFunc p_function); MessageQueue::get_singleton()->flush(); \ } +#define SEND_GUI_KEY_UP_EVENT(m_input) \ + { \ + Ref<InputEventKey> event = InputEventKey::create_reference(m_input); \ + event->set_pressed(false); \ + _SEND_DISPLAYSERVER_EVENT(event); \ + MessageQueue::get_singleton()->flush(); \ + } + #define _UPDATE_EVENT_MODIFERS(m_event, m_modifers) \ m_event->set_shift_pressed(((m_modifers) & KeyModifierMask::SHIFT) != Key::NONE); \ m_event->set_alt_pressed(((m_modifers) & KeyModifierMask::ALT) != Key::NONE); \ diff --git a/thirdparty/mbedtls/include/godot_module_mbedtls_config.h b/thirdparty/mbedtls/include/godot_module_mbedtls_config.h index aed276766f..2011827b7a 100644 --- a/thirdparty/mbedtls/include/godot_module_mbedtls_config.h +++ b/thirdparty/mbedtls/include/godot_module_mbedtls_config.h @@ -49,8 +49,10 @@ #undef MBEDTLS_DES_C #undef MBEDTLS_DHM_C -#ifndef __linux__ +#if !(defined(__linux__) && defined(__aarch64__)) // ARMv8 hardware AES operations. Detection only possible on linux. +// May technically be supported on some ARM32 arches but doesn't seem +// to be in our current Linux SDK's neon-fp-armv8. #undef MBEDTLS_AESCE_C #endif |