WordComposer.java revision 01ab7c8b59a7f12862fbd95fb252e56719f1757f
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;
22import com.android.inputmethod.keyboard.KeyboardActionListener;
23
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    private static final int N = BinaryDictionary.MAX_WORD_LENGTH;
35
36    private int[] mPrimaryKeyCodes;
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    private int mCodePointSize;
47
48    /**
49     * Whether the user chose to capitalize the first char of the word.
50     */
51    private boolean mIsFirstCharCapitalized;
52
53    public WordComposer() {
54        mPrimaryKeyCodes = new int[N];
55        mTypedWord = new StringBuilder(N);
56        mXCoordinates = new int[N];
57        mYCoordinates = new int[N];
58        mAutoCorrection = null;
59        mTrailingSingleQuotesCount = 0;
60        refreshSize();
61    }
62
63    public WordComposer(WordComposer source) {
64        init(source);
65    }
66
67    public void init(WordComposer source) {
68        mPrimaryKeyCodes = Arrays.copyOf(source.mPrimaryKeyCodes, source.mPrimaryKeyCodes.length);
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        refreshSize();
77    }
78
79    /**
80     * Clear out the keys registered so far.
81     */
82    public void reset() {
83        mTypedWord.setLength(0);
84        mAutoCorrection = null;
85        mCapsCount = 0;
86        mIsFirstCharCapitalized = false;
87        mTrailingSingleQuotesCount = 0;
88        refreshSize();
89    }
90
91    public final void refreshSize() {
92        mCodePointSize = mTypedWord.codePointCount(0, mTypedWord.length());
93    }
94
95    /**
96     * Number of keystrokes in the composing word.
97     * @return the number of keystrokes
98     */
99    public final int size() {
100        return mCodePointSize;
101    }
102
103    public final boolean isComposingWord() {
104        return size() > 0;
105    }
106
107    public int getCodeAt(int index) {
108        return mPrimaryKeyCodes[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    // TODO: remove input keyDetector
125    public void add(int primaryCode, int x, int y, KeyDetector keyDetector) {
126        final int[] codes;
127        final int keyX;
128        final int keyY;
129        if (null == keyDetector
130                || x == KeyboardActionListener.SUGGESTION_STRIP_COORDINATE
131                || y == KeyboardActionListener.SUGGESTION_STRIP_COORDINATE
132                || x == KeyboardActionListener.NOT_A_TOUCH_COORDINATE
133                || y == KeyboardActionListener.NOT_A_TOUCH_COORDINATE) {
134            codes = new int[] { primaryCode };
135            keyX = x;
136            keyY = y;
137        } else {
138            final Key key = keyDetector.detectHitKey(x, y);
139            // TODO: Pass an integer instead of an integer array
140            codes = new int[] { key != null ? key.mCode : NOT_A_CODE };
141            keyX = keyDetector.getTouchX(x);
142            keyY = keyDetector.getTouchY(y);
143        }
144        add(primaryCode, codes, keyX, keyY);
145    }
146
147    /**
148     * Add a new keystroke, with codes[0] containing the pressed key's unicode and the rest of
149     * the array containing unicode for adjacent keys, sorted by reducing probability/proximity.
150     * @param codes the array of unicode values
151     */
152    private void add(int primaryCode, int[] codes, int keyX, int keyY) {
153        final int newIndex = size();
154        mTypedWord.appendCodePoint(primaryCode);
155        refreshSize();
156        mPrimaryKeyCodes[newIndex] = codes[0];
157        if (newIndex < BinaryDictionary.MAX_WORD_LENGTH) {
158            mXCoordinates[newIndex] = keyX;
159            mYCoordinates[newIndex] = keyY;
160        }
161        mIsFirstCharCapitalized = isFirstCharCapitalized(
162                newIndex, primaryCode, mIsFirstCharCapitalized);
163        if (Character.isUpperCase(primaryCode)) mCapsCount++;
164        if (Keyboard.CODE_SINGLE_QUOTE == primaryCode) {
165            ++mTrailingSingleQuotesCount;
166        } else {
167            mTrailingSingleQuotesCount = 0;
168        }
169        mAutoCorrection = null;
170    }
171
172    /**
173     * Internal method to retrieve reasonable proximity info for a character.
174     */
175    private void addKeyInfo(final int codePoint, final Keyboard keyboard) {
176        for (final Key key : keyboard.mKeys) {
177            if (key.mCode == codePoint) {
178                final int x = key.mX + key.mWidth / 2;
179                final int y = key.mY + key.mHeight / 2;
180                // TODO: Pass an integer instead of an integer array
181                add(codePoint, new int[] { key.mCode }, x, y);
182                return;
183            }
184        }
185        add(codePoint, new int[] { codePoint },
186                WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
187    }
188
189    /**
190     * Set the currently composing word to the one passed as an argument.
191     * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity.
192     */
193    public void setComposingWord(final CharSequence word, final Keyboard keyboard) {
194        reset();
195        final int length = word.length();
196        for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
197            int codePoint = Character.codePointAt(word, i);
198            addKeyInfo(codePoint, keyboard);
199        }
200    }
201
202    /**
203     * Delete the last keystroke as a result of hitting backspace.
204     */
205    public void deleteLast() {
206        final int size = size();
207        if (size > 0) {
208            // Note: mTypedWord.length() and mCodes.length differ when there are surrogate pairs
209            final int stringBuilderLength = mTypedWord.length();
210            if (stringBuilderLength < size) {
211                throw new RuntimeException(
212                        "In WordComposer: mCodes and mTypedWords have non-matching lengths");
213            }
214            final int lastChar = mTypedWord.codePointBefore(stringBuilderLength);
215            if (Character.isSupplementaryCodePoint(lastChar)) {
216                mTypedWord.delete(stringBuilderLength - 2, stringBuilderLength);
217            } else {
218                mTypedWord.deleteCharAt(stringBuilderLength - 1);
219            }
220            if (Character.isUpperCase(lastChar)) mCapsCount--;
221            refreshSize();
222        }
223        // We may have deleted the last one.
224        if (0 == size()) {
225            mIsFirstCharCapitalized = false;
226        }
227        if (mTrailingSingleQuotesCount > 0) {
228            --mTrailingSingleQuotesCount;
229        } else {
230            int i = mTypedWord.length();
231            while (i > 0) {
232                i = mTypedWord.offsetByCodePoints(i, -1);
233                if (Keyboard.CODE_SINGLE_QUOTE != mTypedWord.codePointAt(i)) break;
234                ++mTrailingSingleQuotesCount;
235            }
236        }
237        mAutoCorrection = null;
238    }
239
240    /**
241     * Returns the word as it was typed, without any correction applied.
242     * @return the word that was typed so far. Never returns null.
243     */
244    public String getTypedWord() {
245        return mTypedWord.toString();
246    }
247
248    /**
249     * Whether or not the user typed a capital letter as the first letter in the word
250     * @return capitalization preference
251     */
252    public boolean isFirstCharCapitalized() {
253        return mIsFirstCharCapitalized;
254    }
255
256    public int trailingSingleQuotesCount() {
257        return mTrailingSingleQuotesCount;
258    }
259
260    /**
261     * Whether or not all of the user typed chars are upper case
262     * @return true if all user typed chars are upper case, false otherwise
263     */
264    public boolean isAllUpperCase() {
265        return (mCapsCount > 0) && (mCapsCount == size());
266    }
267
268    /**
269     * Returns true if more than one character is upper case, otherwise returns false.
270     */
271    public boolean isMostlyCaps() {
272        return mCapsCount > 1;
273    }
274
275    /**
276     * Saves the reason why the word is capitalized - whether it was automatic or
277     * due to the user hitting shift in the middle of a sentence.
278     * @param auto whether it was an automatic capitalization due to start of sentence
279     */
280    public void setAutoCapitalized(boolean auto) {
281        mAutoCapitalized = auto;
282    }
283
284    /**
285     * Returns whether the word was automatically capitalized.
286     * @return whether the word was automatically capitalized
287     */
288    public boolean isAutoCapitalized() {
289        return mAutoCapitalized;
290    }
291
292    /**
293     * Sets the auto-correction for this word.
294     */
295    public void setAutoCorrection(final CharSequence correction) {
296        mAutoCorrection = correction;
297    }
298
299    /**
300     * @return the auto-correction for this word, or null if none.
301     */
302    public CharSequence getAutoCorrectionOrNull() {
303        return mAutoCorrection;
304    }
305
306    // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above.
307    public LastComposedWord commitWord(final int type, final String committedWord,
308            final int separatorCode) {
309        // Note: currently, we come here whenever we commit a word. If it's a MANUAL_PICK
310        // or a DECIDED_WORD we may cancel the commit later; otherwise, we should deactivate
311        // the last composed word to ensure this does not happen.
312        final int[] primaryKeyCodes = mPrimaryKeyCodes;
313        final int[] xCoordinates = mXCoordinates;
314        final int[] yCoordinates = mYCoordinates;
315        mPrimaryKeyCodes = new int[N];
316        mXCoordinates = new int[N];
317        mYCoordinates = new int[N];
318        final LastComposedWord lastComposedWord = new LastComposedWord(primaryKeyCodes,
319                xCoordinates, yCoordinates, mTypedWord.toString(), committedWord, separatorCode);
320        if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD
321                && type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) {
322            lastComposedWord.deactivate();
323        }
324        mTypedWord.setLength(0);
325        refreshSize();
326        mAutoCorrection = null;
327        return lastComposedWord;
328    }
329
330    public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) {
331        mPrimaryKeyCodes = lastComposedWord.mPrimaryKeyCodes;
332        mXCoordinates = lastComposedWord.mXCoordinates;
333        mYCoordinates = lastComposedWord.mYCoordinates;
334        mTypedWord.setLength(0);
335        mTypedWord.append(lastComposedWord.mTypedWord);
336        refreshSize();
337        mAutoCorrection = null; // This will be filled by the next call to updateSuggestion.
338    }
339}
340