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