fi.jasoft.dragdroplayouts.client.ui.VLayoutDragDropMouseHandler.java Source code

Java tutorial

Introduction

Here is the source code for fi.jasoft.dragdroplayouts.client.ui.VLayoutDragDropMouseHandler.java

Source

/*
 * Copyright 2015 John Ahlroos
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package fi.jasoft.dragdroplayouts.client.ui;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.TouchStartEvent;
import com.google.gwt.event.dom.client.TouchStartHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.ui.ComplexPanel;
import com.google.gwt.user.client.ui.LabelBase;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.BrowserInfo;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.Util;
import com.vaadin.client.VCaption;
import com.vaadin.client.VConsole;
import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.VAccordion;
import com.vaadin.client.ui.VAccordion.StackItem;
import com.vaadin.client.ui.VCssLayout;
import com.vaadin.client.ui.VFormLayout;
import com.vaadin.client.ui.VPanel;
import com.vaadin.client.ui.VTabsheet;
import com.vaadin.client.ui.VTabsheet.Tab;
import com.vaadin.client.ui.VTabsheet.TabCaption;
import com.vaadin.client.ui.dd.VDragAndDropManager;
import com.vaadin.client.ui.dd.VDragEvent;
import com.vaadin.client.ui.dd.VTransferable;

import fi.jasoft.dragdroplayouts.client.ui.accordion.VDDAccordion;
import fi.jasoft.dragdroplayouts.client.ui.formlayout.VDDFormLayout;
import fi.jasoft.dragdroplayouts.client.ui.interfaces.VDragImageProvider;
import fi.jasoft.dragdroplayouts.client.ui.interfaces.VHasDragFilter;
import fi.jasoft.dragdroplayouts.client.ui.interfaces.VHasDragImageReferenceSupport;

/**
 * Mouse handler for starting component drag operations
 * 
 * @author John Ahlroos / www.jasoft.fi
 * @since 0.4.0
 */
public class VLayoutDragDropMouseHandler
        implements MouseDownHandler, TouchStartHandler, VHasDragImageReferenceSupport {

    public static final String ACTIVE_DRAG_SOURCE_STYLENAME = "v-dd-active-drag-source";

    private LayoutDragMode dragMode = LayoutDragMode.NONE;

    private final Widget root;

    private Widget currentDraggedWidget;

    private HandlerRegistration mouseUpHandlerReg;

    private HandlerRegistration mouseDownHandlerReg;

    private final List<HandlerRegistration> handlers = new LinkedList<HandlerRegistration>();

    private final List<DragStartListener> dragStartListeners = new ArrayList<VLayoutDragDropMouseHandler.DragStartListener>();

    private Widget attachTarget;

    private VDragImageProvider dragImageProvider;

    private boolean startDragOnMove = true;

    /**
     * A listener to listen for drag start events
     */
    public interface DragStartListener {
        /**
         * Called when a drag is about to begin
         * 
         * @param widget
         *            The widget which is about to be dragged
         * @param mode
         *            The draggin mode
         * @return Should the dragging be commenced.
         */
        boolean dragStart(Widget widget, LayoutDragMode mode);
    }

    /**
     * Constructor
     * 
     * @param root
     *            The root element
     * @param dragMode
     *            The drag mode of the layout
     */
    public VLayoutDragDropMouseHandler(Widget root, LayoutDragMode dragMode) {
        this.dragMode = dragMode;
        this.root = root;
    }

    /**
     * Is the mouse down event a valid mouse drag event, i.e. left mouse button
     * is pressed without any modifier keys
     * 
     * @param event
     *            The mouse event
     * @return Is the mouse event a valid drag event
     */
    private boolean isMouseDragEvent(NativeEvent event) {
        boolean hasModifierKey = event.getAltKey() || event.getCtrlKey() || event.getMetaKey()
                || event.getShiftKey();
        return !(hasModifierKey || event.getButton() > NativeEvent.BUTTON_LEFT);
    }

    @Override
    public void onTouchStart(TouchStartEvent event) {
        NativeEvent nativeEvent = event.getNativeEvent();
        if (isElementNode(nativeEvent) && isChildOfRoot(nativeEvent)) {
            if (startDragOnMove) {
                initiateDragOnMove(event.getNativeEvent());
            } else {
                initiateDrag(event.getNativeEvent());
            }
        }
    }

    @Override
    public void onMouseDown(MouseDownEvent event) {
        NativeEvent nativeEvent = event.getNativeEvent();
        if (isElementNode(nativeEvent) && isChildOfRoot(nativeEvent)) {
            if (startDragOnMove) {
                initiateDragOnMove(event.getNativeEvent());
            } else {
                initiateDrag(event.getNativeEvent());
            }
        }
    }

    private boolean isChildOfRoot(NativeEvent event) {
        EventTarget eventTarget = event.getEventTarget();
        Element targetElement = Element.as(eventTarget);
        if (root.getElement() != targetElement && root.getElement().isOrHasChild(targetElement)) {
            return true;
        }
        return false;
    }

    private boolean isElementNode(NativeEvent event) {
        EventTarget eventTarget = event.getEventTarget();
        if (Element.is(eventTarget)) {
            return true;
        }
        return false;
    }

    /**
     * Initiates the drag only on the first move event
     * 
     * @param originalEvent
     *            the original Mouse Down event. Only events on elements are
     *            passed in here (Element.as() is safe without check here)
     */
    protected void initiateDragOnMove(final NativeEvent originalEvent) {
        EventTarget eventTarget = originalEvent.getEventTarget();

        boolean stopEventPropagation = false;

        Element targetElement = Element.as(eventTarget);
        Widget target = WidgetUtil.findWidget(targetElement, null);
        Widget targetParent = target.getParent();

        // Stop event propagation and prevent default behaviour if
        // - target is *not* a VTabsheet.TabCaption or
        // - drag mode is caption mode and widget is caption
        boolean isTabCaption = targetParent instanceof VTabsheet.TabCaption;
        boolean isCaption = VDragDropUtil.isCaptionOrCaptionless(targetParent);

        if (dragMode == LayoutDragMode.CLONE && isTabCaption == false) {

            stopEventPropagation = true;

            // overwrite stopEventPropagation flag again if
            // - root implements VHasDragFilter but
            // - target is not part of its drag filter and
            // - target is not a GWT Label based widget
            if (root instanceof VHasDragFilter) {
                if (((VHasDragFilter) root).getDragFilter().isDraggable(target) == false
                        && (target instanceof LabelBase) == false) {
                    stopEventPropagation = false;
                }
            }
        }

        if (dragMode == LayoutDragMode.CAPTION && isCaption) {
            stopEventPropagation = true;
        }

        if (isElementNotDraggable(targetElement)) {
            stopEventPropagation = false;
        }

        if (stopEventPropagation) {
            originalEvent.stopPropagation();
            originalEvent.preventDefault();

            // Manually focus as preventDefault() will also cancel focus
            targetElement.focus();
        }

        mouseDownHandlerReg = Event.addNativePreviewHandler(new NativePreviewHandler() {

            @Override
            public void onPreviewNativeEvent(NativePreviewEvent event) {
                int type = event.getTypeInt();
                if (type == Event.ONMOUSEUP || type == Event.ONTOUCHCANCEL || type == Event.ONTOUCHEND) {
                    mouseDownHandlerReg.removeHandler();
                    mouseDownHandlerReg = null;

                } else if (type == Event.ONMOUSEMOVE || type == Event.ONTOUCHMOVE) {
                    mouseDownHandlerReg.removeHandler();
                    mouseDownHandlerReg = null;
                    initiateDrag(originalEvent);
                }
            }
        });
    }

    private boolean isElementNotDraggable(Element targetElement) {
        // do not try to drag tabsheet close button it breaks close on touch devices
        return targetElement.getClassName().contains("v-tabsheet-caption-close");
    }

    /**
     * Called when the dragging a component should be initiated by both a mouse
     * down event as well as a touch start event
     * 
     * FIXME This method is a BIG hack to circumvent Vaadin's very poor client
     * side API's. This will break often. Refactor once Vaadin gets a grip.
     * 
     * @param event
     */
    protected void initiateDrag(NativeEvent event) {
        // Check that dragging is enabled
        if (dragMode == LayoutDragMode.NONE) {
            return;
        }

        // Dragging can only be done with left mouse button and no modifier keys
        if (!isMouseDragEvent(event) && !Util.isTouchEvent(event)) {
            return;
        }

        // Get target widget
        EventTarget eventTarget = event.getEventTarget();
        Element targetElement = Element.as(eventTarget);
        Widget target = WidgetUtil.findWidget(targetElement, null);

        if (isEventOnScrollBar(event)) {
            return;
        }

        // do not drag close button of TabSheet tab
        if (isElementNotDraggable(targetElement)) {
            VDragAndDropManager.get().interruptDrag();
            return;
        }

        // Abort if drag mode is caption mode and widget is not a caption
        boolean isPanelCaption = target instanceof VPanel
                && targetElement.getParentElement().getClassName().contains("v-panel-caption");
        boolean isCaption = isPanelCaption || VDragDropUtil.isCaptionOrCaptionless(target);

        if (dragMode == LayoutDragMode.CAPTION && !isCaption) {
            /*
             * Ensure target is a caption in caption mode
             */
            return;
        }

        if (dragMode == LayoutDragMode.CAPTION && isCaption) {

            /*
             * Ensure that captions in nested layouts don't get accepted if in
             * caption mode
             */

            Widget w = VDragDropUtil.getTransferableWidget(target);
            ComponentConnector c = Util.findConnectorFor(w);
            ComponentConnector parent = (ComponentConnector) c.getParent();
            if (parent.getWidget() != root) {
                return;
            }
        }

        // Create the transfarable
        VTransferable transferable = VDragDropUtil.createLayoutTransferableFromMouseDown(event, root, target);

        // Are we trying to drag the root layout
        if (transferable == null) {
            VConsole.log("Creating transferable on mouse down returned null");
            return;
        }

        // Resolve the component
        final Widget w;
        ComponentConnector c = null, parent = null;

        if (target instanceof TabCaption) {
            TabCaption tabCaption = (TabCaption) target;
            Tab tab = tabCaption.getTab();
            int tabIndex = ((ComplexPanel) tab.getParent()).getWidgetIndex(tab);
            VTabsheet tabsheet = tab.getTabsheet();

            w = tab;
            c = tabsheet.getTab(tabIndex);
            parent = Util.findConnectorFor(tabsheet);

        } else if (root instanceof VDDAccordion) {
            w = target;
            parent = Util.findConnectorFor(root);

            StackItem tab = WidgetUtil.findWidget(targetElement, StackItem.class);
            if (tab != null && root.getElement().isOrHasChild(tab.getElement())) {
                c = ((VDDAccordion) root).getTab(((VDDAccordion) root).getTabPosition(tab));
            }

        } else if (transferable.getData(Constants.TRANSFERABLE_DETAIL_COMPONENT) != null) {

            ComponentConnector connector = (ComponentConnector) transferable
                    .getData(Constants.TRANSFERABLE_DETAIL_COMPONENT);
            w = connector.getWidget();
            c = Util.findConnectorFor(w);
            parent = (ComponentConnector) c.getParent();

        } else {
            // Failsafe if no widget was found
            w = root;
            c = Util.findConnectorFor(w);
            parent = (ComponentConnector) c.getParent();
            VConsole.log("Could not resolve component, using root as component");
        }

        VConsole.log("Dragging widget: " + w);
        VConsole.log(" in parent: " + parent);

        // Ensure component is draggable
        if (!VDragDropUtil.isDraggingEnabled(parent, w)) {
            VConsole.log("Dragging disabled for " + w.getClass().getName() + " in "
                    + parent.getWidget().getClass().getName());
            VDragAndDropManager.get().interruptDrag();
            return;
        }

        // Announce drag start to listeners
        for (DragStartListener dl : dragStartListeners) {
            if (!dl.dragStart(w, dragMode)) {
                VDragAndDropManager.get().interruptDrag();
                return;
            }
        }

        currentDraggedWidget = w;

        // Announce to handler that we are starting a drag operation
        VDragEvent currentDragEvent = VDragAndDropManager.get().startDrag(transferable, event, true);

        /*
         * Create the drag image
         */
        com.google.gwt.dom.client.Element dragImageElement = dragImageProvider == null ? null
                : dragImageProvider.getDragImageElement(w);

        if (dragImageElement != null) {

            // Set stylename to proxy component as well
            dragImageElement.addClassName(ACTIVE_DRAG_SOURCE_STYLENAME);

        } else if (root instanceof VCssLayout) {
            /*
             * CSS Layout does not have an enclosing div so we just use the
             * component div
             */
            dragImageElement = w.getElement();

        } else if (root instanceof VTabsheet) {
            /*
             * Tabsheet should use the dragged tab as a drag image
             */
            dragImageElement = targetElement;

        } else if (root instanceof VAccordion) {
            /*
             * Accordion should use the dragged tab as a drag image
             */
            dragImageElement = targetElement;

        } else if (root instanceof VFormLayout) {
            /*
             * Dragging a component in a form layout should include the caption
             * and error indicator as well
             */
            Element rowElement = (Element) VDDFormLayout
                    .getRowFromChildElement((com.google.gwt.dom.client.Element) w.getElement().cast(),
                            (com.google.gwt.dom.client.Element) root.getElement().cast())
                    .cast();

            dragImageElement = rowElement;

        } else {
            /*
             * For other layouts we just use the target element;
             */
            dragImageElement = w.getElement();
        }

        currentDragEvent.createDragImage(dragImageElement, true);
        Element clone = currentDragEvent.getDragImage();
        assert (clone != null);

        // Lock drag image dimensions
        clone.getStyle().setWidth(dragImageElement.getOffsetWidth(), Unit.PX);
        clone.getStyle().setHeight(dragImageElement.getOffsetHeight(), Unit.PX);

        if (c != null && c.delegateCaptionHandling() && !(root instanceof VTabsheet)
                && !(root instanceof VAccordion)) {
            /*
             * Captions are not being dragged with the widget since they are
             * separate. Manually add a clone of the caption to the drag image.
             */
            if (target instanceof VCaption) {
                clone.insertFirst(targetElement.cloneNode(true));
            }
        }

        if (BrowserInfo.get().isIE()) {
            // Fix IE not aligning the drag image correctly when dragging
            // layouts
            clone.getStyle().setPosition(Position.ABSOLUTE);
        }

        currentDraggedWidget.addStyleName(ACTIVE_DRAG_SOURCE_STYLENAME);

        // Listen to mouse up for cleanup
        mouseUpHandlerReg = Event.addNativePreviewHandler(new Event.NativePreviewHandler() {
            @Override
            public void onPreviewNativeEvent(NativePreviewEvent event) {
                if (event.getTypeInt() == Event.ONMOUSEUP || event.getTypeInt() == Event.ONTOUCHEND
                        || event.getTypeInt() == Event.ONTOUCHCANCEL) {
                    if (mouseUpHandlerReg != null) {
                        mouseUpHandlerReg.removeHandler();
                        if (currentDraggedWidget != null) {

                            currentDraggedWidget.removeStyleName(ACTIVE_DRAG_SOURCE_STYLENAME);

                            if (dragImageProvider != null) {
                                com.google.gwt.dom.client.Element dragImageElement = dragImageProvider
                                        .getDragImageElement(currentDraggedWidget);
                                if (dragImageElement != null) {
                                    dragImageElement.removeClassName(ACTIVE_DRAG_SOURCE_STYLENAME);
                                }
                            }

                            currentDraggedWidget = null;
                        }
                    }

                    // Ensure capturing is turned off at mouse up
                    Event.releaseCapture(RootPanel.getBodyElement());
                }
            }
        });

    }

    /*
     * Whether the event was performed on a scrollbar.
     */
    private boolean isEventOnScrollBar(NativeEvent event) {
        Element element = Element.as(event.getEventTarget());
        ;

        if (WidgetUtil.mayHaveScrollBars(element)) {

            final int nativeScrollbarSize = WidgetUtil.getNativeScrollbarSize();

            int x = WidgetUtil.getTouchOrMouseClientX(event) - element.getAbsoluteLeft();
            int y = WidgetUtil.getTouchOrMouseClientY(event) - element.getAbsoluteTop();

            // Hopefully we have horizontal scroll.
            final int scrollWidth = element.getScrollWidth();
            final int clientWidth = element.getClientWidth();
            if (scrollWidth > clientWidth && clientWidth - nativeScrollbarSize < x) {
                return true;
            }

            // Hopefully we have vertical scroll.
            final int scrollHeight = element.getScrollHeight();
            final int clientHeight = element.getClientHeight();
            if (scrollHeight > clientHeight && clientHeight - nativeScrollbarSize < y) {
                return true;
            }

        }

        return false;
    }

    /**
     * Set the current drag mode
     * 
     * @param dragMode
     *            The drag mode to use
     */
    public void updateDragMode(LayoutDragMode dragMode) {
        if (dragMode == this.dragMode) {
            return;
        }

        this.dragMode = dragMode;
        if (dragMode == LayoutDragMode.NONE) {
            detach();
        } else {
            attach();
        }
    }

    /**
     * Add a drag start listener to monitor drag starts
     * 
     * @param listener
     */
    public void addDragStartListener(DragStartListener listener) {
        dragStartListeners.add(listener);
    }

    /**
     * Remove a drag start listener
     * 
     * @param listener
     */
    public void removeDragStartListener(DragStartListener listener) {
        dragStartListeners.remove(listener);
    }

    /**
     * Start listening to events
     */
    private void attach() {
        if (handlers.isEmpty()) {
            if (attachTarget == null) {
                handlers.add(root.addDomHandler(this, MouseDownEvent.getType()));
                handlers.add(root.addDomHandler(this, TouchStartEvent.getType()));
            } else {
                handlers.add(attachTarget.addDomHandler(this, MouseDownEvent.getType()));
                handlers.add(attachTarget.addDomHandler(this, TouchStartEvent.getType()));
            }
        }
    }

    /**
     * Stop listening to events
     */
    private void detach() {
        for (HandlerRegistration reg : handlers) {
            reg.removeHandler();
        }
        handlers.clear();
    }

    public Widget getAttachTarget() {
        return attachTarget;
    }

    public void setAttachTarget(Widget attachTarget) {
        this.attachTarget = attachTarget;
    }

    public LayoutDragMode getDragMode() {
        return dragMode;
    }

    @Override
    public void setDragImageProvider(VDragImageProvider provider) {
        this.dragImageProvider = provider;
    }

    public boolean isStartDragOnMove() {
        return startDragOnMove;
    }

    public void setStartDragOnMove(boolean startDragOnMove) {
        this.startDragOnMove = startDragOnMove;
    }
}