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