WordComposer.java revision 5fa2202e36a84bd5808fa10ca25c9179acb1b173
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
17package com.android.inputmethod.latin;
18
19import com.android.inputmethod.keyboard.Key;
20import com.android.inputmethod.keyboard.Keyboard;
21import com.android.inputmethod.latin.utils.StringUtils;
22
23import java.util.Arrays;
24
25/**
26 * A place to store the currently composing word with information such as adjacent key codes as well
27 */
28public final class WordComposer {
29    private static final int MAX_WORD_LENGTH = Constants.Dictionary.MAX_WORD_LENGTH;
30    private static final boolean DBG = LatinImeLogger.sDBG;
31
32    public static final int CAPS_MODE_OFF = 0;
33    // 1 is shift bit, 2 is caps bit, 4 is auto bit but this is just a convention as these bits
34    // aren't used anywhere in the code
35    public static final int CAPS_MODE_MANUAL_SHIFTED = 0x1;
36    public static final int CAPS_MODE_MANUAL_SHIFT_LOCKED = 0x3;
37    public static final int CAPS_MODE_AUTO_SHIFTED = 0x5;
38    public static final int CAPS_MODE_AUTO_SHIFT_LOCKED = 0x7;
39
40    // An array of code points representing the characters typed so far.
41    // The array is limited to MAX_WORD_LENGTH code points, but mTypedWord extends past that
42    // and mCodePointSize can go past that. If mCodePointSize is greater than MAX_WORD_LENGTH,
43    // this just does not contain the associated code points past MAX_WORD_LENGTH.
44    private int[] mPrimaryKeyCodes;
45    private final InputPointers mInputPointers = new InputPointers(MAX_WORD_LENGTH);
46    // This is the typed word, as a StringBuilder. This has the same contents as mPrimaryKeyCodes
47    // but under a StringBuilder representation for ease of use, depending on what is more useful
48    // at any given time. However this is not limited in size, while mPrimaryKeyCodes is limited
49    // to MAX_WORD_LENGTH code points.
50    private final StringBuilder mTypedWord;
51    private String mAutoCorrection;
52    private boolean mIsResumed;
53    private boolean mIsBatchMode;
54    // A memory of the last rejected batch mode suggestion, if any. This goes like this: the user
55    // gestures a word, is displeased with the results and hits backspace, then gestures again.
56    // At the very least we should avoid re-suggesting the same thing, and to do that we memorize
57    // the rejected suggestion in this variable.
58    // TODO: this should be done in a comprehensive way by the User History feature instead of
59    // as an ad-hockery here.
60    private String mRejectedBatchModeSuggestion;
61
62    // Cache these values for performance
63    private int mCapsCount;
64    private int mDigitsCount;
65    private int mCapitalizedMode;
66    private int mTrailingSingleQuotesCount;
67    // This is the number of code points entered so far. This is not limited to MAX_WORD_LENGTH.
68    // In general, this contains the size of mPrimaryKeyCodes, except when this is greater than
69    // MAX_WORD_LENGTH in which case mPrimaryKeyCodes only contain the first MAX_WORD_LENGTH
70    // code points.
71    private int mCodePointSize;
72    private int mCursorPositionWithinWord;
73
74    /**
75     * Whether the user chose to capitalize the first char of the word.
76     */
77    private boolean mIsFirstCharCapitalized;
78
79    public WordComposer() {
80        mPrimaryKeyCodes = new int[MAX_WORD_LENGTH];
81        mTypedWord = new StringBuilder(MAX_WORD_LENGTH);
82        mAutoCorrection = null;
83        mTrailingSingleQuotesCount = 0;
84        mIsResumed = false;
85        mIsBatchMode = false;
86        mCursorPositionWithinWord = 0;
87        mRejectedBatchModeSuggestion = null;
88        refreshSize();
89    }
90
91    public WordComposer(final WordComposer source) {
92        mPrimaryKeyCodes = Arrays.copyOf(source.mPrimaryKeyCodes, source.mPrimaryKeyCodes.length);
93        mTypedWord = new StringBuilder(source.mTypedWord);
94        mInputPointers.copy(source.mInputPointers);
95        mCapsCount = source.mCapsCount;
96        mDigitsCount = source.mDigitsCount;
97        mIsFirstCharCapitalized = source.mIsFirstCharCapitalized;
98        mCapitalizedMode = source.mCapitalizedMode;
99        mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount;
100        mIsResumed = source.mIsResumed;
101        mIsBatchMode = source.mIsBatchMode;
102        mCursorPositionWithinWord = source.mCursorPositionWithinWord;
103        mRejectedBatchModeSuggestion = source.mRejectedBatchModeSuggestion;
104        refreshSize();
105    }
106
107    /**
108     * Clear out the keys registered so far.
109     */
110    public void reset() {
111        mTypedWord.setLength(0);
112        mAutoCorrection = null;
113        mCapsCount = 0;
114        mDigitsCount = 0;
115        mIsFirstCharCapitalized = false;
116        mTrailingSingleQuotesCount = 0;
117        mIsResumed = false;
118        mIsBatchMode = false;
119        mCursorPositionWithinWord = 0;
120        mRejectedBatchModeSuggestion = null;
121        refreshSize();
122    }
123
124    private final void refreshSize() {
125        mCodePointSize = mTypedWord.codePointCount(0, mTypedWord.length());
126    }
127
128    /**
129     * Number of keystrokes in the composing word.
130     * @return the number of keystrokes
131     */
132    public final int size() {
133        return mCodePointSize;
134    }
135
136    public final boolean isComposingWord() {
137        return size() > 0;
138    }
139
140    // TODO: make sure that the index should not exceed MAX_WORD_LENGTH
141    public int getCodeAt(int index) {
142        if (index >= MAX_WORD_LENGTH) {
143            return -1;
144        }
145        return mPrimaryKeyCodes[index];
146    }
147
148    public int getCodeBeforeCursor() {
149        if (mCursorPositionWithinWord < 1 || mCursorPositionWithinWord > mPrimaryKeyCodes.length) {
150            return Constants.NOT_A_CODE;
151        }
152        return mPrimaryKeyCodes[mCursorPositionWithinWord - 1];
153    }
154
155    public InputPointers getInputPointers() {
156        return mInputPointers;
157    }
158
159    private static boolean isFirstCharCapitalized(final int index, final int codePoint,
160            final boolean previous) {
161        if (index == 0) return Character.isUpperCase(codePoint);
162        return previous && !Character.isUpperCase(codePoint);
163    }
164
165    /**
166     * Add a new keystroke, with the pressed key's code point with the touch point coordinates.
167     */
168    public void add(final int primaryCode, final int keyX, final int keyY) {
169        final int newIndex = size();
170        mTypedWord.appendCodePoint(primaryCode);
171        refreshSize();
172        mCursorPositionWithinWord = mCodePointSize;
173        if (newIndex < MAX_WORD_LENGTH) {
174            mPrimaryKeyCodes[newIndex] = primaryCode >= Constants.CODE_SPACE
175                    ? Character.toLowerCase(primaryCode) : primaryCode;
176            // In the batch input mode, the {@code mInputPointers} holds batch input points and
177            // shouldn't be overridden by the "typed key" coordinates
178            // (See {@link #setBatchInputWord}).
179            if (!mIsBatchMode) {
180                // TODO: Set correct pointer id and time
181                mInputPointers.addPointer(newIndex, keyX, keyY, 0, 0);
182            }
183        }
184        mIsFirstCharCapitalized = isFirstCharCapitalized(
185                newIndex, primaryCode, mIsFirstCharCapitalized);
186        if (Character.isUpperCase(primaryCode)) mCapsCount++;
187        if (Character.isDigit(primaryCode)) mDigitsCount++;
188        if (Constants.CODE_SINGLE_QUOTE == primaryCode) {
189            ++mTrailingSingleQuotesCount;
190        } else {
191            mTrailingSingleQuotesCount = 0;
192        }
193        mAutoCorrection = null;
194    }
195
196    public void setCursorPositionWithinWord(final int posWithinWord) {
197        mCursorPositionWithinWord = posWithinWord;
198    }
199
200    public boolean isCursorFrontOrMiddleOfComposingWord() {
201        if (DBG && mCursorPositionWithinWord > mCodePointSize) {
202            throw new RuntimeException("Wrong cursor position : " + mCursorPositionWithinWord
203                    + "in a word of size " + mCodePointSize);
204        }
205        return mCursorPositionWithinWord != mCodePointSize;
206    }
207
208    /**
209     * When the cursor is moved by the user, we need to update its position.
210     * If it falls inside the currently composing word, we don't reset the composition, and
211     * only update the cursor position.
212     *
213     * @param expectedMoveAmount How many java chars to move the cursor. Negative values move
214     * the cursor backward, positive values move the cursor forward.
215     * @return true if the cursor is still inside the composing word, false otherwise.
216     */
217    public boolean moveCursorByAndReturnIfInsideComposingWord(final int expectedMoveAmount) {
218        int actualMoveAmountWithinWord = 0;
219        int cursorPos = mCursorPositionWithinWord;
220        final int[] codePoints;
221        if (mCodePointSize >= MAX_WORD_LENGTH) {
222            // If we have more than MAX_WORD_LENGTH characters, we don't have everything inside
223            // mPrimaryKeyCodes. This should be rare enough that we can afford to just compute
224            // the array on the fly when this happens.
225            codePoints = StringUtils.toCodePointArray(mTypedWord.toString());
226        } else {
227            codePoints = mPrimaryKeyCodes;
228        }
229        if (expectedMoveAmount >= 0) {
230            // Moving the cursor forward for the expected amount or until the end of the word has
231            // been reached, whichever comes first.
232            while (actualMoveAmountWithinWord < expectedMoveAmount && cursorPos < mCodePointSize) {
233                actualMoveAmountWithinWord += Character.charCount(codePoints[cursorPos]);
234                ++cursorPos;
235            }
236        } else {
237            // Moving the cursor backward for the expected amount or until the start of the word
238            // has been reached, whichever comes first.
239            while (actualMoveAmountWithinWord > expectedMoveAmount && cursorPos > 0) {
240                --cursorPos;
241                actualMoveAmountWithinWord -= Character.charCount(codePoints[cursorPos]);
242            }
243        }
244        // If the actual and expected amounts differ, we crossed the start or the end of the word
245        // so the result would not be inside the composing word.
246        if (actualMoveAmountWithinWord != expectedMoveAmount) return false;
247        mCursorPositionWithinWord = cursorPos;
248        return true;
249    }
250
251    public void setBatchInputPointers(final InputPointers batchPointers) {
252        mInputPointers.set(batchPointers);
253        mIsBatchMode = true;
254    }
255
256    public void setBatchInputWord(final String word) {
257        reset();
258        mIsBatchMode = true;
259        final int length = word.length();
260        for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
261            final int codePoint = Character.codePointAt(word, i);
262            // We don't want to override the batch input points that are held in mInputPointers
263            // (See {@link #add(int,int,int)}).
264            add(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
265        }
266    }
267
268    /**
269     * Add a dummy key by retrieving reasonable coordinates
270     */
271    public void addKeyInfo(final int codePoint, final Keyboard keyboard) {
272        final int x, y;
273        final Key key;
274        if (keyboard != null && (key = keyboard.getKey(codePoint)) != null) {
275            x = key.mX + key.mWidth / 2;
276            y = key.mY + key.mHeight / 2;
277        } else {
278            x = Constants.NOT_A_COORDINATE;
279            y = Constants.NOT_A_COORDINATE;
280        }
281        add(codePoint, x, y);
282    }
283
284    /**
285     * Set the currently composing word to the one passed as an argument.
286     * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity.
287     */
288    public void setComposingWord(final CharSequence word, final Keyboard keyboard) {
289        reset();
290        final int length = word.length();
291        for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
292            final int codePoint = Character.codePointAt(word, i);
293            addKeyInfo(codePoint, keyboard);
294        }
295        mIsResumed = true;
296    }
297
298    /**
299     * Delete the last keystroke as a result of hitting backspace.
300     */
301    public void deleteLast() {
302        final int size = size();
303        if (size > 0) {
304            // Note: mTypedWord.length() and mCodes.length differ when there are surrogate pairs
305            final int stringBuilderLength = mTypedWord.length();
306            if (stringBuilderLength < size) {
307                throw new RuntimeException(
308                        "In WordComposer: mCodes and mTypedWords have non-matching lengths");
309            }
310            final int lastChar = mTypedWord.codePointBefore(stringBuilderLength);
311            if (Character.isSupplementaryCodePoint(lastChar)) {
312                mTypedWord.delete(stringBuilderLength - 2, stringBuilderLength);
313            } else {
314                mTypedWord.deleteCharAt(stringBuilderLength - 1);
315            }
316            if (Character.isUpperCase(lastChar)) mCapsCount--;
317            if (Character.isDigit(lastChar)) mDigitsCount--;
318            refreshSize();
319        }
320        // We may have deleted the last one.
321        if (0 == size()) {
322            mIsFirstCharCapitalized = false;
323        }
324        if (mTrailingSingleQuotesCount > 0) {
325            --mTrailingSingleQuotesCount;
326        } else {
327            int i = mTypedWord.length();
328            while (i > 0) {
329                i = mTypedWord.offsetByCodePoints(i, -1);
330                if (Constants.CODE_SINGLE_QUOTE != mTypedWord.codePointAt(i)) break;
331                ++mTrailingSingleQuotesCount;
332            }
333        }
334        mCursorPositionWithinWord = mCodePointSize;
335        mAutoCorrection = null;
336    }
337
338    /**
339     * Returns the word as it was typed, without any correction applied.
340     * @return the word that was typed so far. Never returns null.
341     */
342    public String getTypedWord() {
343        return mTypedWord.toString();
344    }
345
346    /**
347     * Whether or not the user typed a capital letter as the first letter in the word
348     * @return capitalization preference
349     */
350    public boolean isFirstCharCapitalized() {
351        return mIsFirstCharCapitalized;
352    }
353
354    public int trailingSingleQuotesCount() {
355        return mTrailingSingleQuotesCount;
356    }
357
358    /**
359     * Whether or not all of the user typed chars are upper case
360     * @return true if all user typed chars are upper case, false otherwise
361     */
362    public boolean isAllUpperCase() {
363        if (size() <= 1) {
364            return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
365                    || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFT_LOCKED;
366        } else {
367            return mCapsCount == size();
368        }
369    }
370
371    public boolean wasShiftedNoLock() {
372        return mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED
373                || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFTED;
374    }
375
376    /**
377     * Returns true if more than one character is upper case, otherwise returns false.
378     */
379    public boolean isMostlyCaps() {
380        return mCapsCount > 1;
381    }
382
383    /**
384     * Returns true if we have digits in the composing word.
385     */
386    public boolean hasDigits() {
387        return mDigitsCount > 0;
388    }
389
390    /**
391     * Saves the caps mode at the start of composing.
392     *
393     * WordComposer needs to know about this for several reasons. The first is, we need to know
394     * after the fact what the reason was, to register the correct form into the user history
395     * dictionary: if the word was automatically capitalized, we should insert it in all-lower
396     * case but if it's a manual pressing of shift, then it should be inserted as is.
397     * Also, batch input needs to know about the current caps mode to display correctly
398     * capitalized suggestions.
399     * @param mode the mode at the time of start
400     */
401    public void setCapitalizedModeAtStartComposingTime(final int mode) {
402        mCapitalizedMode = mode;
403    }
404
405    /**
406     * Returns whether the word was automatically capitalized.
407     * @return whether the word was automatically capitalized
408     */
409    public boolean wasAutoCapitalized() {
410        return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
411                || mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED;
412    }
413
414    /**
415     * Sets the auto-correction for this word.
416     */
417    public void setAutoCorrection(final String correction) {
418        mAutoCorrection = correction;
419    }
420
421    /**
422     * @return the auto-correction for this word, or null if none.
423     */
424    public String getAutoCorrectionOrNull() {
425        return mAutoCorrection;
426    }
427
428    /**
429     * @return whether we started composing this word by resuming suggestion on an existing string
430     */
431    public boolean isResumed() {
432        return mIsResumed;
433    }
434
435    // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above.
436    public LastComposedWord commitWord(final int type, final String committedWord,
437            final String separatorString, final String prevWord) {
438        // Note: currently, we come here whenever we commit a word. If it's a MANUAL_PICK
439        // or a DECIDED_WORD we may cancel the commit later; otherwise, we should deactivate
440        // the last composed word to ensure this does not happen.
441        final int[] primaryKeyCodes = mPrimaryKeyCodes;
442        mPrimaryKeyCodes = new int[MAX_WORD_LENGTH];
443        final LastComposedWord lastComposedWord = new LastComposedWord(primaryKeyCodes,
444                mInputPointers, mTypedWord.toString(), committedWord, separatorString,
445                prevWord, mCapitalizedMode);
446        mInputPointers.reset();
447        if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD
448                && type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) {
449            lastComposedWord.deactivate();
450        }
451        mCapsCount = 0;
452        mDigitsCount = 0;
453        mIsBatchMode = false;
454        mTypedWord.setLength(0);
455        mCodePointSize = 0;
456        mTrailingSingleQuotesCount = 0;
457        mIsFirstCharCapitalized = false;
458        mCapitalizedMode = CAPS_MODE_OFF;
459        refreshSize();
460        mAutoCorrection = null;
461        mCursorPositionWithinWord = 0;
462        mIsResumed = false;
463        mRejectedBatchModeSuggestion = null;
464        return lastComposedWord;
465    }
466
467    public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) {
468        mPrimaryKeyCodes = lastComposedWord.mPrimaryKeyCodes;
469        mInputPointers.set(lastComposedWord.mInputPointers);
470        mTypedWord.setLength(0);
471        mTypedWord.append(lastComposedWord.mTypedWord);
472        refreshSize();
473        mCapitalizedMode = lastComposedWord.mCapitalizedMode;
474        mAutoCorrection = null; // This will be filled by the next call to updateSuggestion.
475        mCursorPositionWithinWord = mCodePointSize;
476        mRejectedBatchModeSuggestion = null;
477        mIsResumed = true;
478    }
479
480    public boolean isBatchMode() {
481        return mIsBatchMode;
482    }
483
484    public void setRejectedBatchModeSuggestion(final String rejectedSuggestion) {
485        mRejectedBatchModeSuggestion = rejectedSuggestion;
486    }
487
488    public String getRejectedBatchModeSuggestion() {
489        return mRejectedBatchModeSuggestion;
490    }
491}
492