From a4ec6556f9ef3a409cceb9bfdb0d19dfc7d98d4a Mon Sep 17 00:00:00 2001
From: Sven Gothel
+ * Implementation allows use of custom {@link GLCapabilities}. + *
+ */ +public class NewtCanvasJFX extends Canvas implements WindowClosingProtocol { + private static final boolean DEBUG = Debug.debug("Window"); + + private volatile javafx.stage.Window parentWindow = null; + private volatile AbstractGraphicsScreen screen = null; + + private WindowClosingMode newtChildCloseOp = WindowClosingMode.DISPOSE_ON_CLOSE; + private final Rectangle clientArea = new Rectangle(); + + private volatile JFXNativeWindow nativeWindow = null; + private volatile Window newtChild = null; + private volatile boolean newtChildReady = false; // ready if JFXEDTUtil is set and newtChild parented + private volatile boolean postSetSize = false; // pending resize + private volatile boolean postSetPos = false; // pending pos + + private final EventHandler+ * Note: The NEWT child {@link Display}'s {@link EDTUtil} is being set to an JFX conform implementation + * via {@link Display#setEDTUtil(EDTUtil)}. + *
+ * @param child optional preassigned {@link #Window}, maybe null + */ + public NewtCanvasJFX(final Window child) { + super(); + + updateParentWindowAndScreen(); + + this.widthProperty().addListener(new ChangeListenerdestroy()
on the NEWT Child
+ * A previously detached newChild
will be released to top-level status
+ * and made invisible.
+ *
+ * Note: When switching NEWT child's, detaching the previous first via setNEWTChild(null)
+ * produced much cleaner visual results.
+ *
+ * Note: The NEWT child {@link Display}'s {@link EDTUtil} is being set to an JFX conform implementation + * via {@link Display#setEDTUtil(EDTUtil)}. + *
+ * @return the previous attached newt child. + */ + public Window setNEWTChild(final Window newChild) { + final Window prevChild = newtChild; + if(DEBUG) { + System.err.println("NewtCanvasJFX.setNEWTChild.0: win "+newtWinHandleToHexString(prevChild)+" -> "+newtWinHandleToHexString(newChild)); + } + // remove old one + if(null != newtChild) { + reparentWindow( false ); + newtChild = null; + } + // add new one, reparent only if ready + newtChild = newChild; + if( null != newtChild && ( null != nativeWindow || validateNative(false /* completeReparent */) ) ) { + reparentWindow( true ); + } + return prevChild; + } + + private void reparentWindow(final boolean add) { + if( null == newtChild ) { + return; // nop + } + if(DEBUG) { + System.err.println("NewtCanvasJFX.reparentWindow.0: add="+add+", win "+newtWinHandleToHexString(newtChild)+", EDTUtil: cur "+newtChild.getScreen().getDisplay().getEDTUtil()); + } + + newtChild.setFocusAction(null); // no AWT focus traversal .. + if(add) { + assert null != nativeWindow && null != parentWindow; + updatePosSizeCheck(); + final int x = clientArea.getX(); + final int y = clientArea.getY(); + final int w = clientArea.getWidth(); + final int h = clientArea.getHeight(); + + // set JFX EDT and start it + if(false) { + final Display newtDisplay = newtChild.getScreen().getDisplay(); + final EDTUtil edtUtil = new JFXEDTUtil(newtDisplay); + edtUtil.start(); + newtDisplay.setEDTUtil( edtUtil ); + } + + newtChild.setSize(w, h); + newtChild.reparentWindow(nativeWindow, x, y, Window.REPARENT_HINT_BECOMES_VISIBLE); + newtChild.setPosition(x, y); + newtChild.setVisible(true); + configureNewtChild(true); + newtChild.sendWindowEvent(WindowEvent.EVENT_WINDOW_RESIZED); // trigger a resize/relayout to listener + + // force this JFX Canvas to be focus-able, + // since it is completely covered by the newtChild (z-order). + // FIXME ??? super.requestFocus(); + } else { + configureNewtChild(false); + newtChild.setVisible(false); + newtChild.reparentWindow(null, -1, -1, 0 /* hints */); + } + if(DEBUG) { + System.err.println("NewtCanvasJFX.reparentWindow.X: add="+add+", win "+newtWinHandleToHexString(newtChild)+", EDTUtil: cur "+newtChild.getScreen().getDisplay().getEDTUtil()); + } + } + + private void configureNewtChild(final boolean attach) { + newtChildReady = attach; + if( null != newtChild ) { + newtChild.setKeyboardFocusHandler(null); + if(attach) { + newtChildCloseOp = newtChild.setDefaultCloseOperation(WindowClosingMode.DO_NOTHING_ON_CLOSE); + } else { + newtChild.setFocusAction(null); + newtChild.setDefaultCloseOperation(newtChildCloseOp); + } + } + } + + /** @return the current NEWT child */ + public Window getNEWTChild() { + return newtChild; + } + + /** @return this JFX Canvas NativeWindow representation, may be null in case it has not been realized. */ + public NativeWindow getNativeWindow() { return nativeWindow; } + + @Override + public WindowClosingMode getDefaultCloseOperation() { + return newtChildCloseOp; // TODO: implement ?! + } + + @Override + public WindowClosingMode setDefaultCloseOperation(final WindowClosingMode op) { + return newtChildCloseOp = op; // TODO: implement ?! + } + + + boolean isParent() { + return null!=newtChild ; + } + + boolean isFullscreen() { + return null != newtChild && newtChild.isFullscreen(); + } + + private final void requestFocusNEWTChild() { + if( newtChildReady ) { + newtChild.setFocusAction(null); + newtChild.requestFocus(); + } + } + + @Override + public void requestFocus() { + NewtCanvasJFX.super.requestFocus(); + requestFocusNEWTChild(); + } + + private class JFXNativeWindow implements NativeWindow { + private final AbstractGraphicsConfiguration config; + private final long nativeWindowHandle; + private final InsetsImmutable insets; // only required to allow proper client position calculation on OSX + + public JFXNativeWindow(final AbstractGraphicsConfiguration config, final long nativeWindowHandle) { + this.config = config; + this.nativeWindowHandle = nativeWindowHandle; + this.insets = new Insets(0, 0, 0, 0); + } + + @Override + public int lockSurface() throws NativeWindowException, RuntimeException { + return NativeSurface.LOCK_SUCCESS; + } + + @Override + public void unlockSurface() { } + + @Override + public boolean isSurfaceLockedByOtherThread() { + return false; + } + + @Override + public Thread getSurfaceLockOwner() { + return null; + } + + @Override + public boolean surfaceSwap() { + return false; + } + + @Override + public void addSurfaceUpdatedListener(final SurfaceUpdatedListener l) { } + + @Override + public void addSurfaceUpdatedListener(final int index, final SurfaceUpdatedListener l) throws IndexOutOfBoundsException { + } + + @Override + public void removeSurfaceUpdatedListener(final SurfaceUpdatedListener l) { } + + @Override + public long getSurfaceHandle() { + return 0; + } + + @Override + public int getWidth() { + return getSurfaceWidth(); // FIXME: Use 'scale' or an actual window-width + } + + @Override + public int getHeight() { + return getSurfaceHeight(); // FIXME: Use 'scale' or an actual window-width + } + + @Override + public final int[] convertToWindowUnits(final int[] pixelUnitsAndResult) { + return pixelUnitsAndResult; // FIXME HiDPI: use 'pixelScale' + } + + @Override + public final int[] convertToPixelUnits(final int[] windowUnitsAndResult) { + return windowUnitsAndResult; // FIXME HiDPI: use 'pixelScale' + } + + @Override + public int getSurfaceWidth() { + return clientArea.getWidth(); + } + + @Override + public int getSurfaceHeight() { + return clientArea.getHeight(); + } + + @Override + public final NativeSurface getNativeSurface() { return this; } + + @Override + public AbstractGraphicsConfiguration getGraphicsConfiguration() { + return config; + } + + @Override + public long getDisplayHandle() { + return config.getScreen().getDevice().getHandle(); + } + + @Override + public int getScreenIndex() { + return config.getScreen().getIndex(); + } + + @Override + public void surfaceUpdated(final Object updater, final NativeSurface ns, final long when) { } + + @Override + public void destroy() { } + + @Override + public NativeWindow getParent() { + return null; + } + + @Override + public long getWindowHandle() { + return nativeWindowHandle; + } + + @Override + public InsetsImmutable getInsets() { + return insets; + } + + @Override + public int getX() { + return 0; + } + + @Override + public int getY() { + return 0; + } + + @Override + public Point getLocationOnScreen(final Point point) { + final Point los = NativeWindowFactory.getLocationOnScreen(this); // client window location on screen + if(null!=point) { + return point.translate(los); + } else { + return los; + } + } + + @Override + public boolean hasFocus() { + return isFocused(); + } + }; + + static String newtWinHandleToHexString(final Window w) { + return null != w ? toHexString(w.getWindowHandle()) : "nil"; + } + static String toHexString(final long l) { + return "0x"+Long.toHexString(l); + } +} + diff --git a/src/newt/classes/jogamp/newt/javafx/JFXEDTUtil.java b/src/newt/classes/jogamp/newt/javafx/JFXEDTUtil.java new file mode 100644 index 000000000..4ee15b43d --- /dev/null +++ b/src/newt/classes/jogamp/newt/javafx/JFXEDTUtil.java @@ -0,0 +1,358 @@ +/** + * Copyright 2019 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ +package jogamp.newt.javafx; + +import com.jogamp.nativewindow.NativeWindowException; +import com.jogamp.nativewindow.javafx.JFXAccessor; + +import jogamp.newt.Debug; + +import com.jogamp.common.ExceptionUtils; +import com.jogamp.common.util.InterruptSource; +import com.jogamp.common.util.InterruptedRuntimeException; +import com.jogamp.common.util.RunnableTask; +import com.jogamp.newt.util.EDTUtil; + +import javafx.application.Platform; + +/** + * Simple {@link EDTUtil} implementation utilizing the JFX UI thread + * of the given {@link Display}. + */ +public class JFXEDTUtil implements EDTUtil { + public static final boolean DEBUG = Debug.debug("EDT"); + + private final Object edtLock = new Object(); // locking the EDT start/stop state + private final ThreadGroup threadGroup; + private final String name; + private final Runnable dispatchMessages; + private NEDT nedt = null; + private int start_iter=0; + private static long pollPeriod = EDTUtil.defaultEDTPollPeriod; + + public JFXEDTUtil(final com.jogamp.newt.Display newtDisplay) { + this.threadGroup = Thread.currentThread().getThreadGroup(); + this.name=Thread.currentThread().getName()+"-JFXDisplay-"+newtDisplay.getFQName()+"-EDT-"; + this.dispatchMessages = new Runnable() { + @Override + public void run() { + ((jogamp.newt.DisplayImpl) newtDisplay).dispatchMessages(); + } }; + this.nedt = new NEDT(threadGroup, name); + this.nedt.setDaemon(true); // don't stop JVM from shutdown .. + } + + @Override + public long getPollPeriod() { + return pollPeriod; + } + + @Override + public void setPollPeriod(final long ms) { + pollPeriod = ms; // writing to static field is intended + } + + @Override + public final void start() throws IllegalStateException { + synchronized(edtLock) { + if( nedt.isRunning() ) { + final Thread curT = Thread.currentThread(); + final boolean onJFXEDT = JFXAccessor.isJFXThread(); + throw new IllegalStateException("EDT still running and not subject to stop. Curr "+curT.getName()+ + ", NEDT "+nedt.getName()+", isRunning "+nedt.isRunning+", shouldStop "+nedt.shouldStop+", JFX-EDT "+JFXAccessor.getJFXThreadName()+", on JFX-EDT "+onJFXEDT); + } + if(DEBUG) { + System.err.println(Thread.currentThread()+": JFX-EDT reset - edt: "+nedt); + } + if( !JFXAccessor.hasJFXThreadStopped() ) { + if( nedt.getState() != Thread.State.NEW ) { + nedt = new NEDT(threadGroup, name); + nedt.setDaemon(true); // don't stop JVM from shutdown .. + } + startImpl(); + } + } + if( !JFXAccessor.hasJFXThreadStopped() ) { + if( !nedt.isRunning() ) { + throw new RuntimeException("EDT could not be started: "+nedt); + } + } else { + // FIXME: Throw exception ? + } + } + + private final void startImpl() { + if(nedt.isAlive()) { + throw new RuntimeException("JFX-EDT Thread.isAlive(): true, isRunning: "+nedt.isRunning+", shouldStop "+nedt.shouldStop+", edt: "+nedt); + } + start_iter++; + nedt.setName(name+start_iter); + if(DEBUG) { + System.err.println(Thread.currentThread()+": JFX-EDT START - edt: "+nedt+", jfxThread "+JFXAccessor.getJFXThreadName()); + // Thread.dumpStack(); + } + nedt.start(); + } + + @Override + public boolean isCurrentThreadEDT() { + return JFXAccessor.isJFXThread(); + } + + @Override + public final boolean isCurrentThreadNEDT() { + return nedt == Thread.currentThread(); + } + + @Override + public final boolean isCurrentThreadEDTorNEDT() { + return JFXAccessor.isJFXThread() || Thread.currentThread() == nedt ; + } + + @Override + public boolean isRunning() { + return nedt.isRunning(); + } + + @Override + public final boolean invokeStop(final boolean wait, final Runnable task) { + return invokeImpl(wait, task, true); + } + + @Override + public final boolean invoke(final boolean wait, final Runnable task) { + return invokeImpl(wait, task, false); + } + + private static Runnable nullTask = new Runnable() { + @Override + public void run() { } + }; + + private final boolean invokeImpl(boolean wait, final Runnable task, boolean stop) { + final RunnableTask rTask; + final Object rTaskLock = new Object(); + synchronized(rTaskLock) { // lock the optional task execution + synchronized(edtLock) { // lock the EDT status + if( nedt.shouldStop ) { + // drop task .. + if(DEBUG) { + System.err.println(Thread.currentThread()+": Warning: JFX-EDT about (1) to stop, won't enqueue new task: "+nedt+", isRunning "+nedt.isRunning+", shouldStop "+nedt.shouldStop); + ExceptionUtils.dumpStack(System.err); + } + return false; + } + final boolean hasJFXThreadStopped = JFXAccessor.hasJFXThreadStopped(); + + if( hasJFXThreadStopped ) { + stop = true; + } + + if( isCurrentThreadEDT() ) { + if(null != task) { + task.run(); + } + wait = false; // running in same thread (EDT) -> no wait + rTask = null; + if( stop ) { + nedt.shouldStop = true; + } + } else { + if( !nedt.isRunning && !hasJFXThreadStopped ) { + if( null != task ) { + if( stop ) { + System.err.println(Thread.currentThread()+": Warning: JFX-EDT is about (3) to stop and stopped already, dropping task. NEDT "+nedt); + } else { + System.err.println(Thread.currentThread()+": Warning: JFX-EDT is not running, dropping task. NEDT "+nedt); + } + if(DEBUG) { + ExceptionUtils.dumpStack(System.err); + } + } + return false; + } else if( stop ) { + if( nedt.isRunning ) { + if(DEBUG) { + System.err.println(Thread.currentThread()+": JFX-EDT signal STOP (on edt: "+isCurrentThreadEDT()+") - "+nedt+", isRunning "+nedt.isRunning+", shouldStop "+nedt.shouldStop); + } + synchronized(nedt.sync) { + nedt.shouldStop = true; + nedt.sync.notifyAll(); // stop immediate if waiting (poll freq) + } + } + if( JFXAccessor.hasJFXThreadStopped() ) { + System.err.println(Thread.currentThread()+": Warning: JFX-EDT is about (3) to stop and stopped already, dropping task. "+nedt); + if(DEBUG) { + ExceptionUtils.dumpStack(System.err); + } + return false; + } + } + + if( null != task ) { + rTask = new RunnableTask(task, + wait ? rTaskLock : null, + true /* always catch and report Exceptions, don't disturb EDT */, + wait ? null : System.err); + Platform.runLater(rTask); + } else { + wait = false; + rTask = null; + } + } + } + if( wait ) { + try { + while( rTask.isInQueue() ) { + rTaskLock.wait(); // free lock, allow execution of rTask + } + } catch (final InterruptedException ie) { + throw new InterruptedRuntimeException(ie); + } + final Throwable throwable = rTask.getThrowable(); + if(null!=throwable) { + if(throwable instanceof NativeWindowException) { + throw (NativeWindowException)throwable; + } + throw new RuntimeException(throwable); + } + } + return true; + } + } + + @Override + final public boolean waitUntilIdle() { + final NEDT _nedt; + synchronized(edtLock) { + _nedt = nedt; + } + if( !_nedt.isRunning || Thread.currentThread() == _nedt || JFXAccessor.hasJFXThreadStopped() || JFXAccessor.isJFXThread() ) { + return false; + } + JFXAccessor.runOnJFXThread(true, nullTask); + return true; + } + + @Override + final public boolean waitUntilStopped() { + synchronized(edtLock) { + final Thread curT = Thread.currentThread(); + final boolean onJFXEDT = JFXAccessor.isJFXThread(); + if( nedt.isRunning && nedt != curT && !onJFXEDT ) { + try { + while( nedt.isRunning ) { + edtLock.wait(); + } + } catch (final InterruptedException e) { + throw new InterruptedRuntimeException(e); + } + return true; + } else { + return false; + } + } + } + + class NEDT extends InterruptSource.Thread { + volatile boolean shouldStop = false; + volatile boolean isRunning = false; + Object sync = new Object(); + + public NEDT(final ThreadGroup tg, final String name) { + super(tg, null, name); + } + + final public boolean isRunning() { + return isRunning && !shouldStop; + } + + @Override + final public void start() throws IllegalThreadStateException { + isRunning = true; + super.start(); + } + + /** + * Utilizing locking only on tasks and its execution, + * not for event dispatching. + */ + @Override + final public void run() { + if(DEBUG) { + System.err.println(getName()+": JFX-EDT run() START "+ getName()); + } + RuntimeException error = null; + try { + do { + // event dispatch + if(!shouldStop) { + // EDT invoke thread is JFX-EDT, + // hence dispatching is required to run on JFX-EDT as well. + // Otherwise a deadlock may happen due to dispatched event's + // triggering a locking action. + JFXAccessor.runOnJFXThread(true, dispatchMessages); + } + // wait + synchronized(sync) { + if(!shouldStop) { + try { + sync.wait(pollPeriod); + } catch (final InterruptedException e) { + throw new InterruptedRuntimeException(e); + } + } + } + } while(!shouldStop) ; + } catch (final Throwable t) { + // handle errors .. + shouldStop = true; + if(t instanceof RuntimeException) { + error = (RuntimeException) t; + } else { + error = new RuntimeException("Within JFX-EDT", t); + } + } finally { + if(DEBUG) { + System.err.println(getName()+": JFX-EDT run() END "+ getName()+", "+error); + } + synchronized(edtLock) { + isRunning = false; + edtLock.notifyAll(); + } + if(DEBUG) { + System.err.println(getName()+": JFX-EDT run() EXIT "+ getName()+", exception: "+error); + } + if(null!=error) { + throw error; + } + } // finally + } // run() + } // EventDispatchThread + +} diff --git a/src/test/com/jogamp/opengl/test/junit/jogl/javafx/PureJFXApp01.java b/src/test/com/jogamp/opengl/test/junit/jogl/javafx/PureJFXApp01.java new file mode 100644 index 000000000..d2a3b8073 --- /dev/null +++ b/src/test/com/jogamp/opengl/test/junit/jogl/javafx/PureJFXApp01.java @@ -0,0 +1,54 @@ +/** + * Copyright 2019 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ + +package com.jogamp.opengl.test.junit.jogl.javafx; + +import javafx.application.Application; +import javafx.scene.Group; +import javafx.scene.Scene; +import javafx.scene.text.Font; +import javafx.scene.text.Text; +import javafx.stage.Stage; + +public class PureJFXApp01 extends Application { + + @Override public void start(Stage stage) { + Text text = new Text(10, 40, "Pure JFX App 01"); + text.setFont(new Font(40)); + Scene scene = new Scene(new Group(text)); + + stage.setTitle("JavaFX Stage"); + stage.setScene(scene); + stage.sizeToScene(); + stage.show(); + } + + public static void main(String[] args) { + Application.launch(args); + } +} diff --git a/src/test/com/jogamp/opengl/test/junit/jogl/javafx/TestNewtCanvasJFXGLn.java b/src/test/com/jogamp/opengl/test/junit/jogl/javafx/TestNewtCanvasJFXGLn.java new file mode 100644 index 000000000..8ec5a87d1 --- /dev/null +++ b/src/test/com/jogamp/opengl/test/junit/jogl/javafx/TestNewtCanvasJFXGLn.java @@ -0,0 +1,462 @@ +/** + * Copyright 2019 JogAmp Community. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of JogAmp Community. + */ + +package com.jogamp.opengl.test.junit.jogl.javafx; + +import com.jogamp.opengl.GLAutoDrawable; +import com.jogamp.opengl.GLCapabilities; +import com.jogamp.opengl.GLCapabilitiesImmutable; +import com.jogamp.opengl.GLEventListener; +import com.jogamp.opengl.GLProfile; + +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Test; +import org.junit.FixMethodOrder; +import org.junit.runners.MethodSorters; + +import com.jogamp.common.util.RunnableTask; +import com.jogamp.nativewindow.javafx.JFXAccessor; +import com.jogamp.nativewindow.swt.SWTAccessor; +import com.jogamp.newt.NewtFactory; +import com.jogamp.newt.Screen; +import com.jogamp.newt.javafx.NewtCanvasJFX; +import com.jogamp.newt.opengl.GLWindow; +import com.jogamp.opengl.test.junit.jogl.demos.es2.GearsES2; +import com.jogamp.opengl.test.junit.jogl.demos.es2.MultisampleDemoES2; +import com.jogamp.opengl.test.junit.util.AWTRobotUtil; +import com.jogamp.opengl.test.junit.util.MiscUtils; +import com.jogamp.opengl.test.junit.util.UITestCase; +import com.jogamp.opengl.util.Animator; +import com.jogamp.opengl.util.GLReadBufferUtil; +import com.jogamp.opengl.util.texture.TextureIO; + +import javafx.application.Application; +import javafx.application.Platform; +import javafx.scene.Group; +import javafx.scene.Scene; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.scene.text.Text; +import javafx.stage.Stage; + +/** + * Tests that a basic SWT app can open without crashing under different GL profiles + * _and_ custom GLCapabilities. + *
+ * Uses JOGL's NewtCanvasSWT, which allows to be a native container of a NEWT Window.
+ * This method allows utilizing custom GLCapability settings,
+ * independent from the already instantiated SWT visual.
+ *
+ * Note that {@link SWTAccessor#invoke(boolean, Runnable)} is still used to comply w/ + * SWT running on Mac OSX, i.e. to enforce UI action on the main thread. + *
+ */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class TestNewtCanvasJFXGLn extends UITestCase { + + static int duration = 5000; // 250; + + static final int iwidth = 640; + static final int iheight = 480; + + com.jogamp.newt.Display jfxNewtDisplay = null; + + public static class JFXApp extends Application { + static Stage stage; + + final static Object sync = new Object(); + static volatile boolean isLaunched = false; + + public JFXApp() { + } + + @Override public void init() throws Exception { + // pre JFX thread + System.err.println("JFX init ...: "+Thread.currentThread()); + } + + @Override public void start(final Stage stage) { + System.err.println("JFX start.0 ...: "+Thread.currentThread()); + synchronized(sync) { + try { + // on JFX thread + final Scene scene = new Scene(new Group(), defWidth, defHeight); + stage.setTitle(TestNewtCanvasJFXGLn.class.getSimpleName()); + stage.setScene(scene); + stage.sizeToScene(); + { + final long h = JFXAccessor.getWindowHandle(stage); + System.err.println("t1 - Native window: 0x"+Long.toHexString(h)); + } + stage.show(); + { + final long h = JFXAccessor.getWindowHandle(stage); + System.err.println("t2 - Native window: 0x"+Long.toHexString(h)); + } + JFXApp.stage = stage; + } finally { + isLaunched = true; + sync.notifyAll(); + } + } + System.err.println("JFX start.X ...: "+Thread.currentThread()); + } + @Override public void stop() throws Exception { + System.err.println("JFX stop ...: "+Thread.currentThread()); + } + public static void startup() throws InterruptedException { + System.out.println( "GLProfile " + GLProfile.glAvailabilityToString() ); + System.err.println("JFX Available: "+JFXAccessor.isJFXAvailable()); + if( JFXAccessor.isJFXAvailable() ) { + Platform.setImplicitExit(false); // FIXME: Default for all NEWT cases? + synchronized(sync) { + final Thread ct = Thread.currentThread(); + RunnableTask.invokeOnNewThread(ct.getThreadGroup(), ct.getName()+"JFXLauncher", false, + new Runnable() { + public void run() { + Application.launch(JFXApp.class); + } + }); + while(!isLaunched) { + sync.wait(); + } + } + System.err.println("JFX launched ..."); + } + } + public static void shutdown() { + JFXAccessor.runOnJFXThread(true, new Runnable() { + public void run() { + if( null != stage ) { + stage.close(); + } + } }); + } + } + + @BeforeClass + public static void startup() throws InterruptedException { + JFXApp.startup(); + } + + @AfterClass + public static void shutdown() { + JFXApp.shutdown(); + Platform.exit(); + } + + @Before + public void init() { + jfxNewtDisplay = NewtFactory.createDisplay(null, false); // no-reuse + } + + @After + public void release() { + jfxNewtDisplay = null; + } + + class WaitAction implements Runnable { + private final long sleepMS; + + WaitAction(final long sleepMS) { + this.sleepMS = sleepMS; + } + public void run() { + // blocks on linux .. display.sleep(); + try { + Thread.sleep(sleepMS); + } catch (final InterruptedException e) { } + } + } + final WaitAction awtRobotWaitAction = new WaitAction(AWTRobotUtil.TIME_SLICE); + final WaitAction generalWaitAction = new WaitAction(10); + + static final int defWidth = 800, defHeight = 600; + + static void populateScene(final Scene scene, final boolean postAttach, + final GLWindow glWindow, + final int width, final int height, final boolean useBorder, + final NewtCanvasJFX[] res) { + final javafx.stage.Window w = scene.getWindow(); + final boolean isShowing = null != w && w.isShowing(); + final Group g = new Group(); + + final int cx, cy, cw, ch, bw, bh; + if( useBorder ) { + bw = width/5; bh = height/5; + cx = bw; cy = bh; cw = width-bw-bw; ch = height-bh-bh; + } else { + bw = 0; bh = 0; + cx = 0; cy = 0; cw = width; ch = height; + } + System.err.println("Scene "+width+"x"+height+", isShowing "+isShowing+", postAttach "+postAttach); + System.err.println("Scene.canvas "+cx+"/"+cy+" "+cw+"x"+ch); + System.err.println("Scene.border "+bw+"x"+bh); + + if( !postAttach ) { + if(isShowing) { + JFXAccessor.runOnJFXThread(true, new Runnable() { + @Override + public void run() { + scene.setRoot(g); + }}); + } else { + scene.setRoot(g); + } + } + + final Canvas canvas0; + if( null == res ) { + canvas0 = new Canvas(); + } else { + res[0] = new NewtCanvasJFX( glWindow ); + canvas0 = res[0]; + } + canvas0.setWidth(cw); + canvas0.setHeight(ch); + if( null == res ) { + final GraphicsContext gc = canvas0.getGraphicsContext2D(); + gc.setFill(Color.BLUE); + gc.fillRect(0, 0, cw, ch); + } + canvas0.relocate(cx, cy); + + final Text text0 = new Text(0, 0, "left"); + { + text0.setFont(new Font(40)); + text0.relocate(0, height/2); + } + final Text text1 = new Text(0, 0, "above"); + { + text1.setFont(new Font(40)); + text1.relocate(width/2, bh-40); + } + final Text text2 = new Text(0, 0, "right"); + { + text2.setFont(new Font(40)); + text2.relocate(width-bw, height/2); + } + final Text text3 = new Text(0, 0, "below"); + { + text3.setFont(new Font(40)); + text3.relocate(width/2, height-bh); + } + final Runnable attach2Group = new Runnable() { + @Override + public void run() { + g.getChildren().add(text0); + g.getChildren().add(text1); + g.getChildren().add(canvas0); + g.getChildren().add(text2); + g.getChildren().add(text3); + } }; + if( !postAttach && isShowing ) { + JFXAccessor.runOnJFXThread(true, attach2Group); + } else { + attach2Group.run(); + } + if( postAttach ) { + if(isShowing) { + JFXAccessor.runOnJFXThread(true, new Runnable() { + @Override + public void run() { + scene.setRoot(g); + }}); + } else { + scene.setRoot(g); + } + } + } + + protected void runTestAGL( final GLCapabilitiesImmutable caps, final GLEventListener demo, + final boolean postAttachNewtCanvas, final boolean postAttachGLWindow, + final boolean useAnimator ) throws InterruptedException { + if( !JFXAccessor.isJFXAvailable() ) { + System.err.println("JFX not available"); + return; + } + final GLReadBufferUtil screenshot = new GLReadBufferUtil(false, false); + final GLWindow glWindow1; + if( null == demo ) { + glWindow1 = null; + } else { + final Screen screen = NewtFactory.createScreen(jfxNewtDisplay, 0); + glWindow1 = GLWindow.create(screen, caps); + Assert.assertNotNull(glWindow1); + Assert.assertEquals(false, glWindow1.isVisible()); + Assert.assertEquals(false, glWindow1.isNativeValid()); + Assert.assertNull(glWindow1.getParent()); + glWindow1.addGLEventListener(demo); + glWindow1.addGLEventListener(new GLEventListener() { + int displayCount = 0; + public void init(final GLAutoDrawable drawable) { } + public void reshape(final GLAutoDrawable drawable, final int x, final int y, final int width, final int height) { } + public void display(final GLAutoDrawable drawable) { + if(displayCount < 3) { + snapshot(displayCount++, null, drawable.getGL(), screenshot, TextureIO.PNG, null); + } + } + public void dispose(final GLAutoDrawable drawable) { } + }); + } + + final NewtCanvasJFX[] glCanvas = null==demo? null : new NewtCanvasJFX[]{null}; + + final Scene scene = new Scene(new Group(), defWidth, defHeight); + if(!postAttachNewtCanvas) { + System.err.println("Stage set.A0"); + JFXAccessor.runOnJFXThread(true, new Runnable() { + public void run() { + System.err.println("Stage set.A1"); + JFXApp.stage.setScene(scene); + JFXApp.stage.sizeToScene(); + System.err.println("Stage set.AX"); + } }); + } + populateScene( scene, postAttachNewtCanvas, postAttachGLWindow?null:glWindow1, defWidth, defHeight, true, glCanvas); + if(postAttachNewtCanvas) { + System.err.println("Stage set.B0"); + JFXAccessor.runOnJFXThread(true, new Runnable() { + public void run() { + System.err.println("Stage set.B1"); + JFXApp.stage.setScene(scene); + JFXApp.stage.sizeToScene(); + System.err.println("Stage set.BX"); + } }); + } + + if(postAttachGLWindow && null != demo) { + glCanvas[0].setNEWTChild(glWindow1); + } + + if( null != glWindow1 ) { + Assert.assertTrue("GLWindow didn't become visible natively!", AWTRobotUtil.waitForRealized(glWindow1, awtRobotWaitAction, true)); + System.err.println("GLWindow LOS.0: "+glWindow1.getLocationOnScreen(null)); + } + if( null != demo ) { + System.err.println("NewtCanvasSWT LOS.0: "+glCanvas[0].getNativeWindow().getLocationOnScreen(null)); + } + + Animator anim; + if(useAnimator && null != demo) { + anim = new Animator(glWindow1); + anim.start(); + } else { + anim = null; + } + + final long lStartTime = System.currentTimeMillis(); + final long lEndTime = lStartTime + duration; + try { + while( (System.currentTimeMillis() < lEndTime) ) { + generalWaitAction.run(); + } + } catch( final Throwable throwable ) { + throwable.printStackTrace(); + Assume.assumeNoException( throwable ); + } + if(null != anim) { + anim.stop(); + } + + JFXAccessor.runOnJFXThread(true, new Runnable() { + public void run() { + populateScene( JFXApp.stage.getScene(), false, null, defWidth, defHeight, true, null); + JFXApp.stage.sizeToScene(); + } }); + } + + @Test + public void test00() throws InterruptedException { + runTestAGL( null, null, + false /* postAttachNewtCanvas */, false /* postAttach */, false /* animator */); + } + + @Test + public void test11_preAttachNewtGL_NoAnim() throws InterruptedException { + runTestAGL( new GLCapabilities(GLProfile.getGL2ES2()), new GearsES2(), + false /* postAttachNewtCanvas */, false /* postAttachGLWindow */, false /* animator */); + } + + @Test + public void test12_postAttachNewt_NoAnim() throws InterruptedException { + runTestAGL( new GLCapabilities(GLProfile.getGL2ES2()), new GearsES2(), + true /* postAttachNewtCanvas */, false /* postAttachGLWindow */, false /* animator */); + } + + @Test + public void test13_postAttachGL_NoAnim() throws InterruptedException { + runTestAGL( new GLCapabilities(GLProfile.getGL2ES2()), new GearsES2(), + false /* postAttachNewtCanvas */, true /* postAttachGLWindow */, false /* animator */); + } + + @Test + public void test14_postAttachNewtGL_NoAnim() throws InterruptedException { + runTestAGL( new GLCapabilities(GLProfile.getGL2ES2()), new GearsES2(), + true /* postAttachNewtCanvas */, true /* postAttachGLWindow */, false /* animator */); + } + + @Test + public void test21_preAttachNewtGL_DoAnim() throws InterruptedException { + runTestAGL( new GLCapabilities(GLProfile.getGL2ES2()), new GearsES2(), + false /* postAttachNewtCanvas */, false /* postAttachGLWindow */, true /* animator */); + } + + @Test + public void test22_postAttachNewt_DoAnim() throws InterruptedException { + runTestAGL( new GLCapabilities(GLProfile.getGL2ES2()), new GearsES2(), + true /* postAttachNewtCanvas */, false /* postAttachGLWindow */, true /* animator */); + } + + @Test + public void test30_MultisampleAndAlpha() throws InterruptedException { + final GLCapabilities caps = new GLCapabilities(GLProfile.getGL2ES2()); + caps.setSampleBuffers(true); + caps.setNumSamples(2); + runTestAGL( caps, new MultisampleDemoES2(true), + false /* postAttachNewtCanvas */, false /* postAttachGLWindow */, false /* animator */); + } + + public static void main(final String args[]) { + for(int i=0; i