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); |
| } |