WordComposer.java revision 0fd625bcfdfac1c10e7bd7f9088bf425fec08989
1/* 2 * Copyright (C) 2008 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 17package com.android.inputmethod.latin; 18 19import android.text.TextUtils; 20 21import com.android.inputmethod.keyboard.Key; 22import com.android.inputmethod.keyboard.KeyDetector; 23import com.android.inputmethod.keyboard.Keyboard; 24 25import java.util.ArrayList; 26import java.util.Arrays; 27 28/** 29 * A place to store the currently composing word with information such as adjacent key codes as well 30 */ 31public class WordComposer { 32 33 public static final int NOT_A_CODE = KeyDetector.NOT_A_CODE; 34 public static final int NOT_A_COORDINATE = -1; 35 36 // TODO: Straighten out commit behavior so that the flags here are more understandable, 37 // and possibly adjust their names. 38 // COMMIT_TYPE_USER_TYPED_WORD is used when the word committed is the exact typed word, with 39 // no hinting from the IME. It happens when some external event happens (rotating the device, 40 // for example) or when auto-correction is off by settings or editor attributes. 41 public static final int COMMIT_TYPE_USER_TYPED_WORD = 0; 42 // COMMIT_TYPE_MANUAL_PICK is used when the user pressed a field in the suggestion strip. 43 public static final int COMMIT_TYPE_MANUAL_PICK = 1; 44 // COMMIT_TYPE_DECIDED_WORD is used when the IME commits the word it decided was best 45 // for the current user input. It may be different from what the user typed (true auto-correct) 46 // or it may be exactly what the user typed if it's in the dictionary or the IME does not have 47 // enough confidence in any suggestion to auto-correct (auto-correct to typed word). 48 public static final int COMMIT_TYPE_DECIDED_WORD = 2; 49 // COMMIT_TYPE_CANCEL_AUTO_CORRECT is used upon committing back the old word upon cancelling 50 // an auto-correction. 51 public static final int COMMIT_TYPE_CANCEL_AUTO_CORRECT = 3; 52 53 // Storage for all the info about the current input. 54 private static class CharacterStore { 55 /** 56 * The list of unicode values for each keystroke (including surrounding keys) 57 */ 58 ArrayList<int[]> mCodes; 59 int[] mXCoordinates; 60 int[] mYCoordinates; 61 StringBuilder mTypedWord; 62 CharSequence mAutoCorrection; 63 CharacterStore() { 64 final int N = BinaryDictionary.MAX_WORD_LENGTH; 65 mCodes = new ArrayList<int[]>(N); 66 mTypedWord = new StringBuilder(N); 67 mXCoordinates = new int[N]; 68 mYCoordinates = new int[N]; 69 mAutoCorrection = null; 70 } 71 CharacterStore(final CharacterStore that) { 72 mCodes = new ArrayList<int[]>(that.mCodes); 73 mTypedWord = new StringBuilder(that.mTypedWord); 74 mXCoordinates = Arrays.copyOf(that.mXCoordinates, that.mXCoordinates.length); 75 mYCoordinates = Arrays.copyOf(that.mYCoordinates, that.mYCoordinates.length); 76 } 77 void reset() { 78 // For better performance than creating a new character store. 79 mCodes.clear(); 80 mTypedWord.setLength(0); 81 mAutoCorrection = null; 82 } 83 } 84 85 // The currently typing word. May not be null. 86 private CharacterStore mCurrentWord; 87 // The information being kept for resuming suggestion. May be null if wiped. 88 private CharacterStore mCommittedWordSavedForSuggestionResuming; 89 90 private int mCapsCount; 91 92 private boolean mAutoCapitalized; 93 // Cache this value for performance 94 private int mTrailingSingleQuotesCount; 95 96 /** 97 * Whether the user chose to capitalize the first char of the word. 98 */ 99 private boolean mIsFirstCharCapitalized; 100 101 public WordComposer() { 102 mCurrentWord = new CharacterStore(); 103 mCommittedWordSavedForSuggestionResuming = null; 104 mTrailingSingleQuotesCount = 0; 105 } 106 107 public WordComposer(WordComposer source) { 108 init(source); 109 } 110 111 public void init(WordComposer source) { 112 mCurrentWord = new CharacterStore(source.mCurrentWord); 113 mCommittedWordSavedForSuggestionResuming = source.mCommittedWordSavedForSuggestionResuming; 114 mCapsCount = source.mCapsCount; 115 mIsFirstCharCapitalized = source.mIsFirstCharCapitalized; 116 mAutoCapitalized = source.mAutoCapitalized; 117 mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount; 118 } 119 120 /** 121 * Clear out the keys registered so far. 122 */ 123 public void reset() { 124 mCurrentWord.reset(); 125 mCommittedWordSavedForSuggestionResuming = null; 126 mCapsCount = 0; 127 mIsFirstCharCapitalized = false; 128 mTrailingSingleQuotesCount = 0; 129 } 130 131 /** 132 * Number of keystrokes in the composing word. 133 * @return the number of keystrokes 134 */ 135 public final int size() { 136 return mCurrentWord.mTypedWord.length(); 137 } 138 139 public final boolean isComposingWord() { 140 return size() > 0; 141 } 142 143 /** 144 * Returns the codes at a particular position in the word. 145 * @param index the position in the word 146 * @return the unicode for the pressed and surrounding keys 147 */ 148 public int[] getCodesAt(int index) { 149 return mCurrentWord.mCodes.get(index); 150 } 151 152 public int[] getXCoordinates() { 153 return mCurrentWord.mXCoordinates; 154 } 155 156 public int[] getYCoordinates() { 157 return mCurrentWord.mYCoordinates; 158 } 159 160 private static boolean isFirstCharCapitalized(int index, int codePoint, boolean previous) { 161 if (index == 0) return Character.isUpperCase(codePoint); 162 return previous && !Character.isUpperCase(codePoint); 163 } 164 165 /** 166 * Add a new keystroke, with codes[0] containing the pressed key's unicode and the rest of 167 * the array containing unicode for adjacent keys, sorted by reducing probability/proximity. 168 * @param codes the array of unicode values 169 */ 170 public void add(int primaryCode, int[] codes, int x, int y) { 171 final int newIndex = size(); 172 mCurrentWord.mTypedWord.append((char) primaryCode); 173 correctPrimaryJuxtapos(primaryCode, codes); 174 mCurrentWord.mCodes.add(codes); 175 if (newIndex < BinaryDictionary.MAX_WORD_LENGTH) { 176 mCurrentWord.mXCoordinates[newIndex] = x; 177 mCurrentWord.mYCoordinates[newIndex] = y; 178 } 179 mIsFirstCharCapitalized = isFirstCharCapitalized( 180 newIndex, primaryCode, mIsFirstCharCapitalized); 181 if (Character.isUpperCase(primaryCode)) mCapsCount++; 182 if (Keyboard.CODE_SINGLE_QUOTE == primaryCode) { 183 ++mTrailingSingleQuotesCount; 184 } else { 185 mTrailingSingleQuotesCount = 0; 186 } 187 mCurrentWord.mAutoCorrection = null; 188 } 189 190 /** 191 * Internal method to retrieve reasonable proximity info for a character. 192 */ 193 private void addKeyInfo(final int codePoint, final Keyboard keyboard, 194 final KeyDetector keyDetector) { 195 for (final Key key : keyboard.mKeys) { 196 if (key.mCode == codePoint) { 197 final int x = key.mX + key.mWidth / 2; 198 final int y = key.mY + key.mHeight / 2; 199 final int[] codes = keyDetector.newCodeArray(); 200 keyDetector.getKeyAndNearbyCodes(x, y, codes); 201 add(codePoint, codes, x, y); 202 return; 203 } 204 } 205 add(codePoint, new int[] { codePoint }, 206 WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); 207 } 208 209 /** 210 * Set the currently composing word to the one passed as an argument. 211 * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity. 212 */ 213 public void setComposingWord(final CharSequence word, final Keyboard keyboard, 214 final KeyDetector keyDetector) { 215 reset(); 216 final int length = word.length(); 217 for (int i = 0; i < length; ++i) { 218 int codePoint = word.charAt(i); 219 addKeyInfo(codePoint, keyboard, keyDetector); 220 } 221 mCommittedWordSavedForSuggestionResuming = null; 222 } 223 224 /** 225 * Shortcut for the above method, this will create a new KeyDetector for the passed keyboard. 226 */ 227 public void setComposingWord(final CharSequence word, final Keyboard keyboard) { 228 final KeyDetector keyDetector = new KeyDetector(0); 229 keyDetector.setKeyboard(keyboard, 0, 0); 230 keyDetector.setProximityCorrectionEnabled(true); 231 keyDetector.setProximityThreshold(keyboard.mMostCommonKeyWidth); 232 setComposingWord(word, keyboard, keyDetector); 233 } 234 235 /** 236 * Swaps the first and second values in the codes array if the primary code is not the first 237 * value in the array but the second. This happens when the preferred key is not the key that 238 * the user released the finger on. 239 * @param primaryCode the preferred character 240 * @param codes array of codes based on distance from touch point 241 */ 242 private static void correctPrimaryJuxtapos(int primaryCode, int[] codes) { 243 if (codes.length < 2) return; 244 if (codes[0] > 0 && codes[1] > 0 && codes[0] != primaryCode && codes[1] == primaryCode) { 245 codes[1] = codes[0]; 246 codes[0] = primaryCode; 247 } 248 } 249 250 /** 251 * Delete the last keystroke as a result of hitting backspace. 252 */ 253 public void deleteLast() { 254 final int size = size(); 255 if (size > 0) { 256 final int lastPos = size - 1; 257 char lastChar = mCurrentWord.mTypedWord.charAt(lastPos); 258 mCurrentWord.mCodes.remove(lastPos); 259 mCurrentWord.mTypedWord.deleteCharAt(lastPos); 260 if (Character.isUpperCase(lastChar)) mCapsCount--; 261 } 262 if (size() == 0) { 263 mIsFirstCharCapitalized = false; 264 } 265 if (mTrailingSingleQuotesCount > 0) { 266 --mTrailingSingleQuotesCount; 267 } else { 268 for (int i = mCurrentWord.mTypedWord.length() - 1; i >= 0; --i) { 269 if (Keyboard.CODE_SINGLE_QUOTE != mCurrentWord.mTypedWord.codePointAt(i)) break; 270 ++mTrailingSingleQuotesCount; 271 } 272 } 273 mCurrentWord.mAutoCorrection = null; 274 } 275 276 /** 277 * Returns the word as it was typed, without any correction applied. 278 * @return the word that was typed so far. Never returns null. 279 */ 280 public String getTypedWord() { 281 return mCurrentWord.mTypedWord.toString(); 282 } 283 284 /** 285 * Whether or not the user typed a capital letter as the first letter in the word 286 * @return capitalization preference 287 */ 288 public boolean isFirstCharCapitalized() { 289 return mIsFirstCharCapitalized; 290 } 291 292 public int trailingSingleQuotesCount() { 293 return mTrailingSingleQuotesCount; 294 } 295 296 /** 297 * Whether or not all of the user typed chars are upper case 298 * @return true if all user typed chars are upper case, false otherwise 299 */ 300 public boolean isAllUpperCase() { 301 return (mCapsCount > 0) && (mCapsCount == size()); 302 } 303 304 /** 305 * Returns true if more than one character is upper case, otherwise returns false. 306 */ 307 public boolean isMostlyCaps() { 308 return mCapsCount > 1; 309 } 310 311 /** 312 * Saves the reason why the word is capitalized - whether it was automatic or 313 * due to the user hitting shift in the middle of a sentence. 314 * @param auto whether it was an automatic capitalization due to start of sentence 315 */ 316 public void setAutoCapitalized(boolean auto) { 317 mAutoCapitalized = auto; 318 } 319 320 /** 321 * Returns whether the word was automatically capitalized. 322 * @return whether the word was automatically capitalized 323 */ 324 public boolean isAutoCapitalized() { 325 return mAutoCapitalized; 326 } 327 328 /** 329 * Sets the auto-correction for this word. 330 */ 331 public void setAutoCorrection(final CharSequence correction) { 332 mCurrentWord.mAutoCorrection = correction; 333 } 334 335 /** 336 * Remove any auto-correction that may have been set. 337 */ 338 public void deleteAutoCorrection() { 339 mCurrentWord.mAutoCorrection = null; 340 } 341 342 /** 343 * @return the auto-correction for this word, or null if none. 344 */ 345 public CharSequence getAutoCorrectionOrNull() { 346 return mCurrentWord.mAutoCorrection; 347 } 348 349 // `type' should be one of the COMMIT_TYPE_* constants above. 350 public void onCommitWord(final int type) { 351 mCommittedWordSavedForSuggestionResuming = mCurrentWord; 352 // Note: currently, we come here whenever we commit a word. If it's any *other* kind that 353 // DECIDED_WORD, we should reset mAutoCorrection so that we don't attempt to cancel later. 354 // If it's a DECIDED_WORD, it may be an actual auto-correction by the IME, or what the user 355 // typed because the IME decided *not* to auto-correct for whatever reason. 356 // Ideally we would also null it when it was a DECIDED_WORD that was not an auto-correct. 357 // As it happens these two cases should behave differently, because the former can be 358 // canceled while the latter can't. Currently, we figure this out in 359 // #didAutoCorrectToAnotherWord with #equals(). It would be marginally cleaner to do it 360 // here, but it would be slower (since we would #equals() for each commit, instead of 361 // only on cancel), and ultimately we want to figure it out even earlier anyway. 362 if (type != COMMIT_TYPE_DECIDED_WORD) { 363 // Only ever revert an auto-correct. 364 mCommittedWordSavedForSuggestionResuming.mAutoCorrection = null; 365 } 366 // TODO: improve performance by swapping buffers instead of creating a new object. 367 mCurrentWord = new CharacterStore(); 368 } 369 370 public boolean hasWordKeptForSuggestionResuming() { 371 return null != mCommittedWordSavedForSuggestionResuming; 372 } 373 374 public void resumeSuggestionOnKeptWord() { 375 mCurrentWord = mCommittedWordSavedForSuggestionResuming; 376 mCommittedWordSavedForSuggestionResuming = null; 377 } 378 379 public boolean didAutoCorrectToAnotherWord() { 380 return null != mCommittedWordSavedForSuggestionResuming 381 && !TextUtils.isEmpty(mCommittedWordSavedForSuggestionResuming.mAutoCorrection) 382 && !TextUtils.equals(mCommittedWordSavedForSuggestionResuming.mTypedWord, 383 mCommittedWordSavedForSuggestionResuming.mAutoCorrection); 384 } 385} 386