com.google.appinventor.client.widgets.dnd.DragSourceSupport.java Source code

Java tutorial

Introduction

Here is the source code for com.google.appinventor.client.widgets.dnd.DragSourceSupport.java

Source

// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

package com.google.appinventor.client.widgets.dnd;

import com.google.appinventor.client.output.OdeLog;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.MouseListener;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.Widget;

/**
 * Provides support for dragging from a {@link DragSource}
 * (typically a widget) to a {@link DropTarget}.
 *
 */
public final class DragSourceSupport implements MouseListener {
    /**
     * Interface to functionality provided by the {@link DOM} class.
     * Used as a testing seam.
     *
     */
    // @VisibleForTesting
    static interface IDom {
        public void setCapture(Element elem);

        public void releaseCapture(Element elem);

        public void eventPreventDefaultOfCurrentEvent();

        public com.google.gwt.dom.client.Element getFromElementOfCurrentEvent();

        public com.google.gwt.dom.client.Element getToElementOfCurrentEvent();
    }

    /**
     * Implementation of {@link IDom} that delegates to the real
     * {@link DOM} class.
     *
     */
    private static class RealDom implements IDom {
        private static final RealDom INSTANCE = new RealDom();

        /**
         * Prevent instantiation of static class.
         */
        private RealDom() {
            // nothing
        }

        public void setCapture(Element elem) {
            DOM.setCapture(elem);
        }

        public void releaseCapture(Element elem) {
            DOM.releaseCapture(elem);
        }

        public void eventPreventDefaultOfCurrentEvent() {
            DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
        }

        public com.google.gwt.dom.client.Element getFromElementOfCurrentEvent() {
            return DOM.eventGetCurrentEvent().getFromElement();
        }

        public com.google.gwt.dom.client.Element getToElementOfCurrentEvent() {
            return DOM.eventGetCurrentEvent().getToElement();
        }
    }

    /**
     * This class is used to show a widget while dragging. This could be anything
     * from a simple outline to a copy of the {@code DragSource} widget.
     */
    private static class DragWidgetPopup extends PopupPanel {
        public DragWidgetPopup(Widget w) {
            super(true);
            setWidget(w);
        }
    }

    /**
     * Number of pixels away from the click-point that a drag-source must be
     * dragged to initiate a drag action.
     */
    // @VisibleForTesting
    static final int DRAG_THRESHOLD = 5;

    // Provider of the drag widget and the set of permissible drop targets
    private final DragSource dragSource;
    // DOM implementation
    private final IDom dom;

    // Location (in the drag-widget coordinate system) where the last mouse-down originated.
    // When a drag is in progress, this is the origin of the click that initiated the drag.
    private int startX;
    private int startY;

    private boolean captured;
    private boolean mouseIsDown;
    private boolean dragInProgress;

    // Location (in the drag-widget coordinate system) where the last mouse-move originated
    // while the mouse button was down.
    private int dragX;
    private int dragY;

    // Array of widgets that the drag source widget can be dropped on
    private DropTarget[] dropTargets;

    // Popup containing the widget being shown while dragging
    private DragWidgetPopup dragWidgetPopup;

    // The drop target that the cursor is hovering over currently
    private DropTarget hoverDropTarget;

    /**
     * Creates a new instance of this class to provide support for dragging
     * from the specified drag source to any of the drop targets that it defines.
     * <p>
     * After creation, the caller must add this {@link DragSourceSupport} as
     * a {@link MouseListener} to whatever actual {@link UIObject} will
     * receive drag gestures.
     */
    public DragSourceSupport(DragSource dragSource) {
        this(dragSource, RealDom.INSTANCE);
    }

    // @VisibleForTesting
    DragSourceSupport(DragSource dragSource, IDom dom) {
        this.dragSource = dragSource;
        this.dom = dom;

        startX = -1;
        startY = -1;
        mouseIsDown = false;
        dragInProgress = false;
        dragX = -1;
        dragY = -1;

        dropTargets = null;
        dragWidgetPopup = null;
        hoverDropTarget = null;
    }

    // Private utility methods

    /**
     * Clears any existing selections in the browser.
     * <p>
     * While we are normally trying to avoid falling back to using embedded Javascript, it seems
     * that this cannot currently be done using the GWT APIs.
     */
    private static native void clearSelections() /*-{
                                                 try {
                                                 if ($doc.selection && $doc.selection.empty) {
                                                 $doc.selection.empty();
                                                 } else if ($wnd.getSelection) {
                                                 var sel = $wnd.getSelection();
                                                 if (sel) {
                                                 if (sel.removeAllRanges) {
                                                 sel.removeAllRanges();
                                                 }
                                                 if (sel.collapse) {
                                                 sel.collapse();
                                                 }
                                                 }
                                                 }
                                                 } catch (ignore) {
                                                 // Well, we tried...
                                                 }
                                                 }-*/;

    /**
     * Returns whether the specified widget contains a position given
     * by the absolute coordinates.
     *
     * @param w  widget to test
     * @param absX  absolute x coordinate of position
     * @param absY  absolute y coordinate of position
     * @return  {@code true} if the position is within the widget, {@code false}
     *          otherwise
     */
    private static boolean isInside(Widget w, int absX, int absY) {
        int wx = w.getAbsoluteLeft();
        int wy = w.getAbsoluteTop();
        int ww = w.getOffsetWidth();
        int wh = w.getOffsetHeight();

        return (wx <= absX) && (absX < wx + ww) && (wy <= absY) && (absY < wy + wh);
    }

    // Drag-widget positioning

    /**
     * Configures the specified drag-widget (that will be returned by
     * {@link DragSource#createDragWidget(int, int)}) so that the cursor's hot spot
     * will appear at the point (x,y) in the widget's coordinate system.
     */
    public static void configureDragWidgetToAppearWithCursorAt(Widget w, int x, int y) {
        Element e = w.getElement();
        DOM.setStyleAttribute(e, "position", "absolute");
        DOM.setStyleAttribute(e, "left", -x + "px");
        DOM.setStyleAttribute(e, "top", -y + "px");
    }

    /**
     * Returns the x-coordinate where the cursor appears in the specified
     * drag-widget's coordinate system.
     */
    private static int getDragWidgetOffsetX(Widget w) {
        return -parsePixelValue(DOM.getStyleAttribute(w.getElement(), "left"));
    }

    /**
     * Returns the y-coordinate where the cursor appears in the specified
     * drag-widget's coordinate system.
     */
    private static int getDragWidgetOffsetY(Widget w) {
        return -parsePixelValue(DOM.getStyleAttribute(w.getElement(), "top"));
    }

    private static int parsePixelValue(String pixelValueStr) {
        if ((pixelValueStr != null) && pixelValueStr.endsWith("px")) {
            try {
                return Integer.parseInt(pixelValueStr.substring(0, pixelValueStr.length() - "px".length()));
            } catch (NumberFormatException e) {
                return 0;
            }
        } else {
            return 0;
        }
    }

    // MouseListener implementation

    @Override
    public void onMouseDown(Widget sender, int x, int y) {
        if (mouseIsDown) {
            OdeLog.wlog("received onMouseDown event when we thought the mouse was already down");
        }
        mouseIsDown = true;

        startX = x;
        startY = y;

        if (!captured) {
            // Force browser to keep sending us events until the mouse is released
            dom.setCapture(sender.getElement());
            captured = true;
        }

        // Prevent default actions like image-dragging and text selections from being triggered
        dom.eventPreventDefaultOfCurrentEvent();
        // TODO(user): Consider removing this, since it seems to have
        //                    less effect (at least on Firefox 2) than the line above,
        //                    is more complex, and is browser-dependent.
        DeferredCommand.addCommand(new Command() {
            @Override
            public void execute() {
                clearSelections();
            }
        });
    }

    // NOTE: At least in Firefox 2, if the user drags outside of the browser window,
    //       mouse-move (and even mouse-down) events will not be received until
    //       the user drags back inside the window. A workaround for this issue
    //       exists in the implementation for onMouseLeave().
    @Override
    public void onMouseMove(Widget sender, int x, int y) {
        if (mouseIsDown) {
            dragX = x;
            dragY = y;

            if (dragInProgress) {
                onDragContinue(sender, x, y);
            } else {
                dragInProgress = (manhattanDist(x, y, startX, startY) >= DRAG_THRESHOLD);
                if (dragInProgress) {
                    onDragStart(sender, x, y);

                    // Check whether we are already hovering over a potential drop target
                    onDragContinue(sender, x, y);
                }
            }

            // Prevent default actions from being triggered
            dom.eventPreventDefaultOfCurrentEvent();
        }
    }

    @Override
    public void onMouseUp(Widget sender, int x, int y) {
        if (!mouseIsDown) {
            OdeLog.wlog("received onMouseUp event when we thought the mouse was already up");
        }
        mouseIsDown = false;

        if (captured) {
            // Allow other elements to receive events after the drag/click
            dom.releaseCapture(sender.getElement());
            captured = false;
        }

        if (dragInProgress) {
            onDragEnd(sender, x, y);
        }

        startX = -1;
        startY = -1;
        dragInProgress = false;

        // Prevent default actions from being triggered
        dom.eventPreventDefaultOfCurrentEvent();
    }

    @Override
    public void onMouseEnter(Widget sender) {
        if (dragInProgress) {
            // Firefox 2 specific. IE6 does not need this.
            if (dom.getFromElementOfCurrentEvent() == getDragWidget().getElement()
                    && isRootHtmlElement(dom.getToElementOfCurrentEvent())) {
                // The user moved the mouse outside the browser window.
                //
                // Simulate a mouse-moved event to a position offscreen,
                // since this is not done automatically in Firefox 2.
                onMouseMove(sender, /*localX*/ (/*absX*/ -1) - sender.getAbsoluteLeft(),
                        /*localY*/ (/*absY*/ -1) - sender.getAbsoluteTop());
                return;
            }
        }
    }

    @Override
    public void onMouseLeave(Widget sender) {
        if (dragInProgress) {
            // Firefox 2 specific. IE6 does not need this.
            if (isRootHtmlElement(dom.getFromElementOfCurrentEvent()) && dom.getToElementOfCurrentEvent() == null) {
                // The user released the mouse button while
                // the mouse was outside the browser window.
                //
                // Simulate a mouse-release event, since this
                // is not done automatically in Firefox 2.
                onMouseUp(sender, dragX, dragY);
                return;
            }
        }
    }

    private static int manhattanDist(int x1, int y1, int x2, int y2) {
        return Math.abs(x1 - x2) + Math.abs(y1 - y2);
    }

    /**
     * Returns whether the specified element is the root HTML element of the web page.
     */
    private static boolean isRootHtmlElement(com.google.gwt.dom.client.Element element) {
        return "html".equalsIgnoreCase(element.getTagName());
    }

    /**
     * Returns the drag widget created by the last call to
     * {@link DragSource#createDragWidget(int, int)}.
     */
    public Widget getDragWidget() {
        return dragWidgetPopup.getWidget();
    }

    // Drag handling

    private void onDragStart(Widget sender, int x, int y) {
        // Notify drag source of the drag starting
        dragSource.onDragStart();

        // Cache the set of permissible drop targets
        dropTargets = dragSource.getDropTargets();

        // Show drag proxy widget
        dragWidgetPopup = new DragWidgetPopup(dragSource.createDragWidget(startX, startY));
        dragWidgetPopup.setPopupPosition(/*absX*/ x + sender.getAbsoluteLeft(),
                /*absY*/ y + sender.getAbsoluteTop());
        dragWidgetPopup.show();

        // Initialize hover state
        hoverDropTarget = null;
    }

    private void onDragContinue(Widget sender, int x, int y) {
        int absX = x + sender.getAbsoluteLeft();
        int absY = y + sender.getAbsoluteTop();

        // Move drag proxy to new position
        dragWidgetPopup.setPopupPosition(absX, absY);

        // Find drop target that the cursor is currently hovering over
        for (DropTarget target : dropTargets) {
            Widget targetWidget = target.getDropTargetWidget();
            if (target == sender) {
                // can't drop onto self - only an issue if sender is a container
                continue;
            }

            boolean isInsideTargetWidget = isInside(targetWidget, absX, absY);

            if (target == hoverDropTarget) {
                if (isInsideTargetWidget) {
                    // The last identified drop-target "captures" the attention
                    // of the drag and drop system while the user is still dragging
                    // within its bounds and no other contained drop target accepts the drag
                    break;
                } else {
                    // Drag has left the bounds of the current hover-target
                    hoverDropTarget.onDragLeave(dragSource);
                    hoverDropTarget = null;

                    // Continue searching for enclosing and non-intersecting
                    // drop targets to accept the current drag
                    continue;
                }
            }

            if (isInsideTargetWidget) {
                int localX = absX - targetWidget.getAbsoluteLeft();
                int localY = absY - targetWidget.getAbsoluteTop();
                if (target.onDragEnter(dragSource, localX, localY)) {
                    if (hoverDropTarget != null) {
                        // Drag exits the old hover-target because it has entered
                        // the bounds of an accepting drop target that is within
                        // the bounds of the old hover-target
                        hoverDropTarget.onDragLeave(dragSource);
                    }

                    // Drag accepted; current target becomes the new hover-target
                    hoverDropTarget = target;

                    // The guaranteed onDragContinue() event that follows all invocations
                    // of onDragEnter() that accept the drag is fired later in this method
                    break;
                }
            }
        }

        // Inform the hover-target of the continuing drag
        if (hoverDropTarget != null) {
            Widget targetWidget = hoverDropTarget.getDropTargetWidget();
            hoverDropTarget.onDragContinue(dragSource, /*localX*/ absX - targetWidget.getAbsoluteLeft(),
                    /*localY*/ absY - targetWidget.getAbsoluteTop());
        }
    }

    private void onDragEnd(Widget sender, int x, int y) {
        // Make sure the current hover-target is still valid,
        // and send the guaranteed onDragContinue() prior to onDrop()
        onDragContinue(sender, x, y);

        // Hide drag widget popup
        dragWidgetPopup.hide();

        // Inform the hover-target of the drop
        if (hoverDropTarget != null) {
            Widget targetWidget = hoverDropTarget.getDropTargetWidget();
            Widget dragWidget = getDragWidget();
            hoverDropTarget.onDrop(dragSource,
                    /*localX*/ (/*absX*/ x + sender.getAbsoluteLeft()) - targetWidget.getAbsoluteLeft(),
                    /*localY*/ (/*absY*/ y + sender.getAbsoluteTop()) - targetWidget.getAbsoluteTop(),
                    getDragWidgetOffsetX(dragWidget), getDragWidgetOffsetY(dragWidget));
        }

        // Notify drag source of the drag end
        dragSource.onDragEnd();

        // Clean up
        dropTargets = null;
        dragWidgetPopup = null;
        hoverDropTarget = null;
    }
}