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
21import org.chromium.content.browser.PositionObserver;
22
23/**
24 * CursorController for inserting text at the cursor position.
25 */
26public 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