/ime/latinime/src/com/googlecode/eyesfree/inputmethod/latin/EditingUtil.java
Java | 339 lines | 198 code | 37 blank | 104 comment | 50 complexity | 609ff4a540720327998e0f58d273bee8 MD5 | raw file
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 17package com.googlecode.eyesfree.inputmethod.latin; 18 19import android.text.TextUtils; 20import android.view.inputmethod.ExtractedText; 21import android.view.inputmethod.ExtractedTextRequest; 22import android.view.inputmethod.InputConnection; 23 24import java.lang.reflect.InvocationTargetException; 25import java.lang.reflect.Method; 26import java.util.regex.Pattern; 27 28/** 29 * Utility methods to deal with editing text through an InputConnection. 30 */ 31public 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 separators 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 // Do nothing 155 } 156 157 int cursor = getCursorPosition(connection); 158 if (start >= 0 && cursor + end <= after.length() + before.length()) { 159 String word = before.toString().substring(start, before.length()) 160 + after.toString().substring(0, end); 161 162 Range returnRange = range != null? range : new Range(); 163 returnRange.charsBefore = before.length() - start; 164 returnRange.charsAfter = end; 165 returnRange.word = word; 166 return returnRange; 167 } 168 169 return null; 170 } 171 172 private static boolean isWhitespace(int code, String whitespace) { 173 return whitespace.contains(String.valueOf((char) code)); 174 } 175 176 private static final Pattern spaceRegex = Pattern.compile("\\s+"); 177 178 public static CharSequence getPreviousWord(InputConnection connection, 179 String sentenceSeperators) { 180 //TODO: Should fix this. This could be slow! 181 CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0); 182 if (prev == null) { 183 return null; 184 } 185 String[] w = spaceRegex.split(prev); 186 if (w.length >= 2 && w[w.length-2].length() > 0) { 187 char lastChar = w[w.length-2].charAt(w[w.length-2].length() -1); 188 if (sentenceSeperators.contains(String.valueOf(lastChar))) { 189 return null; 190 } 191 return w[w.length-2]; 192 } else { 193 return null; 194 } 195 } 196 197 public static class SelectedWord { 198 public int start; 199 public int end; 200 public CharSequence word; 201 } 202 203 /** 204 * Takes a character sequence with a single character and checks if the character occurs 205 * in a list of word separators or is empty. 206 * @param singleChar A CharSequence with null, zero or one character 207 * @param wordSeparators A String containing the word separators 208 * @return true if the character is at a word boundary, false otherwise 209 */ 210 private static boolean isWordBoundary(CharSequence singleChar, String wordSeparators) { 211 return TextUtils.isEmpty(singleChar) || wordSeparators.contains(singleChar); 212 } 213 214 /** 215 * Checks if the cursor is inside a word or the current selection is a whole word. 216 * @param ic the InputConnection for accessing the text field 217 * @param selStart the start position of the selection within the text field 218 * @param selEnd the end position of the selection within the text field. This could be 219 * the same as selStart, if there's no selection. 220 * @param wordSeparators the word separator characters for the current language 221 * @return an object containing the text and coordinates of the selected/touching word, 222 * null if the selection/cursor is not marking a whole word. 223 */ 224 public static SelectedWord getWordAtCursorOrSelection(final InputConnection ic, 225 int selStart, int selEnd, String wordSeparators) { 226 if (selStart == selEnd) { 227 // There is just a cursor, so get the word at the cursor 228 EditingUtil.Range range = new EditingUtil.Range(); 229 CharSequence touching = getWordAtCursor(ic, wordSeparators, range); 230 if (!TextUtils.isEmpty(touching)) { 231 SelectedWord selWord = new SelectedWord(); 232 selWord.word = touching; 233 selWord.start = selStart - range.charsBefore; 234 selWord.end = selEnd + range.charsAfter; 235 return selWord; 236 } 237 } else { 238 // Is the previous character empty or a word separator? If not, return null. 239 CharSequence charsBefore = ic.getTextBeforeCursor(1, 0); 240 if (!isWordBoundary(charsBefore, wordSeparators)) { 241 return null; 242 } 243 244 // Is the next character empty or a word separator? If not, return null. 245 CharSequence charsAfter = ic.getTextAfterCursor(1, 0); 246 if (!isWordBoundary(charsAfter, wordSeparators)) { 247 return null; 248 } 249 250 // Extract the selection alone 251 CharSequence touching = getSelectedText(ic, selStart, selEnd); 252 if (TextUtils.isEmpty(touching)) return null; 253 // Is any part of the selection a separator? If so, return null. 254 final int length = touching.length(); 255 for (int i = 0; i < length; i++) { 256 if (wordSeparators.contains(touching.subSequence(i, i + 1))) { 257 return null; 258 } 259 } 260 // Prepare the selected word 261 SelectedWord selWord = new SelectedWord(); 262 selWord.start = selStart; 263 selWord.end = selEnd; 264 selWord.word = touching; 265 return selWord; 266 } 267 return null; 268 } 269 270 /** 271 * Cache method pointers for performance 272 */ 273 private static void initializeMethodsForReflection() { 274 try { 275 // These will either both exist or not, so no need for separate try/catch blocks. 276 // If other methods are added later, use separate try/catch blocks. 277 sMethodGetSelectedText = InputConnection.class.getMethod("getSelectedText", int.class); 278 sMethodSetComposingRegion = InputConnection.class.getMethod("setComposingRegion", 279 int.class, int.class); 280 } catch (NoSuchMethodException exc) { 281 // Ignore 282 } 283 sMethodsInitialized = true; 284 } 285 286 /** 287 * Returns the selected text between the selStart and selEnd positions. 288 */ 289 private static CharSequence getSelectedText(InputConnection ic, int selStart, int selEnd) { 290 // Use reflection, for backward compatibility 291 CharSequence result = null; 292 if (!sMethodsInitialized) { 293 initializeMethodsForReflection(); 294 } 295 if (sMethodGetSelectedText != null) { 296 try { 297 result = (CharSequence) sMethodGetSelectedText.invoke(ic, 0); 298 return result; 299 } catch (InvocationTargetException exc) { 300 // Ignore 301 } catch (IllegalArgumentException e) { 302 // Ignore 303 } catch (IllegalAccessException e) { 304 // Ignore 305 } 306 } 307 // Reflection didn't work, try it the poor way, by moving the cursor to the start, 308 // getting the text after the cursor and moving the text back to selected mode. 309 // TODO: Verify that this works properly in conjunction with 310 // LatinIME#onUpdateSelection 311 ic.setSelection(selStart, selEnd); 312 result = ic.getTextAfterCursor(selEnd - selStart, 0); 313 ic.setSelection(selStart, selEnd); 314 return result; 315 } 316 317 /** 318 * Tries to set the text into composition mode if there is support for it in the framework. 319 */ 320 public static void underlineWord(InputConnection ic, SelectedWord word) { 321 // Use reflection, for backward compatibility 322 // If method not found, there's nothing we can do. It still works but just wont underline 323 // the word. 324 if (!sMethodsInitialized) { 325 initializeMethodsForReflection(); 326 } 327 if (sMethodSetComposingRegion != null) { 328 try { 329 sMethodSetComposingRegion.invoke(ic, word.start, word.end); 330 } catch (InvocationTargetException exc) { 331 // Ignore 332 } catch (IllegalArgumentException e) { 333 // Ignore 334 } catch (IllegalAccessException e) { 335 // Ignore 336 } 337 } 338 } 339}