/*******************************************************************************
* Copyright (c) 2012 Google, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Google, Inc. - initial API and implementation
*******************************************************************************/
package com.windowtester.runtime.swt.internal.dnd;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Widget;
import abbot.swt.Robot;
import abbot.tester.swt.WidgetLocator;
import com.windowtester.runtime.IUIContext;
import com.windowtester.runtime.MultipleWidgetsFoundException;
import com.windowtester.runtime.WidgetNotFoundException;
import com.windowtester.runtime.internal.factory.WTRuntimeManager;
import com.windowtester.runtime.swt.condition.SWTIdleCondition;
import com.windowtester.runtime.swt.internal.UIContextSWT;
import com.windowtester.runtime.swt.internal.hover.IHoverInfo;
import com.windowtester.runtime.swt.internal.operation.SWTDisplayLocation;
import com.windowtester.runtime.swt.internal.operation.SWTLocation;
import com.windowtester.runtime.swt.internal.operation.SWTMouseOperation;
import com.windowtester.runtime.swt.internal.reveal.RevealStrategyFactory;
import com.windowtester.runtime.swt.internal.selector.UIDriver;
import com.windowtester.runtime.swt.internal.widgets.ISWTWidgetReference;
/**
* A UIContext helper class to manage drag and drop operations.
* <p>
* A few things worth noting. First, DragAndDropHelper posts primitive events rather than delegating to the UIContext.
* This is important because:
* <ol>
* <li> We do not want conditions to be checked mid drag and
* <li> nor do we want any syncExecs to be called once the drag (mouseDown) has begun
* (such syncExecs would lead to deadlock).
* </ol>
* Second, it is the UIContext's responsibility to track the drag source.
* The WindowTester DND API uses <em>implicit</em> drag source identification: it is
* the user's responsibility to move the mouse to the source of the drag by calling
* one of the family of {@link IUIContext#mouseMove(com.windowtester.runtime.locator.ILocator)} methods.
*
* The UIContext caches these mouse moves (and potential drag sources) by calling
* {@link DragAndDropHelper#setDragSourceLocation(Point)} from within
* {@link UIContextSWT#mouseMove(int, int)}.
* <p>
* Notice that a source is set <em>on all moves</em>
* whether they are followed by a drag event or not. Source location is only
* used in cases of drag events. (Drag source is roughly equivalent to "lastMouseLocation").
*
*/
public class DragAndDropHelper {
/** OS-identifying flag */
private static final boolean IS_LINUX = SWT.getPlatform().equals("gtk");
/** Amount to wiggle to trigger drag/drop events in the native OS */
private static final int WIGGLE = IS_LINUX ? 11 : 1;
private static final Robot wiggleBot = new Robot();
/** A back-pointer to the host UIContext */
private final UIContextSWT ui;
/** An OS-specific DND strategy */
private IDNDAction _dndStrategy;
/**
* Create an instance.
* @param ui the host UIContext
*/
public DragAndDropHelper(UIContextSWT ui) {
this.ui = ui;
}
///////////////////////////////////////////////////////////////////
//
// Accessors.
//
///////////////////////////////////////////////////////////////////
/**
* Get the associated UIDriver for posting low-level UI events.
* @return the associated UIDriver instance
*/
protected UIDriver getDriver() {
return ui.getDriver();
}
/**
* Get the OS-Specific mouse mask associated with drag gestures.
* @return the drag/drop mouse mask
*/
protected int getDragMouseMask() {
/*
* TODO: ultimately, this should come from a service (in UIDriver?) that handles OS-specific details
*/
return SWT.BUTTON1;
}
/**
* Get the drag source location.
* @return a point describing the drag source.
*/
protected Point getDragSourceLocation() {
return getDragSource().getLocation();
}
/**
* Get the drag source location.
* @return a point describing the drag source.
*/
protected IHoverInfo getDragSource() {
return getDriver().getCurrentMouseHoverInfo();
}
///////////////////////////////////////////////////////////////////
//
// Drag/drop API support.
//
// The key to this support is that NO syncExecs are called once the
// drag has begun (hence the calculation of the target before mousDown)
//
///////////////////////////////////////////////////////////////////
/**
* Perform a drag to this widget at this offset using the source point retrieved
* from {@link #getDragSource()}.
* <p>
* Note that all widget relative dragTos ultimately call this one.
* @param target the target of the drop
* @param x the x offset
* @param y the y offset
*/
public Widget dragTo(Widget target, int x, int y) {
//check to see if target is visible and make it so if not
handleReveal(target, x, y);
Point dest = getLocation(target);
dragTo(dest.x+x, dest.y+y);
return target;
}
/**
* Perform a drag operation to the item in this widget identified
* by path.
* @param w the parent widget (e.g., Tree)
* @param path the path string (e.g., "parent/child")
* @param x the x offset
* @param y the y offset
* @throws MultipleWidgetsFoundException
* @throws WidgetNotFoundException
*/
public Widget dragTo(Widget w, String path, int x, int y) throws WidgetNotFoundException, MultipleWidgetsFoundException {
Widget target = handleReveal(w, path, x, y);
Point dest = getLocation(target);
dragTo(dest.x+x, dest.y+y);
return target;
}
/**
* Perform a drag to the center of this widget using the source point retrieved
* from {@link #getDragSource()}.
* @param target the target of the drop
*/
public Widget dragTo(Widget target) {
Rectangle rect = getBounds(target);
dragTo(target, rect.width/2, rect.height/2);
return target;
}
/**
* Perform a drag to this absolute x,y coordinate using the source point retrieved
* from {@link #getDragSource()}.
* @param x the x coordinate
* @param y the y coordinate
*/
public void dragTo(int x, int y) {
dragTo(new Point(x, y));
}
/**
* Perform a drag to this destination using the source point retrieved
* from {@link #getDragSource()}.
* <p>
* All drag operations ultimately call this one.
* <p>
* Note: all reveal operations are expected to have been done before calling this method.
* @param dest the target of the drag
* @throws IllegalStateException if no drag source is set
*/
public void dragTo(final Point dest) {
Point src = getDragSourceLocation();
if (src == null)
throw new IllegalStateException("no drag source set in drag/drop operation");
//delegate to OS-specific strategy
getDNDStrategy().doDND(src, dest);
}
///////////////////////////////////////////////////////////////////
//
// Internal
//
///////////////////////////////////////////////////////////////////
private IDNDAction getDNDStrategy() {
if (_dndStrategy == null) {
if (isLinux())
_dndStrategy = new LinuxDNDAction();
else
_dndStrategy = new WindowsDNDAction();
}
return _dndStrategy;
}
private void syncExec(Runnable runnable) {
ui.getDisplay().syncExec(runnable);
}
private void pause(int ms) {
ui.pause(ms);
}
/**
* Wait for the ui thread to be idle.
*/
private void waitForIdle() {
// _ui.waitForIdle();
new SWTIdleCondition(ui.getDisplay()).waitForIdle();
}
/**
* Test to see if we're running on GTK-linux.
*/
private boolean isLinux() {
return IS_LINUX;
}
/**
* Wiggle around this point. Required to ensure drag and drop gestures
* are recognized by the OS.
* @param pt the point around which to wiggle
*/
private void wiggle(Point pt) {
moveTo(getWiggle(pt));
//TODO: consider moving back to the origin?
}
/**
* Wiggle around this point. Required to ensure drag and drop gestures
* are recognized by the OS.
* @param pt the point around which to wiggle
*/
private void awtWiggle(Point pt) {
Point to = getWiggle(pt);
wiggleBot.mouseMove(to.x, to.y);
}
/**
* Get a point offset from this one that is sufficient to signal a
* drag or drop gesture.
* @param pt the target point
*/
private Point getWiggle(Point pt) {
int wx = (pt.x > WIGGLE) ? pt.x-WIGGLE : pt.x +WIGGLE;
return new Point(wx, pt.y);
}
/**
* Ensure that the target is visible, revealing if necessary.
* @param target
* @param x
* @param y
*/
private Widget handleReveal(Widget target, int x, int y) {
Widget revealed = RevealStrategyFactory.getRevealer(target).reveal(target, x, y);
//reposition the mouse over our drag source:
moveTo(getDragSourceLocation()); //ideally this would only happen in case of a reveal; for now it always happens
return revealed;
}
private Widget handleReveal(Widget target, String path, int x, int y) throws WidgetNotFoundException, MultipleWidgetsFoundException {
Widget revealed = RevealStrategyFactory.getRevealer(target).reveal(target, path, x, y);
//reposition the mouse over our drag source:
moveTo(getDragSourceLocation()); //ideally this would only happen in case of a reveal; for now it alwasy happens
return revealed;
}
///////////////////////////////////////////////////////////////////
//
// Primitive event generation
//
///////////////////////////////////////////////////////////////////
/**
* Move the mouse to this point using primitive driver operations.
* @param dest the destination point
*/
private void moveTo(Point dest) {
getDriver().mouseMove(dest.x, dest.y);
}
/**
* Start the drag operation.
*/
private void startDrag() {
getDriver().mouseDown(getDragMouseMask());
}
/**
* Finish the drag operation.
*/
private void finishDrag() {
//a slight pause before dropping
UIDriver.pause(500); //NOTE: using driver and NOT context here
//drop
getDriver().mouseUp(getDragMouseMask());
}
///////////////////////////////////////////////////////////////////
//
// Location calculating helpers.
//
///////////////////////////////////////////////////////////////////
/**
* Get the absolute location of this widget.
* @param w - the widget in question
* @return the widget's point in space
*/
private static Point getLocation(final Widget w) {
final Point[] point = new Point[1];
// TODO[pq]: getLocation should be in the widget ref
w.getDisplay().syncExec(new Runnable(){
public void run(){
point[0] = WidgetLocator.getLocation(w);
}
});
return point[0];
}
/**
* Get this target widget's bounds.
* @param target the widget in question
* @return the widget's bounds
*/
private static Rectangle getBounds(Widget target) {
// TODO[pq]: this reference should be pre-calculated and cached
ISWTWidgetReference<?> ref = (ISWTWidgetReference<?>) WTRuntimeManager.asReference(target);
return ref.getDisplayBounds();
}
interface IDNDAction {
void doDND(Point src, Point dest);
}
class WindowsDNDAction implements IDNDAction {
public void doDND(Point src, Point dest) {
// //start dragging
// startDrag();
//
// //wiggle to ensure drag gesture is recognized
// wiggle(src);
//
// //moveTo and wiggle to ensure drop gesture is recognized
// wiggle(dest);
//
// //settle on target
// moveTo(dest);
//
// //end drag
// finishDrag();
SWTLocation start = new SWTDisplayLocation().offset(src);
SWTLocation end = new SWTDisplayLocation().offset(dest);
new SWTMouseOperation(getDragMouseMask()).at(start).dragTo(end).execute();
}
}
class LinuxDNDAction implements IDNDAction {
final int NUM_DROP_WIGGLES = 4;
final int DROP_PAUSE = 250;
public void doDND(final Point src, final Point dest) {
/*
* Start drag
*/
syncExec(new Runnable() {
public void run() {
//start dragging
startDrag();
//wiggle to ensure drag gesture is recognized
wiggle(src);
}
});
waitForIdle();
/*
* Initiate drop.
*
* N.B.: Linux is very finnicky and occasionally does not
* register the drop event. To attempt to address this we
* hover, pause and wiggle a number of times before making the
* drop. This _seems_ to do the trick.
* Caveat: the number and pause amount constants are derived
* from experimentation.
*/
for (int i=0; i <= NUM_DROP_WIGGLES; ++i) {
awtWiggle(dest);
waitForIdle();
pause(DROP_PAUSE);
}
//end drag
syncExec(new Runnable() {
public void run() {
finishDrag();
}
});
waitForIdle();
}
}
}