diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin/inputlogic')
-rw-r--r-- | java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java | 688 | ||||
-rw-r--r-- | java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java | 9 |
2 files changed, 487 insertions, 210 deletions
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index 74d879919..fdab7f25f 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -16,21 +16,29 @@ package com.android.inputmethod.latin.inputlogic; +import android.graphics.Color; +import android.inputmethodservice.InputMethodService; import android.os.SystemClock; import android.text.SpannableString; +import android.text.Spanned; import android.text.TextUtils; +import android.text.style.BackgroundColorSpan; import android.text.style.SuggestionSpan; import android.util.Log; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.inputmethod.CorrectionInfo; +import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; +import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper; import com.android.inputmethod.compat.SuggestionSpanUtils; import com.android.inputmethod.event.Event; import com.android.inputmethod.event.InputTransaction; import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.ProximityInfo; +import com.android.inputmethod.keyboard.TextDecorator; +import com.android.inputmethod.keyboard.TextDecoratorUiOperator; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.DictionaryFacilitator; @@ -45,6 +53,7 @@ import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.WordComposer; import com.android.inputmethod.latin.define.DebugFlags; +import com.android.inputmethod.latin.define.ProductionFlags; import com.android.inputmethod.latin.settings.SettingsValues; import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; @@ -80,6 +89,14 @@ public final class InputLogic { public final Suggest mSuggest; private final DictionaryFacilitator mDictionaryFacilitator; + private final TextDecorator mTextDecorator = new TextDecorator(new TextDecorator.Listener() { + @Override + public void onClickComposingTextToAddToDictionary(final String word) { + mLatinIME.addWordToUserDictionary(word); + mLatinIME.dismissAddToDictionaryHint(); + } + }); + public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; // This has package visibility so it can be accessed from InputLogicHandler. /* package */ final WordComposer mWordComposer; @@ -123,8 +140,9 @@ public final class InputLogic { * Call this when input starts or restarts in some editor (typically, in onStartInputView). * * @param combiningSpec the combining spec string for this subtype + * @param settingsValues the current settings values */ - public void startInput(final String combiningSpec) { + public void startInput(final String combiningSpec, final SettingsValues settingsValues) { mEnteredText = null; mWordComposer.restartCombining(combiningSpec); resetComposingState(true /* alsoResetLastComposedWord */); @@ -142,15 +160,25 @@ public final class InputLogic { } else { mInputLogicHandler.reset(); } + + if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK) { + // AcceptTypedWord feature relies on CursorAnchorInfo. + if (settingsValues.mShouldShowUiToAcceptTypedWord) { + mConnection.requestCursorUpdates(true /* enableMonitor */, + true /* requestImmediateCallback */); + } + mTextDecorator.reset(); + } } /** * Call this when the subtype changes. * @param combiningSpec the spec string for the combining rules + * @param settingsValues the current settings values */ - public void onSubtypeChanged(final String combiningSpec) { + public void onSubtypeChanged(final String combiningSpec, final SettingsValues settingsValues) { finishInput(); - startInput(combiningSpec); + startInput(combiningSpec, settingsValues); } /** @@ -206,7 +234,7 @@ public final class InputLogic { final int keyboardShiftMode, // TODO: remove this argument final LatinIME.UIHandler handler) { - final String rawText = event.mText.toString(); + final String rawText = event.getTextToCommit().toString(); final InputTransaction inputTransaction = new InputTransaction(settingsValues, event, SystemClock.uptimeMillis(), mSpaceState, getActualCapsMode(settingsValues, keyboardShiftMode)); @@ -216,7 +244,7 @@ public final class InputLogic { } else { resetComposingState(true /* alsoResetLastComposedWord */); } - handler.postUpdateSuggestionStrip(); + handler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_TYPING); final String text = performSpecificTldProcessingOnTextInput(rawText); if (SpaceState.PHANTOM == mSpaceState) { promotePhantomSpace(settingsValues); @@ -232,6 +260,20 @@ public final class InputLogic { } /** + * Determines whether "Touch again to save" should be shown or not. + * @param suggestionInfo the suggested word chosen by the user. + * @return {@code true} if we should show the "Touch again to save" hint. + */ + private boolean shouldShowAddToDictionaryHint(final SuggestedWordInfo suggestionInfo) { + // We should show the "Touch again to save" hint if the user pressed the first entry + // AND it's in none of our current dictionaries (main, user or otherwise). + return (suggestionInfo.isKindOf(SuggestedWordInfo.KIND_TYPED) + || suggestionInfo.isKindOf(SuggestedWordInfo.KIND_OOV_CORRECTION)) + && !mDictionaryFacilitator.isValidWord(suggestionInfo.mWord, true /* ignoreCase */) + && mDictionaryFacilitator.isUserDictionaryEnabled(); + } + + /** * A suggestion was picked from the suggestion strip. * @param settingsValues the current values of the settings. * @param suggestionInfo the suggestion info. @@ -288,11 +330,9 @@ public final class InputLogic { return inputTransaction; } - // We need to log before we commit, because the word composer will store away the user - // typed word. - final String replacedWord = mWordComposer.getTypedWord(); - commitChosenWord(settingsValues, suggestion, - LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR); + final boolean shouldShowAddToDictionaryHint = shouldShowAddToDictionaryHint(suggestionInfo); + commitChosenWord(settingsValues, suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, + LastComposedWord.NOT_A_SEPARATOR); mConnection.endBatchEdit(); // Don't allow cancellation of manual pick mLastComposedWord.deactivate(); @@ -300,18 +340,12 @@ public final class InputLogic { mSpaceState = SpaceState.PHANTOM; inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); - // We should show the "Touch again to save" hint if the user pressed the first entry - // AND it's in none of our current dictionaries (main, user or otherwise). - final boolean showingAddToDictionaryHint = - (suggestionInfo.isKindOf(SuggestedWordInfo.KIND_TYPED) - || suggestionInfo.isKindOf(SuggestedWordInfo.KIND_OOV_CORRECTION)) - && !mDictionaryFacilitator.isValidWord(suggestion, true /* ignoreCase */); - - if (showingAddToDictionaryHint && mDictionaryFacilitator.isUserDictionaryEnabled()) { + if (shouldShowAddToDictionaryHint) { mSuggestionStripViewAccessor.showAddToDictionaryHint(suggestion); } else { // If we're not showing the "Touch again to save", then update the suggestion strip. - handler.postUpdateSuggestionStrip(); + // That's going to be predictions (or punctuation suggestions), so INPUT_STYLE_NONE. + handler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_NONE); } return inputTransaction; } @@ -324,10 +358,11 @@ public final class InputLogic { * @param oldSelEnd old selection end * @param newSelStart new selection start * @param newSelEnd new selection end + * @param settingsValues the current values of the settings. * @return whether the cursor has moved as a result of user interaction. */ public boolean onUpdateSelection(final int oldSelStart, final int oldSelEnd, - final int newSelStart, final int newSelEnd) { + final int newSelStart, final int newSelEnd, final SettingsValues settingsValues) { if (mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart, oldSelEnd, newSelEnd)) { return false; } @@ -352,8 +387,9 @@ public final class InputLogic { // should be true, but that is if the framework had taken that wrong cursor position // into account, which means we have to reset the entire composing state whenever there // is or was a selection regardless of whether it changed or not. - if (hasOrHadSelection || (selectionChangedOrSafeToReset - && !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) { + if (hasOrHadSelection || !settingsValues.needsToLookupSuggestions() + || (selectionChangedOrSafeToReset + && !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) { // If we are composing a word and moving the cursor, we would want to set a // suggestion span for recorrection to work correctly. Unfortunately, that // would involve the keyboard committing some new text, which would move the @@ -380,6 +416,11 @@ public final class InputLogic { // The cursor has been moved : we now accept to perform recapitalization mRecapitalizeStatus.enable(); + // We moved the cursor and need to invalidate the indicator right now. + mTextDecorator.reset(); + // Remaining background color that was used for the add-to-dictionary indicator should be + // removed. + mConnection.removeBackgroundColorFromHighlightedTextIfNecessary(); // We moved the cursor. If we are touching a word, we need to resume suggestion. mLatinIME.mHandler.postResumeSuggestions(false /* shouldIncludeResumedWordInSuggestions */, true /* shouldDelay */); @@ -405,128 +446,44 @@ public final class InputLogic { final int keyboardShiftMode, // TODO: remove these arguments final int currentKeyboardScriptId, final LatinIME.UIHandler handler) { - final InputTransaction inputTransaction = new InputTransaction(settingsValues, event, - SystemClock.uptimeMillis(), mSpaceState, + final Event processedEvent = mWordComposer.processEvent(event); + final InputTransaction inputTransaction = new InputTransaction(settingsValues, + processedEvent, SystemClock.uptimeMillis(), mSpaceState, getActualCapsMode(settingsValues, keyboardShiftMode)); - if (event.mKeyCode != Constants.CODE_DELETE + if (processedEvent.mKeyCode != Constants.CODE_DELETE || inputTransaction.mTimestamp > mLastKeyTime + Constants.LONG_PRESS_MILLISECONDS) { mDeleteCount = 0; } mLastKeyTime = inputTransaction.mTimestamp; mConnection.beginBatchEdit(); if (!mWordComposer.isComposingWord()) { + // TODO: is this useful? It doesn't look like it should be done here, but rather after + // a word is committed. mIsAutoCorrectionIndicatorOn = false; } // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state. - if (event.mCodePoint != Constants.CODE_SPACE) { + if (processedEvent.mCodePoint != Constants.CODE_SPACE) { cancelDoubleSpacePeriodCountdown(); } - boolean didAutoCorrect = false; - if (event.isFunctionalKeyEvent()) { - // A special key, like delete, shift, emoji, or the settings key. - switch (event.mKeyCode) { - case Constants.CODE_DELETE: - handleBackspace(inputTransaction, currentKeyboardScriptId); - // Backspace is a functional key, but it affects the contents of the editor. - inputTransaction.setDidAffectContents(); - break; - case Constants.CODE_SHIFT: - performRecapitalization(inputTransaction.mSettingsValues); - inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); - if (mSuggestedWords.mIsPrediction) { - inputTransaction.setRequiresUpdateSuggestions(); - } - break; - case Constants.CODE_CAPSLOCK: - // Note: Changing keyboard to shift lock state is handled in - // {@link KeyboardSwitcher#onCodeInput(int)}. - break; - case Constants.CODE_SYMBOL_SHIFT: - // Note: Calling back to the keyboard on the symbol Shift key is handled in - // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. - break; - case Constants.CODE_SWITCH_ALPHA_SYMBOL: - // Note: Calling back to the keyboard on symbol key is handled in - // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. - break; - case Constants.CODE_SETTINGS: - onSettingsKeyPressed(); - break; - case Constants.CODE_SHORTCUT: - // We need to switch to the shortcut IME. This is handled by LatinIME since the - // input logic has no business with IME switching. - break; - case Constants.CODE_ACTION_NEXT: - performEditorAction(EditorInfo.IME_ACTION_NEXT); - break; - case Constants.CODE_ACTION_PREVIOUS: - performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); - break; - case Constants.CODE_LANGUAGE_SWITCH: - handleLanguageSwitchKey(); - break; - case Constants.CODE_EMOJI: - // Note: Switching emoji keyboard is being handled in - // {@link KeyboardState#onCodeInput(int,int)}. - break; - case Constants.CODE_ALPHA_FROM_EMOJI: - // Note: Switching back from Emoji keyboard to the main keyboard is being - // handled in {@link KeyboardState#onCodeInput(int,int)}. - break; - case Constants.CODE_SHIFT_ENTER: - // TODO: remove this object - final Event tmpEvent = Event.createSoftwareKeypressEvent(Constants.CODE_ENTER, - event.mKeyCode, event.mX, event.mY, event.isKeyRepeat()); - final InputTransaction tmpTransaction = new InputTransaction( - inputTransaction.mSettingsValues, tmpEvent, - inputTransaction.mTimestamp, inputTransaction.mSpaceState, - inputTransaction.mShiftState); - didAutoCorrect = handleNonSpecialCharacter(tmpTransaction, handler); - // Shift + Enter is treated as a functional key but it results in adding a new - // line, so that does affect the contents of the editor. - inputTransaction.setDidAffectContents(); - break; - default: - throw new RuntimeException("Unknown key code : " + event.mKeyCode); - } - } else { - inputTransaction.setDidAffectContents(); - switch (event.mCodePoint) { - case Constants.CODE_ENTER: - final EditorInfo editorInfo = getCurrentInputEditorInfo(); - final int imeOptionsActionId = - InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo); - if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) { - // Either we have an actionLabel and we should performEditorAction with - // actionId regardless of its value. - performEditorAction(editorInfo.actionId); - } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) { - // We didn't have an actionLabel, but we had another action to execute. - // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast, - // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it - // means there should be an action and the app didn't bother to set a specific - // code for it - presumably it only handles one. It does not have to be treated - // in any specific way: anything that is not IME_ACTION_NONE should be sent to - // performEditorAction. - performEditorAction(imeOptionsActionId); - } else { - // No action label, and the action from imeOptions is NONE: this is a regular - // enter key that should input a carriage return. - didAutoCorrect = handleNonSpecialCharacter(inputTransaction, handler); - } - break; - default: - didAutoCorrect = handleNonSpecialCharacter(inputTransaction, handler); - break; + Event currentEvent = processedEvent; + while (null != currentEvent) { + if (currentEvent.isConsumed()) { + handleConsumedEvent(currentEvent, inputTransaction); + } else if (currentEvent.isFunctionalKeyEvent()) { + handleFunctionalEvent(currentEvent, inputTransaction, currentKeyboardScriptId, + handler); + } else { + handleNonFunctionalEvent(currentEvent, inputTransaction, handler); } + currentEvent = currentEvent.mNextEvent; } - if (!didAutoCorrect && event.mKeyCode != Constants.CODE_SHIFT - && event.mKeyCode != Constants.CODE_CAPSLOCK - && event.mKeyCode != Constants.CODE_SWITCH_ALPHA_SYMBOL) + if (!inputTransaction.didAutoCorrect() && processedEvent.mKeyCode != Constants.CODE_SHIFT + && processedEvent.mKeyCode != Constants.CODE_CAPSLOCK + && processedEvent.mKeyCode != Constants.CODE_SWITCH_ALPHA_SYMBOL) mLastComposedWord.deactivate(); - if (Constants.CODE_DELETE != event.mKeyCode) { + if (Constants.CODE_DELETE != processedEvent.mKeyCode) { mEnteredText = null; } mConnection.endBatchEdit(); @@ -542,7 +499,9 @@ public final class InputLogic { handler.cancelUpdateSuggestionStrip(); ++mAutoCommitSequenceNumber; mConnection.beginBatchEdit(); - if (mWordComposer.isComposingWord()) { + if (!mWordComposer.isComposingWord()) { + mConnection.removeBackgroundColorFromHighlightedTextIfNecessary(); + } else { if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { // If we are in the middle of a recorrection, we need to commit the recorrection // first so that we can insert the batch input at the current cursor position. @@ -639,7 +598,8 @@ public final class InputLogic { // TODO: on the long term, this method should become private, but it will be difficult. // Especially, how do we deal with InputMethodService.onDisplayCompletions? - public void setSuggestedWords(final SuggestedWords suggestedWords) { + public void setSuggestedWords(final SuggestedWords suggestedWords, + final SettingsValues settingsValues, final LatinIME.UIHandler handler) { if (SuggestedWords.EMPTY != suggestedWords) { final String autoCorrection; if (suggestedWords.mWillAutoCorrect) { @@ -653,6 +613,7 @@ public final class InputLogic { } mSuggestedWords = suggestedWords; final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect; + // Put a blue underline to a word in TextView which will be auto-corrected. if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator && mWordComposer.isComposingWord()) { @@ -663,7 +624,154 @@ public final class InputLogic { // message, this is called outside any batch edit. Potentially, this may result in some // janky flickering of the screen, although the display speed makes it unlikely in // the practice. - mConnection.setComposingText(textWithUnderline, 1); + setComposingTextInternal(textWithUnderline, 1); + } + } + + /** + * Handle a consumed event. + * + * Consumed events represent events that have already been consumed, typically by the + * combining chain. + * + * @param event The event to handle. + * @param inputTransaction The transaction in progress. + */ + private void handleConsumedEvent(final Event event, final InputTransaction inputTransaction) { + // A consumed event may have text to commit and an update to the composing state, so + // we evaluate both. With some combiners, it's possible than an event contains both + // and we enter both of the following if clauses. + final CharSequence textToCommit = event.getTextToCommit(); + if (!TextUtils.isEmpty(textToCommit)) { + mConnection.commitText(textToCommit, 1); + inputTransaction.setDidAffectContents(); + } + if (mWordComposer.isComposingWord()) { + setComposingTextInternal(mWordComposer.getTypedWord(), 1); + inputTransaction.setDidAffectContents(); + inputTransaction.setRequiresUpdateSuggestions(); + } + } + + /** + * Handle a functional key event. + * + * A functional event is a special key, like delete, shift, emoji, or the settings key. + * Non-special keys are those that generate a single code point. + * This includes all letters, digits, punctuation, separators, emoji. It excludes keys that + * manage keyboard-related stuff like shift, language switch, settings, layout switch, or + * any key that results in multiple code points like the ".com" key. + * + * @param event The event to handle. + * @param inputTransaction The transaction in progress. + */ + private void handleFunctionalEvent(final Event event, final InputTransaction inputTransaction, + // TODO: remove these arguments + final int currentKeyboardScriptId, final LatinIME.UIHandler handler) { + switch (event.mKeyCode) { + case Constants.CODE_DELETE: + handleBackspaceEvent(event, inputTransaction, currentKeyboardScriptId); + // Backspace is a functional key, but it affects the contents of the editor. + inputTransaction.setDidAffectContents(); + break; + case Constants.CODE_SHIFT: + performRecapitalization(inputTransaction.mSettingsValues); + inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); + if (mSuggestedWords.isPrediction()) { + inputTransaction.setRequiresUpdateSuggestions(); + } + break; + case Constants.CODE_CAPSLOCK: + // Note: Changing keyboard to shift lock state is handled in + // {@link KeyboardSwitcher#onCodeInput(int)}. + break; + case Constants.CODE_SYMBOL_SHIFT: + // Note: Calling back to the keyboard on the symbol Shift key is handled in + // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. + break; + case Constants.CODE_SWITCH_ALPHA_SYMBOL: + // Note: Calling back to the keyboard on symbol key is handled in + // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. + break; + case Constants.CODE_SETTINGS: + onSettingsKeyPressed(); + break; + case Constants.CODE_SHORTCUT: + // We need to switch to the shortcut IME. This is handled by LatinIME since the + // input logic has no business with IME switching. + break; + case Constants.CODE_ACTION_NEXT: + performEditorAction(EditorInfo.IME_ACTION_NEXT); + break; + case Constants.CODE_ACTION_PREVIOUS: + performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); + break; + case Constants.CODE_LANGUAGE_SWITCH: + handleLanguageSwitchKey(); + break; + case Constants.CODE_EMOJI: + // Note: Switching emoji keyboard is being handled in + // {@link KeyboardState#onCodeInput(int,int)}. + break; + case Constants.CODE_ALPHA_FROM_EMOJI: + // Note: Switching back from Emoji keyboard to the main keyboard is being + // handled in {@link KeyboardState#onCodeInput(int,int)}. + break; + case Constants.CODE_SHIFT_ENTER: + // TODO: remove this object + final Event tmpEvent = Event.createSoftwareKeypressEvent(Constants.CODE_ENTER, + event.mKeyCode, event.mX, event.mY, event.isKeyRepeat()); + handleNonSpecialCharacterEvent(tmpEvent, inputTransaction, handler); + // Shift + Enter is treated as a functional key but it results in adding a new + // line, so that does affect the contents of the editor. + inputTransaction.setDidAffectContents(); + break; + default: + throw new RuntimeException("Unknown key code : " + event.mKeyCode); + } + } + + /** + * Handle an event that is not a functional event. + * + * These events are generally events that cause input, but in some cases they may do other + * things like trigger an editor action. + * + * @param event The event to handle. + * @param inputTransaction The transaction in progress. + */ + private void handleNonFunctionalEvent(final Event event, + final InputTransaction inputTransaction, + // TODO: remove this argument + final LatinIME.UIHandler handler) { + inputTransaction.setDidAffectContents(); + switch (event.mCodePoint) { + case Constants.CODE_ENTER: + final EditorInfo editorInfo = getCurrentInputEditorInfo(); + final int imeOptionsActionId = + InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo); + if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) { + // Either we have an actionLabel and we should performEditorAction with + // actionId regardless of its value. + performEditorAction(editorInfo.actionId); + } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) { + // We didn't have an actionLabel, but we had another action to execute. + // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast, + // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it + // means there should be an action and the app didn't bother to set a specific + // code for it - presumably it only handles one. It does not have to be treated + // in any specific way: anything that is not IME_ACTION_NONE should be sent to + // performEditorAction. + performEditorAction(imeOptionsActionId); + } else { + // No action label, and the action from imeOptions is NONE: this is a regular + // enter key that should input a carriage return. + handleNonSpecialCharacterEvent(event, inputTransaction, handler); + } + break; + default: + handleNonSpecialCharacterEvent(event, inputTransaction, handler); + break; } } @@ -675,21 +783,29 @@ public final class InputLogic { * manage keyboard-related stuff like shift, language switch, settings, layout switch, or * any key that results in multiple code points like the ".com" key. * + * @param event The event to handle. * @param inputTransaction The transaction in progress. - * @return whether this caused an auto-correction to happen. */ - private boolean handleNonSpecialCharacter(final InputTransaction inputTransaction, + private void handleNonSpecialCharacterEvent(final Event event, + final InputTransaction inputTransaction, // TODO: remove this argument final LatinIME.UIHandler handler) { - final int codePoint = inputTransaction.mEvent.mCodePoint; + if (!mWordComposer.isComposingWord()) { + mConnection.removeBackgroundColorFromHighlightedTextIfNecessary(); + // In case the "add to dictionary" hint was still displayed. + // TODO: Do we really need to check if we have composing text here? + if (mSuggestionStripViewAccessor.isShowingAddToDictionaryHint()) { + mSuggestionStripViewAccessor.dismissAddToDictionaryHint(); + mTextDecorator.reset(); + } + } + + final int codePoint = event.mCodePoint; mSpaceState = SpaceState.NONE; - final boolean didAutoCorrect; if (inputTransaction.mSettingsValues.isWordSeparator(codePoint) || Character.getType(codePoint) == Character.OTHER_SYMBOL) { - didAutoCorrect = handleSeparator(inputTransaction, - inputTransaction.mEvent.isSuggestionStripPress(), handler); + handleSeparatorEvent(event, inputTransaction, handler); } else { - didAutoCorrect = false; if (SpaceState.PHANTOM == inputTransaction.mSpaceState) { if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { // If we are in the middle of a recorrection, we need to commit the recorrection @@ -700,22 +816,23 @@ public final class InputLogic { commitTyped(inputTransaction.mSettingsValues, LastComposedWord.NOT_A_SEPARATOR); } } - handleNonSeparator(inputTransaction.mSettingsValues, inputTransaction); + handleNonSeparatorEvent(event, inputTransaction.mSettingsValues, inputTransaction); } - return didAutoCorrect; } /** * Handle a non-separator. + * @param event The event to handle. * @param settingsValues The current settings values. * @param inputTransaction The transaction in progress. */ - private void handleNonSeparator(final SettingsValues settingsValues, + private void handleNonSeparatorEvent(final Event event, final SettingsValues settingsValues, final InputTransaction inputTransaction) { - final int codePoint = inputTransaction.mEvent.mCodePoint; + final int codePoint = event.mCodePoint; // TODO: refactor this method to stop flipping isComposingWord around all the time, and - // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter - // which has the same name as other handle* methods but is not the same. + // make it shorter (possibly cut into several pieces). Also factor + // handleNonSpecialCharacterEvent which has the same name as other handle* methods but is + // not the same. boolean isComposingWord = mWordComposer.isComposingWord(); // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead. @@ -762,41 +879,35 @@ public final class InputLogic { resetComposingState(false /* alsoResetLastComposedWord */); } if (isComposingWord) { - mWordComposer.processEvent(inputTransaction.mEvent); + mWordComposer.applyProcessedEvent(event); // If it's the first letter, make note of auto-caps state if (mWordComposer.isSingleLetter()) { mWordComposer.setCapitalizedModeAtStartComposingTime(inputTransaction.mShiftState); } - mConnection.setComposingText(getTextWithUnderline( - mWordComposer.getTypedWord()), 1); + setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1); } else { - final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead( - inputTransaction, inputTransaction.mEvent.isSuggestionStripPress()); + final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(event, + inputTransaction); - if (swapWeakSpace && trySwapSwapperAndSpace(inputTransaction)) { + if (swapWeakSpace && trySwapSwapperAndSpace(event, inputTransaction)) { mSpaceState = SpaceState.WEAK; } else { sendKeyCodePoint(settingsValues, codePoint); } - // In case the "add to dictionary" hint was still displayed. - mSuggestionStripViewAccessor.dismissAddToDictionaryHint(); } inputTransaction.setRequiresUpdateSuggestions(); } /** * Handle input of a separator code point. + * @param event The event to handle. * @param inputTransaction The transaction in progress. - * @param isFromSuggestionStrip whether this code point comes from the suggestion strip. - * @return whether this caused an auto-correction to happen. */ - private boolean handleSeparator(final InputTransaction inputTransaction, - final boolean isFromSuggestionStrip, + private void handleSeparatorEvent(final Event event, final InputTransaction inputTransaction, // TODO: remove this argument final LatinIME.UIHandler handler) { - final int codePoint = inputTransaction.mEvent.mCodePoint; + final int codePoint = event.mCodePoint; final SettingsValues settingsValues = inputTransaction.mSettingsValues; - boolean didAutoCorrect = false; final boolean wasComposingWord = mWordComposer.isComposingWord(); // We avoid sending spaces in languages without spaces if we were composing. final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == codePoint @@ -814,15 +925,15 @@ public final class InputLogic { final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR : StringUtils.newSingleCodePointString(codePoint); commitCurrentAutoCorrection(settingsValues, separator, handler); - didAutoCorrect = true; + inputTransaction.setDidAutoCorrect(); } else { commitTyped(settingsValues, StringUtils.newSingleCodePointString(codePoint)); } } - final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead( - inputTransaction, isFromSuggestionStrip); + final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(event, + inputTransaction); final boolean isInsideDoubleQuoteOrAfterDigit = Constants.CODE_DOUBLE_QUOTE == codePoint && mConnection.isInsideDoubleQuoteOrAfterDigit(); @@ -846,10 +957,10 @@ public final class InputLogic { promotePhantomSpace(settingsValues); } - if (tryPerformDoubleSpacePeriod(inputTransaction)) { + if (tryPerformDoubleSpacePeriod(event, inputTransaction)) { mSpaceState = SpaceState.DOUBLE; inputTransaction.setRequiresUpdateSuggestions(); - } else if (swapWeakSpace && trySwapSwapperAndSpace(inputTransaction)) { + } else if (swapWeakSpace && trySwapSwapperAndSpace(event, inputTransaction)) { mSpaceState = SpaceState.SWAP_PUNCTUATION; mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); } else if (Constants.CODE_SPACE == codePoint) { @@ -892,14 +1003,14 @@ public final class InputLogic { } inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); - return didAutoCorrect; } /** * Handle a press on the backspace key. + * @param event The event to handle. * @param inputTransaction The transaction in progress. */ - private void handleBackspace(final InputTransaction inputTransaction, + private void handleBackspaceEvent(final Event event, final InputTransaction inputTransaction, // TODO: remove this argument, put it into settingsValues final int currentKeyboardScriptId) { mSpaceState = SpaceState.NONE; @@ -913,7 +1024,7 @@ public final class InputLogic { // Then again, even in the case of a key repeat, if the cursor is at start of text, it // can't go any further back, so we can update right away even if it's a key repeat. final int shiftUpdateKind = - inputTransaction.mEvent.isKeyRepeat() && mConnection.getExpectedSelectionStart() > 0 + event.isKeyRepeat() && mConnection.getExpectedSelectionStart() > 0 ? InputTransaction.SHIFT_UPDATE_LATER : InputTransaction.SHIFT_UPDATE_NOW; inputTransaction.requireShiftUpdate(shiftUpdateKind); @@ -933,17 +1044,17 @@ public final class InputLogic { mDictionaryFacilitator.removeWordFromPersonalizedDicts(rejectedSuggestion); } } else { - mWordComposer.processEvent(inputTransaction.mEvent); + mWordComposer.applyProcessedEvent(event); } if (mWordComposer.isComposingWord()) { - mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); + setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1); } else { mConnection.commitText("", 1); } inputTransaction.setRequiresUpdateSuggestions(); } else { if (mLastComposedWord.canRevertCommit()) { - revertCommit(inputTransaction); + revertCommit(inputTransaction, inputTransaction.mSettingsValues); return; } if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { @@ -1052,16 +1163,18 @@ public final class InputLogic { * * This method will check that there are two characters before the cursor and that the first * one is a space before it does the actual swapping. + * @param event The event to handle. * @param inputTransaction The transaction in progress. * @return true if the swap has been performed, false if it was prevented by preliminary checks. */ - private boolean trySwapSwapperAndSpace(final InputTransaction inputTransaction) { + private boolean trySwapSwapperAndSpace(final Event event, + final InputTransaction inputTransaction) { final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); if (Constants.CODE_SPACE != codePointBeforeCursor) { return false; } mConnection.deleteSurroundingText(1, 0); - final String text = inputTransaction.mEvent.getTextToCommit() + " "; + final String text = event.getTextToCommit() + " "; mConnection.commitText(text, 1); inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); return true; @@ -1069,13 +1182,14 @@ public final class InputLogic { /* * Strip a trailing space if necessary and returns whether it's a swap weak space situation. + * @param event The event to handle. * @param inputTransaction The transaction in progress. - * @param isFromSuggestionStrip Whether this code point is coming from the suggestion strip. * @return whether we should swap the space instead of removing it. */ - private boolean tryStripSpaceAndReturnWhetherShouldSwapInstead( - final InputTransaction inputTransaction, final boolean isFromSuggestionStrip) { - final int codePoint = inputTransaction.mEvent.mCodePoint; + private boolean tryStripSpaceAndReturnWhetherShouldSwapInstead(final Event event, + final InputTransaction inputTransaction) { + final int codePoint = event.mCodePoint; + final boolean isFromSuggestionStrip = event.isSuggestionStripPress(); if (Constants.CODE_ENTER == codePoint && SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) { mConnection.removeTrailingSpace(); @@ -1120,14 +1234,16 @@ public final class InputLogic { * these conditions are fulfilled, this method applies the transformation and returns true. * Otherwise, it does nothing and returns false. * + * @param event The event to handle. * @param inputTransaction The transaction in progress. * @return true if we applied the double-space-to-period transformation, false otherwise. */ - private boolean tryPerformDoubleSpacePeriod(final InputTransaction inputTransaction) { + private boolean tryPerformDoubleSpacePeriod(final Event event, + final InputTransaction inputTransaction) { // Check the setting, the typed character and the countdown. If any of the conditions is // not fulfilled, return false. if (!inputTransaction.mSettingsValues.mUseDoubleSpacePeriod - || Constants.CODE_SPACE != inputTransaction.mEvent.mCodePoint + || Constants.CODE_SPACE != event.mCodePoint || !isDoubleSpacePeriodCountdownActive(inputTransaction)) { return false; } @@ -1235,7 +1351,8 @@ public final class InputLogic { prevWordsInfo, timeStampInSeconds, settingsValues.mBlockPotentiallyOffensive); } - public void performUpdateSuggestionStripSync(final SettingsValues settingsValues) { + public void performUpdateSuggestionStripSync(final SettingsValues settingsValues, + final int inputStyle) { // Check if we have a suggestion engine attached. if (!settingsValues.needsToLookupSuggestions()) { if (mWordComposer.isComposingWord()) { @@ -1253,8 +1370,8 @@ public final class InputLogic { } final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<>(); - mInputLogicHandler.getSuggestedWords(Suggest.SESSION_TYPING, - SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() { + mInputLogicHandler.getSuggestedWords(inputStyle, SuggestedWords.NOT_A_SEQUENCE_NUMBER, + new OnGetSuggestedWordsCallback() { @Override public void onGetSuggestedWords(final SuggestedWords suggestedWords) { final String typedWord = mWordComposer.getTypedWord(); @@ -1315,12 +1432,11 @@ public final class InputLogic { if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations)) { // Show predictions. mWordComposer.setCapitalizedModeAtStartComposingTime(WordComposer.CAPS_MODE_OFF); - mLatinIME.mHandler.postUpdateSuggestionStrip(); + mLatinIME.mHandler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_RECORRECTION); return; } final TextRange range = mConnection.getWordRangeAtCursor( - settingsValues.mSpacingAndPunctuations.mSortedWordSeparators, - currentKeyboardScriptId); + settingsValues.mSpacingAndPunctuations, currentKeyboardScriptId); if (null == range) return; // Happens if we don't have an input connection at all if (range.length() <= 0) { // Race condition, or touching a word in a non-supported script. @@ -1373,13 +1489,14 @@ public final class InputLogic { mLatinIME.getCoordinatesForCurrentKeyboard(codePoints)); mWordComposer.setCursorPositionWithinWord( typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor)); + mConnection.maybeMoveTheCursorAroundAndRestoreToWorkaroundABug(); mConnection.setComposingRegion(expectedCursorPosition - numberOfCharsInWordBeforeCursor, expectedCursorPosition + range.getNumberOfCharsInWordAfterCursor()); if (suggestions.size() <= (shouldIncludeResumedWordInSuggestions ? 1 : 0)) { // If there weren't any suggestion spans on this word, suggestions#size() will be 1 // if shouldIncludeResumedWordInSuggestions is true, 0 otherwise. In this case, we // have no useful suggestions, so we will try to compute some for it instead. - mInputLogicHandler.getSuggestedWords(Suggest.SESSION_TYPING, + mInputLogicHandler.getSuggestedWords(Suggest.SESSION_ID_TYPING, SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() { @Override public void onGetSuggestedWords( @@ -1389,10 +1506,10 @@ public final class InputLogic { && !shouldIncludeResumedWordInSuggestions) { // We were able to compute new suggestions for this word. // Remove the typed word, since we don't want to display it in this - // case. The #getSuggestedWordsExcludingTypedWord() method sets - // willAutoCorrect to false. + // case. The #getSuggestedWordsExcludingTypedWordForRecorrection() + // method sets willAutoCorrect to false. suggestedWords = suggestedWordsIncludingTypedWord - .getSuggestedWordsExcludingTypedWord(); + .getSuggestedWordsExcludingTypedWordForRecorrection(); } else { // No saved suggestions, and we were unable to compute any good one // either. Rather than displaying an empty suggestion strip, we'll @@ -1409,10 +1526,9 @@ public final class InputLogic { // color of the word in the suggestion strip changes according to this parameter, // and false gives the correct color. final SuggestedWords suggestedWords = new SuggestedWords(suggestions, - null /* rawSuggestions */, typedWord, - false /* typedWordValid */, false /* willAutoCorrect */, - false /* isObsoleteSuggestions */, false /* isPrediction */, - SuggestedWords.NOT_A_SEQUENCE_NUMBER); + null /* rawSuggestions */, typedWord, false /* typedWordValid */, + false /* willAutoCorrect */, false /* isObsoleteSuggestions */, + SuggestedWords.INPUT_STYLE_RECORRECTION, SuggestedWords.NOT_A_SEQUENCE_NUMBER); mIsAutoCorrectionIndicatorOn = false; mLatinIME.mHandler.showSuggestionStrip(suggestedWords); } @@ -1424,14 +1540,19 @@ public final class InputLogic { * This is triggered upon pressing backspace just after a commit with auto-correction. * * @param inputTransaction The transaction in progress. + * @param settingsValues the current values of the settings. */ - private void revertCommit(final InputTransaction inputTransaction) { + private void revertCommit(final InputTransaction inputTransaction, + final SettingsValues settingsValues) { final CharSequence originallyTypedWord = mLastComposedWord.mTypedWord; + final String originallyTypedWordString = + originallyTypedWord != null ? originallyTypedWord.toString() : ""; final CharSequence committedWord = mLastComposedWord.mCommittedWord; final String committedWordString = committedWord.toString(); final int cancelLength = committedWord.length(); + final String separatorString = mLastComposedWord.mSeparatorString; // We want java chars, not codepoints for the following. - final int separatorLength = mLastComposedWord.mSeparatorString.length(); + final int separatorLength = separatorString.length(); // TODO: should we check our saved separator against the actual contents of the text view? final int deleteLength = cancelLength + separatorLength; if (DebugFlags.DEBUG_ENABLED) { @@ -1450,7 +1571,7 @@ public final class InputLogic { if (!TextUtils.isEmpty(committedWord)) { mDictionaryFacilitator.removeWordFromPersonalizedDicts(committedWordString); } - final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString; + final String stringToCommit = originallyTypedWord + separatorString; final SpannableString textToCommit = new SpannableString(stringToCommit); if (committedWord instanceof SpannableString) { final SpannableString committedWordWithSuggestionSpans = (SpannableString)committedWord; @@ -1487,23 +1608,53 @@ public final class InputLogic { suggestions.toArray(new String[suggestions.size()]), 0 /* flags */), 0 /* start */, lastCharIndex /* end */, 0 /* flags */); } + + final boolean shouldShowAddToDictionaryForTypedWord = + shouldShowAddToDictionaryForTypedWord(mLastComposedWord, settingsValues); + if (inputTransaction.mSettingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) { // For languages with spaces, we revert to the typed string, but the cursor is still // after the separator so we don't resume suggestions. If the user wants to correct // the word, they have to press backspace again. - mConnection.commitText(textToCommit, 1); + if (shouldShowAddToDictionaryForTypedWord) { + mConnection.commitTextWithBackgroundColor(textToCommit, 1, + settingsValues.mTextHighlightColorForAddToDictionaryIndicator, + originallyTypedWordString.length()); + } else { + mConnection.commitText(textToCommit, 1); + } } else { // For languages without spaces, we revert the typed string but the cursor is flush // with the typed word, so we need to resume suggestions right away. final int[] codePoints = StringUtils.toCodePointArray(stringToCommit); mWordComposer.setComposingWord(codePoints, mLatinIME.getCoordinatesForCurrentKeyboard(codePoints)); - mConnection.setComposingText(textToCommit, 1); + if (shouldShowAddToDictionaryForTypedWord) { + setComposingTextInternalWithBackgroundColor(textToCommit, 1, + settingsValues.mTextHighlightColorForAddToDictionaryIndicator, + originallyTypedWordString.length()); + } else { + setComposingTextInternal(textToCommit, 1); + } } // Don't restart suggestion yet. We'll restart if the user deletes the separator. mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; - // We have a separator between the word and the cursor: we should show predictions. - inputTransaction.setRequiresUpdateSuggestions(); + + if (shouldShowAddToDictionaryForTypedWord) { + // Due to the API limitation as of L, we cannot reliably retrieve the reverted text + // when the separator causes line breaking. Until this API limitation is addressed in + // the framework, show the indicator only when the separator doesn't contain + // line-breaking characters. + if (!StringUtils.hasLineBreakCharacter(separatorString)) { + mTextDecorator.showAddToDictionaryIndicator(originallyTypedWordString, + mConnection.getExpectedSelectionStart(), + mConnection.getExpectedSelectionEnd()); + } + mSuggestionStripViewAccessor.showAddToDictionaryHint(originallyTypedWordString); + } else { + // We have a separator between the word and the cursor: we should show predictions. + inputTransaction.setRequiresUpdateSuggestions(); + } } /** @@ -1708,7 +1859,7 @@ public final class InputLogic { SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, oldSuggestedWords); return new SuggestedWords(typedWordAndPreviousSuggestions, null /* rawSuggestions */, false /* typedWordValid */, false /* hasAutoCorrectionCandidate */, - true /* isObsoleteSuggestions */, false /* isPrediction */); + true /* isObsoleteSuggestions */, oldSuggestedWords.mInputStyle); } /** @@ -1831,10 +1982,10 @@ public final class InputLogic { } final String lastWord = batchInputText.substring(indexOfLastSpace); mWordComposer.setBatchInputWord(lastWord); - mConnection.setComposingText(lastWord, 1); + setComposingTextInternal(lastWord, 1); } else { mWordComposer.setBatchInputWord(batchInputText); - mConnection.setComposingText(batchInputText, 1); + setComposingTextInternal(batchInputText, 1); } mConnection.endBatchEdit(); // Space state must be updated before calling updateShiftState @@ -1891,7 +2042,15 @@ public final class InputLogic { // Complete any pending suggestions query first if (handler.hasPendingUpdateSuggestions()) { handler.cancelUpdateSuggestionStrip(); - performUpdateSuggestionStripSync(settingsValues); + // To know the input style here, we should retrieve the in-flight "update suggestions" + // message and read its arg1 member here. However, the Handler class does not let + // us retrieve this message, so we can't do that. But in fact, we notice that + // we only ever come here when the input style was typing. In the case of batch + // input, we update the suggestions synchronously when the tail batch comes. Likewise + // for application-specified completions. As for recorrections, we never auto-correct, + // so we don't come here either. Hence, the input style is necessarily + // INPUT_STYLE_TYPING. + performUpdateSuggestionStripSync(settingsValues, SuggestedWords.INPUT_STYLE_TYPING); } final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull(); final String typedWord = mWordComposer.getTypedWord(); @@ -1955,14 +2114,13 @@ public final class InputLogic { * This method handles the retry, and re-schedules a new retry if we still can't access. * We only retry up to 5 times before giving up. * - * @param settingsValues the current values of the settings. * @param tryResumeSuggestions Whether we should resume suggestions or not. * @param remainingTries How many times we may try again before giving up. * @return whether true if the caches were successfully reset, false otherwise. */ // TODO: make this private - public boolean retryResetCachesAndReturnSuccess(final SettingsValues settingsValues, - final boolean tryResumeSuggestions, final int remainingTries, + public boolean retryResetCachesAndReturnSuccess(final boolean tryResumeSuggestions, + final int remainingTries, // TODO: remove these arguments final LatinIME.UIHandler handler) { final boolean shouldFinishComposition = mConnection.hasSelection() @@ -1988,7 +2146,7 @@ public final class InputLogic { } public void getSuggestedWords(final SettingsValues settingsValues, - final ProximityInfo proximityInfo, final int keyboardShiftMode, final int sessionId, + final ProximityInfo proximityInfo, final int keyboardShiftMode, final int inputStyle, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { mWordComposer.adviseCapitalizedModeBeforeFetchingSuggestions( getActualCapsMode(settingsValues, keyboardShiftMode)); @@ -2004,6 +2162,124 @@ public final class InputLogic { settingsValues.mPhraseGestureEnabled, settingsValues.mAdditionalFeaturesSettingValues), settingsValues.mAutoCorrectionEnabledPerUserSettings, - sessionId, sequenceNumber, callback); + inputStyle, sequenceNumber, callback); + } + + /** + * Used as an injection point for each call of + * {@link RichInputConnection#setComposingText(CharSequence, int)}. + * + * <p>Currently using this method is optional and you can still directly call + * {@link RichInputConnection#setComposingText(CharSequence, int)}, but it is recommended to + * use this method whenever possible to optimize the behavior of {@link TextDecorator}.<p> + * <p>TODO: Should we move this mechanism to {@link RichInputConnection}?</p> + * + * @param newComposingText the composing text to be set + * @param newCursorPosition the new cursor position + */ + private void setComposingTextInternal(final CharSequence newComposingText, + final int newCursorPosition) { + setComposingTextInternalWithBackgroundColor(newComposingText, newCursorPosition, + Color.TRANSPARENT, newComposingText.length()); + } + + /** + * Equivalent to {@link #setComposingTextInternal(CharSequence, int)} except that this method + * allows to set {@link BackgroundColorSpan} to the composing text with the given color. + * + * <p>TODO: Currently the background color is exclusive with the black underline, which is + * automatically added by the framework. We need to change the framework if we need to have both + * of them at the same time.</p> + * <p>TODO: Should we move this method to {@link RichInputConnection}?</p> + * + * @param newComposingText the composing text to be set + * @param newCursorPosition the new cursor position + * @param backgroundColor the background color to be set to the composing text. Set + * {@link Color#TRANSPARENT} to disable the background color. + * @param coloredTextLength the length of text, in Java chars, which should be rendered with + * the given background color. + */ + private void setComposingTextInternalWithBackgroundColor(final CharSequence newComposingText, + final int newCursorPosition, final int backgroundColor, final int coloredTextLength) { + final CharSequence composingTextToBeSet; + if (backgroundColor == Color.TRANSPARENT) { + composingTextToBeSet = newComposingText; + } else { + final SpannableString spannable = new SpannableString(newComposingText); + final BackgroundColorSpan backgroundColorSpan = + new BackgroundColorSpan(backgroundColor); + final int spanLength = Math.min(coloredTextLength, spannable.length()); + spannable.setSpan(backgroundColorSpan, 0, spanLength, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING); + composingTextToBeSet = spannable; + } + mConnection.setComposingText(composingTextToBeSet, newCursorPosition); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + // Following methods are tentatively placed in this class for the integration with + // TextDecorator. + // TODO: Decouple things that are not related to the input logic. + ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Sets the UI operator for {@link TextDecorator}. + * @param uiOperator the UI operator which should be associated with {@link TextDecorator}. + */ + public void setTextDecoratorUi(final TextDecoratorUiOperator uiOperator) { + mTextDecorator.setUiOperator(uiOperator); + } + + /** + * Must be called from {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} is + * called. + * @param info The wrapper object with which we can access cursor/anchor info. + */ + public void onUpdateCursorAnchorInfo(final CursorAnchorInfoCompatWrapper info) { + mTextDecorator.onUpdateCursorAnchorInfo(info); + } + + /** + * Must be called when {@link InputMethodService#updateFullscreenMode} is called. + * @param isFullscreen {@code true} if the input method is in full-screen mode. + */ + public void onUpdateFullscreenMode(final boolean isFullscreen) { + mTextDecorator.notifyFullScreenMode(isFullscreen); + } + + /** + * Must be called from {@link LatinIME#addWordToUserDictionary(String)}. + */ + public void onAddWordToUserDictionary() { + mConnection.removeBackgroundColorFromHighlightedTextIfNecessary(); + mTextDecorator.reset(); + } + + /** + * Returns whether the add to dictionary indicator should be shown or not. + * @param lastComposedWord the last composed word information. + * @param settingsValues the current settings value. + * @return {@code true} if the commit indicator should be shown. + */ + private boolean shouldShowAddToDictionaryForTypedWord(final LastComposedWord lastComposedWord, + final SettingsValues settingsValues) { + if (!mConnection.isCursorAnchorInfoMonitorEnabled()) { + // We cannot help in this case because we are heavily relying on this new API. + return false; + } + if (!settingsValues.mShouldShowUiToAcceptTypedWord) { + return false; + } + if (TextUtils.isEmpty(lastComposedWord.mTypedWord)) { + return false; + } + if (TextUtils.equals(lastComposedWord.mTypedWord, lastComposedWord.mCommittedWord)) { + return false; + } + if (!mDictionaryFacilitator.isUserDictionaryEnabled()) { + return false; + } + return !mDictionaryFacilitator.isValidWord(lastComposedWord.mTypedWord, + true /* ignoreCase */); } } diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java index 9dbe2c38b..c6f83d0b9 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java @@ -96,7 +96,7 @@ class InputLogicHandler implements Handler.Callback { public boolean handleMessage(final Message msg) { switch (msg.what) { case MSG_GET_SUGGESTED_WORDS: - mLatinIME.getSuggestedWords(msg.arg1 /* sessionId */, + mLatinIME.getSuggestedWords(msg.arg1 /* inputStyle */, msg.arg2 /* sequenceNumber */, (OnGetSuggestedWordsCallback) msg.obj); break; } @@ -134,7 +134,8 @@ class InputLogicHandler implements Handler.Callback { return; } mInputLogic.mWordComposer.setBatchInputPointers(batchPointers); - getSuggestedWords(Suggest.SESSION_GESTURE, sequenceNumber, + getSuggestedWords(isTailBatchInput ? SuggestedWords.INPUT_STYLE_TAIL_BATCH + : SuggestedWords.INPUT_STYLE_UPDATE_BATCH, sequenceNumber, new OnGetSuggestedWordsCallback() { @Override public void onGetSuggestedWords(SuggestedWords suggestedWords) { @@ -205,9 +206,9 @@ class InputLogicHandler implements Handler.Callback { updateBatchInput(batchPointers, sequenceNumber, true /* isTailBatchInput */); } - public void getSuggestedWords(final int sessionId, final int sequenceNumber, + public void getSuggestedWords(final int inputStyle, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { mNonUIThreadHandler.obtainMessage( - MSG_GET_SUGGESTED_WORDS, sessionId, sequenceNumber, callback).sendToTarget(); + MSG_GET_SUGGESTED_WORDS, inputStyle, sequenceNumber, callback).sendToTarget(); } } |