WordComposer.java revision 449415c72f437f523a49a9ccfcde8a3c0f583a18
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    final int N = BinaryDictionary.MAX_WORD_LENGTH;
37
38    private ArrayList<int[]> mCodes;
39    private int[] mXCoordinates;
40    private int[] mYCoordinates;
41    private StringBuilder mTypedWord;
42    private CharSequence mAutoCorrection;
43
44    // Cache these values for performance
45    private int mCapsCount;
46    private boolean mAutoCapitalized;
47    private int mTrailingSingleQuotesCount;
48
49    /**
50     * Whether the user chose to capitalize the first char of the word.
51     */
52    private boolean mIsFirstCharCapitalized;
53
54    public WordComposer() {
55        mCodes = new ArrayList<int[]>(N);
56        mTypedWord = new StringBuilder(N);
57        mXCoordinates = new int[N];
58        mYCoordinates = new int[N];
59        mAutoCorrection = null;
60        mTrailingSingleQuotesCount = 0;
61    }
62
63    public WordComposer(WordComposer source) {
64        init(source);
65    }
66
67    public void init(WordComposer source) {
68        mCodes = new ArrayList<int[]>(source.mCodes);
69        mTypedWord = new StringBuilder(source.mTypedWord);
70        mXCoordinates = Arrays.copyOf(source.mXCoordinates, source.mXCoordinates.length);
71        mYCoordinates = Arrays.copyOf(source.mYCoordinates, source.mYCoordinates.length);
72        mCapsCount = source.mCapsCount;
73        mIsFirstCharCapitalized = source.mIsFirstCharCapitalized;
74        mAutoCapitalized = source.mAutoCapitalized;
75        mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount;
76    }
77
78    /**
79     * Clear out the keys registered so far.
80     */
81    public void reset() {
82        mCodes.clear();
83        mTypedWord.setLength(0);
84        mAutoCorrection = null;
85        mCapsCount = 0;
86        mIsFirstCharCapitalized = false;
87        mTrailingSingleQuotesCount = 0;
88    }
89
90    /**
91     * Number of keystrokes in the composing word.
92     * @return the number of keystrokes
93     */
94    public final int size() {
95        return mTypedWord.length();
96    }
97
98    public final boolean isComposingWord() {
99        return size() > 0;
100    }
101
102    /**
103     * Returns the codes at a particular position in the word.
104     * @param index the position in the word
105     * @return the unicode for the pressed and surrounding keys
106     */
107    public int[] getCodesAt(int index) {
108        return mCodes.get(index);
109    }
110
111    public int[] getXCoordinates() {
112        return mXCoordinates;
113    }
114
115    public int[] getYCoordinates() {
116        return mYCoordinates;
117    }
118
119    private static boolean isFirstCharCapitalized(int index, int codePoint, boolean previous) {
120        if (index == 0) return Character.isUpperCase(codePoint);
121        return previous && !Character.isUpperCase(codePoint);
122    }
123
124    /**
125     * Add a new keystroke, with codes[0] containing the pressed key's unicode and the rest of
126     * the array containing unicode for adjacent keys, sorted by reducing probability/proximity.
127     * @param codes the array of unicode values
128     */
129    public void add(int primaryCode, int[] codes, int x, int y) {
130        final int newIndex = size();
131        mTypedWord.append((char) primaryCode);
132        correctPrimaryJuxtapos(primaryCode, codes);
133        mCodes.add(codes);
134        if (newIndex < BinaryDictionary.MAX_WORD_LENGTH) {
135            mXCoordinates[newIndex] = x;
136            mYCoordinates[newIndex] = y;
137        }
138        mIsFirstCharCapitalized = isFirstCharCapitalized(
139                newIndex, primaryCode, mIsFirstCharCapitalized);
140        if (Character.isUpperCase(primaryCode)) mCapsCount++;
141        if (Keyboard.CODE_SINGLE_QUOTE == primaryCode) {
142            ++mTrailingSingleQuotesCount;
143        } else {
144            mTrailingSingleQuotesCount = 0;
145        }
146        mAutoCorrection = null;
147    }
148
149    /**
150     * Internal method to retrieve reasonable proximity info for a character.
151     */
152    private void addKeyInfo(final int codePoint, final Keyboard keyboard,
153            final KeyDetector keyDetector) {
154        for (final Key key : keyboard.mKeys) {
155            if (key.mCode == codePoint) {
156                final int x = key.mX + key.mWidth / 2;
157                final int y = key.mY + key.mHeight / 2;
158                final int[] codes = keyDetector.newCodeArray();
159                keyDetector.getKeyAndNearbyCodes(x, y, codes);
160                add(codePoint, codes, x, y);
161                return;
162            }
163        }
164        add(codePoint, new int[] { codePoint },
165                WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
166    }
167
168    /**
169     * Set the currently composing word to the one passed as an argument.
170     * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity.
171     */
172    public void setComposingWord(final CharSequence word, final Keyboard keyboard,
173            final KeyDetector keyDetector) {
174        reset();
175        final int length = word.length();
176        for (int i = 0; i < length; ++i) {
177            int codePoint = word.charAt(i);
178            addKeyInfo(codePoint, keyboard, keyDetector);
179        }
180    }
181
182    /**
183     * Shortcut for the above method, this will create a new KeyDetector for the passed keyboard.
184     */
185    public void setComposingWord(final CharSequence word, final Keyboard keyboard) {
186        final KeyDetector keyDetector = new KeyDetector(0);
187        keyDetector.setKeyboard(keyboard, 0, 0);
188        keyDetector.setProximityCorrectionEnabled(true);
189        keyDetector.setProximityThreshold(keyboard.mMostCommonKeyWidth);
190        setComposingWord(word, keyboard, keyDetector);
191    }
192
193    /**
194     * Swaps the first and second values in the codes array if the primary code is not the first
195     * value in the array but the second. This happens when the preferred key is not the key that
196     * the user released the finger on.
197     * @param primaryCode the preferred character
198     * @param codes array of codes based on distance from touch point
199     */
200    private static void correctPrimaryJuxtapos(int primaryCode, int[] codes) {
201        if (codes.length < 2) return;
202        if (codes[0] > 0 && codes[1] > 0 && codes[0] != primaryCode && codes[1] == primaryCode) {
203            codes[1] = codes[0];
204            codes[0] = primaryCode;
205        }
206    }
207
208    /**
209     * Delete the last keystroke as a result of hitting backspace.
210     */
211    public void deleteLast() {
212        final int size = size();
213        if (size > 0) {
214            final int lastPos = size - 1;
215            char lastChar = mTypedWord.charAt(lastPos);
216            mCodes.remove(lastPos);
217            mTypedWord.deleteCharAt(lastPos);
218            if (Character.isUpperCase(lastChar)) mCapsCount--;
219        }
220        if (size() == 0) {
221            mIsFirstCharCapitalized = false;
222        }
223        if (mTrailingSingleQuotesCount > 0) {
224            --mTrailingSingleQuotesCount;
225        } else {
226            for (int i = mTypedWord.length() - 1; i >= 0; --i) {
227                if (Keyboard.CODE_SINGLE_QUOTE != mTypedWord.codePointAt(i)) break;
228                ++mTrailingSingleQuotesCount;
229            }
230        }
231        mAutoCorrection = null;
232    }
233
234    /**
235     * Returns the word as it was typed, without any correction applied.
236     * @return the word that was typed so far. Never returns null.
237     */
238    public String getTypedWord() {
239        return mTypedWord.toString();
240    }
241
242    /**
243     * Whether or not the user typed a capital letter as the first letter in the word
244     * @return capitalization preference
245     */
246    public boolean isFirstCharCapitalized() {
247        return mIsFirstCharCapitalized;
248    }
249
250    public int trailingSingleQuotesCount() {
251        return mTrailingSingleQuotesCount;
252    }
253
254    /**
255     * Whether or not all of the user typed chars are upper case
256     * @return true if all user typed chars are upper case, false otherwise
257     */
258    public boolean isAllUpperCase() {
259        return (mCapsCount > 0) && (mCapsCount == size());
260    }
261
262    /**
263     * Returns true if more than one character is upper case, otherwise returns false.
264     */
265    public boolean isMostlyCaps() {
266        return mCapsCount > 1;
267    }
268
269    /**
270     * Saves the reason why the word is capitalized - whether it was automatic or
271     * due to the user hitting shift in the middle of a sentence.
272     * @param auto whether it was an automatic capitalization due to start of sentence
273     */
274    public void setAutoCapitalized(boolean auto) {
275        mAutoCapitalized = auto;
276    }
277
278    /**
279     * Returns whether the word was automatically capitalized.
280     * @return whether the word was automatically capitalized
281     */
282    public boolean isAutoCapitalized() {
283        return mAutoCapitalized;
284    }
285
286    /**
287     * Sets the auto-correction for this word.
288     */
289    public void setAutoCorrection(final CharSequence correction) {
290        mAutoCorrection = correction;
291    }
292
293    /**
294     * @return the auto-correction for this word, or null if none.
295     */
296    public CharSequence getAutoCorrectionOrNull() {
297        return mAutoCorrection;
298    }
299
300    // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above.
301    public LastComposedWord commitWord(final int type) {
302        // Note: currently, we come here whenever we commit a word. If it's any *other* kind than
303        // DECIDED_WORD, we should reset mAutoCorrection so that we don't attempt to cancel later.
304        // If it's a DECIDED_WORD, it may be an actual auto-correction by the IME, or what the user
305        // typed because the IME decided *not* to auto-correct for whatever reason.
306        // Ideally we would also null it when it was a DECIDED_WORD that was not an auto-correct.
307        // As it happens these two cases should behave differently, because the former can be
308        // canceled while the latter can't. Currently, we figure this out in
309        // LastComposedWord#didAutoCorrectToAnotherWord with #equals(). It would be marginally
310        // cleaner to do it here, but it would be slower (since we would #equals() for each commit,
311        // instead of only on cancel), and ultimately we want to figure it out even earlier anyway.
312        final LastComposedWord lastComposedWord = new LastComposedWord(mCodes,
313                mXCoordinates, mYCoordinates, mTypedWord.toString(),
314                null == mAutoCorrection ? null : mAutoCorrection.toString());
315        if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD) {
316            lastComposedWord.deactivate();
317        }
318        mCodes.clear();
319        mTypedWord.setLength(0);
320        mAutoCorrection = null;
321        return lastComposedWord;
322    }
323
324    public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) {
325        mCodes = lastComposedWord.mCodes;
326        mXCoordinates = lastComposedWord.mXCoordinates;
327        mYCoordinates = lastComposedWord.mYCoordinates;
328        mTypedWord.setLength(0);
329        mTypedWord.append(lastComposedWord.mTypedWord);
330        mAutoCorrection = lastComposedWord.mAutoCorrection;
331    }
332}
333