Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6a839703 authored by Gilles Debunne's avatar Gilles Debunne Committed by Android (Google) Code Review
Browse files

Merge "Word suggestion popup added to EditText."

parents 908ac169 6934044f
Loading
Loading
Loading
Loading
+45 −1
Original line number Diff line number Diff line
@@ -10025,6 +10025,39 @@
 visibility="public"
>
</field>
<field name="textEditSuggestionItemLayout"
 type="int"
 transient="false"
 volatile="false"
 value="16843626"
 static="true"
 final="true"
 deprecated="not deprecated"
 visibility="public"
>
</field>
<field name="textEditSuggestionsBottomWindowLayout"
 type="int"
 transient="false"
 volatile="false"
 value="16843624"
 static="true"
 final="true"
 deprecated="not deprecated"
 visibility="public"
>
</field>
<field name="textEditSuggestionsTopWindowLayout"
 type="int"
 transient="false"
 volatile="false"
 value="16843625"
 static="true"
 final="true"
 deprecated="not deprecated"
 visibility="public"
>
</field>
<field name="textFilterEnabled"
 type="int"
 transient="false"
@@ -10146,6 +10179,17 @@
 visibility="public"
>
</field>
<field name="textSuggestionsWindowStyle"
 type="int"
 transient="false"
 volatile="false"
 value="16843623"
 static="true"
 final="true"
 deprecated="not deprecated"
 visibility="public"
>
</field>
<field name="textViewStyle"
 type="int"
 transient="false"
@@ -267852,7 +267896,7 @@
 deprecated="not deprecated"
 visibility="public"
>
<parameter name="arg0" type="T">
<parameter name="t" type="T">
</parameter>
</method>
</interface>
+225 −17
Original line number Diff line number Diff line
@@ -43,7 +43,6 @@ import android.inputmethodservice.ExtractEditText;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Parcel;
import android.os.Parcelable;
@@ -88,6 +87,7 @@ import android.text.style.URLSpan;
import android.text.style.UpdateAppearance;
import android.text.util.Linkify;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.FloatMath;
import android.util.Log;
import android.util.TypedValue;
@@ -311,6 +311,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
    private int mTextEditPasteWindowLayout, mTextEditSidePasteWindowLayout;
    private int mTextEditNoPasteWindowLayout, mTextEditSideNoPasteWindowLayout;

    private int mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout;
    private int mTextEditSuggestionItemLayout;
    private SuggestionsPopupWindow mSuggestionsPopupWindow;

    private int mCursorDrawableRes;
    private final Drawable[] mCursorDrawable = new Drawable[2];
    private int mCursorCount; // Actual current number of used mCursorDrawable: 0, 1 or 2
@@ -779,6 +783,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
                mTextEditSideNoPasteWindowLayout = a.getResourceId(attr, 0);
                break;

            case com.android.internal.R.styleable.TextView_textEditSuggestionsBottomWindowLayout:
                mTextEditSuggestionsBottomWindowLayout = a.getResourceId(attr, 0);
                break;

            case com.android.internal.R.styleable.TextView_textEditSuggestionsTopWindowLayout:
                mTextEditSuggestionsTopWindowLayout = a.getResourceId(attr, 0);
                break;

            case com.android.internal.R.styleable.TextView_textEditSuggestionItemLayout:
                mTextEditSuggestionItemLayout = a.getResourceId(attr, 0);
                break;

            case com.android.internal.R.styleable.TextView_textIsSelectable:
                mTextIsSelectable = a.getBoolean(attr, false);
                break;
@@ -7343,6 +7359,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
                    startSelectionActionMode();
                } else {
                    stopSelectionActionMode();
                    hideSuggestions();
                    if (hasInsertionController() && !selectAllGotFocus && mText.length() > 0) {
                        getInsertionController().show();
                    }
@@ -8221,6 +8238,201 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
    }

    private class SuggestionsPopupWindow implements OnClickListener {
        private static final int MAX_NUMBER_SUGGESTIONS = 5;
        private static final long NO_SUGGESTIONS = -1L;
        private final PopupWindow mContainer;
        private final ViewGroup[] mSuggestionViews = new ViewGroup[2];
        private final int[] mSuggestionViewLayouts = new int[] {
                mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout};

        public SuggestionsPopupWindow() {
            mContainer = new PopupWindow(TextView.this.mContext, null,
                    com.android.internal.R.attr.textSuggestionsWindowStyle);
            mContainer.setSplitTouchEnabled(true);
            mContainer.setClippingEnabled(false);
            mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);

            mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
            mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
        }

        private ViewGroup getViewGroup(boolean under) {
            final int viewIndex = under ? 0 : 1;
            ViewGroup viewGroup = mSuggestionViews[viewIndex];

            if (viewGroup == null) {
                final int layout = mSuggestionViewLayouts[viewIndex];
                LayoutInflater inflater = (LayoutInflater) TextView.this.mContext.
                        getSystemService(Context.LAYOUT_INFLATER_SERVICE);

                if (inflater == null) {
                    throw new IllegalArgumentException(
                            "Unable to create TextEdit suggestion window inflater");
                }

                View view = inflater.inflate(layout, null);

                if (! (view instanceof ViewGroup)) {
                    throw new IllegalArgumentException(
                            "Inflated TextEdit suggestion window is not a ViewGroup: " + view);
                }

                viewGroup = (ViewGroup) view;

                // Inflate the suggestion items once and for all.
                for (int i = 0; i < MAX_NUMBER_SUGGESTIONS; i++) {
                    View childView = inflater.inflate(mTextEditSuggestionItemLayout, viewGroup,
                            false);

                    if (! (childView instanceof TextView)) {
                        throw new IllegalArgumentException(
                               "Inflated TextEdit suggestion item is not a TextView: " + childView);
                    }

                    viewGroup.addView(childView);
                    childView.setOnClickListener(this);
                }

                mSuggestionViews[viewIndex] = viewGroup;
            }

            return viewGroup;
        }

        public void show() {
            if (!(mText instanceof Editable)) return;

            final int pos = TextView.this.getSelectionStart();
            Spannable spannable = (Spannable)TextView.this.mText;
            CorrectionSpan[] correctionSpans = spannable.getSpans(pos, pos, CorrectionSpan.class);
            final int nbSpans = correctionSpans.length;

            ViewGroup viewGroup = getViewGroup(true);
            mContainer.setContentView(viewGroup);

            int totalNbSuggestions = 0;
            for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
                CorrectionSpan correctionSpan = correctionSpans[spanIndex];
                final int spanStart = spannable.getSpanStart(correctionSpan);
                final int spanEnd = spannable.getSpanEnd(correctionSpan);
                final Long spanRange = packRangeInLong(spanStart, spanEnd);

                String[] suggestions = correctionSpan.getSuggestions();
                int nbSuggestions = suggestions.length;
                for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
                    TextView textView = (TextView) viewGroup.getChildAt(totalNbSuggestions);
                    textView.setText(suggestions[suggestionIndex]);
                    textView.setTag(spanRange);

                    totalNbSuggestions++;
                    if (totalNbSuggestions == MAX_NUMBER_SUGGESTIONS) {
                        spanIndex = nbSpans;
                        break;
                    }
                }
            }

            if (totalNbSuggestions == 0) {
                // TODO Replace by final text, use a dedicated layout, add a fade out timer...
                TextView textView = (TextView) viewGroup.getChildAt(0);
                textView.setText("No suggestions available");
                textView.setTag(NO_SUGGESTIONS);
                totalNbSuggestions++;
            }

            for (int i = 0; i < MAX_NUMBER_SUGGESTIONS; i++) {
                viewGroup.getChildAt(i).setVisibility(i < totalNbSuggestions ? VISIBLE : GONE);
            }

            final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
            viewGroup.measure(size, size);

            positionAtCursor();
        }

        public void hide() {
            mContainer.dismiss();
        }

        @Override
        public void onClick(View view) {
            if (view instanceof TextView) {
                TextView textView = (TextView) view;
                Long range = ((Long) view.getTag());
                if (range != NO_SUGGESTIONS) {
                    final int spanStart = extractRangeStartFromLong(range);
                    final int spanEnd = extractRangeEndFromLong(range);
                    ((Editable) mText).replace(spanStart, spanEnd, textView.getText());
                }
            }
            hide();
        }

        void positionAtCursor() {
            View contentView = mContainer.getContentView();
            int width = contentView.getMeasuredWidth();
            int height = contentView.getMeasuredHeight();
            final int offset = TextView.this.getSelectionStart();
            final int line = mLayout.getLineForOffset(offset);
            final int lineBottom = mLayout.getLineBottom(line);
            float primaryHorizontal = mLayout.getPrimaryHorizontal(offset);

            final Rect bounds = sCursorControllerTempRect;
            bounds.left = (int) (primaryHorizontal - width / 2.0f);
            bounds.top = lineBottom;

            bounds.right = bounds.left + width;
            bounds.bottom = bounds.top + height;

            convertFromViewportToContentCoordinates(bounds);

            final int[] coords = mTempCoords;
            TextView.this.getLocationInWindow(coords);
            coords[0] += bounds.left;
            coords[1] += bounds.top;

            final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
            final int screenHeight = displayMetrics.heightPixels;

            // Vertical clipping
            if (coords[1] + height > screenHeight) {
                // Try to position above current line instead
                // TODO use top layout instead, reverse suggestion order,
                // try full screen vertical down if it still does not fit. TBD with designers.

                // Update dimensions from new view
                contentView = mContainer.getContentView();
                width = contentView.getMeasuredWidth();
                height = contentView.getMeasuredHeight();

                final int lineTop = mLayout.getLineTop(line);
                final int lineHeight = lineBottom - lineTop;
                coords[1] -= height + lineHeight;
            }

            // Horizontal clipping
            coords[0] = Math.max(0, coords[0]);
            coords[0] = Math.min(displayMetrics.widthPixels - width, coords[0]);

            mContainer.showAtLocation(TextView.this, Gravity.NO_GRAVITY, coords[0], coords[1]);
        }
    }

    void showSuggestions() {
        if (mSuggestionsPopupWindow == null) {
            mSuggestionsPopupWindow = new SuggestionsPopupWindow();
        }
        hideControllers();
        mSuggestionsPopupWindow.show();
    }

    void hideSuggestions() {
        if (mSuggestionsPopupWindow != null) {
            mSuggestionsPopupWindow.hide();
        }
    }

    /**
     * If provided, this ActionMode.Callback will be used to create the ActionMode when text
     * selection is initiated in this View.
@@ -8429,16 +8641,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        }
    }

    private class PastePopupMenu implements OnClickListener {
    private class PastePopupWindow implements OnClickListener {
        private final PopupWindow mContainer;
        private int mPositionX;
        private int mPositionY;
        private final View[] mPasteViews = new View[4];
        private final int[] mPasteViewLayouts = new int[] { 
                mTextEditPasteWindowLayout,  mTextEditNoPasteWindowLayout, 
                mTextEditSidePasteWindowLayout, mTextEditSideNoPasteWindowLayout };
        
        public PastePopupMenu() {
        public PastePopupWindow() {
            mContainer = new PopupWindow(TextView.this.mContext, null,
                    com.android.internal.R.attr.textSelectHandleWindowStyle);
            mContainer.setSplitTouchEnabled(true);
@@ -8521,14 +8731,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener

            convertFromViewportToContentCoordinates(bounds);

            mPositionX = bounds.left;
            mPositionY = bounds.top;


            final int[] coords = mTempCoords;
            TextView.this.getLocationInWindow(coords);
            coords[0] += mPositionX;
            coords[1] += mPositionY;
            coords[0] += bounds.left;
            coords[1] += bounds.top;

            final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels;
            if (coords[1] < 0) {
@@ -8883,10 +9089,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        private static final int DELAY_BEFORE_FADE_OUT = 4000;
        private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds

        // Used to detect taps on the insertion handle, which will affect the PastePopupMenu
        // Used to detect taps on the insertion handle, which will affect the PastePopupWindow
        private long mTouchTimer;
        private float mDownPositionX, mDownPositionY;
        private PastePopupMenu mPastePopupWindow;
        private PastePopupWindow mPastePopupWindow;
        private Runnable mHider;
        private Runnable mPastePopupShower;

@@ -9026,7 +9232,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        void showAssociatedPopupWindow() {
            if (mPastePopupWindow == null) {
                // Lazy initialisation: create when actually shown only.
                mPastePopupWindow = new PastePopupMenu();
                mPastePopupWindow = new PastePopupWindow();
            }
            mPastePopupWindow.show();
        }
@@ -9237,6 +9443,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
            mEndHandle.show();

            hideInsertionPointCursorController();
            hideSuggestions();
        }

        public void hide() {
@@ -9264,7 +9471,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
                            final int deltaY = y - mPreviousTapPositionY;
                            final int distanceSquared = deltaX * deltaX + deltaY * deltaY;
                            if (distanceSquared < mSquaredTouchSlopDistance) {
                                startSelectionActionMode();
                                showSuggestions();
                                mDiscardNextActionUp = true;
                            }
                        }
@@ -9354,6 +9561,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
    private void hideControllers() {
        hideInsertionPointCursorController();
        stopSelectionActionMode();
        hideSuggestions();
    }

    /**
+605 B
Loading image diff...
+585 B
Loading image diff...
+27 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at
  
          http://www.apache.org/licenses/LICENSE-2.0
  
     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:paddingLeft="16dip"
          android:paddingRight="16dip"
          android:paddingTop="8dip"
          android:paddingBottom="8dip"
          android:layout_gravity="center"
          android:textAppearance="?android:attr/textAppearanceMedium"
          android:textColor="@android:color/black" />
Loading