changeset 6594:1cff0246b7b6

RT-35306: Add zoom and rotate gesture recognizers.
author Seeon Birger <seeon.birger@oracle.com>
date Wed, 02 Apr 2014 18:20:08 +0300
parents dbc816d19498
children db120a66633f
files buildSrc/armv6hf.gradle buildSrc/armv6sf.gradle modules/graphics/src/main/java/com/sun/javafx/tk/quantum/GlassViewEventHandler.java modules/graphics/src/main/java/com/sun/javafx/tk/quantum/RotateGestureRecognizer.java modules/graphics/src/main/java/com/sun/javafx/tk/quantum/ZoomGestureRecognizer.java
diffstat 5 files changed, 763 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- a/buildSrc/armv6hf.gradle	Wed Apr 02 14:49:25 2014 +0200
+++ b/buildSrc/armv6hf.gradle	Wed Apr 02 18:20:08 2014 +0300
@@ -220,6 +220,9 @@
 directfb.prism.order=sw
 directfb.com.sun.javafx.isEmbedded=true
 directfb.com.sun.javafx.scene.control.skin.FXVK.cache=true
+directfb.com.sun.javafx.gestures.zoom=true
+directfb.com.sun.javafx.gestures.rotate=true
+directfb.com.sun.javafx.gestures.scroll=true
 eglfb.com.sun.javafx.scene.control.skin.ListViewSkin.pannable=true
 eglfb.com.sun.javafx.scene.control.skin.TreeViewSkin.pannable=true
 eglfb.com.sun.javafx.scene.control.skin.TableViewSkin.pannable=true
@@ -237,6 +240,9 @@
 eglfb.doNativeComposite=true
 eglfb.com.sun.javafx.scene.control.skin.FXVK.cache=true
 eglfb.prism.glDepthSize=0
+eglfb.com.sun.javafx.gestures.zoom=true
+eglfb.com.sun.javafx.gestures.rotate=true
+eglfb.com.sun.javafx.gestures.scroll=true
 fb.com.sun.javafx.scene.control.skin.ListViewSkin.pannable=true
 fb.com.sun.javafx.scene.control.skin.TreeViewSkin.pannable=true
 fb.com.sun.javafx.scene.control.skin.TableViewSkin.pannable=true
@@ -246,6 +252,9 @@
 fb.com.sun.javafx.isEmbedded=true
 fb.glass.restrictWindowToScreen=true
 fb.com.sun.javafx.scene.control.skin.FXVK.cache=true
+fb.com.sun.javafx.gestures.zoom=true
+fb.com.sun.javafx.gestures.rotate=true
+fb.com.sun.javafx.gestures.scroll=true
 monocle.glass.platform=Monocle
 monocle.prism.order=es2,sw
 monocle.prism.eglfb=true
@@ -259,6 +268,9 @@
 monocle.doNativeComposite=true
 monocle.com.sun.javafx.scene.control.skin.FXVK.cache=true
 monocle.prism.glDepthSize=0
+monocle.com.sun.javafx.gestures.zoom=true
+monocle.com.sun.javafx.gestures.rotate=true
+monocle.com.sun.javafx.gestures.scroll=true
 eglx11.com.sun.javafx.scene.control.skin.ListViewSkin.pannable=true
 eglx11.com.sun.javafx.scene.control.skin.TreeViewSkin.pannable=true
 eglx11.com.sun.javafx.scene.control.skin.TableViewSkin.pannable=true
@@ -275,13 +287,19 @@
 eglx11.com.sun.javafx.isEmbedded=true
 eglx11.com.sun.javafx.scene.control.skin.FXVK.cache=true
 eglx11.prism.glDepthSize=0
+eglx11.com.sun.javafx.gestures.zoom=true
+eglx11.com.sun.javafx.gestures.rotate=true
+eglx11.com.sun.javafx.gestures.scroll=true
 gtk.com.sun.javafx.scene.control.skin.ListViewSkin.pannable=true
 gtk.com.sun.javafx.scene.control.skin.TreeViewSkin.pannable=true
 gtk.com.sun.javafx.scene.control.skin.TableViewSkin.pannable=true
 gtk.glass.platform=gtk
 gtk.prism.order=sw
 gtk.com.sun.javafx.isEmbedded=true
-gtk.com.sun.javafx.scene.control.skin.FXVK.cache=true"""
+gtk.com.sun.javafx.scene.control.skin.FXVK.cache=true
+gtk.com.sun.javafx.gestures.zoom=true
+gtk.com.sun.javafx.gestures.rotate=true
+gtk.com.sun.javafx.gestures.scroll=true"""
 
 def pangoCCFlags = [extraCFlags, "-D_ENABLE_PANGO"];
 def pangoLinkFlags = [extraLFlags];
--- a/buildSrc/armv6sf.gradle	Wed Apr 02 14:49:25 2014 +0200
+++ b/buildSrc/armv6sf.gradle	Wed Apr 02 18:20:08 2014 +0300
@@ -225,6 +225,9 @@
 directfb.prism.order=sw
 directfb.com.sun.javafx.isEmbedded=true
 directfb.com.sun.javafx.scene.control.skin.FXVK.cache=true
+directfb.com.sun.javafx.gestures.zoom=true
+directfb.com.sun.javafx.gestures.rotate=true
+directfb.com.sun.javafx.gestures.scroll=true
 eglfb.com.sun.javafx.scene.control.skin.ListViewSkin.pannable=true
 eglfb.com.sun.javafx.scene.control.skin.TreeViewSkin.pannable=true
 eglfb.com.sun.javafx.scene.control.skin.TableViewSkin.pannable=true
@@ -243,6 +246,9 @@
 eglfb.com.sun.javafx.isEmbedded=true
 eglfb.com.sun.javafx.scene.control.skin.FXVK.cache=true
 eglfb.prism.glDepthSize=0
+eglfb.com.sun.javafx.gestures.zoom=true
+eglfb.com.sun.javafx.gestures.rotate=true
+eglfb.com.sun.javafx.gestures.scroll=true
 fb.com.sun.javafx.scene.control.skin.ListViewSkin.pannable=true
 fb.com.sun.javafx.scene.control.skin.TreeViewSkin.pannable=true
 fb.com.sun.javafx.scene.control.skin.TableViewSkin.pannable=true
@@ -252,6 +258,9 @@
 fb.com.sun.javafx.isEmbedded=true
 fb.glass.restrictWindowToScreen=true
 fb.com.sun.javafx.scene.control.skin.FXVK.cache=true
+fb.com.sun.javafx.gestures.zoom=true
+fb.com.sun.javafx.gestures.rotate=true
+fb.com.sun.javafx.gestures.scroll=true
 monocle.glass.platform=Monocle
 monocle.prism.order=es2,sw
 monocle.prism.eglfb=true
@@ -265,6 +274,9 @@
 monocle.doNativeComposite=true
 monocle.com.sun.javafx.scene.control.skin.FXVK.cache=true
 monocle.prism.glDepthSize=0
+monocle.com.sun.javafx.gestures.zoom=true
+monocle.com.sun.javafx.gestures.rotate=true
+monocle.com.sun.javafx.gestures.scroll=true
 eglx11.com.sun.javafx.scene.control.skin.ListViewSkin.pannable=true
 eglx11.com.sun.javafx.scene.control.skin.TreeViewSkin.pannable=true
 eglx11.com.sun.javafx.scene.control.skin.TableViewSkin.pannable=true
@@ -281,13 +293,19 @@
 eglx11.com.sun.javafx.isEmbedded=true
 eglx11.com.sun.javafx.scene.control.skin.FXVK.cache=true
 eglx11.prism.glDepthSize=0
+eglx11.com.sun.javafx.gestures.zoom=true
+eglx11.com.sun.javafx.gestures.rotate=true
+eglx11.com.sun.javafx.gestures.scroll=true
 gtk.com.sun.javafx.scene.control.skin.ListViewSkin.pannable=true
 gtk.com.sun.javafx.scene.control.skin.TreeViewSkin.pannable=true
 gtk.com.sun.javafx.scene.control.skin.TableViewSkin.pannable=true
 gtk.glass.platform=gtk
 gtk.prism.order=sw
 gtk.com.sun.javafx.isEmbedded=true
-gtk.com.sun.javafx.scene.control.skin.FXVK.cache=true"""
+gtk.com.sun.javafx.scene.control.skin.FXVK.cache=true
+gtk.com.sun.javafx.gestures.zoom=true
+gtk.com.sun.javafx.gestures.rotate=true
+gtk.com.sun.javafx.gestures.scroll=true"""
 
 def pangoCCFlags = [extraCFlags, "-D_ENABLE_PANGO"];
 def pangoLinkFlags = [extraLFlags];
--- a/modules/graphics/src/main/java/com/sun/javafx/tk/quantum/GlassViewEventHandler.java	Wed Apr 02 14:49:25 2014 +0200
+++ b/modules/graphics/src/main/java/com/sun/javafx/tk/quantum/GlassViewEventHandler.java	Wed Apr 02 18:20:08 2014 +0300
@@ -63,6 +63,18 @@
 
 class GlassViewEventHandler extends View.EventHandler {
 
+    static boolean zoomGestureEnabled;
+    static boolean rotateGestureEnabled;
+    static {
+        AccessController.doPrivileged(new PrivilegedAction<Void>() {
+            @Override public Void run() {
+                zoomGestureEnabled = Boolean.valueOf(System.getProperty("com.sun.javafx.gestures.zoom", "false"));
+                rotateGestureEnabled = Boolean.valueOf(System.getProperty("com.sun.javafx.gestures.rotate", "false"));
+                return null;
+            }
+        });
+    }    
+
     private ViewScene scene;
     private final GlassSceneDnDEventHandler dndHandler;
     private final GestureRecognizers gestures;
@@ -76,6 +88,12 @@
         if (PlatformUtil.isWindows() || PlatformUtil.isIOS() || PlatformUtil.isEmbedded()) {
             gestures.add(new SwipeGestureRecognizer(scene));
         }
+        if (zoomGestureEnabled) {
+            gestures.add(new ZoomGestureRecognizer(scene));
+        }
+        if (rotateGestureEnabled) {
+            gestures.add(new RotateGestureRecognizer(scene));
+        }
     }
 
     // Default fullscreen allows limited keyboard input.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/graphics/src/main/java/com/sun/javafx/tk/quantum/RotateGestureRecognizer.java	Wed Apr 02 18:20:08 2014 +0300
@@ -0,0 +1,381 @@
+/*
+ * Copyright (c) 2014, 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.
+ */
+
+package com.sun.javafx.tk.quantum;
+
+import com.sun.glass.events.KeyEvent;
+import com.sun.glass.events.TouchEvent;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import javafx.event.EventType;
+import javafx.scene.input.RotateEvent;
+
+class RotateGestureRecognizer implements GestureRecognizer {
+    private ViewScene scene;
+
+    // gesture will be activated if |rotation| > ROTATATION_THRESHOLD
+    private static double ROTATATION_THRESHOLD = 5; //in degrees
+    static {
+        AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
+            String s = System.getProperty("com.sun.javafx.gestures.rotate.threshold");
+            if (s != null) {
+                ROTATATION_THRESHOLD = Double.valueOf(s);
+            }
+            return null;
+        });
+    }    
+
+    private RotateRecognitionState state = RotateRecognitionState.IDLE;
+
+    // from MultiTouchTracker
+    Map<Long, TouchPointTracker> trackers =
+            new HashMap<Long, TouchPointTracker>();
+
+    int modifiers;
+    boolean direct;
+
+    //private int touchCount;
+    private int currentTouchCount = 0;
+    private boolean touchPointsSetChanged;
+    int touchPointsInEvent;
+    long touchPointID1 = -1;
+    long touchPointID2 = -1;
+    double centerX, centerY;
+    double centerAbsX, centerAbsY;
+
+    double currentRotation;
+    double angleReference;
+    double totalRotation = 0;
+    
+    RotateGestureRecognizer(final ViewScene scene) {
+        this.scene = scene;
+    }
+
+    @Override
+    public void notifyBeginTouchEvent(long time, int modifiers, boolean isDirect,
+            int touchEventCount) {
+        params(modifiers, isDirect);
+        touchPointsSetChanged = false;
+        touchPointsInEvent = 0;
+    }
+
+    @Override
+    public void notifyNextTouchEvent(long time, int type, long touchId,
+                                     int x, int y, int xAbs, int yAbs) {
+        touchPointsInEvent++;
+        switch(type) {
+            case TouchEvent.TOUCH_PRESSED:
+                touchPointsSetChanged = true;
+                touchPressed(touchId, time, x, y, xAbs, yAbs);
+                break;
+            case TouchEvent.TOUCH_STILL:
+                break;
+            case TouchEvent.TOUCH_MOVED:
+                touchMoved(touchId, time, x, y, xAbs, yAbs);
+                break;
+            case TouchEvent.TOUCH_RELEASED:
+                touchPointsSetChanged = true;
+                touchReleased(touchId, time, x, y, xAbs, yAbs);
+                break;
+            default:
+                throw new RuntimeException("Error in Rotate gesture recognition: "
+                        + "unknown touch state: " + state);
+        }
+    }
+
+    private void calculateCenter() {
+        if (currentTouchCount <= 0) {
+            throw new RuntimeException("Error in Rotate gesture recognition: "
+                    + "touch count is zero!");
+        }
+        double totalX = 0.0;
+        double totalY = 0.0;
+        double totalAbsX = 0.0;
+        double totalAbsY = 0.0;
+        for (TouchPointTracker tracker : trackers.values()) {
+            totalX += tracker.getX();
+            totalY += tracker.getY();
+            totalAbsX += tracker.getAbsX();
+            totalAbsY += tracker.getAbsY();
+        }      
+        centerX = totalX / currentTouchCount;
+        centerY = totalY / currentTouchCount;
+        centerAbsX = totalAbsX / currentTouchCount;
+        centerAbsY = totalAbsY / currentTouchCount;
+    }
+
+    private double getAngle(TouchPointTracker tp1, TouchPointTracker tp2) {
+        double dx = tp2.getAbsX() - tp1.getAbsX();
+        double dy = -(tp2.getAbsY() - tp1.getAbsY()); //standard math y-axis direction (increases upwards) is opposite to touchscreen y-axis direction (increases downwards)
+        double newAngle = Math.toDegrees(Math.atan2(dy, dx)); // Result in range [-180,+180]
+        return newAngle;
+    }
+
+    //oldAngle, newAngle expected to be in rangle [-180,+180]
+    private double getNormalizedDelta(double oldAngle, double newAngle) {
+        // while the input angles reflect the normal polar angle (angle increase with anti-clockwise rotation)
+        //  for rotation events positive values are used for clockwise rotation, therefore the negation.
+        double delta = -(newAngle - oldAngle);
+
+        
+        //delta now in [-360,+360]. Normalize to [-180,+180]
+        if (delta > 180) {
+            delta -= 360;
+        } else if (delta < -180) {
+            delta += 360;
+        }
+        return delta;
+    }
+
+    private void assignActiveTouchpoints() {
+        boolean needToReassign = false;
+        if (!trackers.containsKey(touchPointID1)) {
+            touchPointID1 = -1;
+            needToReassign = true;
+        }
+        if (!trackers.containsKey(touchPointID2)) {
+            touchPointID2 = -1;
+            needToReassign = true;
+        }
+
+        if (needToReassign) {
+            for (Long id : trackers.keySet()) {
+                if (id == touchPointID1 || id == touchPointID2) {
+                    //already used, skip
+                } else {
+                    if (touchPointID1 == -1) {
+                        // assign to first touch point
+                        touchPointID1 = id;
+                    } else if (touchPointID2 == -1) {
+                        // assign to second touch point
+                        touchPointID2 = id;
+                    } else {
+                        // 2 touch points assigned
+                        break;
+                    }
+                }
+            }
+        }
+    }
+    
+    @Override
+    public void notifyEndTouchEvent(long time) {
+        if (currentTouchCount != trackers.size()) {
+            throw new RuntimeException("Error in Rotate gesture recognition: "
+                    + "touch count is wrong: " + currentTouchCount);
+        }
+
+        if (currentTouchCount < 2) {
+            if (state == RotateRecognitionState.ACTIVE) {
+                sendRotateFinishedEvent();
+            }
+            reset();
+        } else {
+            // currentTouchCount >= 2
+            if (state == RotateRecognitionState.IDLE) {
+                state = RotateRecognitionState.TRACKING;
+                assignActiveTouchpoints();
+            }
+            
+            calculateCenter();
+
+            if (touchPointsSetChanged) {
+                assignActiveTouchpoints();
+            }
+            TouchPointTracker tp1 = trackers.get(touchPointID1);
+            TouchPointTracker tp2 = trackers.get(touchPointID2);
+            double newAngle = getAngle(tp1, tp2);
+                
+            if (touchPointsSetChanged) {
+                //No rotate event, just update the current angle. Keep total rotation
+                angleReference = newAngle;
+            } else {
+                currentRotation = getNormalizedDelta(angleReference, newAngle);
+                if (state == RotateRecognitionState.TRACKING) {
+                    if (Math.abs(currentRotation) > ROTATATION_THRESHOLD) {
+                        state = RotateRecognitionState.ACTIVE;
+                        sendRotateStartedEvent();
+                    }
+                }
+                
+                if (state == RotateRecognitionState.ACTIVE) {
+                    totalRotation += currentRotation;
+                    sendRotateEvent();
+                    angleReference = newAngle;
+                }
+            }
+        }
+    }
+
+    private void sendRotateStartedEvent() {
+        AccessController.doPrivileged(new PrivilegedAction<Void>() {
+            @Override
+            public Void run() {
+                if (scene.sceneListener != null) {
+                    scene.sceneListener.rotateEvent(RotateEvent.ROTATION_STARTED,
+                        0, 0,
+                        centerX, centerY,
+                        centerAbsX, centerAbsY,
+                        (modifiers & KeyEvent.MODIFIER_SHIFT) != 0,
+                        (modifiers & KeyEvent.MODIFIER_CONTROL) != 0,
+                        (modifiers & KeyEvent.MODIFIER_ALT) != 0,
+                        (modifiers & KeyEvent.MODIFIER_WINDOWS) != 0,
+                        direct, 
+                        false /*inertia*/);
+                }
+                return null;
+            }
+        }, scene.getAccessControlContext());
+    }
+
+    private void sendRotateEvent() {
+        AccessController.doPrivileged(new PrivilegedAction<Void>() {
+            @Override
+            public Void run() {
+                if (scene.sceneListener != null) {
+                    scene.sceneListener.rotateEvent(RotateEvent.ROTATE,
+                        currentRotation, totalRotation,
+                        centerX, centerY,
+                        centerAbsX, centerAbsY,
+                        (modifiers & KeyEvent.MODIFIER_SHIFT) != 0,
+                        (modifiers & KeyEvent.MODIFIER_CONTROL) != 0,
+                        (modifiers & KeyEvent.MODIFIER_ALT) != 0,
+                        (modifiers & KeyEvent.MODIFIER_WINDOWS) != 0,
+                        direct, false /*inertia*/);
+                }
+                return null;
+            }
+        }, scene.getAccessControlContext());
+    }
+
+    private void sendRotateFinishedEvent() {
+        AccessController.doPrivileged(new PrivilegedAction<Void>() {
+            @Override
+            public Void run() {
+                if (scene.sceneListener != null) {
+                    scene.sceneListener.rotateEvent(RotateEvent.ROTATION_FINISHED,
+                        0, totalRotation,
+                        centerX, centerY,
+                        centerAbsX, centerAbsY,
+                        (modifiers & KeyEvent.MODIFIER_SHIFT) != 0,
+                        (modifiers & KeyEvent.MODIFIER_CONTROL) != 0,
+                        (modifiers & KeyEvent.MODIFIER_ALT) != 0,
+                        (modifiers & KeyEvent.MODIFIER_WINDOWS) != 0,
+                        direct, 
+                        false /*inertia*/);
+                }
+                return null;
+            }
+        }, scene.getAccessControlContext());
+    }
+
+    public void params(int modifiers, boolean direct) {
+        this.modifiers = modifiers;
+        this.direct = direct;
+    }
+
+    public void touchPressed(long id, long nanos, int x, int y, int xAbs, int yAbs) {
+        currentTouchCount++;
+        TouchPointTracker tracker = new TouchPointTracker();
+        tracker.update(nanos, x, y, xAbs, yAbs);
+        trackers.put(id, tracker);
+    }
+
+    public void touchReleased(long id, long nanos, int x, int y, int xAbs, int yAbs) {
+        if (state != RotateRecognitionState.FAILURE) {
+            TouchPointTracker tracker = trackers.get(id);
+            if (tracker == null) {
+                // we don't know this ID, something went completely wrong
+                state = RotateRecognitionState.FAILURE;
+                throw new RuntimeException("Error in Rotate gesture "
+                        + "recognition: released unknown touch point");
+            }
+            trackers.remove(id);
+        }
+        currentTouchCount--;
+    }
+
+    public void touchMoved(long id, long nanos, int x, int y, int xAbs, int yAbs) {
+        if (state == RotateRecognitionState.FAILURE) {
+            return;
+        }
+
+        TouchPointTracker tracker = trackers.get(id);
+        if (tracker == null) {
+            // we don't know this ID, something went completely wrong
+            state = RotateRecognitionState.FAILURE;
+            throw new RuntimeException("Error in rotate gesture "
+                    + "recognition: reported unknown touch point");
+        }
+        tracker.update(nanos, x, y, xAbs, yAbs);
+    }
+
+    void reset() {
+        state = RotateRecognitionState.IDLE;
+        touchPointID1 = -1;
+        touchPointID2 = -1;
+        currentRotation = 0;
+        totalRotation = 0;
+    }
+
+    private static class TouchPointTracker {
+        double x, y;
+        double absX, absY;
+
+        public void update(long nanos, double x, double y, double absX, double absY) {
+            this.x = x;
+            this.y = y;
+            this.absX = absX;
+            this.absY = absY;
+        }
+
+        public double getX() {
+            return x;
+        }
+
+        public double getY() {
+            return y;
+        }
+
+        public double getAbsX() {
+            return absX;
+        }
+
+        public double getAbsY() {
+            return absY;
+        }
+    }
+
+    private enum RotateRecognitionState {
+        IDLE,       // <2 touch points available
+        TRACKING,   // 2+ touch points, angle is tracked
+        ACTIVE,     // threshold accepted, gesture is started
+        FAILURE
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/graphics/src/main/java/com/sun/javafx/tk/quantum/ZoomGestureRecognizer.java	Wed Apr 02 18:20:08 2014 +0300
@@ -0,0 +1,326 @@
+/*
+ * Copyright (c) 2014, 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.
+ */
+
+package com.sun.javafx.tk.quantum;
+
+import com.sun.glass.events.KeyEvent;
+import com.sun.glass.events.TouchEvent;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.HashMap;
+import java.util.Map;
+import javafx.event.EventType;
+import javafx.scene.input.ZoomEvent;
+
+class ZoomGestureRecognizer implements GestureRecognizer {
+    // gesture will be activated if |zoomFactor - 1| > ZOOM_FACTOR_THRESHOLD
+    private static double ZOOM_FACTOR_THRESHOLD = 0.1;
+    static {
+        AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
+            String s = System.getProperty("com.sun.javafx.gestures.zoom.threshold");
+            if (s != null) {
+                ZOOM_FACTOR_THRESHOLD = Double.valueOf(s);
+            }
+            return null;
+        });
+    }    
+
+    private ViewScene scene;
+
+    private ZoomRecognitionState state = ZoomRecognitionState.IDLE;
+
+    private Map<Long, TouchPointTracker> trackers =
+            new HashMap<Long, TouchPointTracker>();
+
+    private int modifiers;
+    private boolean direct;
+
+    private int currentTouchCount = 0;
+    private boolean touchPointsSetChanged;
+    
+    private double centerX, centerY;
+    private double centerAbsX, centerAbsY;
+    private double currentDistance;
+    private double distanceReference;
+    private double zoomFactor = 1.0;
+    private double totalZoomFactor = 1.0;
+    
+    ZoomGestureRecognizer(final ViewScene scene) {
+        this.scene = scene;
+    }
+
+    @Override
+    public void notifyBeginTouchEvent(long time, int modifiers, boolean isDirect,
+            int touchEventCount) {
+        params(modifiers, isDirect);
+        touchPointsSetChanged = false;
+    }
+
+    @Override
+    public void notifyNextTouchEvent(long time, int type, long touchId,
+                                     int x, int y, int xAbs, int yAbs) {
+        switch(type) {
+            case TouchEvent.TOUCH_PRESSED:
+                touchPointsSetChanged = true;
+                touchPressed(touchId, time, x, y, xAbs, yAbs);
+                break;
+            case TouchEvent.TOUCH_STILL:
+                break;
+            case TouchEvent.TOUCH_MOVED:
+                touchMoved(touchId, time, x, y, xAbs, yAbs);
+                break;
+            case TouchEvent.TOUCH_RELEASED:
+                touchPointsSetChanged = true;
+                touchReleased(touchId, time, x, y, xAbs, yAbs);
+                break;
+            default:
+                throw new RuntimeException("Error in Zoom gesture recognition: "
+                        + "unknown touch state: " + state);
+        }
+    }
+
+    private void calculateCenter() {
+        if (currentTouchCount <= 0) {
+            throw new RuntimeException("Error in Zoom gesture recognition: "
+                    + "touch count is zero!");
+        }
+        double totalX = 0.0;
+        double totalY = 0.0;
+        double totalAbsX = 0.0;
+        double totalAbsY = 0.0;
+        for (TouchPointTracker tracker : trackers.values()) {
+            totalX += tracker.getX();
+            totalY += tracker.getY();
+            totalAbsX += tracker.getAbsX();
+            totalAbsY += tracker.getAbsY();
+        }      
+        centerX = totalX / currentTouchCount;
+        centerY = totalY / currentTouchCount;
+        centerAbsX = totalAbsX / currentTouchCount;
+        centerAbsY = totalAbsY / currentTouchCount;
+    }
+
+    private double calculateMaxDistance() {
+        //calculate max square distance from a touch point to the center
+        double maxSquareDist = 0.0;
+        for (TouchPointTracker tracker : trackers.values()) {
+            double deltaX = tracker.getAbsX() - centerAbsX;
+            double deltaY = tracker.getAbsY() - centerAbsY;
+            
+            double squareDist = deltaX * deltaX + deltaY * deltaY;
+            if (squareDist > maxSquareDist) {
+                maxSquareDist = squareDist;
+            }
+        }      
+        return Math.sqrt(maxSquareDist);
+    }
+    
+    @Override
+    public void notifyEndTouchEvent(long time) {
+        if (currentTouchCount != trackers.size()) {
+            throw new RuntimeException("Error in Zoom gesture recognition: "
+                    + "touch count is wrong: " + currentTouchCount);
+        }
+
+        if (currentTouchCount < 2) {
+            if (state == ZoomRecognitionState.ACTIVE) {
+                sendZoomFinishedEvent();
+            }
+            reset();
+        } else {
+            // currentTouchCount >= 2
+            if (state == ZoomRecognitionState.IDLE) {
+                state = ZoomRecognitionState.TRACKING;
+            }
+            
+            calculateCenter();
+            double currentDistance = calculateMaxDistance();
+                
+            if (touchPointsSetChanged) {
+                //No zoom event. 
+                //Just update the distance reference. Keep the total zoomfactor
+                distanceReference = currentDistance;
+            } else {
+                zoomFactor = currentDistance / distanceReference;
+                if (state == ZoomRecognitionState.TRACKING) {
+                    if ( Math.abs(zoomFactor - 1) > ZOOM_FACTOR_THRESHOLD) {
+                        state = ZoomRecognitionState.ACTIVE;
+                        sendZoomStartedEvent();
+                    }
+                }
+                if (state == ZoomRecognitionState.ACTIVE) {
+                    totalZoomFactor *= zoomFactor;
+                    sendZoomEvent();
+                    distanceReference = currentDistance;
+                }
+            }
+        }
+    }
+    
+    private void sendZoomStartedEvent() {
+        AccessController.doPrivileged(new PrivilegedAction<Void>() {
+            @Override
+            public Void run() {
+                if (scene.sceneListener != null) {
+                    scene.sceneListener.zoomEvent(ZoomEvent.ZOOM_STARTED,
+                        1, 1,
+                        centerX, centerY,
+                        centerAbsX, centerAbsY,
+                        (modifiers & KeyEvent.MODIFIER_SHIFT) != 0,
+                        (modifiers & KeyEvent.MODIFIER_CONTROL) != 0,
+                        (modifiers & KeyEvent.MODIFIER_ALT) != 0,
+                        (modifiers & KeyEvent.MODIFIER_WINDOWS) != 0,
+                        direct, 
+                        false /*inertia*/);
+                }
+                return null;
+            }
+        }, scene.getAccessControlContext());
+    }
+
+    private void sendZoomEvent() {
+        AccessController.doPrivileged(new PrivilegedAction<Void>() {
+            @Override
+            public Void run() {
+                if (scene.sceneListener != null) {
+                    scene.sceneListener.zoomEvent(ZoomEvent.ZOOM,
+                        zoomFactor, totalZoomFactor,
+                        centerX, centerY,
+                        centerAbsX, centerAbsY,
+                        (modifiers & KeyEvent.MODIFIER_SHIFT) != 0,
+                        (modifiers & KeyEvent.MODIFIER_CONTROL) != 0,
+                        (modifiers & KeyEvent.MODIFIER_ALT) != 0,
+                        (modifiers & KeyEvent.MODIFIER_WINDOWS) != 0,
+                        direct, false /*inertia*/);
+                }
+                return null;
+            }
+        }, scene.getAccessControlContext());
+    }
+
+    private void sendZoomFinishedEvent() {
+        AccessController.doPrivileged(new PrivilegedAction<Void>() {
+            @Override
+            public Void run() {
+                if (scene.sceneListener != null) {
+                    scene.sceneListener.zoomEvent(ZoomEvent.ZOOM_FINISHED,
+                        1, totalZoomFactor,
+                        centerX, centerY,
+                        centerAbsX, centerAbsY,
+                        (modifiers & KeyEvent.MODIFIER_SHIFT) != 0,
+                        (modifiers & KeyEvent.MODIFIER_CONTROL) != 0,
+                        (modifiers & KeyEvent.MODIFIER_ALT) != 0,
+                        (modifiers & KeyEvent.MODIFIER_WINDOWS) != 0,
+                        direct, 
+                        false /*inertia*/);
+                }
+                return null;
+            }
+        }, scene.getAccessControlContext());
+    }
+
+    public void params(int modifiers, boolean direct) {
+        this.modifiers = modifiers;
+        this.direct = direct;
+    }
+
+    public void touchPressed(long id, long nanos, int x, int y, int xAbs, int yAbs) {
+        currentTouchCount++;
+        TouchPointTracker tracker = new TouchPointTracker();
+        tracker.update(nanos, x, y, xAbs, yAbs);
+        trackers.put(id, tracker);
+    }
+
+    public void touchReleased(long id, long nanos, int x, int y, int xAbs, int yAbs) {
+        if (state != ZoomRecognitionState.FAILURE) {
+            TouchPointTracker tracker = trackers.get(id);
+            if (tracker == null) {
+                // we don't know this ID, something went completely wrong
+                state = ZoomRecognitionState.FAILURE;
+                throw new RuntimeException("Error in Zoom gesture "
+                        + "recognition: released unknown touch point");
+            }
+            trackers.remove(id);
+        }
+        currentTouchCount--;
+    }
+
+    public void touchMoved(long id, long nanos, int x, int y, int xAbs, int yAbs) {
+        if (state == ZoomRecognitionState.FAILURE) {
+            return;
+        }
+
+        TouchPointTracker tracker = trackers.get(id);
+        if (tracker == null) {
+            // we don't know this ID, something went completely wrong
+            state = ZoomRecognitionState.FAILURE;
+            throw new RuntimeException("Error in zoom gesture "
+                    + "recognition: reported unknown touch point");
+        }
+        tracker.update(nanos, x, y, xAbs, yAbs);
+    }
+
+    void reset() {
+        state = ZoomRecognitionState.IDLE;
+        zoomFactor = 1.0;
+        totalZoomFactor = 1.0;
+    }
+
+    private static class TouchPointTracker {
+        double x, y;
+        double absX, absY;
+
+        public void update(long nanos, double x, double y, double absX, double absY) {
+            this.x = x;
+            this.y = y;
+            this.absX = absX;
+            this.absY = absY;
+        }
+
+        public double getX() {
+            return x;
+        }
+
+        public double getY() {
+            return y;
+        }
+
+        public double getAbsX() {
+            return absX;
+        }
+
+        public double getAbsY() {
+            return absY;
+        }
+    }
+
+    private enum ZoomRecognitionState {
+        IDLE,       // <2 touch points available
+        TRACKING,   // 2+ touch points, distance is tracked
+        ACTIVE,     // threshold accepted, gesture is started
+        FAILURE
+    }
+}