/* * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.javafx.application; import com.sun.javafx.PlatformUtil; import com.sun.javafx.css.StyleManager; import com.sun.javafx.runtime.SystemProperties; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.security.AccessControlContext; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import javafx.application.Application; import javafx.application.ConditionalFeature; import com.sun.javafx.tk.TKListener; import com.sun.javafx.tk.TKStage; import com.sun.javafx.tk.Toolkit; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.scene.Scene; import java.security.AccessController; import java.security.AllPermission; import java.security.PrivilegedAction; public class PlatformImpl { private static AtomicBoolean initialized = new AtomicBoolean(false); private static AtomicBoolean platformExit = new AtomicBoolean(false); private static AtomicBoolean toolkitExit = new AtomicBoolean(false); private static CountDownLatch startupLatch = new CountDownLatch(1); private static AtomicBoolean listenersRegistered = new AtomicBoolean(false); private static TKListener toolkitListener = null; private static volatile boolean implicitExit = true; private static boolean taskbarApplication = true; private static boolean contextual2DNavigation; private static AtomicInteger pendingRunnables = new AtomicInteger(0); private static AtomicInteger numWindows = new AtomicInteger(0); private static volatile boolean firstWindowShown = false; private static volatile boolean lastWindowClosed = false; private static AtomicBoolean reallyIdle = new AtomicBoolean(false); private static Set finishListeners = new CopyOnWriteArraySet(); private final static Object runLaterLock = new Object(); private static Boolean isGraphicsSupported; private static Boolean isControlsSupported; private static Boolean isMediaSupported; private static Boolean isWebSupported; private static Boolean isSWTSupported; private static Boolean isSwingSupported; private static Boolean isFXMLSupported; private static Boolean hasTwoLevelFocus; private static Boolean hasVirtualKeyboard; private static Boolean hasTouch; private static Boolean hasMultiTouch; private static Boolean hasPointer; private static boolean isThreadMerged = false; private static BooleanProperty accessibilityActive = new SimpleBooleanProperty(); /** * Set a flag indicating whether this application should show up in the * task bar. The default value is true. * * @param taskbarApplication the new value of this attribute */ public static void setTaskbarApplication(boolean taskbarApplication) { PlatformImpl.taskbarApplication = taskbarApplication; } /** * Returns the current value of the taskBarApplication flag. * * @return the current state of the flag. */ public static boolean isTaskbarApplication() { return taskbarApplication; } /** * Sets the name of the this application based on the Application class. * This method is called by the launcher or by the deploy code, and is not * called from the FX Application Thread, so we need to do it in a runLater. * We do not need to wait for the result since it will complete before the * Application start() method is called regardless. * * @param appClass the Application class. */ public static void setApplicationName(final Class appClass) { runLater(() -> com.sun.glass.ui.Application.GetApplication().setName(appClass.getName())); } /** * Return whether or not focus navigation between controls is context- * sensitive. * @return true if the context-sensitive algorithm for focus navigation is * used */ public static boolean isContextual2DNavigation() { return contextual2DNavigation; } /** * This method is invoked typically on the main thread. At this point, * the JavaFX Application Thread has not been started. Any attempt * to call startup twice results in an exception. * @param r */ public static void startup(final Runnable r) { // NOTE: if we ever support re-launching an application and/or // launching a second application in the same VM/classloader // this will need to be changed. if (platformExit.get()) { throw new IllegalStateException("Platform.exit has been called"); } if (initialized.getAndSet(true)) { // If we've already initialized, just put the runnable on the queue. runLater(r); return; } AccessController.doPrivileged((PrivilegedAction) () -> { contextual2DNavigation = Boolean.getBoolean( "com.sun.javafx.isContextual2DNavigation"); String s = System.getProperty("com.sun.javafx.twoLevelFocus"); if (s != null) { hasTwoLevelFocus = Boolean.valueOf(s); } s = System.getProperty("com.sun.javafx.virtualKeyboard"); if (s != null) { if (s.equalsIgnoreCase("none")) { hasVirtualKeyboard = false; } else if (s.equalsIgnoreCase("javafx")) { hasVirtualKeyboard = true; } else if (s.equalsIgnoreCase("native")) { hasVirtualKeyboard = true; } } s = System.getProperty("com.sun.javafx.touch"); if (s != null) { hasTouch = Boolean.valueOf(s); } s = System.getProperty("com.sun.javafx.multiTouch"); if (s != null) { hasMultiTouch = Boolean.valueOf(s); } s = System.getProperty("com.sun.javafx.pointer"); if (s != null) { hasPointer = Boolean.valueOf(s); } s = System.getProperty("javafx.embed.singleThread"); if (s != null) { isThreadMerged = Boolean.valueOf(s); } return null; }); if (!taskbarApplication) { AccessController.doPrivileged((PrivilegedAction) () -> { System.setProperty("glass.taskbarApplication", "false"); return null; }); } // Create Toolkit listener and register it with the Toolkit. // Call notifyFinishListeners when we get notified. toolkitListener = new TKListener() { @Override public void changedTopLevelWindows(List windows) { numWindows.set(windows.size()); checkIdle(); } @Override public void exitedLastNestedLoop() { checkIdle(); } }; Toolkit.getToolkit().addTkListener(toolkitListener); Toolkit.getToolkit().startup(() -> { startupLatch.countDown(); r.run(); }); //Initialize the thread merging mechanism if (isThreadMerged) { installFwEventQueue(); } } private static void installFwEventQueue() { invokeSwingFXUtilsMethod("installFwEventQueue"); } private static void removeFwEventQueue() { invokeSwingFXUtilsMethod("removeFwEventQueue"); } private static void invokeSwingFXUtilsMethod(final String methodName) { //Use reflection in case we are running compact profile try { Class swingFXUtilsClass = Class.forName("javafx.embed.swing.SwingFXUtils"); Method installFwEventQueue = swingFXUtilsClass.getDeclaredMethod(methodName); AccessController.doPrivileged((PrivilegedAction) () -> { installFwEventQueue.setAccessible(true); return null; }); waitForStart(); installFwEventQueue.invoke(null); } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { throw new RuntimeException("Property javafx.embed.singleThread is not supported"); } catch (InvocationTargetException e) { throw new RuntimeException(e); } } private static void waitForStart() { // If the startup runnable has not yet been called, then wait it. // Note that we check the count before calling await() to avoid // the try/catch which is unnecessary after startup. if (startupLatch.getCount() > 0) { try { startupLatch.await(); } catch (InterruptedException ex) { ex.printStackTrace(); } } } public static boolean isFxApplicationThread() { return Toolkit.getToolkit().isFxUserThread(); } public static void runLater(final Runnable r) { runLater(r, false); } private static void runLater(final Runnable r, boolean exiting) { if (!initialized.get()) { throw new IllegalStateException("Toolkit not initialized"); } pendingRunnables.incrementAndGet(); waitForStart(); if (SystemProperties.isDebug()) { Toolkit.getToolkit().pauseCurrentThread(); } synchronized (runLaterLock) { if (!exiting && toolkitExit.get()) { // Don't schedule a runnable after we have exited the toolkit pendingRunnables.decrementAndGet(); return; } final AccessControlContext acc = AccessController.getContext(); // Don't catch exceptions, they are handled by Toolkit.defer() Toolkit.getToolkit().defer(() -> { try { AccessController.doPrivileged((PrivilegedAction) () -> { r.run(); return null; }, acc); } finally { pendingRunnables.decrementAndGet(); checkIdle(); } }); } } public static void runAndWait(final Runnable r) { runAndWait(r, false); } private static void runAndWait(final Runnable r, boolean exiting) { if (SystemProperties.isDebug()) { Toolkit.getToolkit().pauseCurrentThread(); } if (isFxApplicationThread()) { try { r.run(); } catch (Throwable t) { System.err.println("Exception in runnable"); t.printStackTrace(); } } else { final CountDownLatch doneLatch = new CountDownLatch(1); runLater(() -> { try { r.run(); } finally { doneLatch.countDown(); } }, exiting); if (!exiting && toolkitExit.get()) { throw new IllegalStateException("Toolkit has exited"); } try { doneLatch.await(); } catch (InterruptedException ex) { ex.printStackTrace(); } } } public static void setImplicitExit(boolean implicitExit) { PlatformImpl.implicitExit = implicitExit; checkIdle(); } public static boolean isImplicitExit() { return implicitExit; } public static void addListener(FinishListener l) { listenersRegistered.set(true); finishListeners.add(l); } public static void removeListener(FinishListener l) { finishListeners.remove(l); listenersRegistered.set(!finishListeners.isEmpty()); if (!listenersRegistered.get()) { checkIdle(); } } private static void notifyFinishListeners(boolean exitCalled) { // Notify listeners if any are registered, else exit directly if (listenersRegistered.get()) { for (FinishListener l : finishListeners) { if (exitCalled) { l.exitCalled(); } else { l.idle(implicitExit); } } } else if (implicitExit || platformExit.get()) { tkExit(); } } // Check for idle, meaning the last top-level window has been closed and // there are no pending Runnables waiting to be run. private static void checkIdle() { // If we aren't initialized yet, then this method is a no-op. if (!initialized.get()) { return; } if (!isFxApplicationThread()) { // Add a dummy runnable to the runLater queue, which will then call // checkIdle() on the FX application thread. runLater(() -> { }); return; } boolean doNotify = false; synchronized (PlatformImpl.class) { int numWin = numWindows.get(); if (numWin > 0) { firstWindowShown = true; lastWindowClosed = false; reallyIdle.set(false); } else if (numWin == 0 && firstWindowShown) { lastWindowClosed = true; } // In case there is an event in process, allow for it to show // another window. If no new window is shown before all pending // runnables (including this one) are done and there is no running // nested loops, then we will shutdown. if (lastWindowClosed && pendingRunnables.get() == 0 && (toolkitExit.get() || !Toolkit.getToolkit().isNestedLoopRunning())) { // System.err.println("Last window closed and no pending runnables"); if (reallyIdle.getAndSet(true)) { // System.err.println("Really idle now"); doNotify = true; lastWindowClosed = false; } else { // System.err.println("Queuing up a dummy idle check runnable"); runLater(() -> { // System.err.println("Dummy runnable"); }); } } } if (doNotify) { notifyFinishListeners(false); } } // package scope method for testing private static final CountDownLatch platformExitLatch = new CountDownLatch(1); static CountDownLatch test_getPlatformExitLatch() { return platformExitLatch; } public static void tkExit() { if (toolkitExit.getAndSet(true)) { return; } if (initialized.get()) { // Always call toolkit exit on FX app thread // System.err.println("PlatformImpl.tkExit: scheduling Toolkit.exit"); PlatformImpl.runAndWait(() -> { // System.err.println("PlatformImpl.tkExit: calling Toolkit.exit"); Toolkit.getToolkit().exit(); }, true); if (isThreadMerged) { removeFwEventQueue(); } Toolkit.getToolkit().removeTkListener(toolkitListener); toolkitListener = null; platformExitLatch.countDown(); } } public static BooleanProperty accessibilityActiveProperty() { return accessibilityActive; } public static void exit() { platformExit.set(true); notifyFinishListeners(true); } private static Boolean checkForClass(String classname) { try { Class.forName(classname, false, PlatformImpl.class.getClassLoader()); return Boolean.TRUE; } catch (ClassNotFoundException cnfe) { return Boolean.FALSE; } } public static boolean isSupported(ConditionalFeature feature) { final boolean supported = isSupportedImpl(feature); if (supported && (feature == ConditionalFeature.TRANSPARENT_WINDOW)) { // some features require the application to have the corresponding // permissions, if the application doesn't have them, the platform // will behave as if the feature wasn't supported final SecurityManager securityManager = System.getSecurityManager(); if (securityManager != null) { try { securityManager.checkPermission(new AllPermission()); } catch (final SecurityException e) { return false; } } return true; } return supported; } public static interface FinishListener { public void idle(boolean implicitExit); public void exitCalled(); } /** * Set the platform user agent stylesheet to the default. */ public static void setDefaultPlatformUserAgentStylesheet() { setPlatformUserAgentStylesheet(Application.STYLESHEET_MODENA); } private static boolean isModena = false; private static boolean isCaspian = false; /** * Current Platform User Agent Stylesheet is Modena. * * Note: Please think hard before using this as we really want to avoid special cases in the platform for specific * themes. This was added to allow tempory work arounds in the platform for bugs. * * @return true if using modena stylesheet */ public static boolean isModena() { return isModena; } /** * Current Platform User Agent Stylesheet is Caspian. * * Note: Please think hard before using this as we really want to avoid special cases in the platform for specific * themes. This was added to allow tempory work arounds in the platform for bugs. * * @return true if using caspian stylesheet */ public static boolean isCaspian() { return isCaspian; } /** * Set the platform user agent stylesheet to the given URL. This method has special handling for platform theme * name constants. */ public static void setPlatformUserAgentStylesheet(final String stylesheetUrl) { if (isFxApplicationThread()) { _setPlatformUserAgentStylesheet(stylesheetUrl); } else { runLater(() -> _setPlatformUserAgentStylesheet(stylesheetUrl)); } } private static String accessibilityTheme; public static boolean setAccessibilityTheme(String platformTheme) { if (accessibilityTheme != null) { StyleManager.getInstance().removeUserAgentStylesheet(accessibilityTheme); accessibilityTheme = null; } _setAccessibilityTheme(platformTheme); if (accessibilityTheme != null) { StyleManager.getInstance().addUserAgentStylesheet(accessibilityTheme); return true; } return false; } private static void _setAccessibilityTheme(String platformTheme) { // check to see if there is an override to enable a high-contrast theme final String userTheme = AccessController.doPrivileged( (PrivilegedAction) () -> System.getProperty("com.sun.javafx.highContrastTheme")); if (isCaspian()) { if (platformTheme != null || userTheme != null) { // caspian has only one high contrast theme, use it regardless of the user or platform theme. accessibilityTheme = "com/sun/javafx/scene/control/skin/caspian/highcontrast.css"; } } else if (isModena()) { // User-defined property takes precedence if (userTheme != null) { switch (userTheme.toUpperCase()) { case "BLACKONWHITE": accessibilityTheme = "com/sun/javafx/scene/control/skin/modena/blackOnWhite.css"; break; case "WHITEONBLACK": accessibilityTheme = "com/sun/javafx/scene/control/skin/modena/whiteOnBlack.css"; break; case "YELLOWONBLACK": accessibilityTheme = "com/sun/javafx/scene/control/skin/modena/yellowOnBlack.css"; break; default: } } else { if (platformTheme != null) { // The following names are Platform specific (Windows 7 and 8) switch (platformTheme) { case "High Contrast White": accessibilityTheme = "com/sun/javafx/scene/control/skin/modena/blackOnWhite.css"; break; case "High Contrast Black": accessibilityTheme = "com/sun/javafx/scene/control/skin/modena/whiteOnBlack.css"; break; case "High Contrast #1": case "High Contrast #2": //TODO #2 should be green on black accessibilityTheme = "com/sun/javafx/scene/control/skin/modena/yellowOnBlack.css"; break; default: } } } } } private static void _setPlatformUserAgentStylesheet(String stylesheetUrl) { isModena = isCaspian = false; // check for command line override final String overrideStylesheetUrl = AccessController.doPrivileged( (PrivilegedAction) () -> System.getProperty("javafx.userAgentStylesheetUrl")); if (overrideStylesheetUrl != null) { stylesheetUrl = overrideStylesheetUrl; } final List uaStylesheets = new ArrayList<>(); // check for named theme constants for modena and caspian if (Application.STYLESHEET_CASPIAN.equalsIgnoreCase(stylesheetUrl)) { isCaspian = true; uaStylesheets.add("com/sun/javafx/scene/control/skin/caspian/caspian.css"); if (isSupported(ConditionalFeature.INPUT_TOUCH)) { uaStylesheets.add("com/sun/javafx/scene/control/skin/caspian/embedded.css"); if (com.sun.javafx.util.Utils.isQVGAScreen()) { uaStylesheets.add("com/sun/javafx/scene/control/skin/caspian/embedded-qvga.css"); } if (PlatformUtil.isAndroid()) { uaStylesheets.add("com/sun/javafx/scene/control/skin/caspian/android.css"); } } if (isSupported(ConditionalFeature.TWO_LEVEL_FOCUS)) { uaStylesheets.add("com/sun/javafx/scene/control/skin/caspian/two-level-focus.css"); } if (isSupported(ConditionalFeature.VIRTUAL_KEYBOARD)) { uaStylesheets.add("com/sun/javafx/scene/control/skin/caspian/fxvk.css"); } if (!isSupported(ConditionalFeature.TRANSPARENT_WINDOW)) { uaStylesheets.add("com/sun/javafx/scene/control/skin/caspian/caspian-no-transparency.css"); } } else if (Application.STYLESHEET_MODENA.equalsIgnoreCase(stylesheetUrl)) { isModena = true; uaStylesheets.add("com/sun/javafx/scene/control/skin/modena/modena.css"); if (isSupported(ConditionalFeature.INPUT_TOUCH)) { uaStylesheets.add("com/sun/javafx/scene/control/skin/modena/touch.css"); } // when running on embedded add a extra stylesheet to tune performance of modena theme if (PlatformUtil.isEmbedded()) { uaStylesheets.add("com/sun/javafx/scene/control/skin/modena/modena-embedded-performance.css"); } if (PlatformUtil.isAndroid()) { uaStylesheets.add("com/sun/javafx/scene/control/skin/modena/android.css"); } if (isSupported(ConditionalFeature.TWO_LEVEL_FOCUS)) { uaStylesheets.add("com/sun/javafx/scene/control/skin/modena/two-level-focus.css"); } if (isSupported(ConditionalFeature.VIRTUAL_KEYBOARD)) { uaStylesheets.add("com/sun/javafx/scene/control/skin/caspian/fxvk.css"); } if (!isSupported(ConditionalFeature.TRANSPARENT_WINDOW)) { uaStylesheets.add("com/sun/javafx/scene/control/skin/modena/modena-no-transparency.css"); } } else { uaStylesheets.add(stylesheetUrl); } // Ensure that accessibility starts right _setAccessibilityTheme(Toolkit.getToolkit().getThemeName()); if (accessibilityTheme != null) { uaStylesheets.add(accessibilityTheme); } AccessController.doPrivileged((PrivilegedAction) () -> { StyleManager.getInstance().setUserAgentStylesheets(uaStylesheets); return null; }); } public static void addNoTransparencyStylesheetToScene(final Scene scene) { if (PlatformImpl.isCaspian()) { AccessController.doPrivileged((PrivilegedAction) () -> { StyleManager.getInstance().addUserAgentStylesheet(scene, "com/sun/javafx/scene/control/skin/caspian/caspian-no-transparency.css"); return null; }); } else if (PlatformImpl.isModena()) { AccessController.doPrivileged((PrivilegedAction) () -> { StyleManager.getInstance().addUserAgentStylesheet(scene, "com/sun/javafx/scene/control/skin/modena/modena-no-transparency.css"); return null; }); } } private static boolean isSupportedImpl(ConditionalFeature feature) { switch (feature) { case GRAPHICS: if (isGraphicsSupported == null) { isGraphicsSupported = checkForClass("javafx.stage.Stage"); } return isGraphicsSupported; case CONTROLS: if (isControlsSupported == null) { isControlsSupported = checkForClass( "javafx.scene.control.Control"); } return isControlsSupported; case MEDIA: if (isMediaSupported == null) { isMediaSupported = checkForClass( "javafx.scene.media.MediaView"); if (isMediaSupported && PlatformUtil.isEmbedded()) { AccessController.doPrivileged((PrivilegedAction) () -> { String s = System.getProperty( "com.sun.javafx.experimental.embedded.media", "false"); isMediaSupported = Boolean.valueOf(s); return null; }); } } return isMediaSupported; case WEB: if (isWebSupported == null) { isWebSupported = checkForClass("javafx.scene.web.WebView"); if (isWebSupported && PlatformUtil.isEmbedded()) { AccessController.doPrivileged((PrivilegedAction) () -> { String s = System.getProperty( "com.sun.javafx.experimental.embedded.web", "false"); isWebSupported = Boolean.valueOf(s); return null; }); } } return isWebSupported; case SWT: if (isSWTSupported == null) { isSWTSupported = checkForClass("javafx.embed.swt.FXCanvas"); } return isSWTSupported; case SWING: if (isSwingSupported == null) { isSwingSupported = // check for JComponent first, it may not be present checkForClass("javax.swing.JComponent") && checkForClass("javafx.embed.swing.JFXPanel"); } return isSwingSupported; case FXML: if (isFXMLSupported == null) { isFXMLSupported = checkForClass("javafx.fxml.FXMLLoader") && checkForClass("javax.xml.stream.XMLInputFactory"); } return isFXMLSupported; case TWO_LEVEL_FOCUS: if (hasTwoLevelFocus == null) { return Toolkit.getToolkit().isSupported(feature); } return hasTwoLevelFocus; case VIRTUAL_KEYBOARD: if (hasVirtualKeyboard == null) { return Toolkit.getToolkit().isSupported(feature); } return hasVirtualKeyboard; case INPUT_TOUCH: if (hasTouch == null) { return Toolkit.getToolkit().isSupported(feature); } return hasTouch; case INPUT_MULTITOUCH: if (hasMultiTouch == null) { return Toolkit.getToolkit().isSupported(feature); } return hasMultiTouch; case INPUT_POINTER: if (hasPointer == null) { return Toolkit.getToolkit().isSupported(feature); } return hasPointer; default: return Toolkit.getToolkit().isSupported(feature); } } }