org.primaresearch.web.gwt.client.ui.MouseScrollPanel.java Source code

Java tutorial

Introduction

Here is the source code for org.primaresearch.web.gwt.client.ui.MouseScrollPanel.java

Source

/*
 * Copyright 2014 PRImA Research Lab, University of Salford, United Kingdom
 *
 * 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 org.primaresearch.web.gwt.client.ui;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.primaresearch.maths.geometry.Point;

import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.event.dom.client.HasAllMouseHandlers;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.event.dom.client.MouseOverEvent;
import com.google.gwt.event.dom.client.MouseOverHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.dom.client.MouseWheelEvent;
import com.google.gwt.event.dom.client.MouseWheelHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Widget;

/**
 * Scroll panel handling mouse events to enable panning by dragging the mouse.<br>
 * <br>
 * The first widget that is added to the panel is used for the scrolling.
 * 
 * @author Christian Clausner
 *
 */
public class MouseScrollPanel extends AbsolutePanel implements HasAllMouseHandlers, MouseWheelHandler,
        MouseOverHandler, MouseOutHandler, MouseMoveHandler, MouseUpHandler, MouseDownHandler {
    /** Values close to 1 mean slow deceleration; higher values mean fast deceleration. Reasonable range: (1.1,5) */
    private static double scrollingDecelerationFactor = 1.5;

    private Point mouseDownPoint = null;

    private Point scrollWidgetBasePosition;

    //Mouse move history
    private Point mouseMovePoint1 = null;
    private long mouseDownTime1;
    private Point mouseMovePoint2 = null;
    private long mouseDownTime2;

    //Auto scroll
    private double scrollingSpeedX;
    private double scrollingSpeedY;
    private Timer autoScrollTimer = null;
    private boolean isAutoScrolling = false;
    private Set<ScrollListener> scrollListeners = new HashSet<ScrollListener>();

    private double targetScrollX = Double.MIN_VALUE;
    private double targetScrollY = Double.MIN_VALUE;

    private boolean enableMouseWheelScrolling;

    private boolean isDragged = false;

    private MouseHandlerExtension mouseHandlerExtention = null;

    /**
     * Constructor
     * @param enableMouseWheelScrolling Set to <code>true</code> to enable scrolling up/down via mouse wheel
     */
    public MouseScrollPanel(boolean enableMouseWheelScrolling) {
        super();
        this.enableMouseWheelScrolling = enableMouseWheelScrolling;
        this.addMouseWheelHandler(this);
        this.addMouseOverHandler(this);
        this.addMouseOutHandler(this);
        this.addMouseMoveHandler(this);
        this.addMouseUpHandler(this);
        this.addMouseDownHandler(this);
        getElement().getStyle().setProperty("overflow", "visible");
    }

    public void setMouseHandlerExtension(MouseHandlerExtension mouseHandlerExtention) {
        this.mouseHandlerExtention = mouseHandlerExtention;
    }

    public void addScrollListener(ScrollListener listener) {
        this.scrollListeners.add(listener);
    }

    public void removeScrollListener(ScrollListener listener) {
        this.scrollListeners.remove(listener);
    }

    @Override
    public final void onMouseWheel(MouseWheelEvent event) {
        boolean handle = true;
        if (mouseHandlerExtention != null)
            handle = !mouseHandlerExtention.onMouseWheel(event);

        if (handle) {
            if (autoScrollTimer != null)
                autoScrollTimer.cancel();

            if (!enableMouseWheelScrolling)
                return;

            //Ctrl + Mouse wheel
            if (event.isControlKeyDown()) {
                //Handled somewhere else
            }
            //Scrolling
            else {
                int dy = (int) confineVertically(-10 * event.getDeltaY());
                if (dy == 0)
                    dy = workaroundEventGetMouseWheelVelocityY(event.getNativeEvent());
                if (dy != 0) {
                    for (int i = 0; i < this.getWidgetCount(); i++) {
                        Widget widget = this.getWidget(i);
                        if (widget != null && mouseDownPoint == null) {
                            int x = this.getWidgetLeft(widget);
                            int y = this.getWidgetTop(widget);
                            this.setWidgetPosition(widget, x, y + dy - 1);
                        }
                    }
                }
            }
        }

        if (mouseHandlerExtention != null)
            mouseHandlerExtention.postMouseWheel(event);
    }

    private static native int workaroundEventGetMouseWheelVelocityY(NativeEvent evt) /*-{
                                                                                     if (typeof evt.wheelDelta == "undefined") {
                                                                                     return 0;
                                                                                     }
                                                                                     return Math.round(evt.wheelDelta / 40) || 0;
                                                                                     }-*/;

    @Override
    public void onMouseOver(MouseOverEvent event) {
        boolean handle = true;
        if (mouseHandlerExtention != null)
            handle = !mouseHandlerExtention.onMouseOver(event);

        if (handle) {
        }

        if (mouseHandlerExtention != null)
            mouseHandlerExtention.postMouseOver(event);
    }

    @Override
    public final void onMouseOut(MouseOutEvent event) {
        boolean handle = true;
        if (mouseHandlerExtention != null)
            handle = !mouseHandlerExtention.onMouseOut(event);

        if (handle) {
        }

        isDragged = false;

        if (mouseHandlerExtention != null)
            mouseHandlerExtention.postMouseOut(event);

        //mouseDownPoint = null;
        //event.stopPropagation();
    }

    @Override
    public final void onMouseMove(MouseMoveEvent event) {
        Event e = DOM.eventGetCurrentEvent();
        e.preventDefault();

        boolean handle = true;
        if (mouseHandlerExtention != null)
            handle = !mouseHandlerExtention.onMouseMove(event);

        if (mouseDownPoint != null) {
            mouseMovePoint2 = mouseMovePoint1;
            mouseDownTime2 = mouseDownTime1;
            mouseMovePoint1 = new Point(event.getX(), event.getY());
            mouseDownTime1 = new Date().getTime();

            if (handle) {
                Point refPos = getScrollWidgetPos();

                int mouseDx = event.getClientX() - mouseDownPoint.x;
                int mouseDy = event.getClientY() - mouseDownPoint.y;

                int dx = (scrollWidgetBasePosition.x + mouseDx) - refPos.x;
                int dy = (scrollWidgetBasePosition.y + mouseDy) - refPos.y;

                moveScrollWidget(dx, dy);
            }

            event.stopPropagation();

            isDragged = isDragged
                    || dist(event.getClientX(), event.getClientY(), mouseDownPoint.x, mouseDownPoint.y) > 5.0;
        }

        if (mouseHandlerExtention != null)
            mouseHandlerExtention.postMouseMove(event);
    }

    private Point getScrollWidgetPos() {
        if (this.getWidgetCount() > 0)
            return new Point(this.getWidget(0).getElement().getOffsetLeft(),
                    this.getWidget(0).getElement().getOffsetTop());
        return new Point();
    }

    private void moveScrollWidget(double dx, double dy) {
        dx = confineHorizontally(dx);
        dy = confineVertically(dy);
        //Logger logger = Logger.getLogger("CrowdSourcing");
        //logger.log(Level.INFO, "autoScroll: "+dx+", "+dy); 
        if (this.getWidgetCount() > 0) {
            Widget widget = this.getWidget(0);
            if (widget != null) {
                int x = widget.getElement().getOffsetLeft() + (int) (dx + 0.5);
                int y = widget.getElement().getOffsetTop() + (int) (dy + 0.5);
                //this.setWidgetPosition(widget, x, y);
                widget.getElement().getStyle().setPosition(Position.ABSOLUTE);
                widget.getElement().getStyle().setLeft(x, Style.Unit.PX);
                widget.getElement().getStyle().setTop(y, Style.Unit.PX);
            }
        }
        notifyScrollPositionChanged();
    }

    private double confineHorizontally(double dx) {
        double confined = dx;
        if (this.getWidgetCount() <= 0)
            return confined;
        int panelWidth = this.getElement().getClientWidth();
        Widget refWidget = this.getWidget(0);
        int refWidgetWidth = refWidget.getElement().getClientWidth();

        //Maximum visible background around the reference widget
        int padding = Math.max(panelWidth - refWidgetWidth / 2, panelWidth / 2);

        double refLeft = refWidget.getElement().getOffsetLeft();

        //Widget moving to the right
        if (dx > 0) {
            //Check if out of bounds
            if (refLeft + dx > padding) {
                return padding - refLeft;
                //return Math.max(0.0, padding - refLeft);
            }
        }
        //Widget moving to the left
        else if (dx < 0) {
            double refRight = refLeft + refWidgetWidth;
            //Check if out of bounds
            if (refRight + dx < panelWidth - padding) {
                return panelWidth - padding - refRight;
                //return  Math.min(0.0, panelWidth-padding-refRight);
            }
        }
        return confined;
    }

    private double confineVertically(double dy) {
        double confined = dy;
        if (this.getWidgetCount() <= 0)
            return confined;
        int panelHeight = this.getElement().getClientHeight();
        Widget refWidget = this.getWidget(0);
        int refWidgetHeight = refWidget.getElement().getClientHeight();

        //Maximum visible background around the reference widget
        int padding = Math.max(panelHeight - refWidgetHeight / 2, panelHeight / 2);

        double refTop = refWidget.getElement().getOffsetTop();

        //Widget moving to the bottom
        if (dy > 0) {
            //Check if out of bounds
            if (refTop + dy > padding) {
                return padding - refTop;
                //return Math.max(0.0, padding - refTop);
            }
        }
        //Widget moving to the top
        else if (dy < 0) {
            double refBottom = refTop + refWidgetHeight;
            //Check if out of bounds
            if (refBottom + dy < panelHeight - padding) {
                return panelHeight - padding - refBottom;
                //return Math.min(0.0, panelHeight-padding - refBottom);
            }
        }
        return confined;
    }

    @Override
    public final void onMouseUp(MouseUpEvent event) {
        boolean handle = true;
        if (mouseHandlerExtention != null)
            handle = !mouseHandlerExtention.onMouseUp(event);

        if (handle) {
            //Calculate speed
            if (mouseMovePoint2 != null) {
                long now = new Date().getTime();
                int timePassed = (int) (now - mouseDownTime2);
                //double distance = dist(event.getX(), event.getY(), (int)mouseMovePoint2.getX(), (int)mouseMovePoint2.getY());
                double moveX = -mouseMovePoint2.x + event.getX();
                double moveY = -mouseMovePoint2.y + event.getY();
                scrollingSpeedX = 0.0;
                scrollingSpeedY = 0.0;
                if (timePassed > 0) {
                    scrollingSpeedX = moveX / (double) timePassed;
                    scrollingSpeedY = moveY / (double) timePassed;
                }
                if (Math.abs(scrollingSpeedX) > 0.0 || Math.abs(scrollingSpeedY) > 0.0) {
                    // Setup timer to refresh 
                    if (autoScrollTimer != null)
                        autoScrollTimer.cancel();
                    startAutoScroll(); //inertia mode
                }
            }
        }

        isDragged = false;

        mouseMovePoint1 = null;
        mouseMovePoint2 = null;
        mouseDownPoint = null;
        event.stopPropagation();
        DOM.releaseCapture(this.getElement());

        if (mouseHandlerExtention != null)
            mouseHandlerExtention.postMouseUp(event);
    }

    private void startAutoScroll() {
        final int refreshTimespan = 30;
        autoScrollTimer = new Timer() {
            @Override
            public void run() {
                synchronized (this) {
                    isAutoScrolling = true;
                    autoScroll(refreshTimespan, this);
                }
            }

            @Override
            public void cancel() {
                super.cancel();
                synchronized (this) {
                    if (isAutoScrolling) {
                        targetScrollX = Double.MIN_VALUE;
                        targetScrollY = Double.MIN_VALUE;
                        notifyAutoScrollFinished();
                    }
                    isAutoScrolling = false;
                }
            }
        };
        autoScrollTimer.scheduleRepeating(refreshTimespan);
    }

    public boolean isDragged() {
        return isDragged;
    }

    /**
     * Returns true if scrolling at the moment.
     */
    public boolean isAutoScrolling() {
        return isAutoScrolling;
    }

    private void autoScroll(int refreshTimespan, Timer refreshTimer) {

        //There are two different modes:
        //  1: Inertia scrolling: Slowing down after mouse drag
        //  2: Smooth moving to target position

        //Mode 1 (inertia)
        if (targetScrollX == Double.MIN_VALUE) {
            double dx = scrollingSpeedX * refreshTimespan;
            double dy = scrollingSpeedY * refreshTimespan;

            moveScrollWidget(dx, dy);

            //Slow down
            scrollingSpeedX /= scrollingDecelerationFactor;
            scrollingSpeedY /= scrollingDecelerationFactor;

            //Stop timer?
            if (Math.abs(scrollingSpeedX) < 0.0001 && Math.abs(scrollingSpeedY) < 0.0001) {
                refreshTimer.cancel();
            }
        }
        //Mode 2 (smooth moving to target)
        else {
            boolean reachedTargetX = false;
            boolean reachedTargetY = false;

            Point refPos = getScrollWidgetPos();

            double dx = targetScrollX - refPos.x;
            double dy = targetScrollY - refPos.y;

            //X
            if (Math.abs(dx) <= 4.0)
                reachedTargetX = true;
            //Y
            if (Math.abs(dy) <= 4.0)
                reachedTargetY = true;

            //Finished?
            if (reachedTargetX && reachedTargetY) {
                refreshTimer.cancel();
            } else {
                moveScrollWidget(dx / 3.0, dy / 3.0);
            }
        }
    }

    //Distance between two points
    private double dist(int x1, int y1, int x2, int y2) {
        return Math.sqrt(((double) ((x2 - x1) * (x2 - x1))) + ((double) ((y2 - y1) * (y2 - y1))));
    }

    @Override
    public void onMouseDown(MouseDownEvent event) {
        DOM.setCapture(this.getElement());
        Event e = DOM.eventGetCurrentEvent();
        e.preventDefault();

        boolean handle = true;
        if (mouseHandlerExtention != null)
            handle = !mouseHandlerExtention.onMouseDown(event);

        if (autoScrollTimer != null)
            autoScrollTimer.cancel();

        if (handle) {
            mouseDownPoint = new Point(event.getClientX(), event.getClientY());
            //Get widget positions
            scrollWidgetBasePosition = getScrollWidgetPos();
        }

        //for (int i=0; i<this.getWidgetCount(); i++) {
        //   Widget widget = this.getWidget(i);
        //   DOM.setIntStyleAttribute(widget.getElement(), "zIndex", i);
        //}

        event.stopPropagation();

        if (mouseHandlerExtention != null)
            mouseHandlerExtention.postMouseDown(event);
    }

    /**
     * Returns the view rectangle position in relation to the first child widget.
     */
    public Point getScrollPosition() {
        if (this.getWidgetCount() > 0) {
            Point refPos = getScrollWidgetPos();
            return new Point(-refPos.x, -refPos.y);
        }
        return new Point();
    }

    /**
     * Scrolls the view rectangle to a position relative to the first child widget.  
     */
    public void scrollToPosition(int x, int y) {
        if (autoScrollTimer != null)
            autoScrollTimer.cancel();

        if (this.getWidgetCount() > 0) {
            Widget widget = this.getWidget(0);
            int dx = -this.getWidgetLeft(widget) - x;
            int dy = -this.getWidgetTop(widget) - y;
            moveScrollWidget(dx, dy);
        }
    }

    /**
     * Scrolls relatively to the current position.
     */
    public void scroll(int dx, int dy) {
        scroll(dx, dy, false);
    }

    /**
     * Scrolls relatively to the current position.
     */
    public void scroll(int dx, int dy, boolean smooth) {
        dx = (int) confineHorizontally(dx);
        dy = (int) confineVertically(dy);

        if (autoScrollTimer != null)
            autoScrollTimer.cancel();

        if (smooth) { //Smooth auto scrolling
            Point refPos = getScrollWidgetPos();
            targetScrollX = refPos.x + dx;
            targetScrollY = refPos.y + dy;
            startAutoScroll();
        } else { //Scroll immediately
            moveScrollWidget(dx, dy);
        }
    }

    public void scrollToCenter() {
        if (autoScrollTimer != null)
            autoScrollTimer.cancel();

        if (this.getWidgetCount() > 0) {
            int clientWidth = this.getElement().getClientWidth();
            int clientHeight = this.getElement().getClientHeight();

            int widgetWidth = this.getWidget(0).getElement().getClientWidth();
            int widgetHeight = this.getWidget(0).getElement().getClientHeight();

            scrollToPosition(widgetWidth / 2 - clientWidth / 2, widgetHeight / 2 - clientHeight / 2);
        }
    }

    public void stopAutoScroll() {
        if (autoScrollTimer != null)
            autoScrollTimer.cancel();
    }

    @Override
    public HandlerRegistration addMouseDownHandler(MouseDownHandler handler) {
        return addDomHandler(handler, MouseDownEvent.getType());
    }

    @Override
    public HandlerRegistration addMouseUpHandler(MouseUpHandler handler) {
        return addDomHandler(handler, MouseUpEvent.getType());
    }

    @Override
    public HandlerRegistration addMouseOutHandler(MouseOutHandler handler) {
        return addDomHandler(handler, MouseOutEvent.getType());
    }

    @Override
    public HandlerRegistration addMouseOverHandler(MouseOverHandler handler) {
        return addDomHandler(handler, MouseOverEvent.getType());
    }

    @Override
    public HandlerRegistration addMouseMoveHandler(MouseMoveHandler handler) {
        return addDomHandler(handler, MouseMoveEvent.getType());
    }

    @Override
    public HandlerRegistration addMouseWheelHandler(MouseWheelHandler handler) {
        return addDomHandler(handler, MouseWheelEvent.getType());
    }

    private void notifyScrollPositionChanged() {
        for (Iterator<ScrollListener> it = scrollListeners.iterator(); it.hasNext();)
            it.next().scrollPositionChanged();
    }

    private void notifyAutoScrollFinished() {
        List<ScrollListener> toRemove = new ArrayList<ScrollListener>();
        for (Iterator<ScrollListener> it = scrollListeners.iterator(); it.hasNext();) {
            ScrollListener l = it.next();
            if (l.autoScrollingFinished())
                toRemove.add(l);
        }
        for (int i = 0; i < toRemove.size(); i++)
            scrollListeners.remove(toRemove.get(i));
    }

    public static interface ScrollListener {
        public void scrollPositionChanged();

        /**
         * Called when a smooth auto scrolling has finished.
         * @return Returning true removes the listener.
         */
        public boolean autoScrollingFinished();
    }

    /**
     * Mouse handling extension for MouseScrollPanel
     * 
     * @author Christian Clausner
     *
     */
    public static interface MouseHandlerExtension {
        /**
         * Mouse wheel handling
         * @return <code>true</code> if the event has been handled and the MouseScrollPanel should not handle it further.
         */
        boolean onMouseWheel(MouseWheelEvent event);

        /**
         * Mouse over handling
         * @return <code>true</code> if the event has been handled and the MouseScrollPanel should not handle it further.
         */
        boolean onMouseOver(MouseOverEvent event);

        /**
         * Mouse out handling
         * @return <code>true</code> if the event has been handled and the MouseScrollPanel should not handle it further.
         */
        boolean onMouseOut(MouseOutEvent event);

        /**
         * Mouse down handling
         * @return <code>true</code> if the event has been handled and the MouseScrollPanel should not handle it further.
         */
        boolean onMouseDown(MouseDownEvent event);

        /**
         * Mouse up handling
         * @return <code>true</code> if the event has been handled and the MouseScrollPanel should not handle it further.
         */
        boolean onMouseUp(MouseUpEvent event);

        /**
         * Mouse move handling
         * @return <code>true</code> if the event has been handled and the MouseScrollPanel should not handle it further.
         */
        boolean onMouseMove(MouseMoveEvent event);

        void postMouseWheel(MouseWheelEvent event);

        void postMouseOver(MouseOverEvent event);

        void postMouseOut(MouseOutEvent event);

        void postMouseDown(MouseDownEvent event);

        void postMouseUp(MouseUpEvent event);

        void postMouseMove(MouseMoveEvent event);
    }
}