• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
EditingUtil()42     private EditingUtil() {};
43 
44     /**
45      * Append newText to the text field represented by connection.
46      * The new text becomes selected.
47      */
appendText(InputConnection connection, String newText)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 
getCursorPosition(InputConnection connection)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      */
getWordAtCursor( InputConnection connection, String separators, Range range)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      */
deleteWordAtCursor( InputConnection connection, String separators)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 
Range()124         public Range() {}
125 
Range(int charsBefore, int charsAfter, String word)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 
getWordRangeAtCursor( InputConnection connection, String sep, Range range)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 
isWhitespace(int code, String whitespace)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 
getPreviousWord(InputConnection connection, String sentenceSeperators)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      */
isWordBoundary(CharSequence singleChar, String wordSeparators)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      */
getWordAtCursorOrSelection(final InputConnection ic, int selStart, int selEnd, String wordSeparators)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      */
initializeMethodsForReflection()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      */
getSelectedText(InputConnection ic, int selStart, int selEnd)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      */
underlineWord(InputConnection ic, SelectedWord word)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