/*
 * 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.DropFeedback;
import com.android.ide.common.api.IViewRule;
import com.android.ide.common.api.Rect;
import com.android.ide.common.api.SegmentType;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
import com.android.sdklib.SdkConstants;
import com.android.util.Pair;

import org.eclipse.jface.action.IStatusLineManager;
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.Cursor;
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 org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorSite;

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();

    /** Tooltip shown during the gesture, or null */
    private GestureToolTip mTooltip;

    /**
     * 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;

    /**
     * Flag tracking whether we've set a message or error message on the global status
     * line (since we only want to clear that message if we have set it ourselves).
     * This is the actual message rather than a boolean such that (if we can get our
     * hands on the global message) we can check to see if the current message is the
     * one we set and only in that case clear it when it is no longer applicable.
     */
    private String mDisplayingMessage;

    /**
     * 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);
        }
    }

    /**
     * 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;
            updateMessage(null);
            updateCursor(mousePos);
            mCanvas.redraw();
        }
    }

    /**
     * Update the cursor to show the type of operation we expect on a mouse press:
     * <ul>
     * <li>Over a selection handle, show a directional cursor depending on the position of
     * the selection handle
     * <li>Over a widget, show a move (hand) cursor
     * <li>Otherwise, show the default arrow cursor
     * </ul>
     */
    void updateCursor(ControlPoint controlPoint) {
        // We don't hover on the root since it's not a widget per see and it is always there.
        SelectionManager selectionManager = mCanvas.getSelectionManager();

        if (!selectionManager.isEmpty()) {
            Display display = mCanvas.getDisplay();
            Pair<SelectionItem, SelectionHandle> handlePair =
                selectionManager.findHandle(controlPoint);
            if (handlePair != null) {
                SelectionHandle handle = handlePair.getSecond();
                int cursorType = handle.getSwtCursorType();
                Cursor cursor = display.getSystemCursor(cursorType);
                if (cursor != mCanvas.getCursor()) {
                    mCanvas.setCursor(cursor);
                }
                return;
            }

            // See if it's over a selected view
            LayoutPoint layoutPoint = controlPoint.toLayout();
            for (SelectionItem item : selectionManager.getSelections()) {
                if (item.getRect().contains(layoutPoint.x, layoutPoint.y)
                        && !item.isRoot()) {
                    Cursor cursor = display.getSystemCursor(SWT.CURSOR_HAND);
                    if (cursor != mCanvas.getCursor()) {
                        mCanvas.setCursor(cursor);
                    }
                    return;
                }
            }
        }

        if (mCanvas.getCursor() != null) {
            mCanvas.setCursor(null);
        }
    }

    /**
     * Update the Eclipse status message with any feedback messages from the given
     * {@link DropFeedback} object, or clean up if there is no more feedback to process
     * @param feedback the feedback whose message we want to display, or null to clear the
     *            message if previously set
     */
    void updateMessage(DropFeedback feedback) {
        IEditorSite editorSite = mCanvas.getLayoutEditor().getEditorSite();
        IStatusLineManager status = editorSite.getActionBars().getStatusLineManager();
        if (feedback == null) {
            if (mDisplayingMessage != null) {
                status.setMessage(null);
                status.setErrorMessage(null);
                mDisplayingMessage = null;
            }
        } else if (feedback.errorMessage != null) {
            if (!feedback.errorMessage.equals(mDisplayingMessage)) {
                mDisplayingMessage = feedback.errorMessage;
                status.setErrorMessage(mDisplayingMessage);
            }
        } else if (feedback.message != null) {
            if (!feedback.message.equals(mDisplayingMessage)) {
                mDisplayingMessage = feedback.message;
                status.setMessage(mDisplayingMessage);
            }
        } else if (mDisplayingMessage != null) {
            // TODO: Can we check the existing message and only clear it if it's the
            // same as the one we set?
            mDisplayingMessage = null;
            status.setMessage(null);
            status.setErrorMessage(null);
        }

        // Tooltip
        if (feedback != null && feedback.tooltip != null) {
            Pair<Boolean,Boolean> position = mCurrentGesture.getTooltipPosition();
            boolean below = position.getFirst();
            if (feedback.tooltipY != null) {
                below = feedback.tooltipY == SegmentType.BOTTOM;
            }
            boolean toRightOf = position.getSecond();
            if (feedback.tooltipX != null) {
                toRightOf = feedback.tooltipX == SegmentType.RIGHT;
            }
            if (mTooltip == null) {
                mTooltip = new GestureToolTip(mCanvas, below, toRightOf);
            }
            mTooltip.update(feedback.tooltip, below, toRightOf);
        } else if (mTooltip != null) {
            mTooltip.dispose();
            mTooltip = null;
        }
    }

    /**
     * Returns the current mouse position as a {@link ControlPoint}
     *
     * @return the current mouse position as a {@link ControlPoint}
     */
    public ControlPoint getCurrentControlPoint() {
        return ControlPoint.create(mCanvas, mLastMouseX, mLastMouseY);
    }

    /**
     * Returns the current SWT modifier key mask as an {@link IViewRule} modifier mask
     *
     * @return the current SWT modifier key mask as an {@link IViewRule} modifier mask
     */
    public int getRuleModifierMask() {
        int swtMask = mLastStateMask;
        int modifierMask = 0;
        if ((swtMask & SWT.MOD1) != 0) {
            modifierMask |= DropFeedback.MODIFIER1;
        }
        if ((swtMask & SWT.MOD2) != 0) {
            modifierMask |= DropFeedback.MODIFIER2;
        }
        if ((swtMask & SWT.MOD3) != 0) {
            modifierMask |= DropFeedback.MODIFIER3;
        }
        return modifierMask;
    }

    /**
     * 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 {
                updateCursor(ControlPoint.create(mCanvas, e));
                mCanvas.hover(e);
            }
        }

        // --- MouseListener ---

        public void mouseUp(MouseEvent e) {
            ControlPoint mousePos = ControlPoint.create(mCanvas, e);
            if (mCurrentGesture == null) {
                // Just a click, select
                Pair<SelectionItem, SelectionHandle> handlePair =
                    mCanvas.getSelectionManager().findHandle(mousePos);
                if (handlePair == null) {
                    mCanvas.getSelectionManager().select(e);
                }
            }
            if (mCurrentGesture == null) {
                updateCursor(mousePos);
            } else if (mCurrentGesture instanceof DropGesture) {
                // Mouse Up shouldn't be delivered in the middle of a drag & drop -
                // but this can happen on some versions of Linux
                // (see http://code.google.com/p/android/issues/detail?id=19057 )
                // and if we process the mouseUp it will abort the remainder of
                // the drag & drop operation, so ignore this event!
            } else {
                finishGesture(mousePos, 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) {
            mLastStateMask = e.stateMask;
            // Workaround for the fact that in keyPressed the current state
            // mask is not yet updated
            if (e.keyCode == SWT.SHIFT) {
                mLastStateMask |= SWT.MOD2;
            }
            if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) {
                if (e.keyCode == SWT.COMMAND) {
                    mLastStateMask |= SWT.MOD1;
                }
            } else {
                if (e.keyCode == SWT.CTRL) {
                    mLastStateMask |= SWT.MOD1;
                }
            }

            // Give gestures a first chance to see and consume the key press
            if (mCurrentGesture != null) {
                // unless it's "Escape", which cancels the gesture
                if (e.keyCode == SWT.ESC) {
                    ControlPoint controlPoint = ControlPoint.create(mCanvas,
                            mLastMouseX, mLastMouseY);
                    finishGesture(controlPoint, true);
                    return;
                }

                if (mCurrentGesture.keyPressed(e)) {
                    return;
                }
            }

            // Fall back to canvas actions for the key press
            mCanvas.handleKeyPressed(e);
        }

        public void keyReleased(KeyEvent e) {
            mLastStateMask = e.stateMask;
            // Workaround for the fact that in keyPressed the current state
            // mask is not yet updated
            if (e.keyCode == SWT.SHIFT) {
                mLastStateMask &= ~SWT.MOD2;
            }
            if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) {
                if (e.keyCode == SWT.COMMAND) {
                    mLastStateMask &= ~SWT.MOD1;
                }
            } else {
                if (e.keyCode == SWT.CTRL) {
                    mLastStateMask &= ~SWT.MOD1;
                }
            }

            if (mCurrentGesture != null) {
                mCurrentGesture.keyReleased(e);
            }
        }
    }

    /** Listener for Drag &amp; 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);
            ControlPoint controlPoint = ControlPoint.create(mCanvas, e);
            SelectionManager selectionManager = mCanvas.getSelectionManager();

            // See if the mouse is over a selection handle; if so, start a resizing
            // gesture.
            Pair<SelectionItem, SelectionHandle> handle =
                selectionManager.findHandle(controlPoint);
            if (handle != null) {
                startGesture(controlPoint, new ResizeGesture(mCanvas, handle.getFirst(),
                        handle.getSecond()), mLastStateMask);
                e.detail = DND.DROP_NONE;
                e.doit = false;
                mCanvas.redraw();
                return;
            }

            // 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 = selectionManager.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() && !vi.isHidden()) {
                        selectionManager.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() && !cs.isHidden()) {
                                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() && !vi.isHidden()) {
                    selectionManager.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() || mDragSelection.get(0).isHidden()))) {
                boolean toggle = (mLastStateMask & (SWT.CTRL | SWT.SHIFT | SWT.COMMAND)) != 0;
                startGesture(controlPoint,
                        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, 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);

                        // Record the baseline such that we can perform baseline alignment
                        // on the node as it's dragged around
                        NodeProxy firstNode =
                            mCanvas.getNodeFactory().create(mDragSelection.get(0).getViewInfo());
                        dragInfo.setDragBaseline(firstNode.getBaseline());
                    }
                }
            }

            // 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();
        }
    }
}
