Skip to content

Commit 36d7c0d

Browse files
committed
Honor selected content upon input
This addresses issue #214 of the upstream GitHub project: #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.
1 parent 8f9c970 commit 36d7c0d

File tree

1 file changed

+148
-18
lines changed
  • app/src/main/java/org/solovyev/android/calculator

1 file changed

+148
-18
lines changed

app/src/main/java/org/solovyev/android/calculator/Editor.java

Lines changed: 148 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,86 @@ protected void onPostExecute(@Nonnull EditorState state) {
9898
}
9999
}
100100

101+
/**
102+
* Splits selected text into parts left, mid (selected) and right of selection.
103+
*/
104+
private class SplitText {
105+
public int selectionStart = 0;
106+
public int selectionEnd = 0;
107+
public int selectionLength = 0;
108+
public int insertionPos = 0;
109+
public boolean textSelected = false;
110+
public String textLeft = "";
111+
public String textMid = "";
112+
public String textRight = "";
113+
public String text = "";
114+
115+
/**
116+
* SplitText constructor.
117+
*
118+
* @param text the content of the calculator's text input field.
119+
* @param cursorPos the current position of the text cursor in the input field.
120+
*/
121+
public SplitText(String text, int cursorPos) {
122+
this.text = text;
123+
selectionStart = view.getSelectionStart();
124+
selectionEnd = view.getSelectionEnd();
125+
selectionLength = selectionEnd - selectionStart;
126+
textSelected = selectionLength != 0;
127+
insertionPos = textSelected ?
128+
clamp(selectionStart, text)
129+
: clamp(cursorPos, text);
130+
textLeft = text.substring(0, insertionPos);
131+
textMid = text.substring(selectionStart, selectionEnd);
132+
textRight = text.substring(insertionPos + selectionLength, text.length());
133+
}
134+
135+
/**
136+
* Retrieves the middle part (=selection) of the split text.
137+
*
138+
* @param deleteSelection specifies whether the selection is to be deleted.
139+
* @return the selected text or an empty string depending on deletion context.
140+
*/
141+
public String getTextMid(boolean deleteSelection) {
142+
return deleteSelection ? "" : this.textMid;
143+
}
144+
145+
/**
146+
* Retrieves the text left of the selection/cursor pos when deleting.
147+
*
148+
* <p>The left part of the text upon deletion depends on whether text
149+
* is selected or not. For selected text, the left part is simply from
150+
* the beginning of the text input to the beginning of the selection.
151+
* <p>For no text selected, the deletion is a one-character deletion,
152+
* so returns the string from the beginning of the text input to one
153+
* character left of the cursor position.
154+
* <p>A special case is given when the cursor position is to the right of
155+
* a decimal grouping separator (e.g. a whitespace) in which case the
156+
* grouping separator plus the digit left to it has to be deleted
157+
* (because deleting only the grouping separator would restore it
158+
* immediately after deletion). Thus, the string from the beginning
159+
* to the original cursor position up until two characters left of it
160+
* is returned.
161+
* @return the left part of the text after a deletion operation.
162+
*/
163+
public String getDelTextLeft() {
164+
MathType type = MathType.getType(text, insertionPos - 1, false, engine).type;
165+
return this.textSelected ?
166+
this.textLeft
167+
: type == MathType.grouping_separator ?
168+
this.textLeft.substring(0, Math.max(this.textLeft.length()-2, 0))
169+
: this.textLeft.substring(0, Math.max(this.textLeft.length()-1, 0));
170+
}
171+
172+
/**
173+
* Returns the cursor position after a deletion.
174+
*/
175+
public int getDelPos() {
176+
return this.getDelTextLeft().length();
177+
}
178+
}
179+
180+
101181
@VisibleForTesting
102182
@Nullable
103183
EditorTextProcessor textProcessor;
@@ -238,22 +318,25 @@ public EditorState moveCursorRight() {
238318
return newSelectionViewState(state.selection + 1);
239319
}
240320

321+
/**
322+
* Erases text in the input field upon pressing of the backspace button.
323+
*
324+
* @return whether the content of the text input is empty befor or after deletion.
325+
*/
241326
public boolean erase() {
242327
Check.isMainThread();
243-
final int selection = state.selection;
328+
final int delPos = state.selection;
244329
final String text = state.getTextString();
245-
if (selection <= 0 || text.length() <= 0 || selection > text.length()) {
330+
final SplitText st = new SplitText(text, delPos);
331+
332+
if (delPos <= 0 || text.length() <= 0 || delPos > text.length()) {
246333
return false;
247334
}
248-
int removeStart = selection - 1;
249-
if (MathType.getType(text, selection - 1, false, engine).type == MathType.grouping_separator) {
250-
// we shouldn't remove just separator as it will be re-added after the evaluation is done. Remove the digit
251-
// before
252-
removeStart -= 1;
253-
}
254335

255-
final String newText = text.substring(0, removeStart) + text.substring(selection, text.length());
256-
onTextChanged(EditorState.create(newText, removeStart));
336+
// For an erase operation with text selected that text will be deleted (mid part empty).
337+
final String newText = st.getDelTextLeft() + st.getTextMid(true) + st.textRight;
338+
onTextChanged(EditorState.create(newText, st.getDelPos()));
339+
257340
return !newText.isEmpty();
258341
}
259342

@@ -264,7 +347,8 @@ public void clear() {
264347

265348
public void setText(@Nonnull String text) {
266349
Check.isMainThread();
267-
onTextChanged(EditorState.create(text, text.length()));
350+
final int cursorPos = view.getSelectionEnd();
351+
onTextChanged(EditorState.create(text, cursorPos));
268352
}
269353

270354
public void setText(@Nonnull String text, int selection) {
@@ -277,17 +361,63 @@ public void insert(@Nonnull String text) {
277361
insert(text, 0);
278362
}
279363

280-
public void insert(@Nonnull String text, int selectionOffset) {
364+
/**
365+
* Inserts new content into the text input field.
366+
*
367+
* <p>This might be anything inserted via some function of the calculator
368+
* input keys, e.g. a simple digit, parentheses, some function, something
369+
* pasted from the clipboard etc.
370+
*
371+
* @param textToInsert the text to put into the input field at a certain position.
372+
* @param cursorOffset an integer specifying whether to move the cursor after insertion.
373+
*/
374+
public void insert(@Nonnull String textToInsert, int cursorOffset) {
281375
Check.isMainThread();
282-
if (TextUtils.isEmpty(text) && selectionOffset == 0) {
376+
if (TextUtils.isEmpty(textToInsert) && cursorOffset == 0) {
283377
return;
284378
}
285379
final String oldText = state.getTextString();
286-
final int selection = clamp(state.selection, oldText);
287-
final int newTextLength = text.length() + oldText.length();
288-
final int newSelection = clamp(text.length() + selection + selectionOffset, newTextLength);
289-
final String newText = oldText.substring(0, selection) + text + oldText.substring(selection);
290-
onTextChanged(EditorState.create(newText, newSelection));
380+
final MathType type = MathType.getType(textToInsert, 0, false, engine).type;
381+
final SplitText st = new SplitText(oldText, state.selection);
382+
383+
boolean deleteSelection = false;
384+
if (st.textSelected && type == MathType.digit) {
385+
deleteSelection = true;
386+
}
387+
388+
if (st.textSelected && type == MathType.binary_operation) {
389+
// Add parentheses to the left of the text to be inserted and prepare to move
390+
// the cursor to inside of the parentheses, e.g. when pressing "^2", "+" etc.
391+
// with text selected. At that position the _selected_ text will be inserted.
392+
textToInsert = "()" + textToInsert;
393+
cursorOffset = -textToInsert.length() + 1;
394+
}
395+
396+
final int insertedTextLength = textToInsert.length();
397+
// pluginPos is the position at which to plug in selected text in the string to be
398+
// inserted, i.e. a "local" position (in contrast to a "global" cursor position in
399+
// the text input field).
400+
final int pluginPos = insertedTextLength + cursorOffset;
401+
// For pluginPos == insertedTextLength the inserted text is split into a left
402+
// and a right part.
403+
final String insertLeft = textToInsert.substring(0, pluginPos);
404+
final String insertRight = textToInsert.substring(pluginPos, insertedTextLength);
405+
406+
final String textMid = st.getTextMid(deleteSelection);
407+
// New content of text input field (with example strings in comments, assuming the
408+
// text input field to contain "5*6+7*8" with "6+7" selected and "^2" pressed).
409+
final String newText = st.textLeft // "5*"
410+
+ insertLeft // "("
411+
+ textMid // "6+7"
412+
+ insertRight // ")^2"
413+
+ st.textRight; // "*8" => "5*(6+7)*8"
414+
415+
// Example cursor position: after the parentheses and the operator, i.e. at: "5*(6+7)^2|*8".
416+
int newCursorPos = st.textLeft.length() + insertLeft.length() + textMid.length();
417+
if (st.textSelected) newCursorPos += insertRight.length();
418+
newCursorPos = clamp(newCursorPos, newText);
419+
420+
onTextChanged(EditorState.create(newText, newCursorPos));
291421
}
292422

293423
@Nonnull

0 commit comments

Comments
 (0)