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.event.CombinerChain; 20 import com.android.inputmethod.event.Event; 21 import com.android.inputmethod.latin.define.DebugFlags; 22 import com.android.inputmethod.latin.utils.CoordinateUtils; 23 import com.android.inputmethod.latin.utils.StringUtils; 24 25 import java.util.ArrayList; 26 import java.util.Collections; 27 28 import javax.annotation.Nonnull; 29 30 /** 31 * A place to store the currently composing word with information such as adjacent key codes as well 32 */ 33 public final class WordComposer { 34 private static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH; 35 private static final boolean DBG = DebugFlags.DEBUG_ENABLED; 36 37 public static final int CAPS_MODE_OFF = 0; 38 // 1 is shift bit, 2 is caps bit, 4 is auto bit but this is just a convention as these bits 39 // aren't used anywhere in the code 40 public static final int CAPS_MODE_MANUAL_SHIFTED = 0x1; 41 public static final int CAPS_MODE_MANUAL_SHIFT_LOCKED = 0x3; 42 public static final int CAPS_MODE_AUTO_SHIFTED = 0x5; 43 public static final int CAPS_MODE_AUTO_SHIFT_LOCKED = 0x7; 44 45 private CombinerChain mCombinerChain; 46 private String mCombiningSpec; // Memory so that we don't uselessly recreate the combiner chain 47 48 // The list of events that served to compose this string. 49 private final ArrayList<Event> mEvents; 50 private final InputPointers mInputPointers = new InputPointers(MAX_WORD_LENGTH); 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 CharSequence mTypedWordCache; 64 private int mCapsCount; 65 private int mDigitsCount; 66 private int mCapitalizedMode; 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 composing word has the only first char capitalized. 76 */ 77 private boolean mIsOnlyFirstCharCapitalized; 78 WordComposer()79 public WordComposer() { 80 mCombinerChain = new CombinerChain(""); 81 mEvents = new ArrayList<>(); 82 mAutoCorrection = null; 83 mIsResumed = false; 84 mIsBatchMode = false; 85 mCursorPositionWithinWord = 0; 86 mRejectedBatchModeSuggestion = null; 87 refreshTypedWordCache(); 88 } 89 90 /** 91 * Restart the combiners, possibly with a new spec. 92 * @param combiningSpec The spec string for combining. This is found in the extra value. 93 */ restartCombining(final String combiningSpec)94 public void restartCombining(final String combiningSpec) { 95 final String nonNullCombiningSpec = null == combiningSpec ? "" : combiningSpec; 96 if (!nonNullCombiningSpec.equals(mCombiningSpec)) { 97 mCombinerChain = new CombinerChain( 98 mCombinerChain.getComposingWordWithCombiningFeedback().toString(), 99 CombinerChain.createCombiners(nonNullCombiningSpec)); 100 mCombiningSpec = nonNullCombiningSpec; 101 } 102 } 103 104 /** 105 * Clear out the keys registered so far. 106 */ reset()107 public void reset() { 108 mCombinerChain.reset(); 109 mEvents.clear(); 110 mAutoCorrection = null; 111 mCapsCount = 0; 112 mDigitsCount = 0; 113 mIsOnlyFirstCharCapitalized = false; 114 mIsResumed = false; 115 mIsBatchMode = false; 116 mCursorPositionWithinWord = 0; 117 mRejectedBatchModeSuggestion = null; 118 refreshTypedWordCache(); 119 } 120 refreshTypedWordCache()121 private final void refreshTypedWordCache() { 122 mTypedWordCache = mCombinerChain.getComposingWordWithCombiningFeedback(); 123 mCodePointSize = Character.codePointCount(mTypedWordCache, 0, mTypedWordCache.length()); 124 } 125 126 /** 127 * Number of keystrokes in the composing word. 128 * @return the number of keystrokes 129 */ 130 // This may be made public if need be, but right now it's not used anywhere size()131 /* package for tests */ int size() { 132 return mCodePointSize; 133 } 134 135 /** 136 * Copy the code points in the typed word to a destination array of ints. 137 * 138 * If the array is too small to hold the code points in the typed word, nothing is copied and 139 * -1 is returned. 140 * 141 * @param destination the array of ints. 142 * @return the number of copied code points. 143 */ copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount( final int[] destination)144 public int copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount( 145 final int[] destination) { 146 // This method can be called on a separate thread and mTypedWordCache can change while we 147 // are executing this method. 148 final String typedWord = mTypedWordCache.toString(); 149 // lastIndex is exclusive 150 final int lastIndex = typedWord.length() 151 - StringUtils.getTrailingSingleQuotesCount(typedWord); 152 if (lastIndex <= 0) { 153 // The string is empty or contains only single quotes. 154 return 0; 155 } 156 157 // The following function counts the number of code points in the text range which begins 158 // at index 0 and extends to the character at lastIndex. 159 final int codePointSize = Character.codePointCount(typedWord, 0, lastIndex); 160 if (codePointSize > destination.length) { 161 return -1; 162 } 163 return StringUtils.copyCodePointsAndReturnCodePointCount(destination, typedWord, 0, 164 lastIndex, true /* downCase */); 165 } 166 isSingleLetter()167 public boolean isSingleLetter() { 168 return size() == 1; 169 } 170 isComposingWord()171 public final boolean isComposingWord() { 172 return size() > 0; 173 } 174 getInputPointers()175 public InputPointers getInputPointers() { 176 return mInputPointers; 177 } 178 179 /** 180 * Process an event and return an event, and return a processed event to apply. 181 * @param event the unprocessed event. 182 * @return the processed event. Never null, but may be marked as consumed. 183 */ 184 @Nonnull processEvent(final Event event)185 public Event processEvent(final Event event) { 186 final Event processedEvent = mCombinerChain.processEvent(mEvents, event); 187 // The retained state of the combiner chain may have changed while processing the event, 188 // so we need to update our cache. 189 refreshTypedWordCache(); 190 mEvents.add(event); 191 return processedEvent; 192 } 193 194 /** 195 * Apply a processed input event. 196 * 197 * All input events should be supported, including software/hardware events, characters as well 198 * as deletions, multiple inputs and gestures. 199 * 200 * @param event the event to apply. Must not be null. 201 */ applyProcessedEvent(final Event event)202 public void applyProcessedEvent(final Event event) { 203 mCombinerChain.applyProcessedEvent(event); 204 final int primaryCode = event.mCodePoint; 205 final int keyX = event.mX; 206 final int keyY = event.mY; 207 final int newIndex = size(); 208 refreshTypedWordCache(); 209 mCursorPositionWithinWord = mCodePointSize; 210 // We may have deleted the last one. 211 if (0 == mCodePointSize) { 212 mIsOnlyFirstCharCapitalized = false; 213 } 214 if (Constants.CODE_DELETE != event.mKeyCode) { 215 if (newIndex < MAX_WORD_LENGTH) { 216 // In the batch input mode, the {@code mInputPointers} holds batch input points and 217 // shouldn't be overridden by the "typed key" coordinates 218 // (See {@link #setBatchInputWord}). 219 if (!mIsBatchMode) { 220 // TODO: Set correct pointer id and time 221 mInputPointers.addPointerAt(newIndex, keyX, keyY, 0, 0); 222 } 223 } 224 if (0 == newIndex) { 225 mIsOnlyFirstCharCapitalized = Character.isUpperCase(primaryCode); 226 } else { 227 mIsOnlyFirstCharCapitalized = mIsOnlyFirstCharCapitalized 228 && !Character.isUpperCase(primaryCode); 229 } 230 if (Character.isUpperCase(primaryCode)) mCapsCount++; 231 if (Character.isDigit(primaryCode)) mDigitsCount++; 232 } 233 mAutoCorrection = null; 234 } 235 setCursorPositionWithinWord(final int posWithinWord)236 public void setCursorPositionWithinWord(final int posWithinWord) { 237 mCursorPositionWithinWord = posWithinWord; 238 // TODO: compute where that puts us inside the events 239 } 240 isCursorFrontOrMiddleOfComposingWord()241 public boolean isCursorFrontOrMiddleOfComposingWord() { 242 if (DBG && mCursorPositionWithinWord > mCodePointSize) { 243 throw new RuntimeException("Wrong cursor position : " + mCursorPositionWithinWord 244 + "in a word of size " + mCodePointSize); 245 } 246 return mCursorPositionWithinWord != mCodePointSize; 247 } 248 249 /** 250 * When the cursor is moved by the user, we need to update its position. 251 * If it falls inside the currently composing word, we don't reset the composition, and 252 * only update the cursor position. 253 * 254 * @param expectedMoveAmount How many java chars to move the cursor. Negative values move 255 * the cursor backward, positive values move the cursor forward. 256 * @return true if the cursor is still inside the composing word, false otherwise. 257 */ moveCursorByAndReturnIfInsideComposingWord(final int expectedMoveAmount)258 public boolean moveCursorByAndReturnIfInsideComposingWord(final int expectedMoveAmount) { 259 // TODO: should uncommit the composing feedback 260 mCombinerChain.reset(); 261 int actualMoveAmountWithinWord = 0; 262 int cursorPos = mCursorPositionWithinWord; 263 // TODO: Don't make that copy. We can do this directly from mTypedWordCache. 264 final int[] codePoints = StringUtils.toCodePointArray(mTypedWordCache); 265 if (expectedMoveAmount >= 0) { 266 // Moving the cursor forward for the expected amount or until the end of the word has 267 // been reached, whichever comes first. 268 while (actualMoveAmountWithinWord < expectedMoveAmount && cursorPos < mCodePointSize) { 269 actualMoveAmountWithinWord += Character.charCount(codePoints[cursorPos]); 270 ++cursorPos; 271 } 272 } else { 273 // Moving the cursor backward for the expected amount or until the start of the word 274 // has been reached, whichever comes first. 275 while (actualMoveAmountWithinWord > expectedMoveAmount && cursorPos > 0) { 276 --cursorPos; 277 actualMoveAmountWithinWord -= Character.charCount(codePoints[cursorPos]); 278 } 279 } 280 // If the actual and expected amounts differ, we crossed the start or the end of the word 281 // so the result would not be inside the composing word. 282 if (actualMoveAmountWithinWord != expectedMoveAmount) return false; 283 mCursorPositionWithinWord = cursorPos; 284 return true; 285 } 286 setBatchInputPointers(final InputPointers batchPointers)287 public void setBatchInputPointers(final InputPointers batchPointers) { 288 mInputPointers.set(batchPointers); 289 mIsBatchMode = true; 290 } 291 setBatchInputWord(final String word)292 public void setBatchInputWord(final String word) { 293 reset(); 294 mIsBatchMode = true; 295 final int length = word.length(); 296 for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) { 297 final int codePoint = Character.codePointAt(word, i); 298 // We don't want to override the batch input points that are held in mInputPointers 299 // (See {@link #add(int,int,int)}). 300 final Event processedEvent = 301 processEvent(Event.createEventForCodePointFromUnknownSource(codePoint)); 302 applyProcessedEvent(processedEvent); 303 } 304 } 305 306 /** 307 * Set the currently composing word to the one passed as an argument. 308 * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity. 309 * @param codePoints the code points to set as the composing word. 310 * @param coordinates the x, y coordinates of the key in the CoordinateUtils format 311 */ setComposingWord(final int[] codePoints, final int[] coordinates)312 public void setComposingWord(final int[] codePoints, final int[] coordinates) { 313 reset(); 314 final int length = codePoints.length; 315 for (int i = 0; i < length; ++i) { 316 final Event processedEvent = 317 processEvent(Event.createEventForCodePointFromAlreadyTypedText(codePoints[i], 318 CoordinateUtils.xFromArray(coordinates, i), 319 CoordinateUtils.yFromArray(coordinates, i))); 320 applyProcessedEvent(processedEvent); 321 } 322 mIsResumed = true; 323 } 324 325 /** 326 * Returns the word as it was typed, without any correction applied. 327 * @return the word that was typed so far. Never returns null. 328 */ getTypedWord()329 public String getTypedWord() { 330 return mTypedWordCache.toString(); 331 } 332 333 /** 334 * Whether this composer is composing or about to compose a word in which only the first letter 335 * is a capital. 336 * 337 * If we do have a composing word, we just return whether the word has indeed only its first 338 * character capitalized. If we don't, then we return a value based on the capitalized mode, 339 * which tell us what is likely to happen for the next composing word. 340 * 341 * @return capitalization preference 342 */ isOrWillBeOnlyFirstCharCapitalized()343 public boolean isOrWillBeOnlyFirstCharCapitalized() { 344 return isComposingWord() ? mIsOnlyFirstCharCapitalized 345 : (CAPS_MODE_OFF != mCapitalizedMode); 346 } 347 348 /** 349 * Whether or not all of the user typed chars are upper case 350 * @return true if all user typed chars are upper case, false otherwise 351 */ isAllUpperCase()352 public boolean isAllUpperCase() { 353 if (size() <= 1) { 354 return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED 355 || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFT_LOCKED; 356 } else { 357 return mCapsCount == size(); 358 } 359 } 360 wasShiftedNoLock()361 public boolean wasShiftedNoLock() { 362 return mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED 363 || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFTED; 364 } 365 366 /** 367 * Returns true if more than one character is upper case, otherwise returns false. 368 */ isMostlyCaps()369 public boolean isMostlyCaps() { 370 return mCapsCount > 1; 371 } 372 373 /** 374 * Returns true if we have digits in the composing word. 375 */ hasDigits()376 public boolean hasDigits() { 377 return mDigitsCount > 0; 378 } 379 380 /** 381 * Saves the caps mode at the start of composing. 382 * 383 * WordComposer needs to know about the caps mode for several reasons. The first is, we need 384 * to know after the fact what the reason was, to register the correct form into the user 385 * history dictionary: if the word was automatically capitalized, we should insert it in 386 * all-lower case but if it's a manual pressing of shift, then it should be inserted as is. 387 * Also, batch input needs to know about the current caps mode to display correctly 388 * capitalized suggestions. 389 * @param mode the mode at the time of start 390 */ setCapitalizedModeAtStartComposingTime(final int mode)391 public void setCapitalizedModeAtStartComposingTime(final int mode) { 392 mCapitalizedMode = mode; 393 } 394 395 /** 396 * Before fetching suggestions, we don't necessarily know about the capitalized mode yet. 397 * 398 * If we don't have a composing word yet, we take a note of this mode so that we can then 399 * supply this information to the suggestion process. If we have a composing word, then 400 * the previous mode has priority over this. 401 * @param mode the mode just before fetching suggestions 402 */ adviseCapitalizedModeBeforeFetchingSuggestions(final int mode)403 public void adviseCapitalizedModeBeforeFetchingSuggestions(final int mode) { 404 if (!isComposingWord()) { 405 mCapitalizedMode = mode; 406 } 407 } 408 409 /** 410 * Returns whether the word was automatically capitalized. 411 * @return whether the word was automatically capitalized 412 */ wasAutoCapitalized()413 public boolean wasAutoCapitalized() { 414 return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED 415 || mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED; 416 } 417 418 /** 419 * Sets the auto-correction for this word. 420 */ setAutoCorrection(final String correction)421 public void setAutoCorrection(final String correction) { 422 mAutoCorrection = correction; 423 } 424 425 /** 426 * @return the auto-correction for this word, or null if none. 427 */ getAutoCorrectionOrNull()428 public String getAutoCorrectionOrNull() { 429 return mAutoCorrection; 430 } 431 432 /** 433 * @return whether we started composing this word by resuming suggestion on an existing string 434 */ isResumed()435 public boolean isResumed() { 436 return mIsResumed; 437 } 438 439 // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above. 440 // committedWord should contain suggestion spans if applicable. commitWord(final int type, final CharSequence committedWord, final String separatorString, final PrevWordsInfo prevWordsInfo)441 public LastComposedWord commitWord(final int type, final CharSequence committedWord, 442 final String separatorString, final PrevWordsInfo prevWordsInfo) { 443 // Note: currently, we come here whenever we commit a word. If it's a MANUAL_PICK 444 // or a DECIDED_WORD we may cancel the commit later; otherwise, we should deactivate 445 // the last composed word to ensure this does not happen. 446 final LastComposedWord lastComposedWord = new LastComposedWord(mEvents, 447 mInputPointers, mTypedWordCache.toString(), committedWord, separatorString, 448 prevWordsInfo, mCapitalizedMode); 449 mInputPointers.reset(); 450 if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD 451 && type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) { 452 lastComposedWord.deactivate(); 453 } 454 mCapsCount = 0; 455 mDigitsCount = 0; 456 mIsBatchMode = false; 457 mCombinerChain.reset(); 458 mEvents.clear(); 459 mCodePointSize = 0; 460 mIsOnlyFirstCharCapitalized = false; 461 mCapitalizedMode = CAPS_MODE_OFF; 462 refreshTypedWordCache(); 463 mAutoCorrection = null; 464 mCursorPositionWithinWord = 0; 465 mIsResumed = false; 466 mRejectedBatchModeSuggestion = null; 467 return lastComposedWord; 468 } 469 resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord)470 public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) { 471 mEvents.clear(); 472 Collections.copy(mEvents, lastComposedWord.mEvents); 473 mInputPointers.set(lastComposedWord.mInputPointers); 474 mCombinerChain.reset(); 475 refreshTypedWordCache(); 476 mCapitalizedMode = lastComposedWord.mCapitalizedMode; 477 mAutoCorrection = null; // This will be filled by the next call to updateSuggestion. 478 mCursorPositionWithinWord = mCodePointSize; 479 mRejectedBatchModeSuggestion = null; 480 mIsResumed = true; 481 } 482 isBatchMode()483 public boolean isBatchMode() { 484 return mIsBatchMode; 485 } 486 setRejectedBatchModeSuggestion(final String rejectedSuggestion)487 public void setRejectedBatchModeSuggestion(final String rejectedSuggestion) { 488 mRejectedBatchModeSuggestion = rejectedSuggestion; 489 } 490 getRejectedBatchModeSuggestion()491 public String getRejectedBatchModeSuggestion() { 492 return mRejectedBatchModeSuggestion; 493 } 494 } 495