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 | /** |
22 | * CursorController for inserting text at the cursor position. |
23 | */ |
24 | public 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 | } |