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 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 N = BinaryDictionary.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(N); 40 private final StringBuilder mTypedWord; 41 private CharSequence 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[N]; 59 mTypedWord = new StringBuilder(N); 60 mAutoCorrection = null; 61 mTrailingSingleQuotesCount = 0; 62 mIsResumed = false; 63 mIsBatchMode = false; 64 refreshSize(); 65 } 66 67 public WordComposer(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 >= BinaryDictionary.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(int index, int codePoint, boolean previous) { 125 if (index == 0) return Character.isUpperCase(codePoint); 126 return previous && !Character.isUpperCase(codePoint); 127 } 128 129 /** 130 * Add a new keystroke, with the pressed key's code point with the touch point coordinates. 131 */ 132 public void add(int primaryCode, int keyX, int keyY) { 133 final int newIndex = size(); 134 mTypedWord.appendCodePoint(primaryCode); 135 refreshSize(); 136 if (newIndex < BinaryDictionary.MAX_WORD_LENGTH) { 137 mPrimaryKeyCodes[newIndex] = primaryCode >= Keyboard.CODE_SPACE 138 ? Character.toLowerCase(primaryCode) : primaryCode; 139 // In the batch input mode, the {@code mInputPointers} holds batch input points and 140 // shouldn't be overridden by the "typed key" coordinates 141 // (See {@link #setBatchInputWord}). 142 if (!mIsBatchMode) { 143 // TODO: Set correct pointer id and time 144 mInputPointers.addPointer(newIndex, keyX, keyY, 0, 0); 145 } 146 } 147 mIsFirstCharCapitalized = isFirstCharCapitalized( 148 newIndex, primaryCode, mIsFirstCharCapitalized); 149 if (Character.isUpperCase(primaryCode)) mCapsCount++; 150 if (Character.isDigit(primaryCode)) mDigitsCount++; 151 if (Keyboard.CODE_SINGLE_QUOTE == primaryCode) { 152 ++mTrailingSingleQuotesCount; 153 } else { 154 mTrailingSingleQuotesCount = 0; 155 } 156 mAutoCorrection = null; 157 } 158 159 public void setBatchInputPointers(InputPointers batchPointers) { 160 mInputPointers.set(batchPointers); 161 mIsBatchMode = true; 162 } 163 164 public void setBatchInputWord(CharSequence word) { 165 reset(); 166 mIsBatchMode = true; 167 final int length = word.length(); 168 for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) { 169 final int codePoint = Character.codePointAt(word, i); 170 // We don't want to override the batch input points that are held in mInputPointers 171 // (See {@link #add(int,int,int)}). 172 add(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); 173 } 174 } 175 176 /** 177 * Internal method to retrieve reasonable proximity info for a character. 178 */ 179 private void addKeyInfo(final int codePoint, final Keyboard keyboard) { 180 final int x, y; 181 final Key key; 182 if (keyboard != null && (key = keyboard.getKey(codePoint)) != null) { 183 x = key.mX + key.mWidth / 2; 184 y = key.mY + key.mHeight / 2; 185 } else { 186 x = Constants.NOT_A_COORDINATE; 187 y = Constants.NOT_A_COORDINATE; 188 } 189 add(codePoint, x, y); 190 } 191 192 /** 193 * Set the currently composing word to the one passed as an argument. 194 * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity. 195 */ 196 public void setComposingWord(final CharSequence word, final Keyboard keyboard) { 197 reset(); 198 final int length = word.length(); 199 for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) { 200 final int codePoint = Character.codePointAt(word, i); 201 addKeyInfo(codePoint, keyboard); 202 } 203 mIsResumed = true; 204 } 205 206 /** 207 * Delete the last keystroke as a result of hitting backspace. 208 */ 209 public void deleteLast() { 210 final int size = size(); 211 if (size > 0) { 212 // Note: mTypedWord.length() and mCodes.length differ when there are surrogate pairs 213 final int stringBuilderLength = mTypedWord.length(); 214 if (stringBuilderLength < size) { 215 throw new RuntimeException( 216 "In WordComposer: mCodes and mTypedWords have non-matching lengths"); 217 } 218 final int lastChar = mTypedWord.codePointBefore(stringBuilderLength); 219 if (Character.isSupplementaryCodePoint(lastChar)) { 220 mTypedWord.delete(stringBuilderLength - 2, stringBuilderLength); 221 } else { 222 mTypedWord.deleteCharAt(stringBuilderLength - 1); 223 } 224 if (Character.isUpperCase(lastChar)) mCapsCount--; 225 if (Character.isDigit(lastChar)) mDigitsCount--; 226 refreshSize(); 227 } 228 // We may have deleted the last one. 229 if (0 == size()) { 230 mIsFirstCharCapitalized = false; 231 } 232 if (mTrailingSingleQuotesCount > 0) { 233 --mTrailingSingleQuotesCount; 234 } else { 235 int i = mTypedWord.length(); 236 while (i > 0) { 237 i = mTypedWord.offsetByCodePoints(i, -1); 238 if (Keyboard.CODE_SINGLE_QUOTE != mTypedWord.codePointAt(i)) break; 239 ++mTrailingSingleQuotesCount; 240 } 241 } 242 mAutoCorrection = null; 243 } 244 245 /** 246 * Returns the word as it was typed, without any correction applied. 247 * @return the word that was typed so far. Never returns null. 248 */ 249 public String getTypedWord() { 250 return mTypedWord.toString(); 251 } 252 253 /** 254 * Whether or not the user typed a capital letter as the first letter in the word 255 * @return capitalization preference 256 */ 257 public boolean isFirstCharCapitalized() { 258 return mIsFirstCharCapitalized; 259 } 260 261 public int trailingSingleQuotesCount() { 262 return mTrailingSingleQuotesCount; 263 } 264 265 /** 266 * Whether or not all of the user typed chars are upper case 267 * @return true if all user typed chars are upper case, false otherwise 268 */ 269 public boolean isAllUpperCase() { 270 if (size() <= 1) { 271 return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED 272 || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFT_LOCKED; 273 } else { 274 return mCapsCount == size(); 275 } 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 String separatorString, 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, separatorString, 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