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