| Index: content/public/android/java/src/org/chromium/content/browser/input/CursorAnchorInfoController.java
|
| diff --git a/content/public/android/java/src/org/chromium/content/browser/input/CursorAnchorInfoController.java b/content/public/android/java/src/org/chromium/content/browser/input/CursorAnchorInfoController.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..17c1bd5db04dd08c6ce7092736ab55e405c0f376
|
| --- /dev/null
|
| +++ b/content/public/android/java/src/org/chromium/content/browser/input/CursorAnchorInfoController.java
|
| @@ -0,0 +1,467 @@
|
| +// Copyright 2014 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +package org.chromium.content.browser.input;
|
| +
|
| +import android.graphics.Matrix;
|
| +import android.graphics.RectF;
|
| +import android.os.Build;
|
| +import android.text.TextUtils;
|
| +import android.view.View;
|
| +import android.view.inputmethod.CursorAnchorInfo;
|
| +import android.view.inputmethod.InputConnection;
|
| +
|
| +import org.chromium.base.CommandLine;
|
| +import org.chromium.base.VisibleForTesting;
|
| +import org.chromium.content.browser.RenderCoordinates;
|
| +import org.chromium.content.common.ContentSwitches;
|
| +
|
| +import java.util.Arrays;
|
| +
|
| +import javax.annotation.Nonnull;
|
| +import javax.annotation.Nullable;
|
| +
|
| +/*
|
| + * A state machine interface which receives Chromium internal events to determines when to call
|
| + * {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}. This interface is
|
| + * also used in unit tests to mock out {@link CursorAnchorInfo}, which is available only in
|
| + * Android 5.0 (Lollipop) and later.
|
| + */
|
| +final class CursorAnchorInfoController {
|
| + /**
|
| + * An interface to mock out {@link View#getLocationOnScreen(int[])} for testing.
|
| + */
|
| + public interface ViewDelegate {
|
| + void getLocationOnScreen(View view, int[] location);
|
| + }
|
| +
|
| + private static final class CursorAnchorInfoWrapperImpl implements CursorAnchorInfoWrapper {
|
| + private static final class Builder implements CursorAnchorInfoWrapper.Builder {
|
| + private final CursorAnchorInfo.Builder mBuilder = new CursorAnchorInfo.Builder();
|
| +
|
| + @Override
|
| + public CursorAnchorInfoWrapper.Builder setSelectionRange(int newStart, int newEnd) {
|
| + mBuilder.setSelectionRange(newStart, newEnd);
|
| + return this;
|
| + }
|
| +
|
| + @Override
|
| + public CursorAnchorInfoWrapper.Builder setComposingText(
|
| + int composingTextStart, CharSequence composingText) {
|
| + mBuilder.setComposingText(composingTextStart, composingText);
|
| + return this;
|
| + }
|
| +
|
| + @Override
|
| + public CursorAnchorInfoWrapper.Builder setInsertionMarkerLocation(
|
| + float horizontalPosition, float lineTop, float lineBaseline, float lineBottom,
|
| + int flags) {
|
| + mBuilder.setInsertionMarkerLocation(horizontalPosition, lineTop, lineBaseline,
|
| + lineBottom, flags);
|
| + return this;
|
| + }
|
| +
|
| + @Override
|
| + public CursorAnchorInfoWrapper.Builder addCharacterBounds(
|
| + int index, float left, float top, float right, float bottom, int flags) {
|
| + mBuilder.addCharacterBounds(index, left, top, right, bottom, flags);
|
| + return this;
|
| + }
|
| +
|
| + @Override
|
| + public CursorAnchorInfoWrapper.Builder setMatrix(Matrix matrix) {
|
| + mBuilder.setMatrix(matrix);
|
| + return this;
|
| + }
|
| +
|
| + @Override
|
| + public CursorAnchorInfoWrapper build() {
|
| + return new CursorAnchorInfoWrapperImpl(mBuilder.build());
|
| + }
|
| +
|
| + @Override
|
| + public void reset() {
|
| + mBuilder.reset();
|
| + }
|
| + }
|
| +
|
| + private final CursorAnchorInfo mObj;
|
| + public CursorAnchorInfoWrapperImpl(CursorAnchorInfo obj) {
|
| + mObj = obj;
|
| + }
|
| +
|
| + @Override
|
| + public int getSelectionStart() {
|
| + return mObj.getSelectionStart();
|
| + }
|
| +
|
| + @Override
|
| + public int getSelectionEnd() {
|
| + return mObj.getSelectionEnd();
|
| + }
|
| +
|
| + @Override
|
| + public int getComposingTextStart() {
|
| + return mObj.getComposingTextStart();
|
| + }
|
| +
|
| + @Override
|
| + public CharSequence getComposingText() {
|
| + return mObj.getComposingText();
|
| + }
|
| +
|
| + @Override
|
| + public int getInsertionMarkerFlags() {
|
| + return mObj.getInsertionMarkerFlags();
|
| + }
|
| +
|
| + @Override
|
| + public float getInsertionMarkerHorizontal() {
|
| + return mObj.getInsertionMarkerHorizontal();
|
| + }
|
| +
|
| + @Override
|
| + public float getInsertionMarkerTop() {
|
| + return mObj.getInsertionMarkerTop();
|
| + }
|
| +
|
| + @Override
|
| + public float getInsertionMarkerBaseline() {
|
| + return mObj.getInsertionMarkerBaseline();
|
| + }
|
| +
|
| + @Override
|
| + public float getInsertionMarkerBottom() {
|
| + return mObj.getInsertionMarkerBottom();
|
| + }
|
| +
|
| + @Override
|
| + public RectF getCharacterBounds(int index) {
|
| + return mObj.getCharacterBounds(index);
|
| + }
|
| +
|
| + @Override
|
| + public int getCharacterBoundsFlags(int index) {
|
| + return mObj.getCharacterBoundsFlags(index);
|
| + }
|
| +
|
| + @Override
|
| + public Matrix getMatrix() {
|
| + return mObj.getMatrix();
|
| + }
|
| +
|
| + @Override
|
| + public Object unwrap() {
|
| + return mObj;
|
| + }
|
| + }
|
| +
|
| + @Nullable
|
| + private CharSequence mText;
|
| + private int mSelectionStart;
|
| + private int mSelectionEnd;
|
| + private int mComposingTextStart;
|
| + private int mComposingTextEnd;
|
| + @Nullable
|
| + private float[] mCompositionCharacterBounds;
|
| + private boolean mHasInsertionMarker;
|
| + private boolean mIsInsertionMarkerVisible;
|
| + private float mInsertionMarkerHorizontal;
|
| + private float mInsertionMarkerTop;
|
| + private float mInsertionMarkerBottom;
|
| + private float mScale;
|
| + private float mTranslationX;
|
| + private float mTranslationY;
|
| + private boolean mIsEditable;
|
| + private boolean mHasPendingRequest;
|
| + private boolean mMonitorModeEnabled;
|
| + private boolean mHasCoordinateInfo;
|
| +
|
| + @Nonnull
|
| + private final CursorAnchorInfoWrapper.Builder mCursorAnchorInfoBuilder;
|
| + @Nullable
|
| + private volatile CursorAnchorInfoWrapper mLastCursorAnchorInfo;
|
| +
|
| + @Nonnull
|
| + private final Matrix mMatrix = new Matrix();
|
| + @Nonnull
|
| + private final int[] mViewOrigin = new int[2];
|
| + @Nonnull
|
| + private final ViewDelegate mViewDelegate;
|
| +
|
| + @Nullable
|
| + private InputMethodManagerWrapper mInputMethodManagerWrapper;
|
| +
|
| + private static final boolean sIsSupported = isSupportedInit();
|
| +
|
| + /**
|
| + * @return {@code true} if {@link CursorAnchorInfo} is supported on this device.
|
| + */
|
| + private static boolean isSupportedInit() {
|
| + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
| + return false;
|
| + if (CommandLine.getInstance() != null
|
| + && !CommandLine.getInstance().hasSwitch(ContentSwitches.ENABLE_CURSOR_ANCHOR_INFO))
|
| + return false;
|
| + return true;
|
| + }
|
| +
|
| + /**
|
| + * @return {@code true} if {@link CursorAnchorInfo} is supported on this device.
|
| + */
|
| + public static boolean isSupported() {
|
| + return sIsSupported;
|
| + }
|
| +
|
| + private CursorAnchorInfoController(InputMethodManagerWrapper inputMethodManagerWrapper,
|
| + CursorAnchorInfoWrapper.Builder builder,
|
| + ViewDelegate viewDelegate) {
|
| + mInputMethodManagerWrapper = inputMethodManagerWrapper;
|
| + mCursorAnchorInfoBuilder = builder;
|
| + mViewDelegate = viewDelegate;
|
| + }
|
| +
|
| + public static CursorAnchorInfoController create(
|
| + InputMethodManagerWrapper inputMethodManagerWrapper) {
|
| + return isSupported() ? new CursorAnchorInfoController(inputMethodManagerWrapper,
|
| + new CursorAnchorInfoWrapperImpl.Builder(), new ViewDelegate() {
|
| + @Override
|
| + public void getLocationOnScreen(View view, int[] location) {
|
| + view.getLocationOnScreen(location);
|
| + }
|
| + }) : null;
|
| + }
|
| +
|
| + @VisibleForTesting
|
| + public void setInputMethodManagerWrapper(InputMethodManagerWrapper inputMethodManagerWrapper) {
|
| + mInputMethodManagerWrapper = inputMethodManagerWrapper;
|
| + }
|
| +
|
| + @VisibleForTesting
|
| + public static CursorAnchorInfoController createForTest(
|
| + InputMethodManagerWrapper inputMethodManagerWrapper,
|
| + CursorAnchorInfoWrapper.Builder builder,
|
| + ViewDelegate viewDelegate) {
|
| + return new CursorAnchorInfoController(inputMethodManagerWrapper, builder, viewDelegate);
|
| + }
|
| +
|
| + /**
|
| + * @return Current composing text (if any). {@code null} otherwise.
|
| + */
|
| + @Nullable
|
| + public CharSequence getComposingText() {
|
| + if (mText == null)
|
| + return null;
|
| + if (0 <= mComposingTextStart && mComposingTextStart <= mText.length())
|
| + return mText.subSequence(mComposingTextStart, mComposingTextEnd);
|
| + return "";
|
| + }
|
| +
|
| + /**
|
| + * Updates text in the focused text area, selection range, and the composing text range.
|
| + * @param text Text in the focused text field.
|
| + * @param composingTextStart Index where the text composition starts. {@code -1} if there is
|
| + * no selection.
|
| + * @param composingTextEnd Index where the text composition ends. {@code -1} if there is no
|
| + * selection.
|
| + * @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.
|
| + */
|
| + public void updateTextAndSelection(CharSequence text, int composingTextStart,
|
| + int composingTextEnd, int selectionStart, int selectionEnd) {
|
| + if (!mIsEditable)
|
| + return;
|
| + if (mLastCursorAnchorInfo == null && mMonitorModeEnabled)
|
| + mHasPendingRequest = true;
|
| + if (mLastCursorAnchorInfo != null && !mHasPendingRequest) {
|
| + if (!TextUtils.equals(text, mText) || selectionStart != mSelectionStart
|
| + || selectionEnd != mSelectionEnd || composingTextStart != mComposingTextStart
|
| + || composingTextEnd != mComposingTextEnd) {
|
| + mLastCursorAnchorInfo = null;
|
| + if (mMonitorModeEnabled) {
|
| + mHasPendingRequest = true;
|
| + }
|
| + }
|
| + }
|
| + mText = text;
|
| + mSelectionStart = selectionStart;
|
| + mSelectionEnd = selectionEnd;
|
| + mComposingTextStart = composingTextStart;
|
| + mComposingTextEnd = composingTextEnd;
|
| + }
|
| +
|
| + /**
|
| + * 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 (!mIsEditable)
|
| + return;
|
| + if (mLastCursorAnchorInfo == null && mMonitorModeEnabled)
|
| + mHasPendingRequest = true;
|
| + if (mLastCursorAnchorInfo != null && !mHasPendingRequest) {
|
| + if (!Arrays.equals(compositionCharacterBounds, mCompositionCharacterBounds)) {
|
| + mLastCursorAnchorInfo = null;
|
| + if (mMonitorModeEnabled) {
|
| + mHasPendingRequest = true;
|
| + }
|
| + }
|
| + }
|
| + mCompositionCharacterBounds = compositionCharacterBounds;
|
| + }
|
| +
|
| + /**
|
| + * Sets coordinates system parameters and selection marker information.
|
| + * @param hasInsertionMarker {@code true} if the insertion marker exists.
|
| + * @param isInsertionMarkerVisible {@code true} if the insertion insertion marker is visible.
|
| + * @param insertionMarkerHorizontal X coordinate of the top of the first selection marker.
|
| + * @param insertionMarkerTop Y coordinate of the top of the first selection marker.
|
| + * @param insertionMarkerBottom Y coordinate of the bottom of the first selection marker.
|
| + * @param view The attached view.
|
| + */
|
| + public void onUpdateFrameInfo(@Nonnull RenderCoordinates renderCoordinates,
|
| + boolean hasInsertionMarker, boolean isInsertionMarkerVisible,
|
| + float insertionMarkerHorizontal, float insertionMarkerTop,
|
| + float insertionMarkerBottom, @Nonnull View view) {
|
| + if (!mIsEditable)
|
| + return;
|
| +
|
| + float scale =
|
| + renderCoordinates.getPageScaleFactor() * renderCoordinates.getDeviceScaleFactor();
|
| + float translationX;
|
| + float translationY;
|
| + // Reuse {@param #mViewOrigin} to avoid object creation, as this method is supposed to be
|
| + // called at relatively high rate.
|
| + mViewDelegate.getLocationOnScreen(view, mViewOrigin);
|
| + translationX = -renderCoordinates.getScrollX() * scale + mViewOrigin[0];
|
| + translationY = -renderCoordinates.getScrollY() * scale
|
| + + renderCoordinates.getContentOffsetYPix() + mViewOrigin[1];
|
| +
|
| + if (mLastCursorAnchorInfo == null && mMonitorModeEnabled)
|
| + mHasPendingRequest = true;
|
| + if (mLastCursorAnchorInfo != null && !mHasPendingRequest) {
|
| + if (!mHasCoordinateInfo
|
| + || scale != mScale
|
| + || translationX != mTranslationX
|
| + || translationY != mTranslationY
|
| + || hasInsertionMarker != mHasInsertionMarker
|
| + || isInsertionMarkerVisible != mIsInsertionMarkerVisible
|
| + || insertionMarkerHorizontal != mInsertionMarkerHorizontal
|
| + || insertionMarkerTop != mInsertionMarkerTop
|
| + || insertionMarkerBottom != mInsertionMarkerBottom) {
|
| + mLastCursorAnchorInfo = null;
|
| + if (mMonitorModeEnabled) {
|
| + mHasPendingRequest = true;
|
| + }
|
| + }
|
| + }
|
| +
|
| + mHasInsertionMarker = hasInsertionMarker;
|
| + mIsInsertionMarkerVisible = isInsertionMarkerVisible;
|
| + mInsertionMarkerHorizontal = insertionMarkerHorizontal;
|
| + mInsertionMarkerTop = insertionMarkerTop;
|
| + mInsertionMarkerBottom = insertionMarkerBottom;
|
| + mScale = scale;
|
| + mTranslationX = translationX;
|
| + mTranslationY = translationY;
|
| + mHasCoordinateInfo = true;
|
| + if (mLastCursorAnchorInfo == null && mHasPendingRequest)
|
| + updateCursorAnchorInfo();
|
| + if (mLastCursorAnchorInfo != null && mHasPendingRequest) {
|
| + if (mInputMethodManagerWrapper != null) {
|
| + mInputMethodManagerWrapper.updateCursorAnchorInfo(view, mLastCursorAnchorInfo);
|
| + }
|
| + mHasPendingRequest = false;
|
| + }
|
| + }
|
| +
|
| + public void focusedNodeChanged(boolean isEditable) {
|
| + mIsEditable = isEditable;
|
| + mHasCoordinateInfo = false;
|
| + mText = null;
|
| + mSelectionStart = -1;
|
| + mSelectionEnd = -1;
|
| + mComposingTextStart = -1;
|
| + mComposingTextEnd = -1;
|
| + mCompositionCharacterBounds = null;
|
| + mHasInsertionMarker = false;
|
| + mInsertionMarkerHorizontal = Float.NaN;
|
| + mInsertionMarkerTop = Float.NaN;
|
| + mInsertionMarkerBottom = Float.NaN;
|
| + mScale = 1.0f;
|
| + mTranslationX = 0.0f;
|
| + mTranslationY = 0.0f;
|
| + mLastCursorAnchorInfo = null;
|
| + mCursorAnchorInfoBuilder.reset();
|
| + }
|
| +
|
| + public boolean onRequestCursorUpdates(int cursorUpdateMode, @Nonnull View view) {
|
| + if (!mIsEditable)
|
| + return false;
|
| + final int knownRequestCursorUpdatesFlags =
|
| + InputConnection.CURSOR_UPDATE_MONITOR | InputConnection.CURSOR_UPDATE_IMMEDIATE;
|
| + if ((cursorUpdateMode & ~knownRequestCursorUpdatesFlags) != 0) {
|
| + // Does nothing when at least one unknown bit flag is set.
|
| + return false;
|
| + }
|
| + boolean updateImmediate =
|
| + (cursorUpdateMode & InputConnection.CURSOR_UPDATE_IMMEDIATE) != 0;
|
| + boolean updateMonitor =
|
| + (cursorUpdateMode & InputConnection.CURSOR_UPDATE_MONITOR) != 0;
|
| + mMonitorModeEnabled = updateMonitor;
|
| + if (updateImmediate) {
|
| + if (mLastCursorAnchorInfo == null && mHasCoordinateInfo) {
|
| + updateCursorAnchorInfo();
|
| + }
|
| + if (mLastCursorAnchorInfo != null) {
|
| + if (mInputMethodManagerWrapper != null) {
|
| + mInputMethodManagerWrapper.updateCursorAnchorInfo(view, mLastCursorAnchorInfo);
|
| + }
|
| + } else {
|
| + mHasPendingRequest = true;
|
| + }
|
| + }
|
| + return true;
|
| + }
|
| +
|
| + private void updateCursorAnchorInfo() {
|
| + synchronized (mCursorAnchorInfoBuilder) {
|
| + CharSequence composingText = getComposingText();
|
| + int composingTextStart = mComposingTextStart;
|
| + if (composingText != null) {
|
| + mCursorAnchorInfoBuilder.setComposingText(composingTextStart, composingText);
|
| + float[] compositionCharacterBounds = mCompositionCharacterBounds;
|
| + if (compositionCharacterBounds != null) {
|
| + int numCharacter = compositionCharacterBounds.length / 4;
|
| + for (int i = 0; i < numCharacter; ++i) {
|
| + float left = compositionCharacterBounds[i * 4];
|
| + float top = compositionCharacterBounds[i * 4 + 1];
|
| + float right = compositionCharacterBounds[i * 4 + 2];
|
| + float bottom = compositionCharacterBounds[i * 4 + 3];
|
| + int charIndex = composingTextStart + i;
|
| + mCursorAnchorInfoBuilder.addCharacterBounds(charIndex, left, top, right,
|
| + bottom, CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION);
|
| + }
|
| + }
|
| + }
|
| + mCursorAnchorInfoBuilder.setSelectionRange(mSelectionStart, mSelectionEnd);
|
| + mMatrix.setScale(mScale, mScale);
|
| + mMatrix.postTranslate(mTranslationX, mTranslationY);
|
| + mCursorAnchorInfoBuilder.setMatrix(mMatrix);
|
| + if (mHasInsertionMarker) {
|
| + mCursorAnchorInfoBuilder.setInsertionMarkerLocation(
|
| + mInsertionMarkerHorizontal,
|
| + mInsertionMarkerTop,
|
| + mInsertionMarkerBottom,
|
| + mInsertionMarkerBottom,
|
| + mIsInsertionMarkerVisible ? CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION :
|
| + CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION);
|
| + }
|
| + mLastCursorAnchorInfo = mCursorAnchorInfoBuilder.build();
|
| + mCursorAnchorInfoBuilder.reset();
|
| + }
|
| + }
|
| +}
|
|
|