| 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..443e7d25a4c660083b3906c8b44f6f471bfc4050
|
| --- /dev/null
|
| +++ b/content/public/android/java/src/org/chromium/content/browser/input/CursorAnchorInfoController.java
|
| @@ -0,0 +1,280 @@
|
| +// Copyright 2016 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.annotation.TargetApi;
|
| +import android.graphics.Matrix;
|
| +import android.os.Build;
|
| +import android.view.View;
|
| +import android.view.inputmethod.CursorAnchorInfo;
|
| +import android.view.inputmethod.InputConnection;
|
| +
|
| +import org.chromium.base.VisibleForTesting;
|
| +import org.chromium.base.annotations.SuppressFBWarnings;
|
| +import org.chromium.content.browser.RenderCoordinates;
|
| +
|
| +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.
|
| + */
|
| +@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
| +final class CursorAnchorInfoController {
|
| + /**
|
| + * An interface to mock out {@link View#getLocationOnScreen(int[])} for testing.
|
| + */
|
| + public interface ViewDelegate {
|
| + void getLocationOnScreen(View view, int[] location);
|
| + }
|
| +
|
| + /**
|
| + * An interface to mock out composing text retrieval from ImeAdapter.
|
| + */
|
| + public interface ComposingTextDelegate {
|
| + CharSequence getText();
|
| + int getSelectionStart();
|
| + int getSelectionEnd();
|
| + int getComposingTextStart();
|
| + int getComposingTextEnd();
|
| + }
|
| +
|
| + // Current focus and monitoring states.
|
| + private boolean mIsEditable;
|
| + private boolean mHasPendingImmediateRequest;
|
| + private boolean mMonitorModeEnabled;
|
| +
|
| + // Parmeter for CursorAnchorInfo, updated by setCompositionCharacterBounds.
|
| + @Nullable
|
| + private float[] mCompositionCharacterBounds;
|
| + // Paremeters for CursorAnchorInfo, updated by onUpdateFrameInfo.
|
| + private boolean mHasCoordinateInfo;
|
| + private float mScale;
|
| + private float mTranslationX;
|
| + private float mTranslationY;
|
| + private boolean mHasInsertionMarker;
|
| + private boolean mIsInsertionMarkerVisible;
|
| + private float mInsertionMarkerHorizontal;
|
| + private float mInsertionMarkerTop;
|
| + private float mInsertionMarkerBottom;
|
| +
|
| + @Nullable
|
| + private CursorAnchorInfo mLastCursorAnchorInfo;
|
| +
|
| + @Nonnull
|
| + private final Matrix mMatrix = new Matrix();
|
| + @Nonnull
|
| + private final int[] mViewOrigin = new int[2];
|
| + @Nonnull
|
| + private final CursorAnchorInfo.Builder mCursorAnchorInfoBuilder =
|
| + new CursorAnchorInfo.Builder();
|
| +
|
| + @Nullable
|
| + private InputMethodManagerWrapper mInputMethodManagerWrapper;
|
| + @Nullable
|
| + private final ComposingTextDelegate mComposingTextDelegate;
|
| + @Nonnull
|
| + private final ViewDelegate mViewDelegate;
|
| +
|
| + private CursorAnchorInfoController(InputMethodManagerWrapper inputMethodManagerWrapper,
|
| + ComposingTextDelegate composingTextDelegate, ViewDelegate viewDelegate) {
|
| + mInputMethodManagerWrapper = inputMethodManagerWrapper;
|
| + mComposingTextDelegate = composingTextDelegate;
|
| + mViewDelegate = viewDelegate;
|
| + }
|
| +
|
| + public static CursorAnchorInfoController create(
|
| + InputMethodManagerWrapper inputMethodManagerWrapper,
|
| + ComposingTextDelegate composingTextDelegate) {
|
| + return new CursorAnchorInfoController(inputMethodManagerWrapper,
|
| + composingTextDelegate, new ViewDelegate() {
|
| + @Override
|
| + public void getLocationOnScreen(View view, int[] location) {
|
| + view.getLocationOnScreen(location);
|
| + }
|
| + });
|
| + }
|
| +
|
| + @VisibleForTesting
|
| + public void setInputMethodManagerWrapperForTest(
|
| + InputMethodManagerWrapper inputMethodManagerWrapper) {
|
| + mInputMethodManagerWrapper = inputMethodManagerWrapper;
|
| + }
|
| +
|
| + @VisibleForTesting
|
| + public static CursorAnchorInfoController createForTest(
|
| + InputMethodManagerWrapper inputMethodManagerWrapper,
|
| + ComposingTextDelegate composingTextDelegate,
|
| + ViewDelegate viewDelegate) {
|
| + return new CursorAnchorInfoController(inputMethodManagerWrapper, composingTextDelegate,
|
| + viewDelegate);
|
| + }
|
| +
|
| + /**
|
| + * Called by ImeAdapter when a IME related web content state is changed.
|
| + */
|
| + public void invalidateLastCursorAnchorInfo() {
|
| + if (!mIsEditable) return;
|
| +
|
| + mLastCursorAnchorInfo = null;
|
| + }
|
| +
|
| + /**
|
| + * 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 (!Arrays.equals(compositionCharacterBounds, mCompositionCharacterBounds)) {
|
| + mLastCursorAnchorInfo = null;
|
| + 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.
|
| + */
|
| + @SuppressFBWarnings("FE_FLOATING_POINT_EQUALITY")
|
| + public void onUpdateFrameInfo(@Nonnull RenderCoordinates renderCoordinates,
|
| + boolean hasInsertionMarker, boolean isInsertionMarkerVisible,
|
| + float insertionMarkerHorizontal, float insertionMarkerTop,
|
| + float insertionMarkerBottom, @Nonnull View view) {
|
| + if (!mIsEditable) return;
|
| +
|
| + // Reuse {@param #mViewOrigin} to avoid object creation, as this method is supposed to be
|
| + // called at relatively high rate.
|
| + mViewDelegate.getLocationOnScreen(view, mViewOrigin);
|
| +
|
| + // Character bounds and insertion marker locations come in device independent pixels
|
| + // relative from the top-left corner of the web view content area. (In other words, the
|
| + // effects of various kinds of zooming and scrolling are already taken into account.)
|
| + //
|
| + // We need to prepare parameters that convert such values to physical pixels, in the
|
| + // screen coordinate. Hence the following values are derived.
|
| + float scale = renderCoordinates.getDeviceScaleFactor();
|
| + float translationX = mViewOrigin[0];
|
| + float translationY = mViewOrigin[1] + renderCoordinates.getContentOffsetYPix();
|
| +
|
| + if (!mHasCoordinateInfo
|
| + || scale != mScale
|
| + || translationX != mTranslationX
|
| + || translationY != mTranslationY
|
| + || hasInsertionMarker != mHasInsertionMarker
|
| + || isInsertionMarkerVisible != mIsInsertionMarkerVisible
|
| + || insertionMarkerHorizontal != mInsertionMarkerHorizontal
|
| + || insertionMarkerTop != mInsertionMarkerTop
|
| + || insertionMarkerBottom != mInsertionMarkerBottom) {
|
| + mLastCursorAnchorInfo = null;
|
| + mHasCoordinateInfo = true;
|
| + mScale = scale;
|
| + mTranslationX = translationX;
|
| + mTranslationY = translationY;
|
| + mHasInsertionMarker = hasInsertionMarker;
|
| + mIsInsertionMarkerVisible = isInsertionMarkerVisible;
|
| + mInsertionMarkerHorizontal = insertionMarkerHorizontal;
|
| + mInsertionMarkerTop = insertionMarkerTop;
|
| + mInsertionMarkerBottom = insertionMarkerBottom;
|
| + }
|
| +
|
| + // Notify to IME if there is a pending request, or if it is in monitor mode and we have
|
| + // some change in the state.
|
| + if (mHasPendingImmediateRequest
|
| + || (mMonitorModeEnabled && mLastCursorAnchorInfo == null)) {
|
| + updateCursorAnchorInfo(view);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Resets the current state on update monitoring mode to the default (= do nothing.)
|
| + */
|
| + public void resetMonitoringState() {
|
| + mMonitorModeEnabled = false;
|
| + mHasPendingImmediateRequest = false;
|
| + }
|
| +
|
| + public void focusedNodeChanged(boolean isEditable) {
|
| + mIsEditable = isEditable;
|
| + mCompositionCharacterBounds = null;
|
| + mHasCoordinateInfo = false;
|
| + mLastCursorAnchorInfo = null;
|
| + }
|
| +
|
| + public boolean onRequestCursorUpdates(int cursorUpdateMode, View view) {
|
| + if (!mIsEditable) return false;
|
| +
|
| + mMonitorModeEnabled = (cursorUpdateMode & InputConnection.CURSOR_UPDATE_MONITOR) != 0;
|
| + if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_IMMEDIATE) != 0) {
|
| + mHasPendingImmediateRequest = true;
|
| + updateCursorAnchorInfo(view);
|
| + }
|
| + return true;
|
| + }
|
| +
|
| + /**
|
| + * Computes the CursorAnchorInfo instance and notify to InputMethodManager if needed.
|
| + */
|
| + private void updateCursorAnchorInfo(View view) {
|
| + if (!mHasCoordinateInfo) return;
|
| +
|
| + if (mLastCursorAnchorInfo == null) {
|
| + // Reuse the builder object.
|
| + mCursorAnchorInfoBuilder.reset();
|
| +
|
| + CharSequence text = mComposingTextDelegate.getText();
|
| + int selectionStart = mComposingTextDelegate.getSelectionStart();
|
| + int selectionEnd = mComposingTextDelegate.getSelectionEnd();
|
| + int composingTextStart = mComposingTextDelegate.getComposingTextStart();
|
| + int composingTextEnd = mComposingTextDelegate.getComposingTextEnd();
|
| + if (text != null && 0 <= composingTextStart && composingTextEnd <= text.length()) {
|
| + mCursorAnchorInfoBuilder.setComposingText(composingTextStart,
|
| + text.subSequence(composingTextStart, composingTextEnd));
|
| + 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(selectionStart, selectionEnd);
|
| + 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();
|
| + }
|
| +
|
| + if (mInputMethodManagerWrapper != null) {
|
| + mInputMethodManagerWrapper.updateCursorAnchorInfo(view, mLastCursorAnchorInfo);
|
| + }
|
| + mHasPendingImmediateRequest = false;
|
| + }
|
| +}
|
|
|