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