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