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