1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.inputmethod.latin; 18 19 import com.android.inputmethod.keyboard.Key; 20 import com.android.inputmethod.keyboard.Keyboard; 21 import com.android.inputmethod.latin.utils.StringUtils; 22 23 import java.util.Arrays; 24 25 /** 26 * A place to store the currently composing word with information such as adjacent key codes as well 27 */ 28 public final class WordComposer { 29 private static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH; 30 private static final boolean DBG = LatinImeLogger.sDBG; 31 32 public static final int CAPS_MODE_OFF = 0; 33 // 1 is shift bit, 2 is caps bit, 4 is auto bit but this is just a convention as these bits 34 // aren't used anywhere in the code 35 public static final int CAPS_MODE_MANUAL_SHIFTED = 0x1; 36 public static final int CAPS_MODE_MANUAL_SHIFT_LOCKED = 0x3; 37 public static final int CAPS_MODE_AUTO_SHIFTED = 0x5; 38 public static final int CAPS_MODE_AUTO_SHIFT_LOCKED = 0x7; 39 40 // An array of code points representing the characters typed so far. 41 // The array is limited to MAX_WORD_LENGTH code points, but mTypedWord extends past that 42 // and mCodePointSize can go past that. If mCodePointSize is greater than MAX_WORD_LENGTH, 43 // this just does not contain the associated code points past MAX_WORD_LENGTH. 44 private int[] mPrimaryKeyCodes; 45 private final InputPointers mInputPointers = new InputPointers(MAX_WORD_LENGTH); 46 // This is the typed word, as a StringBuilder. This has the same contents as mPrimaryKeyCodes 47 // but under a StringBuilder representation for ease of use, depending on what is more useful 48 // at any given time. However this is not limited in size, while mPrimaryKeyCodes is limited 49 // to MAX_WORD_LENGTH code points. 50 private final StringBuilder mTypedWord; 51 private String mAutoCorrection; 52 private boolean mIsResumed; 53 private boolean mIsBatchMode; 54 // A memory of the last rejected batch mode suggestion, if any. This goes like this: the user 55 // gestures a word, is displeased with the results and hits backspace, then gestures again. 56 // At the very least we should avoid re-suggesting the same thing, and to do that we memorize 57 // the rejected suggestion in this variable. 58 // TODO: this should be done in a comprehensive way by the User History feature instead of 59 // as an ad-hockery here. 60 private String mRejectedBatchModeSuggestion; 61 62 // Cache these values for performance 63 private int mCapsCount; 64 private int mDigitsCount; 65 private int mCapitalizedMode; 66 private int mTrailingSingleQuotesCount; 67 // This is the number of code points entered so far. This is not limited to MAX_WORD_LENGTH. 68 // In general, this contains the size of mPrimaryKeyCodes, except when this is greater than 69 // MAX_WORD_LENGTH in which case mPrimaryKeyCodes only contain the first MAX_WORD_LENGTH 70 // code points. 71 private int mCodePointSize; 72 private int mCursorPositionWithinWord; 73 74 /** 75 * Whether the user chose to capitalize the first char of the word. 76 */ 77 private boolean mIsFirstCharCapitalized; 78 79 public WordComposer() { 80 mPrimaryKeyCodes = new int[MAX_WORD_LENGTH]; 81 mTypedWord = new StringBuilder(MAX_WORD_LENGTH); 82 mAutoCorrection = null; 83 mTrailingSingleQuotesCount = 0; 84 mIsResumed = false; 85 mIsBatchMode = false; 86 mCursorPositionWithinWord = 0; 87 mRejectedBatchModeSuggestion = null; 88 refreshSize(); 89 } 90 91 public WordComposer(final WordComposer source) { 92 mPrimaryKeyCodes = Arrays.copyOf(source.mPrimaryKeyCodes, source.mPrimaryKeyCodes.length); 93 mTypedWord = new StringBuilder(source.mTypedWord); 94 mInputPointers.copy(source.mInputPointers); 95 mCapsCount = source.mCapsCount; 96 mDigitsCount = source.mDigitsCount; 97 mIsFirstCharCapitalized = source.mIsFirstCharCapitalized; 98 mCapitalizedMode = source.mCapitalizedMode; 99 mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount; 100 mIsResumed = source.mIsResumed; 101 mIsBatchMode = source.mIsBatchMode; 102 mCursorPositionWithinWord = source.mCursorPositionWithinWord; 103 mRejectedBatchModeSuggestion = source.mRejectedBatchModeSuggestion; 104 refreshSize(); 105 } 106 107 /** 108 * Clear out the keys registered so far. 109 */ 110 public void reset() { 111 mTypedWord.setLength(0); 112 mAutoCorrection = null; 113 mCapsCount = 0; 114 mDigitsCount = 0; 115 mIsFirstCharCapitalized = false; 116 mTrailingSingleQuotesCount = 0; 117 mIsResumed = false; 118 mIsBatchMode = false; 119 mCursorPositionWithinWord = 0; 120 mRejectedBatchModeSuggestion = null; 121 refreshSize(); 122 } 123 124 private final void refreshSize() { 125 mCodePointSize = mTypedWord.codePointCount(0, mTypedWord.length()); 126 } 127 128 /** 129 * Number of keystrokes in the composing word. 130 * @return the number of keystrokes 131 */ 132 public final int size() { 133 return mCodePointSize; 134 } 135 136 public final boolean isComposingWord() { 137 return size() > 0; 138 } 139 140 // TODO: make sure that the index should not exceed MAX_WORD_LENGTH 141 public int getCodeAt(int index) { 142 if (index >= MAX_WORD_LENGTH) { 143 return -1; 144 } 145 return mPrimaryKeyCodes[index]; 146 } 147 148 public int getCodeBeforeCursor() { 149 if (mCursorPositionWithinWord < 1 || mCursorPositionWithinWord > mPrimaryKeyCodes.length) { 150 return Constants.NOT_A_CODE; 151 } 152 return mPrimaryKeyCodes[mCursorPositionWithinWord - 1]; 153 } 154 155 public InputPointers getInputPointers() { 156 return mInputPointers; 157 } 158 159 private static boolean isFirstCharCapitalized(final int index, final int codePoint, 160 final boolean previous) { 161 if (index == 0) return Character.isUpperCase(codePoint); 162 return previous && !Character.isUpperCase(codePoint); 163 } 164 165 /** 166 * Add a new keystroke, with the pressed key's code point with the touch point coordinates. 167 */ 168 public void add(final int primaryCode, final int keyX, final int keyY) { 169 final int newIndex = size(); 170 mTypedWord.appendCodePoint(primaryCode); 171 refreshSize(); 172 mCursorPositionWithinWord = mCodePointSize; 173 if (newIndex < MAX_WORD_LENGTH) { 174 mPrimaryKeyCodes[newIndex] = primaryCode >= Constants.CODE_SPACE 175 ? Character.toLowerCase(primaryCode) : primaryCode; 176 // In the batch input mode, the {@code mInputPointers} holds batch input points and 177 // shouldn't be overridden by the "typed key" coordinates 178 // (See {@link #setBatchInputWord}). 179 if (!mIsBatchMode) { 180 // TODO: Set correct pointer id and time 181 mInputPointers.addPointer(newIndex, keyX, keyY, 0, 0); 182 } 183 } 184 mIsFirstCharCapitalized = isFirstCharCapitalized( 185 newIndex, primaryCode, mIsFirstCharCapitalized); 186 if (Character.isUpperCase(primaryCode)) mCapsCount++; 187 if (Character.isDigit(primaryCode)) mDigitsCount++; 188 if (Constants.CODE_SINGLE_QUOTE == primaryCode) { 189 ++mTrailingSingleQuotesCount; 190 } else { 191 mTrailingSingleQuotesCount = 0; 192 } 193 mAutoCorrection = null; 194 } 195 196 public void setCursorPositionWithinWord(final int posWithinWord) { 197 mCursorPositionWithinWord = posWithinWord; 198 } 199 200 public boolean isCursorFrontOrMiddleOfComposingWord() { 201 if (DBG && mCursorPositionWithinWord > mCodePointSize) { 202 throw new RuntimeException("Wrong cursor position : " + mCursorPositionWithinWord 203 + "in a word of size " + mCodePointSize); 204 } 205 return mCursorPositionWithinWord != mCodePointSize; 206 } 207 208 /** 209 * When the cursor is moved by the user, we need to update its position. 210 * If it falls inside the currently composing word, we don't reset the composition, and 211 * only update the cursor position. 212 * 213 * @param expectedMoveAmount How many java chars to move the cursor. Negative values move 214 * the cursor backward, positive values move the cursor forward. 215 * @return true if the cursor is still inside the composing word, false otherwise. 216 */ 217 public boolean moveCursorByAndReturnIfInsideComposingWord(final int expectedMoveAmount) { 218 int actualMoveAmountWithinWord = 0; 219 int cursorPos = mCursorPositionWithinWord; 220 final int[] codePoints; 221 if (mCodePointSize >= MAX_WORD_LENGTH) { 222 // If we have more than MAX_WORD_LENGTH characters, we don't have everything inside 223 // mPrimaryKeyCodes. This should be rare enough that we can afford to just compute 224 // the array on the fly when this happens. 225 codePoints = StringUtils.toCodePointArray(mTypedWord.toString()); 226 } else { 227 codePoints = mPrimaryKeyCodes; 228 } 229 if (expectedMoveAmount >= 0) { 230 // Moving the cursor forward for the expected amount or until the end of the word has 231 // been reached, whichever comes first. 232 while (actualMoveAmountWithinWord < expectedMoveAmount && cursorPos < mCodePointSize) { 233 actualMoveAmountWithinWord += Character.charCount(codePoints[cursorPos]); 234 ++cursorPos; 235 } 236 } else { 237 // Moving the cursor backward for the expected amount or until the start of the word 238 // has been reached, whichever comes first. 239 while (actualMoveAmountWithinWord > expectedMoveAmount && cursorPos > 0) { 240 --cursorPos; 241 actualMoveAmountWithinWord -= Character.charCount(codePoints[cursorPos]); 242 } 243 } 244 // If the actual and expected amounts differ, we crossed the start or the end of the word 245 // so the result would not be inside the composing word. 246 if (actualMoveAmountWithinWord != expectedMoveAmount) return false; 247 mCursorPositionWithinWord = cursorPos; 248 return true; 249 } 250 251 public void setBatchInputPointers(final InputPointers batchPointers) { 252 mInputPointers.set(batchPointers); 253 mIsBatchMode = true; 254 } 255 256 public void setBatchInputWord(final String word) { 257 reset(); 258 mIsBatchMode = true; 259 final int length = word.length(); 260 for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) { 261 final int codePoint = Character.codePointAt(word, i); 262 // We don't want to override the batch input points that are held in mInputPointers 263 // (See {@link #add(int,int,int)}). 264 add(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); 265 } 266 } 267 268 /** 269 * Add a dummy key by retrieving reasonable coordinates 270 */ 271 public void addKeyInfo(final int codePoint, final Keyboard keyboard) { 272 final int x, y; 273 final Key key; 274 if (keyboard != null && (key = keyboard.getKey(codePoint)) != null) { 275 x = key.getX() + key.getWidth() / 2; 276 y = key.getY() + key.getHeight() / 2; 277 } else { 278 x = Constants.NOT_A_COORDINATE; 279 y = Constants.NOT_A_COORDINATE; 280 } 281 add(codePoint, x, y); 282 } 283 284 /** 285 * Set the currently composing word to the one passed as an argument. 286 * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity. 287 */ 288 public void setComposingWord(final CharSequence word, final Keyboard keyboard) { 289 reset(); 290 final int length = word.length(); 291 for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) { 292 final int codePoint = Character.codePointAt(word, i); 293 addKeyInfo(codePoint, keyboard); 294 } 295 mIsResumed = true; 296 } 297 298 /** 299 * Delete the last keystroke as a result of hitting backspace. 300 */ 301 public void deleteLast() { 302 final int size = size(); 303 if (size > 0) { 304 // Note: mTypedWord.length() and mCodes.length differ when there are surrogate pairs 305 final int stringBuilderLength = mTypedWord.length(); 306 if (stringBuilderLength < size) { 307 throw new RuntimeException( 308 "In WordComposer: mCodes and mTypedWords have non-matching lengths"); 309 } 310 final int lastChar = mTypedWord.codePointBefore(stringBuilderLength); 311 if (Character.isSupplementaryCodePoint(lastChar)) { 312 mTypedWord.delete(stringBuilderLength - 2, stringBuilderLength); 313 } else { 314 mTypedWord.deleteCharAt(stringBuilderLength - 1); 315 } 316 if (Character.isUpperCase(lastChar)) mCapsCount--; 317 if (Character.isDigit(lastChar)) mDigitsCount--; 318 refreshSize(); 319 } 320 // We may have deleted the last one. 321 if (0 == size()) { 322 mIsFirstCharCapitalized = false; 323 } 324 if (mTrailingSingleQuotesCount > 0) { 325 --mTrailingSingleQuotesCount; 326 } else { 327 int i = mTypedWord.length(); 328 while (i > 0) { 329 i = mTypedWord.offsetByCodePoints(i, -1); 330 if (Constants.CODE_SINGLE_QUOTE != mTypedWord.codePointAt(i)) break; 331 ++mTrailingSingleQuotesCount; 332 } 333 } 334 mCursorPositionWithinWord = mCodePointSize; 335 mAutoCorrection = null; 336 } 337 338 /** 339 * Returns the word as it was typed, without any correction applied. 340 * @return the word that was typed so far. Never returns null. 341 */ 342 public String getTypedWord() { 343 return mTypedWord.toString(); 344 } 345 346 /** 347 * Whether or not the user typed a capital letter as the first letter in the word 348 * @return capitalization preference 349 */ 350 public boolean isFirstCharCapitalized() { 351 return mIsFirstCharCapitalized; 352 } 353 354 public int trailingSingleQuotesCount() { 355 return mTrailingSingleQuotesCount; 356 } 357 358 /** 359 * Whether or not all of the user typed chars are upper case 360 * @return true if all user typed chars are upper case, false otherwise 361 */ 362 public boolean isAllUpperCase() { 363 if (size() <= 1) { 364 return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED 365 || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFT_LOCKED; 366 } else { 367 return mCapsCount == size(); 368 } 369 } 370 371 public boolean wasShiftedNoLock() { 372 return mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED 373 || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFTED; 374 } 375 376 /** 377 * Returns true if more than one character is upper case, otherwise returns false. 378 */ 379 public boolean isMostlyCaps() { 380 return mCapsCount > 1; 381 } 382 383 /** 384 * Returns true if we have digits in the composing word. 385 */ 386 public boolean hasDigits() { 387 return mDigitsCount > 0; 388 } 389 390 /** 391 * Saves the caps mode at the start of composing. 392 * 393 * WordComposer needs to know about this for several reasons. The first is, we need to know 394 * after the fact what the reason was, to register the correct form into the user history 395 * dictionary: if the word was automatically capitalized, we should insert it in all-lower 396 * case but if it's a manual pressing of shift, then it should be inserted as is. 397 * Also, batch input needs to know about the current caps mode to display correctly 398 * capitalized suggestions. 399 * @param mode the mode at the time of start 400 */ 401 public void setCapitalizedModeAtStartComposingTime(final int mode) { 402 mCapitalizedMode = mode; 403 } 404 405 /** 406 * Returns whether the word was automatically capitalized. 407 * @return whether the word was automatically capitalized 408 */ 409 public boolean wasAutoCapitalized() { 410 return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED 411 || mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED; 412 } 413 414 /** 415 * Sets the auto-correction for this word. 416 */ 417 public void setAutoCorrection(final String correction) { 418 mAutoCorrection = correction; 419 } 420 421 /** 422 * @return the auto-correction for this word, or null if none. 423 */ 424 public String getAutoCorrectionOrNull() { 425 return mAutoCorrection; 426 } 427 428 /** 429 * @return whether we started composing this word by resuming suggestion on an existing string 430 */ 431 public boolean isResumed() { 432 return mIsResumed; 433 } 434 435 // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above. 436 public LastComposedWord commitWord(final int type, final String committedWord, 437 final String separatorString, final String prevWord) { 438 // Note: currently, we come here whenever we commit a word. If it's a MANUAL_PICK 439 // or a DECIDED_WORD we may cancel the commit later; otherwise, we should deactivate 440 // the last composed word to ensure this does not happen. 441 final int[] primaryKeyCodes = mPrimaryKeyCodes; 442 mPrimaryKeyCodes = new int[MAX_WORD_LENGTH]; 443 final LastComposedWord lastComposedWord = new LastComposedWord(primaryKeyCodes, 444 mInputPointers, mTypedWord.toString(), committedWord, separatorString, 445 prevWord, mCapitalizedMode); 446 mInputPointers.reset(); 447 if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD 448 && type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) { 449 lastComposedWord.deactivate(); 450 } 451 mCapsCount = 0; 452 mDigitsCount = 0; 453 mIsBatchMode = false; 454 mTypedWord.setLength(0); 455 mCodePointSize = 0; 456 mTrailingSingleQuotesCount = 0; 457 mIsFirstCharCapitalized = false; 458 mCapitalizedMode = CAPS_MODE_OFF; 459 refreshSize(); 460 mAutoCorrection = null; 461 mCursorPositionWithinWord = 0; 462 mIsResumed = false; 463 mRejectedBatchModeSuggestion = null; 464 return lastComposedWord; 465 } 466 467 public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) { 468 mPrimaryKeyCodes = lastComposedWord.mPrimaryKeyCodes; 469 mInputPointers.set(lastComposedWord.mInputPointers); 470 mTypedWord.setLength(0); 471 mTypedWord.append(lastComposedWord.mTypedWord); 472 refreshSize(); 473 mCapitalizedMode = lastComposedWord.mCapitalizedMode; 474 mAutoCorrection = null; // This will be filled by the next call to updateSuggestion. 475 mCursorPositionWithinWord = mCodePointSize; 476 mRejectedBatchModeSuggestion = null; 477 mIsResumed = true; 478 } 479 480 public boolean isBatchMode() { 481 return mIsBatchMode; 482 } 483 484 public void setRejectedBatchModeSuggestion(final String rejectedSuggestion) { 485 mRejectedBatchModeSuggestion = rejectedSuggestion; 486 } 487 488 public String getRejectedBatchModeSuggestion() { 489 return mRejectedBatchModeSuggestion; 490 } 491 } 492