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 & 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