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