diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GestureRecognizer.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GestureRecognizer.java index 869ad553507..4fef2f9c659 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GestureRecognizer.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GestureRecognizer.java @@ -25,5 +25,9 @@ package com.sun.javafx.tk.quantum; +import java.util.concurrent.TimeUnit; + interface GestureRecognizer extends GlassTouchEventListener { + static final long INITIAL_VELOCITY_THRESHOLD_NANOS = 100L * 1000; + static final double NANOS_TO_SECONDS = 1.0 / TimeUnit.SECONDS.toNanos(1); } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassViewEventHandler.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassViewEventHandler.java index d948fa24a72..06bcbf96e3e 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassViewEventHandler.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassViewEventHandler.java @@ -78,12 +78,23 @@ class GlassViewEventHandler extends View.EventHandler { }); } - private ViewScene scene; + private final ViewScene scene; + private final PrivilegedSceneListenerAccessor privilegedSceneListenerAccessor; private final GlassSceneDnDEventHandler dndHandler; private final GestureRecognizers gestures; + @SuppressWarnings("removal") public GlassViewEventHandler(final ViewScene scene) { this.scene = scene; + this.privilegedSceneListenerAccessor = consumer -> AccessController.doPrivileged( + (PrivilegedAction) () -> { + if (scene.sceneListener != null) { + consumer.accept(scene.sceneListener); + } + return null; + }, + scene.getAccessControlContext() + ); dndHandler = new GlassSceneDnDEventHandler(scene); @@ -95,7 +106,7 @@ public GlassViewEventHandler(final ViewScene scene) { gestures.add(new ZoomGestureRecognizer(scene)); } if (rotateGestureEnabled) { - gestures.add(new RotateGestureRecognizer(scene)); + gestures.add(new RotateGestureRecognizer(privilegedSceneListenerAccessor)); } if (scrollGestureEnabled) { gestures.add(new ScrollGestureRecognizer(scene)); diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/PrivilegedSceneListenerAccessor.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/PrivilegedSceneListenerAccessor.java new file mode 100644 index 00000000000..c4029e6610e --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/PrivilegedSceneListenerAccessor.java @@ -0,0 +1,13 @@ +package com.sun.javafx.tk.quantum; + +import java.util.function.Consumer; + +import com.sun.javafx.tk.TKSceneListener; + +/** + * Interface that allows access to {@link TKSceneListener} when it is + * not {@code null}. + */ +public interface PrivilegedSceneListenerAccessor { + void withSceneListener(Consumer consumer); +} diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/RotateGestureRecognizer.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/RotateGestureRecognizer.java index e390869b729..b1395f35e31 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/RotateGestureRecognizer.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/RotateGestureRecognizer.java @@ -41,8 +41,8 @@ import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; -class RotateGestureRecognizer implements GestureRecognizer { - private ViewScene scene; +public class RotateGestureRecognizer implements GestureRecognizer { + private PrivilegedSceneListenerAccessor accessor; // gesture will be activated if |rotation| > ROTATATION_THRESHOLD private static double ROTATATION_THRESHOLD = 5; //in degrees @@ -92,8 +92,8 @@ class RotateGestureRecognizer implements GestureRecognizer { double totalRotation = 0; double inertiaLastTime = 0; - RotateGestureRecognizer(final ViewScene scene) { - this.scene = scene; + public RotateGestureRecognizer(PrivilegedSceneListenerAccessor accessor) { + this.accessor = accessor; inertiaRotationVelocity.addListener(valueModel -> { double currentTime = inertiaTimeline.getCurrentTime().toSeconds(); double timePassed = currentTime - inertiaLastTime; @@ -314,60 +314,51 @@ else if (initialInertiaRotationVelocity < -MAX_INITIAL_VELOCITY) } } - @SuppressWarnings("removal") private void sendRotateStartedEvent() { - AccessController.doPrivileged((PrivilegedAction) () -> { - 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()); + accessor.withSceneListener(sceneListener -> { + 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 + ); + }); } - @SuppressWarnings("removal") private void sendRotateEvent(boolean isInertia) { - AccessController.doPrivileged((PrivilegedAction) () -> { - 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, isInertia); - } - return null; - }, scene.getAccessControlContext()); + accessor.withSceneListener(sceneListener -> { + 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, isInertia + ); + }); } - @SuppressWarnings("removal") private void sendRotateFinishedEvent() { - AccessController.doPrivileged((PrivilegedAction) () -> { - 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()); + accessor.withSceneListener(sceneListener -> { + 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 + ); + }); } public void params(int modifiers, boolean direct) { diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ScrollGestureRecognizer.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ScrollGestureRecognizer.java index 8830ca10432..ba745440cb3 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ScrollGestureRecognizer.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ScrollGestureRecognizer.java @@ -25,58 +25,61 @@ 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.util.Duration; -import javafx.scene.input.ScrollEvent; +import java.util.concurrent.TimeUnit; + +import com.sun.glass.events.KeyEvent; +import com.sun.glass.events.TouchEvent; + import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; +import javafx.scene.input.ScrollEvent; +import javafx.util.Duration; class ScrollGestureRecognizer implements GestureRecognizer { - // gesture will be activated if |scroll amount| > SCROLL_THRESHOLD - private static double SCROLL_THRESHOLD = 10; //in pixels - private static boolean SCROLL_INERTIA_ENABLED = true; - private static double MAX_INITIAL_VELOCITY = 1000; - private static double SCROLL_INERTIA_MILLIS = 1500; + private static final double MAX_INITIAL_VELOCITY = 1000; + private static final double SCROLL_INERTIA_MILLIS = 1500; + private static final long SCROLL_INERTIA_THRESHOLD_NANOS = TimeUnit.MILLISECONDS.toNanos(300); + + // gesture will be activated if |scroll amount| > scrollThreshold + private static double scrollThreshold = 10; //in pixels + private static boolean scrollInertiaEnabled = true; + static { @SuppressWarnings("removal") var dummy = AccessController.doPrivileged((PrivilegedAction) () -> { String s = System.getProperty("com.sun.javafx.gestures.scroll.threshold"); if (s != null) { - SCROLL_THRESHOLD = Double.valueOf(s); + scrollThreshold = Double.valueOf(s); } s = System.getProperty("com.sun.javafx.gestures.scroll.inertia"); if (s != null) { - SCROLL_INERTIA_ENABLED = Boolean.valueOf(s); + scrollInertiaEnabled = Boolean.valueOf(s); } return null; }); } - private ViewScene scene; + private final Timeline inertiaTimeline = new Timeline(); + private final DoubleProperty inertiaScrollVelocity = new SimpleDoubleProperty(); + private final Map trackers = new HashMap<>(); + private ViewScene scene; private ScrollRecognitionState state = ScrollRecognitionState.IDLE; - private Timeline inertiaTimeline = new Timeline(); - private DoubleProperty inertiaScrollVelocity = new SimpleDoubleProperty(); - private double initialInertiaScrollVelocity = 0; - private double scrollStartTime = 0; - private double lastTouchEventTime = 0; - - private Map trackers = new HashMap<>(); + private double initialInertiaScrollVelocity; + private long scrollStartNanos; private int modifiers; private boolean direct; - private int currentTouchCount = 0; + private int currentTouchCount; private int lastTouchCount; private boolean touchPointsSetChanged; private boolean touchPointsPressed; @@ -88,7 +91,7 @@ class ScrollGestureRecognizer implements GestureRecognizer { private double deltaX, deltaY; private double totalDeltaX, totalDeltaY; private double factorX, factorY; - double inertiaLastTime = 0; + private double inertiaLastTime; ScrollGestureRecognizer(final ViewScene scene) { this.scene = scene; @@ -123,16 +126,16 @@ public void notifyNextTouchEvent(long time, int type, long touchId, case TouchEvent.TOUCH_PRESSED: touchPointsSetChanged = true; touchPointsPressed = true; - touchPressed(touchId, time, x, y, xAbs, yAbs); + touchPressed(touchId, x, y, xAbs, yAbs); break; case TouchEvent.TOUCH_STILL: break; case TouchEvent.TOUCH_MOVED: - touchMoved(touchId, time, x, y, xAbs, yAbs); + touchMoved(touchId, x, y, xAbs, yAbs); break; case TouchEvent.TOUCH_RELEASED: touchPointsSetChanged = true; - touchReleased(touchId, time, x, y, xAbs, yAbs); + touchReleased(touchId); break; default: throw new RuntimeException("Error in Scroll gesture recognition: " @@ -162,8 +165,7 @@ private void calculateCenter() { } @Override - public void notifyEndTouchEvent(long time) { - lastTouchEventTime = time; + public void notifyEndTouchEvent(long nanos) { if (currentTouchCount != trackers.size()) { throw new RuntimeException("Error in Scroll gesture recognition: " + "touch count is wrong: " + currentTouchCount); @@ -173,9 +175,10 @@ public void notifyEndTouchEvent(long time) { if (state == ScrollRecognitionState.ACTIVE) { sendScrollFinishedEvent(lastCenterAbsX, lastCenterAbsY, lastTouchCount); - if (SCROLL_INERTIA_ENABLED) { - double timeFromLastScroll = ((double)time - scrollStartTime) / 1000000; - if (timeFromLastScroll < 300) { + if (scrollInertiaEnabled) { + double nanosSinceLastScroll = nanos - scrollStartNanos; + + if (nanosSinceLastScroll < SCROLL_INERTIA_THRESHOLD_NANOS) { state = ScrollRecognitionState.INERTIA; // activate inertia inertiaLastTime = 0; @@ -188,10 +191,7 @@ public void notifyEndTouchEvent(long time) { new KeyValue(inertiaScrollVelocity, initialInertiaScrollVelocity, Interpolator.LINEAR)), new KeyFrame( Duration.millis(SCROLL_INERTIA_MILLIS * Math.abs(initialInertiaScrollVelocity) / MAX_INITIAL_VELOCITY), - event -> { - //stop inertia - reset(); - }, + event -> reset(), // stop inertia new KeyValue(inertiaScrollVelocity, 0, Interpolator.LINEAR)) ); inertiaTimeline.playFromStart(); @@ -232,7 +232,7 @@ public void notifyEndTouchEvent(long time) { deltaX = centerAbsX - lastCenterAbsX; deltaY = centerAbsY - lastCenterAbsY; if (state == ScrollRecognitionState.TRACKING) { - if ( Math.abs(deltaX) > SCROLL_THRESHOLD || Math.abs(deltaY) > SCROLL_THRESHOLD) { + if ( Math.abs(deltaX) > scrollThreshold || Math.abs(deltaY) > scrollThreshold) { state = ScrollRecognitionState.ACTIVE; sendScrollStartedEvent(centerAbsX, centerAbsY, currentTouchCount); } @@ -242,15 +242,16 @@ public void notifyEndTouchEvent(long time) { totalDeltaY += deltaY; sendScrollEvent(false, centerAbsX, centerAbsY, currentTouchCount); - double timePassed = ((double)time - scrollStartTime) / 1000000000; - if (timePassed > 1e-4) { + long nanosPassed = nanos - scrollStartNanos; + + if (nanosPassed > INITIAL_VELOCITY_THRESHOLD_NANOS) { //capture radius (pytaguras) or init to variables x,y ??? double scrollMagnitude = Math.sqrt(deltaX * deltaX + deltaY * deltaY); factorX = deltaX / scrollMagnitude; factorY = deltaY / scrollMagnitude; - initialInertiaScrollVelocity = scrollMagnitude / timePassed; + initialInertiaScrollVelocity = scrollMagnitude / nanosPassed * NANOS_TO_SECONDS; - scrollStartTime = time; + scrollStartNanos = nanos; } lastCenterAbsX = centerAbsX; @@ -326,19 +327,19 @@ private void sendScrollFinishedEvent(double xAbs, double yAbs, int touchCount) { }, scene.getAccessControlContext()); } - public void params(int modifiers, boolean direct) { + private 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) { + private void touchPressed(long id, int x, int y, int xAbs, int yAbs) { currentTouchCount++; TouchPointTracker tracker = new TouchPointTracker(); - tracker.update(nanos, x, y, xAbs, yAbs); + tracker.update(x, y, xAbs, yAbs); trackers.put(id, tracker); } - public void touchReleased(long id, long nanos, int x, int y, int xAbs, int yAbs) { + private void touchReleased(long id) { if (state != ScrollRecognitionState.FAILURE) { TouchPointTracker tracker = trackers.get(id); if (tracker == null) { @@ -352,7 +353,7 @@ public void touchReleased(long id, long nanos, int x, int y, int xAbs, int yAbs) currentTouchCount--; } - public void touchMoved(long id, long nanos, int x, int y, int xAbs, int yAbs) { + private void touchMoved(long id, int x, int y, int xAbs, int yAbs) { if (state == ScrollRecognitionState.FAILURE) { return; } @@ -364,10 +365,10 @@ public void touchMoved(long id, long nanos, int x, int y, int xAbs, int yAbs) { throw new RuntimeException("Error in scroll gesture " + "recognition: reported unknown touch point"); } - tracker.update(nanos, x, y, xAbs, yAbs); + tracker.update(x, y, xAbs, yAbs); } - void reset() { + private void reset() { state = ScrollRecognitionState.IDLE; totalDeltaX = 0.0; totalDeltaY = 0.0; @@ -377,7 +378,7 @@ private static class TouchPointTracker { double x, y; double absX, absY; - public void update(long nanos, double x, double y, double absX, double absY) { + public void update(double x, double y, double absX, double absY) { this.x = x; this.y = y; this.absX = absX; diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ZoomGestureRecognizer.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ZoomGestureRecognizer.java index 9496c8f6253..befbb410049 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ZoomGestureRecognizer.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ZoomGestureRecognizer.java @@ -25,58 +25,58 @@ 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.util.Duration; -import javafx.scene.input.ZoomEvent; +import java.util.concurrent.TimeUnit; + +import com.sun.glass.events.KeyEvent; +import com.sun.glass.events.TouchEvent; + import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; +import javafx.scene.input.ZoomEvent; +import javafx.util.Duration; class ZoomGestureRecognizer implements GestureRecognizer { - // gesture will be activated if |zoomFactor - 1| > ZOOM_FACTOR_THRESHOLD - private static double ZOOM_FACTOR_THRESHOLD = 0.1; - private static boolean ZOOM_INERTIA_ENABLED = true; - private static double MAX_ZOOMIN_VELOCITY = 3.0; - private static double MAX_ZOOMOUT_VELOCITY = 0.3333; - private static double ZOOM_INERTIA_MILLIS = 500; - private static double MAX_ZOOM_IN_FACTOR = 10; - private static double MAX_ZOOM_OUT_FACTOR = 0.1; + private static final double ZOOM_INERTIA_MILLIS = 500; + private static final double MAX_ZOOM_IN_FACTOR = 10; + private static final double MAX_ZOOM_OUT_FACTOR = 0.1; + private static final long ZOOM_INERTIA_THRESHOLD_NANOS = TimeUnit.MILLISECONDS.toNanos(200); + + // gesture will be activated if |zoomFactor - 1| > zoomFactorThreshold + private static double zoomFactorThreshold = 0.1; + private static boolean zoomInertiaEnabled = true; static { @SuppressWarnings("removal") var dummy = AccessController.doPrivileged((PrivilegedAction) () -> { String s = System.getProperty("com.sun.javafx.gestures.zoom.threshold"); if (s != null) { - ZOOM_FACTOR_THRESHOLD = Double.valueOf(s); + zoomFactorThreshold = Double.valueOf(s); } s = System.getProperty("com.sun.javafx.gestures.zoom.inertia"); if (s != null) { - ZOOM_INERTIA_ENABLED = Boolean.valueOf(s); + zoomInertiaEnabled = Boolean.valueOf(s); } return null; }); } + private final Timeline inertiaTimeline = new Timeline(); + private final DoubleProperty inertiaZoomVelocity = new SimpleDoubleProperty(); + private final Map trackers = new HashMap<>(); + private ViewScene scene; - private Timeline inertiaTimeline = new Timeline(); - private DoubleProperty inertiaZoomVelocity = new SimpleDoubleProperty(); - private double initialInertiaZoomVelocity = 0; - private double zoomStartTime = 0; - private double lastTouchEventTime = 0; + private double initialInertiaZoomVelocity; + private long zoomStartNanos; private ZoomRecognitionState state = ZoomRecognitionState.IDLE; - - private Map trackers = new HashMap<>(); - private int modifiers; private boolean direct; @@ -86,11 +86,10 @@ class ZoomGestureRecognizer implements GestureRecognizer { private double centerX, centerY; private double centerAbsX, centerAbsY; - private double currentDistance; private double distanceReference; private double zoomFactor = 1.0; private double totalZoomFactor = 1.0; - double inertiaLastTime = 0; + private double inertiaLastTime; ZoomGestureRecognizer(final ViewScene scene) { this.scene = scene; @@ -122,16 +121,16 @@ public void notifyNextTouchEvent(long time, int type, long touchId, case TouchEvent.TOUCH_PRESSED: touchPointsSetChanged = true; touchPointsPressed = true; - touchPressed(touchId, time, x, y, xAbs, yAbs); + touchPressed(touchId, x, y, xAbs, yAbs); break; case TouchEvent.TOUCH_STILL: break; case TouchEvent.TOUCH_MOVED: - touchMoved(touchId, time, x, y, xAbs, yAbs); + touchMoved(touchId, x, y, xAbs, yAbs); break; case TouchEvent.TOUCH_RELEASED: touchPointsSetChanged = true; - touchReleased(touchId, time, x, y, xAbs, yAbs); + touchReleased(touchId); break; default: throw new RuntimeException("Error in Zoom gesture recognition: " @@ -176,8 +175,7 @@ private double calculateMaxDistance() { } @Override - public void notifyEndTouchEvent(long time) { - lastTouchEventTime = time; + public void notifyEndTouchEvent(long nanos) { if (currentTouchCount != trackers.size()) { throw new RuntimeException("Error in Zoom gesture recognition: " + "touch count is wrong: " + currentTouchCount); @@ -187,9 +185,10 @@ public void notifyEndTouchEvent(long time) { if (state == ZoomRecognitionState.ACTIVE) { sendZoomFinishedEvent(); } - if (ZOOM_INERTIA_ENABLED && (state == ZoomRecognitionState.PRE_INERTIA || state == ZoomRecognitionState.ACTIVE)) { - double timeFromLastZoom = ((double)time - zoomStartTime) / 1000000; - if (initialInertiaZoomVelocity != 0 && timeFromLastZoom < 200) { + if (zoomInertiaEnabled && (state == ZoomRecognitionState.PRE_INERTIA || state == ZoomRecognitionState.ACTIVE)) { + long nanosSinceLastZoom = nanos - zoomStartNanos; + + if (initialInertiaZoomVelocity != 0 && nanosSinceLastZoom < ZOOM_INERTIA_THRESHOLD_NANOS) { state = ZoomRecognitionState.INERTIA; // activate inertia inertiaLastTime = 0; @@ -214,12 +213,8 @@ public void notifyEndTouchEvent(long time) { Duration.millis(0), new KeyValue(inertiaZoomVelocity, initialInertiaZoomVelocity, Interpolator.LINEAR)), new KeyFrame( - //Duration.millis(ZOOM_INERTIA_MILLIS * Math.abs(initialInertiaZoomVelocity - 1) / (MAX_ZOOMIN_VELOCITY - 1)), Duration.seconds(duration), - event -> { - //stop inertia - reset(); - }, + event -> reset(), // stop inertia new KeyValue(inertiaZoomVelocity, 0, Interpolator.LINEAR)) ); inertiaTimeline.playFromStart(); @@ -240,7 +235,7 @@ public void notifyEndTouchEvent(long time) { if (currentTouchCount == 1) { if (state == ZoomRecognitionState.ACTIVE) { sendZoomFinishedEvent(); - if (ZOOM_INERTIA_ENABLED) { + if (zoomInertiaEnabled) { //prepare for inertia state = ZoomRecognitionState.PRE_INERTIA; } else { @@ -252,7 +247,7 @@ public void notifyEndTouchEvent(long time) { // currentTouchCount >= 2 if (state == ZoomRecognitionState.IDLE) { state = ZoomRecognitionState.TRACKING; - zoomStartTime = time; + zoomStartNanos = nanos; } calculateCenter(); @@ -265,7 +260,7 @@ public void notifyEndTouchEvent(long time) { } else { zoomFactor = currentDistance / distanceReference; if (state == ZoomRecognitionState.TRACKING) { - if ( Math.abs(zoomFactor - 1) > ZOOM_FACTOR_THRESHOLD) { + if ( Math.abs(zoomFactor - 1) > zoomFactorThreshold) { state = ZoomRecognitionState.ACTIVE; sendZoomStartedEvent(); } @@ -275,10 +270,11 @@ public void notifyEndTouchEvent(long time) { totalZoomFactor *= zoomFactor; sendZoomEvent(false); distanceReference = currentDistance; - double timePassed = ((double)time - zoomStartTime) / 1000000000; - if (timePassed > 1e-4) { - initialInertiaZoomVelocity = (totalZoomFactor - prevTotalZoomFactor) / timePassed; - zoomStartTime = time; + long nanosPassed = nanos - zoomStartNanos; + + if (nanosPassed > INITIAL_VELOCITY_THRESHOLD_NANOS) { + initialInertiaZoomVelocity = (totalZoomFactor - prevTotalZoomFactor) / nanosPassed * NANOS_TO_SECONDS; + zoomStartNanos = nanos; } else { initialInertiaZoomVelocity = 0; } @@ -344,19 +340,19 @@ private void sendZoomFinishedEvent() { }, scene.getAccessControlContext()); } - public void params(int modifiers, boolean direct) { + private 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) { + private void touchPressed(long id, int x, int y, int xAbs, int yAbs) { currentTouchCount++; TouchPointTracker tracker = new TouchPointTracker(); - tracker.update(nanos, x, y, xAbs, yAbs); + tracker.update(x, y, xAbs, yAbs); trackers.put(id, tracker); } - public void touchReleased(long id, long nanos, int x, int y, int xAbs, int yAbs) { + private void touchReleased(long id) { if (state != ZoomRecognitionState.FAILURE) { TouchPointTracker tracker = trackers.get(id); if (tracker == null) { @@ -370,7 +366,7 @@ public void touchReleased(long id, long nanos, int x, int y, int xAbs, int yAbs) currentTouchCount--; } - public void touchMoved(long id, long nanos, int x, int y, int xAbs, int yAbs) { + private void touchMoved(long id, int x, int y, int xAbs, int yAbs) { if (state == ZoomRecognitionState.FAILURE) { return; } @@ -382,10 +378,10 @@ public void touchMoved(long id, long nanos, int x, int y, int xAbs, int yAbs) { throw new RuntimeException("Error in zoom gesture " + "recognition: reported unknown touch point"); } - tracker.update(nanos, x, y, xAbs, yAbs); + tracker.update(x, y, xAbs, yAbs); } - void reset() { + private void reset() { state = ZoomRecognitionState.IDLE; zoomFactor = 1.0; totalZoomFactor = 1.0; @@ -395,7 +391,7 @@ private static class TouchPointTracker { double x, y; double absX, absY; - public void update(long nanos, double x, double y, double absX, double absY) { + public void update(double x, double y, double absX, double absY) { this.x = x; this.y = y; this.absX = absX; diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/tk/quantum/RotateGestureRecognizerTest.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/tk/quantum/RotateGestureRecognizerTest.java new file mode 100644 index 00000000000..2d282816fc5 --- /dev/null +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/tk/quantum/RotateGestureRecognizerTest.java @@ -0,0 +1,325 @@ +package test.com.sun.javafx.tk.quantum; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.sun.glass.events.TouchEvent; +import com.sun.glass.ui.Accessible; +import com.sun.javafx.tk.TKSceneListener; +import com.sun.javafx.tk.Toolkit; +import com.sun.javafx.tk.quantum.PrivilegedSceneListenerAccessor; +import com.sun.javafx.tk.quantum.RotateGestureRecognizer; + +import javafx.collections.ObservableList; +import javafx.event.EventType; +import javafx.scene.input.InputMethodEvent; +import javafx.scene.input.InputMethodTextRun; +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.TouchPoint.State; +import javafx.scene.input.ZoomEvent; +import test.com.sun.javafx.pgstub.StubToolkit; + +public class RotateGestureRecognizerTest { + private static final double EPSILON = 0.0001; + + private final List rotationEvents = new ArrayList<>(); + private final StubToolkit toolkit = (StubToolkit) Toolkit.getToolkit(); + private final RotateGestureRecognizer recognizer = new RotateGestureRecognizer(createAccessor()); + + private long nanos; + + @Nested + class WhenThereAreTwoTouches { + { + clearEvents(); + passTime(0); + //passTime(Long.MAX_VALUE / 1000 / 1000 - 10000000); // a value close to Long.MAX_VALUE in nanos to test accuracy + + recognizer.notifyBeginTouchEvent(nanos, 0, false, 0); + recognizer.notifyNextTouchEvent(nanos, TouchEvent.TOUCH_PRESSED, 1, 100, 100, 100, 100); + recognizer.notifyNextTouchEvent(nanos, TouchEvent.TOUCH_PRESSED, 2, 150, 100, 150, 100); + recognizer.notifyEndTouchEvent(nanos); + } + + @Test + void shouldHaveNoEvents() { + assertNoRotationEvents(); + } + + @Nested + class AndTouchesAreReleased { + { + clearEvents(); + passTime(100); + + recognizer.notifyBeginTouchEvent(nanos, 0, false, 0); + recognizer.notifyNextTouchEvent(nanos, TouchEvent.TOUCH_RELEASED, 1, 100, 100, 100, 100); + recognizer.notifyNextTouchEvent(nanos, TouchEvent.TOUCH_RELEASED, 2, 150, 100, 150, 100); + recognizer.notifyEndTouchEvent(nanos); + } + + @Test + void shouldSendNoEvents() { + assertNoRotationEvents(); + } + } + + @Nested + class AndSecondTouchIsMoved45DegreesCCW { + { + clearEvents(); + passTime(100); + + recognizer.notifyBeginTouchEvent(nanos, 0, false, 0); + recognizer.notifyNextTouchEvent(nanos, TouchEvent.TOUCH_MOVED, 2, 150, 50, 150, 50); + recognizer.notifyEndTouchEvent(nanos); + } + + @Test + void shouldSendStartRotationEvents() { + assertRotationEvent(new RotationEvent(RotateEvent.ROTATION_STARTED, 0, 0, 125, 75, 125, 75, false)); + assertRotationEvent(new RotationEvent(RotateEvent.ROTATE, -45, -45, 125, 75, 125, 75, false)); + } + + @Nested + class AndSecondTouchIsReleased { + { + clearEvents(); + passTime(100); + + recognizer.notifyBeginTouchEvent(nanos, 0, false, 0); + recognizer.notifyNextTouchEvent(nanos, TouchEvent.TOUCH_RELEASED, 2, 150, 50, 150, 50); + recognizer.notifyEndTouchEvent(nanos); + } + + @Test + void shouldSendRotationFinishedEventAndDoNoInertia() { + assertRotationEvent(new RotationEvent(RotateEvent.ROTATION_FINISHED, 0, -45, 125, 75, 125, 75, false)); + + passTime(250); + + assertNoRotationEvents(); + } + } + + @Nested + class AndSecondTouchIsMovedAnother45DegreesCCW { + { + clearEvents(); + passTime(100); + + recognizer.notifyBeginTouchEvent(nanos, 0, false, 0); + recognizer.notifyNextTouchEvent(nanos, TouchEvent.TOUCH_MOVED, 2, 100, 50, 100, 50); + recognizer.notifyEndTouchEvent(nanos); + } + + @Test + void shouldSendAnotherRotationEvent() { + assertRotationEvent(new RotationEvent(RotateEvent.ROTATE, -45, -90, 100, 75, 100, 75, false)); + } + + @Nested + class AndBothTouchesAreReleased { + { + clearEvents(); + + recognizer.notifyBeginTouchEvent(nanos, 0, false, 0); + recognizer.notifyNextTouchEvent(nanos, TouchEvent.TOUCH_RELEASED, 1, 100, 100, 100, 100); + recognizer.notifyNextTouchEvent(nanos, TouchEvent.TOUCH_RELEASED, 2, 100, 50, 100, 50); + recognizer.notifyEndTouchEvent(nanos); + } + + @Test + void shouldSendRotationFinishedEvent() { + assertRotationEvent(new RotationEvent(RotateEvent.ROTATION_FINISHED, 0, -90, 100, 75, 100, 75, false)); + } + + @Nested + class AndTimePasses { + { + clearEvents(); + passTime(250); + } + + @Test + void shouldDoInertia() { + assertRotationEvent(new RotationEvent(RotateEvent.ROTATE, -91.6666, -181.6666, 100, 75, 100, 75, true)); + + // Trigger a few more inertia events: + passTime(250); + passTime(250); + passTime(250); + passTime(250); + passTime(250); + + // Assert Inertia events; angle moved slowly reduces to 0: + assertRotationEvent(new RotationEvent(RotateEvent.ROTATE, -70.8333, -252.5000, 100, 75, 100, 75, true)); + assertRotationEvent(new RotationEvent(RotateEvent.ROTATE, -50.0000, -302.5000, 100, 75, 100, 75, true)); + assertRotationEvent(new RotationEvent(RotateEvent.ROTATE, -29.1666, -331.6666, 100, 75, 100, 75, true)); + assertRotationEvent(new RotationEvent(RotateEvent.ROTATE, -8.3333, -340.0000, 100, 75, 100, 75, true)); + assertRotationEvent(new RotationEvent(RotateEvent.ROTATE, 0.0, -340.0000, 100, 75, 100, 75, true)); + } + + @Nested + class AndASingleTouchOccurs { + { + // Initial inertia event: + assertRotationEvent(new RotationEvent(RotateEvent.ROTATE, -91.6666, -181.6666, 100, 75, 100, 75, true)); + + passTime(250); + + // Second intetia event: + assertRotationEvent(new RotationEvent(RotateEvent.ROTATE, -70.8333, -252.5000, 100, 75, 100, 75, true)); + + // Halt inertia: + recognizer.notifyBeginTouchEvent(nanos, 0, false, 0); + recognizer.notifyNextTouchEvent(nanos, TouchEvent.TOUCH_PRESSED, 3, 200, 200, 200, 200); + recognizer.notifyEndTouchEvent(nanos); + } + + @Test + void shouldHaltInertia() { + assertNoRotationEvents(); + passTime(250); + assertNoRotationEvents(); + } + } + } + } + } + } + } + + private void assertRotationEvent(RotationEvent rotationEvent) { + assertTrue(rotationEvents.size() > 0, "Expected rotation event, but none available"); + + RotationEvent remove = rotationEvents.remove(0); + + assertTrue(rotationEvent.anglesCloseToEquals(remove), remove + " must match " + rotationEvent); + } + + private void assertNoRotationEvents() { + assertTrue(rotationEvents.isEmpty(), "No rotation event expected, but there were some available: " + rotationEvents); + } + + private void clearEvents() { + rotationEvents.clear(); + } + + private void passTime(long millis) { + nanos += millis * 1000 * 1000; + toolkit.setAnimationTime(nanos / 1000 / 1000); + } + + private record RotationEvent(EventType eventType, double angle, double totalAngle, double x, + double y, double screenX, double screenY, boolean inertia) { + + public boolean anglesCloseToEquals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + RotationEvent other = (RotationEvent) obj; + return Math.abs(angle - other.angle) < EPSILON + && Objects.equals(eventType, other.eventType) + && inertia == other.inertia + && Double.doubleToLongBits(screenX) == Double.doubleToLongBits(other.screenX) + && Double.doubleToLongBits(screenY) == Double.doubleToLongBits(other.screenY) + && Math.abs(totalAngle - other.totalAngle) < EPSILON + && Double.doubleToLongBits(x) == Double.doubleToLongBits(other.x) + && Double.doubleToLongBits(y) == Double.doubleToLongBits(other.y); + } + } + + private PrivilegedSceneListenerAccessor createAccessor() { + return consumer -> consumer.accept(new TKSceneListener() { + @Override + public void changedLocation(float x, float y) { + } + + @Override + public void changedSize(float width, float height) { + } + + @Override + public void mouseEvent(EventType type, double x, double y, double screenX, double screenY, + MouseButton button, boolean popupTrigger, boolean synthesized, boolean shiftDown, + boolean controlDown, boolean altDown, boolean metaDown, boolean primaryDown, boolean middleDown, + boolean secondaryDown, boolean backDown, boolean forwardDown) { + } + + @Override + public void keyEvent(KeyEvent keyEvent) { + } + + @Override + public void inputMethodEvent(EventType type, + ObservableList composed, String committed, int caretPosition) { + } + + @Override + public void scrollEvent(EventType eventType, double scrollX, double scrollY, + double totalScrollX, double totalScrollY, double xMultiplier, double yMultiplier, + int touchCount, int scrollTextX, int scrollTextY, int defaultTextX, int defaultTextY, double x, + double y, double screenX, double screenY, boolean _shiftDown, boolean _controlDown, + boolean _altDown, boolean _metaDown, boolean _direct, boolean _inertia) { + } + + @Override + public void menuEvent(double x, double y, double xAbs, double yAbs, boolean isKeyboardTrigger) { + } + + @Override + public void zoomEvent(EventType eventType, double zoomFactor, double totalZoomFactor, + double x, double y, double screenX, double screenY, boolean _shiftDown, boolean _controlDown, + boolean _altDown, boolean _metaDown, boolean _direct, boolean _inertia) { + } + + + @Override + public void rotateEvent(EventType eventType, double angle, double totalAngle, double x, + double y, double screenX, double screenY, boolean _shiftDown, boolean _controlDown, + boolean _altDown, boolean _metaDown, boolean _direct, boolean _inertia) { + rotationEvents.add(new RotationEvent(eventType, angle, totalAngle, x, y, screenX, screenY, _inertia)); + } + + @Override + public void swipeEvent(EventType eventType, int touchCount, double x, double y, + double screenX, double screenY, boolean _shiftDown, boolean _controlDown, boolean _altDown, + boolean _metaDown, boolean _direct) { + } + + @Override + public void touchEventBegin(long time, int touchCount, boolean isDirect, boolean _shiftDown, + boolean _controlDown, boolean _altDown, boolean _metaDown) { + } + + @Override + public void touchEventNext(State state, long touchId, double x, double y, double xAbs, double yAbs) { + } + + @Override + public void touchEventEnd() { + } + + @Override + public Accessible getSceneAccessible() { + return null; + } + }); + } +}