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