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.event.CombinerChain;
     20 import com.android.inputmethod.event.Event;
     21 import com.android.inputmethod.latin.define.DebugFlags;
     22 import com.android.inputmethod.latin.utils.CoordinateUtils;
     23 import com.android.inputmethod.latin.utils.StringUtils;
     24 
     25 import java.util.ArrayList;
     26 import java.util.Collections;
     27 
     28 import javax.annotation.Nonnull;
     29 
     30 /**
     31  * A place to store the currently composing word with information such as adjacent key codes as well
     32  */
     33 public final class WordComposer {
     34     private static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH;
     35     private static final boolean DBG = DebugFlags.DEBUG_ENABLED;
     36 
     37     public static final int CAPS_MODE_OFF = 0;
     38     // 1 is shift bit, 2 is caps bit, 4 is auto bit but this is just a convention as these bits
     39     // aren't used anywhere in the code
     40     public static final int CAPS_MODE_MANUAL_SHIFTED = 0x1;
     41     public static final int CAPS_MODE_MANUAL_SHIFT_LOCKED = 0x3;
     42     public static final int CAPS_MODE_AUTO_SHIFTED = 0x5;
     43     public static final int CAPS_MODE_AUTO_SHIFT_LOCKED = 0x7;
     44 
     45     private CombinerChain mCombinerChain;
     46     private String mCombiningSpec; // Memory so that we don't uselessly recreate the combiner chain
     47 
     48     // The list of events that served to compose this string.
     49     private final ArrayList<Event> mEvents;
     50     private final InputPointers mInputPointers = new InputPointers(MAX_WORD_LENGTH);
     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 CharSequence mTypedWordCache;
     64     private int mCapsCount;
     65     private int mDigitsCount;
     66     private int mCapitalizedMode;
     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 composing word has the only first char capitalized.
     76      */
     77     private boolean mIsOnlyFirstCharCapitalized;
     78 
     79     public WordComposer() {
     80         mCombinerChain = new CombinerChain("");
     81         mEvents = new ArrayList<>();
     82         mAutoCorrection = null;
     83         mIsResumed = false;
     84         mIsBatchMode = false;
     85         mCursorPositionWithinWord = 0;
     86         mRejectedBatchModeSuggestion = null;
     87         refreshTypedWordCache();
     88     }
     89 
     90     /**
     91      * Restart the combiners, possibly with a new spec.
     92      * @param combiningSpec The spec string for combining. This is found in the extra value.
     93      */
     94     public void restartCombining(final String combiningSpec) {
     95         final String nonNullCombiningSpec = null == combiningSpec ? "" : combiningSpec;
     96         if (!nonNullCombiningSpec.equals(mCombiningSpec)) {
     97             mCombinerChain = new CombinerChain(
     98                     mCombinerChain.getComposingWordWithCombiningFeedback().toString(),
     99                     CombinerChain.createCombiners(nonNullCombiningSpec));
    100             mCombiningSpec = nonNullCombiningSpec;
    101         }
    102     }
    103 
    104     /**
    105      * Clear out the keys registered so far.
    106      */
    107     public void reset() {
    108         mCombinerChain.reset();
    109         mEvents.clear();
    110         mAutoCorrection = null;
    111         mCapsCount = 0;
    112         mDigitsCount = 0;
    113         mIsOnlyFirstCharCapitalized = false;
    114         mIsResumed = false;
    115         mIsBatchMode = false;
    116         mCursorPositionWithinWord = 0;
    117         mRejectedBatchModeSuggestion = null;
    118         refreshTypedWordCache();
    119     }
    120 
    121     private final void refreshTypedWordCache() {
    122         mTypedWordCache = mCombinerChain.getComposingWordWithCombiningFeedback();
    123         mCodePointSize = Character.codePointCount(mTypedWordCache, 0, mTypedWordCache.length());
    124     }
    125 
    126     /**
    127      * Number of keystrokes in the composing word.
    128      * @return the number of keystrokes
    129      */
    130     // This may be made public if need be, but right now it's not used anywhere
    131     /* package for tests */ int size() {
    132         return mCodePointSize;
    133     }
    134 
    135     /**
    136      * Copy the code points in the typed word to a destination array of ints.
    137      *
    138      * If the array is too small to hold the code points in the typed word, nothing is copied and
    139      * -1 is returned.
    140      *
    141      * @param destination the array of ints.
    142      * @return the number of copied code points.
    143      */
    144     public int copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount(
    145             final int[] destination) {
    146         // This method can be called on a separate thread and mTypedWordCache can change while we
    147         // are executing this method.
    148         final String typedWord = mTypedWordCache.toString();
    149         // lastIndex is exclusive
    150         final int lastIndex = typedWord.length()
    151                 - StringUtils.getTrailingSingleQuotesCount(typedWord);
    152         if (lastIndex <= 0) {
    153             // The string is empty or contains only single quotes.
    154             return 0;
    155         }
    156 
    157         // The following function counts the number of code points in the text range which begins
    158         // at index 0 and extends to the character at lastIndex.
    159         final int codePointSize = Character.codePointCount(typedWord, 0, lastIndex);
    160         if (codePointSize > destination.length) {
    161             return -1;
    162         }
    163         return StringUtils.copyCodePointsAndReturnCodePointCount(destination, typedWord, 0,
    164                 lastIndex, true /* downCase */);
    165     }
    166 
    167     public boolean isSingleLetter() {
    168         return size() == 1;
    169     }
    170 
    171     public final boolean isComposingWord() {
    172         return size() > 0;
    173     }
    174 
    175     public InputPointers getInputPointers() {
    176         return mInputPointers;
    177     }
    178 
    179     /**
    180      * Process an event and return an event, and return a processed event to apply.
    181      * @param event the unprocessed event.
    182      * @return the processed event. Never null, but may be marked as consumed.
    183      */
    184     @Nonnull
    185     public Event processEvent(final Event event) {
    186         final Event processedEvent = mCombinerChain.processEvent(mEvents, event);
    187         // The retained state of the combiner chain may have changed while processing the event,
    188         // so we need to update our cache.
    189         refreshTypedWordCache();
    190         mEvents.add(event);
    191         return processedEvent;
    192     }
    193 
    194     /**
    195      * Apply a processed input event.
    196      *
    197      * All input events should be supported, including software/hardware events, characters as well
    198      * as deletions, multiple inputs and gestures.
    199      *
    200      * @param event the event to apply. Must not be null.
    201      */
    202     public void applyProcessedEvent(final Event event) {
    203         mCombinerChain.applyProcessedEvent(event);
    204         final int primaryCode = event.mCodePoint;
    205         final int keyX = event.mX;
    206         final int keyY = event.mY;
    207         final int newIndex = size();
    208         refreshTypedWordCache();
    209         mCursorPositionWithinWord = mCodePointSize;
    210         // We may have deleted the last one.
    211         if (0 == mCodePointSize) {
    212             mIsOnlyFirstCharCapitalized = false;
    213         }
    214         if (Constants.CODE_DELETE != event.mKeyCode) {
    215             if (newIndex < MAX_WORD_LENGTH) {
    216                 // In the batch input mode, the {@code mInputPointers} holds batch input points and
    217                 // shouldn't be overridden by the "typed key" coordinates
    218                 // (See {@link #setBatchInputWord}).
    219                 if (!mIsBatchMode) {
    220                     // TODO: Set correct pointer id and time
    221                     mInputPointers.addPointerAt(newIndex, keyX, keyY, 0, 0);
    222                 }
    223             }
    224             if (0 == newIndex) {
    225                 mIsOnlyFirstCharCapitalized = Character.isUpperCase(primaryCode);
    226             } else {
    227                 mIsOnlyFirstCharCapitalized = mIsOnlyFirstCharCapitalized
    228                         && !Character.isUpperCase(primaryCode);
    229             }
    230             if (Character.isUpperCase(primaryCode)) mCapsCount++;
    231             if (Character.isDigit(primaryCode)) mDigitsCount++;
    232         }
    233         mAutoCorrection = null;
    234     }
    235 
    236     public void setCursorPositionWithinWord(final int posWithinWord) {
    237         mCursorPositionWithinWord = posWithinWord;
    238         // TODO: compute where that puts us inside the events
    239     }
    240 
    241     public boolean isCursorFrontOrMiddleOfComposingWord() {
    242         if (DBG && mCursorPositionWithinWord > mCodePointSize) {
    243             throw new RuntimeException("Wrong cursor position : " + mCursorPositionWithinWord
    244                     + "in a word of size " + mCodePointSize);
    245         }
    246         return mCursorPositionWithinWord != mCodePointSize;
    247     }
    248 
    249     /**
    250      * When the cursor is moved by the user, we need to update its position.
    251      * If it falls inside the currently composing word, we don't reset the composition, and
    252      * only update the cursor position.
    253      *
    254      * @param expectedMoveAmount How many java chars to move the cursor. Negative values move
    255      * the cursor backward, positive values move the cursor forward.
    256      * @return true if the cursor is still inside the composing word, false otherwise.
    257      */
    258     public boolean moveCursorByAndReturnIfInsideComposingWord(final int expectedMoveAmount) {
    259         // TODO: should uncommit the composing feedback
    260         mCombinerChain.reset();
    261         int actualMoveAmountWithinWord = 0;
    262         int cursorPos = mCursorPositionWithinWord;
    263         // TODO: Don't make that copy. We can do this directly from mTypedWordCache.
    264         final int[] codePoints = StringUtils.toCodePointArray(mTypedWordCache);
    265         if (expectedMoveAmount >= 0) {
    266             // Moving the cursor forward for the expected amount or until the end of the word has
    267             // been reached, whichever comes first.
    268             while (actualMoveAmountWithinWord < expectedMoveAmount && cursorPos < mCodePointSize) {
    269                 actualMoveAmountWithinWord += Character.charCount(codePoints[cursorPos]);
    270                 ++cursorPos;
    271             }
    272         } else {
    273             // Moving the cursor backward for the expected amount or until the start of the word
    274             // has been reached, whichever comes first.
    275             while (actualMoveAmountWithinWord > expectedMoveAmount && cursorPos > 0) {
    276                 --cursorPos;
    277                 actualMoveAmountWithinWord -= Character.charCount(codePoints[cursorPos]);
    278             }
    279         }
    280         // If the actual and expected amounts differ, we crossed the start or the end of the word
    281         // so the result would not be inside the composing word.
    282         if (actualMoveAmountWithinWord != expectedMoveAmount) return false;
    283         mCursorPositionWithinWord = cursorPos;
    284         return true;
    285     }
    286 
    287     public void setBatchInputPointers(final InputPointers batchPointers) {
    288         mInputPointers.set(batchPointers);
    289         mIsBatchMode = true;
    290     }
    291 
    292     public void setBatchInputWord(final String word) {
    293         reset();
    294         mIsBatchMode = true;
    295         final int length = word.length();
    296         for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
    297             final int codePoint = Character.codePointAt(word, i);
    298             // We don't want to override the batch input points that are held in mInputPointers
    299             // (See {@link #add(int,int,int)}).
    300             final Event processedEvent =
    301                     processEvent(Event.createEventForCodePointFromUnknownSource(codePoint));
    302             applyProcessedEvent(processedEvent);
    303         }
    304     }
    305 
    306     /**
    307      * Set the currently composing word to the one passed as an argument.
    308      * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity.
    309      * @param codePoints the code points to set as the composing word.
    310      * @param coordinates the x, y coordinates of the key in the CoordinateUtils format
    311      */
    312     public void setComposingWord(final int[] codePoints, final int[] coordinates) {
    313         reset();
    314         final int length = codePoints.length;
    315         for (int i = 0; i < length; ++i) {
    316             final Event processedEvent =
    317                     processEvent(Event.createEventForCodePointFromAlreadyTypedText(codePoints[i],
    318                     CoordinateUtils.xFromArray(coordinates, i),
    319                     CoordinateUtils.yFromArray(coordinates, i)));
    320             applyProcessedEvent(processedEvent);
    321         }
    322         mIsResumed = true;
    323     }
    324 
    325     /**
    326      * Returns the word as it was typed, without any correction applied.
    327      * @return the word that was typed so far. Never returns null.
    328      */
    329     public String getTypedWord() {
    330         return mTypedWordCache.toString();
    331     }
    332 
    333     /**
    334      * Whether this composer is composing or about to compose a word in which only the first letter
    335      * is a capital.
    336      *
    337      * If we do have a composing word, we just return whether the word has indeed only its first
    338      * character capitalized. If we don't, then we return a value based on the capitalized mode,
    339      * which tell us what is likely to happen for the next composing word.
    340      *
    341      * @return capitalization preference
    342      */
    343     public boolean isOrWillBeOnlyFirstCharCapitalized() {
    344         return isComposingWord() ? mIsOnlyFirstCharCapitalized
    345                 : (CAPS_MODE_OFF != mCapitalizedMode);
    346     }
    347 
    348     /**
    349      * Whether or not all of the user typed chars are upper case
    350      * @return true if all user typed chars are upper case, false otherwise
    351      */
    352     public boolean isAllUpperCase() {
    353         if (size() <= 1) {
    354             return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
    355                     || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFT_LOCKED;
    356         } else {
    357             return mCapsCount == size();
    358         }
    359     }
    360 
    361     public boolean wasShiftedNoLock() {
    362         return mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED
    363                 || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFTED;
    364     }
    365 
    366     /**
    367      * Returns true if more than one character is upper case, otherwise returns false.
    368      */
    369     public boolean isMostlyCaps() {
    370         return mCapsCount > 1;
    371     }
    372 
    373     /**
    374      * Returns true if we have digits in the composing word.
    375      */
    376     public boolean hasDigits() {
    377         return mDigitsCount > 0;
    378     }
    379 
    380     /**
    381      * Saves the caps mode at the start of composing.
    382      *
    383      * WordComposer needs to know about the caps mode for several reasons. The first is, we need
    384      * to know after the fact what the reason was, to register the correct form into the user
    385      * history dictionary: if the word was automatically capitalized, we should insert it in
    386      * all-lower case but if it's a manual pressing of shift, then it should be inserted as is.
    387      * Also, batch input needs to know about the current caps mode to display correctly
    388      * capitalized suggestions.
    389      * @param mode the mode at the time of start
    390      */
    391     public void setCapitalizedModeAtStartComposingTime(final int mode) {
    392         mCapitalizedMode = mode;
    393     }
    394 
    395     /**
    396      * Before fetching suggestions, we don't necessarily know about the capitalized mode yet.
    397      *
    398      * If we don't have a composing word yet, we take a note of this mode so that we can then
    399      * supply this information to the suggestion process. If we have a composing word, then
    400      * the previous mode has priority over this.
    401      * @param mode the mode just before fetching suggestions
    402      */
    403     public void adviseCapitalizedModeBeforeFetchingSuggestions(final int mode) {
    404         if (!isComposingWord()) {
    405             mCapitalizedMode = mode;
    406         }
    407     }
    408 
    409     /**
    410      * Returns whether the word was automatically capitalized.
    411      * @return whether the word was automatically capitalized
    412      */
    413     public boolean wasAutoCapitalized() {
    414         return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
    415                 || mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED;
    416     }
    417 
    418     /**
    419      * Sets the auto-correction for this word.
    420      */
    421     public void setAutoCorrection(final String correction) {
    422         mAutoCorrection = correction;
    423     }
    424 
    425     /**
    426      * @return the auto-correction for this word, or null if none.
    427      */
    428     public String getAutoCorrectionOrNull() {
    429         return mAutoCorrection;
    430     }
    431 
    432     /**
    433      * @return whether we started composing this word by resuming suggestion on an existing string
    434      */
    435     public boolean isResumed() {
    436         return mIsResumed;
    437     }
    438 
    439     // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above.
    440     // committedWord should contain suggestion spans if applicable.
    441     public LastComposedWord commitWord(final int type, final CharSequence committedWord,
    442             final String separatorString, final PrevWordsInfo prevWordsInfo) {
    443         // Note: currently, we come here whenever we commit a word. If it's a MANUAL_PICK
    444         // or a DECIDED_WORD we may cancel the commit later; otherwise, we should deactivate
    445         // the last composed word to ensure this does not happen.
    446         final LastComposedWord lastComposedWord = new LastComposedWord(mEvents,
    447                 mInputPointers, mTypedWordCache.toString(), committedWord, separatorString,
    448                 prevWordsInfo, mCapitalizedMode);
    449         mInputPointers.reset();
    450         if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD
    451                 && type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) {
    452             lastComposedWord.deactivate();
    453         }
    454         mCapsCount = 0;
    455         mDigitsCount = 0;
    456         mIsBatchMode = false;
    457         mCombinerChain.reset();
    458         mEvents.clear();
    459         mCodePointSize = 0;
    460         mIsOnlyFirstCharCapitalized = false;
    461         mCapitalizedMode = CAPS_MODE_OFF;
    462         refreshTypedWordCache();
    463         mAutoCorrection = null;
    464         mCursorPositionWithinWord = 0;
    465         mIsResumed = false;
    466         mRejectedBatchModeSuggestion = null;
    467         return lastComposedWord;
    468     }
    469 
    470     public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) {
    471         mEvents.clear();
    472         Collections.copy(mEvents, lastComposedWord.mEvents);
    473         mInputPointers.set(lastComposedWord.mInputPointers);
    474         mCombinerChain.reset();
    475         refreshTypedWordCache();
    476         mCapitalizedMode = lastComposedWord.mCapitalizedMode;
    477         mAutoCorrection = null; // This will be filled by the next call to updateSuggestion.
    478         mCursorPositionWithinWord = mCodePointSize;
    479         mRejectedBatchModeSuggestion = null;
    480         mIsResumed = true;
    481     }
    482 
    483     public boolean isBatchMode() {
    484         return mIsBatchMode;
    485     }
    486 
    487     public void setRejectedBatchModeSuggestion(final String rejectedSuggestion) {
    488         mRejectedBatchModeSuggestion = rejectedSuggestion;
    489     }
    490 
    491     public String getRejectedBatchModeSuggestion() {
    492         return mRejectedBatchModeSuggestion;
    493     }
    494 }
    495