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