• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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