From 36d7c0df3ef231f8949c406db4003dd410f38777 Mon Sep 17 00:00:00 2001 From: Thomas Mitterfellner Date: Fri, 13 Dec 2024 23:24:05 +0100 Subject: [PATCH] Honor selected content upon input This addresses issue #214 of the upstream GitHub project: https://github.com/serso/android-calculatorpp/issues/214 When text is selected in the editor input field, pressing a key will now behave differently from when no text is selected. * When pressing a digit or pasting text, the selected text will be replaced with the cursor to the right of the inserted text. * When hitting backspace, the selected text will be erased without deleting the character left of the selection. * When cutting selected text, the cursor will stay at the start of the original selection. * When pressing an operator, the selected text will be put into parentheses and the cursor will be placed after these and the operator. * When pressing parentheses, the selected text will be put in parentheses and the cursor will be placed after them. The behavior with no text selected stays the same, i.e. the text cursor will be put inside of the parentheses when inserting them, backspace will erase the content left to the cursor. --- .../solovyev/android/calculator/Editor.java | 166 ++++++++++++++++-- 1 file changed, 148 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/org/solovyev/android/calculator/Editor.java b/app/src/main/java/org/solovyev/android/calculator/Editor.java index cbef5713..dd1667f1 100644 --- a/app/src/main/java/org/solovyev/android/calculator/Editor.java +++ b/app/src/main/java/org/solovyev/android/calculator/Editor.java @@ -98,6 +98,86 @@ protected void onPostExecute(@Nonnull EditorState state) { } } + /** + * Splits selected text into parts left, mid (selected) and right of selection. + */ + private class SplitText { + public int selectionStart = 0; + public int selectionEnd = 0; + public int selectionLength = 0; + public int insertionPos = 0; + public boolean textSelected = false; + public String textLeft = ""; + public String textMid = ""; + public String textRight = ""; + public String text = ""; + + /** + * SplitText constructor. + * + * @param text the content of the calculator's text input field. + * @param cursorPos the current position of the text cursor in the input field. + */ + public SplitText(String text, int cursorPos) { + this.text = text; + selectionStart = view.getSelectionStart(); + selectionEnd = view.getSelectionEnd(); + selectionLength = selectionEnd - selectionStart; + textSelected = selectionLength != 0; + insertionPos = textSelected ? + clamp(selectionStart, text) + : clamp(cursorPos, text); + textLeft = text.substring(0, insertionPos); + textMid = text.substring(selectionStart, selectionEnd); + textRight = text.substring(insertionPos + selectionLength, text.length()); + } + + /** + * Retrieves the middle part (=selection) of the split text. + * + * @param deleteSelection specifies whether the selection is to be deleted. + * @return the selected text or an empty string depending on deletion context. + */ + public String getTextMid(boolean deleteSelection) { + return deleteSelection ? "" : this.textMid; + } + + /** + * Retrieves the text left of the selection/cursor pos when deleting. + * + *

The left part of the text upon deletion depends on whether text + * is selected or not. For selected text, the left part is simply from + * the beginning of the text input to the beginning of the selection. + *

For no text selected, the deletion is a one-character deletion, + * so returns the string from the beginning of the text input to one + * character left of the cursor position. + *

A special case is given when the cursor position is to the right of + * a decimal grouping separator (e.g. a whitespace) in which case the + * grouping separator plus the digit left to it has to be deleted + * (because deleting only the grouping separator would restore it + * immediately after deletion). Thus, the string from the beginning + * to the original cursor position up until two characters left of it + * is returned. + * @return the left part of the text after a deletion operation. + */ + public String getDelTextLeft() { + MathType type = MathType.getType(text, insertionPos - 1, false, engine).type; + return this.textSelected ? + this.textLeft + : type == MathType.grouping_separator ? + this.textLeft.substring(0, Math.max(this.textLeft.length()-2, 0)) + : this.textLeft.substring(0, Math.max(this.textLeft.length()-1, 0)); + } + + /** + * Returns the cursor position after a deletion. + */ + public int getDelPos() { + return this.getDelTextLeft().length(); + } + } + + @VisibleForTesting @Nullable EditorTextProcessor textProcessor; @@ -238,22 +318,25 @@ public EditorState moveCursorRight() { return newSelectionViewState(state.selection + 1); } + /** + * Erases text in the input field upon pressing of the backspace button. + * + * @return whether the content of the text input is empty befor or after deletion. + */ public boolean erase() { Check.isMainThread(); - final int selection = state.selection; + final int delPos = state.selection; final String text = state.getTextString(); - if (selection <= 0 || text.length() <= 0 || selection > text.length()) { + final SplitText st = new SplitText(text, delPos); + + if (delPos <= 0 || text.length() <= 0 || delPos > text.length()) { return false; } - int removeStart = selection - 1; - if (MathType.getType(text, selection - 1, false, engine).type == MathType.grouping_separator) { - // we shouldn't remove just separator as it will be re-added after the evaluation is done. Remove the digit - // before - removeStart -= 1; - } - final String newText = text.substring(0, removeStart) + text.substring(selection, text.length()); - onTextChanged(EditorState.create(newText, removeStart)); + // For an erase operation with text selected that text will be deleted (mid part empty). + final String newText = st.getDelTextLeft() + st.getTextMid(true) + st.textRight; + onTextChanged(EditorState.create(newText, st.getDelPos())); + return !newText.isEmpty(); } @@ -264,7 +347,8 @@ public void clear() { public void setText(@Nonnull String text) { Check.isMainThread(); - onTextChanged(EditorState.create(text, text.length())); + final int cursorPos = view.getSelectionEnd(); + onTextChanged(EditorState.create(text, cursorPos)); } public void setText(@Nonnull String text, int selection) { @@ -277,17 +361,63 @@ public void insert(@Nonnull String text) { insert(text, 0); } - public void insert(@Nonnull String text, int selectionOffset) { + /** + * Inserts new content into the text input field. + * + *

This might be anything inserted via some function of the calculator + * input keys, e.g. a simple digit, parentheses, some function, something + * pasted from the clipboard etc. + * + * @param textToInsert the text to put into the input field at a certain position. + * @param cursorOffset an integer specifying whether to move the cursor after insertion. + */ + public void insert(@Nonnull String textToInsert, int cursorOffset) { Check.isMainThread(); - if (TextUtils.isEmpty(text) && selectionOffset == 0) { + if (TextUtils.isEmpty(textToInsert) && cursorOffset == 0) { return; } final String oldText = state.getTextString(); - final int selection = clamp(state.selection, oldText); - final int newTextLength = text.length() + oldText.length(); - final int newSelection = clamp(text.length() + selection + selectionOffset, newTextLength); - final String newText = oldText.substring(0, selection) + text + oldText.substring(selection); - onTextChanged(EditorState.create(newText, newSelection)); + final MathType type = MathType.getType(textToInsert, 0, false, engine).type; + final SplitText st = new SplitText(oldText, state.selection); + + boolean deleteSelection = false; + if (st.textSelected && type == MathType.digit) { + deleteSelection = true; + } + + if (st.textSelected && type == MathType.binary_operation) { + // Add parentheses to the left of the text to be inserted and prepare to move + // the cursor to inside of the parentheses, e.g. when pressing "^2", "+" etc. + // with text selected. At that position the _selected_ text will be inserted. + textToInsert = "()" + textToInsert; + cursorOffset = -textToInsert.length() + 1; + } + + final int insertedTextLength = textToInsert.length(); + // pluginPos is the position at which to plug in selected text in the string to be + // inserted, i.e. a "local" position (in contrast to a "global" cursor position in + // the text input field). + final int pluginPos = insertedTextLength + cursorOffset; + // For pluginPos == insertedTextLength the inserted text is split into a left + // and a right part. + final String insertLeft = textToInsert.substring(0, pluginPos); + final String insertRight = textToInsert.substring(pluginPos, insertedTextLength); + + final String textMid = st.getTextMid(deleteSelection); + // New content of text input field (with example strings in comments, assuming the + // text input field to contain "5*6+7*8" with "6+7" selected and "^2" pressed). + final String newText = st.textLeft // "5*" + + insertLeft // "(" + + textMid // "6+7" + + insertRight // ")^2" + + st.textRight; // "*8" => "5*(6+7)*8" + + // Example cursor position: after the parentheses and the operator, i.e. at: "5*(6+7)^2|*8". + int newCursorPos = st.textLeft.length() + insertLeft.length() + textMid.length(); + if (st.textSelected) newCursorPos += insertRight.length(); + newCursorPos = clamp(newCursorPos, newText); + + onTextChanged(EditorState.create(newText, newCursorPos)); } @Nonnull