Home | History | Annotate | Download | only in latin
      1 /*
      2  * Copyright (C) 2009 Google Inc.
      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 android.text.TextUtils;
     20 import android.view.inputmethod.ExtractedText;
     21 import android.view.inputmethod.ExtractedTextRequest;
     22 import android.view.inputmethod.InputConnection;
     23 
     24 import java.lang.reflect.InvocationTargetException;
     25 import java.lang.reflect.Method;
     26 import java.util.regex.Pattern;
     27 
     28 /**
     29  * Utility methods to deal with editing text through an InputConnection.
     30  */
     31 public class EditingUtil {
     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 
     37     // Cache Method pointers
     38     private static boolean sMethodsInitialized;
     39     private static Method sMethodGetSelectedText;
     40     private static Method sMethodSetComposingRegion;
     41 
     42     private EditingUtil() {};
     43 
     44     /**
     45      * Append newText to the text field represented by connection.
     46      * The new text becomes selected.
     47      */
     48     public static void appendText(InputConnection connection, String newText) {
     49         if (connection == null) {
     50             return;
     51         }
     52 
     53         // Commit the composing text
     54         connection.finishComposingText();
     55 
     56         // Add a space if the field already has text.
     57         CharSequence charBeforeCursor = connection.getTextBeforeCursor(1, 0);
     58         if (charBeforeCursor != null
     59                 && !charBeforeCursor.equals(" ")
     60                 && (charBeforeCursor.length() > 0)) {
     61             newText = " " + newText;
     62         }
     63 
     64         connection.setComposingText(newText, 1);
     65     }
     66 
     67     private static int getCursorPosition(InputConnection connection) {
     68         ExtractedText extracted = connection.getExtractedText(
     69             new ExtractedTextRequest(), 0);
     70         if (extracted == null) {
     71           return -1;
     72         }
     73         return extracted.startOffset + extracted.selectionStart;
     74     }
     75 
     76     /**
     77      * @param connection connection to the current text field.
     78      * @param sep characters which may separate words
     79      * @param range the range object to store the result into
     80      * @return the word that surrounds the cursor, including up to one trailing
     81      *   separator. For example, if the field contains "he|llo world", where |
     82      *   represents the cursor, then "hello " will be returned.
     83      */
     84     public static String getWordAtCursor(
     85             InputConnection connection, String separators, Range range) {
     86         Range r = getWordRangeAtCursor(connection, separators, range);
     87         return (r == null) ? null : r.word;
     88     }
     89 
     90     /**
     91      * Removes the word surrounding the cursor. Parameters are identical to
     92      * getWordAtCursor.
     93      */
     94     public static void deleteWordAtCursor(
     95         InputConnection connection, String separators) {
     96 
     97         Range range = getWordRangeAtCursor(connection, separators, null);
     98         if (range == null) return;
     99 
    100         connection.finishComposingText();
    101         // Move cursor to beginning of word, to avoid crash when cursor is outside
    102         // of valid range after deleting text.
    103         int newCursor = getCursorPosition(connection) - range.charsBefore;
    104         connection.setSelection(newCursor, newCursor);
    105         connection.deleteSurroundingText(0, range.charsBefore + range.charsAfter);
    106     }
    107 
    108     /**
    109      * Represents a range of text, relative to the current cursor position.
    110      */
    111     public static class Range {
    112         /** Characters before selection start */
    113         public int charsBefore;
    114 
    115         /**
    116          * Characters after selection start, including one trailing word
    117          * separator.
    118          */
    119         public int charsAfter;
    120 
    121         /** The actual characters that make up a word */
    122         public String word;
    123 
    124         public Range() {}
    125 
    126         public Range(int charsBefore, int charsAfter, String word) {
    127             if (charsBefore < 0 || charsAfter < 0) {
    128                 throw new IndexOutOfBoundsException();
    129             }
    130             this.charsBefore = charsBefore;
    131             this.charsAfter = charsAfter;
    132             this.word = word;
    133         }
    134     }
    135 
    136     private static Range getWordRangeAtCursor(
    137             InputConnection connection, String sep, Range range) {
    138         if (connection == null || sep == null) {
    139             return null;
    140         }
    141         CharSequence before = connection.getTextBeforeCursor(1000, 0);
    142         CharSequence after = connection.getTextAfterCursor(1000, 0);
    143         if (before == null || after == null) {
    144             return null;
    145         }
    146 
    147         // Find first word separator before the cursor
    148         int start = before.length();
    149         while (start > 0 && !isWhitespace(before.charAt(start - 1), sep)) start--;
    150 
    151         // Find last word separator after the cursor
    152         int end = -1;
    153         while (++end < after.length() && !isWhitespace(after.charAt(end), sep));
    154 
    155         int cursor = getCursorPosition(connection);
    156         if (start >= 0 && cursor + end <= after.length() + before.length()) {
    157             String word = before.toString().substring(start, before.length())
    158                     + after.toString().substring(0, end);
    159 
    160             Range returnRange = range != null? range : new Range();
    161             returnRange.charsBefore = before.length() - start;
    162             returnRange.charsAfter = end;
    163             returnRange.word = word;
    164             return returnRange;
    165         }
    166 
    167         return null;
    168     }
    169 
    170     private static boolean isWhitespace(int code, String whitespace) {
    171         return whitespace.contains(String.valueOf((char) code));
    172     }
    173 
    174     private static final Pattern spaceRegex = Pattern.compile("\\s+");
    175 
    176     public static CharSequence getPreviousWord(InputConnection connection,
    177             String sentenceSeperators) {
    178         //TODO: Should fix this. This could be slow!
    179         CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
    180         if (prev == null) {
    181             return null;
    182         }
    183         String[] w = spaceRegex.split(prev);
    184         if (w.length >= 2 && w[w.length-2].length() > 0) {
    185             char lastChar = w[w.length-2].charAt(w[w.length-2].length() -1);
    186             if (sentenceSeperators.contains(String.valueOf(lastChar))) {
    187                 return null;
    188             }
    189             return w[w.length-2];
    190         } else {
    191             return null;
    192         }
    193     }
    194 
    195     public static class SelectedWord {
    196         public int start;
    197         public int end;
    198         public CharSequence word;
    199     }
    200 
    201     /**
    202      * Takes a character sequence with a single character and checks if the character occurs
    203      * in a list of word separators or is empty.
    204      * @param singleChar A CharSequence with null, zero or one character
    205      * @param wordSeparators A String containing the word separators
    206      * @return true if the character is at a word boundary, false otherwise
    207      */
    208     private static boolean isWordBoundary(CharSequence singleChar, String wordSeparators) {
    209         return TextUtils.isEmpty(singleChar) || wordSeparators.contains(singleChar);
    210     }
    211 
    212     /**
    213      * Checks if the cursor is inside a word or the current selection is a whole word.
    214      * @param ic the InputConnection for accessing the text field
    215      * @param selStart the start position of the selection within the text field
    216      * @param selEnd the end position of the selection within the text field. This could be
    217      *               the same as selStart, if there's no selection.
    218      * @param wordSeparators the word separator characters for the current language
    219      * @return an object containing the text and coordinates of the selected/touching word,
    220      *         null if the selection/cursor is not marking a whole word.
    221      */
    222     public static SelectedWord getWordAtCursorOrSelection(final InputConnection ic,
    223             int selStart, int selEnd, String wordSeparators) {
    224         if (selStart == selEnd) {
    225             // There is just a cursor, so get the word at the cursor
    226             EditingUtil.Range range = new EditingUtil.Range();
    227             CharSequence touching = getWordAtCursor(ic, wordSeparators, range);
    228             if (!TextUtils.isEmpty(touching)) {
    229                 SelectedWord selWord = new SelectedWord();
    230                 selWord.word = touching;
    231                 selWord.start = selStart - range.charsBefore;
    232                 selWord.end = selEnd + range.charsAfter;
    233                 return selWord;
    234             }
    235         } else {
    236             // Is the previous character empty or a word separator? If not, return null.
    237             CharSequence charsBefore = ic.getTextBeforeCursor(1, 0);
    238             if (!isWordBoundary(charsBefore, wordSeparators)) {
    239                 return null;
    240             }
    241 
    242             // Is the next character empty or a word separator? If not, return null.
    243             CharSequence charsAfter = ic.getTextAfterCursor(1, 0);
    244             if (!isWordBoundary(charsAfter, wordSeparators)) {
    245                 return null;
    246             }
    247 
    248             // Extract the selection alone
    249             CharSequence touching = getSelectedText(ic, selStart, selEnd);
    250             if (TextUtils.isEmpty(touching)) return null;
    251             // Is any part of the selection a separator? If so, return null.
    252             final int length = touching.length();
    253             for (int i = 0; i < length; i++) {
    254                 if (wordSeparators.contains(touching.subSequence(i, i + 1))) {
    255                     return null;
    256                 }
    257             }
    258             // Prepare the selected word
    259             SelectedWord selWord = new SelectedWord();
    260             selWord.start = selStart;
    261             selWord.end = selEnd;
    262             selWord.word = touching;
    263             return selWord;
    264         }
    265         return null;
    266     }
    267 
    268     /**
    269      * Cache method pointers for performance
    270      */
    271     private static void initializeMethodsForReflection() {
    272         try {
    273             // These will either both exist or not, so no need for separate try/catch blocks.
    274             // If other methods are added later, use separate try/catch blocks.
    275             sMethodGetSelectedText = InputConnection.class.getMethod("getSelectedText", int.class);
    276             sMethodSetComposingRegion = InputConnection.class.getMethod("setComposingRegion",
    277                     int.class, int.class);
    278         } catch (NoSuchMethodException exc) {
    279             // Ignore
    280         }
    281         sMethodsInitialized = true;
    282     }
    283 
    284     /**
    285      * Returns the selected text between the selStart and selEnd positions.
    286      */
    287     private static CharSequence getSelectedText(InputConnection ic, int selStart, int selEnd) {
    288         // Use reflection, for backward compatibility
    289         CharSequence result = null;
    290         if (!sMethodsInitialized) {
    291             initializeMethodsForReflection();
    292         }
    293         if (sMethodGetSelectedText != null) {
    294             try {
    295                 result = (CharSequence) sMethodGetSelectedText.invoke(ic, 0);
    296                 return result;
    297             } catch (InvocationTargetException exc) {
    298                 // Ignore
    299             } catch (IllegalArgumentException e) {
    300                 // Ignore
    301             } catch (IllegalAccessException e) {
    302                 // Ignore
    303             }
    304         }
    305         // Reflection didn't work, try it the poor way, by moving the cursor to the start,
    306         // getting the text after the cursor and moving the text back to selected mode.
    307         // TODO: Verify that this works properly in conjunction with
    308         // LatinIME#onUpdateSelection
    309         ic.setSelection(selStart, selEnd);
    310         result = ic.getTextAfterCursor(selEnd - selStart, 0);
    311         ic.setSelection(selStart, selEnd);
    312         return result;
    313     }
    314 
    315     /**
    316      * Tries to set the text into composition mode if there is support for it in the framework.
    317      */
    318     public static void underlineWord(InputConnection ic, SelectedWord word) {
    319         // Use reflection, for backward compatibility
    320         // If method not found, there's nothing we can do. It still works but just wont underline
    321         // the word.
    322         if (!sMethodsInitialized) {
    323             initializeMethodsForReflection();
    324         }
    325         if (sMethodSetComposingRegion != null) {
    326             try {
    327                 sMethodSetComposingRegion.invoke(ic, word.start, word.end);
    328             } catch (InvocationTargetException exc) {
    329                 // Ignore
    330             } catch (IllegalArgumentException e) {
    331                 // Ignore
    332             } catch (IllegalAccessException e) {
    333                 // Ignore
    334             }
    335         }
    336     }
    337 }
    338