1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.eclipse.org/org/documents/epl-v10.php
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
17
18import com.android.ide.common.api.DropFeedback;
19import com.android.ide.common.api.INode;
20import com.android.ide.common.api.InsertType;
21import com.android.ide.common.api.Point;
22import com.android.ide.common.api.Rect;
23import com.android.ide.eclipse.adt.AdtPlugin;
24import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
25import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
26import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
27import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
28import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
29import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode.NodeCreationListener;
30
31import org.eclipse.jface.viewers.ISelection;
32import org.eclipse.jface.viewers.TreePath;
33import org.eclipse.jface.viewers.TreeSelection;
34import org.eclipse.swt.dnd.DND;
35import org.eclipse.swt.dnd.DropTargetEvent;
36import org.eclipse.swt.dnd.TransferData;
37import org.eclipse.swt.graphics.GC;
38import org.eclipse.swt.widgets.Display;
39
40import java.util.ArrayList;
41import java.util.Arrays;
42import java.util.Collections;
43import java.util.List;
44
45/**
46 * The Move gesture provides the operation for moving widgets around in the canvas.
47 */
48public class MoveGesture extends DropGesture {
49    /** The associated {@link LayoutCanvas}. */
50    private LayoutCanvas mCanvas;
51
52    /** Overlay which paints the drag & drop feedback. */
53    private MoveOverlay mOverlay;
54
55    private static final boolean DEBUG = false;
56
57    /**
58     * The top view right under the drag'n'drop cursor.
59     * This can only be null during a drag'n'drop when there is no view under the cursor
60     * or after the state was all cleared.
61     */
62    private CanvasViewInfo mCurrentView;
63
64    /**
65     * The elements currently being dragged. This will always be non-null for a valid
66     * drag'n'drop that happens within the same instance of Eclipse.
67     * <p/>
68     * In the event that the drag and drop happens between different instances of Eclipse
69     * this will remain null.
70     */
71    private SimpleElement[] mCurrentDragElements;
72
73    /**
74     * The first view under the cursor that responded to onDropEnter is called the "target view".
75     * It can differ from mCurrentView, typically because a terminal View doesn't
76     * accept drag'n'drop so its parent layout became the target drag'n'drop receiver.
77     * <p/>
78     * The target node is the proxy node associated with the target view.
79     * This can be null if no view under the cursor accepted the drag'n'drop or if the node
80     * factory couldn't create a proxy for it.
81     */
82    private NodeProxy mTargetNode;
83
84    /**
85     * The latest drop feedback returned by IViewRule.onDropEnter/Move.
86     */
87    private DropFeedback mFeedback;
88
89    /**
90     * {@link #dragLeave(DropTargetEvent)} is unfortunately called right before data is
91     * about to be dropped (between the last {@link #dragOver(DropTargetEvent)} and the
92     * next {@link #dropAccept(DropTargetEvent)}). That means we can't just
93     * trash the current DropFeedback from the current view rule in dragLeave().
94     * Instead we preserve it in mLeaveTargetNode and mLeaveFeedback in case a dropAccept
95     * happens next.
96     */
97    private NodeProxy mLeaveTargetNode;
98
99    /**
100     * @see #mLeaveTargetNode
101     */
102    private DropFeedback mLeaveFeedback;
103
104    /**
105     * @see #mLeaveTargetNode
106     */
107    private CanvasViewInfo mLeaveView;
108
109    /** Singleton used to keep track of drag selection in the same Eclipse instance. */
110    private final GlobalCanvasDragInfo mGlobalDragInfo;
111
112    /**
113     * Constructs a new {@link MoveGesture}, tied to the given canvas.
114     *
115     * @param canvas The canvas to associate the {@link MoveGesture} with.
116     */
117    public MoveGesture(LayoutCanvas canvas) {
118        mCanvas = canvas;
119        mGlobalDragInfo = GlobalCanvasDragInfo.getInstance();
120    }
121
122    @Override
123    public List<Overlay> createOverlays() {
124        mOverlay = new MoveOverlay();
125        return Collections.<Overlay> singletonList(mOverlay);
126    }
127
128    @Override
129    public void begin(ControlPoint pos, int startMask) {
130        super.begin(pos, startMask);
131
132        // Hide selection overlays during a move drag
133        mCanvas.getSelectionOverlay().setHidden(true);
134    }
135
136    @Override
137    public void end(ControlPoint pos, boolean canceled) {
138        super.end(pos, canceled);
139
140        mCanvas.getSelectionOverlay().setHidden(false);
141
142        // Ensure that the outline is back to showing the current selection, since during
143        // a drag gesture we temporarily set it to show the current target node instead.
144        mCanvas.getSelectionManager().syncOutlineSelection();
145    }
146
147    /* TODO: Pass modifier mask to drag rules as well! This doesn't work yet since
148       the drag &amp; drop code seems to steal keyboard events.
149    @Override
150    public boolean keyPressed(KeyEvent event) {
151        update(mCanvas.getGestureManager().getCurrentControlPoint());
152        mCanvas.redraw();
153        return true;
154    }
155
156    @Override
157    public boolean keyReleased(KeyEvent event) {
158        update(mCanvas.getGestureManager().getCurrentControlPoint());
159        mCanvas.redraw();
160        return true;
161    }
162    */
163
164    /*
165     * The cursor has entered the drop target boundaries.
166     * {@inheritDoc}
167     */
168    @Override
169    public void dragEnter(DropTargetEvent event) {
170        if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag enter", event);
171
172        // Make sure we don't have any residual data from an earlier operation.
173        clearDropInfo();
174        mLeaveTargetNode = null;
175        mLeaveFeedback = null;
176        mLeaveView = null;
177
178        // Get the dragged elements.
179        //
180        // The current transfered type can be extracted from the event.
181        // As described in dragOver(), this works basically works on Windows but
182        // not on Linux or Mac, in which case we can't get the type until we
183        // receive dropAccept/drop().
184        // For consistency we try to use the GlobalCanvasDragInfo instance first,
185        // and if it fails we use the event transfer type as a backup (but as said
186        // before it will most likely work only on Windows.)
187        // In any case this can be null even for a valid transfer.
188
189        mCurrentDragElements = mGlobalDragInfo.getCurrentElements();
190
191        if (mCurrentDragElements == null) {
192            SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
193            if (sxt.isSupportedType(event.currentDataType)) {
194                mCurrentDragElements = (SimpleElement[]) sxt.nativeToJava(event.currentDataType);
195            }
196        }
197
198        // if there is no data to transfer, invalidate the drag'n'drop.
199        // The assumption is that the transfer should have at least one element with a
200        // a non-null non-empty FQCN. Everything else is optional.
201        if (mCurrentDragElements == null ||
202                mCurrentDragElements.length == 0 ||
203                mCurrentDragElements[0] == null ||
204                mCurrentDragElements[0].getFqcn() == null ||
205                mCurrentDragElements[0].getFqcn().length() == 0) {
206            event.detail = DND.DROP_NONE;
207        }
208
209        dragOperationChanged(event);
210    }
211
212    /*
213     * The operation being performed has changed (e.g. modifier key).
214     * {@inheritDoc}
215     */
216    @Override
217    public void dragOperationChanged(DropTargetEvent event) {
218        if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag changed", event);
219
220        checkDataType(event);
221        recomputeDragType(event);
222    }
223
224    private void recomputeDragType(DropTargetEvent event) {
225        if (event.detail == DND.DROP_DEFAULT) {
226            // Default means we can now choose the default operation, either copy or move.
227            // If the drag comes from the same canvas we default to move, otherwise we
228            // default to copy.
229
230            if (mGlobalDragInfo.getSourceCanvas() == mCanvas &&
231                    (event.operations & DND.DROP_MOVE) != 0) {
232                event.detail = DND.DROP_MOVE;
233            } else if ((event.operations & DND.DROP_COPY) != 0) {
234                event.detail = DND.DROP_COPY;
235            }
236        }
237
238        // We don't support other types than copy and move
239        if (event.detail != DND.DROP_COPY && event.detail != DND.DROP_MOVE) {
240            event.detail = DND.DROP_NONE;
241        }
242    }
243
244    /*
245     * The cursor has left the drop target boundaries OR data is about to be dropped.
246     * {@inheritDoc}
247     */
248    @Override
249    public void dragLeave(DropTargetEvent event) {
250        if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag leave");
251
252        // dragLeave is unfortunately called right before data is about to be dropped
253        // (between the last dropMove and the next dropAccept). That means we can't just
254        // trash the current DropFeedback from the current view rule, we need to preserve
255        // it in case a dropAccept happens next.
256        // See the corresponding kludge in dropAccept().
257        mLeaveTargetNode = mTargetNode;
258        mLeaveFeedback = mFeedback;
259        mLeaveView = mCurrentView;
260
261        clearDropInfo();
262    }
263
264    /*
265     * The cursor is moving over the drop target.
266     * {@inheritDoc}
267     */
268    @Override
269    public void dragOver(DropTargetEvent event) {
270        processDropEvent(event);
271    }
272
273    /*
274     * The drop is about to be performed.
275     * The drop target is given a last chance to change the nature of the drop.
276     * {@inheritDoc}
277     */
278    @Override
279    public void dropAccept(DropTargetEvent event) {
280        if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drop accept");
281
282        checkDataType(event);
283
284        // If we have a valid target node and it matches the one we saved in
285        // dragLeave then we restore the DropFeedback that we saved in dragLeave.
286        if (mLeaveTargetNode != null) {
287            mTargetNode = mLeaveTargetNode;
288            mFeedback = mLeaveFeedback;
289            mCurrentView = mLeaveView;
290        }
291
292        if (mFeedback != null && mFeedback.invalidTarget) {
293            // The script said we can't drop here.
294            event.detail = DND.DROP_NONE;
295        }
296
297        if (mLeaveTargetNode == null || event.detail == DND.DROP_NONE) {
298            clearDropInfo();
299        }
300
301        mLeaveTargetNode = null;
302        mLeaveFeedback = null;
303        mLeaveView = null;
304    }
305
306    /*
307     * The data is being dropped.
308     * {@inheritDoc}
309     */
310    @Override
311    public void drop(final DropTargetEvent event) {
312        if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "dropped");
313
314        SimpleElement[] elements = null;
315
316        SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
317
318        if (sxt.isSupportedType(event.currentDataType)) {
319            if (event.data instanceof SimpleElement[]) {
320                elements = (SimpleElement[]) event.data;
321            }
322        }
323
324        if (elements == null || elements.length < 1) {
325            if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drop missing drop data");
326            return;
327        }
328
329        if (mCurrentDragElements != null && Arrays.equals(elements, mCurrentDragElements)) {
330            elements = mCurrentDragElements;
331        }
332
333        if (mTargetNode == null) {
334            ViewHierarchy viewHierarchy = mCanvas.getViewHierarchy();
335            if (viewHierarchy.isValid() && viewHierarchy.isEmpty()) {
336                // There is no target node because the drop happens on an empty document.
337                // Attempt to create a root node accordingly.
338                createDocumentRoot(elements);
339            } else {
340                if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "dropped on null targetNode");
341            }
342            return;
343        }
344
345        updateDropFeedback(mFeedback, event);
346
347        final SimpleElement[] elementsFinal = elements;
348        final LayoutPoint canvasPoint = getDropLocation(event).toLayout();
349        String label = computeUndoLabel(mTargetNode, elements, event.detail);
350
351        // Create node listener which (during the drop) listens for node additions
352        // and stores the list of added node such that they can be selected afterwards.
353        final List<UiElementNode> added = new ArrayList<UiElementNode>();
354        // List of "index within parent" for each node
355        final List<Integer> indices = new ArrayList<Integer>();
356        NodeCreationListener listener = new NodeCreationListener() {
357            @Override
358            public void nodeCreated(UiElementNode parent, UiElementNode child, int index) {
359                if (parent == mTargetNode.getNode()) {
360                    added.add(child);
361
362                    // Adjust existing indices
363                    for (int i = 0, n = indices.size(); i < n; i++) {
364                        int idx = indices.get(i);
365                        if (idx >= index) {
366                            indices.set(i, idx + 1);
367                        }
368                    }
369
370                    indices.add(index);
371                }
372            }
373
374            @Override
375            public void nodeDeleted(UiElementNode parent, UiElementNode child, int previousIndex) {
376                if (parent == mTargetNode.getNode()) {
377                    // Adjust existing indices
378                    for (int i = 0, n = indices.size(); i < n; i++) {
379                        int idx = indices.get(i);
380                        if (idx >= previousIndex) {
381                            indices.set(i, idx - 1);
382                        }
383                    }
384
385                    // Make sure we aren't removing the same nodes that are being added
386                    // No, that can happen when canceling out of a drop handler such as
387                    // when dropping an included layout, then canceling out of the
388                    // resource chooser.
389                    //assert !added.contains(child);
390                }
391            }
392        };
393
394        try {
395            UiElementNode.addNodeCreationListener(listener);
396            mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(label, new Runnable() {
397                @Override
398                public void run() {
399                    InsertType insertType = getInsertType(event, mTargetNode);
400                    mCanvas.getRulesEngine().callOnDropped(mTargetNode,
401                            elementsFinal,
402                            mFeedback,
403                            new Point(canvasPoint.x, canvasPoint.y),
404                            insertType);
405                    mTargetNode.applyPendingChanges();
406                    // Clean up drag if applicable
407                    if (event.detail == DND.DROP_MOVE) {
408                        GlobalCanvasDragInfo.getInstance().removeSource();
409                    }
410                    mTargetNode.applyPendingChanges();
411                }
412            });
413        } finally {
414            UiElementNode.removeNodeCreationListener(listener);
415        }
416
417        final List<INode> nodes = new ArrayList<INode>();
418        NodeFactory nodeFactory = mCanvas.getNodeFactory();
419        for (UiElementNode uiNode : added) {
420            if (uiNode instanceof UiViewElementNode) {
421                NodeProxy node = nodeFactory.create((UiViewElementNode) uiNode);
422                if (node != null) {
423                    nodes.add(node);
424                }
425            }
426        }
427
428        // Select the newly dropped nodes:
429        // Find out which nodes were added, and look up their corresponding
430        // CanvasViewInfos.
431        final SelectionManager selectionManager = mCanvas.getSelectionManager();
432        // Don't use the indices to search for corresponding nodes yet, since a
433        // render may not have happened yet and we'd rather use an up to date
434        // view hierarchy than indices to look up the right view infos.
435        if (!selectionManager.selectDropped(nodes, null /* indices */)) {
436            // In some scenarios we can't find the actual view infos yet; this
437            // seems to happen when you drag from one canvas to another (see the
438            // related comment next to the setFocus() call below). In that case
439            // defer selection briefly until the view hierarchy etc is up to
440            // date.
441            Display.getDefault().asyncExec(new Runnable() {
442                @Override
443                public void run() {
444                    selectionManager.selectDropped(nodes, indices);
445                }
446            });
447        }
448
449        clearDropInfo();
450        mCanvas.redraw();
451        // Request focus: This is *necessary* when you are dragging from one canvas editor
452        // to another, because without it, the redraw does not seem to be processed (the change
453        // is invisible until you click on the target canvas to give it focus).
454        mCanvas.setFocus();
455    }
456
457    /**
458     * Returns the right {@link InsertType} to use for the given drop target event and the
459     * given target node
460     *
461     * @param event the drop target event
462     * @param mTargetNode the node targeted by the drop
463     * @return the {link InsertType} to use for the drop
464     */
465    public static InsertType getInsertType(DropTargetEvent event, NodeProxy mTargetNode) {
466        GlobalCanvasDragInfo dragInfo = GlobalCanvasDragInfo.getInstance();
467        if (event.detail == DND.DROP_MOVE) {
468            SelectionItem[] selection = dragInfo.getCurrentSelection();
469            if (selection != null) {
470                for (SelectionItem item : selection) {
471                    if (item.getNode() != null
472                            && item.getNode().getParent() == mTargetNode) {
473                        return InsertType.MOVE_WITHIN;
474                    }
475                }
476            }
477
478            return InsertType.MOVE_INTO;
479        } else if (dragInfo.getSourceCanvas() != null) {
480            return InsertType.PASTE;
481        } else {
482            return InsertType.CREATE;
483        }
484    }
485
486    /**
487     * Computes a suitable Undo label to use for a drop operation, such as
488     * "Drop Button in LinearLayout" and "Move Widgets in RelativeLayout".
489     *
490     * @param targetNode The target of the drop
491     * @param elements The dragged widgets
492     * @param detail The DnD mode, as used in {@link DropTargetEvent#detail}.
493     * @return A string suitable as an undo-label for the drop event
494     */
495    public static String computeUndoLabel(NodeProxy targetNode,
496            SimpleElement[] elements, int detail) {
497        // Decide whether it's a move or a copy; we'll label moves specifically
498        // as a move and consider everything else a "Drop"
499        String verb = (detail == DND.DROP_MOVE) ? "Move" : "Drop";
500
501        // Get the type of widget being dropped/moved, IF there is only one. If
502        // there is more than one, just reference it as "Widgets".
503        String object;
504        if (elements != null && elements.length == 1) {
505            object = getSimpleName(elements[0].getFqcn());
506        } else {
507            object = "Widgets";
508        }
509
510        String where = getSimpleName(targetNode.getFqcn());
511
512        // When we localize this: $1 is the verb (Move or Drop), $2 is the
513        // object (such as "Button"), and $3 is the place we are doing it (such
514        // as "LinearLayout").
515        return String.format("%1$s %2$s in %3$s", verb, object, where);
516    }
517
518    /**
519     * Returns simple name (basename, following last dot) of a fully qualified
520     * class name.
521     *
522     * @param fqcn The fqcn to reduce
523     * @return The base name of the fqcn
524     */
525    public static String getSimpleName(String fqcn) {
526        // Note that the following works even when there is no dot, since
527        // lastIndexOf will return -1 so we get fcqn.substring(-1+1) =
528        // fcqn.substring(0) = fqcn
529        return fqcn.substring(fqcn.lastIndexOf('.') + 1);
530    }
531
532    /**
533     * Updates the {@link DropFeedback#isCopy} and {@link DropFeedback#sameCanvas} fields
534     * of the given {@link DropFeedback}. This is generally called right before invoking
535     * one of the callOnXyz methods of GRE to refresh the fields.
536     *
537     * @param df The current {@link DropFeedback}.
538     * @param event An optional event to determine if the current operation is copy or move.
539     */
540    private void updateDropFeedback(DropFeedback df, DropTargetEvent event) {
541        if (event != null) {
542            df.isCopy = event.detail == DND.DROP_COPY;
543        }
544        df.sameCanvas = mCanvas == mGlobalDragInfo.getSourceCanvas();
545        df.invalidTarget = false;
546        df.dipScale = mCanvas.getEditorDelegate().getGraphicalEditor().getDipScale();
547        df.modifierMask = mCanvas.getGestureManager().getRuleModifierMask();
548
549        // Set the drag bounds, after converting it from control coordinates to
550        // layout coordinates
551        GlobalCanvasDragInfo dragInfo = GlobalCanvasDragInfo.getInstance();
552        Rect dragBounds = null;
553        Rect controlDragBounds = dragInfo.getDragBounds();
554        if (controlDragBounds != null) {
555            CanvasTransform ht = mCanvas.getHorizontalTransform();
556            CanvasTransform vt = mCanvas.getVerticalTransform();
557            double horizScale = ht.getScale();
558            double verticalScale = vt.getScale();
559            int x = (int) (controlDragBounds.x / horizScale);
560            int y = (int) (controlDragBounds.y / verticalScale);
561            int w = (int) (controlDragBounds.w / horizScale);
562            int h = (int) (controlDragBounds.h / verticalScale);
563            dragBounds = new Rect(x, y, w, h);
564        }
565        int baseline = dragInfo.getDragBaseline();
566        if (baseline != -1) {
567            df.dragBaseline = baseline;
568        }
569        df.dragBounds = dragBounds;
570    }
571
572    /**
573     * Verifies that event.currentDataType is of type {@link SimpleXmlTransfer}.
574     * If not, try to find a valid data type.
575     * Otherwise set the drop to {@link DND#DROP_NONE} to cancel it.
576     *
577     * @return True if the data type is accepted.
578     */
579    private static boolean checkDataType(DropTargetEvent event) {
580
581        SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
582
583        TransferData current = event.currentDataType;
584
585        if (sxt.isSupportedType(current)) {
586            return true;
587        }
588
589        // We only support SimpleXmlTransfer and the current data type is not right.
590        // Let's see if we can find another one.
591
592        for (TransferData td : event.dataTypes) {
593            if (td != current && sxt.isSupportedType(td)) {
594                // We like this type better.
595                event.currentDataType = td;
596                return true;
597            }
598        }
599
600        // We failed to find any good transfer type.
601        event.detail = DND.DROP_NONE;
602        return false;
603    }
604
605    /**
606     * Returns the mouse location of the drop target event.
607     *
608     * @param event the drop target event
609     * @return a {@link ControlPoint} location corresponding to the top left corner
610     */
611    private ControlPoint getDropLocation(DropTargetEvent event) {
612        return ControlPoint.create(mCanvas, event);
613    }
614
615    /**
616     * Called on both dragEnter and dragMove.
617     * Generates the onDropEnter/Move/Leave events depending on the currently
618     * selected target node.
619     */
620    private void processDropEvent(DropTargetEvent event) {
621        if (!mCanvas.getViewHierarchy().isValid()) {
622            // We don't allow drop on an invalid layout, even if we have some obsolete
623            // layout info for it.
624            event.detail = DND.DROP_NONE;
625            clearDropInfo();
626            return;
627        }
628
629        LayoutPoint p = getDropLocation(event).toLayout();
630
631        // Is the mouse currently captured by a DropFeedback.captureArea?
632        boolean isCaptured = false;
633        if (mFeedback != null) {
634            Rect r = mFeedback.captureArea;
635            isCaptured = r != null && r.contains(p.x, p.y);
636        }
637
638        // We can't switch views/nodes when the mouse is captured
639        CanvasViewInfo vi;
640        if (isCaptured) {
641            vi = mCurrentView;
642        } else {
643            vi = mCanvas.getViewHierarchy().findViewInfoAt(p);
644
645            // When dragging into the canvas, if you are not over any other view, target
646            // the root element (since it may not "fill" the screen, e.g. if you have a linear
647            // layout but have layout_height wrap_content, then the layout will only extend
648            // to cover the children in the layout, not the whole visible screen area, which
649            // may be surprising
650            if (vi == null) {
651                vi = mCanvas.getViewHierarchy().getRoot();
652            }
653        }
654
655        boolean isMove = true;
656        boolean needRedraw = false;
657
658        if (vi != mCurrentView) {
659            // Current view has changed. Does that also change the target node?
660            // Note that either mCurrentView or vi can be null.
661
662            if (vi == null) {
663                // vi is null but mCurrentView is not, no view is a target anymore
664                // We don't need onDropMove in this case
665                isMove = false;
666                needRedraw = true;
667                event.detail = DND.DROP_NONE;
668                clearDropInfo(); // this will call callDropLeave.
669
670            } else {
671                // vi is a new current view.
672                // Query GRE for onDropEnter on the ViewInfo hierarchy, starting from the child
673                // towards its parent, till we find one that returns a non-null drop feedback.
674
675                DropFeedback df = null;
676                NodeProxy targetNode = null;
677
678                for (CanvasViewInfo targetVi = vi;
679                     targetVi != null && df == null;
680                     targetVi = targetVi.getParent()) {
681                    targetNode = mCanvas.getNodeFactory().create(targetVi);
682                    df = mCanvas.getRulesEngine().callOnDropEnter(targetNode,
683                            targetVi.getViewObject(), mCurrentDragElements);
684
685                    if (df != null) {
686                        // We should also dispatch an onDropMove() call to the initial enter
687                        // position, such that the view is notified of the position where
688                        // we are within the node immediately (before we for example attempt
689                        // to draw feedback). This is necessary since most views perform the
690                        // guideline computations in onDropMove (since only onDropMove is handed
691                        // the -position- of the mouse), and we want this computation to happen
692                        // before we ask the view to draw its feedback.
693                        updateDropFeedback(df, event);
694                        df = mCanvas.getRulesEngine().callOnDropMove(targetNode,
695                                mCurrentDragElements, df, new Point(p.x, p.y));
696                    }
697
698                    if (df != null &&
699                            event.detail == DND.DROP_MOVE &&
700                            mCanvas == mGlobalDragInfo.getSourceCanvas()) {
701                        // You can't move an object into itself in the same canvas.
702                        // E.g. case of moving a layout and the node under the mouse is the
703                        // layout itself: a copy would be ok but not a move operation of the
704                        // layout into himself.
705
706                        SelectionItem[] selection = mGlobalDragInfo.getCurrentSelection();
707                        if (selection != null) {
708                            for (SelectionItem cs : selection) {
709                                if (cs.getViewInfo() == targetVi) {
710                                    // The node that responded is one of the selection roots.
711                                    // Simply invalidate the drop feedback and move on the
712                                    // parent in the ViewInfo chain.
713
714                                    updateDropFeedback(df, event);
715                                    mCanvas.getRulesEngine().callOnDropLeave(
716                                            targetNode, mCurrentDragElements, df);
717                                    df = null;
718                                    targetNode = null;
719                                }
720                            }
721                        }
722                    }
723                }
724
725                if (df == null) {
726                    // Provide visual feedback that we are refusing the drop
727                    event.detail = DND.DROP_NONE;
728                    clearDropInfo();
729
730                } else if (targetNode != mTargetNode) {
731                    // We found a new target node for the drag'n'drop.
732                    // Release the previous one, if any.
733                    callDropLeave();
734
735                    // And assign the new one
736                    mTargetNode = targetNode;
737                    mFeedback = df;
738
739                    // We don't need onDropMove in this case
740                    isMove = false;
741                }
742            }
743
744            mCurrentView = vi;
745        }
746
747        if (isMove && mTargetNode != null && mFeedback != null) {
748            // this is a move inside the same view
749            com.android.ide.common.api.Point p2 =
750                new com.android.ide.common.api.Point(p.x, p.y);
751            updateDropFeedback(mFeedback, event);
752            DropFeedback df = mCanvas.getRulesEngine().callOnDropMove(
753                    mTargetNode, mCurrentDragElements, mFeedback, p2);
754            mCanvas.getGestureManager().updateMessage(mFeedback);
755
756            if (df == null) {
757                // The target is no longer interested in the drop move.
758                event.detail = DND.DROP_NONE;
759                callDropLeave();
760
761            } else if (df != mFeedback) {
762                mFeedback = df;
763            }
764        }
765
766        if (mFeedback != null) {
767            if (event.detail == DND.DROP_NONE && !mFeedback.invalidTarget) {
768                // If we previously provided visual feedback that we were refusing
769                // the drop, we now need to change it to mean we're accepting it.
770                event.detail = DND.DROP_DEFAULT;
771                recomputeDragType(event);
772
773            } else if (mFeedback.invalidTarget) {
774                // Provide visual feedback that we are refusing the drop
775                event.detail = DND.DROP_NONE;
776            }
777        }
778
779        if (needRedraw || (mFeedback != null && mFeedback.requestPaint)) {
780            mCanvas.redraw();
781        }
782
783        // Update outline to show the target node there
784        OutlinePage outline = mCanvas.getOutlinePage();
785        TreeSelection newSelection = TreeSelection.EMPTY;
786        if (mCurrentView != null && mTargetNode != null) {
787            // Find the view corresponding to the target node. The current view can be a leaf
788            // view whereas the target node is always a parent layout.
789            if (mCurrentView.getUiViewNode() != mTargetNode.getNode()) {
790                mCurrentView = mCurrentView.getParent();
791            }
792            if (mCurrentView != null && mCurrentView.getUiViewNode() == mTargetNode.getNode()) {
793                TreePath treePath = SelectionManager.getTreePath(mCurrentView);
794                newSelection = new TreeSelection(treePath);
795            }
796        }
797
798        ISelection currentSelection = outline.getSelection();
799        if (currentSelection == null || !currentSelection.equals(newSelection)) {
800            outline.setSelection(newSelection);
801        }
802    }
803
804    /**
805     * Calls onDropLeave on mTargetNode with the current mFeedback. <br/>
806     * Then clears mTargetNode and mFeedback.
807     */
808    private void callDropLeave() {
809        if (mTargetNode != null && mFeedback != null) {
810            updateDropFeedback(mFeedback, null);
811            mCanvas.getRulesEngine().callOnDropLeave(mTargetNode, mCurrentDragElements, mFeedback);
812        }
813
814        mTargetNode = null;
815        mFeedback = null;
816    }
817
818    private void clearDropInfo() {
819        callDropLeave();
820        mCurrentView = null;
821        mCanvas.redraw();
822    }
823
824    /**
825     * Creates a root element in an empty document.
826     * Only the first element's FQCN of the dragged elements is used.
827     * <p/>
828     * Actual XML handling is done by {@link LayoutCanvas#createDocumentRoot(String)}.
829     */
830    private void createDocumentRoot(SimpleElement[] elements) {
831        if (elements == null || elements.length < 1 || elements[0] == null) {
832            return;
833        }
834
835        mCanvas.createDocumentRoot(elements[0]);
836    }
837
838    /**
839     * An {@link Overlay} to paint the move feedback. This just delegates to the
840     * layout rules.
841     */
842    private class MoveOverlay extends Overlay {
843        @Override
844        public void paint(GC gc) {
845            if (mTargetNode != null && mFeedback != null) {
846                RulesEngine rulesEngine = mCanvas.getRulesEngine();
847                rulesEngine.callDropFeedbackPaint(mCanvas.getGcWrapper(), mTargetNode, mFeedback);
848                mFeedback.requestPaint = false;
849            }
850        }
851    }
852}
853