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