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