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 this.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 assert !added.contains(child); 387 } 388 } 389 }; 390 391 try { 392 UiElementNode.addNodeCreationListener(listener); 393 mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(label, new Runnable() { 394 @Override 395 public void run() { 396 InsertType insertType = getInsertType(event, mTargetNode); 397 mCanvas.getRulesEngine().callOnDropped(mTargetNode, 398 elementsFinal, 399 mFeedback, 400 new Point(canvasPoint.x, canvasPoint.y), 401 insertType); 402 mTargetNode.applyPendingChanges(); 403 // Clean up drag if applicable 404 if (event.detail == DND.DROP_MOVE) { 405 GlobalCanvasDragInfo.getInstance().removeSource(); 406 } 407 } 408 }); 409 } finally { 410 UiElementNode.removeNodeCreationListener(listener); 411 } 412 413 final List<INode> nodes = new ArrayList<INode>(); 414 NodeFactory nodeFactory = mCanvas.getNodeFactory(); 415 for (UiElementNode uiNode : added) { 416 if (uiNode instanceof UiViewElementNode) { 417 NodeProxy node = nodeFactory.create((UiViewElementNode) uiNode); 418 if (node != null) { 419 nodes.add(node); 420 } 421 } 422 } 423 424 // Select the newly dropped nodes: 425 // Find out which nodes were added, and look up their corresponding 426 // CanvasViewInfos. 427 final SelectionManager selectionManager = mCanvas.getSelectionManager(); 428 // Don't use the indices to search for corresponding nodes yet, since a 429 // render may not have happened yet and we'd rather use an up to date 430 // view hierarchy than indices to look up the right view infos. 431 if (!selectionManager.selectDropped(nodes, null /* indices */)) { 432 // In some scenarios we can't find the actual view infos yet; this 433 // seems to happen when you drag from one canvas to another (see the 434 // related comment next to the setFocus() call below). In that case 435 // defer selection briefly until the view hierarchy etc is up to 436 // date. 437 Display.getDefault().asyncExec(new Runnable() { 438 @Override 439 public void run() { 440 selectionManager.selectDropped(nodes, indices); 441 } 442 }); 443 } 444 445 clearDropInfo(); 446 mCanvas.redraw(); 447 // Request focus: This is *necessary* when you are dragging from one canvas editor 448 // to another, because without it, the redraw does not seem to be processed (the change 449 // is invisible until you click on the target canvas to give it focus). 450 mCanvas.setFocus(); 451 } 452 453 /** 454 * Returns the right {@link InsertType} to use for the given drop target event and the 455 * given target node 456 * 457 * @param event the drop target event 458 * @param mTargetNode the node targeted by the drop 459 * @return the {link InsertType} to use for the drop 460 */ getInsertType(DropTargetEvent event, NodeProxy mTargetNode)461 public static InsertType getInsertType(DropTargetEvent event, NodeProxy mTargetNode) { 462 GlobalCanvasDragInfo dragInfo = GlobalCanvasDragInfo.getInstance(); 463 if (event.detail == DND.DROP_MOVE) { 464 SelectionItem[] selection = dragInfo.getCurrentSelection(); 465 if (selection != null) { 466 for (SelectionItem item : selection) { 467 if (item.getNode() != null 468 && item.getNode().getParent() == mTargetNode) { 469 return InsertType.MOVE_WITHIN; 470 } 471 } 472 } 473 474 return InsertType.MOVE_INTO; 475 } else if (dragInfo.getSourceCanvas() != null) { 476 return InsertType.PASTE; 477 } else { 478 return InsertType.CREATE; 479 } 480 } 481 482 /** 483 * Computes a suitable Undo label to use for a drop operation, such as 484 * "Drop Button in LinearLayout" and "Move Widgets in RelativeLayout". 485 * 486 * @param targetNode The target of the drop 487 * @param elements The dragged widgets 488 * @param detail The DnD mode, as used in {@link DropTargetEvent#detail}. 489 * @return A string suitable as an undo-label for the drop event 490 */ computeUndoLabel(NodeProxy targetNode, SimpleElement[] elements, int detail)491 public static String computeUndoLabel(NodeProxy targetNode, 492 SimpleElement[] elements, int detail) { 493 // Decide whether it's a move or a copy; we'll label moves specifically 494 // as a move and consider everything else a "Drop" 495 String verb = (detail == DND.DROP_MOVE) ? "Move" : "Drop"; 496 497 // Get the type of widget being dropped/moved, IF there is only one. If 498 // there is more than one, just reference it as "Widgets". 499 String object; 500 if (elements != null && elements.length == 1) { 501 object = getSimpleName(elements[0].getFqcn()); 502 } else { 503 object = "Widgets"; 504 } 505 506 String where = getSimpleName(targetNode.getFqcn()); 507 508 // When we localize this: $1 is the verb (Move or Drop), $2 is the 509 // object (such as "Button"), and $3 is the place we are doing it (such 510 // as "LinearLayout"). 511 return String.format("%1$s %2$s in %3$s", verb, object, where); 512 } 513 514 /** 515 * Returns simple name (basename, following last dot) of a fully qualified 516 * class name. 517 * 518 * @param fqcn The fqcn to reduce 519 * @return The base name of the fqcn 520 */ getSimpleName(String fqcn)521 public static String getSimpleName(String fqcn) { 522 // Note that the following works even when there is no dot, since 523 // lastIndexOf will return -1 so we get fcqn.substring(-1+1) = 524 // fcqn.substring(0) = fqcn 525 return fqcn.substring(fqcn.lastIndexOf('.') + 1); 526 } 527 528 /** 529 * Updates the {@link DropFeedback#isCopy} and {@link DropFeedback#sameCanvas} fields 530 * of the given {@link DropFeedback}. This is generally called right before invoking 531 * one of the callOnXyz methods of GRE to refresh the fields. 532 * 533 * @param df The current {@link DropFeedback}. 534 * @param event An optional event to determine if the current operation is copy or move. 535 */ updateDropFeedback(DropFeedback df, DropTargetEvent event)536 private void updateDropFeedback(DropFeedback df, DropTargetEvent event) { 537 if (event != null) { 538 df.isCopy = event.detail == DND.DROP_COPY; 539 } 540 df.sameCanvas = mCanvas == mGlobalDragInfo.getSourceCanvas(); 541 df.invalidTarget = false; 542 df.dipScale = mCanvas.getEditorDelegate().getGraphicalEditor().getDipScale(); 543 df.modifierMask = mCanvas.getGestureManager().getRuleModifierMask(); 544 545 // Set the drag bounds, after converting it from control coordinates to 546 // layout coordinates 547 GlobalCanvasDragInfo dragInfo = GlobalCanvasDragInfo.getInstance(); 548 Rect dragBounds = null; 549 Rect controlDragBounds = dragInfo.getDragBounds(); 550 if (controlDragBounds != null) { 551 CanvasTransform ht = mCanvas.getHorizontalTransform(); 552 CanvasTransform vt = mCanvas.getVerticalTransform(); 553 double horizScale = ht.getScale(); 554 double verticalScale = vt.getScale(); 555 int x = (int) (controlDragBounds.x / horizScale); 556 int y = (int) (controlDragBounds.y / verticalScale); 557 int w = (int) (controlDragBounds.w / horizScale); 558 int h = (int) (controlDragBounds.h / verticalScale); 559 dragBounds = new Rect(x, y, w, h); 560 } 561 int baseline = dragInfo.getDragBaseline(); 562 if (baseline != -1) { 563 df.dragBaseline = baseline; 564 } 565 df.dragBounds = dragBounds; 566 } 567 568 /** 569 * Verifies that event.currentDataType is of type {@link SimpleXmlTransfer}. 570 * If not, try to find a valid data type. 571 * Otherwise set the drop to {@link DND#DROP_NONE} to cancel it. 572 * 573 * @return True if the data type is accepted. 574 */ checkDataType(DropTargetEvent event)575 private static boolean checkDataType(DropTargetEvent event) { 576 577 SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance(); 578 579 TransferData current = event.currentDataType; 580 581 if (sxt.isSupportedType(current)) { 582 return true; 583 } 584 585 // We only support SimpleXmlTransfer and the current data type is not right. 586 // Let's see if we can find another one. 587 588 for (TransferData td : event.dataTypes) { 589 if (td != current && sxt.isSupportedType(td)) { 590 // We like this type better. 591 event.currentDataType = td; 592 return true; 593 } 594 } 595 596 // We failed to find any good transfer type. 597 event.detail = DND.DROP_NONE; 598 return false; 599 } 600 601 /** 602 * Returns the mouse location of the drop target event. 603 * 604 * @param event the drop target event 605 * @return a {@link ControlPoint} location corresponding to the top left corner 606 */ getDropLocation(DropTargetEvent event)607 private ControlPoint getDropLocation(DropTargetEvent event) { 608 return ControlPoint.create(mCanvas, event); 609 } 610 611 /** 612 * Called on both dragEnter and dragMove. 613 * Generates the onDropEnter/Move/Leave events depending on the currently 614 * selected target node. 615 */ processDropEvent(DropTargetEvent event)616 private void processDropEvent(DropTargetEvent event) { 617 if (!mCanvas.getViewHierarchy().isValid()) { 618 // We don't allow drop on an invalid layout, even if we have some obsolete 619 // layout info for it. 620 event.detail = DND.DROP_NONE; 621 clearDropInfo(); 622 return; 623 } 624 625 LayoutPoint p = getDropLocation(event).toLayout(); 626 627 // Is the mouse currently captured by a DropFeedback.captureArea? 628 boolean isCaptured = false; 629 if (mFeedback != null) { 630 Rect r = mFeedback.captureArea; 631 isCaptured = r != null && r.contains(p.x, p.y); 632 } 633 634 // We can't switch views/nodes when the mouse is captured 635 CanvasViewInfo vi; 636 if (isCaptured) { 637 vi = mCurrentView; 638 } else { 639 vi = mCanvas.getViewHierarchy().findViewInfoAt(p); 640 641 // When dragging into the canvas, if you are not over any other view, target 642 // the root element (since it may not "fill" the screen, e.g. if you have a linear 643 // layout but have layout_height wrap_content, then the layout will only extend 644 // to cover the children in the layout, not the whole visible screen area, which 645 // may be surprising 646 if (vi == null) { 647 vi = mCanvas.getViewHierarchy().getRoot(); 648 } 649 } 650 651 boolean isMove = true; 652 boolean needRedraw = false; 653 654 if (vi != mCurrentView) { 655 // Current view has changed. Does that also change the target node? 656 // Note that either mCurrentView or vi can be null. 657 658 if (vi == null) { 659 // vi is null but mCurrentView is not, no view is a target anymore 660 // We don't need onDropMove in this case 661 isMove = false; 662 needRedraw = true; 663 event.detail = DND.DROP_NONE; 664 clearDropInfo(); // this will call callDropLeave. 665 666 } else { 667 // vi is a new current view. 668 // Query GRE for onDropEnter on the ViewInfo hierarchy, starting from the child 669 // towards its parent, till we find one that returns a non-null drop feedback. 670 671 DropFeedback df = null; 672 NodeProxy targetNode = null; 673 674 for (CanvasViewInfo targetVi = vi; 675 targetVi != null && df == null; 676 targetVi = targetVi.getParent()) { 677 targetNode = mCanvas.getNodeFactory().create(targetVi); 678 df = mCanvas.getRulesEngine().callOnDropEnter(targetNode, 679 targetVi.getViewObject(), mCurrentDragElements); 680 681 if (df != null) { 682 // We should also dispatch an onDropMove() call to the initial enter 683 // position, such that the view is notified of the position where 684 // we are within the node immediately (before we for example attempt 685 // to draw feedback). This is necessary since most views perform the 686 // guideline computations in onDropMove (since only onDropMove is handed 687 // the -position- of the mouse), and we want this computation to happen 688 // before we ask the view to draw its feedback. 689 updateDropFeedback(df, event); 690 df = mCanvas.getRulesEngine().callOnDropMove(targetNode, 691 mCurrentDragElements, df, new Point(p.x, p.y)); 692 } 693 694 if (df != null && 695 event.detail == DND.DROP_MOVE && 696 mCanvas == mGlobalDragInfo.getSourceCanvas()) { 697 // You can't move an object into itself in the same canvas. 698 // E.g. case of moving a layout and the node under the mouse is the 699 // layout itself: a copy would be ok but not a move operation of the 700 // layout into himself. 701 702 SelectionItem[] selection = mGlobalDragInfo.getCurrentSelection(); 703 if (selection != null) { 704 for (SelectionItem cs : selection) { 705 if (cs.getViewInfo() == targetVi) { 706 // The node that responded is one of the selection roots. 707 // Simply invalidate the drop feedback and move on the 708 // parent in the ViewInfo chain. 709 710 updateDropFeedback(df, event); 711 mCanvas.getRulesEngine().callOnDropLeave( 712 targetNode, mCurrentDragElements, df); 713 df = null; 714 targetNode = null; 715 } 716 } 717 } 718 } 719 } 720 721 if (df == null) { 722 // Provide visual feedback that we are refusing the drop 723 event.detail = DND.DROP_NONE; 724 clearDropInfo(); 725 726 } else if (targetNode != mTargetNode) { 727 // We found a new target node for the drag'n'drop. 728 // Release the previous one, if any. 729 callDropLeave(); 730 731 // And assign the new one 732 mTargetNode = targetNode; 733 mFeedback = df; 734 735 // We don't need onDropMove in this case 736 isMove = false; 737 } 738 } 739 740 mCurrentView = vi; 741 } 742 743 if (isMove && mTargetNode != null && mFeedback != null) { 744 // this is a move inside the same view 745 com.android.ide.common.api.Point p2 = 746 new com.android.ide.common.api.Point(p.x, p.y); 747 updateDropFeedback(mFeedback, event); 748 DropFeedback df = mCanvas.getRulesEngine().callOnDropMove( 749 mTargetNode, mCurrentDragElements, mFeedback, p2); 750 mCanvas.getGestureManager().updateMessage(mFeedback); 751 752 if (df == null) { 753 // The target is no longer interested in the drop move. 754 event.detail = DND.DROP_NONE; 755 callDropLeave(); 756 757 } else if (df != mFeedback) { 758 mFeedback = df; 759 } 760 } 761 762 if (mFeedback != null) { 763 if (event.detail == DND.DROP_NONE && !mFeedback.invalidTarget) { 764 // If we previously provided visual feedback that we were refusing 765 // the drop, we now need to change it to mean we're accepting it. 766 event.detail = DND.DROP_DEFAULT; 767 recomputeDragType(event); 768 769 } else if (mFeedback.invalidTarget) { 770 // Provide visual feedback that we are refusing the drop 771 event.detail = DND.DROP_NONE; 772 } 773 } 774 775 if (needRedraw || (mFeedback != null && mFeedback.requestPaint)) { 776 mCanvas.redraw(); 777 } 778 779 // Update outline to show the target node there 780 OutlinePage outline = mCanvas.getOutlinePage(); 781 TreeSelection newSelection = TreeSelection.EMPTY; 782 if (mCurrentView != null && mTargetNode != null) { 783 // Find the view corresponding to the target node. The current view can be a leaf 784 // view whereas the target node is always a parent layout. 785 if (mCurrentView.getUiViewNode() != mTargetNode.getNode()) { 786 mCurrentView = mCurrentView.getParent(); 787 } 788 if (mCurrentView != null && mCurrentView.getUiViewNode() == mTargetNode.getNode()) { 789 TreePath treePath = SelectionManager.getTreePath(mCurrentView); 790 newSelection = new TreeSelection(treePath); 791 } 792 } 793 794 ISelection currentSelection = outline.getSelection(); 795 if (currentSelection == null || !currentSelection.equals(newSelection)) { 796 outline.setSelection(newSelection); 797 } 798 } 799 800 /** 801 * Calls onDropLeave on mTargetNode with the current mFeedback. <br/> 802 * Then clears mTargetNode and mFeedback. 803 */ callDropLeave()804 private void callDropLeave() { 805 if (mTargetNode != null && mFeedback != null) { 806 updateDropFeedback(mFeedback, null); 807 mCanvas.getRulesEngine().callOnDropLeave(mTargetNode, mCurrentDragElements, mFeedback); 808 } 809 810 mTargetNode = null; 811 mFeedback = null; 812 } 813 clearDropInfo()814 private void clearDropInfo() { 815 callDropLeave(); 816 mCurrentView = null; 817 mCanvas.redraw(); 818 } 819 820 /** 821 * Creates a root element in an empty document. 822 * Only the first element's FQCN of the dragged elements is used. 823 * <p/> 824 * Actual XML handling is done by {@link LayoutCanvas#createDocumentRoot(String)}. 825 */ createDocumentRoot(SimpleElement[] elements)826 private void createDocumentRoot(SimpleElement[] elements) { 827 if (elements == null || elements.length < 1 || elements[0] == null) { 828 return; 829 } 830 831 String rootFqcn = elements[0].getFqcn(); 832 833 mCanvas.createDocumentRoot(rootFqcn); 834 } 835 836 /** 837 * An {@link Overlay} to paint the move feedback. This just delegates to the 838 * layout rules. 839 */ 840 private class MoveOverlay extends Overlay { 841 @Override paint(GC gc)842 public void paint(GC gc) { 843 if (mTargetNode != null && mFeedback != null) { 844 RulesEngine rulesEngine = mCanvas.getRulesEngine(); 845 rulesEngine.callDropFeedbackPaint(mCanvas.getGcWrapper(), mTargetNode, mFeedback); 846 mFeedback.requestPaint = false; 847 } 848 } 849 } 850 } 851