Home | History | Annotate | Download | only in input
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 package org.chromium.content.browser.input;
      6 
      7 import android.content.ClipboardManager;
      8 import android.content.Context;
      9 import android.content.res.TypedArray;
     10 import android.graphics.drawable.Drawable;
     11 import android.view.Gravity;
     12 import android.view.LayoutInflater;
     13 import android.view.View;
     14 import android.view.View.OnClickListener;
     15 import android.view.ViewGroup;
     16 import android.view.ViewGroup.LayoutParams;
     17 import android.widget.PopupWindow;
     18 
     19 import com.google.common.annotations.VisibleForTesting;
     20 
     21 import org.chromium.content.browser.PositionObserver;
     22 
     23 /**
     24  * CursorController for inserting text at the cursor position.
     25  */
     26 public abstract class InsertionHandleController implements CursorController {
     27 
     28     /** The handle view, lazily created when first shown */
     29     private HandleView mHandle;
     30 
     31     /** The view over which the insertion handle should be shown */
     32     private View mParent;
     33 
     34     /** True iff the insertion handle is currently showing */
     35     private boolean mIsShowing;
     36 
     37     /** True iff the insertion handle can be shown automatically when selection changes */
     38     private boolean mAllowAutomaticShowing;
     39 
     40     private Context mContext;
     41 
     42     private PositionObserver mPositionObserver;
     43 
     44     public InsertionHandleController(View parent, PositionObserver positionObserver) {
     45         mParent = parent;
     46 
     47         mContext = parent.getContext();
     48         mPositionObserver = positionObserver;
     49     }
     50 
     51     /** Allows the handle to be shown automatically when cursor position changes */
     52     public void allowAutomaticShowing() {
     53         mAllowAutomaticShowing = true;
     54     }
     55 
     56     /** Disallows the handle from being shown automatically when cursor position changes */
     57     public void hideAndDisallowAutomaticShowing() {
     58         hide();
     59         mAllowAutomaticShowing = false;
     60     }
     61 
     62     /**
     63      * Shows the handle.
     64      */
     65     public void showHandle() {
     66         createHandleIfNeeded();
     67         showHandleIfNeeded();
     68     }
     69 
     70     void showPastePopup() {
     71         if (mIsShowing) {
     72             mHandle.showPastePopupWindow();
     73         }
     74     }
     75 
     76     public void showHandleWithPastePopup() {
     77         showHandle();
     78         showPastePopup();
     79     }
     80 
     81     /**
     82      * @return whether the handle is being dragged.
     83      */
     84     public boolean isDragging() {
     85         return mHandle != null && mHandle.isDragging();
     86     }
     87 
     88     /** Shows the handle at the given coordinates, as long as automatic showing is allowed */
     89     public void onCursorPositionChanged() {
     90         if (mAllowAutomaticShowing) {
     91             showHandle();
     92         }
     93     }
     94 
     95     /**
     96      * Moves the handle so that it points at the given coordinates.
     97      * @param x Handle x in physical pixels.
     98      * @param y Handle y in physical pixels.
     99      */
    100     public void setHandlePosition(float x, float y) {
    101         mHandle.positionAt((int) x, (int) y);
    102     }
    103 
    104     /**
    105      * If the handle is not visible, sets its visibility to View.VISIBLE and begins fading it in.
    106      */
    107     public void beginHandleFadeIn() {
    108         mHandle.beginFadeIn();
    109     }
    110 
    111     /**
    112      * Sets the handle to the given visibility.
    113      */
    114     public void setHandleVisibility(int visibility) {
    115         mHandle.setVisibility(visibility);
    116     }
    117 
    118     int getHandleX() {
    119         return mHandle.getAdjustedPositionX();
    120     }
    121 
    122     int getHandleY() {
    123         return mHandle.getAdjustedPositionY();
    124     }
    125 
    126     @VisibleForTesting
    127     public HandleView getHandleViewForTest() {
    128         return mHandle;
    129     }
    130 
    131     @Override
    132     public void onTouchModeChanged(boolean isInTouchMode) {
    133         if (!isInTouchMode) {
    134             hide();
    135         }
    136     }
    137 
    138     @Override
    139     public void hide() {
    140         if (mIsShowing) {
    141             if (mHandle != null) mHandle.hide();
    142             mIsShowing = false;
    143         }
    144     }
    145 
    146     @Override
    147     public boolean isShowing() {
    148         return mIsShowing;
    149     }
    150 
    151     @Override
    152     public void beforeStartUpdatingPosition(HandleView handle) {}
    153 
    154     @Override
    155     public void updatePosition(HandleView handle, int x, int y) {
    156         setCursorPosition(x, y);
    157     }
    158 
    159     /**
    160      * The concrete implementation must cause the cursor position to move to the given
    161      * coordinates and (possibly asynchronously) set the insertion handle position
    162      * after the cursor position change is made via setHandlePosition.
    163      * @param x
    164      * @param y
    165      */
    166     protected abstract void setCursorPosition(int x, int y);
    167 
    168     /** Pastes the contents of clipboard at the current insertion point */
    169     protected abstract void paste();
    170 
    171     /** Returns the current line height in pixels */
    172     protected abstract int getLineHeight();
    173 
    174     @Override
    175     public void onDetached() {}
    176 
    177     boolean canPaste() {
    178         return ((ClipboardManager)mContext.getSystemService(
    179                 Context.CLIPBOARD_SERVICE)).hasPrimaryClip();
    180     }
    181 
    182     private void createHandleIfNeeded() {
    183         if (mHandle == null) {
    184             mHandle = new HandleView(this, HandleView.CENTER, mParent, mPositionObserver);
    185         }
    186     }
    187 
    188     private void showHandleIfNeeded() {
    189         if (!mIsShowing) {
    190             mIsShowing = true;
    191             mHandle.show();
    192             setHandleVisibility(HandleView.VISIBLE);
    193         }
    194     }
    195 
    196     /*
    197      * This class is based on TextView.PastePopupMenu.
    198      */
    199     class PastePopupMenu implements OnClickListener {
    200         private final PopupWindow mContainer;
    201         private int mPositionX;
    202         private int mPositionY;
    203         private View[] mPasteViews;
    204         private int[] mPasteViewLayouts;
    205 
    206         public PastePopupMenu() {
    207             mContainer = new PopupWindow(mContext, null,
    208                     android.R.attr.textSelectHandleWindowStyle);
    209             mContainer.setSplitTouchEnabled(true);
    210             mContainer.setClippingEnabled(false);
    211 
    212             mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
    213             mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
    214 
    215             final int[] POPUP_LAYOUT_ATTRS = {
    216                 android.R.attr.textEditPasteWindowLayout,
    217                 android.R.attr.textEditNoPasteWindowLayout,
    218                 android.R.attr.textEditSidePasteWindowLayout,
    219                 android.R.attr.textEditSideNoPasteWindowLayout,
    220             };
    221 
    222             mPasteViews = new View[POPUP_LAYOUT_ATTRS.length];
    223             mPasteViewLayouts = new int[POPUP_LAYOUT_ATTRS.length];
    224 
    225             TypedArray attrs = mContext.obtainStyledAttributes(POPUP_LAYOUT_ATTRS);
    226             for (int i = 0; i < attrs.length(); ++i) {
    227                 mPasteViewLayouts[i] = attrs.getResourceId(attrs.getIndex(i), 0);
    228             }
    229             attrs.recycle();
    230         }
    231 
    232         private int viewIndex(boolean onTop) {
    233             return (onTop ? 0 : 1<<1) + (canPaste() ? 0 : 1 << 0);
    234         }
    235 
    236         private void updateContent(boolean onTop) {
    237             final int viewIndex = viewIndex(onTop);
    238             View view = mPasteViews[viewIndex];
    239 
    240             if (view == null) {
    241                 final int layout = mPasteViewLayouts[viewIndex];
    242                 LayoutInflater inflater = (LayoutInflater)mContext.
    243                     getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    244                 if (inflater != null) {
    245                     view = inflater.inflate(layout, null);
    246                 }
    247 
    248                 if (view == null) {
    249                     throw new IllegalArgumentException("Unable to inflate TextEdit paste window");
    250                 }
    251 
    252                 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
    253                 view.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
    254                         ViewGroup.LayoutParams.WRAP_CONTENT));
    255                 view.measure(size, size);
    256 
    257                 view.setOnClickListener(this);
    258 
    259                 mPasteViews[viewIndex] = view;
    260             }
    261 
    262             mContainer.setContentView(view);
    263         }
    264 
    265         void show() {
    266             updateContent(true);
    267             positionAtCursor();
    268         }
    269 
    270         void hide() {
    271             mContainer.dismiss();
    272         }
    273 
    274         boolean isShowing() {
    275             return mContainer.isShowing();
    276         }
    277 
    278         @Override
    279         public void onClick(View v) {
    280             if (canPaste()) {
    281                 paste();
    282             }
    283             hide();
    284         }
    285 
    286         void positionAtCursor() {
    287             View contentView = mContainer.getContentView();
    288             int width = contentView.getMeasuredWidth();
    289             int height = contentView.getMeasuredHeight();
    290 
    291             int lineHeight = getLineHeight();
    292 
    293             mPositionX = (int) (mHandle.getAdjustedPositionX() - width / 2.0f);
    294             mPositionY = mHandle.getAdjustedPositionY() - height - lineHeight;
    295 
    296             final int[] coords = new int[2];
    297             mParent.getLocationInWindow(coords);
    298             coords[0] += mPositionX;
    299             coords[1] += mPositionY;
    300 
    301             final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels;
    302             if (coords[1] < 0) {
    303                 updateContent(false);
    304                 // Update dimensions from new view
    305                 contentView = mContainer.getContentView();
    306                 width = contentView.getMeasuredWidth();
    307                 height = contentView.getMeasuredHeight();
    308 
    309                 // Vertical clipping, move under edited line and to the side of insertion cursor
    310                 // TODO bottom clipping in case there is no system bar
    311                 coords[1] += height;
    312                 coords[1] += lineHeight;
    313 
    314                 // Move to right hand side of insertion cursor by default. TODO RTL text.
    315                 final Drawable handle = mHandle.getDrawable();
    316                 final int handleHalfWidth = handle.getIntrinsicWidth() / 2;
    317 
    318                 if (mHandle.getAdjustedPositionX() + width < screenWidth) {
    319                     coords[0] += handleHalfWidth + width / 2;
    320                 } else {
    321                     coords[0] -= handleHalfWidth + width / 2;
    322                 }
    323             } else {
    324                 // Horizontal clipping
    325                 coords[0] = Math.max(0, coords[0]);
    326                 coords[0] = Math.min(screenWidth - width, coords[0]);
    327             }
    328 
    329             mContainer.showAtLocation(mParent, Gravity.NO_GRAVITY, coords[0], coords[1]);
    330         }
    331     }
    332 }
    333