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