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