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