/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.eclipse.org/org/documents/epl-v10.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ide.eclipse.adt.internal.editors.layout.gle2; import com.android.ide.common.api.Rect; import org.eclipse.swt.SWT; import org.eclipse.swt.dnd.DND; import org.eclipse.swt.dnd.DragSource; import org.eclipse.swt.dnd.DragSourceEvent; import org.eclipse.swt.dnd.DragSourceListener; import org.eclipse.swt.dnd.DropTarget; import org.eclipse.swt.dnd.DropTargetEvent; import org.eclipse.swt.dnd.DropTargetListener; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.events.MouseMoveListener; import org.eclipse.swt.events.TypedEvent; import org.eclipse.swt.graphics.Device; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.Rectangle; import java.util.ArrayList; import java.util.List; /** * The {@link GestureManager} is is the central manager of gestures; it is responsible * for recognizing when particular gestures should begin and terminate. It * listens to the drag, mouse and keyboard systems to find out when to start * gestures and in order to update the gestures along the way. */ public class GestureManager { /** The canvas which owns this GestureManager. */ private final LayoutCanvas mCanvas; /** The currently executing gesture, or null. */ private Gesture mCurrentGesture; /** A listener for drop target events. */ private final DropTargetListener mDropListener = new CanvasDropListener(); /** A listener for drag source events. */ private final DragSourceListener mDragSourceListener = new CanvasDragSourceListener(); /** * The list of overlays associated with {@link #mCurrentGesture}. Will be * null before it has been initialized lazily by the paint routine (the * initialized value can never be null, but it can be an empty collection). */ private List<Overlay> mOverlays; /** * Most recently seen mouse position (x coordinate). We keep a copy of this * value since we sometimes need to know it when we aren't told about the * mouse position (such as when a keystroke is received, such as an arrow * key in order to tweak the current drop position) */ protected int mLastMouseX; /** * Most recently seen mouse position (y coordinate). We keep a copy of this * value since we sometimes need to know it when we aren't told about the * mouse position (such as when a keystroke is received, such as an arrow * key in order to tweak the current drop position) */ protected int mLastMouseY; /** * Most recently seen mouse mask. We keep a copy of this since in some * scenarios (such as on a drag gesture) we don't get access to it. */ protected int mLastStateMask; /** * Listener for mouse motion, click and keyboard events. */ private Listener mListener; /** * When we the drag leaves, we don't know if that's the last we'll see of * this drag or if it's just temporarily outside the canvas and it will * return. We want to restore it if it comes back. This is also necessary * because even on a drop we'll receive a * {@link DropTargetListener#dragLeave} right before the drop, and we need * to restore it in the drop. Therefore, when we lose a {@link DropGesture} * to a {@link DropTargetListener#dragLeave}, we store a reference to the * current gesture as a {@link #mZombieGesture}, since the gesture is dead * but might be brought back to life if we see a subsequent * {@link DropTargetListener#dragEnter} before another gesture begins. */ private DropGesture mZombieGesture; /** * Constructs a new {@link GestureManager} for the given * {@link LayoutCanvas}. * * @param canvas The canvas which controls this {@link GestureManager} */ public GestureManager(LayoutCanvas canvas) { this.mCanvas = canvas; } /** * Returns the canvas associated with this GestureManager. * * @return The {@link LayoutCanvas} associated with this GestureManager. * Never null. */ public LayoutCanvas getCanvas() { return mCanvas; } /** * Returns the current gesture, if one is in progress, and otherwise returns * null. * * @return The current gesture or null. */ public Gesture getCurrentGesture() { return mCurrentGesture; } /** * Paints the overlays associated with the current gesture, if any. * * @param gc The graphics object to paint into. */ public void paint(GC gc) { if (mCurrentGesture == null) { return; } if (mOverlays == null) { mOverlays = mCurrentGesture.createOverlays(); Device device = gc.getDevice(); for (Overlay overlay : mOverlays) { overlay.create(device); } } for (Overlay overlay : mOverlays) { overlay.paint(gc); } } /** * Returns the {@link DropTargetListener} used by the GestureManager. This * is a bit leaky, but the Outline is reusing all this code... This should * be separated out. */ /* package */DropTargetListener getDropTargetListener() { return mDropListener; } /** * Returns the {@link DragSourceListener} used by the GestureManager. This * is a bit leaky, but the Outline is reusing all this code... This should * be separated out. */ /* package */DragSourceListener getDragSourceListener() { return mDragSourceListener; } /** * Registers all the listeners needed by the {@link GestureManager}. * * @param dragSource The drag source in the {@link LayoutCanvas} to listen * to. * @param dropTarget The drop target in the {@link LayoutCanvas} to listen * to. */ public void registerListeners(DragSource dragSource, DropTarget dropTarget) { assert mListener == null; mListener = new Listener(); mCanvas.addMouseMoveListener(mListener); mCanvas.addMouseListener(mListener); mCanvas.addKeyListener(mListener); if (dragSource != null) { dragSource.addDragListener(mDragSourceListener); } if (dropTarget != null) { dropTarget.addDropListener(mDropListener); } } /** * Unregisters all the listeners previously registered by * {@link #registerListeners}. * * @param dragSource The drag source in the {@link LayoutCanvas} to stop * listening to. * @param dropTarget The drop target in the {@link LayoutCanvas} to stop * listening to. */ public void unregisterListeners(DragSource dragSource, DropTarget dropTarget) { if (mCanvas.isDisposed()) { // If the LayoutCanvas is already disposed, we shouldn't try to unregister // the listeners; they are already not active and an attempt to remove the // listener will throw a widget-is-disposed exception. mListener = null; return; } if (mListener != null) { mCanvas.removeMouseMoveListener(mListener); mCanvas.removeMouseListener(mListener); mCanvas.removeKeyListener(mListener); mListener = null; } if (dragSource != null) { dragSource.removeDragListener(mDragSourceListener); } if (dropTarget != null) { dropTarget.removeDropListener(mDropListener); } } /** * Starts the given gesture. * * @param mousePos The most recent mouse coordinate applicable to the new * gesture, in control coordinates. * @param gesture The gesture to initiate */ private void startGesture(ControlPoint mousePos, Gesture gesture, int mask) { if (mCurrentGesture != null) { finishGesture(mousePos, true); assert mCurrentGesture == null; } if (gesture != null) { mCurrentGesture = gesture; mCurrentGesture.begin(mousePos, mask); } } /** * Updates the current gesture, if any, for the given event. * * @param mousePos The most recent mouse coordinate applicable to the new * gesture, in control coordinates. * @param event The event corresponding to this update. May be null. Don't * make any assumptions about the type of this event - for * example, it may not always be a MouseEvent, it could be a * DragSourceEvent, etc. */ private void updateMouse(ControlPoint mousePos, TypedEvent event) { if (mCurrentGesture != null) { mCurrentGesture.update(mousePos); } } /** * Finish the given gesture, either from successful completion or from * cancellation. * * @param mousePos The most recent mouse coordinate applicable to the new * gesture, in control coordinates. * @param canceled True if and only if the gesture was canceled. */ private void finishGesture(ControlPoint mousePos, boolean canceled) { if (mCurrentGesture != null) { mCurrentGesture.end(mousePos, canceled); if (mOverlays != null) { for (Overlay overlay : mOverlays) { overlay.dispose(); } mOverlays = null; } mCurrentGesture = null; mZombieGesture = null; mLastStateMask = 0; } } /** * Helper class which implements the {@link MouseMoveListener}, * {@link MouseListener} and {@link KeyListener} interfaces. */ private class Listener implements MouseMoveListener, MouseListener, KeyListener { // --- MouseMoveListener --- public void mouseMove(MouseEvent e) { mLastMouseX = e.x; mLastMouseY = e.y; mLastStateMask = e.stateMask; if ((e.stateMask & SWT.BUTTON_MASK) != 0) { if (mCurrentGesture != null) { ControlPoint controlPoint = ControlPoint.create(mCanvas, e); updateMouse(controlPoint, e); mCanvas.redraw(); } } else { mCanvas.hover(e); } } // --- MouseListener --- public void mouseUp(MouseEvent e) { if (mCurrentGesture == null) { // Just a click, select mCanvas.getSelectionManager().select(e); } finishGesture(ControlPoint.create(mCanvas, e), false); mCanvas.redraw(); } public void mouseDown(MouseEvent e) { mLastMouseX = e.x; mLastMouseY = e.y; mLastStateMask = e.stateMask; // Not yet used. Should be, for Mac and Linux. } public void mouseDoubleClick(MouseEvent e) { // SWT delivers a double click event even if you click two different buttons // in rapid succession. In any case, we only want to let you double click the // first button to warp to XML: if (e.button == 1) { // Warp to the text editor and show the corresponding XML for the // double-clicked widget LayoutPoint p = ControlPoint.create(mCanvas, e).toLayout(); CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p); if (vi != null) { mCanvas.show(vi); } } } // --- KeyListener --- public void keyPressed(KeyEvent e) { if (mCurrentGesture != null) { mCurrentGesture.keyPressed(e); } else { if (e.keyCode == SWT.ESC) { // It appears that SWT does NOT (on the Mac) pass any // key strokes other than modifier keys when the mouse // button is pressed!! ControlPoint controlPoint = ControlPoint.create(mCanvas, mLastMouseX, mLastMouseY); finishGesture(controlPoint, true); return; } } } public void keyReleased(KeyEvent e) { if (mCurrentGesture != null) { mCurrentGesture.keyReleased(e); } } } /** Listener for Drag & Drop events. */ private class CanvasDropListener implements DropTargetListener { public CanvasDropListener() { } /** * The cursor has entered the drop target boundaries. {@inheritDoc} */ public void dragEnter(DropTargetEvent event) { mCanvas.showInvisibleViews(true); if (mCurrentGesture == null) { Gesture newGesture = mZombieGesture; if (newGesture == null) { newGesture = new MoveGesture(mCanvas); } else { mZombieGesture = null; } startGesture(ControlPoint.create(mCanvas, event), newGesture, 0); } if (mCurrentGesture instanceof DropGesture) { ((DropGesture) mCurrentGesture).dragEnter(event); } } /** * The cursor is moving over the drop target. {@inheritDoc} */ public void dragOver(DropTargetEvent event) { if (mCurrentGesture instanceof DropGesture) { ((DropGesture) mCurrentGesture).dragOver(event); } } /** * The cursor has left the drop target boundaries OR data is about to be * dropped. {@inheritDoc} */ public void dragLeave(DropTargetEvent event) { if (mCurrentGesture instanceof DropGesture) { DropGesture dropGesture = (DropGesture) mCurrentGesture; dropGesture.dragLeave(event); finishGesture(ControlPoint.create(mCanvas, event), true); mZombieGesture = dropGesture; } mCanvas.showInvisibleViews(false); } /** * The drop is about to be performed. The drop target is given a last * chance to change the nature of the drop. {@inheritDoc} */ public void dropAccept(DropTargetEvent event) { Gesture gesture = mCurrentGesture != null ? mCurrentGesture : mZombieGesture; if (gesture instanceof DropGesture) { ((DropGesture) gesture).dropAccept(event); } } /** * The data is being dropped. {@inheritDoc} */ public void drop(final DropTargetEvent event) { // See if we had a gesture just prior to the drop (we receive a dragLeave // right before the drop which we don't know whether means the cursor has // left the canvas for good or just before a drop) Gesture gesture = mCurrentGesture != null ? mCurrentGesture : mZombieGesture; mZombieGesture = null; if (gesture instanceof DropGesture) { ((DropGesture) gesture).drop(event); finishGesture(ControlPoint.create(mCanvas, event), true); } } /** * The operation being performed has changed (e.g. modifier key). * {@inheritDoc} */ public void dragOperationChanged(DropTargetEvent event) { if (mCurrentGesture instanceof DropGesture) { ((DropGesture) mCurrentGesture).dragOperationChanged(event); } } } /** * Our canvas {@link DragSourceListener}. Handles drag being started and * finished and generating the drag data. */ private class CanvasDragSourceListener implements DragSourceListener { /** * The current selection being dragged. This may be a subset of the * canvas selection due to the "sanitize" pass. Can be empty but never * null. */ private final ArrayList<SelectionItem> mDragSelection = new ArrayList<SelectionItem>(); private SimpleElement[] mDragElements; /** * The user has begun the actions required to drag the widget. * <p/> * Initiate a drag only if there is one or more item selected. If * there's none, try to auto-select the one under the cursor. * {@inheritDoc} */ public void dragStart(DragSourceEvent e) { LayoutPoint p = LayoutPoint.create(mCanvas, e); // We need a selection (simple or multiple) to do any transfer. // If there's a selection *and* the cursor is over this selection, // use all the currently selected elements. // If there is no selection or the cursor is not over a selected // element, *change* the selection to match the element under the // cursor and use that. If nothing can be selected, abort the drag // operation. List<SelectionItem> selections = mCanvas.getSelectionManager().getSelections(); mDragSelection.clear(); if (!selections.isEmpty()) { // Is the cursor on top of a selected element? boolean insideSelection = false; for (SelectionItem cs : selections) { if (!cs.isRoot() && cs.getRect().contains(p.x, p.y)) { insideSelection = true; break; } } if (!insideSelection) { CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p); if (vi != null && !vi.isRoot()) { mCanvas.getSelectionManager().selectSingle(vi); insideSelection = true; } } if (insideSelection) { // We should now have a proper selection that matches the // cursor. Let's use this one. We make a copy of it since // the "sanitize" pass below might remove some of the // selected objects. if (selections.size() == 1) { // You are dragging just one element - this might or // might not be the root, but if it's the root that is // fine since we will let you drag the root if it is the // only thing you are dragging. mDragSelection.addAll(selections); } else { // Only drag non-root items. for (SelectionItem cs : selections) { if (!cs.isRoot()) { mDragSelection.add(cs); } } } } } // If you are dragging a non-selected item, select it if (mDragSelection.isEmpty()) { CanvasViewInfo vi = mCanvas.getViewHierarchy().findViewInfoAt(p); if (vi != null && !vi.isRoot()) { mCanvas.getSelectionManager().selectSingle(vi); mDragSelection.addAll(selections); } } SelectionManager.sanitize(mDragSelection); e.doit = !mDragSelection.isEmpty(); int imageCount = mDragSelection.size(); if (e.doit) { mDragElements = SelectionItem.getAsElements(mDragSelection); GlobalCanvasDragInfo.getInstance().startDrag(mDragElements, mDragSelection.toArray(new SelectionItem[imageCount]), mCanvas, new Runnable() { public void run() { mCanvas.getClipboardSupport().deleteSelection("Remove", mDragSelection); } }); } // If you drag on the -background-, we make that into a marquee // selection if (!e.doit || (imageCount == 1 && mDragSelection.get(0).isRoot())) { boolean toggle = (mLastStateMask & (SWT.CTRL | SWT.SHIFT | SWT.COMMAND)) != 0; startGesture(ControlPoint.create(mCanvas, e), new MarqueeGesture(mCanvas, toggle), mLastStateMask); e.detail = DND.DROP_NONE; e.doit = false; } else { // Otherwise, the drag means you are moving something mCanvas.showInvisibleViews(true); startGesture(ControlPoint.create(mCanvas, e), new MoveGesture(mCanvas), 0); // Render drag-images: Copy portions of the full screen render. Image image = mCanvas.getImageOverlay().getImage(); if (image != null) { /** * Transparency of the dragged image ([0-255]). We're using 30% * translucency to make the image faint and not obscure the drag * feedback below it. */ final byte DRAG_TRANSPARENCY = (byte) (0.3 * 255); List<Rectangle> rectangles = new ArrayList<Rectangle>(imageCount); if (imageCount > 0) { ImageData data = image.getImageData(); Rectangle imageRectangle = new Rectangle(0, 0, data.width, data.height); for (SelectionItem item : mDragSelection) { Rectangle bounds = item.getRect(); // Some bounds can be outside the rendered rectangle (for // example, in an absolute layout, you can have negative // coordinates), so create the intersection of these bounds. Rectangle clippedBounds = imageRectangle.intersection(bounds); rectangles.add(clippedBounds); } Rectangle boundingBox = ImageUtils.getBoundingRectangle(rectangles); double scale = mCanvas.getHorizontalTransform().getScale(); e.image = SwtUtils.drawRectangles(image, rectangles, boundingBox, scale, DRAG_TRANSPARENCY); // Set the image offset such that we preserve the relative // distance between the mouse pointer and the top left corner of // the dragged view int deltaX = (int) (scale * (boundingBox.x - p.x)); int deltaY = (int) (scale * (boundingBox.y - p.y)); e.offsetX = -deltaX; e.offsetY = -deltaY; // View rules may need to know it as well GlobalCanvasDragInfo dragInfo = GlobalCanvasDragInfo.getInstance(); Rect dragBounds = null; int width = (int) (scale * boundingBox.width); int height = (int) (scale * boundingBox.height); dragBounds = new Rect(deltaX, deltaY, width, height); dragInfo.setDragBounds(dragBounds); } } } // No hover during drag (since no mouse over events are delivered // during a drag to keep the hovers up to date anyway) mCanvas.clearHover(); mCanvas.redraw(); } /** * Callback invoked when data is needed for the event, typically right * before drop. The drop side decides what type of transfer to use and * this side must now provide the adequate data. {@inheritDoc} */ public void dragSetData(DragSourceEvent e) { if (TextTransfer.getInstance().isSupportedType(e.dataType)) { e.data = SelectionItem.getAsText(mCanvas, mDragSelection); return; } if (SimpleXmlTransfer.getInstance().isSupportedType(e.dataType)) { e.data = mDragElements; return; } // otherwise we failed e.detail = DND.DROP_NONE; e.doit = false; } /** * Callback invoked when the drop has been finished either way. On a * successful move, remove the originating elements. */ public void dragFinished(DragSourceEvent e) { // Clear the selection mDragSelection.clear(); mDragElements = null; GlobalCanvasDragInfo.getInstance().stopDrag(); finishGesture(ControlPoint.create(mCanvas, e), e.detail == DND.DROP_NONE); mCanvas.showInvisibleViews(false); mCanvas.redraw(); } } }