WordComposer.java revision 8914555776a4d3dfd6afc4926a69169ca1c82a0e
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.Keyboard;
21
22import java.util.Arrays;
23
24/**
25 * A place to store the currently composing word with information such as adjacent key codes as well
26 */
27public final class WordComposer {
28    private static final int MAX_WORD_LENGTH = Constants.Dictionary.MAX_WORD_LENGTH;
29
30    public static final int CAPS_MODE_OFF = 0;
31    // 1 is shift bit, 2 is caps bit, 4 is auto bit but this is just a convention as these bits
32    // aren't used anywhere in the code
33    public static final int CAPS_MODE_MANUAL_SHIFTED = 0x1;
34    public static final int CAPS_MODE_MANUAL_SHIFT_LOCKED = 0x3;
35    public static final int CAPS_MODE_AUTO_SHIFTED = 0x5;
36    public static final int CAPS_MODE_AUTO_SHIFT_LOCKED = 0x7;
37
38    private int[] mPrimaryKeyCodes;
39    private final InputPointers mInputPointers = new InputPointers(MAX_WORD_LENGTH);
40    private final StringBuilder mTypedWord;
41    private String mAutoCorrection;
42    private boolean mIsResumed;
43    private boolean mIsBatchMode;
44
45    // Cache these values for performance
46    private int mCapsCount;
47    private int mDigitsCount;
48    private int mCapitalizedMode;
49    private int mTrailingSingleQuotesCount;
50    private int mCodePointSize;
51
52    /**
53     * Whether the user chose to capitalize the first char of the word.
54     */
55    private boolean mIsFirstCharCapitalized;
56
57    public WordComposer() {
58        mPrimaryKeyCodes = new int[MAX_WORD_LENGTH];
59        mTypedWord = new StringBuilder(MAX_WORD_LENGTH);
60        mAutoCorrection = null;
61        mTrailingSingleQuotesCount = 0;
62        mIsResumed = false;
63        mIsBatchMode = false;
64        refreshSize();
65    }
66
67    public WordComposer(final WordComposer source) {
68        mPrimaryKeyCodes = Arrays.copyOf(source.mPrimaryKeyCodes, source.mPrimaryKeyCodes.length);
69        mTypedWord = new StringBuilder(source.mTypedWord);
70        mInputPointers.copy(source.mInputPointers);
71        mCapsCount = source.mCapsCount;
72        mDigitsCount = source.mDigitsCount;
73        mIsFirstCharCapitalized = source.mIsFirstCharCapitalized;
74        mCapitalizedMode = source.mCapitalizedMode;
75        mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount;
76        mIsResumed = source.mIsResumed;
77        mIsBatchMode = source.mIsBatchMode;
78        refreshSize();
79    }
80
81    /**
82     * Clear out the keys registered so far.
83     */
84    public void reset() {
85        mTypedWord.setLength(0);
86        mAutoCorrection = null;
87        mCapsCount = 0;
88        mDigitsCount = 0;
89        mIsFirstCharCapitalized = false;
90        mTrailingSingleQuotesCount = 0;
91        mIsResumed = false;
92        mIsBatchMode = false;
93        refreshSize();
94    }
95
96    private final void refreshSize() {
97        mCodePointSize = mTypedWord.codePointCount(0, mTypedWord.length());
98    }
99
100    /**
101     * Number of keystrokes in the composing word.
102     * @return the number of keystrokes
103     */
104    public final int size() {
105        return mCodePointSize;
106    }
107
108    public final boolean isComposingWord() {
109        return size() > 0;
110    }
111
112    // TODO: make sure that the index should not exceed MAX_WORD_LENGTH
113    public int getCodeAt(int index) {
114        if (index >= MAX_WORD_LENGTH) {
115            return -1;
116        }
117        return mPrimaryKeyCodes[index];
118    }
119
120    public InputPointers getInputPointers() {
121        return mInputPointers;
122    }
123
124    private static boolean isFirstCharCapitalized(final int index, final int codePoint,
125            final boolean previous) {
126        if (index == 0) return Character.isUpperCase(codePoint);
127        return previous && !Character.isUpperCase(codePoint);
128    }
129
130    /**
131     * Add a new keystroke, with the pressed key's code point with the touch point coordinates.
132     */
133    public void add(final int primaryCode, final int keyX, final int keyY) {
134        final int newIndex = size();
135        mTypedWord.appendCodePoint(primaryCode);
136        refreshSize();
137        if (newIndex < MAX_WORD_LENGTH) {
138            mPrimaryKeyCodes[newIndex] = primaryCode >= Constants.CODE_SPACE
139                    ? Character.toLowerCase(primaryCode) : primaryCode;
140            // In the batch input mode, the {@code mInputPointers} holds batch input points and
141            // shouldn't be overridden by the "typed key" coordinates
142            // (See {@link #setBatchInputWord}).
143            if (!mIsBatchMode) {
144                // TODO: Set correct pointer id and time
145                mInputPointers.addPointer(newIndex, keyX, keyY, 0, 0);
146            }
147        }
148        mIsFirstCharCapitalized = isFirstCharCapitalized(
149                newIndex, primaryCode, mIsFirstCharCapitalized);
150        if (Character.isUpperCase(primaryCode)) mCapsCount++;
151        if (Character.isDigit(primaryCode)) mDigitsCount++;
152        if (Constants.CODE_SINGLE_QUOTE == primaryCode) {
153            ++mTrailingSingleQuotesCount;
154        } else {
155            mTrailingSingleQuotesCount = 0;
156        }
157        mAutoCorrection = null;
158    }
159
160    public void setBatchInputPointers(final InputPointers batchPointers) {
161        mInputPointers.set(batchPointers);
162        mIsBatchMode = true;
163    }
164
165    public void setBatchInputWord(final String word) {
166        reset();
167        mIsBatchMode = true;
168        final int length = word.length();
169        for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
170            final int codePoint = Character.codePointAt(word, i);
171            // We don't want to override the batch input points that are held in mInputPointers
172            // (See {@link #add(int,int,int)}).
173            add(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
174        }
175    }
176
177    /**
178     * Internal method to retrieve reasonable proximity info for a character.
179     */
180    private void addKeyInfo(final int codePoint, final Keyboard keyboard) {
181        final int x, y;
182        final Key key;
183        if (keyboard != null && (key = keyboard.getKey(codePoint)) != null) {
184            x = key.mX + key.mWidth / 2;
185            y = key.mY + key.mHeight / 2;
186        } else {
187            x = Constants.NOT_A_COORDINATE;
188            y = Constants.NOT_A_COORDINATE;
189        }
190        add(codePoint, x, y);
191    }
192
193    /**
194     * Set the currently composing word to the one passed as an argument.
195     * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity.
196     */
197    public void setComposingWord(final CharSequence word, final Keyboard keyboard) {
198        reset();
199        final int length = word.length();
200        for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
201            final int codePoint = Character.codePointAt(word, i);
202            addKeyInfo(codePoint, keyboard);
203        }
204        mIsResumed = true;
205    }
206
207    /**
208     * Delete the last keystroke as a result of hitting backspace.
209     */
210    public void deleteLast() {
211        final int size = size();
212        if (size > 0) {
213            // Note: mTypedWord.length() and mCodes.length differ when there are surrogate pairs
214            final int stringBuilderLength = mTypedWord.length();
215            if (stringBuilderLength < size) {
216                throw new RuntimeException(
217                        "In WordComposer: mCodes and mTypedWords have non-matching lengths");
218            }
219            final int lastChar = mTypedWord.codePointBefore(stringBuilderLength);
220            if (Character.isSupplementaryCodePoint(lastChar)) {
221                mTypedWord.delete(stringBuilderLength - 2, stringBuilderLength);
222            } else {
223                mTypedWord.deleteCharAt(stringBuilderLength - 1);
224            }
225            if (Character.isUpperCase(lastChar)) mCapsCount--;
226            if (Character.isDigit(lastChar)) mDigitsCount--;
227            refreshSize();
228        }
229        // We may have deleted the last one.
230        if (0 == size()) {
231            mIsFirstCharCapitalized = false;
232        }
233        if (mTrailingSingleQuotesCount > 0) {
234            --mTrailingSingleQuotesCount;
235        } else {
236            int i = mTypedWord.length();
237            while (i > 0) {
238                i = mTypedWord.offsetByCodePoints(i, -1);
239                if (Constants.CODE_SINGLE_QUOTE != mTypedWord.codePointAt(i)) break;
240                ++mTrailingSingleQuotesCount;
241            }
242        }
243        mAutoCorrection = null;
244    }
245
246    /**
247     * Returns the word as it was typed, without any correction applied.
248     * @return the word that was typed so far. Never returns null.
249     */
250    public String getTypedWord() {
251        return mTypedWord.toString();
252    }
253
254    /**
255     * Whether or not the user typed a capital letter as the first letter in the word
256     * @return capitalization preference
257     */
258    public boolean isFirstCharCapitalized() {
259        return mIsFirstCharCapitalized;
260    }
261
262    public int trailingSingleQuotesCount() {
263        return mTrailingSingleQuotesCount;
264    }
265
266    /**
267     * Whether or not all of the user typed chars are upper case
268     * @return true if all user typed chars are upper case, false otherwise
269     */
270    public boolean isAllUpperCase() {
271        if (size() <= 1) {
272            return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
273                    || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFT_LOCKED;
274        } else {
275            return mCapsCount == size();
276        }
277    }
278
279    public boolean wasShiftedNoLock() {
280        return mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED
281                || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFTED;
282    }
283
284    /**
285     * Returns true if more than one character is upper case, otherwise returns false.
286     */
287    public boolean isMostlyCaps() {
288        return mCapsCount > 1;
289    }
290
291    /**
292     * Returns true if we have digits in the composing word.
293     */
294    public boolean hasDigits() {
295        return mDigitsCount > 0;
296    }
297
298    /**
299     * Saves the caps mode at the start of composing.
300     *
301     * WordComposer needs to know about this for several reasons. The first is, we need to know
302     * after the fact what the reason was, to register the correct form into the user history
303     * dictionary: if the word was automatically capitalized, we should insert it in all-lower
304     * case but if it's a manual pressing of shift, then it should be inserted as is.
305     * Also, batch input needs to know about the current caps mode to display correctly
306     * capitalized suggestions.
307     * @param mode the mode at the time of start
308     */
309    public void setCapitalizedModeAtStartComposingTime(final int mode) {
310        mCapitalizedMode = mode;
311    }
312
313    /**
314     * Returns whether the word was automatically capitalized.
315     * @return whether the word was automatically capitalized
316     */
317    public boolean wasAutoCapitalized() {
318        return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
319                || mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED;
320    }
321
322    /**
323     * Sets the auto-correction for this word.
324     */
325    public void setAutoCorrection(final String correction) {
326        mAutoCorrection = correction;
327    }
328
329    /**
330     * @return the auto-correction for this word, or null if none.
331     */
332    public String getAutoCorrectionOrNull() {
333        return mAutoCorrection;
334    }
335
336    /**
337     * @return whether we started composing this word by resuming suggestion on an existing string
338     */
339    public boolean isResumed() {
340        return mIsResumed;
341    }
342
343    // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above.
344    public LastComposedWord commitWord(final int type, final String committedWord,
345            final String separatorString, final String prevWord) {
346        // Note: currently, we come here whenever we commit a word. If it's a MANUAL_PICK
347        // or a DECIDED_WORD we may cancel the commit later; otherwise, we should deactivate
348        // the last composed word to ensure this does not happen.
349        final int[] primaryKeyCodes = mPrimaryKeyCodes;
350        mPrimaryKeyCodes = new int[MAX_WORD_LENGTH];
351        final LastComposedWord lastComposedWord = new LastComposedWord(primaryKeyCodes,
352                mInputPointers, mTypedWord.toString(), committedWord, separatorString,
353                prevWord, mCapitalizedMode);
354        mInputPointers.reset();
355        if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD
356                && type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) {
357            lastComposedWord.deactivate();
358        }
359        mCapsCount = 0;
360        mDigitsCount = 0;
361        mIsBatchMode = false;
362        mTypedWord.setLength(0);
363        mCodePointSize = 0;
364        mTrailingSingleQuotesCount = 0;
365        mIsFirstCharCapitalized = false;
366        mCapitalizedMode = CAPS_MODE_OFF;
367        refreshSize();
368        mAutoCorrection = null;
369        mIsResumed = false;
370        return lastComposedWord;
371    }
372
373    public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) {
374        mPrimaryKeyCodes = lastComposedWord.mPrimaryKeyCodes;
375        mInputPointers.set(lastComposedWord.mInputPointers);
376        mTypedWord.setLength(0);
377        mTypedWord.append(lastComposedWord.mTypedWord);
378        refreshSize();
379        mCapitalizedMode = lastComposedWord.mCapitalizedMode;
380        mAutoCorrection = null; // This will be filled by the next call to updateSuggestion.
381        mIsResumed = true;
382    }
383
384    public boolean isBatchMode() {
385        return mIsBatchMode;
386    }
387}
388