WordComposer.java revision a7f2500001c53dc5a6de9c2525a75229cc7c6645
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 // TODO: This crashes and catches fire if the code point doesn't fit a char 216 mTypedWord.deleteCharAt(lastPos); 217 if (Character.isUpperCase(lastChar)) mCapsCount--; 218 } 219 if (size() == 0) { 220 mIsFirstCharCapitalized = false; 221 } 222 if (mTrailingSingleQuotesCount > 0) { 223 --mTrailingSingleQuotesCount; 224 } else { 225 int i = mTypedWord.length(); 226 while (i > 0) { 227 i = mTypedWord.offsetByCodePoints(i, -1); 228 if (Keyboard.CODE_SINGLE_QUOTE != mTypedWord.codePointAt(i)) break; 229 ++mTrailingSingleQuotesCount; 230 } 231 } 232 mAutoCorrection = null; 233 } 234 235 /** 236 * Returns the word as it was typed, without any correction applied. 237 * @return the word that was typed so far. Never returns null. 238 */ 239 public String getTypedWord() { 240 return mTypedWord.toString(); 241 } 242 243 /** 244 * Whether or not the user typed a capital letter as the first letter in the word 245 * @return capitalization preference 246 */ 247 public boolean isFirstCharCapitalized() { 248 return mIsFirstCharCapitalized; 249 } 250 251 public int trailingSingleQuotesCount() { 252 return mTrailingSingleQuotesCount; 253 } 254 255 /** 256 * Whether or not all of the user typed chars are upper case 257 * @return true if all user typed chars are upper case, false otherwise 258 */ 259 public boolean isAllUpperCase() { 260 return (mCapsCount > 0) && (mCapsCount == size()); 261 } 262 263 /** 264 * Returns true if more than one character is upper case, otherwise returns false. 265 */ 266 public boolean isMostlyCaps() { 267 return mCapsCount > 1; 268 } 269 270 /** 271 * Saves the reason why the word is capitalized - whether it was automatic or 272 * due to the user hitting shift in the middle of a sentence. 273 * @param auto whether it was an automatic capitalization due to start of sentence 274 */ 275 public void setAutoCapitalized(boolean auto) { 276 mAutoCapitalized = auto; 277 } 278 279 /** 280 * Returns whether the word was automatically capitalized. 281 * @return whether the word was automatically capitalized 282 */ 283 public boolean isAutoCapitalized() { 284 return mAutoCapitalized; 285 } 286 287 /** 288 * Sets the auto-correction for this word. 289 */ 290 public void setAutoCorrection(final CharSequence correction) { 291 mAutoCorrection = correction; 292 } 293 294 /** 295 * @return the auto-correction for this word, or null if none. 296 */ 297 public CharSequence getAutoCorrectionOrNull() { 298 return mAutoCorrection; 299 } 300 301 // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above. 302 public LastComposedWord commitWord(final int type) { 303 // Note: currently, we come here whenever we commit a word. If it's any *other* kind than 304 // DECIDED_WORD, we should reset mAutoCorrection so that we don't attempt to cancel later. 305 // If it's a DECIDED_WORD, it may be an actual auto-correction by the IME, or what the user 306 // typed because the IME decided *not* to auto-correct for whatever reason. 307 // Ideally we would also null it when it was a DECIDED_WORD that was not an auto-correct. 308 // As it happens these two cases should behave differently, because the former can be 309 // canceled while the latter can't. Currently, we figure this out in 310 // LastComposedWord#didAutoCorrectToAnotherWord with #equals(). It would be marginally 311 // cleaner to do it here, but it would be slower (since we would #equals() for each commit, 312 // instead of only on cancel), and ultimately we want to figure it out even earlier anyway. 313 final ArrayList<int[]> codes = mCodes; 314 final int[] xCoordinates = mXCoordinates; 315 final int[] yCoordinates = mYCoordinates; 316 mCodes = new ArrayList<int[]>(N); 317 mXCoordinates = new int[N]; 318 mYCoordinates = new int[N]; 319 final LastComposedWord lastComposedWord = new LastComposedWord(codes, 320 xCoordinates, yCoordinates, mTypedWord.toString(), 321 null == mAutoCorrection ? null : mAutoCorrection.toString()); 322 if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD) { 323 lastComposedWord.deactivate(); 324 } 325 mTypedWord.setLength(0); 326 mAutoCorrection = null; 327 return lastComposedWord; 328 } 329 330 public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) { 331 mCodes = lastComposedWord.mCodes; 332 mXCoordinates = lastComposedWord.mXCoordinates; 333 mYCoordinates = lastComposedWord.mYCoordinates; 334 mTypedWord.setLength(0); 335 mTypedWord.append(lastComposedWord.mTypedWord); 336 mAutoCorrection = lastComposedWord.mAutoCorrection; 337 } 338} 339