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