Merging minimal gesture input

Change-Id: Iee6ae48bb6309c2867b5d2e344fe7d86dfabd654
main
Tom Ouyang 2012-06-12 03:40:37 -07:00 committed by Tadashi G. Takaoka
parent e9808694fe
commit eea34598bf
7 changed files with 483 additions and 32 deletions

View File

@ -16,6 +16,9 @@
package com.android.inputmethod.keyboard; package com.android.inputmethod.keyboard;
import com.android.inputmethod.latin.InputPointers;
import com.android.inputmethod.latin.SuggestedWords;
public interface KeyboardActionListener { public interface KeyboardActionListener {
/** /**
@ -64,13 +67,18 @@ public interface KeyboardActionListener {
*/ */
public void onTextInput(CharSequence text); public void onTextInput(CharSequence text);
// TODO: Should move this method to some more appropriate interface.
/** /**
* Called when user started batch input. * Called when user started batch input.
*/ */
public void onStartBatchInput(); 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. * Sends a sequence of characters to the listener as batch input.
* *
@ -101,6 +109,8 @@ public interface KeyboardActionListener {
@Override @Override
public void onStartBatchInput() {} public void onStartBatchInput() {}
@Override @Override
public SuggestedWords onUpdateBatchInput(InputPointers batchPointers) { return null; }
@Override
public void onEndBatchInput(CharSequence text) {} public void onEndBatchInput(CharSequence text) {}
@Override @Override
public void onCancelInput() {} public void onCancelInput() {}

View File

@ -22,6 +22,7 @@ import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import com.android.inputmethod.keyboard.internal.GestureTracker;
import com.android.inputmethod.keyboard.internal.PointerTrackerQueue; import com.android.inputmethod.keyboard.internal.PointerTrackerQueue;
import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.ResearchLogger; import com.android.inputmethod.latin.ResearchLogger;
@ -161,6 +162,9 @@ public class PointerTracker {
private static final KeyboardActionListener EMPTY_LISTENER = private static final KeyboardActionListener EMPTY_LISTENER =
new KeyboardActionListener.Adapter(); new KeyboardActionListener.Adapter();
// Gesture tracker singleton instance
private static final GestureTracker sGestureTracker = GestureTracker.getInstance();
public static void init(boolean hasDistinctMultitouch, public static void init(boolean hasDistinctMultitouch,
boolean needsPhantomSuddenMoveEventHack) { boolean needsPhantomSuddenMoveEventHack) {
if (hasDistinctMultitouch) { if (hasDistinctMultitouch) {
@ -199,6 +203,7 @@ public class PointerTracker {
for (final PointerTracker tracker : sTrackers) { for (final PointerTracker tracker : sTrackers) {
tracker.mListener = listener; tracker.mListener = listener;
} }
GestureTracker.init(listener);
} }
public static void setKeyDetector(KeyDetector keyDetector) { public static void setKeyDetector(KeyDetector keyDetector) {
@ -207,6 +212,7 @@ public class PointerTracker {
// Mark that keyboard layout has been changed. // Mark that keyboard layout has been changed.
tracker.mKeyboardLayoutHasBeenChanged = true; tracker.mKeyboardLayoutHasBeenChanged = true;
} }
sGestureTracker.setKeyboard(keyDetector.getKeyboard());
} }
public static void dismissAllKeyPreviews() { public static void dismissAllKeyPreviews() {
@ -233,6 +239,9 @@ public class PointerTracker {
// Returns true if keyboard has been changed by this callback. // Returns true if keyboard has been changed by this callback.
private boolean callListenerOnPressAndCheckKeyboardLayoutChange(Key key) { private boolean callListenerOnPressAndCheckKeyboardLayoutChange(Key key) {
if (sGestureTracker.isInGesture()) {
return false;
}
final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier(); final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier();
if (DEBUG_LISTENER) { if (DEBUG_LISTENER) {
Log.d(TAG, "onPress : " + KeyDetector.printableCode(key) 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 // Note that we need primaryCode argument because the keyboard may in shifted state and the
// primaryCode is different from {@link Key#mCode}. // primaryCode is different from {@link Key#mCode}.
private void callListenerOnRelease(Key key, int primaryCode, boolean withSliding) { private void callListenerOnRelease(Key key, int primaryCode, boolean withSliding) {
if (sGestureTracker.isInGesture()) {
return;
}
final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier(); final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier();
if (DEBUG_LISTENER) { if (DEBUG_LISTENER) {
Log.d(TAG, "onRelease : " + Keyboard.printableCode(primaryCode) Log.d(TAG, "onRelease : " + Keyboard.printableCode(primaryCode)
@ -386,7 +398,7 @@ public class PointerTracker {
return; return;
} }
if (!key.noKeyPreview()) { if (!key.noKeyPreview() && !sGestureTracker.isInGesture()) {
mDrawingProxy.showKeyPreview(this); mDrawingProxy.showKeyPreview(this);
} }
updatePressKeyGraphics(key); updatePressKeyGraphics(key);
@ -504,8 +516,8 @@ public class PointerTracker {
} }
final PointerTrackerQueue queue = sPointerTrackerQueue; final PointerTrackerQueue queue = sPointerTrackerQueue;
final Key key = getKeyOn(x, y);
if (queue != null) { if (queue != null) {
final Key key = getKeyOn(x, y);
if (key != null && key.isModifier()) { if (key != null && key.isModifier()) {
// Before processing a down event of modifier key, all pointers already being // Before processing a down event of modifier key, all pointers already being
// tracked should be released. // tracked should be released.
@ -514,6 +526,9 @@ public class PointerTracker {
queue.add(this); queue.add(this);
} }
onDownEventInternal(x, y, eventTime); 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) { private void onDownEventInternal(int x, int y, long eventTime) {
@ -554,10 +569,34 @@ public class PointerTracker {
if (mKeyAlreadyProcessed) if (mKeyAlreadyProcessed)
return; 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 lastX = mLastX;
final int lastY = mLastY; final int lastY = mLastY;
final Key oldKey = mCurrentKey; final Key oldKey = mCurrentKey;
Key key = onMoveKey(x, y); 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 (key != null) {
if (oldKey == null) { if (oldKey == null) {
// The pointer has been slid in to the new key, but the finger was not on any keys. // 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) { if (ProductionFlag.IS_EXPERIMENTAL) {
ResearchLogger.pointerTracker_onMoveEvent(x, y, lastX, lastY); ResearchLogger.pointerTracker_onMoveEvent(x, y, lastX, lastY);
} }
onUpEventInternal(); onUpEventInternal(x, y, eventTime);
onDownEventInternal(x, y, eventTime); onDownEventInternal(x, y, eventTime);
} else { } else {
// HACK: If there are currently multiple touches, register the key even if // HACK: If there are currently multiple touches, register the key even if
@ -617,7 +656,7 @@ public class PointerTracker {
// this hack. // this hack.
if (me != null && me.getPointerCount() > 1 if (me != null && me.getPointerCount() > 1
&& !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) { && !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) {
onUpEventInternal(); onUpEventInternal(x, y, eventTime);
} }
mKeyAlreadyProcessed = true; mKeyAlreadyProcessed = true;
setReleasedKeyGraphics(oldKey); setReleasedKeyGraphics(oldKey);
@ -647,16 +686,18 @@ public class PointerTracker {
final PointerTrackerQueue queue = sPointerTrackerQueue; final PointerTrackerQueue queue = sPointerTrackerQueue;
if (queue != null) { if (queue != null) {
if (mCurrentKey != null && mCurrentKey.isModifier()) { if (!sGestureTracker.isInGesture()) {
// Before processing an up event of modifier key, all pointers already being if (mCurrentKey != null && mCurrentKey.isModifier()) {
// tracked should be released. // Before processing an up event of modifier key, all pointers already being
queue.releaseAllPointersExcept(this, eventTime); // tracked should be released.
} else { queue.releaseAllPointersExcept(this, eventTime);
queue.releaseAllPointersOlderThan(this, eventTime); } else {
queue.releaseAllPointersOlderThan(this, eventTime);
}
} }
queue.remove(this); 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. // 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) { public void onPhantomUpEvent(int x, int y, long eventTime) {
if (DEBUG_EVENT) if (DEBUG_EVENT)
printTouchEvent("onPhntEvent:", x, y, eventTime); printTouchEvent("onPhntEvent:", x, y, eventTime);
onUpEventInternal(); onUpEventInternal(x, y, eventTime);
mKeyAlreadyProcessed = true; mKeyAlreadyProcessed = true;
} }
private void onUpEventInternal() { private void onUpEventInternal(int x, int y, long eventTime) {
mTimerProxy.cancelKeyTimers(); mTimerProxy.cancelKeyTimers();
mIsInSlidingKeyInput = false; mIsInSlidingKeyInput = false;
// Release the last pressed key. // Release the last pressed key.
@ -678,6 +719,24 @@ public class PointerTracker {
mDrawingProxy.dismissMoreKeysPanel(); mDrawingProxy.dismissMoreKeysPanel();
mIsShowingMoreKeysPanel = false; 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) if (mKeyAlreadyProcessed)
return; return;
if (mCurrentKey != null && !mCurrentKey.isRepeatable()) { if (mCurrentKey != null && !mCurrentKey.isRepeatable()) {
@ -689,6 +748,8 @@ public class PointerTracker {
onLongPressed(); onLongPressed();
onDownEvent(x, y, SystemClock.uptimeMillis(), handler); onDownEvent(x, y, SystemClock.uptimeMillis(), handler);
mIsShowingMoreKeysPanel = true; mIsShowingMoreKeysPanel = true;
// TODO: Calls to beginBatchInput() is missing in this class. Reorganize the code.
sGestureTracker.abortBatchInput();
} }
public void onLongPressed() { public void onLongPressed() {
@ -723,7 +784,7 @@ public class PointerTracker {
} }
private void startRepeatKey(Key key) { private void startRepeatKey(Key key) {
if (key != null && key.isRepeatable()) { if (key != null && key.isRepeatable() && !sGestureTracker.isInGesture()) {
onRegisterKey(key); onRegisterKey(key);
mTimerProxy.startKeyRepeatTimer(this); mTimerProxy.startKeyRepeatTimer(this);
} }
@ -753,7 +814,7 @@ public class PointerTracker {
} }
private void startLongPressTimer(Key key) { private void startLongPressTimer(Key key) {
if (key != null && key.isLongPressEnabled()) { if (key != null && key.isLongPressEnabled() && !sGestureTracker.isInGesture()) {
mTimerProxy.startLongPressTimer(this); mTimerProxy.startLongPressTimer(this);
} }
} }

View File

@ -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<GestureStroke> mGestureStrokes = new SparseArray<GestureStroke>();
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);
}
}

View File

@ -31,6 +31,10 @@ public class PointerTrackerQueue {
// TODO: Use ring buffer instead of {@link LinkedList}. // TODO: Use ring buffer instead of {@link LinkedList}.
private final LinkedList<PointerTracker> mQueue = new LinkedList<PointerTracker>(); private final LinkedList<PointerTracker> mQueue = new LinkedList<PointerTracker>();
public int size() {
return mQueue.size();
}
public synchronized void add(PointerTracker tracker) { public synchronized void add(PointerTracker tracker) {
mQueue.add(tracker); mQueue.add(tracker);
} }

View File

@ -1268,13 +1268,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (SPACE_STATE_PHANTOM == spaceState) { if (SPACE_STATE_PHANTOM == spaceState) {
commitTyped(LastComposedWord.NOT_A_SEPARATOR); commitTyped(LastComposedWord.NOT_A_SEPARATOR);
} }
final int keyX, keyY;
final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) { if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) {
handleCharacter(primaryCode, x, y, spaceState); keyX = x;
keyY = y;
} else { } else {
handleCharacter(primaryCode, NOT_A_TOUCH_COORDINATE, NOT_A_TOUCH_COORDINATE, keyX = NOT_A_TOUCH_COORDINATE;
spaceState); keyY = NOT_A_TOUCH_COORDINATE;
} }
handleCharacter(primaryCode, keyX, keyY, spaceState);
} }
mExpectingUpdateSelection = true; mExpectingUpdateSelection = true;
mShouldSwitchToLastSubtype = true; mShouldSwitchToLastSubtype = true;
@ -1320,10 +1323,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mSpaceState = SPACE_STATE_PHANTOM; mSpaceState = SPACE_STATE_PHANTOM;
} }
mConnection.endBatchEdit(); 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 @Override
public void onEndBatchInput(CharSequence text) { public void onEndBatchInput(CharSequence text) {
mWordComposer.setBatchInputWord(text);
mConnection.beginBatchEdit(); mConnection.beginBatchEdit();
if (SPACE_STATE_PHANTOM == mSpaceState) { if (SPACE_STATE_PHANTOM == mSpaceState) {
sendKeyCodePoint(Keyboard.CODE_SPACE); sendKeyCodePoint(Keyboard.CODE_SPACE);
@ -1669,7 +1682,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
} }
// TODO: rename this method to updateSuggestionStrip or simply updateSuggestions // TODO: rename this method to updateSuggestionStrip or simply updateSuggestions
private void updateSuggestionsOrPredictions() { private SuggestedWords updateSuggestionsOrPredictions() {
mHandler.cancelUpdateSuggestionStrip(); mHandler.cancelUpdateSuggestionStrip();
// Check if we have a suggestion engine attached. // Check if we have a suggestion engine attached.
@ -1679,13 +1692,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
+ "requested!"); + "requested!");
mWordComposer.setAutoCorrection(mWordComposer.getTypedWord()); mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
} }
return; return null;
} }
final String typedWord = mWordComposer.getTypedWord(); final String typedWord = mWordComposer.getTypedWord();
if (!mWordComposer.isComposingWord() && !mCurrentSettings.mBigramPredictionEnabled) { if (!mWordComposer.isComposingWord() && !mCurrentSettings.mBigramPredictionEnabled) {
setPunctuationSuggestions(); setPunctuationSuggestions();
return; return null;
} }
// Get the word on which we should search the bigrams. If we are composing a word, it's // 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); suggestedWords = maybeRetrieveOlderSuggestions(typedWord, suggestedWords);
showSuggestions(suggestedWords, typedWord); showSuggestions(suggestedWords, typedWord);
return suggestedWords;
} }
private SuggestedWords maybeRetrieveOlderSuggestions(final CharSequence typedWord, private SuggestedWords maybeRetrieveOlderSuggestions(final CharSequence typedWord,
@ -1761,9 +1775,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (mHandler.hasPendingUpdateSuggestions()) { if (mHandler.hasPendingUpdateSuggestions()) {
updateSuggestionsOrPredictions(); 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) { if (autoCorrection != null) {
final String typedWord = mWordComposer.getTypedWord();
if (TextUtils.isEmpty(typedWord)) { if (TextUtils.isEmpty(typedWord)) {
throw new RuntimeException("We have an auto-correction but the typed word " throw new RuntimeException("We have an auto-correction but the typed word "
+ "is empty? Impossible! I must commit suicide."); + "is empty? Impossible! I must commit suicide.");
@ -1808,7 +1824,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
} }
mConnection.beginBatchEdit(); 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); int firstChar = Character.codePointAt(suggestion, 0);
if ((!mCurrentSettings.isWeakSpaceStripper(firstChar)) if ((!mCurrentSettings.isWeakSpaceStripper(firstChar))
&& (!mCurrentSettings.isWeakSpaceSwapper(firstChar))) { && (!mCurrentSettings.isWeakSpaceSwapper(firstChar))) {

View File

@ -300,11 +300,27 @@ public class Suggest {
final ArrayList<SuggestedWordInfo> suggestionsContainer = final ArrayList<SuggestedWordInfo> suggestionsContainer =
new ArrayList<SuggestedWordInfo>(suggestionsSet); new ArrayList<SuggestedWordInfo>(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); 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, return new SuggestedWords(suggestionsContainer,
true /* typedWordValid */, true /* typedWordValid */,
true /* willAutoCorrect */, false /* willAutoCorrect */,
false /* isPunctuationSuggestions */, false /* isPunctuationSuggestions */,
false /* isObsoleteSuggestions */, false /* isObsoleteSuggestions */,
false /* isPrediction */); false /* isPrediction */);

View File

@ -130,8 +130,13 @@ public class WordComposer {
if (newIndex < BinaryDictionary.MAX_WORD_LENGTH) { if (newIndex < BinaryDictionary.MAX_WORD_LENGTH) {
mPrimaryKeyCodes[newIndex] = primaryCode >= Keyboard.CODE_SPACE mPrimaryKeyCodes[newIndex] = primaryCode >= Keyboard.CODE_SPACE
? Character.toLowerCase(primaryCode) : primaryCode; ? Character.toLowerCase(primaryCode) : primaryCode;
// TODO: Set correct pointer id and time // In the batch input mode, the {@code mInputPointers} holds batch input points and
mInputPointers.addPointer(newIndex, keyX, keyY, 0, 0); // 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( mIsFirstCharCapitalized = isFirstCharCapitalized(
newIndex, primaryCode, mIsFirstCharCapitalized); newIndex, primaryCode, mIsFirstCharCapitalized);
@ -144,12 +149,23 @@ public class WordComposer {
mAutoCorrection = null; mAutoCorrection = null;
} }
// TODO: We may want to have appendBatchInputPointers() as well.
public void setBatchInputPointers(InputPointers batchPointers) { public void setBatchInputPointers(InputPointers batchPointers) {
mInputPointers.copy(batchPointers); mInputPointers.set(batchPointers);
mIsBatchMode = true; 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. * Internal method to retrieve reasonable proximity info for a character.
*/ */
@ -161,7 +177,7 @@ public class WordComposer {
add(codePoint, x, y); add(codePoint, x, y);
return; return;
} }
add(codePoint, WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); add(codePoint, NOT_A_COORDINATE, NOT_A_COORDINATE);
} }
/** /**