WordComposer.java revision 8914555776a4d3dfd6afc4926a69169ca1c82a0e
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.Keyboard; 21 22import java.util.Arrays; 23 24/** 25 * A place to store the currently composing word with information such as adjacent key codes as well 26 */ 27public final class WordComposer { 28 private static final int MAX_WORD_LENGTH = Constants.Dictionary.MAX_WORD_LENGTH; 29 30 public static final int CAPS_MODE_OFF = 0; 31 // 1 is shift bit, 2 is caps bit, 4 is auto bit but this is just a convention as these bits 32 // aren't used anywhere in the code 33 public static final int CAPS_MODE_MANUAL_SHIFTED = 0x1; 34 public static final int CAPS_MODE_MANUAL_SHIFT_LOCKED = 0x3; 35 public static final int CAPS_MODE_AUTO_SHIFTED = 0x5; 36 public static final int CAPS_MODE_AUTO_SHIFT_LOCKED = 0x7; 37 38 private int[] mPrimaryKeyCodes; 39 private final InputPointers mInputPointers = new InputPointers(MAX_WORD_LENGTH); 40 private final StringBuilder mTypedWord; 41 private String mAutoCorrection; 42 private boolean mIsResumed; 43 private boolean mIsBatchMode; 44 45 // Cache these values for performance 46 private int mCapsCount; 47 private int mDigitsCount; 48 private int mCapitalizedMode; 49 private int mTrailingSingleQuotesCount; 50 private int mCodePointSize; 51 52 /** 53 * Whether the user chose to capitalize the first char of the word. 54 */ 55 private boolean mIsFirstCharCapitalized; 56 57 public WordComposer() { 58 mPrimaryKeyCodes = new int[MAX_WORD_LENGTH]; 59 mTypedWord = new StringBuilder(MAX_WORD_LENGTH); 60 mAutoCorrection = null; 61 mTrailingSingleQuotesCount = 0; 62 mIsResumed = false; 63 mIsBatchMode = false; 64 refreshSize(); 65 } 66 67 public WordComposer(final WordComposer source) { 68 mPrimaryKeyCodes = Arrays.copyOf(source.mPrimaryKeyCodes, source.mPrimaryKeyCodes.length); 69 mTypedWord = new StringBuilder(source.mTypedWord); 70 mInputPointers.copy(source.mInputPointers); 71 mCapsCount = source.mCapsCount; 72 mDigitsCount = source.mDigitsCount; 73 mIsFirstCharCapitalized = source.mIsFirstCharCapitalized; 74 mCapitalizedMode = source.mCapitalizedMode; 75 mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount; 76 mIsResumed = source.mIsResumed; 77 mIsBatchMode = source.mIsBatchMode; 78 refreshSize(); 79 } 80 81 /** 82 * Clear out the keys registered so far. 83 */ 84 public void reset() { 85 mTypedWord.setLength(0); 86 mAutoCorrection = null; 87 mCapsCount = 0; 88 mDigitsCount = 0; 89 mIsFirstCharCapitalized = false; 90 mTrailingSingleQuotesCount = 0; 91 mIsResumed = false; 92 mIsBatchMode = false; 93 refreshSize(); 94 } 95 96 private final void refreshSize() { 97 mCodePointSize = mTypedWord.codePointCount(0, mTypedWord.length()); 98 } 99 100 /** 101 * Number of keystrokes in the composing word. 102 * @return the number of keystrokes 103 */ 104 public final int size() { 105 return mCodePointSize; 106 } 107 108 public final boolean isComposingWord() { 109 return size() > 0; 110 } 111 112 // TODO: make sure that the index should not exceed MAX_WORD_LENGTH 113 public int getCodeAt(int index) { 114 if (index >= MAX_WORD_LENGTH) { 115 return -1; 116 } 117 return mPrimaryKeyCodes[index]; 118 } 119 120 public InputPointers getInputPointers() { 121 return mInputPointers; 122 } 123 124 private static boolean isFirstCharCapitalized(final int index, final int codePoint, 125 final boolean previous) { 126 if (index == 0) return Character.isUpperCase(codePoint); 127 return previous && !Character.isUpperCase(codePoint); 128 } 129 130 /** 131 * Add a new keystroke, with the pressed key's code point with the touch point coordinates. 132 */ 133 public void add(final int primaryCode, final int keyX, final int keyY) { 134 final int newIndex = size(); 135 mTypedWord.appendCodePoint(primaryCode); 136 refreshSize(); 137 if (newIndex < MAX_WORD_LENGTH) { 138 mPrimaryKeyCodes[newIndex] = primaryCode >= Constants.CODE_SPACE 139 ? Character.toLowerCase(primaryCode) : primaryCode; 140 // In the batch input mode, the {@code mInputPointers} holds batch input points and 141 // shouldn't be overridden by the "typed key" coordinates 142 // (See {@link #setBatchInputWord}). 143 if (!mIsBatchMode) { 144 // TODO: Set correct pointer id and time 145 mInputPointers.addPointer(newIndex, keyX, keyY, 0, 0); 146 } 147 } 148 mIsFirstCharCapitalized = isFirstCharCapitalized( 149 newIndex, primaryCode, mIsFirstCharCapitalized); 150 if (Character.isUpperCase(primaryCode)) mCapsCount++; 151 if (Character.isDigit(primaryCode)) mDigitsCount++; 152 if (Constants.CODE_SINGLE_QUOTE == primaryCode) { 153 ++mTrailingSingleQuotesCount; 154 } else { 155 mTrailingSingleQuotesCount = 0; 156 } 157 mAutoCorrection = null; 158 } 159 160 public void setBatchInputPointers(final InputPointers batchPointers) { 161 mInputPointers.set(batchPointers); 162 mIsBatchMode = true; 163 } 164 165 public void setBatchInputWord(final String word) { 166 reset(); 167 mIsBatchMode = true; 168 final int length = word.length(); 169 for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) { 170 final int codePoint = Character.codePointAt(word, i); 171 // We don't want to override the batch input points that are held in mInputPointers 172 // (See {@link #add(int,int,int)}). 173 add(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); 174 } 175 } 176 177 /** 178 * Internal method to retrieve reasonable proximity info for a character. 179 */ 180 private void addKeyInfo(final int codePoint, final Keyboard keyboard) { 181 final int x, y; 182 final Key key; 183 if (keyboard != null && (key = keyboard.getKey(codePoint)) != null) { 184 x = key.mX + key.mWidth / 2; 185 y = key.mY + key.mHeight / 2; 186 } else { 187 x = Constants.NOT_A_COORDINATE; 188 y = Constants.NOT_A_COORDINATE; 189 } 190 add(codePoint, x, y); 191 } 192 193 /** 194 * Set the currently composing word to the one passed as an argument. 195 * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity. 196 */ 197 public void setComposingWord(final CharSequence word, final Keyboard keyboard) { 198 reset(); 199 final int length = word.length(); 200 for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) { 201 final int codePoint = Character.codePointAt(word, i); 202 addKeyInfo(codePoint, keyboard); 203 } 204 mIsResumed = true; 205 } 206 207 /** 208 * Delete the last keystroke as a result of hitting backspace. 209 */ 210 public void deleteLast() { 211 final int size = size(); 212 if (size > 0) { 213 // Note: mTypedWord.length() and mCodes.length differ when there are surrogate pairs 214 final int stringBuilderLength = mTypedWord.length(); 215 if (stringBuilderLength < size) { 216 throw new RuntimeException( 217 "In WordComposer: mCodes and mTypedWords have non-matching lengths"); 218 } 219 final int lastChar = mTypedWord.codePointBefore(stringBuilderLength); 220 if (Character.isSupplementaryCodePoint(lastChar)) { 221 mTypedWord.delete(stringBuilderLength - 2, stringBuilderLength); 222 } else { 223 mTypedWord.deleteCharAt(stringBuilderLength - 1); 224 } 225 if (Character.isUpperCase(lastChar)) mCapsCount--; 226 if (Character.isDigit(lastChar)) mDigitsCount--; 227 refreshSize(); 228 } 229 // We may have deleted the last one. 230 if (0 == size()) { 231 mIsFirstCharCapitalized = false; 232 } 233 if (mTrailingSingleQuotesCount > 0) { 234 --mTrailingSingleQuotesCount; 235 } else { 236 int i = mTypedWord.length(); 237 while (i > 0) { 238 i = mTypedWord.offsetByCodePoints(i, -1); 239 if (Constants.CODE_SINGLE_QUOTE != mTypedWord.codePointAt(i)) break; 240 ++mTrailingSingleQuotesCount; 241 } 242 } 243 mAutoCorrection = null; 244 } 245 246 /** 247 * Returns the word as it was typed, without any correction applied. 248 * @return the word that was typed so far. Never returns null. 249 */ 250 public String getTypedWord() { 251 return mTypedWord.toString(); 252 } 253 254 /** 255 * Whether or not the user typed a capital letter as the first letter in the word 256 * @return capitalization preference 257 */ 258 public boolean isFirstCharCapitalized() { 259 return mIsFirstCharCapitalized; 260 } 261 262 public int trailingSingleQuotesCount() { 263 return mTrailingSingleQuotesCount; 264 } 265 266 /** 267 * Whether or not all of the user typed chars are upper case 268 * @return true if all user typed chars are upper case, false otherwise 269 */ 270 public boolean isAllUpperCase() { 271 if (size() <= 1) { 272 return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED 273 || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFT_LOCKED; 274 } else { 275 return mCapsCount == size(); 276 } 277 } 278 279 public boolean wasShiftedNoLock() { 280 return mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED 281 || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFTED; 282 } 283 284 /** 285 * Returns true if more than one character is upper case, otherwise returns false. 286 */ 287 public boolean isMostlyCaps() { 288 return mCapsCount > 1; 289 } 290 291 /** 292 * Returns true if we have digits in the composing word. 293 */ 294 public boolean hasDigits() { 295 return mDigitsCount > 0; 296 } 297 298 /** 299 * Saves the caps mode at the start of composing. 300 * 301 * WordComposer needs to know about this for several reasons. The first is, we need to know 302 * after the fact what the reason was, to register the correct form into the user history 303 * dictionary: if the word was automatically capitalized, we should insert it in all-lower 304 * case but if it's a manual pressing of shift, then it should be inserted as is. 305 * Also, batch input needs to know about the current caps mode to display correctly 306 * capitalized suggestions. 307 * @param mode the mode at the time of start 308 */ 309 public void setCapitalizedModeAtStartComposingTime(final int mode) { 310 mCapitalizedMode = mode; 311 } 312 313 /** 314 * Returns whether the word was automatically capitalized. 315 * @return whether the word was automatically capitalized 316 */ 317 public boolean wasAutoCapitalized() { 318 return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED 319 || mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED; 320 } 321 322 /** 323 * Sets the auto-correction for this word. 324 */ 325 public void setAutoCorrection(final String correction) { 326 mAutoCorrection = correction; 327 } 328 329 /** 330 * @return the auto-correction for this word, or null if none. 331 */ 332 public String getAutoCorrectionOrNull() { 333 return mAutoCorrection; 334 } 335 336 /** 337 * @return whether we started composing this word by resuming suggestion on an existing string 338 */ 339 public boolean isResumed() { 340 return mIsResumed; 341 } 342 343 // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above. 344 public LastComposedWord commitWord(final int type, final String committedWord, 345 final String separatorString, final String prevWord) { 346 // Note: currently, we come here whenever we commit a word. If it's a MANUAL_PICK 347 // or a DECIDED_WORD we may cancel the commit later; otherwise, we should deactivate 348 // the last composed word to ensure this does not happen. 349 final int[] primaryKeyCodes = mPrimaryKeyCodes; 350 mPrimaryKeyCodes = new int[MAX_WORD_LENGTH]; 351 final LastComposedWord lastComposedWord = new LastComposedWord(primaryKeyCodes, 352 mInputPointers, mTypedWord.toString(), committedWord, separatorString, 353 prevWord, mCapitalizedMode); 354 mInputPointers.reset(); 355 if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD 356 && type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) { 357 lastComposedWord.deactivate(); 358 } 359 mCapsCount = 0; 360 mDigitsCount = 0; 361 mIsBatchMode = false; 362 mTypedWord.setLength(0); 363 mCodePointSize = 0; 364 mTrailingSingleQuotesCount = 0; 365 mIsFirstCharCapitalized = false; 366 mCapitalizedMode = CAPS_MODE_OFF; 367 refreshSize(); 368 mAutoCorrection = null; 369 mIsResumed = false; 370 return lastComposedWord; 371 } 372 373 public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) { 374 mPrimaryKeyCodes = lastComposedWord.mPrimaryKeyCodes; 375 mInputPointers.set(lastComposedWord.mInputPointers); 376 mTypedWord.setLength(0); 377 mTypedWord.append(lastComposedWord.mTypedWord); 378 refreshSize(); 379 mCapitalizedMode = lastComposedWord.mCapitalizedMode; 380 mAutoCorrection = null; // This will be filled by the next call to updateSuggestion. 381 mIsResumed = true; 382 } 383 384 public boolean isBatchMode() { 385 return mIsBatchMode; 386 } 387} 388