1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.text;
18 
19 import android.annotation.NonNull;
20 import android.annotation.TestApi;
21 import android.compat.annotation.UnsupportedAppUsage;
22 
23 import java.text.BreakIterator;
24 
25 
26 /**
27  * Utility class for manipulating cursors and selections in CharSequences.
28  * A cursor is a selection where the start and end are at the same offset.
29  */
30 public class Selection {
Selection()31     private Selection() { /* cannot be instantiated */ }
32 
33     /*
34      * Retrieving the selection
35      */
36 
37     /**
38      * Return the offset of the selection anchor or cursor, or -1 if
39      * there is no selection or cursor.
40      */
getSelectionStart(CharSequence text)41     public static final int getSelectionStart(CharSequence text) {
42         if (text instanceof Spanned) {
43             return ((Spanned) text).getSpanStart(SELECTION_START);
44         }
45         return -1;
46     }
47 
48     /**
49      * Return the offset of the selection edge or cursor, or -1 if
50      * there is no selection or cursor.
51      */
getSelectionEnd(CharSequence text)52     public static final int getSelectionEnd(CharSequence text) {
53         if (text instanceof Spanned) {
54             return ((Spanned) text).getSpanStart(SELECTION_END);
55         }
56         return -1;
57     }
58 
getSelectionMemory(CharSequence text)59     private static int getSelectionMemory(CharSequence text) {
60         if (text instanceof Spanned) {
61             return ((Spanned) text).getSpanStart(SELECTION_MEMORY);
62         }
63         return -1;
64     }
65 
66     /*
67      * Setting the selection
68      */
69 
70     // private static int pin(int value, int min, int max) {
71     //     return value < min ? 0 : (value > max ? max : value);
72     // }
73 
74     /**
75      * Set the selection anchor to <code>start</code> and the selection edge
76      * to <code>stop</code>.
77      */
setSelection(Spannable text, int start, int stop)78     public static void setSelection(Spannable text, int start, int stop) {
79         setSelection(text, start, stop, -1);
80     }
81 
82     /**
83      * Set the selection anchor to <code>start</code>, the selection edge
84      * to <code>stop</code> and the memory horizontal to <code>memory</code>.
85      */
setSelection(Spannable text, int start, int stop, int memory)86     private static void setSelection(Spannable text, int start, int stop, int memory) {
87         // int len = text.length();
88         // start = pin(start, 0, len);  XXX remove unless we really need it
89         // stop = pin(stop, 0, len);
90 
91         int ostart = getSelectionStart(text);
92         int oend = getSelectionEnd(text);
93 
94         if (ostart != start || oend != stop) {
95             text.setSpan(SELECTION_START, start, start,
96                     Spanned.SPAN_POINT_POINT | Spanned.SPAN_INTERMEDIATE);
97             text.setSpan(SELECTION_END, stop, stop, Spanned.SPAN_POINT_POINT);
98             updateMemory(text, memory);
99         }
100     }
101 
102     /**
103      * Update the memory position for text. This is used to ensure vertical navigation of lines
104      * with different lengths behaves as expected and remembers the longest horizontal position
105      * seen during a vertical traversal.
106      */
updateMemory(Spannable text, int memory)107     private static void updateMemory(Spannable text, int memory) {
108         if (memory > -1) {
109             int currentMemory = getSelectionMemory(text);
110             if (memory != currentMemory) {
111                 text.setSpan(SELECTION_MEMORY, memory, memory, Spanned.SPAN_POINT_POINT);
112                 if (currentMemory == -1) {
113                     // This is the first value, create a watcher.
114                     final TextWatcher watcher = new MemoryTextWatcher();
115                     text.setSpan(watcher, 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
116                 }
117             }
118         } else {
119             removeMemory(text);
120         }
121     }
122 
removeMemory(Spannable text)123     private static void removeMemory(Spannable text) {
124         text.removeSpan(SELECTION_MEMORY);
125         MemoryTextWatcher[] watchers = text.getSpans(0, text.length(), MemoryTextWatcher.class);
126         for (MemoryTextWatcher watcher : watchers) {
127             text.removeSpan(watcher);
128         }
129     }
130 
131     /**
132      * @hide
133      */
134     @TestApi
135     public static final class MemoryTextWatcher implements TextWatcher {
136 
137         @Override
beforeTextChanged(CharSequence s, int start, int count, int after)138         public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
139 
140         @Override
onTextChanged(CharSequence s, int start, int before, int count)141         public void onTextChanged(CharSequence s, int start, int before, int count) {}
142 
143         @Override
afterTextChanged(Editable s)144         public void afterTextChanged(Editable s) {
145             s.removeSpan(SELECTION_MEMORY);
146             s.removeSpan(this);
147         }
148     }
149 
150     /**
151      * Move the cursor to offset <code>index</code>.
152      */
setSelection(Spannable text, int index)153     public static final void setSelection(Spannable text, int index) {
154         setSelection(text, index, index);
155     }
156 
157     /**
158      * Select the entire text.
159      */
selectAll(Spannable text)160     public static final void selectAll(Spannable text) {
161         setSelection(text, 0, text.length());
162     }
163 
164     /**
165      * Move the selection edge to offset <code>index</code>.
166      */
extendSelection(Spannable text, int index)167     public static final void extendSelection(Spannable text, int index) {
168         extendSelection(text, index, -1);
169     }
170 
171     /**
172      * Move the selection edge to offset <code>index</code> and update the memory horizontal.
173      */
extendSelection(Spannable text, int index, int memory)174     private static void extendSelection(Spannable text, int index, int memory) {
175         if (text.getSpanStart(SELECTION_END) != index) {
176             text.setSpan(SELECTION_END, index, index, Spanned.SPAN_POINT_POINT);
177         }
178         updateMemory(text, memory);
179     }
180 
181     /**
182      * Remove the selection or cursor, if any, from the text.
183      */
removeSelection(Spannable text)184     public static final void removeSelection(Spannable text) {
185         text.removeSpan(SELECTION_START, Spanned.SPAN_INTERMEDIATE);
186         text.removeSpan(SELECTION_END);
187         removeMemory(text);
188     }
189 
190     /*
191      * Moving the selection within the layout
192      */
193 
194     /**
195      * Move the cursor to the buffer offset physically above the current
196      * offset, to the beginning if it is on the top line but not at the
197      * start, or return false if the cursor is already on the top line.
198      */
moveUp(Spannable text, Layout layout)199     public static boolean moveUp(Spannable text, Layout layout) {
200         int start = getSelectionStart(text);
201         int end = getSelectionEnd(text);
202 
203         if (start != end) {
204             int min = Math.min(start, end);
205             int max = Math.max(start, end);
206 
207             setSelection(text, min);
208 
209             if (min == 0 && max == text.length()) {
210                 return false;
211             }
212 
213             return true;
214         } else {
215             int line = layout.getLineForOffset(end);
216 
217             if (line > 0) {
218                 setSelectionAndMemory(
219                         text, layout, line, end, -1 /* direction */, false /* extend */);
220                 return true;
221             } else if (end != 0) {
222                 setSelection(text, 0);
223                 return true;
224             }
225         }
226 
227         return false;
228     }
229 
230     /**
231      * Calculate the movement and memory positions needed, and set or extend the selection.
232      */
setSelectionAndMemory(Spannable text, Layout layout, int line, int end, int direction, boolean extend)233     private static void setSelectionAndMemory(Spannable text, Layout layout, int line, int end,
234             int direction, boolean extend) {
235         int move;
236         int newMemory;
237 
238         if (layout.getParagraphDirection(line)
239                 == layout.getParagraphDirection(line + direction)) {
240             int memory = getSelectionMemory(text);
241             if (memory > -1) {
242                 // We have a memory position
243                 float h = layout.getPrimaryHorizontal(memory);
244                 move = layout.getOffsetForHorizontal(line + direction, h);
245                 newMemory = memory;
246             } else {
247                 // Create a new memory position
248                 float h = layout.getPrimaryHorizontal(end);
249                 move = layout.getOffsetForHorizontal(line + direction, h);
250                 newMemory = end;
251             }
252         } else {
253             move = layout.getLineStart(line + direction);
254             newMemory = -1;
255         }
256 
257         if (extend) {
258             extendSelection(text, move, newMemory);
259         } else {
260             setSelection(text, move, move, newMemory);
261         }
262     }
263 
264     /**
265      * Move the cursor to the buffer offset physically below the current
266      * offset, to the end of the buffer if it is on the bottom line but
267      * not at the end, or return false if the cursor is already at the
268      * end of the buffer.
269      */
moveDown(Spannable text, Layout layout)270     public static boolean moveDown(Spannable text, Layout layout) {
271         int start = getSelectionStart(text);
272         int end = getSelectionEnd(text);
273 
274         if (start != end) {
275             int min = Math.min(start, end);
276             int max = Math.max(start, end);
277 
278             setSelection(text, max);
279 
280             if (min == 0 && max == text.length()) {
281                 return false;
282             }
283 
284             return true;
285         } else {
286             int line = layout.getLineForOffset(end);
287 
288             if (line < layout.getLineCount() - 1) {
289                 setSelectionAndMemory(
290                         text, layout, line, end, 1 /* direction */, false /* extend */);
291                 return true;
292             } else if (end != text.length()) {
293                 setSelection(text, text.length());
294                 return true;
295             }
296         }
297 
298         return false;
299     }
300 
301     /**
302      * Move the cursor to the buffer offset physically to the left of
303      * the current offset, or return false if the cursor is already
304      * at the left edge of the line and there is not another line to move it to.
305      */
moveLeft(Spannable text, Layout layout)306     public static boolean moveLeft(Spannable text, Layout layout) {
307         int start = getSelectionStart(text);
308         int end = getSelectionEnd(text);
309 
310         if (start != end) {
311             setSelection(text, chooseHorizontal(layout, -1, start, end));
312             return true;
313         } else {
314             int to = layout.getOffsetToLeftOf(end);
315 
316             if (to != end) {
317                 setSelection(text, to);
318                 return true;
319             }
320         }
321 
322         return false;
323     }
324 
325     /**
326      * Move the cursor to the buffer offset physically to the right of
327      * the current offset, or return false if the cursor is already at
328      * at the right edge of the line and there is not another line
329      * to move it to.
330      */
moveRight(Spannable text, Layout layout)331     public static boolean moveRight(Spannable text, Layout layout) {
332         int start = getSelectionStart(text);
333         int end = getSelectionEnd(text);
334 
335         if (start != end) {
336             setSelection(text, chooseHorizontal(layout, 1, start, end));
337             return true;
338         } else {
339             int to = layout.getOffsetToRightOf(end);
340 
341             if (to != end) {
342                 setSelection(text, to);
343                 return true;
344             }
345         }
346 
347         return false;
348     }
349 
350     private static final char PARAGRAPH_SEPARATOR = '\n';
351 
352     /**
353      * Move the cusrot to the closest paragraph start offset.
354      *
355      * @param text the spannable text
356      * @param layout layout to be used for drawing.
357      * @return true if the cursor is moved, otherwise false.
358      */
moveToParagraphStart(@onNull Spannable text, @NonNull Layout layout)359     public static boolean moveToParagraphStart(@NonNull Spannable text, @NonNull Layout layout) {
360         int start = getSelectionStart(text);
361         int end = getSelectionEnd(text);
362 
363         if (start != end) {
364             setSelection(text, chooseHorizontal(layout, -1, start, end));
365             return true;
366         } else {
367             int to = TextUtils.lastIndexOf(text, PARAGRAPH_SEPARATOR, start - 1);
368             if (to == -1) {
369                 to = 0;  // If not found, use the document start offset as a paragraph start.
370             }
371             if (to != end) {
372                 setSelection(text, to);
373                 return true;
374             }
375         }
376         return false;
377     }
378 
379     /**
380      * Move the cursor to the closest paragraph end offset.
381      *
382      * @param text the spannable text
383      * @param layout layout to be used for drawing.
384      * @return true if the cursor is moved, otherwise false.
385      */
moveToParagraphEnd(@onNull Spannable text, @NonNull Layout layout)386     public static boolean moveToParagraphEnd(@NonNull Spannable text, @NonNull Layout layout) {
387         int start = getSelectionStart(text);
388         int end = getSelectionEnd(text);
389 
390         if (start != end) {
391             setSelection(text, chooseHorizontal(layout, 1, start, end));
392             return true;
393         } else {
394             int to = TextUtils.indexOf(text, PARAGRAPH_SEPARATOR, end + 1);
395             if (to == -1) {
396                 to = text.length();
397             }
398             if (to != end) {
399                 setSelection(text, to);
400                 return true;
401             }
402         }
403         return false;
404     }
405 
406     /**
407      * Extend the selection to the closest paragraph start offset.
408      *
409      * @param text the spannable text
410      * @return true if the selection is extended, otherwise false
411      */
extendToParagraphStart(@onNull Spannable text)412     public static boolean extendToParagraphStart(@NonNull Spannable text) {
413         int end = getSelectionEnd(text);
414         int to = TextUtils.lastIndexOf(text, PARAGRAPH_SEPARATOR, end - 1);
415         if (to == -1) {
416             to = 0;  // If not found, use the document start offset as a paragraph start.
417         }
418         if (to != end) {
419             extendSelection(text, to);
420             return true;
421         }
422         return false;
423     }
424 
425     /**
426      * Extend the selection to the closest paragraph end offset.
427      *
428      * @param text the spannable text
429      * @return true if the selection is extended, otherwise false
430      */
extendToParagraphEnd(@onNull Spannable text)431     public static boolean extendToParagraphEnd(@NonNull Spannable text) {
432         int end = getSelectionEnd(text);
433         int to = TextUtils.indexOf(text, PARAGRAPH_SEPARATOR, end + 1);
434         if (to == -1) {
435             to = text.length();
436         }
437         if (to != end) {
438             extendSelection(text, to);
439             return true;
440         }
441         return false;
442     }
443 
444     /**
445      * Move the selection end to the buffer offset physically above
446      * the current selection end.
447      */
extendUp(Spannable text, Layout layout)448     public static boolean extendUp(Spannable text, Layout layout) {
449         int end = getSelectionEnd(text);
450         int line = layout.getLineForOffset(end);
451 
452         if (line > 0) {
453             setSelectionAndMemory(text, layout, line, end, -1 /* direction */, true /* extend */);
454             return true;
455         } else if (end != 0) {
456             extendSelection(text, 0);
457             return true;
458         }
459 
460         return true;
461     }
462 
463     /**
464      * Move the selection end to the buffer offset physically below
465      * the current selection end.
466      */
extendDown(Spannable text, Layout layout)467     public static boolean extendDown(Spannable text, Layout layout) {
468         int end = getSelectionEnd(text);
469         int line = layout.getLineForOffset(end);
470 
471         if (line < layout.getLineCount() - 1) {
472             setSelectionAndMemory(text, layout, line, end, 1 /* direction */, true /* extend */);
473             return true;
474         } else if (end != text.length()) {
475             extendSelection(text, text.length(), -1);
476             return true;
477         }
478 
479         return true;
480     }
481 
482     /**
483      * Move the selection end to the buffer offset physically to the left of
484      * the current selection end.
485      */
extendLeft(Spannable text, Layout layout)486     public static boolean extendLeft(Spannable text, Layout layout) {
487         int end = getSelectionEnd(text);
488         int to = layout.getOffsetToLeftOf(end);
489 
490         if (to != end) {
491             extendSelection(text, to);
492             return true;
493         }
494 
495         return true;
496     }
497 
498     /**
499      * Move the selection end to the buffer offset physically to the right of
500      * the current selection end.
501      */
extendRight(Spannable text, Layout layout)502     public static boolean extendRight(Spannable text, Layout layout) {
503         int end = getSelectionEnd(text);
504         int to = layout.getOffsetToRightOf(end);
505 
506         if (to != end) {
507             extendSelection(text, to);
508             return true;
509         }
510 
511         return true;
512     }
513 
extendToLeftEdge(Spannable text, Layout layout)514     public static boolean extendToLeftEdge(Spannable text, Layout layout) {
515         int where = findEdge(text, layout, -1);
516         extendSelection(text, where);
517         return true;
518     }
519 
extendToRightEdge(Spannable text, Layout layout)520     public static boolean extendToRightEdge(Spannable text, Layout layout) {
521         int where = findEdge(text, layout, 1);
522         extendSelection(text, where);
523         return true;
524     }
525 
moveToLeftEdge(Spannable text, Layout layout)526     public static boolean moveToLeftEdge(Spannable text, Layout layout) {
527         int where = findEdge(text, layout, -1);
528         setSelection(text, where);
529         return true;
530     }
531 
moveToRightEdge(Spannable text, Layout layout)532     public static boolean moveToRightEdge(Spannable text, Layout layout) {
533         int where = findEdge(text, layout, 1);
534         setSelection(text, where);
535         return true;
536     }
537 
538     /** {@hide} */
539     public static interface PositionIterator {
540         public static final int DONE = BreakIterator.DONE;
541 
preceding(int position)542         public int preceding(int position);
following(int position)543         public int following(int position);
544     }
545 
546     /** {@hide} */
547     @UnsupportedAppUsage
moveToPreceding( Spannable text, PositionIterator iter, boolean extendSelection)548     public static boolean moveToPreceding(
549             Spannable text, PositionIterator iter, boolean extendSelection) {
550         final int offset = iter.preceding(getSelectionEnd(text));
551         if (offset != PositionIterator.DONE) {
552             if (extendSelection) {
553                 extendSelection(text, offset);
554             } else {
555                 setSelection(text, offset);
556             }
557         }
558         return true;
559     }
560 
561     /** {@hide} */
562     @UnsupportedAppUsage
moveToFollowing( Spannable text, PositionIterator iter, boolean extendSelection)563     public static boolean moveToFollowing(
564             Spannable text, PositionIterator iter, boolean extendSelection) {
565         final int offset = iter.following(getSelectionEnd(text));
566         if (offset != PositionIterator.DONE) {
567             if (extendSelection) {
568                 extendSelection(text, offset);
569             } else {
570                 setSelection(text, offset);
571             }
572         }
573         return true;
574     }
575 
findEdge(Spannable text, Layout layout, int dir)576     private static int findEdge(Spannable text, Layout layout, int dir) {
577         int pt = getSelectionEnd(text);
578         int line = layout.getLineForOffset(pt);
579         int pdir = layout.getParagraphDirection(line);
580 
581         if (dir * pdir < 0) {
582             return layout.getLineStart(line);
583         } else {
584             int end = layout.getLineEnd(line);
585 
586             if (line == layout.getLineCount() - 1)
587                 return end;
588             else
589                 return end - 1;
590         }
591     }
592 
chooseHorizontal(Layout layout, int direction, int off1, int off2)593     private static int chooseHorizontal(Layout layout, int direction,
594                                         int off1, int off2) {
595         int line1 = layout.getLineForOffset(off1);
596         int line2 = layout.getLineForOffset(off2);
597 
598         if (line1 == line2) {
599             // same line, so it goes by pure physical direction
600 
601             float h1 = layout.getPrimaryHorizontal(off1);
602             float h2 = layout.getPrimaryHorizontal(off2);
603 
604             if (direction < 0) {
605                 // to left
606 
607                 if (h1 < h2)
608                     return off1;
609                 else
610                     return off2;
611             } else {
612                 // to right
613 
614                 if (h1 > h2)
615                     return off1;
616                 else
617                     return off2;
618             }
619         } else {
620             // different line, so which line is "left" and which is "right"
621             // depends upon the directionality of the text
622 
623             // This only checks at one end, but it's not clear what the
624             // right thing to do is if the ends don't agree.  Even if it
625             // is wrong it should still not be too bad.
626             int line = layout.getLineForOffset(off1);
627             int textdir = layout.getParagraphDirection(line);
628 
629             if (textdir == direction)
630                 return Math.max(off1, off2);
631             else
632                 return Math.min(off1, off2);
633         }
634     }
635 
636     private static final class START implements NoCopySpan { }
637     private static final class END implements NoCopySpan { }
638     private static final class MEMORY implements NoCopySpan { }
639     private static final Object SELECTION_MEMORY = new MEMORY();
640 
641     /*
642      * Public constants
643      */
644 
645     public static final Object SELECTION_START = new START();
646     public static final Object SELECTION_END = new END();
647 }
648