WordComposer.java revision 449415c72f437f523a49a9ccfcde8a3c0f583a18
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 final int N = BinaryDictionary.MAX_WORD_LENGTH; 37 38 private ArrayList<int[]> mCodes; 39 private int[] mXCoordinates; 40 private int[] mYCoordinates; 41 private StringBuilder mTypedWord; 42 private CharSequence mAutoCorrection; 43 44 // Cache these values for performance 45 private int mCapsCount; 46 private boolean mAutoCapitalized; 47 private int mTrailingSingleQuotesCount; 48 49 /** 50 * Whether the user chose to capitalize the first char of the word. 51 */ 52 private boolean mIsFirstCharCapitalized; 53 54 public WordComposer() { 55 mCodes = new ArrayList<int[]>(N); 56 mTypedWord = new StringBuilder(N); 57 mXCoordinates = new int[N]; 58 mYCoordinates = new int[N]; 59 mAutoCorrection = null; 60 mTrailingSingleQuotesCount = 0; 61 } 62 63 public WordComposer(WordComposer source) { 64 init(source); 65 } 66 67 public void init(WordComposer source) { 68 mCodes = new ArrayList<int[]>(source.mCodes); 69 mTypedWord = new StringBuilder(source.mTypedWord); 70 mXCoordinates = Arrays.copyOf(source.mXCoordinates, source.mXCoordinates.length); 71 mYCoordinates = Arrays.copyOf(source.mYCoordinates, source.mYCoordinates.length); 72 mCapsCount = source.mCapsCount; 73 mIsFirstCharCapitalized = source.mIsFirstCharCapitalized; 74 mAutoCapitalized = source.mAutoCapitalized; 75 mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount; 76 } 77 78 /** 79 * Clear out the keys registered so far. 80 */ 81 public void reset() { 82 mCodes.clear(); 83 mTypedWord.setLength(0); 84 mAutoCorrection = null; 85 mCapsCount = 0; 86 mIsFirstCharCapitalized = false; 87 mTrailingSingleQuotesCount = 0; 88 } 89 90 /** 91 * Number of keystrokes in the composing word. 92 * @return the number of keystrokes 93 */ 94 public final int size() { 95 return mTypedWord.length(); 96 } 97 98 public final boolean isComposingWord() { 99 return size() > 0; 100 } 101 102 /** 103 * Returns the codes at a particular position in the word. 104 * @param index the position in the word 105 * @return the unicode for the pressed and surrounding keys 106 */ 107 public int[] getCodesAt(int index) { 108 return mCodes.get(index); 109 } 110 111 public int[] getXCoordinates() { 112 return mXCoordinates; 113 } 114 115 public int[] getYCoordinates() { 116 return mYCoordinates; 117 } 118 119 private static boolean isFirstCharCapitalized(int index, int codePoint, boolean previous) { 120 if (index == 0) return Character.isUpperCase(codePoint); 121 return previous && !Character.isUpperCase(codePoint); 122 } 123 124 /** 125 * Add a new keystroke, with codes[0] containing the pressed key's unicode and the rest of 126 * the array containing unicode for adjacent keys, sorted by reducing probability/proximity. 127 * @param codes the array of unicode values 128 */ 129 public void add(int primaryCode, int[] codes, int x, int y) { 130 final int newIndex = size(); 131 mTypedWord.append((char) primaryCode); 132 correctPrimaryJuxtapos(primaryCode, codes); 133 mCodes.add(codes); 134 if (newIndex < BinaryDictionary.MAX_WORD_LENGTH) { 135 mXCoordinates[newIndex] = x; 136 mYCoordinates[newIndex] = y; 137 } 138 mIsFirstCharCapitalized = isFirstCharCapitalized( 139 newIndex, primaryCode, mIsFirstCharCapitalized); 140 if (Character.isUpperCase(primaryCode)) mCapsCount++; 141 if (Keyboard.CODE_SINGLE_QUOTE == primaryCode) { 142 ++mTrailingSingleQuotesCount; 143 } else { 144 mTrailingSingleQuotesCount = 0; 145 } 146 mAutoCorrection = null; 147 } 148 149 /** 150 * Internal method to retrieve reasonable proximity info for a character. 151 */ 152 private void addKeyInfo(final int codePoint, final Keyboard keyboard, 153 final KeyDetector keyDetector) { 154 for (final Key key : keyboard.mKeys) { 155 if (key.mCode == codePoint) { 156 final int x = key.mX + key.mWidth / 2; 157 final int y = key.mY + key.mHeight / 2; 158 final int[] codes = keyDetector.newCodeArray(); 159 keyDetector.getKeyAndNearbyCodes(x, y, codes); 160 add(codePoint, codes, x, y); 161 return; 162 } 163 } 164 add(codePoint, new int[] { codePoint }, 165 WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); 166 } 167 168 /** 169 * Set the currently composing word to the one passed as an argument. 170 * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity. 171 */ 172 public void setComposingWord(final CharSequence word, final Keyboard keyboard, 173 final KeyDetector keyDetector) { 174 reset(); 175 final int length = word.length(); 176 for (int i = 0; i < length; ++i) { 177 int codePoint = word.charAt(i); 178 addKeyInfo(codePoint, keyboard, keyDetector); 179 } 180 } 181 182 /** 183 * Shortcut for the above method, this will create a new KeyDetector for the passed keyboard. 184 */ 185 public void setComposingWord(final CharSequence word, final Keyboard keyboard) { 186 final KeyDetector keyDetector = new KeyDetector(0); 187 keyDetector.setKeyboard(keyboard, 0, 0); 188 keyDetector.setProximityCorrectionEnabled(true); 189 keyDetector.setProximityThreshold(keyboard.mMostCommonKeyWidth); 190 setComposingWord(word, keyboard, keyDetector); 191 } 192 193 /** 194 * Swaps the first and second values in the codes array if the primary code is not the first 195 * value in the array but the second. This happens when the preferred key is not the key that 196 * the user released the finger on. 197 * @param primaryCode the preferred character 198 * @param codes array of codes based on distance from touch point 199 */ 200 private static void correctPrimaryJuxtapos(int primaryCode, int[] codes) { 201 if (codes.length < 2) return; 202 if (codes[0] > 0 && codes[1] > 0 && codes[0] != primaryCode && codes[1] == primaryCode) { 203 codes[1] = codes[0]; 204 codes[0] = primaryCode; 205 } 206 } 207 208 /** 209 * Delete the last keystroke as a result of hitting backspace. 210 */ 211 public void deleteLast() { 212 final int size = size(); 213 if (size > 0) { 214 final int lastPos = size - 1; 215 char lastChar = mTypedWord.charAt(lastPos); 216 mCodes.remove(lastPos); 217 mTypedWord.deleteCharAt(lastPos); 218 if (Character.isUpperCase(lastChar)) mCapsCount--; 219 } 220 if (size() == 0) { 221 mIsFirstCharCapitalized = false; 222 } 223 if (mTrailingSingleQuotesCount > 0) { 224 --mTrailingSingleQuotesCount; 225 } else { 226 for (int i = mTypedWord.length() - 1; i >= 0; --i) { 227 if (Keyboard.CODE_SINGLE_QUOTE != mTypedWord.codePointAt(i)) break; 228 ++mTrailingSingleQuotesCount; 229 } 230 } 231 mAutoCorrection = null; 232 } 233 234 /** 235 * Returns the word as it was typed, without any correction applied. 236 * @return the word that was typed so far. Never returns null. 237 */ 238 public String getTypedWord() { 239 return mTypedWord.toString(); 240 } 241 242 /** 243 * Whether or not the user typed a capital letter as the first letter in the word 244 * @return capitalization preference 245 */ 246 public boolean isFirstCharCapitalized() { 247 return mIsFirstCharCapitalized; 248 } 249 250 public int trailingSingleQuotesCount() { 251 return mTrailingSingleQuotesCount; 252 } 253 254 /** 255 * Whether or not all of the user typed chars are upper case 256 * @return true if all user typed chars are upper case, false otherwise 257 */ 258 public boolean isAllUpperCase() { 259 return (mCapsCount > 0) && (mCapsCount == size()); 260 } 261 262 /** 263 * Returns true if more than one character is upper case, otherwise returns false. 264 */ 265 public boolean isMostlyCaps() { 266 return mCapsCount > 1; 267 } 268 269 /** 270 * Saves the reason why the word is capitalized - whether it was automatic or 271 * due to the user hitting shift in the middle of a sentence. 272 * @param auto whether it was an automatic capitalization due to start of sentence 273 */ 274 public void setAutoCapitalized(boolean auto) { 275 mAutoCapitalized = auto; 276 } 277 278 /** 279 * Returns whether the word was automatically capitalized. 280 * @return whether the word was automatically capitalized 281 */ 282 public boolean isAutoCapitalized() { 283 return mAutoCapitalized; 284 } 285 286 /** 287 * Sets the auto-correction for this word. 288 */ 289 public void setAutoCorrection(final CharSequence correction) { 290 mAutoCorrection = correction; 291 } 292 293 /** 294 * @return the auto-correction for this word, or null if none. 295 */ 296 public CharSequence getAutoCorrectionOrNull() { 297 return mAutoCorrection; 298 } 299 300 // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above. 301 public LastComposedWord commitWord(final int type) { 302 // Note: currently, we come here whenever we commit a word. If it's any *other* kind than 303 // DECIDED_WORD, we should reset mAutoCorrection so that we don't attempt to cancel later. 304 // If it's a DECIDED_WORD, it may be an actual auto-correction by the IME, or what the user 305 // typed because the IME decided *not* to auto-correct for whatever reason. 306 // Ideally we would also null it when it was a DECIDED_WORD that was not an auto-correct. 307 // As it happens these two cases should behave differently, because the former can be 308 // canceled while the latter can't. Currently, we figure this out in 309 // LastComposedWord#didAutoCorrectToAnotherWord with #equals(). It would be marginally 310 // cleaner to do it here, but it would be slower (since we would #equals() for each commit, 311 // instead of only on cancel), and ultimately we want to figure it out even earlier anyway. 312 final LastComposedWord lastComposedWord = new LastComposedWord(mCodes, 313 mXCoordinates, mYCoordinates, mTypedWord.toString(), 314 null == mAutoCorrection ? null : mAutoCorrection.toString()); 315 if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD) { 316 lastComposedWord.deactivate(); 317 } 318 mCodes.clear(); 319 mTypedWord.setLength(0); 320 mAutoCorrection = null; 321 return lastComposedWord; 322 } 323 324 public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) { 325 mCodes = lastComposedWord.mCodes; 326 mXCoordinates = lastComposedWord.mXCoordinates; 327 mYCoordinates = lastComposedWord.mYCoordinates; 328 mTypedWord.setLength(0); 329 mTypedWord.append(lastComposedWord.mTypedWord); 330 mAutoCorrection = lastComposedWord.mAutoCorrection; 331 } 332} 333