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