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