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