// Copyright 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.content.browser.input; import android.content.ClipboardManager; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.widget.PopupWindow; import com.google.common.annotations.VisibleForTesting; import org.chromium.content.browser.PositionObserver; /** * CursorController for inserting text at the cursor position. */ public abstract class InsertionHandleController implements CursorController { /** The handle view, lazily created when first shown */ private HandleView mHandle; /** The view over which the insertion handle should be shown */ private final View mParent; /** True iff the insertion handle is currently showing */ private boolean mIsShowing; /** True iff the insertion handle can be shown automatically when selection changes */ private boolean mAllowAutomaticShowing; private final Context mContext; private final PositionObserver mPositionObserver; public InsertionHandleController(View parent, PositionObserver positionObserver) { mParent = parent; mContext = parent.getContext(); mPositionObserver = positionObserver; } /** Allows the handle to be shown automatically when cursor position changes */ public void allowAutomaticShowing() { mAllowAutomaticShowing = true; } /** Disallows the handle from being shown automatically when cursor position changes */ public void hideAndDisallowAutomaticShowing() { hide(); mAllowAutomaticShowing = false; } /** * Shows the handle. */ public void showHandle() { createHandleIfNeeded(); showHandleIfNeeded(); } void showPastePopup() { if (mIsShowing) { mHandle.showPastePopupWindow(); } } public void showHandleWithPastePopup() { showHandle(); showPastePopup(); } /** * @return whether the handle is being dragged. */ public boolean isDragging() { return mHandle != null && mHandle.isDragging(); } /** Shows the handle at the given coordinates, as long as automatic showing is allowed */ public void onCursorPositionChanged() { if (mAllowAutomaticShowing) { showHandle(); } } /** * Moves the handle so that it points at the given coordinates. * @param x Handle x in physical pixels. * @param y Handle y in physical pixels. */ public void setHandlePosition(float x, float y) { mHandle.positionAt((int) x, (int) y); } /** * If the handle is not visible, sets its visibility to View.VISIBLE and begins fading it in. */ public void beginHandleFadeIn() { mHandle.beginFadeIn(); } /** * Sets the handle to the given visibility. */ public void setHandleVisibility(int visibility) { mHandle.setVisibility(visibility); } int getHandleX() { return mHandle.getAdjustedPositionX(); } int getHandleY() { return mHandle.getAdjustedPositionY(); } @VisibleForTesting public HandleView getHandleViewForTest() { return mHandle; } @Override public void onTouchModeChanged(boolean isInTouchMode) { if (!isInTouchMode) { hide(); } } @Override public void hide() { if (mIsShowing) { if (mHandle != null) mHandle.hide(); mIsShowing = false; } } @Override public boolean isShowing() { return mIsShowing; } @Override public void beforeStartUpdatingPosition(HandleView handle) {} @Override public void updatePosition(HandleView handle, int x, int y) { setCursorPosition(x, y); } /** * The concrete implementation must cause the cursor position to move to the given * coordinates and (possibly asynchronously) set the insertion handle position * after the cursor position change is made via setHandlePosition. * @param x * @param y */ protected abstract void setCursorPosition(int x, int y); /** Pastes the contents of clipboard at the current insertion point */ protected abstract void paste(); /** Returns the current line height in pixels */ protected abstract int getLineHeight(); @Override public void onDetached() {} boolean canPaste() { return ((ClipboardManager) mContext.getSystemService( Context.CLIPBOARD_SERVICE)).hasPrimaryClip(); } private void createHandleIfNeeded() { if (mHandle == null) { mHandle = new HandleView(this, HandleView.CENTER, mParent, mPositionObserver); } } private void showHandleIfNeeded() { if (!mIsShowing) { mIsShowing = true; mHandle.show(); setHandleVisibility(HandleView.VISIBLE); } } /* * This class is based on TextView.PastePopupMenu. */ class PastePopupMenu implements OnClickListener { private final PopupWindow mContainer; private int mPositionX; private int mPositionY; private final View[] mPasteViews; private final int[] mPasteViewLayouts; public PastePopupMenu() { mContainer = new PopupWindow(mContext, null, android.R.attr.textSelectHandleWindowStyle); mContainer.setSplitTouchEnabled(true); mContainer.setClippingEnabled(false); mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); final int[] POPUP_LAYOUT_ATTRS = { android.R.attr.textEditPasteWindowLayout, android.R.attr.textEditNoPasteWindowLayout, android.R.attr.textEditSidePasteWindowLayout, android.R.attr.textEditSideNoPasteWindowLayout, }; mPasteViews = new View[POPUP_LAYOUT_ATTRS.length]; mPasteViewLayouts = new int[POPUP_LAYOUT_ATTRS.length]; TypedArray attrs = mContext.obtainStyledAttributes(POPUP_LAYOUT_ATTRS); for (int i = 0; i < attrs.length(); ++i) { mPasteViewLayouts[i] = attrs.getResourceId(attrs.getIndex(i), 0); } attrs.recycle(); } private int viewIndex(boolean onTop) { return (onTop ? 0 : 1 << 1) + (canPaste() ? 0 : 1 << 0); } private void updateContent(boolean onTop) { final int viewIndex = viewIndex(onTop); View view = mPasteViews[viewIndex]; if (view == null) { final int layout = mPasteViewLayouts[viewIndex]; LayoutInflater inflater = (LayoutInflater) mContext. getSystemService(Context.LAYOUT_INFLATER_SERVICE); if (inflater != null) { view = inflater.inflate(layout, null); } if (view == null) { throw new IllegalArgumentException("Unable to inflate TextEdit paste window"); } final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); view.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); view.measure(size, size); view.setOnClickListener(this); mPasteViews[viewIndex] = view; } mContainer.setContentView(view); } void show() { updateContent(true); positionAtCursor(); } void hide() { mContainer.dismiss(); } boolean isShowing() { return mContainer.isShowing(); } @Override public void onClick(View v) { if (canPaste()) { paste(); } hide(); } void positionAtCursor() { View contentView = mContainer.getContentView(); int width = contentView.getMeasuredWidth(); int height = contentView.getMeasuredHeight(); int lineHeight = getLineHeight(); mPositionX = (int) (mHandle.getAdjustedPositionX() - width / 2.0f); mPositionY = mHandle.getAdjustedPositionY() - height - lineHeight; final int[] coords = new int[2]; mParent.getLocationInWindow(coords); coords[0] += mPositionX; coords[1] += mPositionY; final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels; if (coords[1] < 0) { updateContent(false); // Update dimensions from new view contentView = mContainer.getContentView(); width = contentView.getMeasuredWidth(); height = contentView.getMeasuredHeight(); // Vertical clipping, move under edited line and to the side of insertion cursor // TODO bottom clipping in case there is no system bar coords[1] += height; coords[1] += lineHeight; // Move to right hand side of insertion cursor by default. TODO RTL text. final Drawable handle = mHandle.getDrawable(); final int handleHalfWidth = handle.getIntrinsicWidth() / 2; if (mHandle.getAdjustedPositionX() + width < screenWidth) { coords[0] += handleHalfWidth + width / 2; } else { coords[0] -= handleHalfWidth + width / 2; } } else { // Horizontal clipping coords[0] = Math.max(0, coords[0]); coords[0] = Math.min(screenWidth - width, coords[0]); } mContainer.showAtLocation(mParent, Gravity.NO_GRAVITY, coords[0], coords[1]); } } }