diff options
author | Sven Gothel <[email protected]> | 2019-03-19 11:40:34 +0100 |
---|---|---|
committer | Sven Gothel <[email protected]> | 2019-03-19 11:40:34 +0100 |
commit | a4ec6556f9ef3a409cceb9bfdb0d19dfc7d98d4a (patch) | |
tree | ef9d0b6ea56e890712e97437f3c56659ea1a9506 /src | |
parent | a98ef342cc03a92692584a35291724d7b05c3370 (diff) |
JavaFX: Adding JavaFX Support for NEWT utilizing native Window parenting via NewtCanvasJFX
NewtCanvasJFX, a JavaFX Canvas Node, allows attaching a native NEWT Window to the JavaFX Node's native Window (if attached).
The mechanism is similar to NewtCanvasAWT.
Current implementation supports placing the NEWT Window
into the JavaFX scene of the native window correctly,
as well as the following different lifecycles
- attach NewtCanvasJFX to already visible group->scene->window
- attach NewtCanvasJFX to not yet visible or attached group->scene->window
- attach NEWT Window before or after NewtCanvasJFX's visibility
The above is covered by unit test: TestNewtCanvasJFXGLn
This is the initial commit for JavaFX support and has been tested on
- OpenJDK 8 + OpenJFX 8
- GNU/Linux X11
Diffstat (limited to 'src')
7 files changed, 1818 insertions, 1 deletions
diff --git a/src/nativewindow/classes/com/jogamp/nativewindow/NativeWindowFactory.java b/src/nativewindow/classes/com/jogamp/nativewindow/NativeWindowFactory.java index ebd498401..323bc8c86 100644 --- a/src/nativewindow/classes/com/jogamp/nativewindow/NativeWindowFactory.java +++ b/src/nativewindow/classes/com/jogamp/nativewindow/NativeWindowFactory.java @@ -42,6 +42,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import com.jogamp.nativewindow.util.Point; import com.jogamp.nativewindow.util.PointImmutable; import jogamp.common.os.PlatformPropsImpl; @@ -733,7 +734,7 @@ public abstract class NativeWindowFactory { * FIXME: Bug 973 Needs service provider interface (SPI) for TK dependent implementation * </p> */ - public static PointImmutable getLocationOnScreen(final NativeWindow nw) { + public static Point getLocationOnScreen(final NativeWindow nw) { final String nwt = NativeWindowFactory.getNativeWindowType(true); if( NativeWindowFactory.TYPE_X11 == nwt ) { return X11Lib.GetRelativeLocation(nw.getDisplayHandle(), nw.getScreenIndex(), nw.getWindowHandle(), 0, 0, 0); diff --git a/src/nativewindow/classes/com/jogamp/nativewindow/javafx/JFXAccessor.java b/src/nativewindow/classes/com/jogamp/nativewindow/javafx/JFXAccessor.java new file mode 100644 index 000000000..12cd9bdc0 --- /dev/null +++ b/src/nativewindow/classes/com/jogamp/nativewindow/javafx/JFXAccessor.java @@ -0,0 +1,293 @@ +/** + * 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.nativewindow.javafx; + +import java.lang.reflect.Method; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import com.jogamp.nativewindow.AbstractGraphicsDevice; +import com.jogamp.nativewindow.AbstractGraphicsScreen; +import com.jogamp.nativewindow.NativeWindowException; +import com.jogamp.nativewindow.NativeWindowFactory; +import com.jogamp.nativewindow.VisualIDHolder; +import com.jogamp.nativewindow.macosx.MacOSXGraphicsDevice; +import com.jogamp.nativewindow.windows.WindowsGraphicsDevice; +import com.jogamp.nativewindow.x11.X11GraphicsDevice; +import com.jogamp.common.ExceptionUtils; +import com.jogamp.common.util.InterruptedRuntimeException; +import com.jogamp.common.util.ReflectionUtil; +import com.jogamp.common.util.RunnableTask; +import com.sun.javafx.tk.TKStage; + +import javafx.application.Platform; +import javafx.stage.Window; +import jogamp.nativewindow.Debug; +import jogamp.nativewindow.x11.X11Lib; +import jogamp.nativewindow.x11.X11Util; + +public class JFXAccessor { + private static final boolean DEBUG; + + private static final boolean jfxAvailable; + private static final Method fxUserThreadGetter; + private static final Method tkStageGetter; + private static final Method glassWindowGetter; + private static final Method nativeWindowGetter; + + private static final String nwt; + private static final boolean isOSX; + private static final boolean isWindows; + private static final boolean isX11; + + static { + final boolean[] _DEBUG = new boolean[] { true }; + + final Method[] res = AccessController.doPrivileged(new PrivilegedAction<Method[]>() { + @Override + public Method[] run() { + NativeWindowFactory.initSingleton(); // last resort .. + final Method[] res = new Method[] { null, null, null, null }; + try { + int i=0; + _DEBUG[0] = Debug.debug("JFX"); + /** + * com.sun.javafx.tk.Toolkit + */ + final Class<?> jfxToolkitClz = ReflectionUtil.getClass("com.sun.javafx.tk.Toolkit", false, JFXAccessor.class.getClassLoader()); + res[i] = jfxToolkitClz.getDeclaredMethod("getFxUserThread"); + res[i++].setAccessible(true); + + /*** + * class javafx.stage.Window + * class javafx.stage.Stage extends javafx.stage.Window + * class com.sun.javafx.tk.quantum.WindowStage extends com.sun.javafx.tk.quantum.GlassStage implements com.sun.javafx.tk.TKStage + * abstract com.sun.glass.ui.Window + * + * javafx.stage.Window: com.sun.javafx.tk.TKStage [impl_]getPeer() + * com.sun.javafx.tk.quantum.WindowStage: final com.sun.glass.ui.Window getPlatformWindow() + * com.sun.glass.ui.Window: public long getNativeWindow() + */ + final Class<?> jfxStageWindowClz = ReflectionUtil.getClass("javafx.stage.Window", false, JFXAccessor.class.getClassLoader()); + // final Class<?> jfxTkTKStageClz = ReflectionUtil.getClass("com.sun.javafx.tk.TKStage", false, JFXAccessor.class.getClassLoader()); + final Class<?> jfxTkQuWindowStageClz = ReflectionUtil.getClass("com.sun.javafx.tk.quantum.WindowStage", false, JFXAccessor.class.getClassLoader()); + final Class<?> jfxGlassUiWindowClz = ReflectionUtil.getClass("com.sun.glass.ui.Window", false, JFXAccessor.class.getClassLoader()); + + try { + // jfx 9, 11, 12, .. + res[i] = jfxStageWindowClz.getDeclaredMethod("getPeer"); + } catch (final NoSuchMethodException ex) { + // jfx 8 + res[i] = jfxStageWindowClz.getDeclaredMethod("impl_getPeer"); + } + res[i++].setAccessible(true); + + res[i] = jfxTkQuWindowStageClz.getDeclaredMethod("getPlatformWindow"); + res[i++].setAccessible(true); + res[i] = jfxGlassUiWindowClz.getDeclaredMethod("getNativeWindow"); + res[i++].setAccessible(true); + } catch (final Throwable t) { + if(_DEBUG[0]) { + ExceptionUtils.dumpThrowable("jfx-init", t); + } + } + return res; + } + }); + { + int i=0; + fxUserThreadGetter = res[i++]; + tkStageGetter = res[i++]; + glassWindowGetter = res[i++]; + nativeWindowGetter = res[i++]; + } + jfxAvailable = null != fxUserThreadGetter && null != tkStageGetter && null != glassWindowGetter && null != nativeWindowGetter; + + nwt = NativeWindowFactory.getNativeWindowType(false); + isOSX = NativeWindowFactory.TYPE_MACOSX == nwt; + isWindows = NativeWindowFactory.TYPE_WINDOWS == nwt; + isX11 = NativeWindowFactory.TYPE_X11 == nwt; + + DEBUG = _DEBUG[0]; + if(DEBUG) { + System.err.println(Thread.currentThread().getName()+" - Info: JFXAccessor.<init> available "+jfxAvailable+", nwt "+nwt+"( x11 "+isX11+", win "+isWindows+", osx "+isOSX+")"); + } + } + + // + // Common any toolkit + // + + public static boolean isJFXAvailable() { return jfxAvailable; } + + /** + * Runs given {@code task} on the JFX Thread if it has not stopped and if caller is not already on the JFX Thread, + * otherwise execute given {@code task} on the current thread. + * @param wait + * @param task + * @see #isJFXThreadOrHasJFXThreadStopped() + */ + public static void runOnJFXThread(final boolean wait, final Runnable task) { + final Object rTaskLock = new Object(); + synchronized(rTaskLock) { // lock the task execution + if( isJFXThreadOrHasJFXThreadStopped() ) { + task.run(); + } else if( !wait ) { + Platform.runLater(task); + } else { + final RunnableTask rTask = new RunnableTask(task, + wait ? rTaskLock : null, + true /* always catch and report Exceptions, don't disturb EDT */, + wait ? null : System.err); + Platform.runLater(rTask); + 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); + } + } + } + } + } + + public static Thread getJFXThread() throws NativeWindowException { + try { + return (Thread) fxUserThreadGetter.invoke(null); + } catch (final Throwable e) { + throw new NativeWindowException("Error getting JFX-Thread", e); + } + } + public static String getJFXThreadName() { + final Thread t = getJFXThread(); + return null != t ? t.getName() : null; + } + /** + * @return true if the JFX Thread has stopped + */ + public static boolean hasJFXThreadStopped() { + final Thread t = getJFXThread(); + return null == t || !t.isAlive(); + } + /** + * @return true if caller is on the JFX Thread + */ + public static boolean isJFXThread() { + final Thread t = getJFXThread(); + return Thread.currentThread() == t; + } + /** + * @return true if the JFX Thread has stopped or if caller is on the JFX Thread + */ + public static boolean isJFXThreadOrHasJFXThreadStopped() { + final Thread t = getJFXThread(); + return null == t || !t.isAlive() || Thread.currentThread() == t; + } + + /** + * @param swtControl the SWT Control to retrieve the native device handle from + * @return the AbstractGraphicsDevice w/ the native device handle + * @throws NativeWindowException if the widget handle is null + * @throws UnsupportedOperationException if the windowing system is not supported + */ + public static AbstractGraphicsDevice getDevice(final Window stageWindow) throws NativeWindowException, UnsupportedOperationException { + if( isX11 ) { + // Decoupled X11 Device/Screen allowing X11 display lock-free off-thread rendering + final String connection = null; + final long x11DeviceHandle = X11Util.openDisplay(connection); + if( 0 == x11DeviceHandle ) { + throw new RuntimeException("Error creating display: "+connection); + } + return new X11GraphicsDevice(x11DeviceHandle, AbstractGraphicsDevice.DEFAULT_UNIT, true /* owner */); + } + if( isWindows ) { + return new WindowsGraphicsDevice(AbstractGraphicsDevice.DEFAULT_UNIT); + } + if( isOSX ) { + return new MacOSXGraphicsDevice(AbstractGraphicsDevice.DEFAULT_UNIT); + } + throw new UnsupportedOperationException("n/a for this windowing system: "+nwt); + } + + /** + * @param device + * @param screen -1 is default screen of the given device, e.g. maybe 0 or determined by native API. >= 0 is specific screen + * @return + */ + public static AbstractGraphicsScreen getScreen(final AbstractGraphicsDevice device, final int screen) { + return NativeWindowFactory.createScreen(device, screen); + } + + public static int getNativeVisualID(final AbstractGraphicsDevice device, final long windowHandle) { + if( isX11 ) { + return X11Lib.GetVisualIDFromWindow(device.getHandle(), windowHandle); + } + if( isWindows || isOSX ) { + return VisualIDHolder.VID_UNDEFINED; + } + throw new UnsupportedOperationException("n/a for this windowing system: "+nwt); + } + + /** + * @param swtControl the SWT Control to retrieve the native widget-handle from + * @return the native widget-handle + * @throws NativeWindowException if the widget handle is null + */ + public static long getWindowHandle(final Window stageWindow) throws NativeWindowException { + final long h[] = { 0 }; + runOnJFXThread(true, new Runnable() { + public void run() { + try { + final TKStage tkStage = (TKStage) tkStageGetter.invoke(stageWindow); + if( null != tkStage ) { + final Object platformWindow = glassWindowGetter.invoke(tkStage); + if( null != platformWindow ) { + final Object nativeHandle = nativeWindowGetter.invoke(platformWindow); + h[0] = ((Long) nativeHandle).longValue(); + } else if(DEBUG) { + System.err.println(Thread.currentThread().getName()+" - Info: JFXAccessor null GlassWindow"); + } + } else if(DEBUG) { + System.err.println(Thread.currentThread().getName()+" - Info: JFXAccessor null TKStage"); + } + } catch (final Throwable e) { + throw new NativeWindowException("Error getting Window handle", e); + } + } }); + return h[0]; + } +} diff --git a/src/nativewindow/classes/com/jogamp/nativewindow/util/Point.java b/src/nativewindow/classes/com/jogamp/nativewindow/util/Point.java index fc5465bbf..aa511b625 100644 --- a/src/nativewindow/classes/com/jogamp/nativewindow/util/Point.java +++ b/src/nativewindow/classes/com/jogamp/nativewindow/util/Point.java @@ -120,6 +120,18 @@ public class Point implements Cloneable, PointImmutable { /** * Translate this instance's x- and y-components, + * i.e. add the values of the given delta point to them. + * @param pd delta point + * @return this instance for scaling + */ + public final Point translate(final PointImmutable pd) { + x += pd.getX() ; + y += pd.getY() ; + return this; + } + + /** + * Translate this instance's x- and y-components, * i.e. add the given deltas to them. * @param dx delta for x * @param dy delta for y diff --git a/src/newt/classes/com/jogamp/newt/javafx/NewtCanvasJFX.java b/src/newt/classes/com/jogamp/newt/javafx/NewtCanvasJFX.java new file mode 100644 index 000000000..1fe3fa9ba --- /dev/null +++ b/src/newt/classes/com/jogamp/newt/javafx/NewtCanvasJFX.java @@ -0,0 +1,637 @@ +/** + * 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.newt.javafx; + +import com.jogamp.nativewindow.AbstractGraphicsConfiguration; +import com.jogamp.nativewindow.AbstractGraphicsScreen; +import com.jogamp.nativewindow.Capabilities; +import com.jogamp.nativewindow.CapabilitiesImmutable; +import com.jogamp.nativewindow.GraphicsConfigurationFactory; +import com.jogamp.nativewindow.NativeSurface; +import com.jogamp.nativewindow.NativeWindow; +import com.jogamp.nativewindow.NativeWindowException; +import com.jogamp.nativewindow.NativeWindowFactory; +import com.jogamp.nativewindow.SurfaceUpdatedListener; +import com.jogamp.nativewindow.WindowClosingProtocol; +import com.jogamp.nativewindow.util.Insets; +import com.jogamp.nativewindow.util.InsetsImmutable; +import com.jogamp.nativewindow.util.Point; +import com.jogamp.nativewindow.util.Rectangle; +import com.jogamp.opengl.GLCapabilities; + +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.event.EventHandler; +import javafx.geometry.Bounds; +import javafx.scene.Scene; +import javafx.scene.canvas.Canvas; +import jogamp.newt.Debug; +import jogamp.newt.javafx.JFXEDTUtil; + +import com.jogamp.nativewindow.javafx.JFXAccessor; +import com.jogamp.newt.Display; +import com.jogamp.newt.Window; +import com.jogamp.newt.event.WindowEvent; +import com.jogamp.newt.util.EDTUtil; + +/** + * JFX {@link Canvas} containing a NEWT {@link Window} using native parenting. + * <p> + * Implementation allows use of custom {@link GLCapabilities}. + * </p> + */ +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<javafx.stage.WindowEvent> windowClosingListener = new EventHandler<javafx.stage.WindowEvent>() { + public final void handle(final javafx.stage.WindowEvent e) { + if( DEBUG ) { + System.err.println("NewtCanvasJFX.Event.DISPOSE, "+e); + } + NewtCanvasJFX.this.dispose(); + } }; + private final EventHandler<javafx.stage.WindowEvent> windowShownListener = new EventHandler<javafx.stage.WindowEvent>() { + public final void handle(final javafx.stage.WindowEvent e) { + if( DEBUG ) { + System.err.println("NewtCanvasJFX.Event.SHOWN, "+e); + } + repaintAction(true); + } }; + + /** + * Instantiates a NewtCanvas with a NEWT child. + * + * <p> + * Note: The NEWT child {@link Display}'s {@link EDTUtil} is being set to an JFX conform implementation + * via {@link Display#setEDTUtil(EDTUtil)}. + * </p> + * @param child optional preassigned {@link #Window}, maybe null + */ + public NewtCanvasJFX(final Window child) { + super(); + + updateParentWindowAndScreen(); + + this.widthProperty().addListener(new ChangeListener<Number>() { + @Override public void changed(final ObservableValue<? extends Number> observable, final Number oldValue, final Number newValue) { + if( DEBUG ) { + System.err.println("NewtCanvasJFX.Event.Width, "+oldValue.doubleValue()+" -> "+newValue.doubleValue()+", has "+getWidth()); + } + updateSizeCheck(newValue.intValue(), (int)getHeight()); + repaintAction(isVisible()); + } + }); + this.heightProperty().addListener(new ChangeListener<Number>() { + @Override public void changed(final ObservableValue<? extends Number> observable, final Number oldValue, final Number newValue) { + if( DEBUG ) { + System.err.println("NewtCanvasJFX.Event.Height, "+oldValue.doubleValue()+" -> "+newValue.doubleValue()+", has "+getHeight()); + } + updateSizeCheck((int)getWidth(), newValue.intValue()); + repaintAction(isVisible()); + } + }); + this.visibleProperty().addListener(new ChangeListener<Boolean>() { + @Override public void changed(final ObservableValue<? extends Boolean> observable, final Boolean oldValue, final Boolean newValue) { + if( DEBUG ) { + System.err.println("NewtCanvasJFX.Event.Visible, "+oldValue.booleanValue()+" -> "+newValue.booleanValue()+", has "+isVisible()); + } + repaintAction(newValue.booleanValue()); + } + }); + this.sceneProperty().addListener(new ChangeListener<Scene>() { + @Override public void changed(final ObservableValue<? extends Scene> observable, final Scene oldValue, final Scene newValue) { + if( DEBUG ) { + System.err.println("NewtCanvasJFX.Event.Scene, "+oldValue+" -> "+newValue+", has "+getScene()); + if(null != newValue) { + final javafx.stage.Window w = newValue.getWindow(); + System.err.println("NewtCanvasJFX.Event.Scene window "+w+" (showing "+(null!=w?w.isShowing():0)+")"); + } + } + if( updateParentWindowAndScreen() ) { + repaintAction(isVisible()); + } + } + }); + + if(null != child) { + setNEWTChild(child); + } + } + + private final void repaintAction(final boolean visible) { + if( visible && ( null != nativeWindow || validateNative(true /* completeReparent */) ) ) { + if( newtChildReady ) { + if( postSetSize ) { + newtChild.setSize(clientArea.getWidth(), clientArea.getHeight()); + postSetSize = false; + } + if( postSetPos ) { + newtChild.setPosition(clientArea.getX(), clientArea.getY()); + postSetPos = false; + } + newtChild.windowRepaint(0, 0, clientArea.getWidth(), clientArea.getHeight()); + } + } + } + + private final void updatePosSizeCheck() { + final Bounds b = localToScene(getBoundsInLocal()); + updatePosCheck((int)b.getMinX(), (int)b.getMinY()); + updateSizeCheck((int)getWidth(), (int)getHeight()); + } + private final void updatePosCheck(final int newX, final int newY) { + final boolean posChanged; + { + final Rectangle oClientArea = clientArea; + posChanged = newX != oClientArea.getX() || newY != oClientArea.getY(); + if( posChanged ) { + clientArea.setX(newX); + clientArea.setY(newY); + } + } + if(DEBUG) { + final long nsh = newtChildReady ? newtChild.getSurfaceHandle() : 0; + System.err.println("NewtCanvasJFX.updatePosCheck: posChanged "+posChanged+", ("+Thread.currentThread().getName()+"): newtChildReady "+newtChildReady+", "+clientArea.getX()+"/"+clientArea.getY()+" "+clientArea.getWidth()+"x"+clientArea.getHeight()+" - surfaceHandle 0x"+Long.toHexString(nsh)); + } + if( posChanged ) { + if( newtChildReady ) { + newtChild.setPosition(clientArea.getX(), clientArea.getY()); + } else { + postSetPos = true; + } + } + } + private final void updateSizeCheck(final int newWidth, final int newHeight) { + final boolean sizeChanged; + { + final Rectangle oClientArea = clientArea; + sizeChanged = newWidth != oClientArea.getWidth() || newHeight != oClientArea.getHeight(); + if( sizeChanged ) { + clientArea.setWidth(newWidth); + clientArea.setHeight(newHeight); + } + } + if(DEBUG) { + final long nsh = newtChildReady ? newtChild.getSurfaceHandle() : 0; + System.err.println("NewtCanvasJFX.updateSizeCheck: sizeChanged "+sizeChanged+", ("+Thread.currentThread().getName()+"): newtChildReady "+newtChildReady+", "+clientArea.getX()+"/"+clientArea.getY()+" "+clientArea.getWidth()+"x"+clientArea.getHeight()+" - surfaceHandle 0x"+Long.toHexString(nsh)); + } + if( sizeChanged ) { + if( newtChildReady ) { + newtChild.setSize(clientArea.getWidth(), clientArea.getHeight()); + } else { + postSetSize = true; + } + } + } + + private final ChangeListener<javafx.stage.Window> sceneWindowChangeListener = new ChangeListener<javafx.stage.Window>() { + @Override public void changed(final ObservableValue<? extends javafx.stage.Window> observable, final javafx.stage.Window oldValue, final javafx.stage.Window newValue) { + if( DEBUG ) { + System.err.println("NewtCanvasJFX.Event.Window, "+oldValue+" -> "+newValue); + } + if( updateParentWindowAndScreen() ) { + repaintAction(isVisible()); + } + } }; + + private boolean updateParentWindowAndScreen() { + final Scene s = this.getScene(); + if( null != s ) { + final javafx.stage.Window w = s.getWindow(); + if( DEBUG ) { + System.err.println("NewtCanvasJFX.updateParentWindowAndScreen: Scene "+s+", Window "+w+" (showing "+(null!=w?w.isShowing():0)+")"); + } + if( w != parentWindow ) { + disposeImpl(false); + } + parentWindow = w; + if( null != w ) { + screen = JFXAccessor.getScreen(JFXAccessor.getDevice(parentWindow), -1 /* default */); + parentWindow.addEventHandler(javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST, windowClosingListener); + parentWindow.addEventHandler(javafx.stage.WindowEvent.WINDOW_SHOWN, windowShownListener); + return true; + } else { + s.windowProperty().addListener(sceneWindowChangeListener); + } + } else { + if( DEBUG ) { + System.err.println("NewtCanvasJFX.updateParentWindowAndScreen: Null Scene"); + } + if( null != parentWindow ) { + disposeImpl(false); + } + } + return false; + } + + /** + * Destroys this resource: + * <ul> + * <li> Make the NEWT Child invisible </li> + * <li> Disconnects the NEWT Child from this Canvas NativeWindow, reparent to NULL </li> + * <li> Issues <code>destroy()</code> on the NEWT Child</li> + * <li> Remove reference to the NEWT Child</li> + * </ul> + * @see Window#destroy() + */ + // FIXME JFX similar @Override + public void dispose() { + disposeImpl(true); + } + private void disposeImpl(final boolean disposeNewtChild) { + if(DEBUG) { + System.err.println("NewtCanvasJFX.dispose: (has parent "+(null!=parentWindow)+", hasNative "+(null!=nativeWindow)+",\n\t"+newtChild); + } + if( null != newtChild ) { + if(DEBUG) { + System.err.println("NewtCanvasJFX.dispose.1: EDTUtil cur "+newtChild.getScreen().getDisplay().getEDTUtil()); + } + if( null != nativeWindow ) { + configureNewtChild(false); + newtChild.setVisible(false); + newtChild.reparentWindow(null, -1, -1, 0 /* hint */); + } + if( disposeNewtChild ) { + newtChild.destroy(); + newtChild = null; + } + } + if( null != parentWindow ) { + parentWindow.getScene().windowProperty().removeListener(sceneWindowChangeListener); + parentWindow.removeEventHandler(javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST, windowClosingListener); + parentWindow.removeEventHandler(javafx.stage.WindowEvent.WINDOW_SHOWN, windowShownListener); + parentWindow = null; + } + if( null != screen ) { + screen.getDevice().close(); + screen = null; + } + nativeWindow = null; + } + + private final boolean validateNative(final boolean completeReparent) { + if( null == parentWindow ) { + return false; + } + assert null == nativeWindow; + updatePosSizeCheck(); + if(0 >= clientArea.getWidth() || 0 >= clientArea.getHeight()) { + return false; + } + final long nativeWindowHandle = JFXAccessor.getWindowHandle(parentWindow); + if( 0 == nativeWindowHandle ) { + return false; + } + screen.getDevice().open(); + + /* Native handle for the control, used to associate with GLContext */ + final int visualID = JFXAccessor.getNativeVisualID(screen.getDevice(), nativeWindowHandle); + final boolean visualIDValid = NativeWindowFactory.isNativeVisualIDValidForProcessing(visualID); + if(DEBUG) { + System.err.println("NewtCanvasJFX.validateNative() windowHandle 0x"+Long.toHexString(nativeWindowHandle)+", visualID 0x"+Integer.toHexString(visualID)+", valid "+visualIDValid); + } + if( visualIDValid ) { + /* Get the nativewindow-Graphics Device associated with this control (which is determined by the parent Composite). + * Note: JFX is owner of the native handle, hence no closing operation will be a NOP. */ + final CapabilitiesImmutable caps = new Capabilities(); + final GraphicsConfigurationFactory factory = GraphicsConfigurationFactory.getFactory(screen.getDevice(), caps); + final AbstractGraphicsConfiguration config = factory.chooseGraphicsConfiguration( caps, caps, null, screen, visualID ); + if(DEBUG) { + System.err.println("NewtCanvasJFX.validateNative() factory: "+factory+", windowHandle 0x"+Long.toHexString(nativeWindowHandle)+", visualID 0x"+Integer.toHexString(visualID)+", chosen config: "+config); + // Thread.dumpStack(); + } + if (null == config) { + throw new NativeWindowException("Error choosing GraphicsConfiguration creating window: "+this); + } + + nativeWindow = new JFXNativeWindow(config, nativeWindowHandle); + if( completeReparent ) { + reparentWindow( true ); + } + } + + return null != nativeWindow; + } + + /** + * Sets a new NEWT child, provoking reparenting. + * <p> + * A previously detached <code>newChild</code> will be released to top-level status + * and made invisible. + * </p> + * <p> + * Note: When switching NEWT child's, detaching the previous first via <code>setNEWTChild(null)</code> + * produced much cleaner visual results. + * </p> + * <p> + * Note: The NEWT child {@link Display}'s {@link EDTUtil} is being set to an JFX conform implementation + * via {@link Display#setEDTUtil(EDTUtil)}. + * </p> + * @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. + * <p> + * Uses JOGL's NewtCanvasSWT, which allows to be a native container of a NEWT Window.<br/> + * This method allows utilizing custom GLCapability settings, + * independent from the already instantiated SWT visual. + * </p> + * <p> + * 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. + * </p> + */ +@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<args.length; i++) { + if(args[i].equals("-time")) { + duration = MiscUtils.atoi(args[++i], duration); + } + } + System.out.println("durationPerTest: "+duration); + org.junit.runner.JUnitCore.main(TestNewtCanvasJFXGLn.class.getName()); + } +} |