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