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");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of 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,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under 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 import com.android.inputmethod.latin.utils.StringUtils;
     22 
     23 import java.util.Arrays;
     24 
     25 /**
     26  * A place to store the currently composing word with information such as adjacent key codes as well
     27  */
     28 public final class WordComposer {
     29     private static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH;
     30     private static final boolean DBG = LatinImeLogger.sDBG;
     31 
     32     public static final int CAPS_MODE_OFF = 0;
     33     // 1 is shift bit, 2 is caps bit, 4 is auto bit but this is just a convention as these bits
     34     // aren't used anywhere in the code
     35     public static final int CAPS_MODE_MANUAL_SHIFTED = 0x1;
     36     public static final int CAPS_MODE_MANUAL_SHIFT_LOCKED = 0x3;
     37     public static final int CAPS_MODE_AUTO_SHIFTED = 0x5;
     38     public static final int CAPS_MODE_AUTO_SHIFT_LOCKED = 0x7;
     39 
     40     // An array of code points representing the characters typed so far.
     41     // The array is limited to MAX_WORD_LENGTH code points, but mTypedWord extends past that
     42     // and mCodePointSize can go past that. If mCodePointSize is greater than MAX_WORD_LENGTH,
     43     // this just does not contain the associated code points past MAX_WORD_LENGTH.
     44     private int[] mPrimaryKeyCodes;
     45     private final InputPointers mInputPointers = new InputPointers(MAX_WORD_LENGTH);
     46     // This is the typed word, as a StringBuilder. This has the same contents as mPrimaryKeyCodes
     47     // but under a StringBuilder representation for ease of use, depending on what is more useful
     48     // at any given time. However this is not limited in size, while mPrimaryKeyCodes is limited
     49     // to MAX_WORD_LENGTH code points.
     50     private final StringBuilder mTypedWord;
     51     private String mAutoCorrection;
     52     private boolean mIsResumed;
     53     private boolean mIsBatchMode;
     54     // A memory of the last rejected batch mode suggestion, if any. This goes like this: the user
     55     // gestures a word, is displeased with the results and hits backspace, then gestures again.
     56     // At the very least we should avoid re-suggesting the same thing, and to do that we memorize
     57     // the rejected suggestion in this variable.
     58     // TODO: this should be done in a comprehensive way by the User History feature instead of
     59     // as an ad-hockery here.
     60     private String mRejectedBatchModeSuggestion;
     61 
     62     // Cache these values for performance
     63     private int mCapsCount;
     64     private int mDigitsCount;
     65     private int mCapitalizedMode;
     66     private int mTrailingSingleQuotesCount;
     67     // This is the number of code points entered so far. This is not limited to MAX_WORD_LENGTH.
     68     // In general, this contains the size of mPrimaryKeyCodes, except when this is greater than
     69     // MAX_WORD_LENGTH in which case mPrimaryKeyCodes only contain the first MAX_WORD_LENGTH
     70     // code points.
     71     private int mCodePointSize;
     72     private int mCursorPositionWithinWord;
     73 
     74     /**
     75      * Whether the user chose to capitalize the first char of the word.
     76      */
     77     private boolean mIsFirstCharCapitalized;
     78 
     79     public WordComposer() {
     80         mPrimaryKeyCodes = new int[MAX_WORD_LENGTH];
     81         mTypedWord = new StringBuilder(MAX_WORD_LENGTH);
     82         mAutoCorrection = null;
     83         mTrailingSingleQuotesCount = 0;
     84         mIsResumed = false;
     85         mIsBatchMode = false;
     86         mCursorPositionWithinWord = 0;
     87         mRejectedBatchModeSuggestion = null;
     88         refreshSize();
     89     }
     90 
     91     public WordComposer(final WordComposer source) {
     92         mPrimaryKeyCodes = Arrays.copyOf(source.mPrimaryKeyCodes, source.mPrimaryKeyCodes.length);
     93         mTypedWord = new StringBuilder(source.mTypedWord);
     94         mInputPointers.copy(source.mInputPointers);
     95         mCapsCount = source.mCapsCount;
     96         mDigitsCount = source.mDigitsCount;
     97         mIsFirstCharCapitalized = source.mIsFirstCharCapitalized;
     98         mCapitalizedMode = source.mCapitalizedMode;
     99         mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount;
    100         mIsResumed = source.mIsResumed;
    101         mIsBatchMode = source.mIsBatchMode;
    102         mCursorPositionWithinWord = source.mCursorPositionWithinWord;
    103         mRejectedBatchModeSuggestion = source.mRejectedBatchModeSuggestion;
    104         refreshSize();
    105     }
    106 
    107     /**
    108      * Clear out the keys registered so far.
    109      */
    110     public void reset() {
    111         mTypedWord.setLength(0);
    112         mAutoCorrection = null;
    113         mCapsCount = 0;
    114         mDigitsCount = 0;
    115         mIsFirstCharCapitalized = false;
    116         mTrailingSingleQuotesCount = 0;
    117         mIsResumed = false;
    118         mIsBatchMode = false;
    119         mCursorPositionWithinWord = 0;
    120         mRejectedBatchModeSuggestion = null;
    121         refreshSize();
    122     }
    123 
    124     private final void refreshSize() {
    125         mCodePointSize = mTypedWord.codePointCount(0, mTypedWord.length());
    126     }
    127 
    128     /**
    129      * Number of keystrokes in the composing word.
    130      * @return the number of keystrokes
    131      */
    132     public final int size() {
    133         return mCodePointSize;
    134     }
    135 
    136     public final boolean isComposingWord() {
    137         return size() > 0;
    138     }
    139 
    140     // TODO: make sure that the index should not exceed MAX_WORD_LENGTH
    141     public int getCodeAt(int index) {
    142         if (index >= MAX_WORD_LENGTH) {
    143             return -1;
    144         }
    145         return mPrimaryKeyCodes[index];
    146     }
    147 
    148     public int getCodeBeforeCursor() {
    149         if (mCursorPositionWithinWord < 1 || mCursorPositionWithinWord > mPrimaryKeyCodes.length) {
    150             return Constants.NOT_A_CODE;
    151         }
    152         return mPrimaryKeyCodes[mCursorPositionWithinWord - 1];
    153     }
    154 
    155     public InputPointers getInputPointers() {
    156         return mInputPointers;
    157     }
    158 
    159     private static boolean isFirstCharCapitalized(final int index, final int codePoint,
    160             final boolean previous) {
    161         if (index == 0) return Character.isUpperCase(codePoint);
    162         return previous && !Character.isUpperCase(codePoint);
    163     }
    164 
    165     /**
    166      * Add a new keystroke, with the pressed key's code point with the touch point coordinates.
    167      */
    168     public void add(final int primaryCode, final int keyX, final int keyY) {
    169         final int newIndex = size();
    170         mTypedWord.appendCodePoint(primaryCode);
    171         refreshSize();
    172         mCursorPositionWithinWord = mCodePointSize;
    173         if (newIndex < MAX_WORD_LENGTH) {
    174             mPrimaryKeyCodes[newIndex] = primaryCode >= Constants.CODE_SPACE
    175                     ? Character.toLowerCase(primaryCode) : primaryCode;
    176             // In the batch input mode, the {@code mInputPointers} holds batch input points and
    177             // shouldn't be overridden by the "typed key" coordinates
    178             // (See {@link #setBatchInputWord}).
    179             if (!mIsBatchMode) {
    180                 // TODO: Set correct pointer id and time
    181                 mInputPointers.addPointer(newIndex, keyX, keyY, 0, 0);
    182             }
    183         }
    184         mIsFirstCharCapitalized = isFirstCharCapitalized(
    185                 newIndex, primaryCode, mIsFirstCharCapitalized);
    186         if (Character.isUpperCase(primaryCode)) mCapsCount++;
    187         if (Character.isDigit(primaryCode)) mDigitsCount++;
    188         if (Constants.CODE_SINGLE_QUOTE == primaryCode) {
    189             ++mTrailingSingleQuotesCount;
    190         } else {
    191             mTrailingSingleQuotesCount = 0;
    192         }
    193         mAutoCorrection = null;
    194     }
    195 
    196     public void setCursorPositionWithinWord(final int posWithinWord) {
    197         mCursorPositionWithinWord = posWithinWord;
    198     }
    199 
    200     public boolean isCursorFrontOrMiddleOfComposingWord() {
    201         if (DBG && mCursorPositionWithinWord > mCodePointSize) {
    202             throw new RuntimeException("Wrong cursor position : " + mCursorPositionWithinWord
    203                     + "in a word of size " + mCodePointSize);
    204         }
    205         return mCursorPositionWithinWord != mCodePointSize;
    206     }
    207 
    208     /**
    209      * When the cursor is moved by the user, we need to update its position.
    210      * If it falls inside the currently composing word, we don't reset the composition, and
    211      * only update the cursor position.
    212      *
    213      * @param expectedMoveAmount How many java chars to move the cursor. Negative values move
    214      * the cursor backward, positive values move the cursor forward.
    215      * @return true if the cursor is still inside the composing word, false otherwise.
    216      */
    217     public boolean moveCursorByAndReturnIfInsideComposingWord(final int expectedMoveAmount) {
    218         int actualMoveAmountWithinWord = 0;
    219         int cursorPos = mCursorPositionWithinWord;
    220         final int[] codePoints;
    221         if (mCodePointSize >= MAX_WORD_LENGTH) {
    222             // If we have more than MAX_WORD_LENGTH characters, we don't have everything inside
    223             // mPrimaryKeyCodes. This should be rare enough that we can afford to just compute
    224             // the array on the fly when this happens.
    225             codePoints = StringUtils.toCodePointArray(mTypedWord.toString());
    226         } else {
    227             codePoints = mPrimaryKeyCodes;
    228         }
    229         if (expectedMoveAmount >= 0) {
    230             // Moving the cursor forward for the expected amount or until the end of the word has
    231             // been reached, whichever comes first.
    232             while (actualMoveAmountWithinWord < expectedMoveAmount && cursorPos < mCodePointSize) {
    233                 actualMoveAmountWithinWord += Character.charCount(codePoints[cursorPos]);
    234                 ++cursorPos;
    235             }
    236         } else {
    237             // Moving the cursor backward for the expected amount or until the start of the word
    238             // has been reached, whichever comes first.
    239             while (actualMoveAmountWithinWord > expectedMoveAmount && cursorPos > 0) {
    240                 --cursorPos;
    241                 actualMoveAmountWithinWord -= Character.charCount(codePoints[cursorPos]);
    242             }
    243         }
    244         // If the actual and expected amounts differ, we crossed the start or the end of the word
    245         // so the result would not be inside the composing word.
    246         if (actualMoveAmountWithinWord != expectedMoveAmount) return false;
    247         mCursorPositionWithinWord = cursorPos;
    248         return true;
    249     }
    250 
    251     public void setBatchInputPointers(final InputPointers batchPointers) {
    252         mInputPointers.set(batchPointers);
    253         mIsBatchMode = true;
    254     }
    255 
    256     public void setBatchInputWord(final String word) {
    257         reset();
    258         mIsBatchMode = true;
    259         final int length = word.length();
    260         for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
    261             final int codePoint = Character.codePointAt(word, i);
    262             // We don't want to override the batch input points that are held in mInputPointers
    263             // (See {@link #add(int,int,int)}).
    264             add(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
    265         }
    266     }
    267 
    268     /**
    269      * Add a dummy key by retrieving reasonable coordinates
    270      */
    271     public void addKeyInfo(final int codePoint, final Keyboard keyboard) {
    272         final int x, y;
    273         final Key key;
    274         if (keyboard != null && (key = keyboard.getKey(codePoint)) != null) {
    275             x = key.getX() + key.getWidth() / 2;
    276             y = key.getY() + key.getHeight() / 2;
    277         } else {
    278             x = Constants.NOT_A_COORDINATE;
    279             y = Constants.NOT_A_COORDINATE;
    280         }
    281         add(codePoint, x, y);
    282     }
    283 
    284     /**
    285      * Set the currently composing word to the one passed as an argument.
    286      * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity.
    287      */
    288     public void setComposingWord(final CharSequence word, final Keyboard keyboard) {
    289         reset();
    290         final int length = word.length();
    291         for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
    292             final int codePoint = Character.codePointAt(word, i);
    293             addKeyInfo(codePoint, keyboard);
    294         }
    295         mIsResumed = true;
    296     }
    297 
    298     /**
    299      * Delete the last keystroke as a result of hitting backspace.
    300      */
    301     public void deleteLast() {
    302         final int size = size();
    303         if (size > 0) {
    304             // Note: mTypedWord.length() and mCodes.length differ when there are surrogate pairs
    305             final int stringBuilderLength = mTypedWord.length();
    306             if (stringBuilderLength < size) {
    307                 throw new RuntimeException(
    308                         "In WordComposer: mCodes and mTypedWords have non-matching lengths");
    309             }
    310             final int lastChar = mTypedWord.codePointBefore(stringBuilderLength);
    311             if (Character.isSupplementaryCodePoint(lastChar)) {
    312                 mTypedWord.delete(stringBuilderLength - 2, stringBuilderLength);
    313             } else {
    314                 mTypedWord.deleteCharAt(stringBuilderLength - 1);
    315             }
    316             if (Character.isUpperCase(lastChar)) mCapsCount--;
    317             if (Character.isDigit(lastChar)) mDigitsCount--;
    318             refreshSize();
    319         }
    320         // We may have deleted the last one.
    321         if (0 == size()) {
    322             mIsFirstCharCapitalized = false;
    323         }
    324         if (mTrailingSingleQuotesCount > 0) {
    325             --mTrailingSingleQuotesCount;
    326         } else {
    327             int i = mTypedWord.length();
    328             while (i > 0) {
    329                 i = mTypedWord.offsetByCodePoints(i, -1);
    330                 if (Constants.CODE_SINGLE_QUOTE != mTypedWord.codePointAt(i)) break;
    331                 ++mTrailingSingleQuotesCount;
    332             }
    333         }
    334         mCursorPositionWithinWord = mCodePointSize;
    335         mAutoCorrection = null;
    336     }
    337 
    338     /**
    339      * Returns the word as it was typed, without any correction applied.
    340      * @return the word that was typed so far. Never returns null.
    341      */
    342     public String getTypedWord() {
    343         return mTypedWord.toString();
    344     }
    345 
    346     /**
    347      * Whether or not the user typed a capital letter as the first letter in the word
    348      * @return capitalization preference
    349      */
    350     public boolean isFirstCharCapitalized() {
    351         return mIsFirstCharCapitalized;
    352     }
    353 
    354     public int trailingSingleQuotesCount() {
    355         return mTrailingSingleQuotesCount;
    356     }
    357 
    358     /**
    359      * Whether or not all of the user typed chars are upper case
    360      * @return true if all user typed chars are upper case, false otherwise
    361      */
    362     public boolean isAllUpperCase() {
    363         if (size() <= 1) {
    364             return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
    365                     || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFT_LOCKED;
    366         } else {
    367             return mCapsCount == size();
    368         }
    369     }
    370 
    371     public boolean wasShiftedNoLock() {
    372         return mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED
    373                 || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFTED;
    374     }
    375 
    376     /**
    377      * Returns true if more than one character is upper case, otherwise returns false.
    378      */
    379     public boolean isMostlyCaps() {
    380         return mCapsCount > 1;
    381     }
    382 
    383     /**
    384      * Returns true if we have digits in the composing word.
    385      */
    386     public boolean hasDigits() {
    387         return mDigitsCount > 0;
    388     }
    389 
    390     /**
    391      * Saves the caps mode at the start of composing.
    392      *
    393      * WordComposer needs to know about this for several reasons. The first is, we need to know
    394      * after the fact what the reason was, to register the correct form into the user history
    395      * dictionary: if the word was automatically capitalized, we should insert it in all-lower
    396      * case but if it's a manual pressing of shift, then it should be inserted as is.
    397      * Also, batch input needs to know about the current caps mode to display correctly
    398      * capitalized suggestions.
    399      * @param mode the mode at the time of start
    400      */
    401     public void setCapitalizedModeAtStartComposingTime(final int mode) {
    402         mCapitalizedMode = mode;
    403     }
    404 
    405     /**
    406      * Returns whether the word was automatically capitalized.
    407      * @return whether the word was automatically capitalized
    408      */
    409     public boolean wasAutoCapitalized() {
    410         return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
    411                 || mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED;
    412     }
    413 
    414     /**
    415      * Sets the auto-correction for this word.
    416      */
    417     public void setAutoCorrection(final String correction) {
    418         mAutoCorrection = correction;
    419     }
    420 
    421     /**
    422      * @return the auto-correction for this word, or null if none.
    423      */
    424     public String getAutoCorrectionOrNull() {
    425         return mAutoCorrection;
    426     }
    427 
    428     /**
    429      * @return whether we started composing this word by resuming suggestion on an existing string
    430      */
    431     public boolean isResumed() {
    432         return mIsResumed;
    433     }
    434 
    435     // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above.
    436     public LastComposedWord commitWord(final int type, final String committedWord,
    437             final String separatorString, final String prevWord) {
    438         // Note: currently, we come here whenever we commit a word. If it's a MANUAL_PICK
    439         // or a DECIDED_WORD we may cancel the commit later; otherwise, we should deactivate
    440         // the last composed word to ensure this does not happen.
    441         final int[] primaryKeyCodes = mPrimaryKeyCodes;
    442         mPrimaryKeyCodes = new int[MAX_WORD_LENGTH];
    443         final LastComposedWord lastComposedWord = new LastComposedWord(primaryKeyCodes,
    444                 mInputPointers, mTypedWord.toString(), committedWord, separatorString,
    445                 prevWord, mCapitalizedMode);
    446         mInputPointers.reset();
    447         if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD
    448                 && type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) {
    449             lastComposedWord.deactivate();
    450         }
    451         mCapsCount = 0;
    452         mDigitsCount = 0;
    453         mIsBatchMode = false;
    454         mTypedWord.setLength(0);
    455         mCodePointSize = 0;
    456         mTrailingSingleQuotesCount = 0;
    457         mIsFirstCharCapitalized = false;
    458         mCapitalizedMode = CAPS_MODE_OFF;
    459         refreshSize();
    460         mAutoCorrection = null;
    461         mCursorPositionWithinWord = 0;
    462         mIsResumed = false;
    463         mRejectedBatchModeSuggestion = null;
    464         return lastComposedWord;
    465     }
    466 
    467     public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) {
    468         mPrimaryKeyCodes = lastComposedWord.mPrimaryKeyCodes;
    469         mInputPointers.set(lastComposedWord.mInputPointers);
    470         mTypedWord.setLength(0);
    471         mTypedWord.append(lastComposedWord.mTypedWord);
    472         refreshSize();
    473         mCapitalizedMode = lastComposedWord.mCapitalizedMode;
    474         mAutoCorrection = null; // This will be filled by the next call to updateSuggestion.
    475         mCursorPositionWithinWord = mCodePointSize;
    476         mRejectedBatchModeSuggestion = null;
    477         mIsResumed = true;
    478     }
    479 
    480     public boolean isBatchMode() {
    481         return mIsBatchMode;
    482     }
    483 
    484     public void setRejectedBatchModeSuggestion(final String rejectedSuggestion) {
    485         mRejectedBatchModeSuggestion = rejectedSuggestion;
    486     }
    487 
    488     public String getRejectedBatchModeSuggestion() {
    489         return mRejectedBatchModeSuggestion;
    490     }
    491 }
    492