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