1 /*
   2  * Copyright (c) 2008, 2015, Oracle and/or its affiliates.
   3  * All rights reserved. Use is subject to license terms.
   4  *
   5  * This file is available and licensed under the following license:
   6  *
   7  * Redistribution and use in source and binary forms, with or without
   8  * modification, are permitted provided that the following conditions
   9  * are met:
  10  *
  11  *  - Redistributions of source code must retain the above copyright
  12  *    notice, this list of conditions and the following disclaimer.
  13  *  - Redistributions in binary form must reproduce the above copyright
  14  *    notice, this list of conditions and the following disclaimer in
  15  *    the documentation and/or other materials provided with the distribution.
  16  *  - Neither the name of Oracle Corporation nor the names of its
  17  *    contributors may be used to endorse or promote products derived
  18  *    from this software without specific prior written permission.
  19  *
  20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31  */
  32 package ensemble;
  33 
  34 import javafx.animation.*;
  35 import javafx.beans.property.DoubleProperty;
  36 import javafx.beans.property.SimpleDoubleProperty;
  37 import javafx.event.Event;
  38 import javafx.event.EventHandler;
  39 import javafx.scene.Scene;
  40 import javafx.scene.input.MouseEvent;
  41 import javafx.scene.input.PickResult;
  42 import javafx.scene.input.ScrollEvent;
  43 import javafx.util.Duration;
  44 
  45 public class ScrollEventSynthesizer implements EventHandler {
  46     private static final int INERTIA_DURATION = 2400;
  47     private static final double CLICK_THRESHOLD = 20;
  48     private static final double CLICK_TIME_THRESHOLD = Integer.parseInt(System.getProperty("click", "400"));
  49     private long startDrag;
  50     private long lastDrag;
  51     private long lastDragDelta;
  52     private int startDragX;
  53     private int startDragY;
  54     private int lastDragX;
  55     private int lastDragY;
  56     private int lastDragStepX;
  57     private int lastDragStepY;
  58     private double dragVelocityX;
  59     private double dragVelocityY;
  60     private boolean clickThresholdBroken;
  61     private Timeline inertiaTimeline = null;
  62     private long lastClickTime = -1;
  63     private boolean isFiredByMe = false;
  64 
  65     public ScrollEventSynthesizer(Scene scene) {
  66         scene.addEventFilter(MouseEvent.ANY, this);
  67         scene.addEventFilter(ScrollEvent.ANY, this);
  68     }
  69 
  70     @Override public void handle(final Event e) {
  71         if (isFiredByMe) return;
  72         if (e instanceof ScrollEvent) {
  73             final ScrollEvent se = (ScrollEvent)e;
  74 //            System.out.println("SCROLL touch = "+se.getTouchCount()+" target = "+se.getTarget()+" e = "+se.getEventType()+"  dx="+se.getDeltaX()+"  dy="+se.getDeltaY()+"  tdx="+se.getTotalDeltaX()+"  tdy="+se.getTotalDeltaY());
  75             if (se.getTouchCount() != 0) se.consume();
  76         } else if (e instanceof MouseEvent) {
  77             final MouseEvent me = (MouseEvent)e;
  78 //            System.out.println("MOUSE "+e.getEventType()+" --> "+e.getTarget());
  79             if (e.getEventType() == MouseEvent.MOUSE_PRESSED) {
  80                 lastDragX = startDragX = (int)me.getX();
  81                 lastDragY = startDragY = (int)me.getY();
  82                 lastDrag = startDrag = System.currentTimeMillis();
  83                 lastDragDelta = 0;
  84                 if(inertiaTimeline != null) inertiaTimeline.stop();
  85                 clickThresholdBroken = false;
  86             } else if (e.getEventType() == MouseEvent.MOUSE_DRAGGED) {
  87                 // Delta of this drag vs. last drag location (or start)
  88                 lastDragStepX = (int)me.getX() - lastDragX;
  89                 lastDragStepY = (int)me.getY() - lastDragY;
  90 
  91                 // Duration of this drag step.
  92                 lastDragDelta = System.currentTimeMillis() - lastDrag;
  93 
  94                 // Velocity of last drag increment.
  95                 dragVelocityX = (double)lastDragStepX/(double)lastDragDelta;
  96                 dragVelocityY = (double)lastDragStepY/(double)lastDragDelta;
  97 
  98                 // Snapshot of this drag event.
  99                 lastDragX = (int)me.getX();
 100                 lastDragY = (int)me.getY();
 101                 lastDrag = System.currentTimeMillis();
 102 
 103                 // Calculate distance so far -- have we dragged enough to scroll?
 104                 final int dragX = (int)me.getX() - startDragX;
 105                 final int dragY = (int)me.getY() - startDragY;
 106                 double distance = Math.abs(Math.sqrt((dragX*dragX) + (dragY*dragY)));
 107 
 108                 int scrollDistX = lastDragStepX;
 109                 int scrollDistY = lastDragStepY;
 110                 if (!clickThresholdBroken && distance > CLICK_THRESHOLD) {
 111                     clickThresholdBroken = true;
 112                     scrollDistX = dragX;
 113                     scrollDistY = dragY;
 114                 }
 115                 if (clickThresholdBroken) {
 116                     Event.fireEvent(e.getTarget(), new ScrollEvent(
 117                             ScrollEvent.SCROLL,
 118                             me.getX(), me.getY(),
 119                             me.getSceneX(), me.getSceneY(),
 120                             me.isShiftDown(), me.isControlDown(), me.isAltDown(), me.isMetaDown(), true, false,
 121                             scrollDistX, scrollDistY,
 122                             scrollDistX, scrollDistY,
 123                             ScrollEvent.HorizontalTextScrollUnits.NONE, 0,
 124                             ScrollEvent.VerticalTextScrollUnits.NONE, 0,
 125                             0,new PickResult(me.getTarget(), me.getSceneX(), me.getSceneY())
 126                             ));
 127                     
 128                     /*
 129                      * final EventType<ScrollEvent> eventType,
 130             double x, double y,
 131             double screenX, double screenY,
 132             boolean shiftDown,
 133             boolean controlDown,
 134             boolean altDown,
 135             boolean metaDown,
 136             boolean direct,
 137             boolean inertia,
 138             double deltaX, double deltaY,
 139             double gestureDeltaX, double gestureDeltaY,
 140             HorizontalTextScrollUnits textDeltaXUnits, double textDeltaX,
 141             VerticalTextScrollUnits textDeltaYUnits, double textDeltaY,
 142             int touchCount,
 143             PickResult pickResult)
 144                      */
 145                 }
 146             } else if (e.getEventType() == MouseEvent.MOUSE_RELEASED) {
 147                 handleRelease(me);
 148             } else if (e.getEventType() == MouseEvent.MOUSE_CLICKED) {
 149                 final long time = System.currentTimeMillis();
 150 //                System.out.println("CLICKED   clickThresholdBroken="+clickThresholdBroken+"   timeSinceLast = "+ (time-lastClickTime)+" CONSUMED="+((time-lastClickTime) < CLICK_TIME_THRESHOLD));
 151                 if (clickThresholdBroken || (lastClickTime != -1 && (time-lastClickTime) < CLICK_TIME_THRESHOLD)) e.consume();
 152                 lastClickTime = time;
 153             }
 154         }
 155     }
 156 
 157     private void handleRelease(final MouseEvent me) {
 158         if (clickThresholdBroken) {
 159             // Calculate last instantaneous velocity. User may have stopped moving
 160             // before they let go of the mouse.
 161             final long time = System.currentTimeMillis() - lastDrag;
 162             dragVelocityX = (double)lastDragStepX/(time + lastDragDelta);
 163             dragVelocityY = (double)lastDragStepY/(time + lastDragDelta);
 164 
 165             // determin if click or drag/flick
 166             final int dragX = (int)me.getX() - startDragX;
 167             final int dragY = (int)me.getY() - startDragY;
 168 
 169             // calculate complete time from start to end of drag
 170             final long totalTime = System.currentTimeMillis() - startDrag;
 171 
 172             // if time is less than 300ms then considered a quick flick and whole time is used
 173             final boolean quick = totalTime < 300;
 174 
 175             // calculate velocity
 176             double velocityX = quick ? (double)dragX / (double)totalTime : dragVelocityX; // pixels/ms
 177             double velocityY = quick ? (double)dragY / (double)totalTime : dragVelocityY; // pixels/ms
 178 
 179 //            System.out.printf("dragVelocityX = %f, dragVelocityY = %f\n", dragVelocityX, dragVelocityY);
 180 //            System.out.printf("velocityX = %f, dragX = %d; velocityY = %f, dragY = %d; totalTime = %d\n",
 181 //                    velocityX, dragX, velocityY, dragY, totalTime);
 182 
 183             // calculate distance we would travel at this speed for INERTIA_DURATION ms of travel
 184             final int distanceX = (int)(velocityX * INERTIA_DURATION); // distance
 185             final int distanceY = (int)(velocityY * INERTIA_DURATION); // distance
 186             //
 187             DoubleProperty animatePosition = new SimpleDoubleProperty() {
 188                 double lastMouseX = me.getX();
 189                 double lastMouseY = me.getY();
 190                 @Override protected void invalidated() {
 191                     final double mouseX = me.getX() + (distanceX * get());
 192                     final double mouseY = me.getY() + (distanceY * get());
 193                     final double dragStepX = mouseX - lastMouseX;
 194                     final double dragStepY = mouseY - lastMouseY;
 195 
 196                     if (Math.abs(dragStepX) >= 1.0 || Math.abs(dragStepY) >= 1.0) {
 197                         Event.fireEvent(me.getTarget(), new ScrollEvent(
 198                                 ScrollEvent.SCROLL,
 199                                 me.getX(), me.getY(),
 200                                 me.getSceneX(), me.getSceneY(),
 201                                 me.isShiftDown(), me.isControlDown(), me.isAltDown(), me.isMetaDown(),
 202                                 true, true,
 203                                 dragStepX, dragStepY,
 204                                 (distanceX * get()), (distanceY * get()),
 205                                 ScrollEvent.HorizontalTextScrollUnits.NONE, 0,
 206                                 ScrollEvent.VerticalTextScrollUnits.NONE, 0,
 207                                 0,new PickResult(me.getTarget(), me.getSceneX(), me.getSceneY())
 208                                 ));
 209                     }
 210                     lastMouseX = mouseX;
 211                     lastMouseY = mouseY;
 212                 }
 213             };
 214 
 215             // animate a slow down from current velocity to zero
 216             inertiaTimeline = new Timeline(
 217                     new KeyFrame(Duration.ZERO, new KeyValue(animatePosition, 0)),
 218                     new KeyFrame(Duration.millis(INERTIA_DURATION), new KeyValue(animatePosition, 1d,
 219                             Interpolator.SPLINE(0.0513, 0.1131, 0.1368, 1.0000)))
 220             );
 221             inertiaTimeline.play();
 222         }
 223     }
 224 }