Home | History | Annotate | Download | only in latin
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * 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, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 
     17 package com.android.inputmethod.latin;
     18 
     19 import com.android.inputmethod.compat.InputConnectionCompatUtils;
     20 
     21 import android.text.TextUtils;
     22 import android.view.inputmethod.ExtractedText;
     23 import android.view.inputmethod.ExtractedTextRequest;
     24 import android.view.inputmethod.InputConnection;
     25 
     26 import java.util.regex.Pattern;
     27 
     28 /**
     29  * Utility methods to deal with editing text through an InputConnection.
     30  */
     31 public class EditingUtils {
     32     /**
     33      * Number of characters we want to look back in order to identify the previous word
     34      */
     35     private static final int LOOKBACK_CHARACTER_NUM = 15;
     36     private static final int INVALID_CURSOR_POSITION = -1;
     37 
     38     private EditingUtils() {
     39         // Unintentional empty constructor for singleton.
     40     }
     41 
     42     /**
     43      * Append newText to the text field represented by connection.
     44      * The new text becomes selected.
     45      */
     46     public static void appendText(InputConnection connection, String newText) {
     47         if (connection == null) {
     48             return;
     49         }
     50 
     51         // Commit the composing text
     52         connection.finishComposingText();
     53 
     54         // Add a space if the field already has text.
     55         String text = newText;
     56         CharSequence charBeforeCursor = connection.getTextBeforeCursor(1, 0);
     57         if (charBeforeCursor != null
     58                 && !charBeforeCursor.equals(" ")
     59                 && (charBeforeCursor.length() > 0)) {
     60             text = " " + text;
     61         }
     62 
     63         connection.setComposingText(text, 1);
     64     }
     65 
     66     private static int getCursorPosition(InputConnection connection) {
     67         if (null == connection) return INVALID_CURSOR_POSITION;
     68         ExtractedText extracted = connection.getExtractedText(
     69             new ExtractedTextRequest(), 0);
     70         if (extracted == null) {
     71             return INVALID_CURSOR_POSITION;
     72         }
     73         return extracted.startOffset + extracted.selectionStart;
     74     }
     75 
     76     /**
     77      * @param connection connection to the current text field.
     78      * @param separators characters which may separate words
     79      * @return the word that surrounds the cursor, including up to one trailing
     80      *   separator. For example, if the field contains "he|llo world", where |
     81      *   represents the cursor, then "hello " will be returned.
     82      */
     83     public static String getWordAtCursor(InputConnection connection, String separators) {
     84         // getWordRangeAtCursor returns null if the connection is null
     85         Range r = getWordRangeAtCursor(connection, separators);
     86         return (r == null) ? null : r.mWord;
     87     }
     88 
     89     /**
     90      * Removes the word surrounding the cursor. Parameters are identical to
     91      * getWordAtCursor.
     92      */
     93     public static void deleteWordAtCursor(InputConnection connection, String separators) {
     94         // getWordRangeAtCursor returns null if the connection is null
     95         Range range = getWordRangeAtCursor(connection, separators);
     96         if (range == null) return;
     97 
     98         connection.finishComposingText();
     99         // Move cursor to beginning of word, to avoid crash when cursor is outside
    100         // of valid range after deleting text.
    101         int newCursor = getCursorPosition(connection) - range.mCharsBefore;
    102         connection.setSelection(newCursor, newCursor);
    103         connection.deleteSurroundingText(0, range.mCharsBefore + range.mCharsAfter);
    104     }
    105 
    106     /**
    107      * Represents a range of text, relative to the current cursor position.
    108      */
    109     public static class Range {
    110         /** Characters before selection start */
    111         public final int mCharsBefore;
    112 
    113         /**
    114          * Characters after selection start, including one trailing word
    115          * separator.
    116          */
    117         public final int mCharsAfter;
    118 
    119         /** The actual characters that make up a word */
    120         public final String mWord;
    121 
    122         public Range(int charsBefore, int charsAfter, String word) {
    123             if (charsBefore < 0 || charsAfter < 0) {
    124                 throw new IndexOutOfBoundsException();
    125             }
    126             this.mCharsBefore = charsBefore;
    127             this.mCharsAfter = charsAfter;
    128             this.mWord = word;
    129         }
    130     }
    131 
    132     private static Range getWordRangeAtCursor(InputConnection connection, String sep) {
    133         if (connection == null || sep == null) {
    134             return null;
    135         }
    136         CharSequence before = connection.getTextBeforeCursor(1000, 0);
    137         CharSequence after = connection.getTextAfterCursor(1000, 0);
    138         if (before == null || after == null) {
    139             return null;
    140         }
    141 
    142         // Find first word separator before the cursor
    143         int start = before.length();
    144         while (start > 0 && !isWhitespace(before.charAt(start - 1), sep)) start--;
    145 
    146         // Find last word separator after the cursor
    147         int end = -1;
    148         while (++end < after.length() && !isWhitespace(after.charAt(end), sep)) {
    149             // Nothing to do here.
    150         }
    151 
    152         int cursor = getCursorPosition(connection);
    153         if (start >= 0 && cursor + end <= after.length() + before.length()) {
    154             String word = before.toString().substring(start, before.length())
    155                     + after.toString().substring(0, end);
    156             return new Range(before.length() - start, end, word);
    157         }
    158 
    159         return null;
    160     }
    161 
    162     private static boolean isWhitespace(int code, String whitespace) {
    163         return whitespace.contains(String.valueOf((char) code));
    164     }
    165 
    166     private static final Pattern spaceRegex = Pattern.compile("\\s+");
    167 
    168 
    169     public static CharSequence getPreviousWord(InputConnection connection,
    170             String sentenceSeperators) {
    171         //TODO: Should fix this. This could be slow!
    172         if (null == connection) return null;
    173         CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
    174         return getPreviousWord(prev, sentenceSeperators);
    175     }
    176 
    177     // Get the word before the whitespace preceding the non-whitespace preceding the cursor.
    178     // Also, it won't return words that end in a separator.
    179     // Example :
    180     // "abc def|" -> abc
    181     // "abc def |" -> abc
    182     // "abc def. |" -> abc
    183     // "abc def . |" -> def
    184     // "abc|" -> null
    185     // "abc |" -> null
    186     // "abc. def|" -> null
    187     public static CharSequence getPreviousWord(CharSequence prev, String sentenceSeperators) {
    188         if (prev == null) return null;
    189         String[] w = spaceRegex.split(prev);
    190 
    191         // If we can't find two words, or we found an empty word, return null.
    192         if (w.length < 2 || w[w.length - 2].length() <= 0) return null;
    193 
    194         // If ends in a separator, return null
    195         char lastChar = w[w.length - 2].charAt(w[w.length - 2].length() - 1);
    196         if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
    197 
    198         return w[w.length - 2];
    199     }
    200 
    201     public static CharSequence getThisWord(InputConnection connection, String sentenceSeperators) {
    202         if (null == connection) return null;
    203         final CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
    204         return getThisWord(prev, sentenceSeperators);
    205     }
    206 
    207     // Get the word immediately before the cursor, even if there is whitespace between it and
    208     // the cursor - but not if there is punctuation.
    209     // Example :
    210     // "abc def|" -> def
    211     // "abc def |" -> def
    212     // "abc def. |" -> null
    213     // "abc def . |" -> null
    214     public static CharSequence getThisWord(CharSequence prev, String sentenceSeperators) {
    215         if (prev == null) return null;
    216         String[] w = spaceRegex.split(prev);
    217 
    218         // No word : return null
    219         if (w.length < 1 || w[w.length - 1].length() <= 0) return null;
    220 
    221         // If ends in a separator, return null
    222         char lastChar = w[w.length - 1].charAt(w[w.length - 1].length() - 1);
    223         if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
    224 
    225         return w[w.length - 1];
    226     }
    227 
    228     public static class SelectedWord {
    229         public final int mStart;
    230         public final int mEnd;
    231         public final CharSequence mWord;
    232 
    233         public SelectedWord(int start, int end, CharSequence word) {
    234             mStart = start;
    235             mEnd = end;
    236             mWord = word;
    237         }
    238     }
    239 
    240     /**
    241      * Takes a character sequence with a single character and checks if the character occurs
    242      * in a list of word separators or is empty.
    243      * @param singleChar A CharSequence with null, zero or one character
    244      * @param wordSeparators A String containing the word separators
    245      * @return true if the character is at a word boundary, false otherwise
    246      */
    247     private static boolean isWordBoundary(CharSequence singleChar, String wordSeparators) {
    248         return TextUtils.isEmpty(singleChar) || wordSeparators.contains(singleChar);
    249     }
    250 
    251     /**
    252      * Checks if the cursor is inside a word or the current selection is a whole word.
    253      * @param ic the InputConnection for accessing the text field
    254      * @param selStart the start position of the selection within the text field
    255      * @param selEnd the end position of the selection within the text field. This could be
    256      *               the same as selStart, if there's no selection.
    257      * @param wordSeparators the word separator characters for the current language
    258      * @return an object containing the text and coordinates of the selected/touching word,
    259      *         null if the selection/cursor is not marking a whole word.
    260      */
    261     public static SelectedWord getWordAtCursorOrSelection(final InputConnection ic,
    262             int selStart, int selEnd, String wordSeparators) {
    263         if (selStart == selEnd) {
    264             // There is just a cursor, so get the word at the cursor
    265             // getWordRangeAtCursor returns null if the connection is null
    266             EditingUtils.Range range = getWordRangeAtCursor(ic, wordSeparators);
    267             if (range != null && !TextUtils.isEmpty(range.mWord)) {
    268                 return new SelectedWord(selStart - range.mCharsBefore, selEnd + range.mCharsAfter,
    269                         range.mWord);
    270             }
    271         } else {
    272             if (null == ic) return null;
    273             // Is the previous character empty or a word separator? If not, return null.
    274             CharSequence charsBefore = ic.getTextBeforeCursor(1, 0);
    275             if (!isWordBoundary(charsBefore, wordSeparators)) {
    276                 return null;
    277             }
    278 
    279             // Is the next character empty or a word separator? If not, return null.
    280             CharSequence charsAfter = ic.getTextAfterCursor(1, 0);
    281             if (!isWordBoundary(charsAfter, wordSeparators)) {
    282                 return null;
    283             }
    284 
    285             // Extract the selection alone
    286             CharSequence touching = InputConnectionCompatUtils.getSelectedText(
    287                     ic, selStart, selEnd);
    288             if (TextUtils.isEmpty(touching)) return null;
    289             // Is any part of the selection a separator? If so, return null.
    290             final int length = touching.length();
    291             for (int i = 0; i < length; i++) {
    292                 if (wordSeparators.contains(touching.subSequence(i, i + 1))) {
    293                     return null;
    294                 }
    295             }
    296             // Prepare the selected word
    297             return new SelectedWord(selStart, selEnd, touching);
    298         }
    299         return null;
    300     }
    301 }
    302