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