Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1314)

Unified Diff: content/public/android/java/src/org/chromium/content/browser/input/ImeAdapter.java

Issue 2650113004: [WIP] Add support for Android SuggestionSpans when editing text (Closed)
Patch Set: Created 3 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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 df58b85a1caa6589cebf562a34cb16f6ee454732..8624f785852d0a5f4047b44fb55c81168fee05a3 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,21 +4,49 @@
package org.chromium.content.browser.input;
+import android.annotation.TargetApi;
+import android.content.Context;
import android.content.res.Configuration;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.ResultReceiver;
import android.os.SystemClock;
+import android.text.Spannable;
+import android.text.Spanned;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
import android.text.style.CharacterStyle;
+import android.text.style.SuggestionSpan;
+import android.text.style.TextAppearanceSpan;
import android.text.style.UnderlineSpan;
+import android.util.DisplayMetrics;
+import android.view.Gravity;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
+import android.view.LayoutInflater;
import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.CursorAnchorInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
+import android.view.WindowManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.BaseAdapter;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.PopupWindow;
+import android.widget.PopupWindow.OnDismissListener;
+import android.widget.TextView;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
@@ -27,6 +55,7 @@ import org.chromium.base.annotations.JNINamespace;
import org.chromium.blink_public.web.WebInputEventModifier;
import org.chromium.blink_public.web.WebInputEventType;
import org.chromium.blink_public.web.WebTextInputMode;
+import org.chromium.content.R;
import org.chromium.content.browser.RenderCoordinates;
import org.chromium.content.browser.picker.InputDialogContainer;
import org.chromium.ui.base.ime.TextInputType;
@@ -104,6 +133,9 @@ public class ImeAdapter {
// re-created, the monitoring status will be reset.
private final CursorAnchorInfoController mCursorAnchorInfoController;
+ SuggestionsPopupWindow mSuggestionsPopupWindow;
+ private SuggestionInfo[] mSuggestionInfos = new SuggestionInfo[0];
+
private int mTextInputType = TextInputType.NONE;
private int mTextInputFlags;
private int mTextInputMode = WebTextInputMode.kDefault;
@@ -117,6 +149,8 @@ public class ImeAdapter {
private int mLastCompositionStart;
private int mLastCompositionEnd;
+ private CursorAnchorInfo mLastCursorAnchorInfo;
+
/**
* @param wrapper InputMethodManagerWrapper that should receive all the call directed to
* InputMethodManager.
@@ -128,10 +162,10 @@ public class ImeAdapter {
// Deep copy newConfig so that we can notice the difference.
mCurrentConfig = new Configuration(
mViewEmbedder.getAttachedView().getResources().getConfiguration());
- // CursorAnchroInfo is supported only after L.
+ // CursorAnchorInfo is supported only after L.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- mCursorAnchorInfoController = CursorAnchorInfoController.create(wrapper,
- new CursorAnchorInfoController.ComposingTextDelegate() {
+ mCursorAnchorInfoController = CursorAnchorInfoController.create(
+ wrapper, new CursorAnchorInfoController.ComposingTextDelegate() {
@Override
public CharSequence getText() {
return mLastText;
@@ -152,7 +186,7 @@ public class ImeAdapter {
public int getComposingTextEnd() {
return mLastCompositionEnd;
}
- });
+ }, this);
} else {
mCursorAnchorInfoController = null;
}
@@ -724,6 +758,248 @@ public class ImeAdapter {
insertionMarkerBottom, mViewEmbedder.getAttachedView());
}
+ /**
+ * Called by CursorAnchorInfoController to notify ImeAdapter when there's
+ * updated CursorAnchorInfo
+ */
+ public void updateCursorAnchorInfo(View view, CursorAnchorInfo cursorAnchorInfo) {
+ mLastCursorAnchorInfo = cursorAnchorInfo;
+ if (mSuggestionsPopupWindow != null) {
+ mSuggestionsPopupWindow.updatePosition();
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private class SuggestionsPopupWindow implements OnItemClickListener, OnDismissListener {
aelias_OOO_until_Jul13 2017/01/25 03:34:26 Please move this into a new .java file.
+ protected PopupWindow mPopupWindow;
+ protected ViewGroup mContentView;
+ int mPositionX, mPositionY;
+ int mClippingLimitLeft, mClippingLimitRight;
+ private Rect mTempRect;
+
+ private SuggestionAdapter mSuggestionsAdapter;
+ private final TextAppearanceSpan mHighlightSpan;
+ private TextView mDeleteButton;
+ private ListView mSuggestionListView;
+ private int mContainerMarginWidth;
+ private int mContainerMarginTop;
+ private double mLastInsertionMarkerBottom = 0.0;
+ private LinearLayout mContainerView;
+
+ private boolean mDismissedByItemTap = false;
+
+ protected void createPopupWindow() {
+ mPopupWindow = new PopupWindow();
+ mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+ mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+
+ mPopupWindow.setFocusable(true);
+ mPopupWindow.setClippingEnabled(false);
+
+ mPopupWindow.setOnDismissListener(this);
+ }
+
+ protected void initContentView() {
+ final LayoutInflater inflater =
+ (LayoutInflater) mInputMethodManagerWrapper.getContext().getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ mContentView =
+ (ViewGroup) inflater.inflate(R.layout.text_edit_suggestion_container, null);
+ mContainerView =
+ (LinearLayout) mContentView.findViewById(R.id.suggestionWindowContainer);
+ ViewGroup.MarginLayoutParams lp =
+ (ViewGroup.MarginLayoutParams) mContainerView.getLayoutParams();
+ mContainerMarginWidth = lp.leftMargin + lp.rightMargin;
+ mContainerMarginTop = lp.topMargin;
+ mClippingLimitLeft = lp.leftMargin;
+ mClippingLimitRight = lp.rightMargin;
+
+ mSuggestionListView = (ListView) mContentView.findViewById(R.id.suggestionContainer);
+
+ mSuggestionsAdapter = new SuggestionAdapter();
+ mSuggestionListView.setAdapter(mSuggestionsAdapter);
+ mSuggestionListView.setOnItemClickListener(this);
+
+ mDeleteButton = (TextView) mContentView.findViewById(R.id.deleteButton);
+ mDeleteButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ nativeDeleteSuggestionHighlight(mNativeImeAdapterAndroid);
+ mDismissedByItemTap = true;
+ mPopupWindow.dismiss();
+ }
+ });
+ }
+
+ public SuggestionsPopupWindow() {
+ mHighlightSpan = new TextAppearanceSpan(
+ mInputMethodManagerWrapper.getContext(), R.style.SuggestionHighlight);
+
+ createPopupWindow();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ mPopupWindow.setWindowLayoutType(
+ WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
+ }
+ mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
+ mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
+ initContentView();
+
+ LayoutParams wrapContent = new LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ mContentView.setLayoutParams(wrapContent);
+ mPopupWindow.setContentView(mContentView);
+ }
+
+ public void updatePosition() {
+ if (!mPopupWindow.isShowing()) {
+ return;
+ }
+ show();
+ }
+
+ public boolean isShowing() {
+ return mPopupWindow.isShowing();
+ }
+
+ private class SuggestionAdapter extends BaseAdapter {
+ private LayoutInflater mInflater =
+ (LayoutInflater) mInputMethodManagerWrapper.getContext().getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ @Override
+ public int getCount() {
+ return mSuggestionInfos.length;
+ }
+ @Override
+ public Object getItem(int position) {
+ return mSuggestionInfos[position];
+ }
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TextView textView = (TextView) convertView;
+ if (textView == null) {
+ textView = (TextView) mInflater.inflate(
+ R.layout.text_edit_suggestion_item, parent, false);
+ }
+ final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
+ SpannableString textToSet = new SpannableString(
+ suggestionInfo.mPrefix + suggestionInfo.mText + suggestionInfo.mSuffix);
+ textToSet.setSpan(mHighlightSpan, suggestionInfo.mPrefix.length(),
+ suggestionInfo.mPrefix.length() + suggestionInfo.mText.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ textView.setText(textToSet);
+ return textView;
+ }
+ }
+
+ protected void measureContent() {
+ final DisplayMetrics displayMetrics =
+ mInputMethodManagerWrapper.getContext().getResources().getDisplayMetrics();
+ final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
+ displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
+ final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
+ displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
+ int width = 0;
+ View view = null;
+ for (int i = 0; i < mSuggestionInfos.length; i++) {
+ view = mSuggestionsAdapter.getView(i, view, mContentView);
+ view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
+ view.measure(horizontalMeasure, verticalMeasure);
+ width = Math.max(width, view.getMeasuredWidth());
+ }
+ mDeleteButton.measure(horizontalMeasure, verticalMeasure);
+ width = Math.max(width, mDeleteButton.getMeasuredWidth());
+ width += mContainerView.getPaddingLeft() + mContainerView.getPaddingRight()
+ + mContainerMarginWidth;
+ // Enforce the width based on actual text widths
+ mContentView.measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
+ verticalMeasure);
+ Drawable popupBackground = mPopupWindow.getBackground();
+ if (popupBackground != null) {
+ if (mTempRect == null) mTempRect = new Rect();
+ popupBackground.getPadding(mTempRect);
+ width += mTempRect.left + mTempRect.right;
+ }
+ mPopupWindow.setWidth(width);
+ }
+
+ public void show() {
+ mSuggestionsAdapter.notifyDataSetChanged();
+ // We get the insertion point coords relative to the WebView bounds.
+ // We need to render the popup relative to the display
+ int[] embedderViewCoords = new int[2];
+ mViewEmbedder.getAttachedView().getLocationOnScreen(embedderViewCoords);
+
+ final DisplayMetrics displayMetrics =
+ mInputMethodManagerWrapper.getContext().getResources().getDisplayMetrics();
+
+ float density = displayMetrics.density;
+
+ measureContent();
+ int width = mContentView.getMeasuredWidth();
+
+ int positionX = 0;
+ int positionY = 0;
+
+ positionX = Math.round(
+ mLastCursorAnchorInfo.getInsertionMarkerHorizontal() * density - width / 2.0f);
+ double insertionMarkerBottom = mLastCursorAnchorInfo.getInsertionMarkerBottom();
+ if (Double.isNaN(insertionMarkerBottom)) {
+ // certain operations, e.g. rotating the device while the menu
aelias_OOO_until_Jul13 2017/01/25 03:34:26 Seems this is no longer needed after changwan@'s f
+ // is open, can cause getInsertionMarkerBottom() to start
+ // returning NaN, just keep using the previous value in this case
+ insertionMarkerBottom = mLastInsertionMarkerBottom;
+ } else {
+ mLastInsertionMarkerBottom = insertionMarkerBottom;
+ }
+ positionY = (int) Math.round(
+ (embedderViewCoords[1] + insertionMarkerBottom - 24) * density);
+
+ // horizontal clipping
+ width = mContentView.getMeasuredWidth();
+ positionX =
+ Math.min(displayMetrics.widthPixels - width + mClippingLimitRight, positionX);
+ positionX = Math.max(-mClippingLimitLeft, positionX);
+
+ // vertical clipping
+ final int height = mContentView.getMeasuredHeight();
+ positionY = Math.min(positionY, displayMetrics.heightPixels - height);
+
+ if (isShowing()) {
+ mPopupWindow.update(positionX, positionY, -1, -1);
+ } else {
+ mPopupWindow.showAtLocation(
+ mViewEmbedder.getAttachedView(), Gravity.NO_GRAVITY, positionX, positionY);
+ }
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ SuggestionInfo suggestionInfo = mSuggestionInfos[position];
+ nativeApplySuggestionReplacement(mNativeImeAdapterAndroid,
+ suggestionInfo.mDocumentMarkerID, suggestionInfo.mSuggestionIndex);
+ // replaceWithSuggestion(suggestionInfo);
+ // hideWithCleanUp();
+ mDismissedByItemTap = true;
aelias_OOO_until_Jul13 2017/01/25 03:34:26 Can we instead not dismiss at all at just yet? We
rlanday 2017/01/25 18:48:10 I don't understand what you're asking here, the me
+ mPopupWindow.dismiss();
+ }
+
+ public void closeMenu() {
aelias_OOO_until_Jul13 2017/01/25 03:34:26 This has no callsites, please remove.
rlanday 2017/01/25 18:48:10 ah, I added this when I was thinking we might want
+ mPopupWindow.dismiss();
+ }
+
+ @Override
+ public void onDismiss() {
+ if (!mDismissedByItemTap) {
+ nativeCloseSuggestionMenu(mNativeImeAdapterAndroid);
+ }
+ mDismissedByItemTap = false;
+ }
+ }
+
@CalledByNative
private void populateUnderlinesFromSpans(CharSequence text, long underlines) {
if (DEBUG_LOGS) {
@@ -739,6 +1015,27 @@ public class ImeAdapter {
nativeAppendBackgroundColorSpan(underlines, spannableString.getSpanStart(span),
spannableString.getSpanEnd(span),
((BackgroundColorSpan) span).getBackgroundColor());
+ } else if (span instanceof SuggestionSpan) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ // The suggestion menu is only supported on Lollipop and newer
+ continue;
+ }
+
+ int underlineColor = 0;
+ // Use getUnderlineColor method by reflection to avoid having to reimplement
aelias_OOO_until_Jul13 2017/01/25 03:34:26 The method is public, it's just @hide. You can pr
rlanday 2017/01/25 18:48:10 Hmm I think I tried that but I'll look into it aga
rlanday 2017/01/26 00:23:05 Trying to call span.getUnderlineColor() directly r
+ try {
+ Method getUnderlineColor = SuggestionSpan.class.getMethod("getUnderlineColor");
+ underlineColor = (int) getUnderlineColor.invoke((SuggestionSpan) span);
+ } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException
+ | RuntimeException e) {
+ continue;
+ }
+
+ SuggestionSpan suggestionSpan = (SuggestionSpan) span;
+ nativeAppendSuggestionSpan(underlines, spannableString.getSpanStart(suggestionSpan),
+ spannableString.getSpanEnd(suggestionSpan), underlineColor,
+ suggestionSpan.getFlags(), suggestionSpan.getSuggestions());
+
} else if (span instanceof UnderlineSpan) {
nativeAppendUnderlineSpan(underlines, spannableString.getSpanStart(span),
spannableString.getSpanEnd(span));
@@ -759,6 +1056,33 @@ public class ImeAdapter {
mViewEmbedder.getAttachedView());
}
+ public class SuggestionInfo {
+ int mDocumentMarkerID;
+ int mSuggestionIndex;
+ String mPrefix;
+ String mText;
+ String mSuffix;
+
+ SuggestionInfo(int documentMarkerID, int suggestionIndex, String prefix, String text,
+ String suffix) {
+ mDocumentMarkerID = documentMarkerID;
+ mSuggestionIndex = suggestionIndex;
+ mPrefix = prefix;
+ mText = text;
+ mSuffix = suffix;
+ }
+ }
+
+ @CalledByNative
+ private void showSuggestionMenu(SuggestionInfo[] suggestionInfos) {
+ mSuggestionInfos = suggestionInfos;
+ if (mSuggestionsPopupWindow == null) {
+ mSuggestionsPopupWindow = new SuggestionsPopupWindow();
+ }
+
+ mSuggestionsPopupWindow.show();
+ }
+
@CalledByNative
private void detach() {
if (DEBUG_LOGS) Log.w(TAG, "detach");
@@ -771,9 +1095,11 @@ public class ImeAdapter {
private native boolean nativeSendKeyEvent(long nativeImeAdapterAndroid, KeyEvent event,
int type, int modifiers, long timestampMs, int keyCode, int scanCode,
boolean isSystemKey, int unicodeChar);
- private static native void nativeAppendUnderlineSpan(long underlinePtr, int start, int end);
private static native void nativeAppendBackgroundColorSpan(long underlinePtr, int start,
int end, int backgroundColor);
+ private static native void nativeAppendSuggestionSpan(long underlinePtr, int start, int end,
+ int underlineColor, int flags, String[] suggestions);
+ private static native void nativeAppendUnderlineSpan(long underlinePtr, int start, int end);
private native void nativeSetComposingText(long nativeImeAdapterAndroid, CharSequence text,
String textStr, int newCursorPosition);
private native void nativeCommitText(
@@ -789,4 +1115,8 @@ public class ImeAdapter {
private native boolean nativeRequestTextInputStateUpdate(long nativeImeAdapterAndroid);
private native void nativeRequestCursorUpdate(long nativeImeAdapterAndroid,
boolean immediateRequest, boolean monitorRequest);
+ private native void nativeApplySuggestionReplacement(
+ long nativeImeAdapterAndroid, int documentMarkerID, int suggestionIndex);
+ private native void nativeDeleteSuggestionHighlight(long nativeImeAdapterAndroid);
+ private native void nativeCloseSuggestionMenu(long nativeImeAdapterAndroid);
}

Powered by Google App Engine
This is Rietveld 408576698