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 
     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 {
     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