diff --git a/modules/graphics/src/main/java/javafx/application/Platform.java b/modules/graphics/src/main/java/javafx/application/Platform.java --- a/modules/graphics/src/main/java/javafx/application/Platform.java +++ b/modules/graphics/src/main/java/javafx/application/Platform.java @@ -25,6 +25,7 @@ package javafx.application; +import com.sun.javafx.tk.Toolkit; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanWrapper; import com.sun.javafx.application.PlatformImpl; @@ -168,6 +169,92 @@ return PlatformImpl.isSupported(feature); } + /** + * Enter a nested event loop and block until the corresponding + * exitNestedEventLoop call is made. + * The key passed into this method is used to + * uniquely identify the matched enter/exit pair. This method creates a + * new nested event loop and blocks until the corresponding + * exitNestedEventLoop method is called with the same key. + * The return value of this method will be the {@code rval} + * object supplied to the exitNestedEventLoop method call that unblocks it. + * + *

+ * This method must either be called from an input event handler or + * from the run method of a Runnable passed to + * {@link javafx.application.Platform#runLater Platform.runLater}. + * It must not be called during animation or layout processing. + *

+ * + * @param key the Object that identifies the nested event loop, which + * must not be null + * + * @throws IllegalArgumentException if the specified key is associated + * with a nested event loop that has not yet returned + * + * @throws NullPointerException if the key is null + * + * @throws IllegalStateException if this method is called during + * animation or layout processing. + * + * @throws IllegalStateException if this method is called on a thread + * other than the JavaFX Application Thread. + * + * @return the value passed into the corresponding call to exitEventLoop + * + * @since 9 + */ + public static Object enterNestedEventLoop(Object key) { + return Toolkit.getToolkit().enterNestedEventLoop(key); + } + + /** + * Exit a nested event loop and unblock the caller of the + * corresponding enterNestedEventLoop. + * The key passed into this method is used to + * uniquely identify the matched enter/exit pair. This method causes the + * nested event loop that was previously created with the key to exit and + * return control to the caller. If the specified nested event loop is not + * the inner-most loop then it will not return until all other inner loops + * also exit. + * + * @param key the Object that identifies the nested event loop, which + * must not be null + * + * @param rval an Object that is returned to the caller of the + * corresponding enterNestedEventLoop. This may be null. + * + * @throws IllegalArgumentException if the specified key is not associated + * with an active nested event loop + * + * @throws NullPointerException if the key is null + * + * @throws IllegalStateException if this method is called on a thread + * other than the FX Application thread + * + * @since 9 + */ + public static void exitNestedEventLoop(Object key, Object rval) { + Toolkit.getToolkit().exitNestedEventLoop(key, rval); + } + + /** + * Checks whether a nested event loop is running, returning true to indicate + * that one is, and false if there are no nested event loops currently + * running. + * This method must be called on the JavaFX Application thread. + * + * @return true if there is a nested event loop running, and false otherwise. + * + * @throws IllegalStateException if this method is called on a thread + * other than the JavaFX Application Thread. + * + * @since 9 + */ + public static boolean isNestedLoopRunning() { + return Toolkit.getToolkit().isNestedLoopRunning(); + } + private static ReadOnlyBooleanWrapper accessibilityActiveProperty; public static boolean isAccessibilityActive() { diff --git a/tests/system/src/test/java/javafx/stage/NestedEventLoopTest.java b/tests/system/src/test/java/javafx/stage/NestedEventLoopTest.java new file mode 100644 --- /dev/null +++ b/tests/system/src/test/java/javafx/stage/NestedEventLoopTest.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2015, 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 javafx.stage; + +import java.util.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.print.PrinterJob; +import javafx.scene.Group; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import javafx.util.Duration; +import junit.framework.AssertionFailedError; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import util.Util; + +import static org.junit.Assert.*; +import static org.junit.Assume.*; +import static util.Util.TIMEOUT; + +/** + * Test program for nested event loop functionality. + */ +public class NestedEventLoopTest { + + // Used to launch the application before running any test + private static final CountDownLatch launchLatch = new CountDownLatch(1); + + // Singleton Application instance + private static MyApp myApp; + + // Application class. An instance is created and initialized before running + // the first test, and it lives through the execution of all tests. + public static class MyApp extends Application { + private Stage primaryStage; + + @Override public void init() { + NestedEventLoopTest.myApp = this; + } + + @Override public void start(Stage primaryStage) throws Exception { + primaryStage.setTitle("Primary stage"); + Group root = new Group(); + Scene scene = new Scene(root); + scene.setFill(Color.LIGHTYELLOW); + primaryStage.setScene(scene); + primaryStage.setX(0); + primaryStage.setY(0); + primaryStage.setWidth(210); + primaryStage.setHeight(180); + + this.primaryStage = primaryStage; + launchLatch.countDown(); + } + } + + @BeforeClass + public static void setupOnce() { + // Start the Application + new Thread(() -> Application.launch(MyApp.class, (String[])null)).start(); + + try { + if (!launchLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)) { + throw new AssertionFailedError("Timeout waiting for Application to launch"); + } + } catch (InterruptedException ex) { + AssertionFailedError err = new AssertionFailedError("Unexpected exception"); + err.initCause(ex); + throw err; + } + } + + @AfterClass + public static void teardownOnce() { + Platform.exit(); + } + + // Verify that we cannot enter a nested event loop on a thread other than + // the FX Application thread + @Test (expected=IllegalStateException.class) + public void testMustRunOnAppThread() { + assertFalse(Platform.isFxApplicationThread()); + Platform.enterNestedEventLoop(new Object()); + } + + // Verify that we can enter and exit a nested event loop + @Test public void testCanEnterAndExitNestedEventLoop() { + final long key = 1024L; + final long result = 2048L; + final AtomicLong returnedValue = new AtomicLong(); + + Util.runAndWait( + () -> { + assertFalse(Platform.isNestedLoopRunning()); + Long actual = (Long) Platform.enterNestedEventLoop(key); + returnedValue.set(actual); + }, + () -> { + assertTrue(Platform.isNestedLoopRunning()); + Platform.exitNestedEventLoop(key, result); + }, + () -> { + assertFalse(Platform.isNestedLoopRunning()); + assertEquals(result, returnedValue.get()); + } + ); + } + + // Verify that we cannot enter a nested event loop with the same key twice + @Test (expected=IllegalArgumentException.class) + public void testUniqueKeyRequired() { + final Object key = new Object(); + Util.runAndWait( + () -> Platform.enterNestedEventLoop(key), + () -> Platform.enterNestedEventLoop(key), + () -> Platform.exitNestedEventLoop(key, null) + ); + } + + // Verify that we cannot enter a nested event loop with a null key + @Test (expected=NullPointerException.class) + public void testNonNullKeyRequired() { + Util.runAndWait( + () -> Platform.enterNestedEventLoop(null) + ); + } + + // Verify that we cannot exit a nested event loop with a null key + @Test (expected=NullPointerException.class) + public void testNonNullExitKeyRequired() { + Util.runAndWait( + () -> Platform.enterNestedEventLoop("validKey"), + () -> Platform.exitNestedEventLoop(null, null), + () -> Platform.exitNestedEventLoop("validKey", null) + ); + } + + // Verify that we cannot exit a nested event loop with a key that has not been used + @Test (expected=IllegalArgumentException.class) + public void testExitLoopKeyHasBeenRegistered() { + Util.runAndWait( + () -> Platform.enterNestedEventLoop("validKey"), + () -> Platform.exitNestedEventLoop("invalidKey", null), + () -> Platform.exitNestedEventLoop("validKey", null) + ); + } + + // Verify that we can enter and exit multiple nested event loops, in the order they are started + @Test public void testCanEnterMultipleNestedLoops_andExitInOrder() { + final long key1 = 1024L; + final long key2 = 1025L; + final long result1 = 2048L; + final long result2 = 2049L; + final AtomicLong returnedValue1 = new AtomicLong(); + final AtomicLong returnedValue2 = new AtomicLong(); + + Util.runAndWait( + () -> { + // enter loop one + assertFalse(Platform.isNestedLoopRunning()); + Long actual = (Long) Platform.enterNestedEventLoop(key1); + returnedValue1.set(actual); + }, + () -> { + // enter loop two + assertTrue(Platform.isNestedLoopRunning()); + Long actual = (Long) Platform.enterNestedEventLoop(key2); + returnedValue2.set(actual); + }, + () -> { + // exit loop two + assertTrue(Platform.isNestedLoopRunning()); + Platform.exitNestedEventLoop(key2, result2); + }, + () -> { + // check loop two is done + assertTrue(Platform.isNestedLoopRunning()); + assertEquals(result2, returnedValue2.get()); + }, + () -> { + // exit loop one + assertTrue(Platform.isNestedLoopRunning()); + Platform.exitNestedEventLoop(key1, result1); + }, + () -> { + // check loop one is done + assertFalse(Platform.isNestedLoopRunning()); + assertEquals(result1, returnedValue1.get()); + } + ); + } +}