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