WordComposer.java revision 0fd625bcfdfac1c10e7bd7f9088bf425fec08989
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 android.text.TextUtils;
20
21import com.android.inputmethod.keyboard.Key;
22import com.android.inputmethod.keyboard.KeyDetector;
23import com.android.inputmethod.keyboard.Keyboard;
24
25import java.util.ArrayList;
26import java.util.Arrays;
27
28/**
29 * A place to store the currently composing word with information such as adjacent key codes as well
30 */
31public class WordComposer {
32
33    public static final int NOT_A_CODE = KeyDetector.NOT_A_CODE;
34    public static final int NOT_A_COORDINATE = -1;
35
36    // TODO: Straighten out commit behavior so that the flags here are more understandable,
37    // and possibly adjust their names.
38    // COMMIT_TYPE_USER_TYPED_WORD is used when the word committed is the exact typed word, with
39    // no hinting from the IME. It happens when some external event happens (rotating the device,
40    // for example) or when auto-correction is off by settings or editor attributes.
41    public static final int COMMIT_TYPE_USER_TYPED_WORD = 0;
42    // COMMIT_TYPE_MANUAL_PICK is used when the user pressed a field in the suggestion strip.
43    public static final int COMMIT_TYPE_MANUAL_PICK = 1;
44    // COMMIT_TYPE_DECIDED_WORD is used when the IME commits the word it decided was best
45    // for the current user input. It may be different from what the user typed (true auto-correct)
46    // or it may be exactly what the user typed if it's in the dictionary or the IME does not have
47    // enough confidence in any suggestion to auto-correct (auto-correct to typed word).
48    public static final int COMMIT_TYPE_DECIDED_WORD = 2;
49    // COMMIT_TYPE_CANCEL_AUTO_CORRECT is used upon committing back the old word upon cancelling
50    // an auto-correction.
51    public static final int COMMIT_TYPE_CANCEL_AUTO_CORRECT = 3;
52
53    // Storage for all the info about the current input.
54    private static class CharacterStore {
55        /**
56         * The list of unicode values for each keystroke (including surrounding keys)
57         */
58        ArrayList<int[]> mCodes;
59        int[] mXCoordinates;
60        int[] mYCoordinates;
61        StringBuilder mTypedWord;
62        CharSequence mAutoCorrection;
63        CharacterStore() {
64            final int N = BinaryDictionary.MAX_WORD_LENGTH;
65            mCodes = new ArrayList<int[]>(N);
66            mTypedWord = new StringBuilder(N);
67            mXCoordinates = new int[N];
68            mYCoordinates = new int[N];
69            mAutoCorrection = null;
70        }
71        CharacterStore(final CharacterStore that) {
72            mCodes = new ArrayList<int[]>(that.mCodes);
73            mTypedWord = new StringBuilder(that.mTypedWord);
74            mXCoordinates = Arrays.copyOf(that.mXCoordinates, that.mXCoordinates.length);
75            mYCoordinates = Arrays.copyOf(that.mYCoordinates, that.mYCoordinates.length);
76        }
77        void reset() {
78            // For better performance than creating a new character store.
79            mCodes.clear();
80            mTypedWord.setLength(0);
81            mAutoCorrection = null;
82        }
83    }
84
85    // The currently typing word. May not be null.
86    private CharacterStore mCurrentWord;
87    // The information being kept for resuming suggestion. May be null if wiped.
88    private CharacterStore mCommittedWordSavedForSuggestionResuming;
89
90    private int mCapsCount;
91
92    private boolean mAutoCapitalized;
93    // Cache this value for performance
94    private int mTrailingSingleQuotesCount;
95
96    /**
97     * Whether the user chose to capitalize the first char of the word.
98     */
99    private boolean mIsFirstCharCapitalized;
100
101    public WordComposer() {
102        mCurrentWord = new CharacterStore();
103        mCommittedWordSavedForSuggestionResuming = null;
104        mTrailingSingleQuotesCount = 0;
105    }
106
107    public WordComposer(WordComposer source) {
108        init(source);
109    }
110
111    public void init(WordComposer source) {
112        mCurrentWord = new CharacterStore(source.mCurrentWord);
113        mCommittedWordSavedForSuggestionResuming = source.mCommittedWordSavedForSuggestionResuming;
114        mCapsCount = source.mCapsCount;
115        mIsFirstCharCapitalized = source.mIsFirstCharCapitalized;
116        mAutoCapitalized = source.mAutoCapitalized;
117        mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount;
118    }
119
120    /**
121     * Clear out the keys registered so far.
122     */
123    public void reset() {
124        mCurrentWord.reset();
125        mCommittedWordSavedForSuggestionResuming = null;
126        mCapsCount = 0;
127        mIsFirstCharCapitalized = false;
128        mTrailingSingleQuotesCount = 0;
129    }
130
131    /**
132     * Number of keystrokes in the composing word.
133     * @return the number of keystrokes
134     */
135    public final int size() {
136        return mCurrentWord.mTypedWord.length();
137    }
138
139    public final boolean isComposingWord() {
140        return size() > 0;
141    }
142
143    /**
144     * Returns the codes at a particular position in the word.
145     * @param index the position in the word
146     * @return the unicode for the pressed and surrounding keys
147     */
148    public int[] getCodesAt(int index) {
149        return mCurrentWord.mCodes.get(index);
150    }
151
152    public int[] getXCoordinates() {
153        return mCurrentWord.mXCoordinates;
154    }
155
156    public int[] getYCoordinates() {
157        return mCurrentWord.mYCoordinates;
158    }
159
160    private static boolean isFirstCharCapitalized(int index, int codePoint, 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 codes[0] containing the pressed key's unicode and the rest of
167     * the array containing unicode for adjacent keys, sorted by reducing probability/proximity.
168     * @param codes the array of unicode values
169     */
170    public void add(int primaryCode, int[] codes, int x, int y) {
171        final int newIndex = size();
172        mCurrentWord.mTypedWord.append((char) primaryCode);
173        correctPrimaryJuxtapos(primaryCode, codes);
174        mCurrentWord.mCodes.add(codes);
175        if (newIndex < BinaryDictionary.MAX_WORD_LENGTH) {
176            mCurrentWord.mXCoordinates[newIndex] = x;
177            mCurrentWord.mYCoordinates[newIndex] = y;
178        }
179        mIsFirstCharCapitalized = isFirstCharCapitalized(
180                newIndex, primaryCode, mIsFirstCharCapitalized);
181        if (Character.isUpperCase(primaryCode)) mCapsCount++;
182        if (Keyboard.CODE_SINGLE_QUOTE == primaryCode) {
183            ++mTrailingSingleQuotesCount;
184        } else {
185            mTrailingSingleQuotesCount = 0;
186        }
187        mCurrentWord.mAutoCorrection = null;
188    }
189
190    /**
191     * Internal method to retrieve reasonable proximity info for a character.
192     */
193    private void addKeyInfo(final int codePoint, final Keyboard keyboard,
194            final KeyDetector keyDetector) {
195        for (final Key key : keyboard.mKeys) {
196            if (key.mCode == codePoint) {
197                final int x = key.mX + key.mWidth / 2;
198                final int y = key.mY + key.mHeight / 2;
199                final int[] codes = keyDetector.newCodeArray();
200                keyDetector.getKeyAndNearbyCodes(x, y, codes);
201                add(codePoint, codes, x, y);
202                return;
203            }
204        }
205        add(codePoint, new int[] { codePoint },
206                WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
207    }
208
209    /**
210     * Set the currently composing word to the one passed as an argument.
211     * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity.
212     */
213    public void setComposingWord(final CharSequence word, final Keyboard keyboard,
214            final KeyDetector keyDetector) {
215        reset();
216        final int length = word.length();
217        for (int i = 0; i < length; ++i) {
218            int codePoint = word.charAt(i);
219            addKeyInfo(codePoint, keyboard, keyDetector);
220        }
221        mCommittedWordSavedForSuggestionResuming = null;
222    }
223
224    /**
225     * Shortcut for the above method, this will create a new KeyDetector for the passed keyboard.
226     */
227    public void setComposingWord(final CharSequence word, final Keyboard keyboard) {
228        final KeyDetector keyDetector = new KeyDetector(0);
229        keyDetector.setKeyboard(keyboard, 0, 0);
230        keyDetector.setProximityCorrectionEnabled(true);
231        keyDetector.setProximityThreshold(keyboard.mMostCommonKeyWidth);
232        setComposingWord(word, keyboard, keyDetector);
233    }
234
235    /**
236     * Swaps the first and second values in the codes array if the primary code is not the first
237     * value in the array but the second. This happens when the preferred key is not the key that
238     * the user released the finger on.
239     * @param primaryCode the preferred character
240     * @param codes array of codes based on distance from touch point
241     */
242    private static void correctPrimaryJuxtapos(int primaryCode, int[] codes) {
243        if (codes.length < 2) return;
244        if (codes[0] > 0 && codes[1] > 0 && codes[0] != primaryCode && codes[1] == primaryCode) {
245            codes[1] = codes[0];
246            codes[0] = primaryCode;
247        }
248    }
249
250    /**
251     * Delete the last keystroke as a result of hitting backspace.
252     */
253    public void deleteLast() {
254        final int size = size();
255        if (size > 0) {
256            final int lastPos = size - 1;
257            char lastChar = mCurrentWord.mTypedWord.charAt(lastPos);
258            mCurrentWord.mCodes.remove(lastPos);
259            mCurrentWord.mTypedWord.deleteCharAt(lastPos);
260            if (Character.isUpperCase(lastChar)) mCapsCount--;
261        }
262        if (size() == 0) {
263            mIsFirstCharCapitalized = false;
264        }
265        if (mTrailingSingleQuotesCount > 0) {
266            --mTrailingSingleQuotesCount;
267        } else {
268            for (int i = mCurrentWord.mTypedWord.length() - 1; i >= 0; --i) {
269                if (Keyboard.CODE_SINGLE_QUOTE != mCurrentWord.mTypedWord.codePointAt(i)) break;
270                ++mTrailingSingleQuotesCount;
271            }
272        }
273        mCurrentWord.mAutoCorrection = null;
274    }
275
276    /**
277     * Returns the word as it was typed, without any correction applied.
278     * @return the word that was typed so far. Never returns null.
279     */
280    public String getTypedWord() {
281        return mCurrentWord.mTypedWord.toString();
282    }
283
284    /**
285     * Whether or not the user typed a capital letter as the first letter in the word
286     * @return capitalization preference
287     */
288    public boolean isFirstCharCapitalized() {
289        return mIsFirstCharCapitalized;
290    }
291
292    public int trailingSingleQuotesCount() {
293        return mTrailingSingleQuotesCount;
294    }
295
296    /**
297     * Whether or not all of the user typed chars are upper case
298     * @return true if all user typed chars are upper case, false otherwise
299     */
300    public boolean isAllUpperCase() {
301        return (mCapsCount > 0) && (mCapsCount == size());
302    }
303
304    /**
305     * Returns true if more than one character is upper case, otherwise returns false.
306     */
307    public boolean isMostlyCaps() {
308        return mCapsCount > 1;
309    }
310
311    /**
312     * Saves the reason why the word is capitalized - whether it was automatic or
313     * due to the user hitting shift in the middle of a sentence.
314     * @param auto whether it was an automatic capitalization due to start of sentence
315     */
316    public void setAutoCapitalized(boolean auto) {
317        mAutoCapitalized = auto;
318    }
319
320    /**
321     * Returns whether the word was automatically capitalized.
322     * @return whether the word was automatically capitalized
323     */
324    public boolean isAutoCapitalized() {
325        return mAutoCapitalized;
326    }
327
328    /**
329     * Sets the auto-correction for this word.
330     */
331    public void setAutoCorrection(final CharSequence correction) {
332        mCurrentWord.mAutoCorrection = correction;
333    }
334
335    /**
336     * Remove any auto-correction that may have been set.
337     */
338    public void deleteAutoCorrection() {
339        mCurrentWord.mAutoCorrection = null;
340    }
341
342    /**
343     * @return the auto-correction for this word, or null if none.
344     */
345    public CharSequence getAutoCorrectionOrNull() {
346        return mCurrentWord.mAutoCorrection;
347    }
348
349    // `type' should be one of the COMMIT_TYPE_* constants above.
350    public void onCommitWord(final int type) {
351        mCommittedWordSavedForSuggestionResuming = mCurrentWord;
352        // Note: currently, we come here whenever we commit a word. If it's any *other* kind that
353        // DECIDED_WORD, we should reset mAutoCorrection so that we don't attempt to cancel later.
354        // If it's a DECIDED_WORD, it may be an actual auto-correction by the IME, or what the user
355        // typed because the IME decided *not* to auto-correct for whatever reason.
356        // Ideally we would also null it when it was a DECIDED_WORD that was not an auto-correct.
357        // As it happens these two cases should behave differently, because the former can be
358        // canceled while the latter can't. Currently, we figure this out in
359        // #didAutoCorrectToAnotherWord with #equals(). It would be marginally cleaner to do it
360        // here, but it would be slower (since we would #equals() for each commit, instead of
361        // only on cancel), and ultimately we want to figure it out even earlier anyway.
362        if (type != COMMIT_TYPE_DECIDED_WORD) {
363            // Only ever revert an auto-correct.
364            mCommittedWordSavedForSuggestionResuming.mAutoCorrection = null;
365        }
366        // TODO: improve performance by swapping buffers instead of creating a new object.
367        mCurrentWord = new CharacterStore();
368    }
369
370    public boolean hasWordKeptForSuggestionResuming() {
371        return null != mCommittedWordSavedForSuggestionResuming;
372    }
373
374    public void resumeSuggestionOnKeptWord() {
375        mCurrentWord = mCommittedWordSavedForSuggestionResuming;
376        mCommittedWordSavedForSuggestionResuming = null;
377    }
378
379    public boolean didAutoCorrectToAnotherWord() {
380        return null != mCommittedWordSavedForSuggestionResuming
381                && !TextUtils.isEmpty(mCommittedWordSavedForSuggestionResuming.mAutoCorrection)
382                && !TextUtils.equals(mCommittedWordSavedForSuggestionResuming.mTypedWord,
383                        mCommittedWordSavedForSuggestionResuming.mAutoCorrection);
384    }
385}
386