Skip to content

Honor selected content upon input (Issue #214) #235

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 148 additions & 18 deletions app/src/main/java/org/solovyev/android/calculator/Editor.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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.
* <p>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.
* <p>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;
Expand Down Expand Up @@ -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();
}

Expand All @@ -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) {
Expand All @@ -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.
*
* <p>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
Expand Down