diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java index dc27769ab..1f3ee7680 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java @@ -16,6 +16,9 @@ package com.android.inputmethod.keyboard; +import com.android.inputmethod.latin.InputPointers; +import com.android.inputmethod.latin.SuggestedWords; + public interface KeyboardActionListener { /** @@ -64,13 +67,18 @@ public interface KeyboardActionListener { */ public void onTextInput(CharSequence text); - // TODO: Should move this method to some more appropriate interface. /** * Called when user started batch input. */ public void onStartBatchInput(); - // TODO: Should move this method to some more appropriate interface. + /** + * Sends the batch input points data to get updated suggestions + * @param batchPointers the batch input points representing the user input + * @return updated suggestions that reflects the user input + */ + public SuggestedWords onUpdateBatchInput(InputPointers batchPointers); + /** * Sends a sequence of characters to the listener as batch input. * @@ -101,6 +109,8 @@ public interface KeyboardActionListener { @Override public void onStartBatchInput() {} @Override + public SuggestedWords onUpdateBatchInput(InputPointers batchPointers) { return null; } + @Override public void onEndBatchInput(CharSequence text) {} @Override public void onCancelInput() {} diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java index 1ae0020a4..733d3b09b 100644 --- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java +++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java @@ -22,6 +22,7 @@ import android.view.MotionEvent; import android.view.View; import android.widget.TextView; +import com.android.inputmethod.keyboard.internal.GestureTracker; import com.android.inputmethod.keyboard.internal.PointerTrackerQueue; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.ResearchLogger; @@ -161,6 +162,9 @@ public class PointerTracker { private static final KeyboardActionListener EMPTY_LISTENER = new KeyboardActionListener.Adapter(); + // Gesture tracker singleton instance + private static final GestureTracker sGestureTracker = GestureTracker.getInstance(); + public static void init(boolean hasDistinctMultitouch, boolean needsPhantomSuddenMoveEventHack) { if (hasDistinctMultitouch) { @@ -199,6 +203,7 @@ public class PointerTracker { for (final PointerTracker tracker : sTrackers) { tracker.mListener = listener; } + GestureTracker.init(listener); } public static void setKeyDetector(KeyDetector keyDetector) { @@ -207,6 +212,7 @@ public class PointerTracker { // Mark that keyboard layout has been changed. tracker.mKeyboardLayoutHasBeenChanged = true; } + sGestureTracker.setKeyboard(keyDetector.getKeyboard()); } public static void dismissAllKeyPreviews() { @@ -233,6 +239,9 @@ public class PointerTracker { // Returns true if keyboard has been changed by this callback. private boolean callListenerOnPressAndCheckKeyboardLayoutChange(Key key) { + if (sGestureTracker.isInGesture()) { + return false; + } final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier(); if (DEBUG_LISTENER) { Log.d(TAG, "onPress : " + KeyDetector.printableCode(key) @@ -286,6 +295,9 @@ public class PointerTracker { // Note that we need primaryCode argument because the keyboard may in shifted state and the // primaryCode is different from {@link Key#mCode}. private void callListenerOnRelease(Key key, int primaryCode, boolean withSliding) { + if (sGestureTracker.isInGesture()) { + return; + } final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier(); if (DEBUG_LISTENER) { Log.d(TAG, "onRelease : " + Keyboard.printableCode(primaryCode) @@ -386,7 +398,7 @@ public class PointerTracker { return; } - if (!key.noKeyPreview()) { + if (!key.noKeyPreview() && !sGestureTracker.isInGesture()) { mDrawingProxy.showKeyPreview(this); } updatePressKeyGraphics(key); @@ -504,8 +516,8 @@ public class PointerTracker { } final PointerTrackerQueue queue = sPointerTrackerQueue; + final Key key = getKeyOn(x, y); if (queue != null) { - final Key key = getKeyOn(x, y); if (key != null && key.isModifier()) { // Before processing a down event of modifier key, all pointers already being // tracked should be released. @@ -514,6 +526,9 @@ public class PointerTracker { queue.add(this); } onDownEventInternal(x, y, eventTime); + if (queue != null && queue.size() == 1) { + sGestureTracker.onDownEvent(this, x, y, eventTime, key); + } } private void onDownEventInternal(int x, int y, long eventTime) { @@ -554,10 +569,34 @@ public class PointerTracker { if (mKeyAlreadyProcessed) return; + if (me != null) { + // Add historical points to gesture path. + final int pointerIndex = me.findPointerIndex(mPointerId); + final int historicalSize = me.getHistorySize(); + for (int h = 0; h < historicalSize; h++) { + final int historicalX = (int)me.getHistoricalX(pointerIndex, h); + final int historicalY = (int)me.getHistoricalY(pointerIndex, h); + final long historicalTime = me.getHistoricalEventTime(h); + sGestureTracker.onMoveEvent(this, historicalX, historicalY, historicalTime, + true /* isHistorical */, null); + } + } + final int lastX = mLastX; final int lastY = mLastY; final Key oldKey = mCurrentKey; Key key = onMoveKey(x, y); + + // Register move event on gesture tracker. + sGestureTracker.onMoveEvent(this, x, y, eventTime, false, key); + if (sGestureTracker.isInGesture()) { + mIgnoreModifierKey = true; + mTimerProxy.cancelLongPressTimer(); + mIsInSlidingKeyInput = true; + mCurrentKey = null; + setReleasedKeyGraphics(oldKey); + } + if (key != null) { if (oldKey == null) { // The pointer has been slid in to the new key, but the finger was not on any keys. @@ -607,7 +646,7 @@ public class PointerTracker { if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.pointerTracker_onMoveEvent(x, y, lastX, lastY); } - onUpEventInternal(); + onUpEventInternal(x, y, eventTime); onDownEventInternal(x, y, eventTime); } else { // HACK: If there are currently multiple touches, register the key even if @@ -617,7 +656,7 @@ public class PointerTracker { // this hack. if (me != null && me.getPointerCount() > 1 && !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) { - onUpEventInternal(); + onUpEventInternal(x, y, eventTime); } mKeyAlreadyProcessed = true; setReleasedKeyGraphics(oldKey); @@ -647,16 +686,18 @@ public class PointerTracker { final PointerTrackerQueue queue = sPointerTrackerQueue; if (queue != null) { - if (mCurrentKey != null && mCurrentKey.isModifier()) { - // Before processing an up event of modifier key, all pointers already being - // tracked should be released. - queue.releaseAllPointersExcept(this, eventTime); - } else { - queue.releaseAllPointersOlderThan(this, eventTime); + if (!sGestureTracker.isInGesture()) { + if (mCurrentKey != null && mCurrentKey.isModifier()) { + // Before processing an up event of modifier key, all pointers already being + // tracked should be released. + queue.releaseAllPointersExcept(this, eventTime); + } else { + queue.releaseAllPointersOlderThan(this, eventTime); + } } queue.remove(this); } - onUpEventInternal(); + onUpEventInternal(x, y, eventTime); } // Let this pointer tracker know that one of newer-than-this pointer trackers got an up event. @@ -665,11 +706,11 @@ public class PointerTracker { public void onPhantomUpEvent(int x, int y, long eventTime) { if (DEBUG_EVENT) printTouchEvent("onPhntEvent:", x, y, eventTime); - onUpEventInternal(); + onUpEventInternal(x, y, eventTime); mKeyAlreadyProcessed = true; } - private void onUpEventInternal() { + private void onUpEventInternal(int x, int y, long eventTime) { mTimerProxy.cancelKeyTimers(); mIsInSlidingKeyInput = false; // Release the last pressed key. @@ -678,6 +719,24 @@ public class PointerTracker { mDrawingProxy.dismissMoreKeysPanel(); mIsShowingMoreKeysPanel = false; } + + if (sGestureTracker.isInGesture()) { + // Register up event on gesture tracker. + sGestureTracker.onUpEvent(this, x, y, eventTime); + if (!sPointerTrackerQueue.isAnyInSlidingKeyInput()) { + // TODO: Calls to beginBatchInput() is missing in this class. Reorganize the code. + sGestureTracker.endBatchInput(); + } + if (mCurrentKey != null) { + callListenerOnRelease(mCurrentKey, mCurrentKey.mCode, true); + } + mCurrentKey = null; + return; + } else { + // TODO: Calls to beginBatchInput() is missing in this class. Reorganize the code. + sGestureTracker.endBatchInput(); + } + if (mKeyAlreadyProcessed) return; if (mCurrentKey != null && !mCurrentKey.isRepeatable()) { @@ -689,6 +748,8 @@ public class PointerTracker { onLongPressed(); onDownEvent(x, y, SystemClock.uptimeMillis(), handler); mIsShowingMoreKeysPanel = true; + // TODO: Calls to beginBatchInput() is missing in this class. Reorganize the code. + sGestureTracker.abortBatchInput(); } public void onLongPressed() { @@ -723,7 +784,7 @@ public class PointerTracker { } private void startRepeatKey(Key key) { - if (key != null && key.isRepeatable()) { + if (key != null && key.isRepeatable() && !sGestureTracker.isInGesture()) { onRegisterKey(key); mTimerProxy.startKeyRepeatTimer(this); } @@ -753,7 +814,7 @@ public class PointerTracker { } private void startLongPressTimer(Key key) { - if (key != null && key.isLongPressEnabled()) { + if (key != null && key.isLongPressEnabled() && !sGestureTracker.isInGesture()) { mTimerProxy.startLongPressTimer(this); } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureTracker.java b/java/src/com/android/inputmethod/keyboard/internal/GestureTracker.java new file mode 100644 index 000000000..f98911445 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureTracker.java @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import android.util.Log; +import android.util.SparseArray; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.KeyboardActionListener; +import com.android.inputmethod.keyboard.PointerTracker; +import com.android.inputmethod.latin.InputPointers; +import com.android.inputmethod.latin.SuggestedWords; + +// TODO: Remove this class by consolidating with PointerTracker +public class GestureTracker { + private static final String TAG = GestureTracker.class.getSimpleName(); + private static final boolean DEBUG_LISTENER = false; + + // TODO: There should be an option to turn on/off the gesture input. + private static final boolean GESTURE_ON = true; + + private static final GestureTracker sInstance = new GestureTracker(); + + private static final int MIN_RECOGNITION_TIME = 100; + private static final int MIN_GESTURE_DURATION = 200; + + private static final float GESTURE_RECOG_SPEED_THRESHOLD = 0.4f; + private static final float SQUARED_GESTURE_RECOG_SPEED_THRESHOLD = + GESTURE_RECOG_SPEED_THRESHOLD * GESTURE_RECOG_SPEED_THRESHOLD; + private static final float GESTURE_RECOG_CURVATURE_THRESHOLD = (float) (Math.PI / 4); + + private boolean mIsAlphabetKeyboard; + private boolean mIsPossibleGesture = false; + private boolean mInGesture = false; + + private KeyboardActionListener mListener; + private SuggestedWords mSuggestions; + + private final SparseArray mGestureStrokes = new SparseArray(); + + private int mLastRecognitionPointSize = 0; + private long mLastRecognitionTime = 0; + + public static void init(KeyboardActionListener listner) { + sInstance.mListener = listner; + } + + public static GestureTracker getInstance() { + return sInstance; + } + + private GestureTracker() { + } + + public void setKeyboard(Keyboard keyboard) { + mIsAlphabetKeyboard = keyboard.mId.isAlphabetKeyboard(); + GestureStroke.setGestureSampleLength(keyboard.mMostCommonKeyWidth / 2, + keyboard.mMostCommonKeyHeight / 6); + } + + private void startBatchInput() { + if (DEBUG_LISTENER) { + Log.d(TAG, "onStartBatchInput"); + } + mInGesture = true; + mListener.onStartBatchInput(); + mSuggestions = null; + } + + // TODO: The corresponding startBatchInput() is a private method. Reorganize the code. + public void endBatchInput() { + if (isInGesture() && mSuggestions != null && mSuggestions.size() > 0) { + final CharSequence text = mSuggestions.getWord(0); + if (DEBUG_LISTENER) { + Log.d(TAG, "onEndBatchInput: text=" + text); + } + mListener.onEndBatchInput(text); + } + mInGesture = false; + clearBatchInputPoints(); + } + + public void abortBatchInput() { + mIsPossibleGesture = false; + mInGesture = false; + } + + public boolean isInGesture() { + return mInGesture; + } + + public void onDownEvent(PointerTracker tracker, int x, int y, long eventTime, Key key) { + mIsPossibleGesture = false; + if (GESTURE_ON && mIsAlphabetKeyboard && key != null && !key.isModifier()) { + mIsPossibleGesture = true; + addPointToStroke(x, y, 0, tracker.mPointerId, false); + } + } + + public void onMoveEvent(PointerTracker tracker, int x, int y, long eventTime, + boolean isHistorical, Key key) { + final int gestureTime = (int)(eventTime - tracker.getDownTime()); + if (GESTURE_ON && mIsPossibleGesture) { + final GestureStroke stroke = addPointToStroke(x, y, gestureTime, tracker.mPointerId, + isHistorical); + if (!isInGesture() && stroke.isStartOfAGesture(gestureTime)) { + startBatchInput(); + } + } + + if (key != null && isInGesture()) { + final InputPointers batchPoints = getIncrementalBatchPoints(); + if (updateBatchInputRecognitionState(eventTime, batchPoints.getPointerSize())) { + if (DEBUG_LISTENER) { + Log.d(TAG, "onUpdateBatchInput: batchPoints=" + batchPoints.getPointerSize()); + } + mSuggestions = mListener.onUpdateBatchInput(batchPoints); + } + } + } + + public void onUpEvent(PointerTracker tracker, int x, int y, long eventTime) { + if (isInGesture()) { + final InputPointers batchPoints = getAllBatchPoints(); + if (DEBUG_LISTENER) { + Log.d(TAG, "onUpdateBatchInput: batchPoints=" + batchPoints.getPointerSize()); + } + mSuggestions = mListener.onUpdateBatchInput(batchPoints); + } + } + + private GestureStroke addPointToStroke(int x, int y, int time, int pointerId, + boolean isHistorical) { + GestureStroke stroke = mGestureStrokes.get(pointerId); + if (stroke == null) { + stroke = new GestureStroke(pointerId); + mGestureStrokes.put(pointerId, stroke); + } + stroke.addPoint(x, y, time, isHistorical); + return stroke; + } + + // The working and return object of the following methods, {@link #getIncrementalBatchPoints()} + // and {@link #getAllBatchPoints()}. + private final InputPointers mAggregatedPointers = new InputPointers(); + + private InputPointers getIncrementalBatchPoints() { + final InputPointers pointers = mAggregatedPointers; + pointers.reset(); + final int strokeSize = mGestureStrokes.size(); + for (int index = 0; index < strokeSize; index++) { + final GestureStroke stroke = mGestureStrokes.valueAt(index); + stroke.appendIncrementalBatchPoints(pointers); + } + return pointers; + } + + private InputPointers getAllBatchPoints() { + final InputPointers pointers = mAggregatedPointers; + pointers.reset(); + final int strokeSize = mGestureStrokes.size(); + for (int index = 0; index < strokeSize; index++) { + final GestureStroke stroke = mGestureStrokes.valueAt(index); + stroke.appendAllBatchPoints(pointers); + } + return pointers; + } + + private void clearBatchInputPoints() { + final int strokeSize = mGestureStrokes.size(); + for (int index = 0; index < strokeSize; index++) { + final GestureStroke stroke = mGestureStrokes.valueAt(index); + stroke.reset(); + } + mLastRecognitionPointSize = 0; + mLastRecognitionTime = 0; + } + + private boolean updateBatchInputRecognitionState(long eventTime, int size) { + if (size > mLastRecognitionPointSize + && eventTime > mLastRecognitionTime + MIN_RECOGNITION_TIME) { + mLastRecognitionPointSize = size; + mLastRecognitionTime = eventTime; + return true; + } + return false; + } + + private static class GestureStroke { + private final int mPointerId; + private final InputPointers mInputPointers = new InputPointers(); + private float mLength; + private float mAngle; + private int mIncrementalRecognitionPoint; + private boolean mHasSharpCorner; + private long mLastPointTime; + private int mLastPointX; + private int mLastPointY; + + private static int sMinGestureLength; + private static int sSquaredGestureSampleLength; + + private static final float DOUBLE_PI = (float)(2 * Math.PI); + + public static void setGestureSampleLength(final int minGestureLength, + final int sampleLength) { + sMinGestureLength = minGestureLength; + sSquaredGestureSampleLength = sampleLength * sampleLength; + } + + public GestureStroke(int pointerId) { + mPointerId = pointerId; + reset(); + } + + public boolean isStartOfAGesture(int downDuration) { + return downDuration > MIN_GESTURE_DURATION / 2 && mLength > sMinGestureLength / 2; + } + + public void reset() { + mLength = 0; + mAngle = 0; + mIncrementalRecognitionPoint = 0; + mHasSharpCorner = false; + mLastPointTime = 0; + mInputPointers.reset(); + } + + private void updateLastPoint(final int x, final int y, final int time) { + mLastPointTime = time; + mLastPointX = x; + mLastPointY = y; + } + + public void addPoint(final int x, final int y, final int time, final boolean isHistorical) { + final int size = mInputPointers.getPointerSize(); + if (size == 0) { + mInputPointers.addPointer(x, y, mPointerId, time); + if (!isHistorical) { + updateLastPoint(x, y, time); + } + return; + } + + final int[] xCoords = mInputPointers.getXCoordinates(); + final int[] yCoords = mInputPointers.getYCoordinates(); + final int lastX = xCoords[size - 1]; + final int lastY = yCoords[size - 1]; + final float dist = squaredDistance(lastX, lastY, x, y); + if (dist > sSquaredGestureSampleLength) { + mInputPointers.addPointer(x, y, mPointerId, time); + mLength += dist; + final float angle = angle(lastX, lastY, x, y); + if (size > 1) { + float curvature = getAngleDiff(angle, mAngle); + if (curvature > GESTURE_RECOG_CURVATURE_THRESHOLD) { + if (size > mIncrementalRecognitionPoint) { + mIncrementalRecognitionPoint = size; + } + mHasSharpCorner = true; + } + if (!mHasSharpCorner) { + mIncrementalRecognitionPoint = size; + } + } + mAngle = angle; + } + + if (!isHistorical) { + final int duration = (int)(time - mLastPointTime); + if (mLastPointTime != 0 && duration > 0) { + final int squaredDuration = duration * duration; + final float squaredSpeed = + squaredDistance(mLastPointX, mLastPointY, x, y) / squaredDuration; + if (squaredSpeed < SQUARED_GESTURE_RECOG_SPEED_THRESHOLD) { + mIncrementalRecognitionPoint = size; + } + } + updateLastPoint(x, y, time); + } + } + + private float getAngleDiff(float a1, float a2) { + final float diff = Math.abs(a1 - a2); + if (diff > Math.PI) { + return DOUBLE_PI - diff; + } + return diff; + } + + public void appendAllBatchPoints(InputPointers out) { + out.append(mInputPointers, 0, mInputPointers.getPointerSize()); + } + + public void appendIncrementalBatchPoints(InputPointers out) { + out.append(mInputPointers, 0, mIncrementalRecognitionPoint); + } + } + + static float squaredDistance(int p1x, int p1y, int p2x, int p2y) { + final float dx = p1x - p2x; + final float dy = p1y - p2y; + return dx * dx + dy * dy; + } + + static float angle(int p1x, int p1y, int p2x, int p2y) { + final int dx = p1x - p2x; + final int dy = p1y - p2y; + if (dx == 0 && dy == 0) return 0; + return (float)Math.atan2(dy, dx); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java index d3bb85d4b..e4a71844a 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java +++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java @@ -31,6 +31,10 @@ public class PointerTrackerQueue { // TODO: Use ring buffer instead of {@link LinkedList}. private final LinkedList mQueue = new LinkedList(); + public int size() { + return mQueue.size(); + } + public synchronized void add(PointerTracker tracker) { mQueue.add(tracker); } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index f27d32150..518bcd5ce 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -1268,13 +1268,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (SPACE_STATE_PHANTOM == spaceState) { commitTyped(LastComposedWord.NOT_A_SEPARATOR); } + final int keyX, keyY; final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) { - handleCharacter(primaryCode, x, y, spaceState); + keyX = x; + keyY = y; } else { - handleCharacter(primaryCode, NOT_A_TOUCH_COORDINATE, NOT_A_TOUCH_COORDINATE, - spaceState); + keyX = NOT_A_TOUCH_COORDINATE; + keyY = NOT_A_TOUCH_COORDINATE; } + handleCharacter(primaryCode, keyX, keyY, spaceState); } mExpectingUpdateSelection = true; mShouldSwitchToLastSubtype = true; @@ -1320,10 +1323,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mSpaceState = SPACE_STATE_PHANTOM; } mConnection.endBatchEdit(); + // TODO: Should handle TextUtils.CAP_MODE_CHARACTER. + mWordComposer.setAutoCapitalized( + getCurrentAutoCapsState() != Constants.TextUtils.CAP_MODE_OFF); + } + + @Override + public SuggestedWords onUpdateBatchInput(InputPointers batchPointers) { + mWordComposer.setBatchInputPointers(batchPointers); + return updateSuggestionsOrPredictions(); } @Override public void onEndBatchInput(CharSequence text) { + mWordComposer.setBatchInputWord(text); mConnection.beginBatchEdit(); if (SPACE_STATE_PHANTOM == mSpaceState) { sendKeyCodePoint(Keyboard.CODE_SPACE); @@ -1669,7 +1682,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } // TODO: rename this method to updateSuggestionStrip or simply updateSuggestions - private void updateSuggestionsOrPredictions() { + private SuggestedWords updateSuggestionsOrPredictions() { mHandler.cancelUpdateSuggestionStrip(); // Check if we have a suggestion engine attached. @@ -1679,13 +1692,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen + "requested!"); mWordComposer.setAutoCorrection(mWordComposer.getTypedWord()); } - return; + return null; } final String typedWord = mWordComposer.getTypedWord(); if (!mWordComposer.isComposingWord() && !mCurrentSettings.mBigramPredictionEnabled) { setPunctuationSuggestions(); - return; + return null; } // Get the word on which we should search the bigrams. If we are composing a word, it's @@ -1701,6 +1714,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen suggestedWords = maybeRetrieveOlderSuggestions(typedWord, suggestedWords); showSuggestions(suggestedWords, typedWord); + return suggestedWords; } private SuggestedWords maybeRetrieveOlderSuggestions(final CharSequence typedWord, @@ -1761,9 +1775,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (mHandler.hasPendingUpdateSuggestions()) { updateSuggestionsOrPredictions(); } - final CharSequence autoCorrection = mWordComposer.getAutoCorrectionOrNull(); + final CharSequence typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull(); + final String typedWord = mWordComposer.getTypedWord(); + final CharSequence autoCorrection = (typedAutoCorrection != null) + ? typedAutoCorrection : typedWord; if (autoCorrection != null) { - final String typedWord = mWordComposer.getTypedWord(); if (TextUtils.isEmpty(typedWord)) { throw new RuntimeException("We have an auto-correction but the typed word " + "is empty? Impossible! I must commit suicide."); @@ -1808,7 +1824,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } mConnection.beginBatchEdit(); - if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0) { + if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0 + // In the batch input mode, a manually picked suggested word should just replace + // the current batch input text and there is no need for a phantom space. + && !mWordComposer.isBatchMode()) { int firstChar = Character.codePointAt(suggestion, 0); if ((!mCurrentSettings.isWeakSpaceStripper(firstChar)) && (!mCurrentSettings.isWeakSpaceSwapper(firstChar))) { diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index 31566bf13..598ef1de7 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -300,11 +300,27 @@ public class Suggest { final ArrayList suggestionsContainer = new ArrayList(suggestionsSet); + final int suggestionsCount = suggestionsContainer.size(); + final boolean isFirstCharCapitalized = wordComposer.isAutoCapitalized(); + // TODO: Handle the manual temporary shifted mode. + // TODO: Should handle TextUtils.CAP_MODE_CHARACTER. + final boolean isAllUpperCase = false; + if (isFirstCharCapitalized || isAllUpperCase) { + for (int i = 0; i < suggestionsCount; ++i) { + final SuggestedWordInfo wordInfo = suggestionsContainer.get(i); + final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo( + wordInfo, mLocale, isAllUpperCase, isFirstCharCapitalized, + 0 /* trailingSingleQuotesCount */); + suggestionsContainer.set(i, transformedWordInfo); + } + } SuggestedWordInfo.removeDups(suggestionsContainer); + // In the batch input mode, the most relevant suggested word should act as a "typed word" + // (typedWordValid=true), not as an "auto correct word" (willAutoCorrect=false). return new SuggestedWords(suggestionsContainer, true /* typedWordValid */, - true /* willAutoCorrect */, + false /* willAutoCorrect */, false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */, false /* isPrediction */); diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index 25e29008e..ca9dbaf05 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -130,8 +130,13 @@ public class WordComposer { if (newIndex < BinaryDictionary.MAX_WORD_LENGTH) { mPrimaryKeyCodes[newIndex] = primaryCode >= Keyboard.CODE_SPACE ? Character.toLowerCase(primaryCode) : primaryCode; - // TODO: Set correct pointer id and time - mInputPointers.addPointer(newIndex, keyX, keyY, 0, 0); + // In the batch input mode, the {@code mInputPointers} holds batch input points and + // shouldn't be overridden by the "typed key" coordinates + // (See {@link #setBatchInputWord}). + if (!mIsBatchMode) { + // TODO: Set correct pointer id and time + mInputPointers.addPointer(newIndex, keyX, keyY, 0, 0); + } } mIsFirstCharCapitalized = isFirstCharCapitalized( newIndex, primaryCode, mIsFirstCharCapitalized); @@ -144,12 +149,23 @@ public class WordComposer { mAutoCorrection = null; } - // TODO: We may want to have appendBatchInputPointers() as well. public void setBatchInputPointers(InputPointers batchPointers) { - mInputPointers.copy(batchPointers); + mInputPointers.set(batchPointers); mIsBatchMode = true; } + public void setBatchInputWord(CharSequence word) { + reset(); + mIsBatchMode = true; + final int length = word.length(); + for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) { + final int codePoint = Character.codePointAt(word, i); + // We don't want to override the batch input points that are held in mInputPointers + // (See {@link #add(int,int,int)}). + add(codePoint, NOT_A_COORDINATE, NOT_A_COORDINATE); + } + } + /** * Internal method to retrieve reasonable proximity info for a character. */ @@ -161,7 +177,7 @@ public class WordComposer { add(codePoint, x, y); return; } - add(codePoint, WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); + add(codePoint, NOT_A_COORDINATE, NOT_A_COORDINATE); } /**