2
0
Эх сурвалжийг харах

Add Caret Insert Below and Above shortcuts to TextEdit

PucklaMotzer09 2 жил өмнө
parent
commit
e5354cacd0

+ 22 - 0
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") },
@@ -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>>();

+ 12 - 0
doc/classes/ProjectSettings.xml

@@ -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.

+ 7 - 0
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" />

+ 119 - 0
scene/gui/text_edit.cpp

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

+ 4 - 0
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);

+ 38 - 1
tests/scene/test_text_edit.h

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