Add to TextEdit END key state support

When the caret is set to the end of a line through `ui_text_caret_line_end`
an internal state is kept in order to always keep the caret at the end
of each line while is being moved through the different lines of the text.
Also add a unit test for `CodeEdit` to check if END key state is kept on
folding.
Also preventing a SIGEV when `SEND_GUI_ACTION` is called with a non existing
action.
This commit is contained in:
Pablo Andres Fuente 2024-10-05 00:40:17 -03:00
parent db66bd35af
commit 1f27fd59f4
5 changed files with 199 additions and 9 deletions

View file

@ -2551,7 +2551,7 @@ void TextEdit::_move_caret_up(bool p_select) {
int cur_wrap_index = get_caret_wrap_index(i);
if (cur_wrap_index > 0) {
set_caret_line(get_caret_line(i), true, false, cur_wrap_index - 1, i);
} else if (get_caret_line(i) == 0) {
} else if (get_caret_line(i) == 0 && !_is_caret_at_eol(i)) {
set_caret_column(0, i == 0, i);
} else {
int new_line = get_caret_line(i) - get_next_visible_line_offset_from(get_caret_line(i) - 1, -1);
@ -2577,7 +2577,7 @@ void TextEdit::_move_caret_down(bool p_select) {
int cur_wrap_index = get_caret_wrap_index(i);
if (cur_wrap_index < get_line_wrap_count(get_caret_line(i))) {
set_caret_line(get_caret_line(i), i == 0, false, cur_wrap_index + 1, i);
} else if (get_caret_line(i) == get_last_unhidden_line()) {
} else if (get_caret_line(i) == get_last_unhidden_line() && !_is_caret_at_eol(i)) {
set_caret_column(text[get_caret_line(i)].length());
} else {
int new_line = get_caret_line(i) + get_next_visible_line_offset_from(CLAMP(get_caret_line(i) + 1, 0, text.size() - 1), 1);
@ -2639,6 +2639,9 @@ void TextEdit::_move_caret_to_line_end(bool p_select) {
} else {
set_caret_column(row_end_col, i == 0, i);
}
// Setting this to flag the caret is at the end of line.
carets.write[i].last_fit_x = INT_MAX;
}
merge_overlapping_carets();
}
@ -2841,7 +2844,9 @@ void TextEdit::_move_caret_document_end(bool p_select) {
}
set_caret_line(get_last_unhidden_line(), true, false, -1);
set_caret_column(text[get_caret_line()].length());
if (!_is_caret_at_eol(0)) {
set_caret_column(text[get_caret_line()].length());
}
}
bool TextEdit::_clear_carets_and_selection() {
@ -4896,7 +4901,9 @@ void TextEdit::collapse_carets(int p_from_line, int p_from_column, int p_to_line
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_caret_at_eol(i)) {
set_caret_column(collapse_column, false, i);
}
if (is_in_mulitcaret_edit() && get_caret_count() > 1) {
multicaret_edit_ignore_carets.insert(i);
}
@ -4909,7 +4916,9 @@ void TextEdit::collapse_carets(int p_from_line, int p_from_column, int p_to_line
// Selection was completely encapsulated.
deselect(i);
set_caret_line(collapse_line, false, true, -1, i);
set_caret_column(collapse_column, false, i);
if (!_is_caret_at_eol(i)) {
set_caret_column(collapse_column, false, i);
}
if (is_in_mulitcaret_edit() && get_caret_count() > 1) {
multicaret_edit_ignore_carets.insert(i);
}
@ -4917,7 +4926,9 @@ void TextEdit::collapse_carets(int p_from_line, int p_from_column, int p_to_line
} 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);
if (!_is_caret_at_eol(i)) {
set_caret_column(collapse_column, false, i);
}
any_collapsed = true;
} else if (is_origin_in) {
// Only selection origin was inside.
@ -5116,8 +5127,13 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_
}
}
} else {
// Clamp the column.
n_col = MIN(get_caret_column(p_caret), get_line(p_line).length());
// Keep the caret at the end of the line.
if (_is_caret_at_eol(p_caret)) {
n_col = get_line(p_line).length();
} 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;
@ -7595,7 +7611,7 @@ int TextEdit::_get_char_pos_for_line(int p_px, int p_line, int p_wrap_index) con
if (is_layout_rtl()) {
p_px = TS->shaped_text_get_size(text_rid).x - p_px + wrap_indent;
} else {
p_px -= wrap_indent;
p_px -= (int)wrap_indent;
}
int ofs = TS->shaped_text_hit_test_position(text_rid, p_px);
if (!caret_mid_grapheme_enabled) {
@ -7745,6 +7761,10 @@ void TextEdit::_cancel_drag_and_drop_text() {
}
}
bool TextEdit::_is_caret_at_eol(int p_caret) {
return carets[p_caret].last_fit_x == INT_MAX;
}
/* Selection */
void TextEdit::_selection_changed(int p_caret) {
if (!selecting_enabled) {

View file

@ -461,6 +461,8 @@ private:
void _cancel_drag_and_drop_text();
bool _is_caret_at_eol(int p_caret = 0);
/* Selection. */
SelectionMode selecting_mode = SelectionMode::SELECTION_MODE_NONE;

View file

@ -5674,6 +5674,44 @@ func _ready():
memdelete(code_edit);
}
TEST_CASE("[SceneTree][CodeEdit] End key state behavior on folded block") {
const String text = R"(extends Node
func some_function(param1, param2, param3):
local_const = 5
if param1 < local_const:
print(">" + param1)
elif param2 > 5:
print(param2)
else:
print("Fail!")
print("No!")
for i in range(20):
print("line longer than for loop")
print(i)
)";
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);
code_edit->set_text(text);
code_edit->set_caret_line(15); // print(i)
code_edit->set_caret_column(0);
SEND_GUI_ACTION("ui_text_caret_line_end");
code_edit->fold_line(13); // for i in range(20):
CHECK(code_edit->get_caret_column() == code_edit->get_line(13).length()); // for i in range(20):
SEND_GUI_ACTION("ui_text_caret_up");
SEND_GUI_ACTION("ui_text_caret_up");
SEND_GUI_ACTION("ui_text_caret_up");
CHECK(code_edit->get_caret_column() == code_edit->get_line(10).length()); // print("Fail!")
memdelete(code_edit);
}
} // namespace TestCodeEdit
#endif // TEST_CODE_EDIT_H

View file

@ -8157,6 +8157,132 @@ TEST_CASE("[SceneTree][TextEdit] gutters") {
memdelete(text_edit);
}
TEST_CASE("[SceneTree][TextEdit] End Key State") {
const String text = R"(extends Node
func some_function(param1, param2, param3):
local_const = 5
if param1 < local_const:
print(">" + param1)
elif param2 > 5:
print(param2)
else:
print("Fail!")
print("No!")
for i in range(20):
print("line longer than for loop")
print(i)
)";
TextEdit *text_edit = memnew(TextEdit);
SceneTree::get_singleton()->get_root()->add_child(text_edit);
text_edit->grab_focus();
text_edit->set_text(text);
text_edit->set_caret_line(6); // print(">" + param1)
text_edit->set_caret_column(0);
SUBCASE("End key state") {
SEND_GUI_ACTION("ui_text_caret_line_end");
CHECK(text_edit->get_caret_column() == text_edit->get_line(6).length()); // print(">" + param1)
// Go down to a shorter line.
SEND_GUI_ACTION("ui_text_caret_down");
CHECK(text_edit->get_caret_column() == text_edit->get_line(7).length()); // elif param2 > 5:
// Go up to a longer line.
SEND_GUI_ACTION("ui_text_caret_up");
SEND_GUI_ACTION("ui_text_caret_up");
SEND_GUI_ACTION("ui_text_caret_up");
CHECK(text_edit->get_caret_column() == text_edit->get_line(4).length()); // if param1 < local_const:
// Go up and go through an empty line
SEND_GUI_ACTION("ui_text_caret_up");
CHECK(text_edit->get_caret_column() == text_edit->get_line(3).length()); // empty line
SEND_GUI_ACTION("ui_text_caret_up");
CHECK(text_edit->get_caret_column() == text_edit->get_line(2).length()); // local_const = 5
}
SUBCASE("Reaching the end of line doesn't set end key state") {
text_edit->set_caret_line(11); // print("No!")
// print(i) is 14 characters long, so set the caret to the 13th character.
text_edit->set_caret_column(13);
SEND_GUI_ACTION("ui_text_caret_right");
CHECK(text_edit->get_caret_column() == text_edit->get_line(11).length()); // print("No!")
SEND_GUI_ACTION("ui_text_caret_up");
CHECK(text_edit->get_caret_column() == 15); // print("Fail!")
}
SUBCASE("Keep end key state even when reach top line") {
text_edit->set_caret_line(2); // func some_function(param1, param2, param3):
SEND_GUI_ACTION("ui_text_caret_line_end");
SEND_GUI_ACTION("ui_text_caret_up");
SEND_GUI_ACTION("ui_text_caret_up");
ERR_PRINT_OFF;
SEND_GUI_ACTION("ui_text_caret_up");
SEND_GUI_ACTION("ui_text_caret_up");
ERR_PRINT_ON;
SEND_GUI_ACTION("ui_text_caret_down");
SEND_GUI_ACTION("ui_text_caret_down");
CHECK(text_edit->get_caret_column() == text_edit->get_line(2).length()); // func some_function(param1, param2, param3):
}
SUBCASE("Keep end key state even when reach bottom line") {
text_edit->set_caret_line(15); // print(i)
SEND_GUI_ACTION("ui_text_caret_line_end");
SEND_GUI_ACTION("ui_text_caret_down"); // Empty line at the end
SEND_GUI_ACTION("ui_text_caret_down"); // Empty line at the end
SEND_GUI_ACTION("ui_text_caret_up"); // print(i)
SEND_GUI_ACTION("ui_text_caret_up"); // print("line longer than for loop")
CHECK(text_edit->get_caret_line() == 14); // print("line longer than for loop")
CHECK(text_edit->get_caret_column() == text_edit->get_line(14).length()); // print("line longer than for loop")
}
SUBCASE("Clear end key state when go to document start") {
text_edit->set_caret_line(9); // else:
SEND_GUI_ACTION("ui_text_caret_line_end");
CHECK(text_edit->get_caret_column() == text_edit->get_line(9).length()); // else:
SEND_GUI_ACTION("ui_text_caret_document_start");
CHECK(text_edit->get_caret_column() == 0);
}
SUBCASE("Keep end key state when go to document end") {
text_edit->set_caret_line(9); // else:
SEND_GUI_ACTION("ui_text_caret_line_end");
CHECK(text_edit->get_caret_column() == text_edit->get_line(9).length()); // else:
SEND_GUI_ACTION("ui_text_caret_document_end"); // Empty line at the end
SEND_GUI_ACTION("ui_text_caret_up"); // print(i)
CHECK(text_edit->get_caret_column() == text_edit->get_line(15).length()); // print(i)
}
SUBCASE("Clear end key state") {
text_edit->set_caret_line(13); // for i in range(20):
SEND_GUI_ACTION("ui_text_caret_line_end");
CHECK(text_edit->get_caret_column() == text_edit->get_line(13).length()); // for i in range(20):
SEND_GUI_ACTION("ui_text_caret_left");
SEND_GUI_ACTION("ui_text_caret_down");
CHECK(text_edit->get_caret_line() == 14); // print("line longer than for loop")
CHECK(text_edit->get_caret_column() != text_edit->get_line(14).length()); // print("line longer than for loop")
}
SUBCASE("Moving caret to right clear end key state") {
SEND_GUI_ACTION("ui_text_caret_line_end");
CHECK(text_edit->get_caret_column() == text_edit->get_line(6).length()); // print(">" + param1)
SEND_GUI_ACTION("ui_text_caret_right");
// Wraps to next line
CHECK(text_edit->get_caret_line() == 7); // elif param2 > 5:
CHECK(text_edit->get_caret_column() == 0); // elif param2 > 5:
SEND_GUI_ACTION("ui_text_caret_up");
CHECK(text_edit->get_caret_line() == 6); // print(">" + param1)
CHECK(text_edit->get_caret_column() == 0); // print(">" + param1)
}
memdelete(text_edit);
}
} // namespace TestTextEdit
#endif // TEST_TEXT_EDIT_H

View file

@ -148,6 +148,10 @@ int register_test_command(String p_command, TestFunc p_function);
#define SEND_GUI_ACTION(m_action) \
{ \
const List<Ref<InputEvent>> *events = InputMap::get_singleton()->action_get_events(m_action); \
if (events == nullptr) { \
FAIL("Invalid " #m_action " action"); \
return; \
} \
const List<Ref<InputEvent>>::Element *first_event = events->front(); \
Ref<InputEventKey> event = first_event->get()->duplicate(); \
event->set_pressed(true); \