Chromium Code Reviews| 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); | 
| } |