1 /* 2 * Copyright (C) 2009 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 17 package com.android.ide.eclipse.adt.internal.editors.layout.gle2; 18 19 import com.android.ide.eclipse.adt.AdtPlugin; 20 import com.android.ide.eclipse.adt.editors.layout.gscripts.DropFeedback; 21 import com.android.ide.eclipse.adt.editors.layout.gscripts.Point; 22 import com.android.ide.eclipse.adt.editors.layout.gscripts.Rect; 23 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; 24 25 import org.eclipse.swt.dnd.DND; 26 import org.eclipse.swt.dnd.DropTargetEvent; 27 import org.eclipse.swt.dnd.DropTargetListener; 28 import org.eclipse.swt.dnd.TransferData; 29 30 import java.util.Arrays; 31 32 /** 33 * Handles drop operations on top of the canvas. 34 * <p/> 35 * Reference for d'n'd: http://www.eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html 36 */ 37 /* package */ class CanvasDropListener implements DropTargetListener { 38 39 private final LayoutCanvas mCanvas; 40 41 /** 42 * The top view right under the drag'n'drop cursor. 43 * This can only be null during a drag'n'drop when there is no view under the cursor 44 * or after the state was all cleared. 45 */ 46 private CanvasViewInfo mCurrentView; 47 48 /** 49 * The elements currently being dragged. This will always be non-null for a valid 50 * drag'n'drop that happens within the same instance of Eclipse. 51 * <p/> 52 * In the event that the drag and drop happens between different instances of Eclipse 53 * this will remain null. 54 */ 55 private SimpleElement[] mCurrentDragElements; 56 57 /** 58 * The first view under the cursor that responded to onDropEnter is called the "target view". 59 * It can differ from mCurrentView, typically because a terminal View doesn't 60 * accept drag'n'drop so its parent layout became the target drag'n'drop receiver. 61 * <p/> 62 * The target node is the proxy node associated with the target view. 63 * This can be null if no view under the cursor accepted the drag'n'drop or if the node 64 * factory couldn't create a proxy for it. 65 */ 66 private NodeProxy mTargetNode; 67 68 /** 69 * The latest drop feedback returned by IViewRule.onDropEnter/Move. 70 */ 71 private DropFeedback mFeedback; 72 73 /** 74 * {@link #dragLeave(DropTargetEvent)} is unfortunately called right before data is 75 * about to be dropped (between the last {@link #dragOver(DropTargetEvent)} and the 76 * next {@link #dropAccept(DropTargetEvent)}). That means we can't just 77 * trash the current DropFeedback from the current view rule in dragLeave(). 78 * Instead we preserve it in mLeaveTargetNode and mLeaveFeedback in case a dropAccept 79 * happens next. 80 */ 81 private NodeProxy mLeaveTargetNode; 82 /** 83 * @see #mLeaveTargetNode 84 */ 85 private DropFeedback mLeaveFeedback; 86 /** 87 * @see #mLeaveTargetNode 88 */ 89 private CanvasViewInfo mLeaveView; 90 91 /** Singleton used to keep track of drag selection in the same Eclipse instance. */ 92 private final GlobalCanvasDragInfo mGlobalDragInfo; 93 CanvasDropListener(LayoutCanvas canvas)94 public CanvasDropListener(LayoutCanvas canvas) { 95 mCanvas = canvas; 96 mGlobalDragInfo = GlobalCanvasDragInfo.getInstance(); 97 } 98 99 /* 100 * The cursor has entered the drop target boundaries. 101 * {@inheritDoc} 102 */ dragEnter(DropTargetEvent event)103 public void dragEnter(DropTargetEvent event) { 104 AdtPlugin.printErrorToConsole("DEBUG", "drag enter", event); 105 106 // Make sure we don't have any residual data from an earlier operation. 107 clearDropInfo(); 108 mLeaveTargetNode = null; 109 mLeaveFeedback = null; 110 mLeaveView = null; 111 112 // Get the dragged elements. 113 // 114 // The current transfered type can be extracted from the event. 115 // As described in dragOver(), this works basically works on Windows but 116 // not on Linux or Mac, in which case we can't get the type until we 117 // receive dropAccept/drop(). 118 // For consistency we try to use the GlobalCanvasDragInfo instance first, 119 // and if it fails we use the event transfer type as a backup (but as said 120 // before it will most likely work only on Windows.) 121 // In any case this can be null even for a valid transfer. 122 123 mCurrentDragElements = mGlobalDragInfo.getCurrentElements(); 124 125 if (mCurrentDragElements == null) { 126 SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance(); 127 if (sxt.isSupportedType(event.currentDataType)) { 128 mCurrentDragElements = (SimpleElement[]) sxt.nativeToJava(event.currentDataType); 129 } 130 } 131 132 // if there is no data to transfer, invalidate the drag'n'drop. 133 // The assumption is that the transfer should have at least one element with a 134 // a non-null non-empty FQCN. Everything else is optional. 135 if (mCurrentDragElements == null || 136 mCurrentDragElements.length == 0 || 137 mCurrentDragElements[0] == null || 138 mCurrentDragElements[0].getFqcn() == null || 139 mCurrentDragElements[0].getFqcn().length() == 0) { 140 event.detail = DND.DROP_NONE; 141 } 142 143 dragOperationChanged(event); 144 } 145 146 /* 147 * The operation being performed has changed (e.g. modifier key). 148 * {@inheritDoc} 149 */ dragOperationChanged(DropTargetEvent event)150 public void dragOperationChanged(DropTargetEvent event) { 151 AdtPlugin.printErrorToConsole("DEBUG", "drag changed", event); 152 153 checkDataType(event); 154 recomputeDragType(event); 155 } 156 recomputeDragType(DropTargetEvent event)157 private void recomputeDragType(DropTargetEvent event) { 158 if (event.detail == DND.DROP_DEFAULT) { 159 // Default means we can now choose the default operation, either copy or move. 160 // If the drag comes from the same canvas we default to move, otherwise we 161 // default to copy. 162 163 if (mGlobalDragInfo.getSourceCanvas() == mCanvas && 164 (event.operations & DND.DROP_MOVE) != 0) { 165 event.detail = DND.DROP_MOVE; 166 } else if ((event.operations & DND.DROP_COPY) != 0) { 167 event.detail = DND.DROP_COPY; 168 } 169 } 170 171 // We don't support other types than copy and move 172 if (event.detail != DND.DROP_COPY && event.detail != DND.DROP_MOVE) { 173 event.detail = DND.DROP_NONE; 174 } 175 } 176 177 /* 178 * The cursor has left the drop target boundaries OR data is about to be dropped. 179 * {@inheritDoc} 180 */ dragLeave(DropTargetEvent event)181 public void dragLeave(DropTargetEvent event) { 182 AdtPlugin.printErrorToConsole("DEBUG", "drag leave"); 183 184 // dragLeave is unfortunately called right before data is about to be dropped 185 // (between the last dropMove and the next dropAccept). That means we can't just 186 // trash the current DropFeedback from the current view rule, we need to preserve 187 // it in case a dropAccept happens next. 188 // See the corresponding kludge in dropAccept(). 189 mLeaveTargetNode = mTargetNode; 190 mLeaveFeedback = mFeedback; 191 mLeaveView = mCurrentView; 192 193 clearDropInfo(); 194 } 195 196 /* 197 * The cursor is moving over the drop target. 198 * {@inheritDoc} 199 */ dragOver(DropTargetEvent event)200 public void dragOver(DropTargetEvent event) { 201 processDropEvent(event); 202 } 203 204 /* 205 * The drop is about to be performed. 206 * The drop target is given a last chance to change the nature of the drop. 207 * {@inheritDoc} 208 */ dropAccept(DropTargetEvent event)209 public void dropAccept(DropTargetEvent event) { 210 AdtPlugin.printErrorToConsole("DEBUG", "drop accept"); 211 212 checkDataType(event); 213 214 // If we have a valid target node and it matches the one we saved in 215 // dragLeave then we restore the DropFeedback that we saved in dragLeave. 216 if (mLeaveTargetNode != null) { 217 mTargetNode = mLeaveTargetNode; 218 mFeedback = mLeaveFeedback; 219 mCurrentView = mLeaveView; 220 } 221 222 if (mLeaveTargetNode == null || event.detail == DND.DROP_NONE) { 223 clearDropInfo(); 224 } 225 226 mLeaveTargetNode = null; 227 mLeaveFeedback = null; 228 mLeaveView = null; 229 } 230 231 /* 232 * The data is being dropped. 233 * {@inheritDoc} 234 */ drop(DropTargetEvent event)235 public void drop(DropTargetEvent event) { 236 AdtPlugin.printErrorToConsole("DEBUG", "dropped"); 237 238 if (mTargetNode == null) { 239 // DEBUG 240 AdtPlugin.printErrorToConsole("DEBUG", "dropped on null targetNode"); 241 return; 242 } 243 244 SimpleElement[] elements = null; 245 246 SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance(); 247 248 if (sxt.isSupportedType(event.currentDataType)) { 249 if (event.data instanceof SimpleElement[]) { 250 elements = (SimpleElement[]) event.data; 251 } 252 } 253 254 if (elements == null || elements.length < 1) { 255 AdtPlugin.printErrorToConsole("DEBUG", "drop missing drop data"); 256 return; 257 } 258 259 if (mCurrentDragElements != null && Arrays.equals(elements, mCurrentDragElements)) { 260 elements = mCurrentDragElements; 261 } 262 263 Point where = mCanvas.displayToCanvasPoint(event.x, event.y); 264 265 updateDropFeedback(mFeedback, event); 266 mCanvas.getRulesEngine().callOnDropped(mTargetNode, 267 elements, 268 mFeedback, 269 where); 270 271 clearDropInfo(); 272 mCanvas.redraw(); 273 } 274 275 /** 276 * Updates the {@link DropFeedback#isCopy} and {@link DropFeedback#sameCanvas} fields 277 * of the given {@link DropFeedback}. This is generally called right before invoking 278 * one of the callOnXyz methods of GRE to refresh the fields. 279 * 280 * @param df The current {@link DropFeedback}. 281 * @param event An optional event to determine if the current operaiton is copy or move. 282 */ updateDropFeedback(DropFeedback df, DropTargetEvent event)283 private void updateDropFeedback(DropFeedback df, DropTargetEvent event) { 284 if (event != null) { 285 df.isCopy = event.detail == DND.DROP_COPY; 286 } 287 df.sameCanvas = mCanvas == mGlobalDragInfo.getSourceCanvas(); 288 } 289 290 /** 291 * Invoked by the canvas to refresh the display. 292 * @param gCWrapper The GC wrapper, never null. 293 */ paintFeedback(GCWrapper gCWrapper)294 public void paintFeedback(GCWrapper gCWrapper) { 295 if (mTargetNode != null && mFeedback != null && mFeedback.requestPaint) { 296 mFeedback.requestPaint = false; 297 mCanvas.getRulesEngine().callDropFeedbackPaint(gCWrapper, mTargetNode, mFeedback); 298 } 299 } 300 301 /** 302 * Verifies that event.currentDataType is of type {@link SimpleXmlTransfer}. 303 * If not, try to find a valid data type. 304 * Otherwise set the drop to {@link DND#DROP_NONE} to cancel it. 305 * 306 * @return True if the data type is accepted. 307 */ checkDataType(DropTargetEvent event)308 private boolean checkDataType(DropTargetEvent event) { 309 310 SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance(); 311 312 TransferData current = event.currentDataType; 313 314 if (sxt.isSupportedType(current)) { 315 return true; 316 } 317 318 // We only support SimpleXmlTransfer and the current data type is not right. 319 // Let's see if we can find another one. 320 321 for (TransferData td : event.dataTypes) { 322 if (td != current && sxt.isSupportedType(td)) { 323 // We like this type better. 324 event.currentDataType = td; 325 return true; 326 } 327 } 328 329 // We failed to find any good transfer type. 330 event.detail = DND.DROP_NONE; 331 return false; 332 } 333 334 /** 335 * Called on both dragEnter and dragMove. 336 * Generates the onDropEnter/Move/Leave events depending on the currently 337 * selected target node. 338 */ processDropEvent(DropTargetEvent event)339 private void processDropEvent(DropTargetEvent event) { 340 if (!mCanvas.isResultValid()) { 341 // We don't allow drop on an invalid layout, even if we have some obsolete 342 // layout info for it. 343 event.detail = DND.DROP_NONE; 344 clearDropInfo(); 345 return; 346 } 347 348 Point p = mCanvas.displayToCanvasPoint(event.x, event.y); 349 int x = p.x; 350 int y = p.y; 351 352 // Is the mouse currently captured by a DropFeedback.captureArea? 353 boolean isCaptured = false; 354 if (mFeedback != null) { 355 Rect r = mFeedback.captureArea; 356 isCaptured = r != null && r.contains(x, y); 357 } 358 359 // We can't switch views/nodes when the mouse is captured 360 CanvasViewInfo vi; 361 if (isCaptured) { 362 vi = mCurrentView; 363 } else { 364 vi = mCanvas.findViewInfoAt(x, y); 365 } 366 367 boolean isMove = true; 368 boolean needRedraw = false; 369 370 if (vi != mCurrentView) { 371 // Current view has changed. Does that also change the target node? 372 // Note that either mCurrentView or vi can be null. 373 374 if (vi == null) { 375 // vi is null but mCurrentView is not, no view is a target anymore 376 // We don't need onDropMove in this case 377 isMove = false; 378 needRedraw = true; 379 event.detail = DND.DROP_NONE; 380 clearDropInfo(); // this will call callDropLeave. 381 382 } else { 383 // vi is a new current view. 384 // Query GRE for onDropEnter on the ViewInfo hierarchy, starting from the child 385 // towards its parent, till we find one that returns a non-null drop feedback. 386 387 DropFeedback df = null; 388 NodeProxy targetNode = null; 389 390 for (CanvasViewInfo targetVi = vi; 391 targetVi != null && df == null; 392 targetVi = targetVi.getParent()) { 393 targetNode = mCanvas.getNodeFactory().create(targetVi); 394 df = mCanvas.getRulesEngine().callOnDropEnter(targetNode, 395 mCurrentDragElements); 396 397 if (df != null && 398 event.detail == DND.DROP_MOVE && 399 mCanvas == mGlobalDragInfo.getSourceCanvas()) { 400 // You can't move an object into itself in the same canvas. 401 // E.g. case of moving a layout and the node under the mouse is the 402 // layout itself: a copy would be ok but not a move operation of the 403 // layout into himself. 404 405 CanvasSelection[] selection = mGlobalDragInfo.getCurrentSelection(); 406 if (selection != null) { 407 for (CanvasSelection cs : selection) { 408 if (cs.getViewInfo() == targetVi) { 409 // The node that responded is one of the selection roots. 410 // Simply invalidate the drop feedback and move on the 411 // parent in the ViewInfo chain. 412 413 updateDropFeedback(df, event); 414 mCanvas.getRulesEngine().callOnDropLeave( 415 targetNode, mCurrentDragElements, df); 416 df = null; 417 targetNode = null; 418 } 419 } 420 } 421 } 422 } 423 424 if (df != null && targetNode != mTargetNode) { 425 // We found a new target node for the drag'n'drop. 426 // Release the previous one, if any. 427 callDropLeave(); 428 429 // If we previously provided visual feedback that we were refusing 430 // the drop, we now need to change it to mean we're accepting it. 431 if (event.detail == DND.DROP_NONE) { 432 event.detail = DND.DROP_DEFAULT; 433 recomputeDragType(event); 434 } 435 436 // And assign the new one 437 mTargetNode = targetNode; 438 mFeedback = df; 439 440 // We don't need onDropMove in this case 441 isMove = false; 442 443 } else if (df == null) { 444 // Provide visual feedback that we are refusing the drop 445 event.detail = DND.DROP_NONE; 446 clearDropInfo(); 447 } 448 } 449 450 mCurrentView = vi; 451 } 452 453 if (isMove && mTargetNode != null && mFeedback != null) { 454 // this is a move inside the same view 455 com.android.ide.eclipse.adt.editors.layout.gscripts.Point p2 = 456 new com.android.ide.eclipse.adt.editors.layout.gscripts.Point(x, y); 457 updateDropFeedback(mFeedback, event); 458 DropFeedback df = mCanvas.getRulesEngine().callOnDropMove( 459 mTargetNode, mCurrentDragElements, mFeedback, p2); 460 if (df == null) { 461 // The target is no longer interested in the drop move. 462 callDropLeave(); 463 } else if (df != mFeedback) { 464 mFeedback = df; 465 } 466 } 467 468 if (needRedraw || (mFeedback != null && mFeedback.requestPaint)) { 469 mCanvas.redraw(); 470 } 471 } 472 473 /** 474 * Calls onDropLeave on mTargetNode with the current mFeedback. <br/> 475 * Then clears mTargetNode and mFeedback. 476 */ callDropLeave()477 private void callDropLeave() { 478 if (mTargetNode != null && mFeedback != null) { 479 updateDropFeedback(mFeedback, null); 480 mCanvas.getRulesEngine().callOnDropLeave(mTargetNode, mCurrentDragElements, mFeedback); 481 } 482 483 mTargetNode = null; 484 mFeedback = null; 485 } 486 clearDropInfo()487 private void clearDropInfo() { 488 callDropLeave(); 489 mCurrentView = null; 490 mCanvas.redraw(); 491 } 492 493 } 494