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