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
17package android.text;
18
19import android.annotation.TestApi;
20
21import 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 */
28public class 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     */
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     */
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
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     */
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     */
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     */
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
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
136        public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
137
138        @Override
139        public void onTextChanged(CharSequence s, int start, int before, int count) {}
140
141        @Override
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     */
151    public static final void setSelection(Spannable text, int index) {
152        setSelection(text, index, index);
153    }
154
155    /**
156     * Select the entire text.
157     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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
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
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
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
446        public int preceding(int position);
447        public int following(int position);
448    }
449
450    /** {@hide} */
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} */
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
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
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