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