| 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 | } |