Home | History | Annotate | Download | only in latin
      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 
     17 package com.android.inputmethod.latin;
     18 
     19 import com.android.inputmethod.keyboard.Key;
     20 import com.android.inputmethod.keyboard.Keyboard;
     21 
     22 import java.util.Arrays;
     23 
     24 /**
     25  * A place to store the currently composing word with information such as adjacent key codes as well
     26  */
     27 public final class WordComposer {
     28     private static final int N = BinaryDictionary.MAX_WORD_LENGTH;
     29 
     30     public static final int CAPS_MODE_OFF = 0;
     31     // 1 is shift bit, 2 is caps bit, 4 is auto bit but this is just a convention as these bits
     32     // aren't used anywhere in the code
     33     public static final int CAPS_MODE_MANUAL_SHIFTED = 0x1;
     34     public static final int CAPS_MODE_MANUAL_SHIFT_LOCKED = 0x3;
     35     public static final int CAPS_MODE_AUTO_SHIFTED = 0x5;
     36     public static final int CAPS_MODE_AUTO_SHIFT_LOCKED = 0x7;
     37 
     38     private int[] mPrimaryKeyCodes;
     39     private final InputPointers mInputPointers = new InputPointers(N);
     40     private final StringBuilder mTypedWord;
     41     private CharSequence mAutoCorrection;
     42     private boolean mIsResumed;
     43     private boolean mIsBatchMode;
     44 
     45     // Cache these values for performance
     46     private int mCapsCount;
     47     private int mDigitsCount;
     48     private int mCapitalizedMode;
     49     private int mTrailingSingleQuotesCount;
     50     private int mCodePointSize;
     51 
     52     /**
     53      * Whether the user chose to capitalize the first char of the word.
     54      */
     55     private boolean mIsFirstCharCapitalized;
     56 
     57     public WordComposer() {
     58         mPrimaryKeyCodes = new int[N];
     59         mTypedWord = new StringBuilder(N);
     60         mAutoCorrection = null;
     61         mTrailingSingleQuotesCount = 0;
     62         mIsResumed = false;
     63         mIsBatchMode = false;
     64         refreshSize();
     65     }
     66 
     67     public WordComposer(WordComposer source) {
     68         mPrimaryKeyCodes = Arrays.copyOf(source.mPrimaryKeyCodes, source.mPrimaryKeyCodes.length);
     69         mTypedWord = new StringBuilder(source.mTypedWord);
     70         mInputPointers.copy(source.mInputPointers);
     71         mCapsCount = source.mCapsCount;
     72         mDigitsCount = source.mDigitsCount;
     73         mIsFirstCharCapitalized = source.mIsFirstCharCapitalized;
     74         mCapitalizedMode = source.mCapitalizedMode;
     75         mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount;
     76         mIsResumed = source.mIsResumed;
     77         mIsBatchMode = source.mIsBatchMode;
     78         refreshSize();
     79     }
     80 
     81     /**
     82      * Clear out the keys registered so far.
     83      */
     84     public void reset() {
     85         mTypedWord.setLength(0);
     86         mAutoCorrection = null;
     87         mCapsCount = 0;
     88         mDigitsCount = 0;
     89         mIsFirstCharCapitalized = false;
     90         mTrailingSingleQuotesCount = 0;
     91         mIsResumed = false;
     92         mIsBatchMode = false;
     93         refreshSize();
     94     }
     95 
     96     private final void refreshSize() {
     97         mCodePointSize = mTypedWord.codePointCount(0, mTypedWord.length());
     98     }
     99 
    100     /**
    101      * Number of keystrokes in the composing word.
    102      * @return the number of keystrokes
    103      */
    104     public final int size() {
    105         return mCodePointSize;
    106     }
    107 
    108     public final boolean isComposingWord() {
    109         return size() > 0;
    110     }
    111 
    112     // TODO: make sure that the index should not exceed MAX_WORD_LENGTH
    113     public int getCodeAt(int index) {
    114         if (index >= BinaryDictionary.MAX_WORD_LENGTH) {
    115             return -1;
    116         }
    117         return mPrimaryKeyCodes[index];
    118     }
    119 
    120     public InputPointers getInputPointers() {
    121         return mInputPointers;
    122     }
    123 
    124     private static boolean isFirstCharCapitalized(int index, int codePoint, boolean previous) {
    125         if (index == 0) return Character.isUpperCase(codePoint);
    126         return previous && !Character.isUpperCase(codePoint);
    127     }
    128 
    129     /**
    130      * Add a new keystroke, with the pressed key's code point with the touch point coordinates.
    131      */
    132     public void add(int primaryCode, int keyX, int keyY) {
    133         final int newIndex = size();
    134         mTypedWord.appendCodePoint(primaryCode);
    135         refreshSize();
    136         if (newIndex < BinaryDictionary.MAX_WORD_LENGTH) {
    137             mPrimaryKeyCodes[newIndex] = primaryCode >= Keyboard.CODE_SPACE
    138                     ? Character.toLowerCase(primaryCode) : primaryCode;
    139             // In the batch input mode, the {@code mInputPointers} holds batch input points and
    140             // shouldn't be overridden by the "typed key" coordinates
    141             // (See {@link #setBatchInputWord}).
    142             if (!mIsBatchMode) {
    143                 // TODO: Set correct pointer id and time
    144                 mInputPointers.addPointer(newIndex, keyX, keyY, 0, 0);
    145             }
    146         }
    147         mIsFirstCharCapitalized = isFirstCharCapitalized(
    148                 newIndex, primaryCode, mIsFirstCharCapitalized);
    149         if (Character.isUpperCase(primaryCode)) mCapsCount++;
    150         if (Character.isDigit(primaryCode)) mDigitsCount++;
    151         if (Keyboard.CODE_SINGLE_QUOTE == primaryCode) {
    152             ++mTrailingSingleQuotesCount;
    153         } else {
    154             mTrailingSingleQuotesCount = 0;
    155         }
    156         mAutoCorrection = null;
    157     }
    158 
    159     public void setBatchInputPointers(InputPointers batchPointers) {
    160         mInputPointers.set(batchPointers);
    161         mIsBatchMode = true;
    162     }
    163 
    164     public void setBatchInputWord(CharSequence word) {
    165         reset();
    166         mIsBatchMode = true;
    167         final int length = word.length();
    168         for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
    169             final int codePoint = Character.codePointAt(word, i);
    170             // We don't want to override the batch input points that are held in mInputPointers
    171             // (See {@link #add(int,int,int)}).
    172             add(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
    173         }
    174     }
    175 
    176     /**
    177      * Internal method to retrieve reasonable proximity info for a character.
    178      */
    179     private void addKeyInfo(final int codePoint, final Keyboard keyboard) {
    180         final int x, y;
    181         final Key key;
    182         if (keyboard != null && (key = keyboard.getKey(codePoint)) != null) {
    183             x = key.mX + key.mWidth / 2;
    184             y = key.mY + key.mHeight / 2;
    185         } else {
    186             x = Constants.NOT_A_COORDINATE;
    187             y = Constants.NOT_A_COORDINATE;
    188         }
    189         add(codePoint, x, y);
    190     }
    191 
    192     /**
    193      * Set the currently composing word to the one passed as an argument.
    194      * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity.
    195      */
    196     public void setComposingWord(final CharSequence word, final Keyboard keyboard) {
    197         reset();
    198         final int length = word.length();
    199         for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
    200             final int codePoint = Character.codePointAt(word, i);
    201             addKeyInfo(codePoint, keyboard);
    202         }
    203         mIsResumed = true;
    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             // Note: mTypedWord.length() and mCodes.length differ when there are surrogate pairs
    213             final int stringBuilderLength = mTypedWord.length();
    214             if (stringBuilderLength < size) {
    215                 throw new RuntimeException(
    216                         "In WordComposer: mCodes and mTypedWords have non-matching lengths");
    217             }
    218             final int lastChar = mTypedWord.codePointBefore(stringBuilderLength);
    219             if (Character.isSupplementaryCodePoint(lastChar)) {
    220                 mTypedWord.delete(stringBuilderLength - 2, stringBuilderLength);
    221             } else {
    222                 mTypedWord.deleteCharAt(stringBuilderLength - 1);
    223             }
    224             if (Character.isUpperCase(lastChar)) mCapsCount--;
    225             if (Character.isDigit(lastChar)) mDigitsCount--;
    226             refreshSize();
    227         }
    228         // We may have deleted the last one.
    229         if (0 == size()) {
    230             mIsFirstCharCapitalized = false;
    231         }
    232         if (mTrailingSingleQuotesCount > 0) {
    233             --mTrailingSingleQuotesCount;
    234         } else {
    235             int i = mTypedWord.length();
    236             while (i > 0) {
    237                 i = mTypedWord.offsetByCodePoints(i, -1);
    238                 if (Keyboard.CODE_SINGLE_QUOTE != mTypedWord.codePointAt(i)) break;
    239                 ++mTrailingSingleQuotesCount;
    240             }
    241         }
    242         mAutoCorrection = null;
    243     }
    244 
    245     /**
    246      * Returns the word as it was typed, without any correction applied.
    247      * @return the word that was typed so far. Never returns null.
    248      */
    249     public String getTypedWord() {
    250         return mTypedWord.toString();
    251     }
    252 
    253     /**
    254      * Whether or not the user typed a capital letter as the first letter in the word
    255      * @return capitalization preference
    256      */
    257     public boolean isFirstCharCapitalized() {
    258         return mIsFirstCharCapitalized;
    259     }
    260 
    261     public int trailingSingleQuotesCount() {
    262         return mTrailingSingleQuotesCount;
    263     }
    264 
    265     /**
    266      * Whether or not all of the user typed chars are upper case
    267      * @return true if all user typed chars are upper case, false otherwise
    268      */
    269     public boolean isAllUpperCase() {
    270         if (size() <= 1) {
    271             return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
    272                     || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFT_LOCKED;
    273         } else {
    274             return mCapsCount == size();
    275         }
    276     }
    277 
    278     public boolean wasShiftedNoLock() {
    279         return mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED
    280                 || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFTED;
    281     }
    282 
    283     /**
    284      * Returns true if more than one character is upper case, otherwise returns false.
    285      */
    286     public boolean isMostlyCaps() {
    287         return mCapsCount > 1;
    288     }
    289 
    290     /**
    291      * Returns true if we have digits in the composing word.
    292      */
    293     public boolean hasDigits() {
    294         return mDigitsCount > 0;
    295     }
    296 
    297     /**
    298      * Saves the caps mode at the start of composing.
    299      *
    300      * WordComposer needs to know about this for several reasons. The first is, we need to know
    301      * after the fact what the reason was, to register the correct form into the user history
    302      * dictionary: if the word was automatically capitalized, we should insert it in all-lower
    303      * case but if it's a manual pressing of shift, then it should be inserted as is.
    304      * Also, batch input needs to know about the current caps mode to display correctly
    305      * capitalized suggestions.
    306      * @param mode the mode at the time of start
    307      */
    308     public void setCapitalizedModeAtStartComposingTime(final int mode) {
    309         mCapitalizedMode = mode;
    310     }
    311 
    312     /**
    313      * Returns whether the word was automatically capitalized.
    314      * @return whether the word was automatically capitalized
    315      */
    316     public boolean wasAutoCapitalized() {
    317         return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
    318                 || mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED;
    319     }
    320 
    321     /**
    322      * Sets the auto-correction for this word.
    323      */
    324     public void setAutoCorrection(final CharSequence correction) {
    325         mAutoCorrection = correction;
    326     }
    327 
    328     /**
    329      * @return the auto-correction for this word, or null if none.
    330      */
    331     public CharSequence getAutoCorrectionOrNull() {
    332         return mAutoCorrection;
    333     }
    334 
    335     /**
    336      * @return whether we started composing this word by resuming suggestion on an existing string
    337      */
    338     public boolean isResumed() {
    339         return mIsResumed;
    340     }
    341 
    342     // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above.
    343     public LastComposedWord commitWord(final int type, final String committedWord,
    344             final String separatorString, final CharSequence prevWord) {
    345         // Note: currently, we come here whenever we commit a word. If it's a MANUAL_PICK
    346         // or a DECIDED_WORD we may cancel the commit later; otherwise, we should deactivate
    347         // the last composed word to ensure this does not happen.
    348         final int[] primaryKeyCodes = mPrimaryKeyCodes;
    349         mPrimaryKeyCodes = new int[N];
    350         final LastComposedWord lastComposedWord = new LastComposedWord(primaryKeyCodes,
    351                 mInputPointers, mTypedWord.toString(), committedWord, separatorString,
    352                 prevWord);
    353         mInputPointers.reset();
    354         if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD
    355                 && type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) {
    356             lastComposedWord.deactivate();
    357         }
    358         mCapsCount = 0;
    359         mDigitsCount = 0;
    360         mIsBatchMode = false;
    361         mTypedWord.setLength(0);
    362         mTrailingSingleQuotesCount = 0;
    363         mIsFirstCharCapitalized = false;
    364         refreshSize();
    365         mAutoCorrection = null;
    366         mIsResumed = false;
    367         return lastComposedWord;
    368     }
    369 
    370     public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) {
    371         mPrimaryKeyCodes = lastComposedWord.mPrimaryKeyCodes;
    372         mInputPointers.set(lastComposedWord.mInputPointers);
    373         mTypedWord.setLength(0);
    374         mTypedWord.append(lastComposedWord.mTypedWord);
    375         refreshSize();
    376         mAutoCorrection = null; // This will be filled by the next call to updateSuggestion.
    377         mIsResumed = true;
    378     }
    379 
    380     public boolean isBatchMode() {
    381         return mIsBatchMode;
    382     }
    383 }
    384