001: /*******************************************************************************
002: * Copyright (c) 2004, 2006 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: *******************************************************************************/package org.eclipse.ui.internal.dnd;
011:
012: import java.util.ArrayList;
013: import java.util.Iterator;
014: import java.util.List;
015:
016: import org.eclipse.jface.util.Geometry;
017: import org.eclipse.swt.SWT;
018: import org.eclipse.swt.graphics.Point;
019: import org.eclipse.swt.graphics.Rectangle;
020: import org.eclipse.swt.widgets.Control;
021: import org.eclipse.swt.widgets.Display;
022: import org.eclipse.swt.widgets.Event;
023: import org.eclipse.swt.widgets.Listener;
024: import org.eclipse.swt.widgets.Shell;
025: import org.eclipse.swt.widgets.Tracker;
026: import org.eclipse.ui.PlatformUI;
027: import org.eclipse.ui.internal.DragCursors;
028:
029: /**
030: * Provides the methods for attaching drag-and-drop listeners to SWT controls.
031: */
032: public class DragUtil {
033: private static final String DROP_TARGET_ID = "org.eclipse.ui.internal.dnd.dropTarget"; //$NON-NLS-1$
034:
035: /**
036: * The location where all drags will end. If this is non-null, then
037: * all user input is ignored in drag/drop. If null, we use user input
038: * to determine where objects should be dropped.
039: */
040: private static TestDropLocation forcedDropTarget = null;
041:
042: /**
043: * List of IDragOverListener
044: */
045: private static List defaultTargets = new ArrayList();
046:
047: /**
048: * Sets the drop target for the given control. It is possible to add one or more
049: * targets for a "null" control. This becomes a default target that is used if no
050: * other targets are found (for example, when dragging objects off the application
051: * window).
052: *
053: * @param control the control that should be treated as a drag target, or null
054: * to indicate the default target
055: * @param target the drag target to handle the given control
056: */
057: public static void addDragTarget(Control control,
058: IDragOverListener target) {
059: if (control == null) {
060: defaultTargets.add(target);
061: } else {
062: List targetList = getTargetList(control);
063:
064: if (targetList == null) {
065: targetList = new ArrayList(1);
066: }
067: targetList.add(target);
068: control.setData(DROP_TARGET_ID, targetList);
069: }
070: }
071:
072: /**
073: * Return the list of 'IDragOverListener' elements associated with
074: * the given control. If there's a 'global' listener then always
075: * return it.
076: *
077: * @param control
078: * @return
079: */
080: private static List getTargetList(Control control) {
081: List result = (List) control.getData(DROP_TARGET_ID);
082: return result;
083: }
084:
085: /**
086: * Removes a drop target from the given control.
087: *
088: * @param control
089: * @param target
090: */
091: public static void removeDragTarget(Control control,
092: IDragOverListener target) {
093: if (control == null) {
094: defaultTargets.remove(target);
095: } else {
096: List targetList = getTargetList(control);
097: if (targetList != null) {
098: targetList.remove(target);
099: if (targetList.isEmpty()) {
100: control.setData(DROP_TARGET_ID, null);
101: }
102: }
103: }
104: }
105:
106: /**
107: * Shorthand method. Returns the bounding rectangle for the given control, in
108: * display coordinates. Note that all 'Shell' controls are expected to be 'top level'
109: * so DO NOT do the origin offset for them.
110: *
111: * @param draggedItem
112: * @param boundsControl
113: * @return
114: */
115: public static Rectangle getDisplayBounds(Control boundsControl) {
116: Control parent = boundsControl.getParent();
117: if (parent == null || boundsControl instanceof Shell) {
118: return boundsControl.getBounds();
119: }
120:
121: return Geometry.toDisplay(parent, boundsControl.getBounds());
122: }
123:
124: public static boolean performDrag(final Object draggedItem,
125: Rectangle sourceBounds, Point initialLocation,
126: boolean allowSnapping) {
127:
128: IDropTarget target = dragToTarget(draggedItem, sourceBounds,
129: initialLocation, allowSnapping);
130:
131: if (target == null) {
132: return false;
133: }
134:
135: target.drop();
136:
137: // If the target can handle a 'finished' notification then send one
138: if (target != null && target instanceof IDropTarget2) {
139: ((IDropTarget2) target).dragFinished(true);
140: }
141:
142: return true;
143: }
144:
145: /**
146: * Drags the given item to the given location (in display coordinates). This
147: * method is intended for use by test suites.
148: *
149: * @param draggedItem object being dragged
150: * @param finalLocation location being dragged to
151: * @return true iff the drop was accepted
152: */
153: public static boolean dragTo(Display display, Object draggedItem,
154: Point finalLocation, Rectangle dragRectangle) {
155: Control currentControl = SwtUtil.findControl(display,
156: finalLocation);
157:
158: IDropTarget target = getDropTarget(currentControl, draggedItem,
159: finalLocation, dragRectangle);
160:
161: if (target == null) {
162: return false;
163: }
164:
165: target.drop();
166:
167: return true;
168: }
169:
170: /**
171: * Forces all drags to end at the given position (display coordinates). Intended
172: * for use by test suites. If this method is called, then all subsequent calls
173: * to performDrag will terminate immediately and behave as though the object were
174: * dragged to the given location. Calling this method with null cancels this
175: * behavior and causes performDrag to behave normally.
176: *
177: * @param forcedLocation location where objects will be dropped (or null to
178: * cause drag/drop to behave normally).
179: */
180: public static void forceDropLocation(TestDropLocation forcedLocation) {
181: forcedDropTarget = forcedLocation;
182: }
183:
184: /**
185: * Drags the given item, given an initial bounding rectangle in display coordinates.
186: * Due to a quirk in the Tracker class, changing the tracking rectangle when using the
187: * keyboard will also cause the mouse cursor to move. Since "snapping" causes the tracking
188: * rectangle to change based on the position of the mouse cursor, it is impossible to do
189: * drag-and-drop with the keyboard when snapping is enabled.
190: *
191: * @param draggedItem object being dragged
192: * @param sourceBounds initial bounding rectangle for the dragged item
193: * @param initialLocation initial position of the mouse cursor
194: * @param allowSnapping true iff the rectangle should snap to the drop location. This must
195: * be false if the user might be doing drag-and-drop using the keyboard.
196: *
197: * @return
198: */
199: static IDropTarget dragToTarget(final Object draggedItem,
200: final Rectangle sourceBounds, final Point initialLocation,
201: final boolean allowSnapping) {
202: final Display display = Display.getCurrent();
203:
204: // Testing...immediately 'drop' onto the test target
205: if (forcedDropTarget != null) {
206: Point location = forcedDropTarget.getLocation();
207:
208: Control currentControl = SwtUtil.findControl(
209: forcedDropTarget.getShells(), location);
210: return getDropTarget(currentControl, draggedItem, location,
211: sourceBounds);
212: }
213:
214: // Create a tracker. This is just an XOR rect on the screen.
215: // As it moves we notify the drag listeners.
216: final Tracker tracker = new Tracker(display, SWT.NULL);
217: tracker.setStippled(true);
218:
219: tracker.addListener(SWT.Move, new Listener() {
220: public void handleEvent(final Event event) {
221: display.syncExec(new Runnable() {
222: public void run() {
223: // Get the curslor location as a point
224: Point location = new Point(event.x, event.y);
225:
226: // Select a drop target; use the global one by default
227: IDropTarget target = null;
228:
229: Control targetControl = display
230: .getCursorControl();
231:
232: // Get the drop target for this location
233: target = getDropTarget(targetControl,
234: draggedItem, location, tracker
235: .getRectangles()[0]);
236:
237: // Set up the tracker feedback based on the target
238: Rectangle snapTarget = null;
239: if (target != null) {
240: snapTarget = target.getSnapRectangle();
241:
242: tracker.setCursor(target.getCursor());
243: } else {
244: tracker.setCursor(DragCursors
245: .getCursor(DragCursors.INVALID));
246: }
247:
248: // If snapping then reset the tracker's rectangle based on the current drop target
249: if (allowSnapping) {
250: if (snapTarget == null) {
251: snapTarget = new Rectangle(
252: sourceBounds.x + location.x
253: - initialLocation.x,
254: sourceBounds.y + location.y
255: - initialLocation.y,
256: sourceBounds.width,
257: sourceBounds.height);
258: }
259:
260: // Try to prevent flicker: don't change the rectangles if they're already in
261: // the right location
262: Rectangle[] currentRectangles = tracker
263: .getRectangles();
264:
265: if (!(currentRectangles.length == 1 && currentRectangles[0]
266: .equals(snapTarget))) {
267: tracker
268: .setRectangles(new Rectangle[] { snapTarget });
269: }
270: }
271: }
272: });
273: }
274: });
275:
276: // Setup...when the drag starts we might already be over a valid target, check this...
277: // If there is a 'global' target then skip the check
278: IDropTarget target = null;
279: Control startControl = display.getCursorControl();
280:
281: if (startControl != null && allowSnapping) {
282: target = getDropTarget(startControl, draggedItem,
283: initialLocation, sourceBounds);
284: }
285:
286: // Set up an initial tracker rectangle
287: Rectangle startRect = sourceBounds;
288: if (target != null) {
289: Rectangle rect = target.getSnapRectangle();
290:
291: if (rect != null) {
292: startRect = rect;
293: }
294:
295: tracker.setCursor(target.getCursor());
296: }
297:
298: if (startRect != null) {
299: tracker.setRectangles(new Rectangle[] { Geometry
300: .copy(startRect) });
301: }
302:
303: // Tracking Loop...tracking is preformed on the 'SWT.Move' listener registered
304: // against the tracker.
305:
306: // HACK:
307: // Some control needs to capture the mouse during the drag or other
308: // controls will interfere with the cursor
309: Shell shell = PlatformUI.getWorkbench()
310: .getActiveWorkbenchWindow().getShell();
311: if (shell != null) {
312: shell.setCapture(true);
313: }
314:
315: // Run tracker until mouse up occurs or escape key pressed.
316: boolean trackingOk = tracker.open();
317:
318: // HACK:
319: // Release the mouse now
320: if (shell != null) {
321: shell.setCapture(false);
322: }
323:
324: // Done tracking...
325:
326: // Get the current drop target
327: IDropTarget dropTarget = null;
328: Point finalLocation = display.getCursorLocation();
329: Control targetControl = display.getCursorControl();
330: dropTarget = getDropTarget(targetControl, draggedItem,
331: finalLocation, tracker.getRectangles()[0]);
332:
333: // Cleanup...
334: tracker.dispose();
335:
336: // if we're going to perform a 'drop' then delay the issuing of the 'finished'
337: // callback until after it's done...
338: if (trackingOk) {
339: return dropTarget;
340: } else if (dropTarget != null
341: && dropTarget instanceof IDropTarget2) {
342: // If the target can handle a 'finished' notification then send one
343: ((IDropTarget2) dropTarget).dragFinished(false);
344: }
345:
346: return null;
347: }
348:
349: /**
350: * Given a list of IDragOverListeners and a description of what is being dragged, it returns
351: * a IDropTarget for the current drop.
352: *
353: * @param toSearch
354: * @param mostSpecificControl
355: * @param draggedObject
356: * @param position
357: * @param dragRectangle
358: * @return
359: */
360: private static IDropTarget getDropTarget(List toSearch,
361: Control mostSpecificControl, Object draggedObject,
362: Point position, Rectangle dragRectangle) {
363: if (toSearch == null) {
364: return null;
365: }
366:
367: Iterator iter = toSearch.iterator();
368: while (iter.hasNext()) {
369: IDragOverListener next = (IDragOverListener) iter.next();
370:
371: IDropTarget dropTarget = next.drag(mostSpecificControl,
372: draggedObject, position, dragRectangle);
373:
374: if (dropTarget != null) {
375: return dropTarget;
376: }
377: }
378:
379: return null;
380: }
381:
382: /**
383: * Returns the drag target for the given control or null if none.
384: *
385: * @param toSearch
386: * @param e
387: * @return
388: */
389: public static IDropTarget getDropTarget(Control toSearch,
390: Object draggedObject, Point position,
391: Rectangle dragRectangle) {
392: // Search for a listener by walking the control's parent hierarchy
393: for (Control current = toSearch; current != null; current = current
394: .getParent()) {
395: IDropTarget dropTarget = getDropTarget(
396: getTargetList(current), toSearch, draggedObject,
397: position, dragRectangle);
398:
399: if (dropTarget != null) {
400: return dropTarget;
401: }
402:
403: // Don't look to parent shells for drop targets
404: if (current instanceof Shell) {
405: break;
406: }
407: }
408:
409: // No controls could handle this event -- check for default targets
410: return getDropTarget(defaultTargets, toSearch, draggedObject,
411: position, dragRectangle);
412: }
413:
414: /**
415: * Returns the location of the given event, in display coordinates
416: * @return
417: */
418: public static Point getEventLoc(Event event) {
419: Control ctrl = (Control) event.widget;
420: return ctrl.toDisplay(new Point(event.x, event.y));
421: }
422:
423: }
|