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