• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 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 final 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 final Context mContext;
41 
42     private final PositionObserver mPositionObserver;
43 
InsertionHandleController(View parent, PositionObserver positionObserver)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 */
allowAutomaticShowing()52     public void allowAutomaticShowing() {
53         mAllowAutomaticShowing = true;
54     }
55 
56     /** Disallows the handle from being shown automatically when cursor position changes */
hideAndDisallowAutomaticShowing()57     public void hideAndDisallowAutomaticShowing() {
58         hide();
59         mAllowAutomaticShowing = false;
60     }
61 
62     /**
63      * Shows the handle.
64      */
showHandle()65     public void showHandle() {
66         createHandleIfNeeded();
67         showHandleIfNeeded();
68     }
69 
showPastePopup()70     void showPastePopup() {
71         if (mIsShowing) {
72             mHandle.showPastePopupWindow();
73         }
74     }
75 
showHandleWithPastePopup()76     public void showHandleWithPastePopup() {
77         showHandle();
78         showPastePopup();
79     }
80 
81     /**
82      * @return whether the handle is being dragged.
83      */
isDragging()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 */
onCursorPositionChanged()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      */
setHandlePosition(float x, float y)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      */
beginHandleFadeIn()107     public void beginHandleFadeIn() {
108         mHandle.beginFadeIn();
109     }
110 
111     /**
112      * Sets the handle to the given visibility.
113      */
setHandleVisibility(int visibility)114     public void setHandleVisibility(int visibility) {
115         mHandle.setVisibility(visibility);
116     }
117 
getHandleX()118     int getHandleX() {
119         return mHandle.getAdjustedPositionX();
120     }
121 
getHandleY()122     int getHandleY() {
123         return mHandle.getAdjustedPositionY();
124     }
125 
126     @VisibleForTesting
getHandleViewForTest()127     public HandleView getHandleViewForTest() {
128         return mHandle;
129     }
130 
131     @Override
onTouchModeChanged(boolean isInTouchMode)132     public void onTouchModeChanged(boolean isInTouchMode) {
133         if (!isInTouchMode) {
134             hide();
135         }
136     }
137 
138     @Override
hide()139     public void hide() {
140         if (mIsShowing) {
141             if (mHandle != null) mHandle.hide();
142             mIsShowing = false;
143         }
144     }
145 
146     @Override
isShowing()147     public boolean isShowing() {
148         return mIsShowing;
149     }
150 
151     @Override
beforeStartUpdatingPosition(HandleView handle)152     public void beforeStartUpdatingPosition(HandleView handle) {}
153 
154     @Override
updatePosition(HandleView handle, int x, int y)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      */
setCursorPosition(int x, int y)166     protected abstract void setCursorPosition(int x, int y);
167 
168     /** Pastes the contents of clipboard at the current insertion point */
paste()169     protected abstract void paste();
170 
171     /** Returns the current line height in pixels */
getLineHeight()172     protected abstract int getLineHeight();
173 
174     @Override
onDetached()175     public void onDetached() {}
176 
canPaste()177     boolean canPaste() {
178         return ((ClipboardManager) mContext.getSystemService(
179                 Context.CLIPBOARD_SERVICE)).hasPrimaryClip();
180     }
181 
createHandleIfNeeded()182     private void createHandleIfNeeded() {
183         if (mHandle == null) {
184             mHandle = new HandleView(this, HandleView.CENTER, mParent, mPositionObserver);
185         }
186     }
187 
showHandleIfNeeded()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 final View[] mPasteViews;
204         private final int[] mPasteViewLayouts;
205 
PastePopupMenu()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 
viewIndex(boolean onTop)232         private int viewIndex(boolean onTop) {
233             return (onTop ? 0 : 1 << 1) + (canPaste() ? 0 : 1 << 0);
234         }
235 
updateContent(boolean onTop)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 
show()265         void show() {
266             updateContent(true);
267             positionAtCursor();
268         }
269 
hide()270         void hide() {
271             mContainer.dismiss();
272         }
273 
isShowing()274         boolean isShowing() {
275             return mContainer.isShowing();
276         }
277 
278         @Override
onClick(View v)279         public void onClick(View v) {
280             if (canPaste()) {
281                 paste();
282             }
283             hide();
284         }
285 
positionAtCursor()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