Index: content/public/android/java/src/org/chromium/content/browser/input/ImeAdapter.java |
diff --git a/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapter.java b/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapter.java |
index e075d8e4d400dbef2f502c3f262b3455eed249a6..9a596088a15e4fabb2104f9857870955cfdec081 100644 |
--- a/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapter.java |
+++ b/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapter.java |
@@ -4,6 +4,9 @@ |
package org.chromium.content.browser.input; |
+import android.annotation.TargetApi; |
+import android.graphics.Matrix; |
+import android.os.Build; |
import android.os.Handler; |
import android.os.ResultReceiver; |
import android.os.SystemClock; |
@@ -15,14 +18,22 @@ import android.text.style.UnderlineSpan; |
import android.view.KeyCharacterMap; |
import android.view.KeyEvent; |
import android.view.View; |
+import android.view.inputmethod.CursorAnchorInfo; |
import android.view.inputmethod.EditorInfo; |
+import android.view.inputmethod.InputConnection; |
+import android.view.inputmethod.InputMethodManager; |
import org.chromium.base.CalledByNative; |
import org.chromium.base.JNINamespace; |
import org.chromium.base.VisibleForTesting; |
+import org.chromium.cc.SelectionBoundType; |
import org.chromium.ui.picker.InputDialogContainer; |
import java.lang.CharSequence; |
+import java.util.Arrays; |
+ |
+import javax.annotation.Nonnull; |
+import javax.annotation.Nullable; |
/** |
* Adapts and plumbs android IME service onto the chrome text input API. |
@@ -143,6 +154,302 @@ public class ImeAdapter { |
private int mTextInputFlags; |
private String mLastComposeText; |
+ /** |
+ * Minimum data set that is required to construct {@link CursorAnchorInfo} object. |
jdduke (slow)
2014/11/12 19:05:06
This is a non-trivial code addition, would it make
yukawa
2014/12/05 07:42:19
Sounds reasonable. Now the logic here is complete
|
+ */ |
+ private static final class CursorAnchorInfoSource { |
+ /** |
+ * Special sequence ID that is never returned from {@link #getSequenceNo()}. |
+ */ |
+ public static int INVALID_SEQUENCE_NO = 0; |
+ |
+ private boolean mHasFrameInfo = false; |
+ private String mText; |
+ private int mSelectionStart; |
+ private int mSelectionEnd; |
+ private int mCompositionStart; |
+ private int mCompositionEnd; |
+ private float[] mCompositionCharacterBounds; |
+ private boolean mHasInsertionMarker; |
+ private float mInsertionMarkerHorizontal; |
+ private float mInsertionMarkerTop; |
+ private float mInsertionMarkerBottom; |
+ private float mScale; |
+ private float mTranslationX; |
+ private float mTranslationY; |
+ private int mSequenceNo = INVALID_SEQUENCE_NO + 1; |
+ |
+ |
+ /** |
+ * Resets the internal state. |
+ */ |
+ public void reset() { |
+ mHasFrameInfo = false; |
+ mText = null; |
+ mSelectionStart = -1; |
+ mSelectionEnd = -1; |
+ mCompositionStart = -1; |
+ mCompositionEnd = -1; |
+ mCompositionCharacterBounds = null; |
+ mHasInsertionMarker = false; |
+ mInsertionMarkerHorizontal = Float.NaN; |
+ mInsertionMarkerTop = Float.NaN; |
+ mInsertionMarkerBottom = Float.NaN; |
+ mScale = 1.0f; |
+ mTranslationX = 0.0f; |
+ mTranslationY = 0.0f; |
+ } |
+ |
+ /** |
+ * @return {@code true} if |
+ * {@link #setFrameInfo(float, float, float, int, float, float, float)} was called after |
+ * the object was instantiated or reinitialized by {@link #reset()}. |
+ */ |
jdduke (slow)
2015/01/21 19:16:42
Do these accessors need public visibility? Where a
|
+ public boolean hasFrameInfo() { |
+ return mHasFrameInfo; |
+ } |
+ |
+ /** |
+ * @return Current composing text (if any). {@code null}. |
+ */ |
+ @Nullable |
+ public CharSequence getComposingText() { |
+ if (mText == null) { |
+ return null; |
+ } |
+ if (0 <= mCompositionStart && mCompositionStart <= mText.length()) { |
+ return mText.subSequence(mCompositionStart, mCompositionEnd); |
+ } |
+ return ""; |
+ } |
+ |
+ /** |
+ * @return Selection start index (inclusive) that was specified in |
+ * {@link #setTextAndSelection(String, int, int, int, int)}. |
+ */ |
+ public int getSelectionStart() { |
+ return mSelectionStart; |
+ } |
+ |
+ /** |
+ * @return Selection end index (exclusive) that was specified in |
+ * {@link #setTextAndSelection(String, int, int, int, int)}. |
+ */ |
+ public int getSelectionEnd() { |
+ return mSelectionEnd; |
+ } |
+ |
+ /** |
+ * @return Composing text start index (inclusive) that was specified in |
+ * {@link #setTextAndSelection(String, int, int, int, int)}. |
+ */ |
+ public int getCompositionStart() { |
+ return mCompositionStart; |
+ } |
+ |
+ /** |
+ * @return Array of local coordinates for each character bounds in the composing text. |
+ */ |
+ @Nullable |
+ public float[] getCompositionCharacterBounds() { |
+ return mCompositionCharacterBounds; |
+ } |
+ |
+ /** |
+ * @return {@code true} if an insertion marker is displayed. |
+ */ |
+ public boolean hasInsertionMarker() { |
+ return mHasInsertionMarker; |
+ } |
+ |
+ /** |
+ * @return Horizontal position of the text insertion marker, in local coordinates. |
+ */ |
+ public float getInsertionMarkerHorizontal() { |
+ return mInsertionMarkerHorizontal; |
+ } |
+ |
+ /** |
+ * @return Top position of the text insertion marker, in local coordinates. |
+ */ |
+ public float getInsertionMarkerTop() { |
+ return mInsertionMarkerTop; |
+ } |
+ |
+ /** |
+ * @return Bottom position of the text insertion marker, in local coordinates. |
+ */ |
+ public float getInsertionMarkerBottom() { |
+ return mInsertionMarkerBottom; |
+ } |
+ |
+ /** |
+ * @return Scaling factor from CSS (document) coordinates to Physical (screen) coordinates. |
+ */ |
+ public float getScale() { |
+ return mScale; |
+ } |
+ |
+ /** |
+ * @return Horizontal offset from CSS (document) coordinates to View-local Physical (screen) |
+ * coordinates. To obtain the display coordinates, add an offset returned from |
+ * {@link View#getLocationOnScreen(int[])}. |
+ */ |
+ public float getTranslationX() { |
+ return mTranslationX; |
+ } |
+ |
+ /** |
+ * @return Vertical offset from CSS (document) coordinates to View-local Physical (screen) |
+ * coordinates. To obtain the display coordinates, add an offset returned from |
+ * {@link View#getLocationOnScreen(int[])}. |
+ */ |
+ public float getTranslationY() { |
+ return mTranslationY; |
+ } |
+ |
+ /** |
+ * @return The sequence number that will be updated when any of the fields is changed. |
+ * Never returns {@link #INVALID_SEQUENCE_NO}. |
+ */ |
+ public int getSequenceNo() { |
+ return mSequenceNo; |
+ } |
+ |
+ private void incrementSequenceNo() { |
+ ++mSequenceNo; |
+ if (mSequenceNo == INVALID_SEQUENCE_NO) { |
+ ++mSequenceNo; |
+ } |
+ } |
+ |
+ /** |
+ * Sets positional information of composing text as an array of character bounds. |
+ * @param compositionCharacterBounds Array of character bounds in local coordinates. |
+ */ |
+ public void setCompositionCharacterBounds(float[] compositionCharacterBounds) { |
+ if (!Arrays.equals(compositionCharacterBounds, mCompositionCharacterBounds)) { |
+ incrementSequenceNo(); |
+ } |
+ mCompositionCharacterBounds = compositionCharacterBounds; |
+ } |
+ |
+ /** |
+ * Sets text in the focused text area, selection range, and the composing text range. |
+ * @param text Text in the focused text field. |
+ * @param selectionStart Index where the text selection starts. {@code -1} if there is no |
+ * selection. |
+ * @param selectionEnd Index where the text selection ends. {@code -1} if there is no |
+ * selection. |
+ * @param compositionStart Index where the text composition starts. {@code -1} if there is |
+ * no selection. |
+ * @param compositionEnd Index where the text composition ends. {@code -1} if there is no |
+ * selection. |
+ */ |
+ public void setTextAndSelection(String text, int selectionStart, int selectionEnd, |
+ int compositionStart, int compositionEnd) { |
+ if (text != mText || selectionStart != mSelectionStart || selectionEnd != mSelectionEnd |
+ || compositionStart != mCompositionStart || compositionEnd != mCompositionEnd) { |
+ incrementSequenceNo(); |
+ } |
+ mText = text; |
+ mSelectionStart = selectionStart; |
+ mSelectionEnd = selectionEnd; |
+ mCompositionStart = compositionStart; |
+ mCompositionEnd = compositionEnd; |
+ } |
+ |
+ /** |
+ * Sets coordinates system parameters and selection marker information. |
+ * @param scale Scaling factor from CSS (document) coordinates to Physical (screen) |
+ * coordinates. |
+ * @param translationX Horizontal offset from CSS (document) coordinates to |
+ * View-local Physical (screen) coordinates. |
+ * @param translationY Vertical offset from CSS (document) coordinates to View-local |
+ * Physical (screen) coordinates. |
+ * @param frameMetadataSelectionStartType Type code that is defined in |
+ * {@link SelectionBoundType} |
+ * @param frameMetadataSelectionStartEdgeTopX X coordinate of the top of the first selection |
+ * marker. |
+ * @param frameMetadataSelectionStartEdgeTopY Y coordinate of the top of the first selection |
+ * marker. |
+ * @param frameMetadataSelectionStartEdgeBottomY Y coordinate of the bottom of the first |
+ * selection marker. |
+ */ |
+ public void setFrameInfo(float scale, float translationX, float translationY, |
+ int frameMetadataSelectionStartType, float frameMetadataSelectionStartEdgeTopX, |
+ float frameMetadataSelectionStartEdgeTopY, |
+ float frameMetadataSelectionStartEdgeBottomY) { |
+ // We are interested only in zero width selection bounds here because non-zero width |
+ // selection bounds cannot be represented in CursorAnchorInfo API in Android Framework |
+ // as of API Level 21. Actually supporting non-zero width selection bounds in |
+ // CursorAnchorInfo API was once considered in the design phase of that API, but the |
+ // idea was abandoned because the IME is still able to retrieve the same information |
+ // from the following parameters in |
+ // CursorAnchorInfo: |
+ // - CursorAnchorInfo#getCharacterBounds and |
+ // - CursorAnchorInfo#getSelection{Start, End}. |
+ final boolean hasInsertionMarker = |
+ frameMetadataSelectionStartType == SelectionBoundType.SELECTION_BOUND_CENTER; |
+ if (scale != mScale || translationX != mTranslationX || translationY != mTranslationY) { |
+ incrementSequenceNo(); |
+ } else if (hasInsertionMarker) { |
+ if (!mHasInsertionMarker |
+ || frameMetadataSelectionStartEdgeTopX != mInsertionMarkerHorizontal |
+ || frameMetadataSelectionStartEdgeTopY != mInsertionMarkerTop |
+ || frameMetadataSelectionStartEdgeBottomY != mInsertionMarkerBottom) { |
+ incrementSequenceNo(); |
+ } |
+ } else if (mHasInsertionMarker) { |
+ incrementSequenceNo(); |
+ } |
+ mHasInsertionMarker = hasInsertionMarker; |
+ if (mHasInsertionMarker) { |
+ mInsertionMarkerHorizontal = frameMetadataSelectionStartEdgeTopX; |
+ mInsertionMarkerTop = frameMetadataSelectionStartEdgeTopY; |
+ mInsertionMarkerBottom = frameMetadataSelectionStartEdgeBottomY; |
+ } |
+ mScale = scale; |
+ mTranslationX = translationX; |
+ mTranslationY = translationY; |
+ mHasFrameInfo = true; |
+ } |
+ } |
+ private CursorAnchorInfoSource mCursorAnchorInfoSource = new CursorAnchorInfoSource(); |
+ |
+ private static class CursorAnchorInfoBuilderWrapper { |
+ |
+ public static boolean isSupported() { |
+ return false; |
+ } |
+ |
+ public static CursorAnchorInfoBuilderWrapper create() { |
+ return new CursorAnchorInfoBuilderWrapper(); |
+ } |
+ |
+ protected CursorAnchorInfoBuilderWrapper() { |
+ } |
+ |
+ /** |
+ * Calls {@link InputConnection#updateCursorAnchorInfo(View, CursorAnchorInfo)} iff the API |
+ * is available and the current IME actually requested it. |
+ * @param inputMethodManagerWrapper Compatibility wrapper of {@link InputConnection}. |
+ * @param source Data source from which {@link CursorAnchorInfo} is created. |
+ * @param view {@link View} object that should be passed to |
+ * {@link InputConnection#updateCursorAnchorInfo(View, CursorAnchorInfo)}. |
+ */ |
+ @SuppressWarnings("unused") |
+ public void send(@Nonnull InputMethodManagerWrapper inputMethodManagerWrapper, |
jdduke (slow)
2015/01/21 19:16:41
I don't quite understand this function?
|
+ @Nonnull CursorAnchorInfoSource source, @Nonnull View view) { |
+ // This is a stub for platforms that do not support CursorAnchorInfo. |
+ } |
+ } |
+ |
+ private final CursorAnchorInfoBuilderWrapper mCursorAnchorInfoBuilder = |
+ CursorAnchorInfoBuilderWrapper.create(); |
+ private boolean mCursorAnchorInfoMonitorEnabled = false; |
+ private boolean mOneshotUpdateCursorAnchorInfoEnabled = false; |
+ |
@VisibleForTesting |
int mLastSyntheticKeyCode; |
@@ -621,6 +928,75 @@ public class ImeAdapter { |
return true; |
} |
+ /** |
+ * @return Whether the {@link CursorAnchorInfo} is available or not on this device. |
+ */ |
+ public boolean isCursorAnchorInfoSupported() { |
+ return CursorAnchorInfoBuilderWrapper.isSupported(); |
+ } |
+ |
+ /** |
+ * Start or stop calling |
+ * {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}. |
+ * @param enabled {@code true} if {@link InputConnection#requestCursorUpdates(int)} is called |
+ * with {@link InputConnection#CURSOR_UPDATE_MONITOR} bit. |
+ */ |
+ public void setCursorAnchorInfoMonitorEnabled(boolean enabled) { |
+ mCursorAnchorInfoMonitorEnabled = enabled; |
+ } |
+ |
+ /** |
+ * Make sure {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)} is called |
+ * at least once immediately when the latest position becomes available. |
+ * @param view The view to be passed to |
+ * {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)} if the latest |
+ * position is already available. Otherwise ignored. |
+ */ |
+ public void scheduleOneshotUpdateCursorAnchorInfo(View view) { |
+ if (!mCursorAnchorInfoSource.hasFrameInfo()) { |
jdduke (slow)
2015/01/21 19:16:41
Where is this method called?
|
+ // If there is no frame info, send the data later when it becomes available. |
+ mOneshotUpdateCursorAnchorInfoEnabled = true; |
+ return; |
+ } |
+ mCursorAnchorInfoBuilder.send(mInputMethodManagerWrapper, mCursorAnchorInfoSource, |
+ mViewEmbedder.getAttachedView()); |
+ mOneshotUpdateCursorAnchorInfoEnabled = false; |
+ } |
+ |
+ /** |
+ * Update the parameters that are to be passed to the IME through |
+ * {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}. |
+ * @param text Text in the focused text field. |
+ * @param selectionStart Index where the text selection starts. {@code -1} if there is no |
+ * selection. |
+ * @param selectionEnd Index where the text selection ends. {@code -1} if there is no |
+ * selection. |
+ * @param compositionStart Index where the text composition starts. {@code -1} if there is no |
+ * selection. |
+ * @param compositionEnd Index where the text composition ends. {@code -1} if there is no |
+ * selection. |
+ */ |
+ public void updateTextAndSelection(String text, int selectionStart, int selectionEnd, |
+ int compositionStart, int compositionEnd) { |
+ mCursorAnchorInfoSource.setTextAndSelection(text, selectionStart, selectionEnd, |
+ compositionStart, compositionEnd); |
+ } |
+ |
+ public void onUpdateFrameInfo(float scale, float translationX, float translationY, |
+ int frameMetadataSelectionStartType, float frameMetadataSelectionStartEdgeTopX, |
+ float frameMetadataSelectionStartEdgeTopY, |
+ float frameMetadataSelectionStartEdgeBottomY) { |
+ mCursorAnchorInfoSource.setFrameInfo(scale, translationX, translationY, |
+ frameMetadataSelectionStartType, frameMetadataSelectionStartEdgeTopX, |
+ frameMetadataSelectionStartEdgeTopY, frameMetadataSelectionStartEdgeBottomY); |
+ if (!mCursorAnchorInfoMonitorEnabled && !mOneshotUpdateCursorAnchorInfoEnabled) { |
+ return; |
+ } |
+ mCursorAnchorInfoBuilder.send(mInputMethodManagerWrapper, mCursorAnchorInfoSource, |
+ mViewEmbedder.getAttachedView()); |
+ mOneshotUpdateCursorAnchorInfoEnabled = false; |
+ } |
+ |
// Calls from C++ to Java |
@CalledByNative |
@@ -699,7 +1075,7 @@ public class ImeAdapter { |
@CalledByNative |
private void setCharacterBounds(float[] characterBounds) { |
- // TODO(yukawa): Implement this. |
+ mCursorAnchorInfoSource.setCompositionCharacterBounds(characterBounds); |
} |
@CalledByNative |
@@ -710,6 +1086,7 @@ public class ImeAdapter { |
} |
mNativeImeAdapterAndroid = 0; |
mTextInputType = 0; |
+ mCursorAnchorInfoSource.reset(); |
} |
private native boolean nativeSendSyntheticKeyEvent(long nativeImeAdapterAndroid, |