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, or return false if the cursor is already on the top line.
120     */
121    public static boolean moveUp(Spannable text, Layout layout) {
122        int start = getSelectionStart(text);
123        int end = getSelectionEnd(text);
124
125        if (start != end) {
126            int min = Math.min(start, end);
127            int max = Math.max(start, end);
128
129            setSelection(text, min);
130
131            if (min == 0 && max == text.length()) {
132                return false;
133            }
134
135            return true;
136        } else {
137            int line = layout.getLineForOffset(end);
138
139            if (line > 0) {
140                int move;
141
142                if (layout.getParagraphDirection(line) ==
143                    layout.getParagraphDirection(line - 1)) {
144                    float h = layout.getPrimaryHorizontal(end);
145                    move = layout.getOffsetForHorizontal(line - 1, h);
146                } else {
147                    move = layout.getLineStart(line - 1);
148                }
149
150                setSelection(text, move);
151                return true;
152            }
153        }
154
155        return false;
156    }
157
158    /**
159     * Move the cursor to the buffer offset physically below the current
160     * offset, or return false if the cursor is already on the bottom line.
161     */
162    public static boolean moveDown(Spannable text, Layout layout) {
163        int start = getSelectionStart(text);
164        int end = getSelectionEnd(text);
165
166        if (start != end) {
167            int min = Math.min(start, end);
168            int max = Math.max(start, end);
169
170            setSelection(text, max);
171
172            if (min == 0 && max == text.length()) {
173                return false;
174            }
175
176            return true;
177        } else {
178            int line = layout.getLineForOffset(end);
179
180            if (line < layout.getLineCount() - 1) {
181                int move;
182
183                if (layout.getParagraphDirection(line) ==
184                    layout.getParagraphDirection(line + 1)) {
185                    float h = layout.getPrimaryHorizontal(end);
186                    move = layout.getOffsetForHorizontal(line + 1, h);
187                } else {
188                    move = layout.getLineStart(line + 1);
189                }
190
191                setSelection(text, move);
192                return true;
193            }
194        }
195
196        return false;
197    }
198
199    /**
200     * Move the cursor to the buffer offset physically to the left of
201     * the current offset, or return false if the cursor is already
202     * at the left edge of the line and there is not another line to move it to.
203     */
204    public static boolean moveLeft(Spannable text, Layout layout) {
205        int start = getSelectionStart(text);
206        int end = getSelectionEnd(text);
207
208        if (start != end) {
209            setSelection(text, chooseHorizontal(layout, -1, start, end));
210            return true;
211        } else {
212            int to = layout.getOffsetToLeftOf(end);
213
214            if (to != end) {
215                setSelection(text, to);
216                return true;
217            }
218        }
219
220        return false;
221    }
222
223    /**
224     * Move the cursor to the buffer offset physically to the right of
225     * the current offset, or return false if the cursor is already at
226     * at the right edge of the line and there is not another line
227     * to move it to.
228     */
229    public static boolean moveRight(Spannable text, Layout layout) {
230        int start = getSelectionStart(text);
231        int end = getSelectionEnd(text);
232
233        if (start != end) {
234            setSelection(text, chooseHorizontal(layout, 1, start, end));
235            return true;
236        } else {
237            int to = layout.getOffsetToRightOf(end);
238
239            if (to != end) {
240                setSelection(text, to);
241                return true;
242            }
243        }
244
245        return false;
246    }
247
248    /**
249     * Move the selection end to the buffer offset physically above
250     * the current selection end.
251     */
252    public static boolean extendUp(Spannable text, Layout layout) {
253        int end = getSelectionEnd(text);
254        int line = layout.getLineForOffset(end);
255
256        if (line > 0) {
257            int move;
258
259            if (layout.getParagraphDirection(line) ==
260                layout.getParagraphDirection(line - 1)) {
261                float h = layout.getPrimaryHorizontal(end);
262                move = layout.getOffsetForHorizontal(line - 1, h);
263            } else {
264                move = layout.getLineStart(line - 1);
265            }
266
267            extendSelection(text, move);
268            return true;
269        } else if (end != 0) {
270            extendSelection(text, 0);
271            return true;
272        }
273
274        return true;
275    }
276
277    /**
278     * Move the selection end to the buffer offset physically below
279     * the current selection end.
280     */
281    public static boolean extendDown(Spannable text, Layout layout) {
282        int end = getSelectionEnd(text);
283        int line = layout.getLineForOffset(end);
284
285        if (line < layout.getLineCount() - 1) {
286            int move;
287
288            if (layout.getParagraphDirection(line) ==
289                layout.getParagraphDirection(line + 1)) {
290                float h = layout.getPrimaryHorizontal(end);
291                move = layout.getOffsetForHorizontal(line + 1, h);
292            } else {
293                move = layout.getLineStart(line + 1);
294            }
295
296            extendSelection(text, move);
297            return true;
298        } else if (end != text.length()) {
299            extendSelection(text, text.length());
300            return true;
301        }
302
303        return true;
304    }
305
306    /**
307     * Move the selection end to the buffer offset physically to the left of
308     * the current selection end.
309     */
310    public static boolean extendLeft(Spannable text, Layout layout) {
311        int end = getSelectionEnd(text);
312        int to = layout.getOffsetToLeftOf(end);
313
314        if (to != end) {
315            extendSelection(text, to);
316            return true;
317        }
318
319        return true;
320    }
321
322    /**
323     * Move the selection end to the buffer offset physically to the right of
324     * the current selection end.
325     */
326    public static boolean extendRight(Spannable text, Layout layout) {
327        int end = getSelectionEnd(text);
328        int to = layout.getOffsetToRightOf(end);
329
330        if (to != end) {
331            extendSelection(text, to);
332            return true;
333        }
334
335        return true;
336    }
337
338    public static boolean extendToLeftEdge(Spannable text, Layout layout) {
339        int where = findEdge(text, layout, -1);
340        extendSelection(text, where);
341        return true;
342    }
343
344    public static boolean extendToRightEdge(Spannable text, Layout layout) {
345        int where = findEdge(text, layout, 1);
346        extendSelection(text, where);
347        return true;
348    }
349
350    public static boolean moveToLeftEdge(Spannable text, Layout layout) {
351        int where = findEdge(text, layout, -1);
352        setSelection(text, where);
353        return true;
354    }
355
356    public static boolean moveToRightEdge(Spannable text, Layout layout) {
357        int where = findEdge(text, layout, 1);
358        setSelection(text, where);
359        return true;
360    }
361
362    /** {@hide} */
363    public static interface PositionIterator {
364        public static final int DONE = BreakIterator.DONE;
365
366        public int preceding(int position);
367        public int following(int position);
368    }
369
370    /** {@hide} */
371    public static boolean moveToPreceding(
372            Spannable text, PositionIterator iter, boolean extendSelection) {
373        final int offset = iter.preceding(getSelectionEnd(text));
374        if (offset != PositionIterator.DONE) {
375            if (extendSelection) {
376                extendSelection(text, offset);
377            } else {
378                setSelection(text, offset);
379            }
380        }
381        return true;
382    }
383
384    /** {@hide} */
385    public static boolean moveToFollowing(
386            Spannable text, PositionIterator iter, boolean extendSelection) {
387        final int offset = iter.following(getSelectionEnd(text));
388        if (offset != PositionIterator.DONE) {
389            if (extendSelection) {
390                extendSelection(text, offset);
391            } else {
392                setSelection(text, offset);
393            }
394        }
395        return true;
396    }
397
398    private static int findEdge(Spannable text, Layout layout, int dir) {
399        int pt = getSelectionEnd(text);
400        int line = layout.getLineForOffset(pt);
401        int pdir = layout.getParagraphDirection(line);
402
403        if (dir * pdir < 0) {
404            return layout.getLineStart(line);
405        } else {
406            int end = layout.getLineEnd(line);
407
408            if (line == layout.getLineCount() - 1)
409                return end;
410            else
411                return end - 1;
412        }
413    }
414
415    private static int chooseHorizontal(Layout layout, int direction,
416                                        int off1, int off2) {
417        int line1 = layout.getLineForOffset(off1);
418        int line2 = layout.getLineForOffset(off2);
419
420        if (line1 == line2) {
421            // same line, so it goes by pure physical direction
422
423            float h1 = layout.getPrimaryHorizontal(off1);
424            float h2 = layout.getPrimaryHorizontal(off2);
425
426            if (direction < 0) {
427                // to left
428
429                if (h1 < h2)
430                    return off1;
431                else
432                    return off2;
433            } else {
434                // to right
435
436                if (h1 > h2)
437                    return off1;
438                else
439                    return off2;
440            }
441        } else {
442            // different line, so which line is "left" and which is "right"
443            // depends upon the directionality of the text
444
445            // This only checks at one end, but it's not clear what the
446            // right thing to do is if the ends don't agree.  Even if it
447            // is wrong it should still not be too bad.
448            int line = layout.getLineForOffset(off1);
449            int textdir = layout.getParagraphDirection(line);
450
451            if (textdir == direction)
452                return Math.max(off1, off2);
453            else
454                return Math.min(off1, off2);
455        }
456    }
457
458    private static final class START implements NoCopySpan { }
459    private static final class END implements NoCopySpan { }
460
461    /*
462     * Public constants
463     */
464
465    public static final Object SELECTION_START = new START();
466    public static final Object SELECTION_END = new END();
467}
468