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.KeyDetector;
     21 import com.android.inputmethod.keyboard.Keyboard;
     22 import com.android.inputmethod.keyboard.KeyboardActionListener;
     23 
     24 import java.util.Arrays;
     25 
     26 /**
     27  * A place to store the currently composing word with information such as adjacent key codes as well
     28  */
     29 public class WordComposer {
     30 
     31     public static final int NOT_A_CODE = KeyDetector.NOT_A_CODE;
     32     public static final int NOT_A_COORDINATE = -1;
     33 
     34     private static final int N = BinaryDictionary.MAX_WORD_LENGTH;
     35 
     36     private int[] mPrimaryKeyCodes;
     37     private int[] mXCoordinates;
     38     private int[] mYCoordinates;
     39     private StringBuilder mTypedWord;
     40     private CharSequence mAutoCorrection;
     41     private boolean mIsResumed;
     42 
     43     // Cache these values for performance
     44     private int mCapsCount;
     45     private boolean mAutoCapitalized;
     46     private int mTrailingSingleQuotesCount;
     47     private int mCodePointSize;
     48 
     49     /**
     50      * Whether the user chose to capitalize the first char of the word.
     51      */
     52     private boolean mIsFirstCharCapitalized;
     53 
     54     public WordComposer() {
     55         mPrimaryKeyCodes = new int[N];
     56         mTypedWord = new StringBuilder(N);
     57         mXCoordinates = new int[N];
     58         mYCoordinates = new int[N];
     59         mAutoCorrection = null;
     60         mTrailingSingleQuotesCount = 0;
     61         mIsResumed = false;
     62         refreshSize();
     63     }
     64 
     65     public WordComposer(WordComposer source) {
     66         init(source);
     67     }
     68 
     69     public void init(WordComposer source) {
     70         mPrimaryKeyCodes = Arrays.copyOf(source.mPrimaryKeyCodes, source.mPrimaryKeyCodes.length);
     71         mTypedWord = new StringBuilder(source.mTypedWord);
     72         mXCoordinates = Arrays.copyOf(source.mXCoordinates, source.mXCoordinates.length);
     73         mYCoordinates = Arrays.copyOf(source.mYCoordinates, source.mYCoordinates.length);
     74         mCapsCount = source.mCapsCount;
     75         mIsFirstCharCapitalized = source.mIsFirstCharCapitalized;
     76         mAutoCapitalized = source.mAutoCapitalized;
     77         mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount;
     78         mIsResumed = source.mIsResumed;
     79         refreshSize();
     80     }
     81 
     82     /**
     83      * Clear out the keys registered so far.
     84      */
     85     public void reset() {
     86         mTypedWord.setLength(0);
     87         mAutoCorrection = null;
     88         mCapsCount = 0;
     89         mIsFirstCharCapitalized = false;
     90         mTrailingSingleQuotesCount = 0;
     91         mIsResumed = false;
     92         refreshSize();
     93     }
     94 
     95     public final void refreshSize() {
     96         mCodePointSize = mTypedWord.codePointCount(0, mTypedWord.length());
     97     }
     98 
     99     /**
    100      * Number of keystrokes in the composing word.
    101      * @return the number of keystrokes
    102      */
    103     public final int size() {
    104         return mCodePointSize;
    105     }
    106 
    107     public final boolean isComposingWord() {
    108         return size() > 0;
    109     }
    110 
    111     // TODO: make sure that the index should not exceed MAX_WORD_LENGTH
    112     public int getCodeAt(int index) {
    113         if (index >= BinaryDictionary.MAX_WORD_LENGTH) {
    114             return -1;
    115         }
    116         return mPrimaryKeyCodes[index];
    117     }
    118 
    119     public int[] getXCoordinates() {
    120         return mXCoordinates;
    121     }
    122 
    123     public int[] getYCoordinates() {
    124         return mYCoordinates;
    125     }
    126 
    127     private static boolean isFirstCharCapitalized(int index, int codePoint, boolean previous) {
    128         if (index == 0) return Character.isUpperCase(codePoint);
    129         return previous && !Character.isUpperCase(codePoint);
    130     }
    131 
    132     // TODO: remove input keyDetector
    133     public void add(int primaryCode, int x, int y, KeyDetector keyDetector) {
    134         final int keyX;
    135         final int keyY;
    136         if (null == keyDetector
    137                 || x == KeyboardActionListener.SUGGESTION_STRIP_COORDINATE
    138                 || y == KeyboardActionListener.SUGGESTION_STRIP_COORDINATE
    139                 || x == KeyboardActionListener.NOT_A_TOUCH_COORDINATE
    140                 || y == KeyboardActionListener.NOT_A_TOUCH_COORDINATE) {
    141             keyX = x;
    142             keyY = y;
    143         } else {
    144             keyX = keyDetector.getTouchX(x);
    145             keyY = keyDetector.getTouchY(y);
    146         }
    147         add(primaryCode, keyX, keyY);
    148     }
    149 
    150     /**
    151      * Add a new keystroke, with the pressed key's code point with the touch point coordinates.
    152      */
    153     private void add(int primaryCode, int keyX, int keyY) {
    154         final int newIndex = size();
    155         mTypedWord.appendCodePoint(primaryCode);
    156         refreshSize();
    157         if (newIndex < BinaryDictionary.MAX_WORD_LENGTH) {
    158             mPrimaryKeyCodes[newIndex] = primaryCode >= Keyboard.CODE_SPACE
    159                     ? Character.toLowerCase(primaryCode) : primaryCode;
    160             mXCoordinates[newIndex] = keyX;
    161             mYCoordinates[newIndex] = keyY;
    162         }
    163         mIsFirstCharCapitalized = isFirstCharCapitalized(
    164                 newIndex, primaryCode, mIsFirstCharCapitalized);
    165         if (Character.isUpperCase(primaryCode)) mCapsCount++;
    166         if (Keyboard.CODE_SINGLE_QUOTE == primaryCode) {
    167             ++mTrailingSingleQuotesCount;
    168         } else {
    169             mTrailingSingleQuotesCount = 0;
    170         }
    171         mAutoCorrection = null;
    172     }
    173 
    174     /**
    175      * Internal method to retrieve reasonable proximity info for a character.
    176      */
    177     private void addKeyInfo(final int codePoint, final Keyboard keyboard) {
    178         for (final Key key : keyboard.mKeys) {
    179             if (key.mCode == codePoint) {
    180                 final int x = key.mX + key.mWidth / 2;
    181                 final int y = key.mY + key.mHeight / 2;
    182                 add(codePoint, x, y);
    183                 return;
    184             }
    185         }
    186         add(codePoint, WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
    187     }
    188 
    189     /**
    190      * Set the currently composing word to the one passed as an argument.
    191      * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity.
    192      */
    193     public void setComposingWord(final CharSequence word, final Keyboard keyboard) {
    194         reset();
    195         final int length = word.length();
    196         for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
    197             int codePoint = Character.codePointAt(word, i);
    198             addKeyInfo(codePoint, keyboard);
    199         }
    200         mIsResumed = true;
    201     }
    202 
    203     /**
    204      * Delete the last keystroke as a result of hitting backspace.
    205      */
    206     public void deleteLast() {
    207         final int size = size();
    208         if (size > 0) {
    209             // Note: mTypedWord.length() and mCodes.length differ when there are surrogate pairs
    210             final int stringBuilderLength = mTypedWord.length();
    211             if (stringBuilderLength < size) {
    212                 throw new RuntimeException(
    213                         "In WordComposer: mCodes and mTypedWords have non-matching lengths");
    214             }
    215             final int lastChar = mTypedWord.codePointBefore(stringBuilderLength);
    216             if (Character.isSupplementaryCodePoint(lastChar)) {
    217                 mTypedWord.delete(stringBuilderLength - 2, stringBuilderLength);
    218             } else {
    219                 mTypedWord.deleteCharAt(stringBuilderLength - 1);
    220             }
    221             if (Character.isUpperCase(lastChar)) mCapsCount--;
    222             refreshSize();
    223         }
    224         // We may have deleted the last one.
    225         if (0 == size()) {
    226             mIsFirstCharCapitalized = false;
    227         }
    228         if (mTrailingSingleQuotesCount > 0) {
    229             --mTrailingSingleQuotesCount;
    230         } else {
    231             int i = mTypedWord.length();
    232             while (i > 0) {
    233                 i = mTypedWord.offsetByCodePoints(i, -1);
    234                 if (Keyboard.CODE_SINGLE_QUOTE != mTypedWord.codePointAt(i)) break;
    235                 ++mTrailingSingleQuotesCount;
    236             }
    237         }
    238         mAutoCorrection = null;
    239     }
    240 
    241     /**
    242      * Returns the word as it was typed, without any correction applied.
    243      * @return the word that was typed so far. Never returns null.
    244      */
    245     public String getTypedWord() {
    246         return mTypedWord.toString();
    247     }
    248 
    249     /**
    250      * Whether or not the user typed a capital letter as the first letter in the word
    251      * @return capitalization preference
    252      */
    253     public boolean isFirstCharCapitalized() {
    254         return mIsFirstCharCapitalized;
    255     }
    256 
    257     public int trailingSingleQuotesCount() {
    258         return mTrailingSingleQuotesCount;
    259     }
    260 
    261     /**
    262      * Whether or not all of the user typed chars are upper case
    263      * @return true if all user typed chars are upper case, false otherwise
    264      */
    265     public boolean isAllUpperCase() {
    266         return (mCapsCount > 0) && (mCapsCount == size());
    267     }
    268 
    269     /**
    270      * Returns true if more than one character is upper case, otherwise returns false.
    271      */
    272     public boolean isMostlyCaps() {
    273         return mCapsCount > 1;
    274     }
    275 
    276     /**
    277      * Saves the reason why the word is capitalized - whether it was automatic or
    278      * due to the user hitting shift in the middle of a sentence.
    279      * @param auto whether it was an automatic capitalization due to start of sentence
    280      */
    281     public void setAutoCapitalized(boolean auto) {
    282         mAutoCapitalized = auto;
    283     }
    284 
    285     /**
    286      * Returns whether the word was automatically capitalized.
    287      * @return whether the word was automatically capitalized
    288      */
    289     public boolean isAutoCapitalized() {
    290         return mAutoCapitalized;
    291     }
    292 
    293     /**
    294      * Sets the auto-correction for this word.
    295      */
    296     public void setAutoCorrection(final CharSequence correction) {
    297         mAutoCorrection = correction;
    298     }
    299 
    300     /**
    301      * @return the auto-correction for this word, or null if none.
    302      */
    303     public CharSequence getAutoCorrectionOrNull() {
    304         return mAutoCorrection;
    305     }
    306 
    307     /**
    308      * @return whether we started composing this word by resuming suggestion on an existing string
    309      */
    310     public boolean isResumed() {
    311         return mIsResumed;
    312     }
    313 
    314     // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above.
    315     public LastComposedWord commitWord(final int type, final String committedWord,
    316             final int separatorCode, final CharSequence prevWord) {
    317         // Note: currently, we come here whenever we commit a word. If it's a MANUAL_PICK
    318         // or a DECIDED_WORD we may cancel the commit later; otherwise, we should deactivate
    319         // the last composed word to ensure this does not happen.
    320         final int[] primaryKeyCodes = mPrimaryKeyCodes;
    321         final int[] xCoordinates = mXCoordinates;
    322         final int[] yCoordinates = mYCoordinates;
    323         mPrimaryKeyCodes = new int[N];
    324         mXCoordinates = new int[N];
    325         mYCoordinates = new int[N];
    326         final LastComposedWord lastComposedWord = new LastComposedWord(primaryKeyCodes,
    327                 xCoordinates, yCoordinates, mTypedWord.toString(), committedWord, separatorCode,
    328                 prevWord);
    329         if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD
    330                 && type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) {
    331             lastComposedWord.deactivate();
    332         }
    333         mTypedWord.setLength(0);
    334         refreshSize();
    335         mAutoCorrection = null;
    336         mIsResumed = false;
    337         return lastComposedWord;
    338     }
    339 
    340     public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) {
    341         mPrimaryKeyCodes = lastComposedWord.mPrimaryKeyCodes;
    342         mXCoordinates = lastComposedWord.mXCoordinates;
    343         mYCoordinates = lastComposedWord.mYCoordinates;
    344         mTypedWord.setLength(0);
    345         mTypedWord.append(lastComposedWord.mTypedWord);
    346         refreshSize();
    347         mAutoCorrection = null; // This will be filled by the next call to updateSuggestion.
    348         mIsResumed = true;
    349     }
    350 }
    351