Add Caret Insert Below and Above shortcuts to TextEdit

This commit is contained in:
PucklaMotzer09 2022-10-09 17:07:42 +02:00
parent 39534a7aec
commit e5354cacd0
6 changed files with 202 additions and 1 deletions

View file

@ -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") },
@ -616,6 +620,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>>();

View file

@ -838,6 +838,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.

View file

@ -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" />

View file

@ -2089,6 +2089,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.
@ -2803,6 +2814,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?
@ -4504,6 +4560,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;
@ -5959,6 +6077,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);

View file

@ -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);

View file

@ -3313,7 +3313,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);
@ -3403,6 +3403,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);
}