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