• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
WordComposer()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 
WordComposer(final WordComposer source)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      */
reset()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 
refreshSize()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      */
size()119     public final int size() {
120         return mCodePointSize;
121     }
122 
isComposingWord()123     public final boolean isComposingWord() {
124         return size() > 0;
125     }
126 
127     // TODO: make sure that the index should not exceed MAX_WORD_LENGTH
getCodeAt(int index)128     public int getCodeAt(int index) {
129         if (index >= MAX_WORD_LENGTH) {
130             return -1;
131         }
132         return mPrimaryKeyCodes[index];
133     }
134 
getCodeBeforeCursor()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 
getInputPointers()142     public InputPointers getInputPointers() {
143         return mInputPointers;
144     }
145 
isFirstCharCapitalized(final int index, final int codePoint, final boolean previous)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      */
add(final int primaryCode, final int keyX, final int keyY)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 
setCursorPositionWithinWord(final int posWithinWord)183     public void setCursorPositionWithinWord(final int posWithinWord) {
184         mCursorPositionWithinWord = posWithinWord;
185     }
186 
isCursorFrontOrMiddleOfComposingWord()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 
setBatchInputPointers(final InputPointers batchPointers)195     public void setBatchInputPointers(final InputPointers batchPointers) {
196         mInputPointers.set(batchPointers);
197         mIsBatchMode = true;
198     }
199 
setBatchInputWord(final String word)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      */
addKeyInfo(final int codePoint, final Keyboard keyboard)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      */
setComposingWord(final CharSequence word, final Keyboard keyboard)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      */
deleteLast()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      */
getTypedWord()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      */
isFirstCharCapitalized()294     public boolean isFirstCharCapitalized() {
295         return mIsFirstCharCapitalized;
296     }
297 
trailingSingleQuotesCount()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      */
isAllUpperCase()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 
wasShiftedNoLock()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      */
isMostlyCaps()323     public boolean isMostlyCaps() {
324         return mCapsCount > 1;
325     }
326 
327     /**
328      * Returns true if we have digits in the composing word.
329      */
hasDigits()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      */
setCapitalizedModeAtStartComposingTime(final int mode)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      */
wasAutoCapitalized()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      */
setAutoCorrection(final String correction)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      */
getAutoCorrectionOrNull()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      */
isResumed()375     public boolean isResumed() {
376         return mIsResumed;
377     }
378 
379     // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above.
commitWord(final int type, final String committedWord, final String separatorString, final String prevWord)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 
resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord)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 
isBatchMode()424     public boolean isBatchMode() {
425         return mIsBatchMode;
426     }
427 
setRejectedBatchModeSuggestion(final String rejectedSuggestion)428     public void setRejectedBatchModeSuggestion(final String rejectedSuggestion) {
429         mRejectedBatchModeSuggestion = rejectedSuggestion;
430     }
431 
getRejectedBatchModeSuggestion()432     public String getRejectedBatchModeSuggestion() {
433         return mRejectedBatchModeSuggestion;
434     }
435 }
436