diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin')
4 files changed, 162 insertions, 26 deletions
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index f7a77cae7..60b436f69 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -403,7 +403,9 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar resetPendingImsCallback(); mIsOrientationChanging = true; final LatinIME latinIme = getOuterInstance(); - latinIme.mKeyboardSwitcher.saveKeyboardState(); + if (latinIme.isInputViewShown()) { + latinIme.mKeyboardSwitcher.saveKeyboardState(); + } } private void resetPendingImsCallback() { @@ -948,6 +950,10 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } mExpectingUpdateSelection = false; mHandler.postUpdateShiftKeyState(); + // TODO: Decide to call restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() or not + // here. It would probably be too expensive to call directly here but we may want to post a + // message to delay it. The point would be to unify behavior between backspace to the + // end of a word and manually put the pointer at the end of the word. // Make a note of the cursor position mLastSelectionStart = newSelStart; @@ -1466,10 +1472,11 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar // inconsistent with backspacing after selecting other suggestions. revertLastWord(ic); } else { - sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); + ic.deleteSurroundingText(1, 0); if (mDeleteCount > DELETE_ACCELERATE_AT) { - sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); + ic.deleteSurroundingText(1, 0); } + restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(ic); } } ic.endBatchEdit(); @@ -1793,6 +1800,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar // The whitelist should be case-insensitive, so it's not possible to be consistent with // a boolean flag. Right now this is handled with a slight hack in // WhitelistDictionary#shouldForciblyAutoCorrectFrom. + final int quotesCount = wordComposer.trailingSingleQuotesCount(); final boolean allowsToBeAutoCorrected = AutoCorrection.allowsToBeAutoCorrected( mSuggest.getUnigramDictionaries(), // If the typed string ends with a single quote, for dictionary lookup purposes @@ -1800,8 +1808,8 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar // typed string in the dictionary (to avoid autocorrecting from an existing // word, so for consistency this lookup should be made WITHOUT the trailing // single quote. - wordComposer.isLastCharASingleQuote() - ? typedWord.subSequence(0, typedWord.length() - 1) : typedWord, + quotesCount > 0 + ? typedWord.subSequence(0, typedWord.length() - quotesCount) : typedWord, preferCapitalization()); if (mCorrectionMode == Suggest.CORRECTION_FULL || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) { @@ -2118,6 +2126,53 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } // "ic" must not be null + /** + * Check if the cursor is actually at the end of a word. If so, restart suggestions on this + * word, else do nothing. + */ + private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord( + final InputConnection ic) { + // Bail out if the cursor is not at the end of a word (cursor must be preceded by + // non-whitespace, non-separator, non-start-of-text) + // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here. + final CharSequence textBeforeCursor = ic.getTextBeforeCursor(1, 0); + if (TextUtils.isEmpty(textBeforeCursor) + || mSettingsValues.isWordSeparator(textBeforeCursor.charAt(0))) return; + + // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace, + // separator or end of line/text) + // Example: "test|"<EOL> "te|st" get rejected here + final CharSequence textAfterCursor = ic.getTextAfterCursor(1, 0); + if (!TextUtils.isEmpty(textAfterCursor) + && !mSettingsValues.isWordSeparator(textAfterCursor.charAt(0))) return; + + // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe) + // Example: " '|" gets rejected here but "I'|" and "I|" are okay + final CharSequence word = EditingUtils.getWordAtCursor(ic, mSettingsValues.mWordSeparators); + if (TextUtils.isEmpty(word)) return; + if (word.length() == 1 && !Character.isLetter(word.charAt(0))) return; + + // Okay, we are at the end of a word. Restart suggestions. + restartSuggestionsOnWordBeforeCursor(ic, word); + } + + // "ic" must not be null + private void restartSuggestionsOnWordBeforeCursor(final InputConnection ic, + final CharSequence word) { + mWordComposer.setComposingWord(word, mKeyboardSwitcher.getLatinKeyboard()); + mComposingStringBuilder.setLength(0); + mComposingStringBuilder.append(word); + // mBestWord will be set appropriately by updateSuggestions() called by the handler + mBestWord = null; + mHasUncommittedTypedChars = true; + mComposingStateManager.onStartComposingText(); + TextEntryState.restartSuggestionsOnWordBeforeCursor(); + ic.deleteSurroundingText(word.length(), 0); + ic.setComposingText(word, 1); + mHandler.postUpdateSuggestions(); + } + + // "ic" must not be null private void revertLastWord(final InputConnection ic) { if (mHasUncommittedTypedChars || mComposingStringBuilder.length() <= 0) { sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); @@ -2143,6 +2198,10 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar // Clear composing text mComposingStringBuilder.setLength(0); } else { + // Note: this relies on the last word still being held in the WordComposer + // Note: in the interest of code simplicity, we may want to just call + // restartSuggestionsOnWordBeforeCursorIfAtEndOfWord instead, but retrieving + // the old WordComposer allows to reuse the actual typed coordinates. mHasUncommittedTypedChars = true; ic.setComposingText(mComposingStringBuilder, 1); TextEntryState.backspace(); diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index 5a3c348a9..2a36f8266 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -82,8 +82,6 @@ public class Suggest implements Dictionary.WordCallback { public static final String DICT_KEY_USER_BIGRAM = "user_bigram"; public static final String DICT_KEY_WHITELIST ="whitelist"; - private static String SINGLE_QUOTE_AS_STRING = String.valueOf((char)Keyboard.CODE_SINGLE_QUOTE); - private static final boolean DBG = LatinImeLogger.sDBG; private AutoCorrection mAutoCorrection; @@ -109,7 +107,7 @@ public class Suggest implements Dictionary.WordCallback { // TODO: Remove these member variables by passing more context to addWord() callback method private boolean mIsFirstCharCapitalized; private boolean mIsAllUpperCase; - private boolean mIsLastCharASingleQuote; + private int mTrailingSingleQuotesCount; private int mCorrectionMode = CORRECTION_BASIC; @@ -299,13 +297,14 @@ public class Suggest implements Dictionary.WordCallback { mAutoCorrection.init(); mIsFirstCharCapitalized = wordComposer.isFirstCharCapitalized(); mIsAllUpperCase = wordComposer.isAllUpperCase(); - mIsLastCharASingleQuote = wordComposer.isLastCharASingleQuote(); + mTrailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount(); collectGarbage(mSuggestions, mPrefMaxSuggestions); Arrays.fill(mScores, 0); final String typedWord = wordComposer.getTypedWord(); - final String consideredWord = mIsLastCharASingleQuote - ? typedWord.substring(0, typedWord.length() - 1) : typedWord; + final String consideredWord = mTrailingSingleQuotesCount > 0 + ? typedWord.substring(0, typedWord.length() - mTrailingSingleQuotesCount) + : typedWord; if (typedWord != null) { // Treating USER_TYPED as UNIGRAM suggestion for logging now. LatinImeLogger.onAddSuggestedWord(typedWord, Suggest.DIC_USER_TYPED, @@ -360,9 +359,11 @@ public class Suggest implements Dictionary.WordCallback { if (key.equals(DICT_KEY_USER_UNIGRAM) || key.equals(DICT_KEY_WHITELIST)) continue; final Dictionary dictionary = mUnigramDictionaries.get(key); - if (mIsLastCharASingleQuote) { + if (mTrailingSingleQuotesCount > 0) { final WordComposer tmpWordComposer = new WordComposer(wordComposer); - tmpWordComposer.deleteLast(); + for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) { + tmpWordComposer.deleteLast(); + } dictionary.getWords(tmpWordComposer, this, proximityInfo); } else { dictionary.getWords(wordComposer, this, proximityInfo); @@ -380,8 +381,15 @@ public class Suggest implements Dictionary.WordCallback { whitelistedWord); if (whitelistedWord != null) { - mSuggestions.add(0, mIsLastCharASingleQuote - ? whitelistedWord + SINGLE_QUOTE_AS_STRING : whitelistedWord); + if (mTrailingSingleQuotesCount > 0) { + final StringBuilder sb = new StringBuilder(whitelistedWord); + for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) { + sb.appendCodePoint(Keyboard.CODE_SINGLE_QUOTE); + } + mSuggestions.add(0, sb.toString()); + } else { + mSuggestions.add(0, whitelistedWord); + } } if (typedWord != null) { @@ -500,7 +508,7 @@ public class Suggest implements Dictionary.WordCallback { } else { sb.append(word, offset, length); } - if (mIsLastCharASingleQuote) { + for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) { sb.appendCodePoint(Keyboard.CODE_SINGLE_QUOTE); } suggestions.add(pos, sb); diff --git a/java/src/com/android/inputmethod/latin/TextEntryState.java b/java/src/com/android/inputmethod/latin/TextEntryState.java index 82242f87e..a6041b310 100644 --- a/java/src/com/android/inputmethod/latin/TextEntryState.java +++ b/java/src/com/android/inputmethod/latin/TextEntryState.java @@ -146,9 +146,24 @@ public class TextEntryState { } else if (sState == UNDO_COMMIT) { setState(IN_WORD); } + // TODO: tidy up this logic. At the moment, for example, writing a word goes to + // ACCEPTED_DEFAULT, backspace will go to UNDO_COMMIT, another backspace will go to IN_WORD, + // and subsequent backspaces will leave the status at IN_WORD, even if the user backspaces + // past the end of the word. We are not in a word any more but the state is still IN_WORD. if (DEBUG) displayState("backspace"); } + public static void restartSuggestionsOnWordBeforeCursor() { + if (UNKNOWN == sState || ACCEPTED_DEFAULT == sState) { + // Here we can come from pretty much any state, except the ones that we can't + // come from after backspace, so supposedly anything except UNKNOWN and + // ACCEPTED_DEFAULT. Note : we could be in UNDO_COMMIT if + // LatinIME#revertLastWord() was calling LatinIME#restartSuggestions...() + Log.e(TAG, "Strange state change : coming from state " + sState); + } + setState(IN_WORD); + } + public static void reset() { setState(START); if (DEBUG) displayState("reset"); diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index 612b16071..44c89f73c 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -17,7 +17,9 @@ package com.android.inputmethod.latin; import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.KeyDetector; +import com.android.inputmethod.keyboard.LatinKeyboard; import java.util.ArrayList; import java.util.Arrays; @@ -44,7 +46,7 @@ public class WordComposer { private boolean mAutoCapitalized; // Cache this value for performance - private boolean mIsLastCharASingleQuote; + private int mTrailingSingleQuotesCount; /** * Whether the user chose to capitalize the first char of the word. @@ -57,7 +59,7 @@ public class WordComposer { mTypedWord = new StringBuilder(N); mXCoordinates = new int[N]; mYCoordinates = new int[N]; - mIsLastCharASingleQuote = false; + mTrailingSingleQuotesCount = 0; } public WordComposer(WordComposer source) { @@ -72,7 +74,7 @@ public class WordComposer { mCapsCount = source.mCapsCount; mIsFirstCharCapitalized = source.mIsFirstCharCapitalized; mAutoCapitalized = source.mAutoCapitalized; - mIsLastCharASingleQuote = source.mIsLastCharASingleQuote; + mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount; } /** @@ -83,7 +85,7 @@ public class WordComposer { mTypedWord.setLength(0); mCapsCount = 0; mIsFirstCharCapitalized = false; - mIsLastCharASingleQuote = false; + mTrailingSingleQuotesCount = 0; } /** @@ -133,7 +135,55 @@ public class WordComposer { mIsFirstCharCapitalized = isFirstCharCapitalized( newIndex, primaryCode, mIsFirstCharCapitalized); if (Character.isUpperCase(primaryCode)) mCapsCount++; - mIsLastCharASingleQuote = Keyboard.CODE_SINGLE_QUOTE == primaryCode; + if (Keyboard.CODE_SINGLE_QUOTE == primaryCode) { + ++mTrailingSingleQuotesCount; + } else { + mTrailingSingleQuotesCount = 0; + } + } + + /** + * Internal method to retrieve reasonable proximity info for a character. + */ + private void addKeyInfo(final int codePoint, final LatinKeyboard keyboard, + final KeyDetector keyDetector) { + for (final Key key : keyboard.mKeys) { + if (key.mCode == codePoint) { + final int x = key.mX + key.mWidth / 2; + final int y = key.mY + key.mHeight / 2; + final int[] codes = keyDetector.newCodeArray(); + keyDetector.getKeyAndNearbyCodes(x, y, codes); + add(codePoint, codes, x, y); + return; + } + } + add(codePoint, new int[] { codePoint }, + WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); + } + + /** + * Set the currently composing word to the one passed as an argument. + * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity. + */ + public void setComposingWord(final CharSequence word, final LatinKeyboard keyboard, + final KeyDetector keyDetector) { + reset(); + final int length = word.length(); + for (int i = 0; i < length; ++i) { + int codePoint = word.charAt(i); + addKeyInfo(codePoint, keyboard, keyDetector); + } + } + + /** + * Shortcut for the above method, this will create a new KeyDetector for the passed keyboard. + */ + public void setComposingWord(final CharSequence word, final LatinKeyboard keyboard) { + final KeyDetector keyDetector = new KeyDetector(0); + keyDetector.setKeyboard(keyboard, 0, 0); + keyDetector.setProximityCorrectionEnabled(true); + keyDetector.setProximityThreshold(keyboard.mMostCommonKeyWidth); + setComposingWord(word, keyboard, keyDetector); } /** @@ -165,10 +215,14 @@ public class WordComposer { } if (size() == 0) { mIsFirstCharCapitalized = false; - mIsLastCharASingleQuote = false; + } + if (mTrailingSingleQuotesCount > 0) { + --mTrailingSingleQuotesCount; } else { - mIsLastCharASingleQuote = - Keyboard.CODE_SINGLE_QUOTE == mTypedWord.codePointAt(mTypedWord.length() - 1); + for (int i = mTypedWord.length() - 1; i >= 0; --i) { + if (Keyboard.CODE_SINGLE_QUOTE != mTypedWord.codePointAt(i)) break; + ++mTrailingSingleQuotesCount; + } } } @@ -191,8 +245,8 @@ public class WordComposer { return mIsFirstCharCapitalized; } - public boolean isLastCharASingleQuote() { - return mIsLastCharASingleQuote; + public int trailingSingleQuotesCount() { + return mTrailingSingleQuotesCount; } /** |