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