summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/input/input_map.cpp22
-rw-r--r--doc/classes/ProjectSettings.xml12
-rw-r--r--doc/classes/TextEdit.xml7
-rw-r--r--scene/gui/text_edit.cpp119
-rw-r--r--scene/gui/text_edit.h4
-rw-r--r--tests/scene/test_text_edit.h39
6 files changed, 202 insertions, 1 deletions
diff --git a/core/input/input_map.cpp b/core/input/input_map.cpp
index 0c765fe23c..ea75a6b079 100644
--- a/core/input/input_map.cpp
+++ b/core/input/input_map.cpp
@@ -331,6 +331,10 @@ static const _BuiltinActionDisplayName _builtin_action_display_names[] = {
{ "ui_text_caret_document_start.macos", TTRC("Caret Document Start") },
{ "ui_text_caret_document_end", TTRC("Caret Document End") },
{ "ui_text_caret_document_end.macos", TTRC("Caret Document End") },
+ { "ui_text_caret_add_below", TTRC("Caret Add Below") },
+ { "ui_text_caret_add_below.macos", TTRC("Caret Add Below") },
+ { "ui_text_caret_add_above", TTRC("Caret Add Above") },
+ { "ui_text_caret_add_above.macos", TTRC("Caret Add Above") },
{ "ui_text_scroll_up", TTRC("Scroll Up") },
{ "ui_text_scroll_up.macos", TTRC("Scroll Up") },
{ "ui_text_scroll_down", TTRC("Scroll Down") },
@@ -617,6 +621,24 @@ const HashMap<String, List<Ref<InputEvent>>> &InputMap::get_builtins() {
inputs.push_back(InputEventKey::create_reference(Key::DOWN | KeyModifierMask::CMD_OR_CTRL));
default_builtin_cache.insert("ui_text_caret_document_end.macos", inputs);
+ // Text Caret Addition Below/Above
+
+ inputs = List<Ref<InputEvent>>();
+ inputs.push_back(InputEventKey::create_reference(Key::DOWN | KeyModifierMask::SHIFT | KeyModifierMask::CMD_OR_CTRL));
+ default_builtin_cache.insert("ui_text_caret_add_below", inputs);
+
+ inputs = List<Ref<InputEvent>>();
+ inputs.push_back(InputEventKey::create_reference(Key::L | KeyModifierMask::SHIFT | KeyModifierMask::CMD_OR_CTRL));
+ default_builtin_cache.insert("ui_text_caret_add_below.macos", inputs);
+
+ inputs = List<Ref<InputEvent>>();
+ inputs.push_back(InputEventKey::create_reference(Key::UP | KeyModifierMask::SHIFT | KeyModifierMask::CMD_OR_CTRL));
+ default_builtin_cache.insert("ui_text_caret_add_above", inputs);
+
+ inputs = List<Ref<InputEvent>>();
+ inputs.push_back(InputEventKey::create_reference(Key::O | KeyModifierMask::SHIFT | KeyModifierMask::CMD_OR_CTRL));
+ default_builtin_cache.insert("ui_text_caret_add_above.macos", inputs);
+
// Text Scrolling
inputs = List<Ref<InputEvent>>();
diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml
index 8da73f1b7d..925d4ec7c4 100644
--- a/doc/classes/ProjectSettings.xml
+++ b/doc/classes/ProjectSettings.xml
@@ -856,6 +856,18 @@
<member name="input/ui_text_backspace_word.macos" type="Dictionary" setter="" getter="">
macOS specific override for the shortcut to delete a word.
</member>
+ <member name="input/ui_text_caret_add_above" type="Dictionary" setter="" getter="">
+ Default [InputEventAction] to add an additional caret above every caret of a text
+ </member>
+ <member name="input/ui_text_caret_add_above.macos" type="Dictionary" setter="" getter="">
+ macOS specific override for the shortcut to add a caret above every caret
+ </member>
+ <member name="input/ui_text_caret_add_below" type="Dictionary" setter="" getter="">
+ Default [InputEventAction] to add an additional caret below every caret of a text
+ </member>
+ <member name="input/ui_text_caret_add_below.macos" type="Dictionary" setter="" getter="">
+ macOS specific override for the shortcut to add a caret below every caret
+ </member>
<member name="input/ui_text_caret_document_end" type="Dictionary" setter="" getter="">
Default [InputEventAction] to move the text cursor the the end of the text.
[b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified.
diff --git a/doc/classes/TextEdit.xml b/doc/classes/TextEdit.xml
index 879a355f25..2c64b78b49 100644
--- a/doc/classes/TextEdit.xml
+++ b/doc/classes/TextEdit.xml
@@ -63,6 +63,13 @@
Adds a new caret at the given location. Returns the index of the new caret, or [code]-1[/code] if the location is invalid.
</description>
</method>
+ <method name="add_caret_at_carets">
+ <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.
+ </description>
+ </method>
<method name="add_gutter">
<return type="void" />
<param index="0" name="at" type="int" default="-1" />
diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp
index 48853cde9c..8f9e2f88e1 100644
--- a/scene/gui/text_edit.cpp
+++ b/scene/gui/text_edit.cpp
@@ -2094,6 +2094,17 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) {
accept_event();
return;
}
+
+ if (k->is_action("ui_text_caret_add_below", true)) {
+ add_caret_at_carets(true);
+ accept_event();
+ return;
+ }
+ if (k->is_action("ui_text_caret_add_above", true)) {
+ add_caret_at_carets(false);
+ accept_event();
+ return;
+ }
}
// MISC.
@@ -2808,6 +2819,51 @@ void TextEdit::_move_caret_document_end(bool p_select) {
}
}
+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 (font.is_null() || font_size <= 0) {
return; // Not in tree?
@@ -4509,6 +4565,68 @@ int TextEdit::get_caret_count() const {
return carets.size();
}
+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);
+ } 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);
+ }
+ }
+
+ // 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);
+
+ // 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);
+ } else {
+ new_caret_from_column = caret_from_column;
+ new_caret_to_column = caret_to_column;
+ }
+ }
+
+ // Add the new caret
+ const int new_caret_index = add_caret(new_caret_line, new_caret_column);
+
+ if (new_caret_index == -1) {
+ continue;
+ }
+ // 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;
+ }
+
+ // Copy the last fit x over
+ carets.write[new_caret_index].last_fit_x = carets[caret_index].last_fit_x;
+ }
+
+ merge_overlapping_carets();
+ queue_redraw();
+}
+
Vector<int> TextEdit::get_caret_index_edit_order() {
if (!caret_index_edit_dirty) {
return caret_index_edit_order;
@@ -6003,6 +6121,7 @@ void TextEdit::_bind_methods() {
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);
diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h
index 86b838e387..e57f321887 100644
--- a/scene/gui/text_edit.h
+++ b/scene/gui/text_edit.h
@@ -598,6 +598,9 @@ private:
void _move_caret_document_start(bool p_select);
void _move_caret_document_end(bool p_select);
+ // 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);
@@ -816,6 +819,7 @@ public:
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);
diff --git a/tests/scene/test_text_edit.h b/tests/scene/test_text_edit.h
index 4b92b9daff..652cbed6e8 100644
--- a/tests/scene/test_text_edit.h
+++ b/tests/scene/test_text_edit.h
@@ -3353,7 +3353,7 @@ TEST_CASE("[SceneTree][TextEdit] caret") {
memdelete(text_edit);
}
-TEST_CASE("[SceneTree][TextEdit] muiticaret") {
+TEST_CASE("[SceneTree][TextEdit] multicaret") {
TextEdit *text_edit = memnew(TextEdit);
SceneTree::get_singleton()->get_root()->add_child(text_edit);
text_edit->set_multiple_carets_enabled(true);
@@ -3443,6 +3443,43 @@ TEST_CASE("[SceneTree][TextEdit] muiticaret") {
CHECK(text_edit->get_caret_index_edit_order() == caret_index_get_order);
}
+ SUBCASE("[TextEdit] add caret at carets") {
+ text_edit->remove_secondary_carets();
+ text_edit->set_caret_line(1);
+ text_edit->set_caret_column(9);
+
+ text_edit->add_caret_at_carets(true);
+ CHECK(text_edit->get_caret_count() == 2);
+ CHECK(text_edit->get_caret_line(1) == 2);
+ CHECK(text_edit->get_caret_column(1) == 4);
+
+ text_edit->add_caret_at_carets(true);
+ CHECK(text_edit->get_caret_count() == 2);
+
+ text_edit->add_caret_at_carets(false);
+ CHECK(text_edit->get_caret_count() == 3);
+ CHECK(text_edit->get_caret_line(2) == 0);
+ CHECK(text_edit->get_caret_column(2) == 7);
+
+ text_edit->remove_secondary_carets();
+ 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(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);
+
+ text_edit->add_caret_at_carets(true);
+ 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);
+ }
+
memdelete(text_edit);
}