diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin/LatinIME.java')
-rw-r--r-- | java/src/com/android/inputmethod/latin/LatinIME.java | 505 |
1 files changed, 313 insertions, 192 deletions
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 7ba7f7d27..5f446a5c4 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -157,6 +157,22 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar SUGGESTION_VISIBILILTY_HIDE_VALUE }; + // Magic space: a space that should disappear on space/apostrophe insertion, move after the + // punctuation on punctuation insertion, and become a real space on alpha char insertion. + // Weak space: a space that should be swapped only by suggestion strip punctuation. + // Double space: the state where the user pressed space twice quickly, which LatinIME + // resolved as period-space. Undoing this converts the period to a space. + // Swap punctuation: the state where a (weak or magic) space and a punctuation from the + // suggestion strip have just been swapped. Undoing this swaps them back. + private static final int SPACE_STATE_NONE = 0; + private static final int SPACE_STATE_DOUBLE = 1; + private static final int SPACE_STATE_SWAP_PUNCTUATION = 2; + private static final int SPACE_STATE_MAGIC = 3; + private static final int SPACE_STATE_WEAK = 4; + + // Current space state of the input method. This can be any of the above constants. + private int mSpaceState; + private Settings.Values mSettingsValues; private View mExtractArea; @@ -177,7 +193,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar private UserDictionary mUserDictionary; private UserBigramDictionary mUserBigramDictionary; private UserUnigramDictionary mUserUnigramDictionary; - private boolean mIsUserDictionaryAvaliable; + private boolean mIsUserDictionaryAvailable; // TODO: Create an inner class to group options and pseudo-options to improve readability. // These variables are initialized according to the {@link EditorInfo#inputType}. @@ -190,12 +206,6 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar private WordComposer mWordComposer = new WordComposer(); private CharSequence mBestWord; private boolean mHasUncommittedTypedChars; - // Magic space: a space that should disappear on space/apostrophe insertion, move after the - // punctuation on punctuation insertion, and become a real space on alpha char insertion. - private boolean mJustAddedMagicSpace; // This indicates whether the last char is a magic space. - // This indicates whether the last keypress resulted in processing of double space replacement - // with period-space. - private boolean mJustReplacedDoubleSpace; private int mCorrectionMode; private int mCommittedLength; @@ -241,9 +251,8 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar private static final int MSG_FADEOUT_LANGUAGE_ON_SPACEBAR = 3; private static final int MSG_DISMISS_LANGUAGE_ON_SPACEBAR = 4; private static final int MSG_SPACE_TYPED = 5; - private static final int MSG_KEY_TYPED = 6; - private static final int MSG_SET_BIGRAM_PREDICTIONS = 7; - private static final int MSG_PENDING_IMS_CALLBACK = 8; + private static final int MSG_SET_BIGRAM_PREDICTIONS = 6; + private static final int MSG_PENDING_IMS_CALLBACK = 7; private int mDelayBeforeFadeoutLanguageOnSpacebar; private int mDelayUpdateSuggestions; @@ -251,7 +260,6 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar private int mDurationOfFadeoutLanguageOnSpacebar; private float mFinalFadeoutFactorOfLanguageOnSpacebar; private long mDoubleSpacesTurnIntoPeriodTimeout; - private long mIgnoreSpecialKeyTimeout; public UIHandler(LatinIME outerInstance) { super(outerInstance); @@ -271,8 +279,6 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar R.integer.config_final_fadeout_percentage_of_language_on_spacebar) / 100.0f; mDoubleSpacesTurnIntoPeriodTimeout = res.getInteger( R.integer.config_double_spaces_turn_into_period_timeout); - mIgnoreSpecialKeyTimeout = res.getInteger( - R.integer.config_ignore_special_key_timeout); } @Override @@ -384,28 +390,22 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar return hasMessages(MSG_SPACE_TYPED); } - public void startKeyTypedTimer() { - removeMessages(MSG_KEY_TYPED); - sendMessageDelayed(obtainMessage(MSG_KEY_TYPED), mIgnoreSpecialKeyTimeout); - } - - public boolean isIgnoringSpecialKey() { - return hasMessages(MSG_KEY_TYPED); - } - // Working variables for the following methods. private boolean mIsOrientationChanging; private boolean mPendingSuccesiveImsCallback; private boolean mHasPendingStartInput; private boolean mHasPendingFinishInputView; private boolean mHasPendingFinishInput; + private EditorInfo mAppliedEditorInfo; public void startOrientationChanging() { removeMessages(MSG_PENDING_IMS_CALLBACK); resetPendingImsCallback(); mIsOrientationChanging = true; final LatinIME latinIme = getOuterInstance(); - latinIme.mKeyboardSwitcher.saveKeyboardState(); + if (latinIme.isInputViewShown()) { + latinIme.mKeyboardSwitcher.saveKeyboardState(); + } } private void resetPendingImsCallback() { @@ -414,18 +414,18 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mHasPendingStartInput = false; } - private void executePendingImsCallback(LatinIME latinIme, EditorInfo attribute, + private void executePendingImsCallback(LatinIME latinIme, EditorInfo editorInfo, boolean restarting) { if (mHasPendingFinishInputView) latinIme.onFinishInputViewInternal(mHasPendingFinishInput); if (mHasPendingFinishInput) latinIme.onFinishInputInternal(); if (mHasPendingStartInput) - latinIme.onStartInputInternal(attribute, restarting); + latinIme.onStartInputInternal(editorInfo, restarting); resetPendingImsCallback(); } - public void onStartInput(EditorInfo attribute, boolean restarting) { + public void onStartInput(EditorInfo editorInfo, boolean restarting) { if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { // Typically this is the second onStartInput after orientation changed. mHasPendingStartInput = true; @@ -436,27 +436,28 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mPendingSuccesiveImsCallback = true; } final LatinIME latinIme = getOuterInstance(); - executePendingImsCallback(latinIme, attribute, restarting); - latinIme.onStartInputInternal(attribute, restarting); + executePendingImsCallback(latinIme, editorInfo, restarting); + latinIme.onStartInputInternal(editorInfo, restarting); } } - public void onStartInputView(EditorInfo attribute, boolean restarting) { - if (hasMessages(MSG_PENDING_IMS_CALLBACK)) { - // Typically this is the second onStartInputView after orientation changed. - resetPendingImsCallback(); - } else { - if (mPendingSuccesiveImsCallback) { - // This is the first onStartInputView after orientation changed. - mPendingSuccesiveImsCallback = false; - resetPendingImsCallback(); - sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK), - PENDING_IMS_CALLBACK_DURATION); - } - final LatinIME latinIme = getOuterInstance(); - executePendingImsCallback(latinIme, attribute, restarting); - latinIme.onStartInputViewInternal(attribute, restarting); - } + public void onStartInputView(EditorInfo editorInfo, boolean restarting) { + if (hasMessages(MSG_PENDING_IMS_CALLBACK) && editorInfo == mAppliedEditorInfo) { + // Typically this is the second onStartInputView after orientation changed. + resetPendingImsCallback(); + } else { + if (mPendingSuccesiveImsCallback) { + // This is the first onStartInputView after orientation changed. + mPendingSuccesiveImsCallback = false; + resetPendingImsCallback(); + sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK), + PENDING_IMS_CALLBACK_DURATION); + } + final LatinIME latinIme = getOuterInstance(); + executePendingImsCallback(latinIme, editorInfo, restarting); + latinIme.onStartInputViewInternal(editorInfo, restarting); + mAppliedEditorInfo = editorInfo; + } } public void onFinishInputView(boolean finishingInput) { @@ -466,6 +467,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } else { final LatinIME latinIme = getOuterInstance(); latinIme.onFinishInputViewInternal(finishingInput); + mAppliedEditorInfo = null; } } @@ -572,7 +574,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mUserDictionary = new UserDictionary(this, localeStr); mSuggest.setUserDictionary(mUserDictionary); - mIsUserDictionaryAvaliable = mUserDictionary.isEnabled(); + mIsUserDictionaryAvailable = mUserDictionary.isEnabled(); resetContactsDictionary(oldContactsDictionary); @@ -693,13 +695,13 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } @Override - public void onStartInput(EditorInfo attribute, boolean restarting) { - mHandler.onStartInput(attribute, restarting); + public void onStartInput(EditorInfo editorInfo, boolean restarting) { + mHandler.onStartInput(editorInfo, restarting); } @Override - public void onStartInputView(EditorInfo attribute, boolean restarting) { - mHandler.onStartInputView(attribute, restarting); + public void onStartInputView(EditorInfo editorInfo, boolean restarting) { + mHandler.onStartInputView(editorInfo, restarting); } @Override @@ -712,19 +714,19 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mHandler.onFinishInput(); } - private void onStartInputInternal(EditorInfo attribute, boolean restarting) { - super.onStartInput(attribute, restarting); + private void onStartInputInternal(EditorInfo editorInfo, boolean restarting) { + super.onStartInput(editorInfo, restarting); } - private void onStartInputViewInternal(EditorInfo attribute, boolean restarting) { - super.onStartInputView(attribute, restarting); + private void onStartInputViewInternal(EditorInfo editorInfo, boolean restarting) { + super.onStartInputView(editorInfo, restarting); final KeyboardSwitcher switcher = mKeyboardSwitcher; LatinKeyboardView inputView = switcher.getKeyboardView(); if (DEBUG) { - Log.d(TAG, "onStartInputView: attribute:" + ((attribute == null) ? "none" + Log.d(TAG, "onStartInputView: editorInfo:" + ((editorInfo == null) ? "none" : String.format("inputType=0x%08x imeOptions=0x%08x", - attribute.inputType, attribute.imeOptions))); + editorInfo.inputType, editorInfo.imeOptions))); } // In landscape mode, this method gets called without the input view being created. if (inputView == null) { @@ -734,7 +736,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar // Forward this event to the accessibility utilities, if enabled. final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance(); if (accessUtils.isTouchExplorationEnabled()) { - accessUtils.onStartInputViewInternal(attribute, restarting); + accessUtils.onStartInputViewInternal(editorInfo, restarting); } mSubtypeSwitcher.updateParametersOnStartInputView(); @@ -745,22 +747,21 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar // know now whether this is a password text field, because we need to know now whether we // want to enable the voice button. final VoiceProxy voiceIme = mVoiceProxy; - final int inputType = (attribute != null) ? attribute.inputType : 0; + final int inputType = (editorInfo != null) ? editorInfo.inputType : 0; voiceIme.resetVoiceStates(InputTypeCompatUtils.isPasswordInputType(inputType) || InputTypeCompatUtils.isVisiblePasswordInputType(inputType)); // The EditorInfo might have a flag that affects fullscreen mode. // Note: This call should be done by InputMethodService? updateFullscreenMode(); - initializeInputAttributes(attribute); + initializeInputAttributes(editorInfo); inputView.closing(); mEnteredText = null; mComposingStringBuilder.setLength(0); mHasUncommittedTypedChars = false; mDeleteCount = 0; - mJustAddedMagicSpace = false; - mJustReplacedDoubleSpace = false; + mSpaceState = SPACE_STATE_NONE; loadSettings(); updateCorrectionMode(); @@ -769,12 +770,12 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar if (mSuggest != null && mSettingsValues.mAutoCorrectEnabled) { mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold); } - mVoiceProxy.loadSettings(attribute, mPrefs); + mVoiceProxy.loadSettings(editorInfo, mPrefs); // This will work only when the subtype is not supported. LanguageSwitcherProxy.loadSettings(); if (mSubtypeSwitcher.isKeyboardMode()) { - switcher.loadKeyboard(attribute, mSettingsValues); + switcher.loadKeyboard(editorInfo, mSettingsValues); } if (mSuggestionsView != null) @@ -783,6 +784,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar isSuggestionsStripVisible(), /* needsInputViewShown */ false); // Delay updating suggestions because keyboard input view may not be shown at this point. mHandler.postUpdateSuggestions(); + mHandler.cancelDoubleSpacesTimer(); inputView.setKeyPreviewPopupEnabled(mSettingsValues.mKeyPreviewPopupOn, mSettingsValues.mKeyPreviewPopupDismissDelay); @@ -793,10 +795,10 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); } - private void initializeInputAttributes(EditorInfo attribute) { - if (attribute == null) + private void initializeInputAttributes(EditorInfo editorInfo) { + if (editorInfo == null) return; - final int inputType = attribute.inputType; + final int inputType = editorInfo.inputType; if (inputType == InputType.TYPE_NULL) { // TODO: We should honor TYPE_NULL specification. Log.i(TAG, "InputType.TYPE_NULL is specified"); @@ -805,7 +807,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar final int variation = inputType & InputType.TYPE_MASK_VARIATION; if (inputClass == 0) { Log.w(TAG, String.format("Unexpected input class: inputType=0x%08x imeOptions=0x%08x", - inputType, attribute.imeOptions)); + inputType, editorInfo.imeOptions)); } mInsertSpaceOnPickSuggestionManually = false; @@ -921,6 +923,13 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar || newSelEnd != candidatesEnd) && mLastSelectionStart != newSelStart; final boolean candidatesCleared = candidatesStart == -1 && candidatesEnd == -1; if (!mExpectingUpdateSelection) { + if (SPACE_STATE_WEAK == mSpaceState) { + // Test for no WEAK_SPACE action because there is a race condition that may end up + // in coming here on a normal key press. We set this to NONE because after + // a cursor move, we don't want the suggestion strip to swap the space with the + // newly inserted punctuation. + mSpaceState = SPACE_STATE_NONE; + } if (((mComposingStringBuilder.length() > 0 && mHasUncommittedTypedChars) || mVoiceProxy.isVoiceInputHighlighted()) && (selectionChanged || candidatesCleared)) { @@ -938,22 +947,19 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar TextEntryState.reset(); updateSuggestions(); } - mJustAddedMagicSpace = false; // The user moved the cursor. - mJustReplacedDoubleSpace = false; } 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; mLastSelectionEnd = newSelEnd; } - public void setLastSelection(int start, int end) { - mLastSelectionStart = start; - mLastSelectionEnd = end; - } - /** * This is called when the user has clicked on the extracted text view, * when running in fullscreen mode. The default implementation hides @@ -1164,25 +1170,22 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar return false; } - private void swapSwapperAndSpace() { - final InputConnection ic = getCurrentInputConnection(); - if (ic == null) return; + // "ic" may be null + private void swapSwapperAndSpaceWhileInBatchEdit(final InputConnection ic) { + if (null == ic) return; CharSequence lastTwo = ic.getTextBeforeCursor(2, 0); // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called. if (lastTwo != null && lastTwo.length() == 2 && lastTwo.charAt(0) == Keyboard.CODE_SPACE) { - ic.beginBatchEdit(); ic.deleteSurroundingText(2, 0); ic.commitText(lastTwo.charAt(1) + " ", 1); - ic.endBatchEdit(); mKeyboardSwitcher.updateShiftState(); } } - private void maybeDoubleSpace() { - if (mCorrectionMode == Suggest.CORRECTION_NONE) return; - final InputConnection ic = getCurrentInputConnection(); - if (ic == null) return; + private boolean maybeDoubleSpaceWhileInBatchEdit(final InputConnection ic) { + if (mCorrectionMode == Suggest.CORRECTION_NONE) return false; + if (ic == null) return false; final CharSequence lastThree = ic.getTextBeforeCursor(3, 0); if (lastThree != null && lastThree.length() == 3 && Utils.canBeFollowedByPeriod(lastThree.charAt(0)) @@ -1190,22 +1193,19 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar && lastThree.charAt(2) == Keyboard.CODE_SPACE && mHandler.isAcceptingDoubleSpaces()) { mHandler.cancelDoubleSpacesTimer(); - ic.beginBatchEdit(); ic.deleteSurroundingText(2, 0); ic.commitText(". ", 1); - ic.endBatchEdit(); mKeyboardSwitcher.updateShiftState(); - mJustReplacedDoubleSpace = true; - } else { - mHandler.startDoubleSpacesTimer(); + return true; } + return false; } - // "ic" must not null - private void maybeRemovePreviousPeriod(final InputConnection ic, CharSequence text) { + // "ic" must not be null + private static void maybeRemovePreviousPeriod(final InputConnection ic, CharSequence text) { // When the text's first character is '.', remove the previous period // if there is one. - CharSequence lastOne = ic.getTextBeforeCursor(1, 0); + final CharSequence lastOne = ic.getTextBeforeCursor(1, 0); if (lastOne != null && lastOne.length() == 1 && lastOne.charAt(0) == Keyboard.CODE_PERIOD && text.charAt(0) == Keyboard.CODE_PERIOD) { @@ -1213,11 +1213,10 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } } - private void removeTrailingSpace() { - final InputConnection ic = getCurrentInputConnection(); + // "ic" may be null + private static void removeTrailingSpaceWhileInBatchEdit(final InputConnection ic) { if (ic == null) return; - - CharSequence lastOne = ic.getTextBeforeCursor(1, 0); + final CharSequence lastOne = ic.getTextBeforeCursor(1, 0); if (lastOne != null && lastOne.length() == 1 && lastOne.charAt(0) == Keyboard.CODE_SPACE) { ic.deleteSurroundingText(1, 0); @@ -1233,12 +1232,8 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar return true; } - private boolean isAlphabet(int code) { - if (Character.isLetter(code)) { - return true; - } else { - return false; - } + private static boolean isAlphabet(int code) { + return Character.isLetter(code); } private void onSettingsKeyPressed() { @@ -1254,6 +1249,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar // Virtual codes representing custom requests. These are used in onCustomRequest() below. public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1; + public static final int CODE_HAPTIC_AND_AUDIO_FEEDBACK = 2; @Override public boolean onCustomRequest(int requestCode) { @@ -1265,6 +1261,9 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar return true; } return false; + case CODE_HAPTIC_AND_AUDIO_FEEDBACK: + hapticAndAudioFeedback(Keyboard.CODE_UNSPECIFIED); + return true; } return false; } @@ -1273,6 +1272,28 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar return mOptionsDialog != null && mOptionsDialog.isShowing(); } + private void insertPunctuationFromSuggestionStrip(final InputConnection ic, final int code) { + final CharSequence beforeText = ic != null ? ic.getTextBeforeCursor(1, 0) : null; + final int toLeft = TextUtils.isEmpty(beforeText) ? 0 : beforeText.charAt(0); + final boolean shouldRegisterSwapPunctuation; + // If we have a space left of the cursor and it's a weak or a magic space, then we should + // swap it, and override the space state with SPACESTATE_SWAP_PUNCTUATION. + // To swap it, we fool handleSeparator to think the previous space state was a + // magic space. + if (Keyboard.CODE_SPACE == toLeft && mSpaceState == SPACE_STATE_WEAK) { + mSpaceState = SPACE_STATE_MAGIC; + shouldRegisterSwapPunctuation = true; + } else { + shouldRegisterSwapPunctuation = false; + } + onCodeInput(code, new int[] { code }, + KeyboardActionListener.NOT_A_TOUCH_COORDINATE, + KeyboardActionListener.NOT_A_TOUCH_COORDINATE); + if (shouldRegisterSwapPunctuation) { + mSpaceState = SPACE_STATE_SWAP_PUNCTUATION; + } + } + // Implementation of {@link KeyboardActionListener}. @Override public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) { @@ -1283,12 +1304,22 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mLastKeyTime = when; final KeyboardSwitcher switcher = mKeyboardSwitcher; final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); - final boolean lastStateOfJustReplacedDoubleSpace = mJustReplacedDoubleSpace; - mJustReplacedDoubleSpace = false; - boolean shouldStartKeyTypedTimer = true; + // The space state depends only on the last character pressed and its own previous + // state. Here, we revert the space state to neutral if the key is actually modifying + // the input contents (any non-shift key), which is what we should do for + // all inputs that do not result in a special state. Each character handling is then + // free to override the state as they see fit. + final int spaceState = mSpaceState; + + // TODO: Consolidate the double space timer, mLastKeyTime, and the space state. + if (primaryCode != Keyboard.CODE_SPACE) { + mHandler.cancelDoubleSpacesTimer(); + } + switch (primaryCode) { case Keyboard.CODE_DELETE: - handleBackspace(lastStateOfJustReplacedDoubleSpace); + mSpaceState = SPACE_STATE_NONE; + handleBackspace(spaceState); mDeleteCount++; mExpectingUpdateSelection = true; LatinImeLogger.logOnDelete(); @@ -1298,14 +1329,12 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar if (!distinctMultiTouch) { switcher.toggleShift(); } - shouldStartKeyTypedTimer = false; break; case Keyboard.CODE_SWITCH_ALPHA_SYMBOL: // Symbol key is handled in onPress() when device has distinct multi-touch panel. if (!distinctMultiTouch) { - switcher.changeKeyboardMode(); + switcher.toggleKeyboardMode(); } - shouldStartKeyTypedTimer = false; break; case Keyboard.CODE_CANCEL: if (!isShowingOptionDialog()) { @@ -1313,24 +1342,14 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } break; case Keyboard.CODE_SETTINGS: - if (!mHandler.isIgnoringSpecialKey()) { - onSettingsKeyPressed(); - } - shouldStartKeyTypedTimer = false; + onSettingsKeyPressed(); break; case Keyboard.CODE_CAPSLOCK: switcher.toggleCapsLock(); - //$FALL-THROUGH$ - case Keyboard.CODE_HAPTIC_AND_AUDIO_FEEDBACK_ONLY: - // Dummy code for haptic and audio feedbacks. - vibrate(); - playKeyClick(primaryCode); + hapticAndAudioFeedback(primaryCode); break; case Keyboard.CODE_SHORTCUT: - if (!mHandler.isIgnoringSpecialKey()) { - mSubtypeSwitcher.switchToShortcutIME(); - } - shouldStartKeyTypedTimer = false; + mSubtypeSwitcher.switchToShortcutIME(); break; case Keyboard.CODE_TAB: handleTab(); @@ -1344,10 +1363,11 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar // To sum it up: do not update mExpectingUpdateSelection here. break; default: + mSpaceState = SPACE_STATE_NONE; if (mSettingsValues.isWordSeparator(primaryCode)) { - handleSeparator(primaryCode, x, y); + handleSeparator(primaryCode, x, y, spaceState); } else { - handleCharacter(primaryCode, keyCodes, x, y); + handleCharacter(primaryCode, keyCodes, x, y, spaceState); } mExpectingUpdateSelection = true; break; @@ -1355,9 +1375,6 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar switcher.onKey(primaryCode); // Reset after any single keystroke mEnteredText = null; - if (shouldStartKeyTypedTimer) { - mHandler.startKeyTypedTimer(); - } } @Override @@ -1372,9 +1389,8 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar ic.endBatchEdit(); mKeyboardSwitcher.updateShiftState(); mKeyboardSwitcher.onKey(Keyboard.CODE_DUMMY); - mJustAddedMagicSpace = false; + mSpaceState = SPACE_STATE_NONE; mEnteredText = text; - mHandler.startKeyTypedTimer(); } @Override @@ -1383,7 +1399,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mKeyboardSwitcher.onCancelInput(); } - private void handleBackspace(boolean justReplacedDoubleSpace) { + private void handleBackspace(final int spaceState) { if (mVoiceProxy.logAndRevertVoiceInput()) return; final InputConnection ic = getCurrentInputConnection(); @@ -1421,15 +1437,24 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } mHandler.postUpdateShiftKeyState(); + // TODO: Merge space state with TextEntryState TextEntryState.backspace(); if (TextEntryState.isUndoCommit()) { revertLastWord(ic); ic.endBatchEdit(); return; } - if (justReplacedDoubleSpace) { + if (SPACE_STATE_DOUBLE == spaceState) { if (revertDoubleSpace(ic)) { ic.endBatchEdit(); + // No need to reset mSpaceState, it has already be done (that's why we + // receive it as a parameter) + return; + } + } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) { + if (revertSwapPunctuation(ic)) { + ic.endBatchEdit(); + // Likewise return; } } @@ -1447,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(); @@ -1479,18 +1505,24 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } } - private void handleCharacter(int primaryCode, int[] keyCodes, int x, int y) { + private void handleCharacter(final int primaryCode, final int[] keyCodes, final int x, + final int y, final int spaceState) { mVoiceProxy.handleCharacter(); - if (mJustAddedMagicSpace && mSettingsValues.isMagicSpaceStripper(primaryCode)) { - removeTrailingSpace(); + final InputConnection ic = getCurrentInputConnection(); + if (ic != null) ic.beginBatchEdit(); + if (SPACE_STATE_MAGIC == spaceState + && mSettingsValues.isMagicSpaceStripper(primaryCode)) { + removeTrailingSpaceWhileInBatchEdit(ic); } int code = primaryCode; if ((isAlphabet(code) || mSettingsValues.isSymbolExcludedFromWordSeparators(code)) && isSuggestionsRequested() && !isCursorTouchingWord()) { if (!mHasUncommittedTypedChars) { - mHasUncommittedTypedChars = true; + // Reset entirely the composing state anyway, then start composing a new word unless + // the character is a single quote. + mHasUncommittedTypedChars = (Keyboard.CODE_SINGLE_QUOTE != code); mComposingStringBuilder.setLength(0); mWordComposer.reset(); clearSuggestions(); @@ -1501,6 +1533,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar if (switcher.isShiftedOrShiftLocked()) { if (keyCodes == null || keyCodes[0] < Character.MIN_CODE_POINT || keyCodes[0] > Character.MAX_CODE_POINT) { + if (null != ic) ic.endBatchEdit(); return; } code = keyCodes[0]; @@ -1514,6 +1547,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } else { // Some keys, such as [eszett], have upper case as multi-characters. onTextInput(upperCaseString); + if (null != ic) ic.endBatchEdit(); return; } } @@ -1521,7 +1555,6 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar if (mHasUncommittedTypedChars) { mComposingStringBuilder.append((char) code); mWordComposer.add(code, keyCodes, x, y); - final InputConnection ic = getCurrentInputConnection(); if (ic != null) { // If it's the first letter, make note of auto-caps state if (mWordComposer.size() == 1) { @@ -1539,18 +1572,19 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } else { sendKeyChar((char)code); } - if (mJustAddedMagicSpace && mSettingsValues.isMagicSpaceSwapper(primaryCode)) { - swapSwapperAndSpace(); - } else { - mJustAddedMagicSpace = false; + if (SPACE_STATE_MAGIC == spaceState + && mSettingsValues.isMagicSpaceSwapper(primaryCode)) { + if (null != ic) swapSwapperAndSpaceWhileInBatchEdit(ic); } switcher.updateShiftState(); if (LatinIME.PERF_DEBUG) measureCps(); TextEntryState.typedCharacter((char) code, mSettingsValues.isWordSeparator(code), x, y); + if (null != ic) ic.endBatchEdit(); } - private void handleSeparator(int primaryCode, int x, int y) { + private void handleSeparator(final int primaryCode, final int x, final int y, + final int spaceState) { mVoiceProxy.handleSeparator(); mComposingStateManager.onFinishComposingText(); @@ -1580,21 +1614,49 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar } } - if (mJustAddedMagicSpace) { + final boolean swapMagicSpace; + if (Keyboard.CODE_ENTER == primaryCode && (SPACE_STATE_MAGIC == spaceState + || SPACE_STATE_SWAP_PUNCTUATION == spaceState)) { + removeTrailingSpaceWhileInBatchEdit(ic); + swapMagicSpace = false; + } else if (SPACE_STATE_MAGIC == spaceState) { if (mSettingsValues.isMagicSpaceSwapper(primaryCode)) { - sendKeyChar((char)primaryCode); - swapSwapperAndSpace(); + swapMagicSpace = true; } else { - if (mSettingsValues.isMagicSpaceStripper(primaryCode)) removeTrailingSpace(); - sendKeyChar((char)primaryCode); - mJustAddedMagicSpace = false; + swapMagicSpace = false; + if (mSettingsValues.isMagicSpaceStripper(primaryCode)) { + removeTrailingSpaceWhileInBatchEdit(ic); + } } } else { - sendKeyChar((char)primaryCode); + swapMagicSpace = false; } - if (isSuggestionsRequested() && primaryCode == Keyboard.CODE_SPACE) { - maybeDoubleSpace(); + sendKeyChar((char)primaryCode); + + if (Keyboard.CODE_SPACE == primaryCode) { + if (isSuggestionsRequested()) { + if (maybeDoubleSpaceWhileInBatchEdit(ic)) { + mSpaceState = SPACE_STATE_DOUBLE; + } else if (!isShowingPunctuationList()) { + mSpaceState = SPACE_STATE_WEAK; + } + } + + mHandler.startDoubleSpacesTimer(); + if (!isCursorTouchingWord()) { + mHandler.cancelUpdateSuggestions(); + mHandler.postUpdateBigramPredictions(); + } + } else { + if (swapMagicSpace) { + swapSwapperAndSpaceWhileInBatchEdit(ic); + mSpaceState = SPACE_STATE_MAGIC; + } + + // Set punctuation right away. onUpdateSelection will fire but tests whether it is + // already displayed or not, so it's okay. + setPunctuationSuggestions(); } TextEntryState.typedCharacter((char) primaryCode, true, x, y); @@ -1607,16 +1669,6 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar ic, mLastSelectionEnd - typedWord.length(), typedWord, mBestWord); } } - if (Keyboard.CODE_SPACE == primaryCode) { - if (!isCursorTouchingWord()) { - mHandler.cancelUpdateSuggestions(); - mHandler.postUpdateBigramPredictions(); - } - } else { - // Set punctuation right away. onUpdateSelection will fire but tests whether it is - // already displayed or not, so it's okay. - setPunctuationSuggestions(); - } mKeyboardSwitcher.updateShiftState(); if (ic != null) { ic.endBatchEdit(); @@ -1651,7 +1703,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar public boolean isSuggestionsStripVisible() { if (mSuggestionsView == null) return false; - if (mSuggestionsView.isShowingAddToDictionaryHint() || TextEntryState.isRecorrecting()) + if (mSuggestionsView.isShowingAddToDictionaryHint()) return true; if (!isShowingSuggestionsStrip()) return false; @@ -1748,15 +1800,23 @@ 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(), typedWord, preferCapitalization()); + mSuggest.getUnigramDictionaries(), + // If the typed string ends with a single quote, for dictionary lookup purposes + // we behave as if the single quote was not here. Here, we are looking up the + // 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. + quotesCount > 0 + ? typedWord.subSequence(0, typedWord.length() - quotesCount) : typedWord, + preferCapitalization()); if (mCorrectionMode == Suggest.CORRECTION_FULL || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) { autoCorrectionAvailable |= (!allowsToBeAutoCorrected); } // Don't auto-correct words with multiple capital letter autoCorrectionAvailable &= !wordComposer.isMostlyCaps(); - autoCorrectionAvailable &= !TextEntryState.isRecorrecting(); // Basically, we update the suggestion strip only when suggestion count > 1. However, // there is an exception: We update the suggestion strip whenever typed word's length @@ -1829,7 +1889,6 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mVoiceProxy.flushAndLogAllTextModificationCounters(index, suggestion, mSettingsValues.mWordSeparators); - final boolean recorrecting = TextEntryState.isRecorrecting(); final InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.beginBatchEdit(); @@ -1859,8 +1918,8 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar LatinImeLogger.logOnManualSuggestion( "", suggestion.toString(), index, suggestions.mWords); // Find out whether the previous character is a space. If it is, as a special case - // for punctuation entered through the suggestion strip, it should be considered - // a magic space even if it was a normal space. This is meant to help in case the user + // for punctuation entered through the suggestion strip, it should be swapped + // if it was a magic or a weak space. This is meant to help in case the user // pressed space on purpose of displaying the suggestion strip punctuation. final int rawPrimaryCode = suggestion.charAt(0); // Maybe apply the "bidi mirrored" conversions for parentheses @@ -1868,15 +1927,8 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar final boolean isRtl = keyboard != null && keyboard.mIsRtlKeyboard; final int primaryCode = Key.getRtlParenthesisCode(rawPrimaryCode, isRtl); - final CharSequence beforeText = ic != null ? ic.getTextBeforeCursor(1, 0) : ""; - final int toLeft = (ic == null || TextUtils.isEmpty(beforeText)) - ? 0 : beforeText.charAt(0); - final boolean oldMagicSpace = mJustAddedMagicSpace; - if (Keyboard.CODE_SPACE == toLeft) mJustAddedMagicSpace = true; - onCodeInput(primaryCode, new int[] { primaryCode }, - KeyboardActionListener.NOT_A_TOUCH_COORDINATE, - KeyboardActionListener.NOT_A_TOUCH_COORDINATE); - mJustAddedMagicSpace = oldMagicSpace; + insertPunctuationFromSuggestionStrip(ic, primaryCode); + // TODO: the following endBatchEdit seems useless, check if (ic != null) { ic.endBatchEdit(); } @@ -1900,7 +1952,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar suggestion.toString(), index, suggestions.mWords); TextEntryState.acceptedSuggestion(mComposingStringBuilder.toString(), suggestion); // Follow it with a space - if (mInsertSpaceOnPickSuggestionManually && !recorrecting) { + if (mInsertSpaceOnPickSuggestionManually) { sendMagicSpace(); } @@ -1920,13 +1972,11 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar || !AutoCorrection.isValidWord( mSuggest.getUnigramDictionaries(), suggestion, true)); - if (!recorrecting) { - // Fool the state watcher so that a subsequent backspace will not do a revert, unless - // we just did a correction, in which case we need to stay in - // TextEntryState.State.PICKED_SUGGESTION state. - TextEntryState.typedCharacter((char) Keyboard.CODE_SPACE, true, - WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); - } + // Fool the state watcher so that a subsequent backspace will not do a revert, unless + // we just did a correction, in which case we need to stay in + // TextEntryState.State.PICKED_SUGGESTION state. + TextEntryState.typedCharacter((char) Keyboard.CODE_SPACE, true, + WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); if (!showingAddToDictionaryHint) { // If we're not showing the "Touch again to save", then show corrections again. // In case the cursor position doesn't change, make sure we show the suggestions again. @@ -1936,8 +1986,9 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar // take a noticeable delay to update them which may feel uneasy. } if (showingAddToDictionaryHint) { - if (mIsUserDictionaryAvaliable) { - mSuggestionsView.showAddToDictionaryHint(suggestion); + if (mIsUserDictionaryAvailable) { + mSuggestionsView.showAddToDictionaryHint( + suggestion, mSettingsValues.mHintToSaveText); } else { mHandler.postUpdateSuggestions(); } @@ -2068,13 +2119,60 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar return false; } - // "ic" must not null - private boolean sameAsTextBeforeCursor(final InputConnection ic, CharSequence text) { + // "ic" must not be null + private static boolean sameAsTextBeforeCursor(final InputConnection ic, CharSequence text) { CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0); return TextUtils.equals(text, beforeText); } - // "ic" must not null + // "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); @@ -2100,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(); @@ -2108,7 +2210,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mHandler.postUpdateSuggestions(); } - // "ic" must not null + // "ic" must not be null private boolean revertDoubleSpace(final InputConnection ic) { mHandler.cancelDoubleSpacesTimer(); // Here we test whether we indeed have a period and a space before us. This should not @@ -2123,13 +2225,28 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar return true; } + private static boolean revertSwapPunctuation(final InputConnection ic) { + // Here we test whether we indeed have a space and something else before us. This should not + // be needed, but it's there just in case something went wrong. + final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0); + // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to + // enter surrogate pairs this code will have been removed. + if (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1)) + return false; + ic.beginBatchEdit(); + ic.deleteSurroundingText(2, 0); + ic.commitText(" " + textBeforeCursor.subSequence(0, 1), 1); + ic.endBatchEdit(); + return true; + } + public boolean isWordSeparator(int code) { return mSettingsValues.isWordSeparator(code); } private void sendMagicSpace() { sendKeyChar((char)Keyboard.CODE_SPACE); - mJustAddedMagicSpace = true; + mSpaceState = SPACE_STATE_MAGIC; mKeyboardSwitcher.updateShiftState(); } @@ -2155,12 +2272,16 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar loadSettings(); } + private void hapticAndAudioFeedback(int primaryCode) { + vibrate(); + playKeyClick(primaryCode); + } + @Override public void onPress(int primaryCode, boolean withSliding) { final KeyboardSwitcher switcher = mKeyboardSwitcher; if (switcher.isVibrateAndSoundFeedbackRequired()) { - vibrate(); - playKeyClick(primaryCode); + hapticAndAudioFeedback(primaryCode); } final boolean distinctMultiTouch = switcher.hasDistinctMultitouch(); if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) { |