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  */
16 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
17 
18 import com.android.ide.common.api.DropFeedback;
19 import com.android.ide.common.api.INode;
20 import com.android.ide.common.api.InsertType;
21 import com.android.ide.common.api.Point;
22 import com.android.ide.common.api.Rect;
23 import com.android.ide.eclipse.adt.AdtPlugin;
24 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeFactory;
25 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
26 import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
27 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
28 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
29 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode.NodeCreationListener;
30 
31 import org.eclipse.jface.viewers.ISelection;
32 import org.eclipse.jface.viewers.TreePath;
33 import org.eclipse.jface.viewers.TreeSelection;
34 import org.eclipse.swt.dnd.DND;
35 import org.eclipse.swt.dnd.DropTargetEvent;
36 import org.eclipse.swt.dnd.TransferData;
37 import org.eclipse.swt.graphics.GC;
38 import org.eclipse.swt.widgets.Display;
39 
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.Collections;
43 import java.util.List;
44 
45 /**
46  * The Move gesture provides the operation for moving widgets around in the canvas.
47  */
48 public 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      */
MoveGesture(LayoutCanvas canvas)117     public MoveGesture(LayoutCanvas canvas) {
118         mCanvas = canvas;
119         mGlobalDragInfo = GlobalCanvasDragInfo.getInstance();
120     }
121 
122     @Override
createOverlays()123     public List<Overlay> createOverlays() {
124         mOverlay = new MoveOverlay();
125         return Collections.<Overlay> singletonList(mOverlay);
126     }
127 
128     @Override
begin(ControlPoint pos, int startMask)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
end(ControlPoint pos, boolean canceled)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
dragEnter(DropTargetEvent event)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
dragOperationChanged(DropTargetEvent event)217     public void dragOperationChanged(DropTargetEvent event) {
218         if (DEBUG) AdtPlugin.printErrorToConsole("DEBUG", "drag changed", event);
219 
220         checkDataType(event);
221         recomputeDragType(event);
222     }
223 
recomputeDragType(DropTargetEvent event)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
dragLeave(DropTargetEvent event)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
dragOver(DropTargetEvent event)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
dropAccept(DropTargetEvent event)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
drop(final DropTargetEvent event)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      */
getInsertType(DropTargetEvent event, NodeProxy mTargetNode)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      */
computeUndoLabel(NodeProxy targetNode, SimpleElement[] elements, int detail)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      */
getSimpleName(String fqcn)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      */
updateDropFeedback(DropFeedback df, DropTargetEvent event)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      */
checkDataType(DropTargetEvent event)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      */
getDropLocation(DropTargetEvent event)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      */
processDropEvent(DropTargetEvent event)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      */
callDropLeave()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 
clearDropInfo()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      */
createDocumentRoot(SimpleElement[] elements)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
paint(GC gc)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