changeset 10045:d1e9b7548bd0

8143596: FXCanvas does not forward touch gestures to embedded scene Reviewed-by: azvegint, kcr
author anyssen
date Tue, 20 Sep 2016 14:23:04 +0300
parents d0e811d5d40e
children 6037afd08442
files modules/javafx.graphics/src/main/java/com/sun/javafx/embed/AbstractEvents.java modules/javafx.graphics/src/main/java/com/sun/javafx/embed/EmbeddedSceneInterface.java modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/EmbeddedScene.java modules/javafx.swing/src/main/java/javafx/embed/swing/JFXPanel.java modules/javafx.swt/src/main/java/javafx/embed/swt/FXCanvas.java modules/javafx.swt/src/main/java/javafx/embed/swt/SWTEvents.java tests/manual/swt/FXCanvasGestureEventsTest.java
diffstat 7 files changed, 554 insertions(+), 30 deletions(-) [+]
line wrap: on
line diff
--- a/modules/javafx.graphics/src/main/java/com/sun/javafx/embed/AbstractEvents.java	Mon Sep 19 15:05:02 2016 -0700
+++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/embed/AbstractEvents.java	Tue Sep 20 14:23:04 2016 +0300
@@ -29,10 +29,12 @@
 import javafx.scene.input.KeyEvent;
 import javafx.scene.input.MouseButton;
 import javafx.scene.input.MouseEvent;
+import javafx.scene.input.RotateEvent;
+import javafx.scene.input.ScrollEvent;
+import javafx.scene.input.SwipeEvent;
+import javafx.scene.input.ZoomEvent;
 
 import com.sun.javafx.tk.FocusCause;
-import javafx.scene.input.InputEvent;
-import javafx.scene.input.ScrollEvent;
 
 /**
  * An utility class to translate input events between embedded
@@ -60,6 +62,23 @@
     public final static int KEYEVENT_RELEASED = 1;
     public final static int KEYEVENT_TYPED = 2;
 
+    public final static int ZOOMEVENT_STARTED = 0;
+    public final static int ZOOMEVENT_ZOOM = 1;
+    public final static int ZOOMEVENT_FINISHED = 2;
+
+    public final static int ROTATEEVENT_STARTED = 0;
+    public final static int ROTATEEVENT_ROTATE = 1;
+    public final static int ROTATEEVENT_FINISHED = 2;
+
+    public final static int SCROLLEVENT_STARTED = 0;
+    public final static int SCROLLEVENT_SCROLL = 1;
+    public final static int SCROLLEVENT_FINISHED = 2;
+
+    public final static int SWIPEEVENT_DOWN = 0;
+    public final static int SWIPEEVENT_UP = 1;
+    public final static int SWIPEEVENT_LEFT = 2;
+    public final static int SWIPEEVENT_RIGHT = 3;
+
     public final static int FOCUSEVENT_ACTIVATED = 0;
     public final static int FOCUSEVENT_TRAVERSED_FORWARD = 1;
     public final static int FOCUSEVENT_TRAVERSED_BACKWARD = 2;
@@ -117,6 +136,62 @@
         return KeyEvent.KEY_TYPED;
     }
 
+    public static EventType<ZoomEvent> zoomIDToFXEventType(int zoomID) {
+        switch(zoomID) {
+            case ZOOMEVENT_STARTED:
+                return ZoomEvent.ZOOM_STARTED;
+            case ZOOMEVENT_ZOOM:
+                return ZoomEvent.ZOOM;
+            case ZOOMEVENT_FINISHED:
+                return ZoomEvent.ZOOM_FINISHED;
+        }
+        // Should never reach here
+        return ZoomEvent.ZOOM;
+    }
+
+    public static EventType<RotateEvent> rotateIDToFXEventType(int rotateID) {
+        switch(rotateID) {
+            case ROTATEEVENT_STARTED:
+                return RotateEvent.ROTATION_STARTED;
+            case ROTATEEVENT_ROTATE:
+                return RotateEvent.ROTATE;
+            case ROTATEEVENT_FINISHED:
+                return RotateEvent.ROTATION_FINISHED;
+        }
+        // Should never reach here
+        return RotateEvent.ROTATE;
+    }
+
+    public static EventType<SwipeEvent> swipeIDToFXEventType(int swipeID) {
+        switch(swipeID) {
+            case SWIPEEVENT_UP:
+                return SwipeEvent.SWIPE_UP;
+            case SWIPEEVENT_DOWN:
+                return SwipeEvent.SWIPE_DOWN;
+            case SWIPEEVENT_LEFT:
+                return SwipeEvent.SWIPE_LEFT;
+            case SWIPEEVENT_RIGHT:
+                return SwipeEvent.SWIPE_RIGHT;
+        }
+        // Should never reach here
+        return SwipeEvent.SWIPE_DOWN;
+    }
+
+    public static EventType<ScrollEvent> scrollIDToFXEventType(int scrollID) {
+        switch(scrollID) {
+            case SCROLLEVENT_STARTED:
+                return ScrollEvent.SCROLL_STARTED;
+            case SCROLLEVENT_FINISHED:
+                return ScrollEvent.SCROLL_FINISHED;
+            case MOUSEEVENT_VERTICAL_WHEEL:
+            case MOUSEEVENT_HORIZONTAL_WHEEL:
+            case SCROLLEVENT_SCROLL:
+                return ScrollEvent.SCROLL;
+        }
+        // Should never reach here
+        return ScrollEvent.SCROLL;
+    }
+
     public static FocusCause focusCauseToPeerFocusCause(int focusCause) {
         switch (focusCause) {
             case FOCUSEVENT_ACTIVATED:
--- a/modules/javafx.graphics/src/main/java/com/sun/javafx/embed/EmbeddedSceneInterface.java	Mon Sep 19 15:05:02 2016 -0700
+++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/embed/EmbeddedSceneInterface.java	Tue Sep 20 14:23:04 2016 +0300
@@ -76,15 +76,37 @@
      * A notification about mouse wheel scroll events received by the host container;
      */
     public void scrollEvent(int type, double scrollX, double scrollY,
+                            double totalScrollX, double totalScrollY,
+                            double xMultiplier, double yMultiplier,
                             double x, double y, double screenX, double screenY,
                             boolean shift, boolean ctrl,
-                            boolean alt, boolean meta);
+                            boolean alt, boolean meta, boolean inertia);
     /*
      * A notification about key event received by host container.
      */
     public void keyEvent(int type, int key, char[] chars, int modifiers);
 
     /*
+     * A notification about zoom events received by the host container.
+     */
+    public void zoomEvent(final int type, final double zoomFactor, final double totalZoomFactor,
+                          final double x, final double y, final double screenX, final double screenY,
+                          boolean shift, boolean ctrl, boolean alt, boolean meta, boolean inertia);
+
+    /*
+     * A notification about rotate events received by the host container.
+     */
+    public void rotateEvent(final int type, final double angle, final double totalAngle,
+                          final double x, final double y, final double screenX, final double screenY,
+                          boolean shift, boolean ctrl, boolean alt, boolean meta, boolean inertia);
+
+    /*
+     * A notification about swipe events received by the host container.
+     */
+    public void swipeEvent(final int type, final double x, final double y, final double screenX, final double screenY,
+                           boolean shift, boolean ctrl, boolean alt, boolean meta);
+
+    /*
      * A notification about menu event received by host container.
      */
     public void menuEvent(int x, int y, int xAbs, int yAbs, boolean isKeyboardTrigger);
--- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/EmbeddedScene.java	Mon Sep 19 15:05:02 2016 -0700
+++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/EmbeddedScene.java	Tue Sep 20 14:23:04 2016 +0300
@@ -294,19 +294,22 @@
     }
 
     @Override
-    public void scrollEvent(final int type, final double scrollX, final double scrollY,
+    public void scrollEvent(final int type,
+                            final double scrollX, final double scrollY,
+                            final double totalScrollX, final double totalScrollY,
+                            double xMultiplier, double yMultiplier,
                             final double x, final double y,
                             final double xAbs, final double yAbs,
-                            final boolean shift, final boolean ctrl, final boolean alt, final boolean meta) {
+                            final boolean shift, final boolean ctrl, final boolean alt, final boolean meta,
+                            final boolean inertia) {
         {
             Platform.runLater(() -> {
                 AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
                     if (sceneListener == null) {
                         return null;
                     }
-                    sceneListener.scrollEvent(ScrollEvent.SCROLL, scrollX, scrollY, 0, 0, 40.0, 40.0,
-                            0, 0, 0, 0, 0,
-                            x, y, xAbs, yAbs, shift, ctrl, alt, meta, false, false);
+                    sceneListener.scrollEvent(AbstractEvents.scrollIDToFXEventType(type), scrollX, scrollY, totalScrollX, totalScrollY, xMultiplier, yMultiplier,
+                            0, 0, 0, 0, 0, x, y, xAbs, yAbs, shift, ctrl, alt, meta, false, inertia);
                     return null;
                 }, getAccessControlContext());
             });
@@ -364,6 +367,61 @@
     }
 
     @Override
+    public void zoomEvent(final int type, final double zoomFactor, final double totalZoomFactor,
+                          final double x, final double y, final double screenX, final double screenY,
+                          boolean shift, boolean ctrl, boolean alt, boolean meta, boolean inertia)
+    {
+        Platform.runLater(() -> {
+            AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
+                if (sceneListener == null) {
+                    return null;
+                }
+                sceneListener.zoomEvent(AbstractEvents.zoomIDToFXEventType(type),
+                        zoomFactor, totalZoomFactor,
+                        x, y, screenX, screenY,
+                        shift, ctrl, alt, meta, false, inertia);
+                return null;
+            }, getAccessControlContext());
+        });
+    }
+
+    @Override
+    public void rotateEvent(final int type, final double angle, final double totalAngle,
+                          final double x, final double y, final double screenX, final double screenY,
+                          boolean shift, boolean ctrl, boolean alt, boolean meta, boolean inertia)
+    {
+        Platform.runLater(() -> {
+            AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
+                if (sceneListener == null) {
+                    return null;
+                }
+                sceneListener.rotateEvent(AbstractEvents.rotateIDToFXEventType(type),
+                        angle, totalAngle,
+                        x, y, screenX, screenY,
+                        shift, ctrl, alt, meta, false, inertia);
+                return null;
+            }, getAccessControlContext());
+        });
+    }
+
+    @Override
+    public void swipeEvent(final int type, final double x, final double y, final double screenX, final double screenY,
+                            boolean shift, boolean ctrl, boolean alt, boolean meta)
+    {
+        Platform.runLater(() -> {
+            AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
+                if (sceneListener == null) {
+                    return null;
+                }
+                sceneListener.swipeEvent(AbstractEvents.swipeIDToFXEventType(type),
+                        0, x, y, screenX, screenY,
+                        shift, ctrl, alt, meta, false);
+                return null;
+            }, getAccessControlContext());
+        });
+    }
+
+    @Override
     public void setCursor(final Object cursor) {
         super.setCursor(cursor);
         host.setCursor((CursorFrame) cursor);
--- a/modules/javafx.swing/src/main/java/javafx/embed/swing/JFXPanel.java	Mon Sep 19 15:05:02 2016 -0700
+++ b/modules/javafx.swing/src/main/java/javafx/embed/swing/JFXPanel.java	Tue Sep 20 14:23:04 2016 +0300
@@ -373,11 +373,14 @@
         if(e.getID() == MouseEvent.MOUSE_WHEEL) {
             scenePeer.scrollEvent(AbstractEvents.MOUSEEVENT_VERTICAL_WHEEL,
                     0, -SwingEvents.getWheelRotation(e),
-                    e.getX(), e.getY(), e.getXOnScreen(), e.getYOnScreen(),
+                    0, 0, // total scroll
+                    40, 40, // multiplier
+                    e.getX(), e.getY(),
+                    e.getXOnScreen(), e.getYOnScreen(),
                     (extModifiers & MouseEvent.SHIFT_DOWN_MASK) != 0,
                     (extModifiers & MouseEvent.CTRL_DOWN_MASK) != 0,
                     (extModifiers & MouseEvent.ALT_DOWN_MASK) != 0,
-                    (extModifiers & MouseEvent.META_DOWN_MASK) != 0);
+                    (extModifiers & MouseEvent.META_DOWN_MASK) != 0, false);
         } else {
             scenePeer.mouseEvent(
                     SwingEvents.mouseIDToEmbedMouseType(e.getID()),
--- a/modules/javafx.swt/src/main/java/javafx/embed/swt/FXCanvas.java	Mon Sep 19 15:05:02 2016 -0700
+++ b/modules/javafx.swt/src/main/java/javafx/embed/swt/FXCanvas.java	Tue Sep 20 14:23:04 2016 +0300
@@ -47,6 +47,7 @@
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.Stack;
 import java.util.concurrent.CountDownLatch;
 
 import javafx.application.Platform;
@@ -77,6 +78,7 @@
 import org.eclipse.swt.events.DisposeListener;
 import org.eclipse.swt.events.FocusEvent;
 import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.GestureEvent;
 import org.eclipse.swt.events.KeyEvent;
 import org.eclipse.swt.events.KeyListener;
 import org.eclipse.swt.events.MenuDetectEvent;
@@ -464,11 +466,19 @@
             }
         });
 
+        // SWT emulates mouse events from PAN gesture events.
+        // We need to suppress them while a gesture is active or inertia events are still processed.
         addListener(SWT.MouseVerticalWheel, e -> {
-            FXCanvas.this.sendScrollEventToFX(e, AbstractEvents.MOUSEEVENT_VERTICAL_WHEEL);
+            if (!gestureActive && (!panGestureInertiaActive || lastGestureEvent == null || e.time != lastGestureEvent.time)) {
+                FXCanvas.this.sendScrollEventToFX(AbstractEvents.MOUSEEVENT_VERTICAL_WHEEL,
+                        0, SWTEvents.getWheelRotation(e), e.x, e.y, e.stateMask, false);
+            }
         });
         addListener(SWT.MouseHorizontalWheel, e -> {
-            FXCanvas.this.sendScrollEventToFX(e, AbstractEvents.MOUSEEVENT_HORIZONTAL_WHEEL);
+            if (!gestureActive && (!panGestureInertiaActive || lastGestureEvent == null || e.time != lastGestureEvent.time)) {
+                FXCanvas.this.sendScrollEventToFX(AbstractEvents.MOUSEEVENT_HORIZONTAL_WHEEL,
+                        SWTEvents.getWheelRotation(e), 0, e.x, e.y, e.stateMask, false);
+            }
         });
 
         addMouseTrackListener(new MouseTrackListener() {
@@ -520,6 +530,10 @@
             }
         });
 
+        addGestureListener(ge -> {
+            FXCanvas.this.sendGestureEventToFX(ge);
+        });
+
         addMenuDetectListener(e -> {
             Runnable r = () -> {
                 if (isDisposed()) return;
@@ -680,20 +694,56 @@
                 false);  // RT-32990: popup trigger not implemented
     }
 
-    private void sendScrollEventToFX(Event event, int type){
+    double totalScrollX = 0;
+    double totalScrollY = 0;
+    private void sendScrollEventToFX(int type, double scrollX, double scrollY, int x, int y, int stateMask, boolean inertia) {
         if (scenePeer == null) {
             return;
         }
-        Point los = toDisplay(event.x, event.y);
+
+        double multiplier = 5.0;
+        if (type == AbstractEvents.MOUSEEVENT_HORIZONTAL_WHEEL  || type == AbstractEvents.MOUSEEVENT_VERTICAL_WHEEL) {
+            // granularity for mouse wheel scroll events is more coarse-grained than for pan gesture events
+            multiplier = 40.0;
+
+            // mouse wheel scroll events do not belong to a gesture,
+            // so total scroll is not accumulated
+            totalScrollX = scrollX;
+            totalScrollY = scrollY;
+        } else {
+            // up to and including SWT 4.5, direction was inverted for pan gestures on the Mac
+            // (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=481331)
+            if ("cocoa".equals(SWT.getPlatform()) && SWT.getVersion() < 4600) {
+                multiplier  *= -1.0;
+            }
+
+            if (type == AbstractEvents.SCROLLEVENT_STARTED) {
+                totalScrollX = 0;
+                totalScrollY = 0;
+            } else if (inertia) {
+                // inertia events do not belong to the gesture,
+                // thus total scroll is not accumulated
+                totalScrollX = scrollX;
+                totalScrollY = scrollY;
+            } else {
+                // accumulate total scroll as long as the gesture occurs
+                totalScrollX += scrollX;
+                totalScrollY += scrollY;
+            }
+        }
+
+        Point los = toDisplay(x, y);
         scenePeer.scrollEvent(type,
-                (type == AbstractEvents.MOUSEEVENT_HORIZONTAL_WHEEL) ? -SWTEvents.getWheelRotation(event) : 0,
-                (type == AbstractEvents.MOUSEEVENT_VERTICAL_WHEEL) ? -SWTEvents.getWheelRotation(event) : 0,
-                event.x, event.y,
+                scrollX, scrollY,
+                totalScrollX, totalScrollY,
+                multiplier, multiplier,
+                x, y,
                 los.x, los.y,
-                (event.stateMask & SWT.SHIFT) != 0,
-                (event.stateMask & SWT.CONTROL) != 0,
-                (event.stateMask & SWT.ALT) != 0,
-                (event.stateMask & SWT.COMMAND) != 0);
+                (stateMask & SWT.SHIFT) != 0,
+                (stateMask & SWT.CONTROL) != 0,
+                (stateMask & SWT.ALT) != 0,
+                (stateMask & SWT.COMMAND) != 0,
+                inertia);
     }
 
     private void sendKeyEventToFX(final KeyEvent e, int type) {
@@ -726,6 +776,191 @@
         }
     }
 
+    // true in between begin and end events of a (compound) gesture (not including inertia events)
+    private boolean gestureActive = false;
+    // true while inertia events of a pan gesture might be processed
+    private boolean panGestureInertiaActive = false;
+    // the last gesture event that was received (may also be an inertia event)
+    private GestureEvent lastGestureEvent;
+    // used to keep track of which (atomic) gestures are enclosed
+    private Stack<Integer> nestedGestures = new Stack<>();
+    // data used to compute inertia values for pan gesture events (as SWT does not provide these)
+    private long inertiaTime = 0;
+    private double inertiaXScroll = 0.0;
+    private double inertiaYScroll = 0.0;
+    private void sendGestureEventToFX(GestureEvent gestureEvent) {
+        if (scenePeer == null) {
+            return;
+        }
+
+        // An SWT gesture may be compound, comprising several MAGNIFY, PAN, and ROTATE events, which are enclosed by a
+        // generic BEGIN and END event (while SWIPE events occur without being enclosed).
+        // In JavaFX, such a compound gesture is represented through (possibly nested) atomic gestures, which all
+        // (again excluding swipe) have their specific START and FINISH events.
+        // While a complex SWT gesture is active, we therefore have to generate START events for atomic gestures as
+        // needed, finishing them all when the compound SWT gesture ends (in the reverse order they were started),
+        // after which we still process inertia events (that only seem to occur for PAN). SWIPE events may simply be
+        // forwarded.
+        switch (gestureEvent.detail) {
+            case SWT.GESTURE_BEGIN:
+                // a (complex) gesture has started
+                gestureActive = true;
+                // we are within an active gesture, so no inertia processing now
+                panGestureInertiaActive = false;
+                break;
+            case SWT.GESTURE_MAGNIFY:
+                // emulate the start of an atomic gesture
+                if (gestureActive && !nestedGestures.contains(SWT.GESTURE_MAGNIFY)) {
+                    sendZoomEventToFX(AbstractEvents.ZOOMEVENT_STARTED, gestureEvent);
+                    nestedGestures.push(SWT.GESTURE_MAGNIFY);
+                }
+                sendZoomEventToFX(AbstractEvents.ZOOMEVENT_ZOOM, gestureEvent);
+                break;
+            case SWT.GESTURE_PAN:
+                // emulate the start of an atomic gesture
+                if (gestureActive && !nestedGestures.contains(SWT.GESTURE_PAN)) {
+                    sendScrollEventToFX(AbstractEvents.SCROLLEVENT_STARTED, gestureEvent.xDirection, gestureEvent.yDirection,
+                            gestureEvent.x, gestureEvent.y, gestureEvent.stateMask, false);
+                    nestedGestures.push(SWT.GESTURE_PAN);
+                }
+
+                // SWT does not flag inertia events and does not allow to distinguish emulated PAN gesture events
+                // (resulting from mouse wheel interaction) from native ones (resulting from touch device interaction);
+                // as it will always send both, mouse wheel as well as PAN gesture events when using the touch device or
+                // the mouse wheel, we can identify native PAN gesture inertia events only based on their temporal relationship
+                // to the preceding gesture event.
+                if(panGestureInertiaActive && gestureEvent.time > lastGestureEvent.time + 250) {
+                    panGestureInertiaActive = false;
+                }
+
+                if(gestureActive || panGestureInertiaActive) {
+                    double xDirection = gestureEvent.xDirection;
+                    double yDirection = gestureEvent.yDirection;
+
+                    if (panGestureInertiaActive) {
+                        // calculate inertia values for scrollX and scrollY, as SWT (at least on MacOSX) provides zero values
+                        if (xDirection == 0 && yDirection == 0) {
+                            double delta = Math.max(0.0, Math.min(1.0, (gestureEvent.time - inertiaTime) / 1500.0));
+                            xDirection = (1.0 - delta) * inertiaXScroll;
+                            yDirection = (1.0 - delta) * inertiaYScroll;
+                        }
+                    }
+
+                    sendScrollEventToFX(AbstractEvents.SCROLLEVENT_SCROLL, xDirection, yDirection,
+                            gestureEvent.x, gestureEvent.y, gestureEvent.stateMask, panGestureInertiaActive);
+                }
+                break;
+            case SWT.GESTURE_ROTATE:
+                // emulate the start of an atomic gesture
+                if(gestureActive && !nestedGestures.contains(SWT.GESTURE_ROTATE)) {
+                    sendRotateEventToFX(AbstractEvents.ROTATEEVENT_STARTED, gestureEvent);
+                    nestedGestures.push(SWT.GESTURE_ROTATE);
+                }
+                sendRotateEventToFX(AbstractEvents.ROTATEEVENT_ROTATE, gestureEvent);
+                break;
+            case SWT.GESTURE_SWIPE:
+                sendSwipeEventToFX(gestureEvent);
+                break;
+            case SWT.GESTURE_END:
+                // finish atomic gesture(s) in reverse order of their start; SWIPE may be ignored,
+                // as JavaFX (like SWT) does not recognize it as a gesture
+                while (!nestedGestures.isEmpty()) {
+                    switch (nestedGestures.pop()) {
+                        case SWT.GESTURE_MAGNIFY:
+                            sendZoomEventToFX(AbstractEvents.ZOOMEVENT_FINISHED, gestureEvent);
+                            break;
+                        case SWT.GESTURE_PAN:
+                            sendScrollEventToFX(AbstractEvents.SCROLLEVENT_FINISHED, gestureEvent.xDirection, gestureEvent.yDirection,
+                                    gestureEvent.x, gestureEvent.y, gestureEvent.stateMask, false);
+                            // use the scroll values of the preceding scroll event to compute values for inertia events
+                            inertiaXScroll = lastGestureEvent.xDirection;
+                            inertiaYScroll = lastGestureEvent.yDirection;
+                            inertiaTime = gestureEvent.time;
+                            // from now on, inertia events may occur
+                            panGestureInertiaActive = true;
+                            break;
+                        case SWT.GESTURE_ROTATE:
+                            sendRotateEventToFX(AbstractEvents.ROTATEEVENT_FINISHED, gestureEvent);
+                            break;
+                    }
+                }
+                // compound SWT gesture has ended
+                gestureActive = false;
+                break;
+            default:
+                // ignore
+        }
+        // keep track of currently received gesture event; this is needed to identify inertia events
+        lastGestureEvent = gestureEvent;
+    }
+
+    // used to compute zoom deltas, which are not provided by SWT
+    private double lastTotalZoom = 0.0;
+    private void sendZoomEventToFX(int type, GestureEvent gestureEvent) {
+        Point los = toDisplay(gestureEvent.x, gestureEvent.y);
+
+        double totalZoom = gestureEvent.magnification;
+        if (type == AbstractEvents.ZOOMEVENT_STARTED) {
+            // ensure first event does not provide any zoom yet
+            totalZoom = lastTotalZoom = 1.0;
+        } else if (type == AbstractEvents.ZOOMEVENT_FINISHED) {
+            // SWT uses 0.0 for final event, while JavaFX still provides a (total) zoom value
+            totalZoom = lastTotalZoom;
+        }
+        double zoom = type == AbstractEvents.ZOOMEVENT_FINISHED ? 1.0 : totalZoom / lastTotalZoom;
+        lastTotalZoom = totalZoom;
+
+        scenePeer.zoomEvent(type, zoom, totalZoom,
+                gestureEvent.x, gestureEvent.y, los.x, los.y,
+                (gestureEvent.stateMask & SWT.SHIFT) != 0,
+                (gestureEvent.stateMask & SWT.CONTROL) != 0,
+                (gestureEvent.stateMask & SWT.ALT) != 0,
+                (gestureEvent.stateMask & SWT.COMMAND) != 0,
+                !gestureActive);
+    }
+
+    private double lastTotalAngle = 0.0;
+    private void sendRotateEventToFX(int type, GestureEvent gestureEvent) {
+        Point los = toDisplay(gestureEvent.x, gestureEvent.y);
+
+        double totalAngle = gestureEvent.rotation;
+        if (type == AbstractEvents.ROTATEEVENT_STARTED) {
+            totalAngle = lastTotalAngle = 0.0;
+        } else if (type == AbstractEvents.ROTATEEVENT_FINISHED) {
+            // SWT uses 0.0 for final event, while JavaFX still provides a (total) rotation value
+            totalAngle = lastTotalAngle;
+        }
+        double angle = type == AbstractEvents.ROTATEEVENT_FINISHED ? 0.0 : totalAngle - lastTotalAngle;
+        lastTotalAngle = totalAngle;
+
+        scenePeer.rotateEvent(type, angle, totalAngle,
+                gestureEvent.x, gestureEvent.y, los.x, los.y,
+                (gestureEvent.stateMask & SWT.SHIFT) != 0,
+                (gestureEvent.stateMask & SWT.CONTROL) != 0,
+                (gestureEvent.stateMask & SWT.ALT) != 0,
+                (gestureEvent.stateMask & SWT.COMMAND) != 0,
+                !gestureActive);
+    }
+
+    private void sendSwipeEventToFX(GestureEvent gestureEvent) {
+        Point los = toDisplay(gestureEvent.x, gestureEvent.y);
+        int type = -1;
+        if(gestureEvent.yDirection > 0) {
+            type = AbstractEvents.SWIPEEVENT_DOWN;
+        } else if(gestureEvent.yDirection < 0) {
+            type = AbstractEvents.SWIPEEVENT_UP;
+        } else if(gestureEvent.xDirection > 0) {
+            type = AbstractEvents.SWIPEEVENT_RIGHT;
+        } else if(gestureEvent.xDirection < 0) {
+            type = AbstractEvents.SWIPEEVENT_LEFT;
+        }
+        scenePeer.swipeEvent(type, gestureEvent.x, gestureEvent.y, los.x, los.y,
+                (gestureEvent.stateMask & SWT.SHIFT) != 0,
+                (gestureEvent.stateMask & SWT.CONTROL) != 0,
+                (gestureEvent.stateMask & SWT.ALT) != 0,
+                (gestureEvent.stateMask & SWT.COMMAND) != 0);
+    }
+
     private void sendMenuEventToFX(MenuDetectEvent me) {
         if (scenePeer == null /*|| !isFxEnabled()*/) {
             return;
--- a/modules/javafx.swt/src/main/java/javafx/embed/swt/SWTEvents.java	Mon Sep 19 15:05:02 2016 -0700
+++ b/modules/javafx.swt/src/main/java/javafx/embed/swt/SWTEvents.java	Tue Sep 20 14:23:04 2016 +0300
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2016, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -26,7 +26,7 @@
 package javafx.embed.swt;
 
 import org.eclipse.swt.SWT;
-import org.eclipse.swt.events.MouseEvent;
+//import org.eclipse.swt.events.MouseEvent;
 
 import java.lang.reflect.Method;
 
@@ -73,9 +73,9 @@
         return AbstractEvents.MOUSEEVENT_NONE_BUTTON;
     }
 
-    static int getWheelRotation(Event e) {
+    static double getWheelRotation(Event e) {
         int divisor = 1;
-        if ("win32".equals(SWT.getPlatform())) {
+        if ("win32".equals(SWT.getPlatform()) && e.type == SWT.MouseVerticalWheel) {
             int [] linesToScroll = new int [1];
             //OS.SystemParametersInfo (OS.SPI_GETWHEELSCROLLLINES, 0, linesToScroll, 0);
             try {
@@ -91,12 +91,13 @@
             if (linesToScroll [0] != -1 /*OS.WHEEL_PAGESCROLL*/) {
                 divisor = linesToScroll [0];
             }
-        } else {
-            if ("gtk".equals(SWT.getPlatform())) {
-                divisor = 3;
-            }
+        } else if ("gtk".equals(SWT.getPlatform())) {
+            divisor = 3;
         }
-        return -e.count / Math.max(1, divisor);
+        else if ("cocoa".equals(SWT.getPlatform())) {
+            divisor = Math.abs(e.count);
+        }
+        return e.count / (double) Math.max(1, divisor);
     }
 
     static int keyIDToEmbedKeyType(int id) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/manual/swt/FXCanvasGestureEventsTest.java	Tue Sep 20 14:23:04 2016 +0300
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import javafx.embed.swt.FXCanvas;
+import javafx.scene.Scene;
+import javafx.scene.control.TextArea;
+import javafx.scene.layout.AnchorPane;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+public class FXCanvasGestureEventsTest {
+
+    static final String instructions =
+            "This tests that SWT gesture events are properly transferred to JavaFX. " +
+                    "It passes if proper event sequences for ZOOM (Mac, Windows), ROTATE (Mac, Windows), SCROLL (Mac, Windows)" +
+                    "are printed to the console (SWT does not support any gestures on Linux yet, and SWIPE does not seem to be supported any more):\n\n " +
+                    " 1) Perform a simple ZOOM gesture and observe that a 'ZoomStarted (Zoom)+ ZoomFinished' event sequence is printed out. The finish event should provide a zoom of '1.0' and a totalZoom that corresponds to the product of the zoom values of all preceding events.\n\n" +
+                    " 2) Perform a simple ROTATE gesture and observe that a 'RotationStarted (Rotate)+ RotationFinished' event sequence is printed out. The finish event should provide an angle of '0.0' and a totalAngle that corresponds to the sum of the angle values of all preceding events. Note that with SWT < 3.8, rotate values will all be zero on MacOS 64-bit due to https://bugs.eclipse.org/bugs/show_bug.cgi?id=349812.\n\n" +
+                    " 3) Perform a complex ROTATE-ZOOM gesture (start with ROTATE then continue with ZOOM) and observe that a 'RotationStarted (Rotate)+ ZoomStarted (Zoom | Rotate)+ ZoomFinished (Rotate)* RotationFinished' event sequence is printed out.\n\n" +
+                    " 4) Perform a complex ZOOM-ROTATE gesture (start with ZOOM then continue with ROTATE) and observe that a 'ZoomStarted (Zoom)+ RotationStarted (Zoom | Rotate)+ RotationFinished (Zoom)* ZoomFinished' event sequence is printed out.\n\n" +
+                    " 5) Perform a simple vertical SCROLL gesture and observe that a 'ScrollStarted (Scroll)+ ScrollFinished (Scroll)*' event sequence is printed out. The finish event should provide a scrollY value of 0 and a totalScrollY value that corresponds to the sum of the scrollY events of all preceding events. The scroll events that occur after the finish event should have inertia set to true, while all others should have set inertia to false.\n\n" +
+                    " 6) Perform a simple horizontal SCROLL gesture and observe that a 'ScrollStarted (Scroll)+ ScrollFinished (Scroll)*' event sequence is printed out. The finish event should provide a scrollX value of 0 and a totalScrollX value that corresponds to the sum of the scrollX events of all preceding events. The scroll events that occur after the finish event should have inertia set to true, while all others should have set inertia to false.\n\n";
+
+    private static TextArea createInfo(String msg) {
+        TextArea t = new TextArea(msg);
+        t.setWrapText(true);
+        t.setEditable(false);
+        return t;
+    }
+
+    public static void main(String[] args) {
+        final Display display = new Display();
+        final Shell shell = new Shell(display);
+        shell.setText("FXCanvasGestureEventsTest");
+        shell.setSize(1000, 500);
+        shell.setLayout(new FillLayout());
+        final FXCanvas canvas = new FXCanvas(shell, SWT.NONE);
+        shell.open();
+
+        // create and hook scene
+        TextArea info = createInfo(instructions);
+        AnchorPane root = new AnchorPane();
+        root.getChildren().add(info);
+        AnchorPane.setBottomAnchor(info, 0d);
+        AnchorPane.setTopAnchor(info, 0d);
+        AnchorPane.setLeftAnchor(info, 0d);
+        AnchorPane.setRightAnchor(info, 0d);
+        final Scene scene = new Scene(root, 600, 400);
+        canvas.setScene(scene);
+
+        final int[] zoomEventCount = {0, 0, 0};
+        root.setOnZoomStarted(zoomEvent -> {
+            System.out.println("ZoomStarted event #" + zoomEventCount[0]++ + ": zoom: " + zoomEvent.getZoomFactor() + ", totalZoom: " + zoomEvent.getTotalZoomFactor());
+        });
+        root.setOnZoom(zoomEvent -> {
+            System.out.println("Zoom event #" + zoomEventCount[1]++ + ": zoom: " + zoomEvent.getZoomFactor() + ", totalZoom: " + zoomEvent.getTotalZoomFactor());
+        });
+        root.setOnZoomFinished(zoomEvent -> {
+            System.out.println("ZoomFinished event #" + zoomEventCount[2]++ + ": zoom: " + zoomEvent.getZoomFactor() + ", totalZoom: " + zoomEvent.getTotalZoomFactor());
+        });
+
+        final int[] scrollEventCount = {0, 0, 0};
+        root.setOnScrollStarted(scrollEvent -> {
+            System.out.println("ScrollStarted event #" + scrollEventCount[0]++ + ": scrollX: " + scrollEvent.getDeltaX() + ", scrollY: " + scrollEvent.getDeltaY() + ", totalScrollX: " + scrollEvent.getTotalDeltaX() + ", totalScrollY: " + scrollEvent.getTotalDeltaY());
+        });
+        root.setOnScroll(scrollEvent -> {
+            System.out.println("Scroll event #" + scrollEventCount[1]++ + ": scrollX: " + scrollEvent.getDeltaX() + ", scrollY: " + scrollEvent.getDeltaY() + ", totalScrollX: " + scrollEvent.getTotalDeltaX() + ", totalScrollY: " + scrollEvent.getTotalDeltaY() + ", inertia: " + scrollEvent.isInertia());
+        });
+        root.setOnScrollFinished(scrollEvent -> {
+            System.out.println("ScrollFinished event #" + scrollEventCount[2]++ + ": scrollX: " + scrollEvent.getDeltaX() + ", scrollY: " + scrollEvent.getDeltaY() + ", totalScrollX: " + scrollEvent.getTotalDeltaX() + ", totalScrollY: " + scrollEvent.getTotalDeltaY());
+        });
+
+        final int[] rotateEventCount = {0, 0, 0};
+        root.setOnRotationStarted(rotateEvent -> {
+            System.out.println("RotationStarted event #" + rotateEventCount[0]++ + ": angle: " + rotateEvent.getAngle() + ", totalAngle: " + rotateEvent.getTotalAngle());
+        });
+        root.setOnRotate(rotateEvent -> {
+            System.out.println("Rotate event #" + rotateEventCount[0]++ + ": angle: " + rotateEvent.getAngle() + ", totalAngle: " + rotateEvent.getTotalAngle());
+        });
+        root.setOnRotationFinished(rotateEvent -> {
+            System.out.println("RotationFinished event #" + rotateEventCount[0]++ + ": angle: " + rotateEvent.getAngle() + ", totalAngle: " + rotateEvent.getTotalAngle());
+        });
+
+        final int[] swipeEventCount = {0};
+        root.setOnSwipeDown(swipeEvent -> {
+            System.out.println("Swipe DOWN event #" + swipeEventCount[0]++);
+        });
+        root.setOnSwipeUp(swipeEvent -> {
+            System.out.println("Swipe UP event #" + swipeEventCount[0]++);
+        });
+        root.setOnSwipeLeft(swipeEvent -> {
+            System.out.println("Swipe LEFT event #" + swipeEventCount[0]++);
+        });
+        root.setOnSwipeRight(swipeEvent -> {
+            System.out.println("Swipe RIGHT event #" + swipeEventCount[0]++);
+        });
+
+        while (!shell.isDisposed()) {
+            // run SWT event loop
+            if (!display.readAndDispatch()) {
+                display.sleep();
+            }
+        }
+        display.dispose();
+    }
+}