All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javafx.scene.Scene Maven / Gradle / Ivy

There is a newer version: 24-ea+15
Show newest version
/*
 * Copyright (c) 2010, 2023, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package javafx.scene;

import com.sun.glass.ui.Application;
import com.sun.glass.ui.Accessible;
import com.sun.javafx.scene.traversal.TraversalMethod;
import com.sun.javafx.util.Logging;
import com.sun.javafx.util.Utils;
import com.sun.javafx.application.PlatformImpl;
import com.sun.javafx.collections.TrackableObservableList;
import com.sun.javafx.css.StyleManager;
import com.sun.javafx.cursor.CursorFrame;
import com.sun.javafx.event.EventQueue;
import com.sun.javafx.geom.PickRay;
import com.sun.javafx.geom.Vec3d;
import com.sun.javafx.geom.transform.BaseTransform;
import com.sun.javafx.perf.PerformanceTracker;
import com.sun.javafx.scene.CssFlags;
import com.sun.javafx.scene.LayoutFlags;
import com.sun.javafx.scene.SceneEventDispatcher;
import com.sun.javafx.scene.SceneHelper;
import com.sun.javafx.scene.input.DragboardHelper;
import com.sun.javafx.scene.input.ExtendedInputMethodRequests;
import com.sun.javafx.scene.input.InputEventUtils;
import com.sun.javafx.scene.input.PickResultChooser;
import com.sun.javafx.scene.traversal.Direction;
import com.sun.javafx.scene.traversal.SceneTraversalEngine;
import com.sun.javafx.scene.traversal.TopMostTraversalEngine;
import com.sun.javafx.stage.EmbeddedWindow;
import com.sun.javafx.sg.prism.NGCamera;
import com.sun.javafx.sg.prism.NGLightBase;
import com.sun.javafx.tk.*;
import com.sun.prism.impl.PrismSettings;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.ConditionalFeature;
import javafx.application.Platform;
import javafx.beans.DefaultProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.NamedArg;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.css.CssMetaData;
import javafx.css.StyleableObjectProperty;
import javafx.css.Stylesheet;
import javafx.event.*;
import javafx.geometry.*;
import javafx.scene.image.WritableImage;
import javafx.scene.input.*;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.stage.PopupWindow;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.Window;
import javafx.util.Callback;
import javafx.util.Duration;
import com.sun.javafx.logging.PlatformLogger;
import com.sun.javafx.logging.PlatformLogger.Level;

import java.io.File;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

import com.sun.javafx.logging.PulseLogger;

import static com.sun.javafx.logging.PulseLogger.PULSE_LOGGING_ENABLED;
import com.sun.javafx.scene.NodeHelper;
import com.sun.javafx.stage.WindowHelper;
import com.sun.javafx.scene.input.ClipboardHelper;
import com.sun.javafx.scene.input.TouchPointHelper;
import java.lang.ref.WeakReference;

/**
 * The JavaFX {@code Scene} class is the container for all content in a scene graph.
 * The background of the scene is filled as specified by the {@code fill} property.
 * 

* The application must specify the root {@code Node} for the scene graph by setting * the {@code root} property. If a {@code Group} is used as the root, the * contents of the scene graph will be clipped by the scene's width and height and * changes to the scene's size (if user resizes the stage) will not alter the * layout of the scene graph. If a resizable node (layout {@code Region} or * {@code Control}) is set as the root, then the root's size will track the * scene's size, causing the contents to be relayed out as necessary. *

* The scene's size may be initialized by the application during construction. * If no size is specified, the scene will automatically compute its initial * size based on the preferred size of its content. If only one dimension is specified, * the other dimension is computed using the specified dimension, respecting content bias * of a root. *

* An application may request depth buffer support or scene anti-aliasing * support at the creation of a {@code Scene}. A scene with only 2D shapes and * without any 3D transforms does not need a depth buffer nor scene * anti-aliasing support. A scene containing 3D shapes or 2D shapes with 3D * transforms may use depth buffer support for proper depth sorted rendering; to * avoid depth fighting (also known as Z fighting), disable depth testing on 2D * shapes that have no 3D transforms. See * {@link Node#depthTestProperty() depthTest} for more information. A scene with * 3D shapes may enable scene anti-aliasing to improve its rendering quality. *

* The depthBuffer and antiAliasing flags are conditional features. With the * respective default values of: false and {@code SceneAntialiasing.DISABLED}. * See {@link javafx.application.ConditionalFeature#SCENE3D ConditionalFeature.SCENE3D} * for more information. *

* A default headlight will be added to a scene that contains one or more * {@code Shape3D} nodes, but no light nodes. This light source is a * {@code Color.WHITE} {@code PointLight} placed at the camera position. * *

* A {@code Scene} may be created and modified on any thread until it is attached * to a {@link Window} that is {@link Window#isShowing() showing}. * After that, it must be modified only on the JavaFX Application Thread. * Note that {@code Scene} is not thread-safe; modifying a {@code Scene} on * multiple threads at the same time will lead to unpredictable results and * must be avoided. *

* *

* The JavaFX Application Thread is created as part of the startup process for * the JavaFX runtime. See the {@link javafx.application.Application} class and * the {@link Platform#startup(Runnable)} method for more information. *

* *

Example:

* *
import javafx.scene.*;
import javafx.scene.paint.*;
import javafx.scene.shape.*;

Group root = new Group();
Scene s = new Scene(root, 300, 300, Color.BLACK);

Rectangle r = new Rectangle(25,25,250,250);
r.setFill(Color.BLUE);

root.getChildren().add(r);
 * 
* * @since JavaFX 2.0 */ @DefaultProperty("root") public class Scene implements EventTarget { private double widthSetByUser = -1.0; private double heightSetByUser = -1.0; private boolean sizeInitialized = false; private final boolean depthBuffer; private final SceneAntialiasing antiAliasing; private EnumSet dirtyBits = EnumSet.noneOf(DirtyBits.class); @SuppressWarnings("removal") final AccessControlContext acc = AccessController.getContext(); private Camera defaultCamera; /** * A node that is temporarily responsible for the FOCUS_NODE * accessibility attribute. E.g. a currently active MenuBar. */ private Node transientFocusContainer; //Neither width nor height are initialized and will be calculated according to content when this Scene //is shown for the first time. // public Scene() { // //this(-1, -1, (Parent) new Group()); // this(-1, -1, (Parent)null); // } /** * Creates a Scene for a specific root Node. * * @param root The root node of the scene graph * * @throws NullPointerException if root is null */ public Scene(@NamedArg("root") Parent root) { this(root, -1, -1, Color.WHITE, false, SceneAntialiasing.DISABLED); } //Public constructor initializing public-init properties //When width < 0, and or height < 0 is passed, then width and/or height are understood as unitialized //Unitialized dimension is calculated when Scene is shown for the first time. // public Scene( // @Default("-1") double width, // @Default("-1") double height) { // //this(width, height, (Parent)new Group()); // this(width, height, (Parent)null); // } // // public Scene(double width, double height, Paint fill) { // //this(width, height, (Parent) new Group()); // this(width, height, (Parent)null); // setFill(fill); // } /** * Creates a Scene for a specific root Node with a specific size. * * @param root The root node of the scene graph * @param width The width of the scene * @param height The height of the scene * * @throws NullPointerException if root is null */ public Scene(@NamedArg("root") Parent root, @NamedArg("width") double width, @NamedArg("height") double height) { this(root, width, height, Color.WHITE, false, SceneAntialiasing.DISABLED); } /** * Creates a Scene for a specific root Node with a fill. * * @param root The parent * @param fill The fill * * @throws NullPointerException if root is null */ public Scene(@NamedArg("root") Parent root, @NamedArg(value="fill", defaultValue="WHITE") Paint fill) { this(root, -1, -1, fill, false, SceneAntialiasing.DISABLED); } /** * Creates a Scene for a specific root Node with a specific size and fill. * * @param root The root node of the scene graph * @param width The width of the scene * @param height The height of the scene * @param fill The fill * * @throws NullPointerException if root is null */ public Scene(@NamedArg("root") Parent root, @NamedArg("width") double width, @NamedArg("height") double height, @NamedArg(value="fill", defaultValue="WHITE") Paint fill) { this(root, width, height, fill, false, SceneAntialiasing.DISABLED); } /** * Constructs a scene consisting of a root, with a dimension of width and * height, and specifies whether a depth buffer is created for this scene. *

* A scene with only 2D shapes and without any 3D transforms does not need a * depth buffer. A scene containing 3D shapes or 2D shapes with 3D * transforms may use depth buffer support for proper depth sorted * rendering; to avoid depth fighting (also known as Z fighting), disable * depth testing on 2D shapes that have no 3D transforms. See * {@link Node#depthTestProperty() depthTest} for more information. * * @param root The root node of the scene graph * @param width The width of the scene * @param height The height of the scene * @param depthBuffer The depth buffer flag *

* The depthBuffer flag is a conditional feature and its default value is * false. See * {@link javafx.application.ConditionalFeature#SCENE3D ConditionalFeature.SCENE3D} * for more information. * * @throws NullPointerException if root is null * * @see javafx.scene.Node#setDepthTest(DepthTest) */ public Scene(@NamedArg("root") Parent root, @NamedArg(value="width", defaultValue="-1") double width, @NamedArg(value="height", defaultValue="-1") double height, @NamedArg("depthBuffer") boolean depthBuffer) { this(root, width, height, Color.WHITE, depthBuffer, SceneAntialiasing.DISABLED); } /** * Constructs a scene consisting of a root, with a dimension of width and * height, specifies whether a depth buffer is created for this scene and * specifies whether scene anti-aliasing is requested. *

* A scene with only 2D shapes and without any 3D transforms does not need a * depth buffer nor scene anti-aliasing support. A scene containing 3D * shapes or 2D shapes with 3D transforms may use depth buffer support for * proper depth sorted rendering; to avoid depth fighting (also known as Z * fighting), disable depth testing on 2D shapes that have no 3D transforms. * See {@link Node#depthTestProperty() depthTest} for more information. A * scene with 3D shapes may enable scene anti-aliasing to improve its * rendering quality. * * @param root The root node of the scene graph * @param width The width of the scene * @param height The height of the scene * @param depthBuffer The depth buffer flag * @param antiAliasing The scene anti-aliasing attribute. A value of * {@code null} is treated as DISABLED. *

* The depthBuffer and antiAliasing are conditional features. With the * respective default values of: false and {@code SceneAntialiasing.DISABLED}. See * {@link javafx.application.ConditionalFeature#SCENE3D ConditionalFeature.SCENE3D} * for more information. * * @throws NullPointerException if root is null * * @see javafx.scene.Node#setDepthTest(DepthTest) * @since JavaFX 8.0 */ public Scene(@NamedArg("root") Parent root, @NamedArg(value="width", defaultValue="-1") double width, @NamedArg(value="height", defaultValue="-1") double height, @NamedArg("depthBuffer") boolean depthBuffer, @NamedArg(value="antiAliasing", defaultValue="DISABLED") SceneAntialiasing antiAliasing) { this(root, width, height, Color.WHITE, depthBuffer, antiAliasing); if (antiAliasing != null && antiAliasing != SceneAntialiasing.DISABLED && !Toolkit.getToolkit().isMSAASupported()) { String logname = Scene.class.getName(); PlatformLogger.getLogger(logname).warning("System can't support " + "antiAliasing"); } } private Scene(Parent root, double width, double height, Paint fill, boolean depthBuffer, SceneAntialiasing antiAliasing) { this.depthBuffer = depthBuffer; this.antiAliasing = antiAliasing; if (root == null) { throw new NullPointerException("Root cannot be null"); } if ((depthBuffer || (antiAliasing != null && antiAliasing != SceneAntialiasing.DISABLED)) && !Platform.isSupported(ConditionalFeature.SCENE3D)) { String logname = Scene.class.getName(); PlatformLogger.getLogger(logname).warning("System can't support " + "ConditionalFeature.SCENE3D"); } init(); setRoot(root); init(width, height); setFill(fill); // Any mouse or touch press on the scene will clear the focusVisible flag of // the current focus owner, if there is one. EventHandler pressedHandler = event -> { Node focusOwner = getFocusOwner(); if (focusOwner != null) { setFocusOwner(focusOwner, false); } }; addEventFilter(MouseEvent.MOUSE_PRESSED, pressedHandler); addEventFilter(TouchEvent.TOUCH_PRESSED, pressedHandler); } static { PerformanceTracker.setSceneAccessor(new PerformanceTracker.SceneAccessor() { @Override public void setPerfTracker(Scene scene, PerformanceTracker tracker) { synchronized (trackerMonitor) { scene.tracker = tracker; } } @Override public PerformanceTracker getPerfTracker(Scene scene) { synchronized (trackerMonitor) { return scene.tracker; } } }); SceneHelper.setSceneAccessor( new SceneHelper.SceneAccessor() { @Override public void enableInputMethodEvents(Scene scene, boolean enable) { scene.enableInputMethodEvents(enable); } @Override public void processKeyEvent(Scene scene, KeyEvent e) { scene.processKeyEvent(e); } @Override public void processMouseEvent(Scene scene, MouseEvent e) { scene.processMouseEvent(e); } @Override public void preferredSize(Scene scene) { scene.preferredSize(); } @Override public void disposePeer(Scene scene) { scene.disposePeer(); } @Override public void initPeer(Scene scene) { scene.initPeer(); } @Override public void setWindow(Scene scene, Window window) { scene.setWindow(window); } @Override public TKScene getPeer(Scene scene) { return scene.getPeer(); } @Override public void setAllowPGAccess(boolean flag) { Scene.setAllowPGAccess(flag); } @Override public void parentEffectiveOrientationInvalidated( final Scene scene) { scene.parentEffectiveOrientationInvalidated(); } @Override public Camera getEffectiveCamera(Scene scene) { return scene.getEffectiveCamera(); } @Override public Scene createPopupScene(Parent root) { return new Scene(root) { @Override void doLayoutPass() { resizeRootToPreferredSize(getRoot()); super.doLayoutPass(); } @Override void resizeRootOnSceneSizeChange( double newWidth, double newHeight) { // don't resize } }; } @Override public void setTransientFocusContainer(Scene scene, Node node) { if (scene != null) { scene.transientFocusContainer = node; } } @Override public Accessible getAccessible(Scene scene) { return scene.getAccessible(); } }); } // Reserve space for 30 nodes in the dirtyNodes set. private static final int MIN_DIRTY_CAPACITY = 30; // For debugging private static boolean inSynchronizer = false; private static boolean inMousePick = false; private static boolean allowPGAccess = false; private static int pgAccessCount = 0; /** * Used for debugging purposes. Returns true if we are in either the * mouse event code (picking) or the synchronizer, or if the scene is * not yet initialized, * */ static boolean isPGAccessAllowed() { return inSynchronizer || inMousePick || allowPGAccess; } static void setAllowPGAccess(boolean flag) { if (Utils.assertionEnabled()) { if (flag) { pgAccessCount++; allowPGAccess = true; } else { if (pgAccessCount <= 0) { throw new java.lang.AssertionError("*** pgAccessCount underflow"); } if (--pgAccessCount == 0) { allowPGAccess = false; } } } } /** * If true, use the platform's drag gesture detection * else use Scene-level detection as per DnDGesture.process(MouseEvent, List) */ private static final boolean PLATFORM_DRAG_GESTURE_INITIATION = false; /** * Set of dirty nodes; processed once per frame by the synchronizer. * When a node's state changes such that it becomes "dirty" with respect * to the graphics stack and requires synchronization, then that node * is added to this list. Note that if state on the Node changes, but it * was already dirty, then the Node doesn't add itself again. *

* Because at initialization time every node in the scene graph is dirty, * we have a special state and special code path during initialization * that does not involve adding each node to the dirtyNodes list. When * dirtyNodes is null, that means this Scene has not yet been synchronized. * A good default size is then created for the dirtyNodes list. *

* We double-buffer the set so that we can add new nodes to the * set while processing the existing set. This avoids our having to * take a snapshot of the set (e.g., with toArray()) and reduces garbage. */ private Node[] dirtyNodes; private int dirtyNodesSize; /** * Add the specified node to this scene's dirty list. Called by the * markDirty method in Node or when the Node's scene changes. */ void addToDirtyList(Node n) { if (dirtyNodes == null || dirtyNodesSize == 0) { if (peer != null) { Toolkit.getToolkit().requestNextPulse(); } } if (dirtyNodes != null) { if (dirtyNodesSize == dirtyNodes.length) { Node[] tmp = new Node[dirtyNodesSize + (dirtyNodesSize >> 1)]; System.arraycopy(dirtyNodes, 0, tmp, 0, dirtyNodesSize); dirtyNodes = tmp; } dirtyNodes[dirtyNodesSize++] = n; } } private void doCSSPass() { final Parent sceneRoot = getRoot(); // // RT-17547: when the tree is synchronized, the dirty bits are // are cleared but the cssFlag might still be something other than // clean. // // Before RT-17547, the code checked the dirty bit. But this is // superfluous since the dirty bit will be set if the flag is not clean, // but the flag will never be anything other than clean if the dirty // bit is not set. The dirty bit is still needed, however, since setting // it ensures a pulse if no other dirty bits have been set. // // For the purpose of showing the change, the dirty bit // check code was commented out and not removed. // // if (sceneRoot.isDirty(com.sun.javafx.scene.DirtyBits.NODE_CSS)) { if (sceneRoot.cssFlag != CssFlags.CLEAN) { // The dirty bit isn't checked but we must ensure it is cleared. // The cssFlag is set to clean in either Node.processCSS or // NodeHelper.processCSS sceneRoot.clearDirty(com.sun.javafx.scene.DirtyBits.NODE_CSS); sceneRoot.processCSS(); } } void doLayoutPass() { final Parent r = getRoot(); if (r != null) { r.layout(); } } /** * The peer of this scene */ private TKScene peer; /* * Get Scene's peer */ TKScene getPeer() { return peer; } /** * The scene pulse listener that gets called on toolkit pulses */ ScenePulseListener scenePulseListener = new ScenePulseListener(); private List preLayoutPulseListeners; private List postLayoutPulseListeners; /** * Adds a new scene pre layout pulse listener to this scene. Every time a pulse occurs, * this listener will be called on the JavaFX Application Thread directly * before the CSS and layout passes, and also before * any rendering is done for * this frame. This scene pulse listener is suitable for knowing when a * scenegraph pulse is happening and also for modifying the scenegraph * (as it is called before CSS and layout, so any changes made will be properly * styled and positioned). * * This method must be called on the JavaFX Application thread. * * @param r The Runnable to be called when the pulse occurs. * * @throws IllegalStateException if this method is called on a thread * other than the JavaFX Application Thread. * * @throws NullPointerException if the provided Runnable is null. * * @since 9 */ public final void addPreLayoutPulseListener(Runnable r) { Toolkit.getToolkit().checkFxUserThread(); if (r == null) { throw new NullPointerException("Scene pulse listener should not be null"); } if (preLayoutPulseListeners == null) { preLayoutPulseListeners = new CopyOnWriteArrayList<>(); } preLayoutPulseListeners.add(r); } /** * Removes a previously registered scene pre layout pulse listener from listening to * pulses in this scene. This method does nothing if the specified Runnable is * not already in the list. * * This method must be called on the JavaFX Application thread. * * @param r The Runnable that should no longer be called when the pulse * occurs for this scene. * * @throws IllegalStateException if this method is called on a thread * other than the JavaFX Application Thread. * * @since 9 */ public final void removePreLayoutPulseListener(Runnable r) { Toolkit.getToolkit().checkFxUserThread(); if (preLayoutPulseListeners == null) { return; } preLayoutPulseListeners.remove(r); } /** * Adds a new scene post layout pulse listener to this scene. Every time a pulse occurs, * this listener will be called on the JavaFX Application Thread directly * after the CSS and layout passes, but before any rendering is done for * this frame. This scene pulse listener is suitable for knowing when a * scenegraph pulse is happening, but it is not suited to use cases related * to modifying the scenegraph (as it is called after CSS and layout, so * any changes will possibly be incorrect until the next pulse is run). * An alternative (and better) solution for situations where a scenegraph * modification is required to happen is to use either the * {@link #addPreLayoutPulseListener(Runnable)} API or the the * {@link javafx.animation.AnimationTimer} API. * * This method must be called on the JavaFX Application thread. * * @param r The Runnable to be called when the pulse occurs. * * @throws IllegalStateException if this method is called on a thread * other than the JavaFX Application Thread. * * @throws NullPointerException if the provided Runnable is null. * * @since 9 */ public final void addPostLayoutPulseListener(Runnable r) { Toolkit.getToolkit().checkFxUserThread(); if (r == null) { throw new NullPointerException("Scene pulse listener should not be null"); } if (postLayoutPulseListeners == null) { postLayoutPulseListeners = new CopyOnWriteArrayList<>(); } postLayoutPulseListeners.add(r); } /** * Removes a previously registered scene post layout pulse listener from listening to * pulses in this scene. This method does nothing if the specified Runnable is * not already in the list. * * This method must be called on the JavaFX Application thread. * * @param r The Runnable that should no longer be called when the pulse * occurs for this scene. * * @throws IllegalStateException if this method is called on a thread * other than the JavaFX Application Thread. * * @since 9 */ public final void removePostLayoutPulseListener(Runnable r) { Toolkit.getToolkit().checkFxUserThread(); if (postLayoutPulseListeners == null) { return; } postLayoutPulseListeners.remove(r); } /** * Return the defined {@code SceneAntialiasing} for this {@code Scene}. *

* Note: this is a conditional feature. See * {@link javafx.application.ConditionalFeature#SCENE3D ConditionalFeature.SCENE3D} * and {@link javafx.scene.SceneAntialiasing SceneAntialiasing} * for more information. * @return the SceneAntialiasing for this scene * @since JavaFX 8.0 */ public final SceneAntialiasing getAntiAliasing() { return antiAliasing; } private boolean getAntiAliasingInternal() { return (antiAliasing != null && Toolkit.getToolkit().isMSAASupported() && Platform.isSupported(ConditionalFeature.SCENE3D)) ? antiAliasing != SceneAntialiasing.DISABLED : false; } /** * The {@code Window} for this {@code Scene} */ private ReadOnlyObjectWrapper window; void setWindow(Window value) { windowPropertyImpl().set(value); } public final Window getWindow() { return window == null ? null : window.get(); } public final ReadOnlyObjectProperty windowProperty() { return windowPropertyImpl().getReadOnlyProperty(); } private ReadOnlyObjectWrapper windowPropertyImpl() { if (window == null) { window = new ReadOnlyObjectWrapper<>() { private Window oldWindow; @Override protected void invalidated() { final Window newWindow = get(); windowForSceneChanged(oldWindow, newWindow); if (oldWindow != null) { disposePeer(); } if (newWindow != null) { initPeer(); } parentEffectiveOrientationInvalidated(); oldWindow = newWindow; } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "window"; } }; } return window; } void initPeer() { assert peer == null; Window window = getWindow(); // initPeer() is only called from Window, either when the window // is being shown, or the window scene is being changed. In any case // this scene's window cannot be null. assert window != null; TKStage windowPeer = WindowHelper.getPeer(window); if (windowPeer == null) { // This is fine, the window is not visible. initPeer() will // be called again later, when the window is being shown. return; } final boolean isTransparentWindowsSupported = Platform.isSupported(ConditionalFeature.TRANSPARENT_WINDOW); if (!isTransparentWindowsSupported) { PlatformImpl.addNoTransparencyStylesheetToScene(this); } PerformanceTracker.logEvent("Scene.initPeer started"); setAllowPGAccess(true); Toolkit tk = Toolkit.getToolkit(); peer = windowPeer.createTKScene(isDepthBufferInternal(), getAntiAliasingInternal(), acc); PerformanceTracker.logEvent("Scene.initPeer TKScene created"); peer.setTKSceneListener(new ScenePeerListener()); peer.setTKScenePaintListener(new ScenePeerPaintListener()); PerformanceTracker.logEvent("Scene.initPeer TKScene set"); peer.setRoot(getRoot().getPeer()); peer.setFillPaint(getFill() == null ? null : tk.getPaint(getFill())); NodeHelper.updatePeer(getEffectiveCamera()); peer.setCamera((NGCamera) getEffectiveCamera().getPeer()); peer.markDirty(); PerformanceTracker.logEvent("Scene.initPeer TKScene initialized"); setAllowPGAccess(false); tk.addSceneTkPulseListener(scenePulseListener); // listen to dnd gestures coming from the platform if (PLATFORM_DRAG_GESTURE_INITIATION) { if (dragGestureListener == null) { dragGestureListener = new DragGestureListener(); } tk.registerDragGestureListener(peer, EnumSet.allOf(TransferMode.class), dragGestureListener); } tk.enableDrop(peer, new DropTargetListener()); tk.installInputMethodRequests(peer, new InputMethodRequestsDelegate()); PerformanceTracker.logEvent("Scene.initPeer finished"); } void disposePeer() { if (peer == null) { // This is fine, the window is either not shown yet and there is no // need in disposing scene peer, or is hidden and disposePeer() // has already been called. return; } PerformanceTracker.logEvent("Scene.disposePeer started"); Toolkit tk = Toolkit.getToolkit(); tk.removeSceneTkPulseListener(scenePulseListener); if (accessible != null) { disposeAccessibles(); Node root = getRoot(); if (root != null) root.releaseAccessible(); accessible.dispose(); accessible = null; } peer.dispose(); peer = null; PerformanceTracker.logEvent("Scene.disposePeer finished"); } DnDGesture dndGesture = null; DragGestureListener dragGestureListener; /** * The horizontal location of this {@code Scene} on the {@code Window}. */ private ReadOnlyDoubleWrapper x; private final void setX(double value) { xPropertyImpl().set(value); } public final double getX() { return x == null ? 0.0 : x.get(); } public final ReadOnlyDoubleProperty xProperty() { return xPropertyImpl().getReadOnlyProperty(); } private ReadOnlyDoubleWrapper xPropertyImpl() { if (x == null) { x = new ReadOnlyDoubleWrapper(this, "x"); } return x; } /** * The vertical location of this {@code Scene} on the {@code Window}. */ private ReadOnlyDoubleWrapper y; private final void setY(double value) { yPropertyImpl().set(value); } public final double getY() { return y == null ? 0.0 : y.get(); } public final ReadOnlyDoubleProperty yProperty() { return yPropertyImpl().getReadOnlyProperty(); } private ReadOnlyDoubleWrapper yPropertyImpl() { if (y == null) { y = new ReadOnlyDoubleWrapper(this, "y"); } return y; } /** * The width of this {@code Scene} */ private ReadOnlyDoubleWrapper width; private final void setWidth(double value) { widthPropertyImpl().set(value); } public final double getWidth() { return width == null ? 0.0 : width.get(); } public final ReadOnlyDoubleProperty widthProperty() { return widthPropertyImpl().getReadOnlyProperty(); } private ReadOnlyDoubleWrapper widthPropertyImpl() { if (width == null) { width = new ReadOnlyDoubleWrapper() { @Override protected void invalidated() { final Parent _root = getRoot(); //TODO - use a better method to update mirroring if (_root.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) { NodeHelper.transformsChanged(_root); } if (_root.isResizable()) { resizeRootOnSceneSizeChange(get() - _root.getLayoutX() - _root.getTranslateX(), _root.getLayoutBounds().getHeight()); } getEffectiveCamera().setViewWidth(get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "width"; } }; } return width; } /** * The height of this {@code Scene} */ private ReadOnlyDoubleWrapper height; private final void setHeight(double value) { heightPropertyImpl().set(value); } public final double getHeight() { return height == null ? 0.0 : height.get(); } public final ReadOnlyDoubleProperty heightProperty() { return heightPropertyImpl().getReadOnlyProperty(); } private ReadOnlyDoubleWrapper heightPropertyImpl() { if (height == null) { height = new ReadOnlyDoubleWrapper() { @Override protected void invalidated() { final Parent _root = getRoot(); if (_root.isResizable()) { resizeRootOnSceneSizeChange(_root.getLayoutBounds().getWidth(), get() - _root.getLayoutY() - _root.getTranslateY()); } getEffectiveCamera().setViewHeight(get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "height"; } }; } return height; } void resizeRootOnSceneSizeChange(double newWidth, double newHeight) { getRoot().resize(newWidth, newHeight); } // Reusable target wrapper (to avoid creating new one for each picking) private TargetWrapper tmpTargetWrapper = new TargetWrapper(); /** * Specifies the type of camera use for rendering this {@code Scene}. * If {@code camera} is null, a parallel camera is used for rendering. * It is illegal to set a camera that belongs to other {@code Scene} * or {@code SubScene}. *

* Note: this is a conditional feature. See * {@link javafx.application.ConditionalFeature#SCENE3D ConditionalFeature.SCENE3D} * for more information. * * @defaultValue null */ private ObjectProperty camera; public final void setCamera(Camera value) { cameraProperty().set(value); } public final Camera getCamera() { return camera == null ? null : camera.get(); } public final ObjectProperty cameraProperty() { if (camera == null) { camera = new ObjectPropertyBase<>() { Camera oldCamera = null; @Override protected void invalidated() { Camera _value = get(); if (_value != null) { if (_value instanceof PerspectiveCamera && !Platform.isSupported(ConditionalFeature.SCENE3D)) { String logname = Scene.class.getName(); PlatformLogger.getLogger(logname).warning("System can't support " + "ConditionalFeature.SCENE3D"); } // Illegal value if it belongs to other scene or any subscene if ((_value.getScene() != null && _value.getScene() != Scene.this) || _value.getSubScene() != null) { throw new IllegalArgumentException(_value + "is already part of other scene or subscene"); } // throws exception if the camera already has a different owner _value.setOwnerScene(Scene.this); _value.setViewWidth(getWidth()); _value.setViewHeight(getHeight()); } if (oldCamera != null && oldCamera != _value) { oldCamera.setOwnerScene(null); } oldCamera = _value; } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "camera"; } }; } return camera; } Camera getEffectiveCamera() { final Camera cam = getCamera(); if (cam == null || (cam instanceof PerspectiveCamera && !Platform.isSupported(ConditionalFeature.SCENE3D))) { if (defaultCamera == null) { defaultCamera = new ParallelCamera(); defaultCamera.setOwnerScene(this); defaultCamera.setViewWidth(getWidth()); defaultCamera.setViewHeight(getHeight()); } return defaultCamera; } return cam; } // Used by the camera void markCameraDirty() { markDirty(DirtyBits.CAMERA_DIRTY); setNeedsRepaint(); } void markCursorDirty() { markDirty(DirtyBits.CURSOR_DIRTY); } /** * Defines the background fill of this {@code Scene}. Both a {@code null} * value meaning 'paint no background' and a {@link javafx.scene.paint.Paint} * with transparency are supported. The default fill of the Scene is * {@link Color#WHITE}, but it is more commonly the case that the initial * color shown to users is the background fill of the * {@link #rootProperty() root node} of the {@code Scene}, as it typically is * stretched to take up all available space in the {@code Scene}. The * root node of the {@code Scene} is given the CSS style class 'root', and * the default user agent stylesheets that ship with JavaFX (presently * Caspian and Modena) apply styling on to this root style class. In the * case of Caspian this does not impact the background fill color of the * root node, but in the case of Modena the default fill is set to be a * light gray color. * * @defaultValue WHITE */ private ObjectProperty fill; public final void setFill(Paint value) { fillProperty().set(value); } public final Paint getFill() { return fill == null ? Color.WHITE : fill.get(); } public final ObjectProperty fillProperty() { if (fill == null) { fill = new ObjectPropertyBase(Color.WHITE) { @Override protected void invalidated() { markDirty(DirtyBits.FILL_DIRTY); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "fill"; } }; } return fill; } /** * Defines the root {@code Node} of the scene graph. * If a {@code Group} is used as the root, the * contents of the scene graph will be clipped by the scene's width and height and * changes to the scene's size (if user resizes the stage) will not alter the * layout of the scene graph. If a resizable node (layout {@code Region} or * {@code Control}) is set as the root, then the root's size will track the * scene's size, causing the contents to be relayed out as necessary. * * Scene doesn't accept null root. * */ private ObjectProperty root; public final void setRoot(Parent value) { rootProperty().set(value); } public final Parent getRoot() { return root == null ? null : root.get(); } Parent oldRoot; public final ObjectProperty rootProperty() { if (root == null) { root = new ObjectPropertyBase<>() { private void forceUnbind() { System.err.println("Unbinding illegal root."); unbind(); } @Override protected void invalidated() { Parent _value = get(); if (_value == null) { if (isBound()) forceUnbind(); throw new NullPointerException("Scene's root cannot be null"); } if (_value.getParent() != null) { if (isBound()) forceUnbind(); throw new IllegalArgumentException(_value + "is already inside a scene-graph and cannot be set as root"); } if (_value.getClipParent() != null) { if (isBound()) forceUnbind(); throw new IllegalArgumentException(_value + "is set as a clip on another node, so cannot be set as root"); } if (_value.getScene() != null && _value.getScene().getRoot() == _value && _value.getScene() != Scene.this) { if (isBound()) forceUnbind(); throw new IllegalArgumentException(_value + "is already set as root of another scene"); } if (oldRoot != null) { oldRoot.setScenes(null, null); oldRoot.getStyleClass().remove("root"); } oldRoot = _value; _value.getStyleClass().add(0, "root"); _value.setScenes(Scene.this, null); markDirty(DirtyBits.ROOT_DIRTY); _value.resize(getWidth(), getHeight()); // maybe no-op if root is not resizable _value.requestLayout(); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "root"; } }; } return root; } void setNeedsRepaint() { if (this.peer != null) { peer.entireSceneNeedsRepaint(); } } // Process CSS and layout and sync the scene prior to the snapshot // operation of the given node for this scene (currently the node // is unused but could possibly be used in the future to optimize this) void doCSSLayoutSyncForSnapshot(Node node) { if (!sizeInitialized) { preferredSize(); } else { doCSSPass(); } // we do not need pulse in the snapshot code // because this scene can be stage-less doLayoutPass(); getRoot().updateBounds(); if (peer != null) { peer.waitForRenderingToComplete(); peer.waitForSynchronization(); try { // Run the synchronizer while holding the render lock scenePulseListener.synchronizeSceneNodes(); } finally { peer.releaseSynchronization(false); } } else { scenePulseListener.synchronizeSceneNodes(); } } // Shared method for Scene.snapshot and Node.snapshot. It is static because // we might be doing a Node snapshot with a null scene static WritableImage doSnapshot(Scene scene, double x, double y, double w, double h, Node root, BaseTransform transform, boolean depthBuffer, Paint fill, Camera camera, WritableImage wimg) { Toolkit tk = Toolkit.getToolkit(); Toolkit.ImageRenderingContext context = new Toolkit.ImageRenderingContext(); int xMin = (int)Math.floor(x); int yMin = (int)Math.floor(y); int width; int height; if (wimg == null) { int xMax = (int)Math.ceil(x + w); int yMax = (int)Math.ceil(y + h); width = Math.max(xMax - xMin, 1); height = Math.max(yMax - yMin, 1); wimg = new WritableImage(width, height); } else { width = (int)wimg.getWidth(); height = (int)wimg.getHeight(); } setAllowPGAccess(true); context.x = xMin; context.y = yMin; context.width = width; context.height = height; context.transform = transform; context.depthBuffer = depthBuffer; context.root = root.getPeer(); context.platformPaint = fill == null ? null : tk.getPaint(fill); double cameraViewWidth = 1.0; double cameraViewHeight = 1.0; if (camera != null) { // temporarily adjust camera viewport to the snapshot size cameraViewWidth = camera.getViewWidth(); cameraViewHeight = camera.getViewHeight(); camera.setViewWidth(width); camera.setViewHeight(height); NodeHelper.updatePeer(camera); context.camera = camera.getPeer(); } else { context.camera = null; } // Grab the lights from the scene context.lights = null; if (scene != null && !scene.lights.isEmpty()) { context.lights = new NGLightBase[scene.lights.size()]; for (int i = 0; i < scene.lights.size(); i++) { context.lights[i] = scene.lights.get(i).getPeer(); } } Toolkit.WritableImageAccessor accessor = Toolkit.getWritableImageAccessor(); context.platformImage = accessor.getTkImageLoader(wimg); setAllowPGAccess(false); Object tkImage = tk.renderToImage(context); if (tkImage != null) { accessor.loadTkImage(wimg, tkImage); } if (camera != null) { setAllowPGAccess(true); camera.setViewWidth(cameraViewWidth); camera.setViewHeight(cameraViewHeight); NodeHelper.updatePeer(camera); setAllowPGAccess(false); } // if this scene belongs to some stage // we need to mark the entire scene as dirty // because dirty logic is buggy if (scene != null && scene.peer != null) { scene.setNeedsRepaint(); } return wimg; } /** * Implementation method for snapshot */ private WritableImage doSnapshot(WritableImage img) { // TODO: no need to do CSS, layout or sync in the deferred case, // if this scene is attached to a visible stage doCSSLayoutSyncForSnapshot(getRoot()); double w = getWidth(); double h = getHeight(); BaseTransform transform = BaseTransform.IDENTITY_TRANSFORM; return doSnapshot(this, 0, 0, w, h, getRoot(), transform, isDepthBufferInternal(), getFill(), getEffectiveCamera(), img); } // Pulse listener used to run all deferred (async) snapshot requests private static TKPulseListener snapshotPulseListener = null; private static List snapshotRunnableListA; private static List snapshotRunnableListB; private static List snapshotRunnableList; @SuppressWarnings("removal") static void addSnapshotRunnable(final Runnable runnable) { Toolkit.getToolkit().checkFxUserThread(); if (snapshotPulseListener == null) { snapshotRunnableListA = new ArrayList<>(); snapshotRunnableListB = new ArrayList<>(); snapshotRunnableList = snapshotRunnableListA; snapshotPulseListener = () -> { if (snapshotRunnableList.size() > 0) { List runnables = snapshotRunnableList; if (snapshotRunnableList == snapshotRunnableListA) { snapshotRunnableList = snapshotRunnableListB; } else { snapshotRunnableList = snapshotRunnableListA; } for (Runnable r : runnables) { try { r.run(); } catch (Throwable th) { System.err.println("Exception in snapshot runnable"); th.printStackTrace(System.err); } } runnables.clear(); } }; // Add listener that will be called after all of the scenes have // had layout and CSS processing, and have been synced Toolkit.getToolkit().addPostSceneTkPulseListener(snapshotPulseListener); } final AccessControlContext acc = AccessController.getContext(); snapshotRunnableList.add(() -> { AccessController.doPrivileged((PrivilegedAction) () -> { runnable.run(); return null; }, acc); }); Toolkit.getToolkit().requestNextPulse(); } /** * Takes a snapshot of this scene and returns the rendered image when * it is ready. * CSS and layout processing will be done for the scene prior to * rendering it. * The entire destination image is cleared using the fill {@code Paint} * of this scene. The nodes in the scene are then rendered to the image. * The point (0,0) in scene coordinates is mapped to (0,0) in the image. * If the image is smaller than the size of the scene, then the rendering * will be clipped by the image. * *

* When taking a snapshot of a scene that is being animated, either * explicitly by the application or implicitly (such as chart animation), * the snapshot will be rendered based on the state of the scene graph at * the moment the snapshot is taken and will not reflect any subsequent * animation changes. *

* * @param image the writable image that will be used to hold the rendered scene. * It may be null in which case a new WritableImage will be constructed. * If the image is non-null, the scene will be rendered into the * existing image. * In this case, the width and height of the image determine the area * that is rendered instead of the width and height of the scene. * * @throws IllegalStateException if this method is called on a thread * other than the JavaFX Application Thread. * * @return the rendered image * @since JavaFX 2.2 */ public WritableImage snapshot(WritableImage image) { Toolkit.getToolkit().checkFxUserThread(); return doSnapshot(image); } /** * Takes a snapshot of this scene at the next frame and calls the * specified callback method when the image is ready. * CSS and layout processing will be done for the scene prior to * rendering it. * The entire destination image is cleared using the fill {@code Paint} * of this scene. The nodes in the scene are then rendered to the image. * The point (0,0) in scene coordinates is mapped to (0,0) in the image. * If the image is smaller than the size of the scene, then the rendering * will be clipped by the image. * *

* This is an asynchronous call, which means that other * events or animation might be processed before the scene is rendered. * If any such events modify a node in the scene that modification will * be reflected in the rendered image (as it will also be reflected in * the frame rendered to the Stage). *

* *

* When taking a snapshot of a scene that is being animated, either * explicitly by the application or implicitly (such as chart animation), * the snapshot will be rendered based on the state of the scene graph at * the moment the snapshot is taken and will not reflect any subsequent * animation changes. *

* * @param callback a class whose call method will be called when the image * is ready. The SnapshotResult that is passed into the call method of * the callback will contain the rendered image and the source scene * that was rendered. The callback parameter must not be null. * * @param image the writable image that will be used to hold the rendered scene. * It may be null in which case a new WritableImage will be constructed. * If the image is non-null, the scene will be rendered into the * existing image. * In this case, the width and height of the image determine the area * that is rendered instead of the width and height of the scene. * * @throws IllegalStateException if this method is called on a thread * other than the JavaFX Application Thread. * * @throws NullPointerException if the callback parameter is null. * @since JavaFX 2.2 */ public void snapshot(Callback callback, WritableImage image) { Toolkit.getToolkit().checkFxUserThread(); if (callback == null) { throw new NullPointerException("The callback must not be null"); } final Callback theCallback = callback; final WritableImage theImage = image; // Create a deferred runnable that will be run from a pulse listener // that is called after all of the scenes have been synced but before // any of them have been rendered. final Runnable snapshotRunnable = () -> { WritableImage img = doSnapshot(theImage); // System.err.println("Calling snapshot callback"); SnapshotResult result = new SnapshotResult(img, Scene.this, null); try { Void v = theCallback.call(result); } catch (Throwable th) { System.err.println("Exception in snapshot callback"); th.printStackTrace(System.err); } }; // System.err.println("Schedule a snapshot in the future"); addSnapshotRunnable(snapshotRunnable); } /** * Defines the mouse cursor for this {@code Scene}. */ private ObjectProperty cursor; public final void setCursor(Cursor value) { cursorProperty().set(value); } public final Cursor getCursor() { return cursor == null ? null : cursor.get(); } public final ObjectProperty cursorProperty() { if (cursor == null) { cursor = new ObjectPropertyBase<>() { @Override protected void invalidated() { markCursorDirty(); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "cursor"; } }; } return cursor; } /** * Looks for any node within the scene graph based on the specified CSS selector. * If more than one node matches the specified selector, this function * returns the first of them. * If no nodes are found with this id, then null is returned. * * @param selector The css selector to look up * @return the {@code Node} in the scene which matches the CSS {@code selector}, * or {@code null} if none is found. */ public Node lookup(String selector) { return getRoot().lookup(selector); } /** * A ObservableList of string URLs linking to the stylesheets to use with this scene's * contents. For additional information about using CSS with the * scene graph, see the CSS Reference * Guide. */ private final ObservableList stylesheets = new TrackableObservableList<>() { @Override protected void onChanged(Change c) { StyleManager.getInstance().stylesheetsChanged(Scene.this, c); // RT-9784 - if stylesheet is removed, reset styled properties to // their initial value. c.reset(); while(c.next()) { if (c.wasRemoved() == false) { continue; } break; // no point in resetting more than once... } getRoot().reapplyCSS(); } }; /** * Gets an observable list of string URLs linking to the stylesheets to use * with this scene's contents. *

* The URL is a hierarchical URI of the form [scheme:][//authority][path]. If the URL * does not have a [scheme:] component, the URL is considered to be the [path] component only. * Any leading '/' character of the [path] is ignored and the [path] is treated as a path relative to * the root of the application's classpath. *

* The RFC 2397 "data" scheme for URLs is supported in addition to the protocol handlers that * are registered for the application. * If a URL uses the "data" scheme and the MIME type is either empty, "text/plain", or "text/css", * the payload will be interpreted as a CSS file. * If the MIME type is "application/octet-stream", the payload will be interpreted as a binary * CSS file (see {@link Stylesheet#convertToBinary(File, File)}). *


     *
     * package com.example.javafx.app;
     *
     * import javafx.application.Application;
     * import javafx.scene.Group;
     * import javafx.scene.Scene;
     * import javafx.stage.Stage;
     *
     * public class MyApp extends Application {
     *
     *     {@literal @}Override public void start(Stage stage) {
     *         Scene scene = new Scene(new Group());
     *         scene.getStylesheets().add("/com/example/javafx/app/mystyles.css");
     *         stage.setScene(scene);
     *         stage.show();
     *     }
     *
     *     public static void main(String[] args) {
     *         launch(args);
     *     }
     * }
     * 
* For additional information about using CSS with the scene graph, * see the CSS Reference Guide. * * @return the list of stylesheets to use with this scene */ public final ObservableList getStylesheets() { return stylesheets; } private ObjectProperty userAgentStylesheet = null; /** * Gets the userAgentStylesheet property. * @return the userAgentStylesheet property * @see #getUserAgentStylesheet() * @see #setUserAgentStylesheet(String) * @since JavaFX 8u20 */ public final ObjectProperty userAgentStylesheetProperty() { if (userAgentStylesheet == null) { userAgentStylesheet = new SimpleObjectProperty<>(Scene.this, "userAgentStylesheet", null) { @Override protected void invalidated() { StyleManager.getInstance().forget(Scene.this); getRoot().reapplyCSS(); } }; } return userAgentStylesheet; } /** * Get the URL of the user-agent stylesheet that will be used by this Scene. If the URL has not been set, * the platform-default user-agent stylesheet will be used. *

* For additional information about using CSS with the scene graph, * see the CSS Reference Guide. *

* @return The URL of the user-agent stylesheet that will be used by this Scene, * or null if has not been set. * @since JavaFX 8u20 */ public final String getUserAgentStylesheet() { return userAgentStylesheet == null ? null : userAgentStylesheet.get(); } /** * Set the URL of the user-agent stylesheet that will be used by this Scene in place of the * the platform-default user-agent stylesheet. If the URL does not resolve to a valid location, * the platform-default user-agent stylesheet will be used. *

* The URL is a hierarchical URI of the form [scheme:][//authority][path]. If the URL * does not have a [scheme:] component, the URL is considered to be the [path] component only. * Any leading '/' character of the [path] is ignored and the [path] is treated as a path relative to * the root of the application's classpath. *

* The RFC 2397 "data" scheme for URLs is supported in addition to the protocol handlers that * are registered for the application. * If a URL uses the "data" scheme and the MIME type is either empty, "text/plain", or "text/css", * the payload will be interpreted as a CSS file. * If the MIME type is "application/octet-stream", the payload will be interpreted as a binary * CSS file (see {@link Stylesheet#convertToBinary(File, File)}). *

* For additional information about using CSS with the scene graph, * see the CSS Reference Guide. * * @param url the URL of the user-agent stylesheet * @since JavaFX 8u20 */ public final void setUserAgentStylesheet(String url) { userAgentStylesheetProperty().set(url); } /** * Retrieves the depth buffer attribute for this scene. * @return the depth buffer attribute. */ public final boolean isDepthBuffer() { return depthBuffer; } boolean isDepthBufferInternal() { if (!Platform.isSupported(ConditionalFeature.SCENE3D)) { return false; } return depthBuffer; } private void init(double width, double height) { if (width >= 0) { widthSetByUser = width; setWidth((float)width); } if (height >= 0) { heightSetByUser = height; setHeight((float)height); } sizeInitialized = (widthSetByUser >= 0 && heightSetByUser >= 0); } private void init() { if (PerformanceTracker.isLoggingEnabled()) { PerformanceTracker.logEvent("Scene.init for [" + this + "]"); } mouseHandler = new MouseHandler(); clickGenerator = new ClickGenerator(); if (PerformanceTracker.isLoggingEnabled()) { PerformanceTracker.logEvent("Scene.init for [" + this + "] - finished"); } } void preferredSize() { final Parent root = getRoot(); // one or the other isn't initialized, need to perform layout in // order to ensure we can properly measure the preferred size of the // scene doCSSPass(); resizeRootToPreferredSize(root); doLayoutPass(); if (widthSetByUser < 0) { setWidth(root.isResizable()? root.getLayoutX() + root.getTranslateX() + root.getLayoutBounds().getWidth() : root.getBoundsInParent().getMaxX()); } else { setWidth(widthSetByUser); } if (heightSetByUser < 0) { setHeight(root.isResizable()? root.getLayoutY() + root.getTranslateY() + root.getLayoutBounds().getHeight() : root.getBoundsInParent().getMaxY()); } else { setHeight(heightSetByUser); } sizeInitialized = (getWidth() > 0) && (getHeight() > 0); PerformanceTracker.logEvent("Scene preferred bounds computation complete"); } final void resizeRootToPreferredSize(Parent root) { final double preferredWidth; final double preferredHeight; final Orientation contentBias = root.getContentBias(); if (contentBias == null) { preferredWidth = getPreferredWidth(root, widthSetByUser, -1); preferredHeight = getPreferredHeight(root, heightSetByUser, -1); } else if (contentBias == Orientation.HORIZONTAL) { // height depends on width preferredWidth = getPreferredWidth(root, widthSetByUser, -1); preferredHeight = getPreferredHeight(root, heightSetByUser, preferredWidth); } else /* if (contentBias == Orientation.VERTICAL) */ { // width depends on height preferredHeight = getPreferredHeight(root, heightSetByUser, -1); preferredWidth = getPreferredWidth(root, widthSetByUser, preferredHeight); } root.resize(preferredWidth, preferredHeight); } private static double getPreferredWidth(Parent root, double forcedWidth, double height) { if (forcedWidth >= 0) { return forcedWidth; } final double normalizedHeight = (height >= 0) ? height : -1; return root.boundedSize(root.prefWidth(normalizedHeight), root.minWidth(normalizedHeight), root.maxWidth(normalizedHeight)); } private static double getPreferredHeight(Parent root, double forcedHeight, double width) { if (forcedHeight >= 0) { return forcedHeight; } final double normalizedWidth = (width >= 0) ? width : -1; return root.boundedSize(root.prefHeight(normalizedWidth), root.minHeight(normalizedWidth), root.maxHeight(normalizedWidth)); } private PerformanceTracker tracker; private static final Object trackerMonitor = new Object(); // mouse events handling private MouseHandler mouseHandler; private ClickGenerator clickGenerator; // gesture events handling private Point2D cursorScreenPos; private Point2D cursorScenePos; private static class TouchGesture { WeakReference target; Point2D sceneCoords; Point2D screenCoords; boolean finished; } private final TouchGesture scrollGesture = new TouchGesture(); private final TouchGesture zoomGesture = new TouchGesture(); private final TouchGesture rotateGesture = new TouchGesture(); private final TouchGesture swipeGesture = new TouchGesture(); // touch events handling private TouchMap touchMap = new TouchMap(); private TouchEvent nextTouchEvent = null; private TouchPoint[] touchPoints = null; private int touchEventSetId = 0; private int touchPointIndex = 0; private Map touchTargets = new HashMap<>(); void processMouseEvent(MouseEvent e) { mouseHandler.process(e, false); } private void processMenuEvent(double x2, double y2, double xAbs, double yAbs, boolean isKeyboardTrigger) { EventTarget eventTarget = null; Scene.inMousePick = true; if (isKeyboardTrigger) { Node sceneFocusOwner = getFocusOwner(); // for keyboard triggers set coordinates inside focus owner final double xOffset = xAbs - x2; final double yOffset = yAbs - y2; if (sceneFocusOwner != null) { final Bounds bounds = sceneFocusOwner.localToScene( sceneFocusOwner.getBoundsInLocal()); x2 = bounds.getMinX() + bounds.getWidth() / 4; y2 = bounds.getMinY() + bounds.getHeight() / 2; eventTarget = sceneFocusOwner; } else { x2 = Scene.this.getWidth() / 4; y2 = Scene.this.getWidth() / 2; eventTarget = Scene.this; } xAbs = x2 + xOffset; yAbs = y2 + yOffset; } final PickResult res = pick(x2, y2); if (!isKeyboardTrigger) { eventTarget = res.getIntersectedNode(); if (eventTarget == null) { eventTarget = this; } } if (eventTarget != null) { ContextMenuEvent context = new ContextMenuEvent(ContextMenuEvent.CONTEXT_MENU_REQUESTED, x2, y2, xAbs, yAbs, isKeyboardTrigger, res); Event.fireEvent(eventTarget, context); } Scene.inMousePick = false; } private void processGestureEvent(GestureEvent e, TouchGesture gesture) { EventTarget pickedTarget = null; if (e.getEventType() == ZoomEvent.ZOOM_STARTED || e.getEventType() == RotateEvent.ROTATION_STARTED || e.getEventType() == ScrollEvent.SCROLL_STARTED) { gesture.target = null; gesture.finished = false; } if (gesture.target != null && (!gesture.finished || e.isInertia())) { pickedTarget = gesture.target.get(); } else { pickedTarget = e.getPickResult().getIntersectedNode(); if (pickedTarget == null) { pickedTarget = this; } } if (e.getEventType() == ZoomEvent.ZOOM_STARTED || e.getEventType() == RotateEvent.ROTATION_STARTED || e.getEventType() == ScrollEvent.SCROLL_STARTED) { gesture.target = new WeakReference<>(pickedTarget); } if (e.getEventType() != ZoomEvent.ZOOM_FINISHED && e.getEventType() != RotateEvent.ROTATION_FINISHED && e.getEventType() != ScrollEvent.SCROLL_FINISHED && !e.isInertia()) { gesture.sceneCoords = new Point2D(e.getSceneX(), e.getSceneY()); gesture.screenCoords = new Point2D(e.getScreenX(), e.getScreenY()); } if (pickedTarget != null) { Event.fireEvent(pickedTarget, e); } if (e.getEventType() == ZoomEvent.ZOOM_FINISHED || e.getEventType() == RotateEvent.ROTATION_FINISHED || e.getEventType() == ScrollEvent.SCROLL_FINISHED) { gesture.finished = true; } } private void processTouchEvent(TouchEvent e, TouchPoint[] touchPoints) { inMousePick = true; touchEventSetId++; List touchList = Arrays.asList(touchPoints); // fire all the events for (TouchPoint tp : touchPoints) { if (tp.getTarget() != null) { EventType type = null; switch (tp.getState()) { case MOVED: type = TouchEvent.TOUCH_MOVED; break; case PRESSED: type = TouchEvent.TOUCH_PRESSED; break; case RELEASED: type = TouchEvent.TOUCH_RELEASED; break; case STATIONARY: type = TouchEvent.TOUCH_STATIONARY; break; } for (TouchPoint t : touchPoints) { TouchPointHelper.reset(t); } TouchEvent te = new TouchEvent(type, tp, touchList, touchEventSetId, e.isShiftDown(), e.isControlDown(), e.isAltDown(), e.isMetaDown()); Event.fireEvent(tp.getTarget(), te); } } // process grabbing for (TouchPoint tp : touchPoints) { EventTarget grabbed = tp.getGrabbed(); if (grabbed != null) { touchTargets.put(tp.getId(), grabbed); } if (grabbed == null || tp.getState() == TouchPoint.State.RELEASED) { touchTargets.remove(tp.getId()); } } inMousePick = false; } /** * Note: The only user of this method is in unit test: PickAndContainTest. */ Node test_pick(double x, double y) { inMousePick = true; PickResult result = mouseHandler.pickNode(new PickRay(x, y, 1, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY)); inMousePick = false; if (result != null) { return result.getIntersectedNode(); } return null; } private PickResult pick(final double x, final double y) { pick(tmpTargetWrapper, x, y); return tmpTargetWrapper.getResult(); } private boolean isInScene(double x, double y) { if (x < 0 || y < 0 || x > getWidth() || y > getHeight()) { return false; } Window w = getWindow(); if (w instanceof Stage && ((Stage) w).getStyle() == StageStyle.TRANSPARENT && getFill() == null) { return false; } return true; } private void pick(TargetWrapper target, final double x, final double y) { final PickRay pickRay = getEffectiveCamera().computePickRay( x, y, null); final double mag = pickRay.getDirectionNoClone().length(); pickRay.getDirectionNoClone().normalize(); final PickResult res = mouseHandler.pickNode(pickRay); if (res != null) { target.setNodeResult(res); } else { //TODO: is this the intersection with projection plane? Vec3d o = pickRay.getOriginNoClone(); Vec3d d = pickRay.getDirectionNoClone(); target.setSceneResult(new PickResult( null, new Point3D( o.x + mag * d.x, o.y + mag * d.y, o.z + mag * d.z), mag), isInScene(x, y) ? this : null); } } /* ************************************************************************* * * * Key Events and Focus Traversal * * * **************************************************************************/ private void windowForSceneChanged(Window oldWindow, Window newWindow) { if (oldWindow != null) { oldWindow.focusedProperty().removeListener(sceneWindowFocusedListener); } if (newWindow != null) { newWindow.focusedProperty().addListener(sceneWindowFocusedListener); setWindowFocused(newWindow.isFocused()); } else { setWindowFocused(false); } } private final InvalidationListener sceneWindowFocusedListener = valueModel -> setWindowFocused(((ReadOnlyBooleanProperty)valueModel).get()); /** * Stores whether the window associated with this scene is currently focused. */ private boolean windowFocused; private void setWindowFocused(boolean value) { windowFocused = value; Node node = getFocusOwner(); if (node != null) { node.setFocusQuietly(windowFocused, focusOwner.focusVisible); node.notifyFocusListeners(); } if (windowFocused && accessible != null) { accessible.sendNotification(AccessibleAttribute.FOCUS_NODE); } if (!windowFocused) { getInternalEventDispatcher().getKeyboardShortcutsHandler().setMnemonicsDisplayEnabled(false); } } /** * Set to true if something has happened to the focused node that makes * it no longer eligible to have the focus. * */ private boolean focusDirty = true; final void setFocusDirty(boolean value) { if (!focusDirty) { Toolkit.getToolkit().requestNextPulse(); } focusDirty = value; } final boolean isFocusDirty() { return focusDirty; } private TopMostTraversalEngine traversalEngine = new SceneTraversalEngine(this); /** * Traverses focus from the given node in the given direction. */ boolean traverse(Node node, Direction dir, TraversalMethod method) { if (node.getSubScene() != null) { return node.getSubScene().traverse(node, dir, method); } return traversalEngine.trav(node, dir, method) != null; } /** * Moves the focus to a reasonable initial location. Called when a scene's * focus is dirty and there's no current owner, or if the owner has been * removed from the scene. */ private void focusInitial() { traversalEngine.traverseToFirst(); } /** * Moves the focus to a reasonble location "near" the given node. * Called when the focused node is no longer eligible to have * the focus because it has become invisible or disabled. This * function assumes that it is still a member of the same scene. */ private void focusIneligible(Node node) { traverse(node, Direction.NEXT, TraversalMethod.DEFAULT); } void processKeyEvent(KeyEvent e) { if (dndGesture != null) { if (!dndGesture.processKey(e)) { dndGesture = null; } } final Node sceneFocusOwner = getFocusOwner(); final EventTarget eventTarget = (sceneFocusOwner != null && sceneFocusOwner.getScene() == Scene.this) ? sceneFocusOwner : Scene.this; // send the key event to the current focus owner or to scene if // the focus owner is not set Event.fireEvent(eventTarget, e); } void requestFocus(Node node, boolean focusVisible) { if (node == null) { setFocusOwner(null, false); } else if (node.isCanReceiveFocus()) { setFocusOwner(node, focusVisible); } } /** * The scene's current focus owner node. This node's "focused" * variable might be false if this scene has no window, or if the * window is inactive (window.focused == false). * @since JavaFX 2.2 */ private FocusOwnerProperty focusOwner = new FocusOwnerProperty(); private class FocusOwnerProperty extends ReadOnlyObjectWrapper { Node oldFocusOwner; /** * Stores whether the current focus owner visibly indicates focus. * This value is used to restore visible focus when a window loses * and re-gains focus. */ boolean focusVisible; @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "focusOwner"; } @Override protected void invalidated() { if (oldFocusOwner != null) { oldFocusOwner.setFocusQuietly(false, false); } Node value = get(); if (value != null) { value.setFocusQuietly(windowFocused, focusVisible); if (value != oldFocusOwner) { value.getScene().enableInputMethodEvents( value.getInputMethodRequests() != null && value.getOnInputMethodTextChanged() != null); } } // for the rest of the method we need to update the oldFocusOwner // and use a local copy of it because the user handlers can cause // recurrent calls of requestFocus Node localOldOwner = oldFocusOwner; oldFocusOwner = value; if (localOldOwner != null) { localOldOwner.notifyFocusListeners(); } if (value != null) { value.notifyFocusListeners(); } PlatformLogger logger = Logging.getFocusLogger(); if (logger.isLoggable(Level.FINE)) { if (value == get()) { logger.fine("Changed focus from " + localOldOwner + " to " + value); } else { logger.fine("Changing focus from " + localOldOwner + " to " + value + " canceled by nested requestFocus"); } } if (accessible != null) { accessible.sendNotification(AccessibleAttribute.FOCUS_NODE); } } }; public final ReadOnlyObjectProperty focusOwnerProperty() { return focusOwner.getReadOnlyProperty(); } public final Node getFocusOwner() { return focusOwner.get(); } private void setFocusOwner(Node node, boolean focusVisible) { // Cancel IM composition if there is one in progress. // This needs to be done before the focus owner is switched as it // generates event that needs to be delivered to the old focus owner. if (focusOwner.oldFocusOwner != null) { final Scene s = focusOwner.oldFocusOwner.getScene(); if (s != null) { final TKScene peer = s.getPeer(); if (peer != null) { peer.finishInputMethodComposition(); } } } // Store the current focusVisible state of the focus owner in case it needs to be // restored when a window loses and re-gains focus. focusOwner.focusVisible = focusVisible; if (focusOwner.get() != node) { // If the focus owner has changed, FocusOwnerProperty::invalidated will update // the node's focusVisible flag. focusOwner.set(node); } else if (node != null) { // If the focus owner has not changed (i.e. only focusVisible has changed), // FocusOwnerProperty::invalidated will not be called, therefore we need to // update the node's focusVisible flag manually. node.focusVisible.set(focusVisible); node.focusVisible.notifyListeners(); } } // For testing. void focusCleanup() { scenePulseListener.focusCleanup(); } private void processInputMethodEvent(InputMethodEvent e) { Node node = getFocusOwner(); if (node != null) { node.fireEvent(e); } } void enableInputMethodEvents(boolean enable) { if (peer != null) { peer.enableInputMethodEvents(enable); } } /** * Returns true if this scene is quiescent, i.e. it has no activity * pending on it such as CSS processing or layout requests. * * Intended to be used for tests only * * @return boolean indicating whether the scene is quiescent */ boolean isQuiescent() { final Parent r = getRoot(); return !isFocusDirty() && (r == null || (r.cssFlag == CssFlags.CLEAN && r.layoutFlag == LayoutFlags.CLEAN)); } /** * A listener for pulses, used for testing. If non-null, this is called at * the very end of ScenePulseListener.pulse(). * * Intended to be used for tests only */ Runnable testPulseListener = null; /** * Set the specified dirty bit and mark the peer as dirty */ private void markDirty(DirtyBits dirtyBit) { setDirty(dirtyBit); if (peer != null) { Toolkit.getToolkit().requestNextPulse(); } } /** * Set the specified dirty bit */ private void setDirty(DirtyBits dirtyBit) { dirtyBits.add(dirtyBit); } /** * Test the specified dirty bit */ private boolean isDirty(DirtyBits dirtyBit) { return dirtyBits.contains(dirtyBit); } /** * Test whether the dirty bits are empty */ private boolean isDirtyEmpty() { return dirtyBits.isEmpty(); } /** * Clear all dirty bits */ private void clearDirty() { dirtyBits.clear(); } private enum DirtyBits { FILL_DIRTY, ROOT_DIRTY, CAMERA_DIRTY, LIGHTS_DIRTY, CURSOR_DIRTY; } private List lights = new ArrayList<>(); // @param light must not be null final void addLight(LightBase light) { if (!lights.contains(light)) { lights.add(light); markDirty(DirtyBits.LIGHTS_DIRTY); } } final void removeLight(LightBase light) { if (lights.remove(light)) { markDirty(DirtyBits.LIGHTS_DIRTY); } } /** * PG Light synchronizer. */ private void syncLights() { if (!isDirty(DirtyBits.LIGHTS_DIRTY)) { return; } inSynchronizer = true; NGLightBase peerLights[] = peer.getLights(); if (!lights.isEmpty() || (peerLights != null)) { if (lights.isEmpty()) { peer.setLights(null); } else { if (peerLights == null || peerLights.length < lights.size()) { peerLights = new NGLightBase[lights.size()]; } int i = 0; for (; i < lights.size(); i++) { peerLights[i] = lights.get(i).getPeer(); } // Clear the rest of the list while (i < peerLights.length && peerLights[i] != null) { peerLights[i++] = null; } peer.setLights(peerLights); } } inSynchronizer = false; } //INNER CLASSES /* ***************************************************************************** * * * Scene Pulse Listener * * * ******************************************************************************/ class ScenePulseListener implements TKPulseListener { private boolean firstPulse = true; /** * PG synchronizer. Called once per frame from the pulse listener. * This function calls the synchronizePGNode method on each node in * the dirty list. */ private void synchronizeSceneNodes() { Toolkit.getToolkit().checkFxUserThread(); Scene.inSynchronizer = true; // if dirtyNodes is null then that means this Scene has not yet been // synchronized, and so we will simply synchronize every node in the // scene and then create the dirty nodes array list if (Scene.this.dirtyNodes == null) { // must do this recursively syncAll(getRoot()); dirtyNodes = new Node[MIN_DIRTY_CAPACITY]; } else { // This is not the first time this scene has been synchronized, // so we will only synchronize those nodes that need it for (int i = 0 ; i < dirtyNodesSize; ++i) { Node node = dirtyNodes[i]; dirtyNodes[i] = null; if (node.getScene() == Scene.this) { node.syncPeer(); } } dirtyNodesSize = 0; } Scene.inSynchronizer = false; } /** * Recursive function for synchronizing every node in the scenegraph. * The return value is the number of nodes in the graph. */ private int syncAll(Node node) { node.syncPeer(); int size = 1; if (node instanceof Parent) { Parent p = (Parent) node; final int childrenCount = p.getChildren().size(); for (int i = 0; i < childrenCount; i++) { Node n = p.getChildren().get(i); if (n != null) { size += syncAll(n); } } } else if (node instanceof SubScene) { SubScene subScene = (SubScene)node; size += syncAll(subScene.getRoot()); } if (node.getClip() != null) { size += syncAll(node.getClip()); } return size; } private void synchronizeSceneProperties() { inSynchronizer = true; if (isDirty(DirtyBits.ROOT_DIRTY)) { peer.setRoot(getRoot().getPeer()); } if (isDirty(DirtyBits.FILL_DIRTY)) { Toolkit tk = Toolkit.getToolkit(); peer.setFillPaint(getFill() == null ? null : tk.getPaint(getFill())); } // new camera was set on the scene or old camera changed final Camera cam = getEffectiveCamera(); if (isDirty(DirtyBits.CAMERA_DIRTY)) { NodeHelper.updatePeer(cam); peer.setCamera((NGCamera) cam.getPeer()); } if (isDirty(DirtyBits.CURSOR_DIRTY)) { mouseHandler.updateCursor(getCursor()); mouseHandler.updateCursorFrame(); } clearDirty(); inSynchronizer = false; } /** * The focus is considered dirty if something happened to * the scene graph that may require the focus to be moved. * This must handle cases where (a) the focus owner may have * become ineligible to have the focus, and (b) where the focus * owner is null and a node may have become traversable and eligible. */ private void focusCleanup() { if (Scene.this.isFocusDirty()) { final Node oldOwner = Scene.this.getFocusOwner(); if (oldOwner == null) { Scene.this.focusInitial(); } else if (oldOwner.getScene() != Scene.this) { Scene.this.requestFocus(null, false); Scene.this.focusInitial(); } else if (!oldOwner.isCanReceiveFocus()) { Scene.this.requestFocus(null, false); Scene.this.focusIneligible(oldOwner); } Scene.this.setFocusDirty(false); } } @Override public void pulse() { if (Scene.this.tracker != null) { Scene.this.tracker.pulse(); } if (firstPulse) { PerformanceTracker.logEvent("Scene - first repaint"); } focusCleanup(); disposeAccessibles(); // run any scene pre pulse listeners immediately _before_ css / layout, // and before scene synchronization if (preLayoutPulseListeners != null) { for (Runnable r : preLayoutPulseListeners) { r.run(); } } if (PULSE_LOGGING_ENABLED) { PulseLogger.newPhase("CSS Pass"); } Scene.this.doCSSPass(); if (PULSE_LOGGING_ENABLED) { PulseLogger.newPhase("Layout Pass"); } Scene.this.doLayoutPass(); // run any scene post pulse listeners immediately _after_ css / layout, // and before scene synchronization if (postLayoutPulseListeners != null) { for (Runnable r : postLayoutPulseListeners) { r.run(); } } boolean dirty = dirtyNodes == null || dirtyNodesSize != 0 || !isDirtyEmpty(); if (dirty) { if (PULSE_LOGGING_ENABLED) { PulseLogger.newPhase("Update bounds"); } getRoot().updateBounds(); if (peer != null) { try { if (PULSE_LOGGING_ENABLED) { PulseLogger.newPhase("Waiting for previous rendering"); } peer.waitForRenderingToComplete(); peer.waitForSynchronization(); // synchronize scene properties if (PULSE_LOGGING_ENABLED) { PulseLogger.newPhase("Copy state to render graph"); } syncLights(); synchronizeSceneProperties(); // Run the synchronizer synchronizeSceneNodes(); Scene.this.mouseHandler.pulse(); // Tell the scene peer that it needs to repaint peer.markDirty(); } finally { peer.releaseSynchronization(true); } } else { if (PULSE_LOGGING_ENABLED) { PulseLogger.newPhase("Synchronize with null peer"); } synchronizeSceneNodes(); Scene.this.mouseHandler.pulse(); } if (Scene.this.getRoot().cssFlag != CssFlags.CLEAN) { NodeHelper.markDirty(Scene.this.getRoot(), com.sun.javafx.scene.DirtyBits.NODE_CSS); } } // required for image cursor created from animated image Scene.this.mouseHandler.updateCursorFrame(); if (firstPulse) { if (PerformanceTracker.isLoggingEnabled()) { PerformanceTracker.logEvent("Scene - first repaint - layout complete"); if (PrismSettings.perfLogFirstPaintFlush) { PerformanceTracker.outputLog(); } if (PrismSettings.perfLogFirstPaintExit) { System.exit(0); } } firstPulse = false; } if (testPulseListener != null) { testPulseListener.run(); } } } /* ***************************************************************************** * * * Scene Peer Listener * * * ******************************************************************************/ class ScenePeerListener implements TKSceneListener { @Override public void changedLocation(float x, float y) { if (x != Scene.this.getX()) { Scene.this.setX(x); } if (y != Scene.this.getY()) { Scene.this.setY(y); } } @Override public void changedSize(float w, float h) { if (w != Scene.this.getWidth()) Scene.this.setWidth(w); if (h != Scene.this.getHeight()) Scene.this.setHeight(h); } @Override public void mouseEvent(EventType type, double x, double y, double screenX, double screenY, MouseButton button, boolean popupTrigger, boolean synthesized, boolean shiftDown, boolean controlDown, boolean altDown, boolean metaDown, boolean primaryDown, boolean middleDown, boolean secondaryDown, boolean backDown, boolean forwardDown) { MouseEvent mouseEvent = new MouseEvent(type, x, y, screenX, screenY, button, 0, // click count will be adjusted by clickGenerator later anyway shiftDown, controlDown, altDown, metaDown, primaryDown, middleDown, secondaryDown, backDown, forwardDown, synthesized, popupTrigger, false, null); processMouseEvent(mouseEvent); } @Override public void keyEvent(KeyEvent keyEvent) { processKeyEvent(keyEvent); } @Override public void inputMethodEvent(EventType type, ObservableList composed, String committed, int caretPosition) { InputMethodEvent inputMethodEvent = new InputMethodEvent( type, composed, committed, caretPosition); processInputMethodEvent(inputMethodEvent); } @Override public void menuEvent(double x, double y, double xAbs, double yAbs, boolean isKeyboardTrigger) { Scene.this.processMenuEvent(x, y, xAbs,yAbs, isKeyboardTrigger); } @Override public void scrollEvent( EventType eventType, double scrollX, double scrollY, double totalScrollX, double totalScrollY, double xMultiplier, double yMultiplier, int touchCount, int scrollTextX, int scrollTextY, int defaultTextX, int defaultTextY, double x, double y, double screenX, double screenY, boolean _shiftDown, boolean _controlDown, boolean _altDown, boolean _metaDown, boolean _direct, boolean _inertia) { ScrollEvent.HorizontalTextScrollUnits xUnits = scrollTextX > 0 ? ScrollEvent.HorizontalTextScrollUnits.CHARACTERS : ScrollEvent.HorizontalTextScrollUnits.NONE; double xText = scrollTextX < 0 ? 0 : scrollTextX * scrollX; ScrollEvent.VerticalTextScrollUnits yUnits = scrollTextY > 0 ? ScrollEvent.VerticalTextScrollUnits.LINES : (scrollTextY < 0 ? ScrollEvent.VerticalTextScrollUnits.PAGES : ScrollEvent.VerticalTextScrollUnits.NONE); double yText = scrollTextY < 0 ? scrollY : scrollTextY * scrollY; xMultiplier = defaultTextX > 0 && scrollTextX >= 0 ? Math.round(xMultiplier * scrollTextX / defaultTextX) : xMultiplier; yMultiplier = defaultTextY > 0 && scrollTextY >= 0 ? Math.round(yMultiplier * scrollTextY / defaultTextY) : yMultiplier; if (eventType == ScrollEvent.SCROLL_FINISHED) { x = scrollGesture.sceneCoords.getX(); y = scrollGesture.sceneCoords.getY(); screenX = scrollGesture.screenCoords.getX(); screenY = scrollGesture.screenCoords.getY(); } else if (Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(screenX) || Double.isNaN(screenY)) { if (cursorScenePos == null || cursorScreenPos == null) { return; } x = cursorScenePos.getX(); y = cursorScenePos.getY(); screenX = cursorScreenPos.getX(); screenY = cursorScreenPos.getY(); } inMousePick = true; Scene.this.processGestureEvent(new ScrollEvent( eventType, x, y, screenX, screenY, _shiftDown, _controlDown, _altDown, _metaDown, _direct, _inertia, scrollX * xMultiplier, scrollY * yMultiplier, totalScrollX * xMultiplier, totalScrollY * yMultiplier, xMultiplier, yMultiplier, xUnits, xText, yUnits, yText, touchCount, pick(x, y)), scrollGesture); inMousePick = false; } @Override public void zoomEvent( EventType eventType, double zoomFactor, double totalZoomFactor, double x, double y, double screenX, double screenY, boolean _shiftDown, boolean _controlDown, boolean _altDown, boolean _metaDown, boolean _direct, boolean _inertia) { if (eventType == ZoomEvent.ZOOM_FINISHED) { x = zoomGesture.sceneCoords.getX(); y = zoomGesture.sceneCoords.getY(); screenX = zoomGesture.screenCoords.getX(); screenY = zoomGesture.screenCoords.getY(); } else if (Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(screenX) || Double.isNaN(screenY)) { if (cursorScenePos == null || cursorScreenPos == null) { return; } x = cursorScenePos.getX(); y = cursorScenePos.getY(); screenX = cursorScreenPos.getX(); screenY = cursorScreenPos.getY(); } inMousePick = true; Scene.this.processGestureEvent(new ZoomEvent(eventType, x, y, screenX, screenY, _shiftDown, _controlDown, _altDown, _metaDown, _direct, _inertia, zoomFactor, totalZoomFactor, pick(x, y)), zoomGesture); inMousePick = false; } @Override public void rotateEvent( EventType eventType, double angle, double totalAngle, double x, double y, double screenX, double screenY, boolean _shiftDown, boolean _controlDown, boolean _altDown, boolean _metaDown, boolean _direct, boolean _inertia) { if (eventType == RotateEvent.ROTATION_FINISHED) { x = rotateGesture.sceneCoords.getX(); y = rotateGesture.sceneCoords.getY(); screenX = rotateGesture.screenCoords.getX(); screenY = rotateGesture.screenCoords.getY(); } else if (Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(screenX) || Double.isNaN(screenY)) { if (cursorScenePos == null || cursorScreenPos == null) { return; } x = cursorScenePos.getX(); y = cursorScenePos.getY(); screenX = cursorScreenPos.getX(); screenY = cursorScreenPos.getY(); } inMousePick = true; Scene.this.processGestureEvent(new RotateEvent( eventType, x, y, screenX, screenY, _shiftDown, _controlDown, _altDown, _metaDown, _direct, _inertia, angle, totalAngle, pick(x, y)), rotateGesture); inMousePick = false; } @Override public void swipeEvent( EventType eventType, int touchCount, double x, double y, double screenX, double screenY, boolean _shiftDown, boolean _controlDown, boolean _altDown, boolean _metaDown, boolean _direct) { if (Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(screenX) || Double.isNaN(screenY)) { if (cursorScenePos == null || cursorScreenPos == null) { return; } x = cursorScenePos.getX(); y = cursorScenePos.getY(); screenX = cursorScreenPos.getX(); screenY = cursorScreenPos.getY(); } inMousePick = true; Scene.this.processGestureEvent(new SwipeEvent( eventType, x, y, screenX, screenY, _shiftDown, _controlDown, _altDown, _metaDown, _direct, touchCount, pick(x, y)), swipeGesture); inMousePick = false; } @Override public void touchEventBegin( long time, int touchCount, boolean isDirect, boolean _shiftDown, boolean _controlDown, boolean _altDown, boolean _metaDown) { if (!isDirect) { nextTouchEvent = null; return; } nextTouchEvent = new TouchEvent( TouchEvent.ANY, null, null, 0, _shiftDown, _controlDown, _altDown, _metaDown); if (touchPoints == null || touchPoints.length != touchCount) { touchPoints = new TouchPoint[touchCount]; } touchPointIndex = 0; } @Override public void touchEventNext( TouchPoint.State state, long touchId, double x, double y, double screenX, double screenY) { inMousePick = true; if (nextTouchEvent == null) { // ignore indirect touch events return; } touchPointIndex++; int id = (state == TouchPoint.State.PRESSED ? touchMap.add(touchId) : touchMap.get(touchId)); if (state == TouchPoint.State.RELEASED) { touchMap.remove(touchId); } int order = touchMap.getOrder(id); if (order >= touchPoints.length) { throw new RuntimeException("Too many touch points reported"); } // pick target boolean isGrabbed = false; PickResult pickRes = pick(x, y); EventTarget pickedTarget = touchTargets.get(id); if (pickedTarget == null) { pickedTarget = pickRes.getIntersectedNode(); if (pickedTarget == null) { pickedTarget = Scene.this; } } else { isGrabbed = true; } TouchPoint tp = new TouchPoint(id, state, x, y, screenX, screenY, pickedTarget, pickRes); touchPoints[order] = tp; if (isGrabbed) { tp.grab(pickedTarget); } if (tp.getState() == TouchPoint.State.PRESSED) { tp.grab(pickedTarget); touchTargets.put(tp.getId(), pickedTarget); } else if (tp.getState() == TouchPoint.State.RELEASED) { touchTargets.remove(tp.getId()); } inMousePick = false; } @Override public void touchEventEnd() { if (nextTouchEvent == null) { // ignore indirect touch events return; } if (touchPointIndex != touchPoints.length) { throw new RuntimeException("Wrong number of touch points reported"); } Scene.this.processTouchEvent(nextTouchEvent, touchPoints); if (touchMap.cleanup()) { // gesture finished touchEventSetId = 0; } } @Override public Accessible getSceneAccessible() { return getAccessible(); } } private class ScenePeerPaintListener implements TKScenePaintListener { @Override public void frameRendered() { // must use tracker with synchronization since this method is called on render thread synchronized (trackerMonitor) { if (Scene.this.tracker != null) { Scene.this.tracker.frameRendered(); } } } } /* ***************************************************************************** * * * Drag and Drop * * * ******************************************************************************/ class DropTargetListener implements TKDropTargetListener { /* * This function is called when an drag operation enters a valid drop target. * This may be from either an internal or external dnd operation. */ @Override public TransferMode dragEnter(double x, double y, double screenX, double screenY, TransferMode transferMode, TKClipboard dragboard) { if (dndGesture == null) { dndGesture = new DnDGesture(); } Dragboard db = DragboardHelper.createDragboard(dragboard); dndGesture.dragboard = db; DragEvent dragEvent = new DragEvent(DragEvent.ANY, dndGesture.dragboard, x, y, screenX, screenY, transferMode, null, null, pick(x, y)); return dndGesture.processTargetEnterOver(dragEvent); } @Override public TransferMode dragOver(double x, double y, double screenX, double screenY, TransferMode transferMode) { if (Scene.this.dndGesture == null) { System.err.println("GOT A dragOver when dndGesture is null!"); return null; } else { if (dndGesture.dragboard == null) { throw new RuntimeException("dndGesture.dragboard is null in dragOver"); } DragEvent dragEvent = new DragEvent(DragEvent.ANY, dndGesture.dragboard, x, y, screenX, screenY, transferMode, null, null, pick(x, y)); return dndGesture.processTargetEnterOver(dragEvent); } } @Override public void dragExit(double x, double y, double screenX, double screenY) { if (dndGesture == null) { System.err.println("GOT A dragExit when dndGesture is null!"); } else { if (dndGesture.dragboard == null) { throw new RuntimeException("dndGesture.dragboard is null in dragExit"); } DragEvent dragEvent = new DragEvent(DragEvent.ANY, dndGesture.dragboard, x, y, screenX, screenY, null, null, null, pick(x, y)); dndGesture.processTargetExit(dragEvent); if (dndGesture.source == null) { dndGesture.dragboard = null; dndGesture = null; } } } @Override public TransferMode drop(double x, double y, double screenX, double screenY, TransferMode transferMode) { if (dndGesture == null) { System.err.println("GOT A drop when dndGesture is null!"); return null; } else { if (dndGesture.dragboard == null) { throw new RuntimeException("dndGesture.dragboard is null in dragDrop"); } DragEvent dragEvent = new DragEvent(DragEvent.ANY, dndGesture.dragboard, x, y, screenX, screenY, transferMode, null, null, pick(x, y)); // Data dropped to the app can be accessed without restriction DragboardHelper.setDataAccessRestriction(dndGesture.dragboard, false); TransferMode tm; try { tm = dndGesture.processTargetDrop(dragEvent); } finally { DragboardHelper.setDataAccessRestriction( dndGesture.dragboard, true); } if (dndGesture.source == null) { dndGesture.dragboard = null; dndGesture = null; } return tm; } } } class DragGestureListener implements TKDragGestureListener { @Override public void dragGestureRecognized(double x, double y, double screenX, double screenY, int button, TKClipboard dragboard) { Dragboard db = DragboardHelper.createDragboard(dragboard); dndGesture = new DnDGesture(); dndGesture.dragboard = db; // TODO: support mouse buttons in DragEvent DragEvent dragEvent = new DragEvent(DragEvent.ANY, db, x, y, screenX, screenY, null, null, null, pick(x, y)); dndGesture.processRecognized(dragEvent); dndGesture = null; } } /** * A Drag and Drop gesture has a lifespan that lasts from mouse * PRESSED event to mouse RELEASED event. */ class DnDGesture { private final double hysteresisSizeX = Toolkit.getToolkit().getMultiClickMaxX(); private final double hysteresisSizeY = Toolkit.getToolkit().getMultiClickMaxY(); private EventTarget source = null; private Set sourceTransferModes = null; private TransferMode acceptedTransferMode = null; private Dragboard dragboard = null; private EventTarget potentialTarget = null; private EventTarget target = null; private DragDetectedState dragDetected = DragDetectedState.NOT_YET; private double pressedX; private double pressedY; private List currentTargets = new ArrayList<>(); private List newTargets = new ArrayList<>(); private EventTarget fullPDRSource = null; /** * Fires event on a given target or on scene if the node is null */ private void fireEvent(EventTarget target, Event e) { if (target != null) { Event.fireEvent(target, e); } } /** * Called when DRAG_DETECTED event is going to be processed by * application */ private void processingDragDetected() { dragDetected = DragDetectedState.PROCESSING; } /** * Called after DRAG_DETECTED event has been processed by application */ private void dragDetectedProcessed() { dragDetected = DragDetectedState.DONE; final boolean hasContent = (dragboard != null) && (ClipboardHelper.contentPut(dragboard)); if (hasContent) { /* start DnD */ Toolkit.getToolkit().startDrag(Scene.this.peer, sourceTransferModes, new DragSourceListener(), dragboard); } else if (fullPDRSource != null) { /* start PDR */ Scene.this.mouseHandler.enterFullPDR(fullPDRSource); } fullPDRSource = null; } /** * Sets the default dragDetect value */ private void processDragDetection(MouseEvent mouseEvent) { if (dragDetected != DragDetectedState.NOT_YET) { mouseEvent.setDragDetect(false); return; } if (mouseEvent.getEventType() == MouseEvent.MOUSE_PRESSED) { pressedX = mouseEvent.getSceneX(); pressedY = mouseEvent.getSceneY(); mouseEvent.setDragDetect(false); } else if (mouseEvent.getEventType() == MouseEvent.MOUSE_DRAGGED) { double deltaX = Math.abs(mouseEvent.getSceneX() - pressedX); double deltaY = Math.abs(mouseEvent.getSceneY() - pressedY); mouseEvent.setDragDetect(deltaX > hysteresisSizeX || deltaY > hysteresisSizeY); } } /** * This function is useful for drag gesture recognition from * within this Scene (as opposed to in the TK implementation... by the platform) */ private boolean process(MouseEvent mouseEvent, EventTarget target) { boolean continueProcessing = true; if (!PLATFORM_DRAG_GESTURE_INITIATION) { if (dragDetected != DragDetectedState.DONE && (mouseEvent.getEventType() == MouseEvent.MOUSE_PRESSED || mouseEvent.getEventType() == MouseEvent.MOUSE_DRAGGED) && mouseEvent.isDragDetect()) { processingDragDetected(); if (target != null) { final MouseEvent detectedEvent = mouseEvent.copyFor( mouseEvent.getSource(), target, MouseEvent.DRAG_DETECTED); try { fireEvent(target, detectedEvent); } finally { // Putting data to dragboard finished, restrict access to them if (dragboard != null) { DragboardHelper.setDataAccessRestriction( dragboard, true); } } } dragDetectedProcessed(); } if (mouseEvent.getEventType() == MouseEvent.MOUSE_RELEASED) { continueProcessing = false; } } return continueProcessing; } /* * Called when a drag source is recognized. This occurs at the very start of * the publicly visible drag and drop API, as it is responsible for calling * the Node.onDragSourceRecognized function. */ private boolean processRecognized(DragEvent de) { MouseEvent me = new MouseEvent( MouseEvent.DRAG_DETECTED, de.getX(), de.getY(), de.getSceneX(), de.getScreenY(), MouseButton.PRIMARY, 1, false, false, false, false, false, true, false, false, false, false, de.getPickResult()); processingDragDetected(); final EventTarget target = de.getPickResult().getIntersectedNode(); try { fireEvent(target != null ? target : Scene.this, me); } finally { // Putting data to dragboard finished, restrict access to them if (dragboard != null) { DragboardHelper.setDataAccessRestriction( dragboard, true); } } dragDetectedProcessed(); final boolean hasContent = dragboard != null && !dragboard.getContentTypes().isEmpty(); return hasContent; } private void processDropEnd(DragEvent de) { if (source == null) { System.out.println("Scene.DnDGesture.processDropEnd() - UNEXPECTED - source is NULL"); return; } de = new DragEvent(de.getSource(), source, DragEvent.DRAG_DONE, de.getDragboard(), de.getSceneX(), de.getSceneY(), de.getScreenX(), de.getScreenY(), de.getTransferMode(), source, target, de.getPickResult()); Event.fireEvent(source, de); tmpTargetWrapper.clear(); handleExitEnter(de, tmpTargetWrapper); // at this point the drag and drop operation is completely over, so we // can tell the toolkit that it can clean up if needs be. Toolkit.getToolkit().stopDrag(dragboard); } private TransferMode processTargetEnterOver(DragEvent de) { pick(tmpTargetWrapper, de.getSceneX(), de.getSceneY()); final EventTarget pickedTarget = tmpTargetWrapper.getEventTarget(); if (dragboard == null) { dragboard = createDragboard(de, false); } de = new DragEvent(de.getSource(), pickedTarget, de.getEventType(), dragboard, de.getSceneX(), de.getSceneY(), de.getScreenX(), de.getScreenY(), de.getTransferMode(), source, potentialTarget, de.getPickResult()); handleExitEnter(de, tmpTargetWrapper); de = new DragEvent(de.getSource(), pickedTarget, DragEvent.DRAG_OVER, de.getDragboard(), de.getSceneX(), de.getSceneY(), de.getScreenX(), de.getScreenY(), de.getTransferMode(), source, potentialTarget, de.getPickResult()); fireEvent(pickedTarget, de); Object acceptingObject = de.getAcceptingObject(); potentialTarget = acceptingObject instanceof EventTarget ? (EventTarget) acceptingObject : null; acceptedTransferMode = de.getAcceptedTransferMode(); return acceptedTransferMode; } private void processTargetActionChanged(DragEvent de) { // Do we want DRAG_TRANSFER_MODE_CHANGED event? // final Node pickedNode = Scene.this.mouseHandler.pickNode(de.getX(), de.getY()); // if (pickedNode != null && pickedNode.isTreeVisible()) { // de = DragEvent.copy(de.getSource(), pickedNode, source, // pickedNode, de, DragEvent.DRAG_TRANSFER_MODE_CHANGED); // // if (dragboard == null) { // dragboard = createDragboard(de); // } // dragboard = de.getPlatformDragboard(); // // fireEvent(pickedNode, de); // } } private void processTargetExit(DragEvent de) { if (dragboard == null) { // dragboard should have been created in processTargetEnterOver() throw new NullPointerException("dragboard is null in processTargetExit()"); } if (currentTargets.size() > 0) { potentialTarget = null; tmpTargetWrapper.clear(); handleExitEnter(de, tmpTargetWrapper); } } private TransferMode processTargetDrop(DragEvent de) { pick(tmpTargetWrapper, de.getSceneX(), de.getSceneY()); final EventTarget pickedTarget = tmpTargetWrapper.getEventTarget(); de = new DragEvent(de.getSource(), pickedTarget, DragEvent.DRAG_DROPPED, de.getDragboard(), de.getSceneX(), de.getSceneY(), de.getScreenX(), de.getScreenY(), acceptedTransferMode, source, potentialTarget, de.getPickResult()); if (dragboard == null) { // dragboard should have been created in processTargetEnterOver() throw new NullPointerException("dragboard is null in processTargetDrop()"); } handleExitEnter(de, tmpTargetWrapper); fireEvent(pickedTarget, de); Object acceptingObject = de.getAcceptingObject(); potentialTarget = acceptingObject instanceof EventTarget ? (EventTarget) acceptingObject : null; target = potentialTarget; TransferMode result = de.isDropCompleted() ? de.getAcceptedTransferMode() : null; tmpTargetWrapper.clear(); handleExitEnter(de, tmpTargetWrapper); return result; } private void handleExitEnter(DragEvent e, TargetWrapper target) { EventTarget currentTarget = currentTargets.size() > 0 ? currentTargets.get(0) : null; if (target.getEventTarget() != currentTarget) { target.fillHierarchy(newTargets); int i = currentTargets.size() - 1; int j = newTargets.size() - 1; while (i >= 0 && j >= 0 && currentTargets.get(i) == newTargets.get(j)) { i--; j--; } for (; i >= 0; i--) { EventTarget t = currentTargets.get(i); if (potentialTarget == t) { potentialTarget = null; } e = e.copyFor(e.getSource(), t, source, potentialTarget, DragEvent.DRAG_EXITED_TARGET); Event.fireEvent(t, e); } potentialTarget = null; for (; j >= 0; j--) { EventTarget t = newTargets.get(j); e = e.copyFor(e.getSource(), t, source, potentialTarget, DragEvent.DRAG_ENTERED_TARGET); Object acceptingObject = e.getAcceptingObject(); if (acceptingObject instanceof EventTarget) { potentialTarget = (EventTarget) acceptingObject; } Event.fireEvent(t, e); } currentTargets.clear(); currentTargets.addAll(newTargets); newTargets.clear(); } } // function getIntendedTransferMode(e:MouseEvent):TransferMode { // return if (e.altDown) TransferMode.COPY else TransferMode.MOVE; // } /* * Function that hooks into the key processing code in Scene to handle the * situation where a drag and drop event is taking place and the user presses * the escape key to cancel the drag and drop operation. */ private boolean processKey(KeyEvent e) { //note: this seems not to be called, the DnD cancelation is provided by platform if ((e.getEventType() == KeyEvent.KEY_PRESSED) && (e.getCode() == KeyCode.ESCAPE)) { // cancel drag and drop DragEvent de = new DragEvent( source, source, DragEvent.DRAG_DONE, dragboard, 0, 0, 0, 0, null, source, null, null); if (source != null) { Event.fireEvent(source, de); } tmpTargetWrapper.clear(); handleExitEnter(de, tmpTargetWrapper); return false; } return true; } /* * This starts the drag gesture running, creating the dragboard used for * the remainder of this drag and drop operation. */ private Dragboard startDrag(EventTarget source, Set t) { if (dragDetected != DragDetectedState.PROCESSING) { throw new IllegalStateException("Cannot start drag and drop " + "outside of DRAG_DETECTED event handler"); } if (t.isEmpty()) { dragboard = null; } else if (dragboard == null) { dragboard = createDragboard(null, true); } // The app can see what it puts to dragboard without restriction DragboardHelper.setDataAccessRestriction(dragboard, false); this.source = source; potentialTarget = source; sourceTransferModes = t; return dragboard; } /* * This starts the full PDR gesture. */ private void startFullPDR(EventTarget source) { fullPDRSource = source; } private Dragboard createDragboard(final DragEvent de, boolean isDragSource) { Dragboard dragboard = null; if (de != null) { dragboard = de.getDragboard(); if (dragboard != null) { return dragboard; } } TKClipboard dragboardPeer = peer.createDragboard(isDragSource); return DragboardHelper.createDragboard(dragboardPeer); } } /** * State of a drag gesture with regards to DRAG_DETECTED event. */ private enum DragDetectedState { NOT_YET, PROCESSING, DONE } class DragSourceListener implements TKDragSourceListener { @Override public void dragDropEnd(double x, double y, double screenX, double screenY, TransferMode transferMode) { if (dndGesture != null) { if (dndGesture.dragboard == null) { throw new RuntimeException("dndGesture.dragboard is null in dragDropEnd"); } DragEvent dragEvent = new DragEvent(DragEvent.ANY, dndGesture.dragboard, x, y, screenX, screenY, transferMode, null, null, null); // DRAG_DONE event is delivered to gesture source, it can access // its own data without restriction DragboardHelper.setDataAccessRestriction(dndGesture.dragboard, false); try { dndGesture.processDropEnd(dragEvent); } finally { DragboardHelper.setDataAccessRestriction(dndGesture.dragboard, true); } dndGesture = null; } } } /* ***************************************************************************** * * * Mouse Event Handling * * * ******************************************************************************/ static class ClickCounter { Toolkit toolkit = Toolkit.getToolkit(); private int count; private boolean out; private boolean still; private Timeline timeout; private double pressedX, pressedY; private void inc() { count++; } private int get() { return count; } private boolean isStill() { return still; } private void clear() { count = 0; stopTimeout(); } private void out() { out = true; stopTimeout(); } private void applyOut() { if (out) clear(); out = false; } private void moved(double x, double y) { if (Math.abs(x - pressedX) > toolkit.getMultiClickMaxX() || Math.abs(y - pressedY) > toolkit.getMultiClickMaxY()) { out(); still = false; } } private void start(double x, double y) { pressedX = x; pressedY = y; out = false; if (timeout != null) { timeout.stop(); } timeout = new Timeline(); timeout.getKeyFrames().add( new KeyFrame(new Duration(toolkit.getMultiClickTime()), event -> { out = true; timeout = null; } )); timeout.play(); still = true; } private void stopTimeout() { if (timeout != null) { timeout.stop(); timeout = null; } } } static class ClickGenerator { private ClickCounter lastPress = null; private Map counters = new EnumMap<>(MouseButton.class); private List pressedTargets = new ArrayList<>(); private List releasedTargets = new ArrayList<>(); public ClickGenerator() { for (MouseButton mb : MouseButton.values()) { if (mb != MouseButton.NONE) { counters.put(mb, new ClickCounter()); } } } private MouseEvent preProcess(MouseEvent e) { for (ClickCounter cc : counters.values()) { cc.moved(e.getSceneX(), e.getSceneY()); } ClickCounter cc = counters.get(e.getButton()); boolean still = lastPress != null ? lastPress.isStill() : false; if (e.getEventType() == MouseEvent.MOUSE_PRESSED) { if (! e.isPrimaryButtonDown()) { counters.get(MouseButton.PRIMARY).clear(); } if (! e.isSecondaryButtonDown()) { counters.get(MouseButton.SECONDARY).clear(); } if (! e.isMiddleButtonDown()) { counters.get(MouseButton.MIDDLE).clear(); } if (! e.isBackButtonDown()) { counters.get(MouseButton.BACK).clear(); } if (! e.isForwardButtonDown()) { counters.get(MouseButton.FORWARD).clear(); } cc.applyOut(); cc.inc(); cc.start(e.getSceneX(), e.getSceneY()); lastPress = cc; } return new MouseEvent(e.getEventType(), e.getSceneX(), e.getSceneY(), e.getScreenX(), e.getScreenY(), e.getButton(), cc != null && e.getEventType() != MouseEvent.MOUSE_MOVED ? cc.get() : 0, e.isShiftDown(), e.isControlDown(), e.isAltDown(), e.isMetaDown(), e.isPrimaryButtonDown(), e.isMiddleButtonDown(), e.isSecondaryButtonDown(), e.isBackButtonDown(), e.isForwardButtonDown(), e.isSynthesized(), e.isPopupTrigger(), still, e.getPickResult()); } private void postProcess(MouseEvent e, TargetWrapper target, TargetWrapper pickedTarget) { if (e.getEventType() == MouseEvent.MOUSE_RELEASED) { ClickCounter cc = counters.get(e.getButton()); target.fillHierarchy(pressedTargets); pickedTarget.fillHierarchy(releasedTargets); int i = pressedTargets.size() - 1; int j = releasedTargets.size() - 1; EventTarget clickedTarget = null; while (i >= 0 && j >= 0 && pressedTargets.get(i) == releasedTargets.get(j)) { clickedTarget = pressedTargets.get(i); i--; j--; } pressedTargets.clear(); releasedTargets.clear(); if (clickedTarget != null && lastPress != null) { MouseEvent click = new MouseEvent(null, clickedTarget, MouseEvent.MOUSE_CLICKED, e.getSceneX(), e.getSceneY(), e.getScreenX(), e.getScreenY(), e.getButton(), cc.get(), e.isShiftDown(), e.isControlDown(), e.isAltDown(), e.isMetaDown(), e.isPrimaryButtonDown(), e.isMiddleButtonDown(), e.isSecondaryButtonDown(), e.isBackButtonDown(), e.isForwardButtonDown(), e.isSynthesized(), e.isPopupTrigger(), lastPress.isStill(), e.getPickResult()); Event.fireEvent(clickedTarget, click); } } } } /** * Generates mouse exited event for a node which is going to be removed * and its children, where appropriate. * @param removing Node which is going to be removed */ void generateMouseExited(Node removing) { mouseHandler.handleNodeRemoval(removing); } class MouseHandler { private TargetWrapper pdrEventTarget = new TargetWrapper(); // pdr - press-drag-release private boolean pdrInProgress = false; private boolean fullPDREntered = false; private EventTarget currentEventTarget = null; private MouseEvent lastEvent; private boolean hover = false; private boolean primaryButtonDown = false; private boolean secondaryButtonDown = false; private boolean middleButtonDown = false; private boolean backButtonDown = false; private boolean forwardButtonDown = false; private EventTarget fullPDRSource = null; private TargetWrapper fullPDRTmpTargetWrapper = new TargetWrapper(); /* lists needed for enter/exit events generation */ private final List pdrEventTargets = new ArrayList<>(); private final List currentEventTargets = new ArrayList<>(); private final List newEventTargets = new ArrayList<>(); private final List fullPDRCurrentEventTargets = new ArrayList<>(); private final List fullPDRNewEventTargets = new ArrayList<>(); private EventTarget fullPDRCurrentTarget = null; private Cursor currCursor; private CursorFrame currCursorFrame; private EventQueue queue = new EventQueue(); private Runnable pickProcess = new Runnable() { @Override public void run() { // Make sure this is run only if the peer is still alive // and there is an event to deliver if (Scene.this.peer != null && lastEvent != null) { process(lastEvent, true); } } }; private void pulse() { if (hover && lastEvent != null) { //Shouldn't run user code directly. User can call stage.showAndWait() and block the pulse. Platform.runLater(pickProcess); } } private void clearPDREventTargets() { pdrInProgress = false; currentEventTarget = currentEventTargets.size() > 0 ? currentEventTargets.get(0) : null; pdrEventTarget.clear(); pdrEventTargets.clear(); fullPDRTmpTargetWrapper.clear(); } public void enterFullPDR(EventTarget gestureSource) { fullPDREntered = true; fullPDRSource = gestureSource; fullPDRCurrentTarget = null; fullPDRCurrentEventTargets.clear(); } public void exitFullPDR(MouseEvent e) { if (!fullPDREntered) { return; } fullPDREntered = false; for (int i = fullPDRCurrentEventTargets.size() - 1; i >= 0; i--) { EventTarget entered = fullPDRCurrentEventTargets.get(i); Event.fireEvent(entered, MouseEvent.copyForMouseDragEvent(e, entered, entered, MouseDragEvent.MOUSE_DRAG_EXITED_TARGET, fullPDRSource, e.getPickResult())); } fullPDRSource = null; fullPDRCurrentEventTargets.clear(); fullPDRCurrentTarget = null; } private void handleNodeRemoval(Node removing) { if (lastEvent == null) { // this can happen only if everything has been exited anyway return; } if (currentEventTargets.contains(removing)) { int i = 0; EventTarget trg = null; while(trg != removing) { trg = currentEventTargets.get(i++); queue.postEvent(lastEvent.copyFor(trg, trg, MouseEvent.MOUSE_EXITED_TARGET)); } currentEventTargets.subList(0, i).clear(); } if (fullPDREntered && fullPDRCurrentEventTargets.contains(removing)) { int i = 0; EventTarget trg = null; while (trg != removing) { trg = fullPDRCurrentEventTargets.get(i++); queue.postEvent( MouseEvent.copyForMouseDragEvent(lastEvent, trg, trg, MouseDragEvent.MOUSE_DRAG_EXITED_TARGET, fullPDRSource, lastEvent.getPickResult())); } fullPDRCurrentEventTargets.subList(0, i).clear(); } queue.fire(); if (pdrInProgress && pdrEventTargets.contains(removing)) { int i = 0; EventTarget trg = null; while (trg != removing) { trg = pdrEventTargets.get(i++); // trg.setHover(false) - already taken care of // by the code above which sent a mouse exited event ((Node) trg).setPressed(false); } pdrEventTargets.subList(0, i).clear(); trg = pdrEventTargets.get(0); final PickResult res = pdrEventTarget.getResult(); if (trg instanceof Node) { pdrEventTarget.setNodeResult(new PickResult((Node) trg, res.getIntersectedPoint(), res.getIntersectedDistance())); } else { pdrEventTarget.setSceneResult(new PickResult(null, res.getIntersectedPoint(), res.getIntersectedDistance()), (Scene) trg); } } } private void handleEnterExit(MouseEvent e, TargetWrapper pickedTarget) { if (pickedTarget.getEventTarget() != currentEventTarget || e.getEventType() == MouseEvent.MOUSE_EXITED) { if (e.getEventType() == MouseEvent.MOUSE_EXITED) { newEventTargets.clear(); } else { pickedTarget.fillHierarchy(newEventTargets); } int newTargetsSize = newEventTargets.size(); int i = currentEventTargets.size() - 1; int j = newTargetsSize - 1; int k = pdrEventTargets.size() - 1; while (i >= 0 && j >= 0 && currentEventTargets.get(i) == newEventTargets.get(j)) { i--; j--; k--; } final int memk = k; for (; i >= 0; i--, k--) { final EventTarget exitedEventTarget = currentEventTargets.get(i); if (pdrInProgress && (k < 0 || exitedEventTarget != pdrEventTargets.get(k))) { break; } queue.postEvent(e.copyFor( exitedEventTarget, exitedEventTarget, MouseEvent.MOUSE_EXITED_TARGET)); } k = memk; for (; j >= 0; j--, k--) { final EventTarget enteredEventTarget = newEventTargets.get(j); if (pdrInProgress && (k < 0 || enteredEventTarget != pdrEventTargets.get(k))) { break; } queue.postEvent(e.copyFor( enteredEventTarget, enteredEventTarget, MouseEvent.MOUSE_ENTERED_TARGET)); } currentEventTarget = pickedTarget.getEventTarget(); currentEventTargets.clear(); for (j++; j < newTargetsSize; j++) { currentEventTargets.add(newEventTargets.get(j)); } } queue.fire(); } private void process(MouseEvent e, boolean onPulse) { Toolkit.getToolkit().checkFxUserThread(); Scene.inMousePick = true; cursorScreenPos = new Point2D(e.getScreenX(), e.getScreenY()); cursorScenePos = new Point2D(e.getSceneX(), e.getSceneY()); boolean gestureStarted = false; if (!onPulse) { if (e.getEventType() == MouseEvent.MOUSE_PRESSED) { if (!(primaryButtonDown || secondaryButtonDown || middleButtonDown || backButtonDown || forwardButtonDown) && Scene.this.dndGesture == null) { //old gesture ended and new one started gestureStarted = true; if (!PLATFORM_DRAG_GESTURE_INITIATION) { Scene.this.dndGesture = new DnDGesture(); } clearPDREventTargets(); } } else if (e.getEventType() == MouseEvent.MOUSE_MOVED) { // gesture ended clearPDREventTargets(); } else if (e.getEventType() == MouseEvent.MOUSE_ENTERED) { hover = true; } else if (e.getEventType() == MouseEvent.MOUSE_EXITED) { hover = false; } primaryButtonDown = e.isPrimaryButtonDown(); secondaryButtonDown = e.isSecondaryButtonDown(); middleButtonDown = e.isMiddleButtonDown(); backButtonDown = e.isBackButtonDown(); forwardButtonDown = e.isForwardButtonDown(); } pick(tmpTargetWrapper, e.getSceneX(), e.getSceneY()); PickResult res = tmpTargetWrapper.getResult(); if (res != null) { e = new MouseEvent(e.getEventType(), e.getSceneX(), e.getSceneY(), e.getScreenX(), e.getScreenY(), e.getButton(), e.getClickCount(), e.isShiftDown(), e.isControlDown(), e.isAltDown(), e.isMetaDown(), e.isPrimaryButtonDown(), e.isMiddleButtonDown(), e.isSecondaryButtonDown(), e.isBackButtonDown(), e.isForwardButtonDown(), e.isSynthesized(), e.isPopupTrigger(), e.isStillSincePress(), res); } if (e.getEventType() == MouseEvent.MOUSE_EXITED) { tmpTargetWrapper.clear(); } TargetWrapper target; if (pdrInProgress) { target = pdrEventTarget; } else { target = tmpTargetWrapper; } if (gestureStarted) { pdrEventTarget.copy(target); pdrEventTarget.fillHierarchy(pdrEventTargets); } if (!onPulse) { e = clickGenerator.preProcess(e); } // enter/exit handling handleEnterExit(e, tmpTargetWrapper); //deliver event to the target node if (Scene.this.dndGesture != null) { Scene.this.dndGesture.processDragDetection(e); } if (fullPDREntered && e.getEventType() == MouseEvent.MOUSE_RELEASED) { processFullPDR(e, onPulse); } if (target.getEventTarget() != null) { if (e.getEventType() != MouseEvent.MOUSE_ENTERED && e.getEventType() != MouseEvent.MOUSE_EXITED && !onPulse) { Event.fireEvent(target.getEventTarget(), e); } } if (fullPDREntered && e.getEventType() != MouseEvent.MOUSE_RELEASED) { processFullPDR(e, onPulse); } if (!onPulse) { clickGenerator.postProcess(e, target, tmpTargetWrapper); } // handle drag and drop if (!PLATFORM_DRAG_GESTURE_INITIATION && !onPulse) { if (Scene.this.dndGesture != null) { if (!Scene.this.dndGesture.process(e, target.getEventTarget())) { dndGesture = null; } } } Cursor cursor = target.getCursor(); if (e.getEventType() != MouseEvent.MOUSE_EXITED) { if (cursor == null && hover) { cursor = Scene.this.getCursor(); } updateCursor(cursor); updateCursorFrame(); } if (gestureStarted) { pdrInProgress = true; } if (pdrInProgress && !(primaryButtonDown || secondaryButtonDown || middleButtonDown || backButtonDown || forwardButtonDown)) { clearPDREventTargets(); exitFullPDR(e); // we need to do new picking in case the originally picked node // was moved or removed by the event handlers pick(tmpTargetWrapper, e.getSceneX(), e.getSceneY()); handleEnterExit(e, tmpTargetWrapper); } lastEvent = e.getEventType() == MouseEvent.MOUSE_EXITED ? null : e; Scene.inMousePick = false; } private void processFullPDR(MouseEvent e, boolean onPulse) { pick(fullPDRTmpTargetWrapper, e.getSceneX(), e.getSceneY()); final PickResult result = fullPDRTmpTargetWrapper.getResult(); final EventTarget eventTarget = fullPDRTmpTargetWrapper.getEventTarget(); // enter/exit handling if (eventTarget != fullPDRCurrentTarget) { fullPDRTmpTargetWrapper.fillHierarchy(fullPDRNewEventTargets); int newTargetsSize = fullPDRNewEventTargets.size(); int i = fullPDRCurrentEventTargets.size() - 1; int j = newTargetsSize - 1; while (i >= 0 && j >= 0 && fullPDRCurrentEventTargets.get(i) == fullPDRNewEventTargets.get(j)) { i--; j--; } for (; i >= 0; i--) { final EventTarget exitedEventTarget = fullPDRCurrentEventTargets.get(i); Event.fireEvent(exitedEventTarget, MouseEvent.copyForMouseDragEvent(e, exitedEventTarget, exitedEventTarget, MouseDragEvent.MOUSE_DRAG_EXITED_TARGET, fullPDRSource, result)); } for (; j >= 0; j--) { final EventTarget enteredEventTarget = fullPDRNewEventTargets.get(j); Event.fireEvent(enteredEventTarget, MouseEvent.copyForMouseDragEvent(e, enteredEventTarget, enteredEventTarget, MouseDragEvent.MOUSE_DRAG_ENTERED_TARGET, fullPDRSource, result)); } fullPDRCurrentTarget = eventTarget; fullPDRCurrentEventTargets.clear(); fullPDRCurrentEventTargets.addAll(fullPDRNewEventTargets); fullPDRNewEventTargets.clear(); } // done enter/exit handling // event delivery if (eventTarget != null && !onPulse) { if (e.getEventType() == MouseEvent.MOUSE_DRAGGED) { Event.fireEvent(eventTarget, MouseEvent.copyForMouseDragEvent(e, eventTarget, eventTarget, MouseDragEvent.MOUSE_DRAG_OVER, fullPDRSource, result)); } if (e.getEventType() == MouseEvent.MOUSE_RELEASED) { Event.fireEvent(eventTarget, MouseEvent.copyForMouseDragEvent(e, eventTarget, eventTarget, MouseDragEvent.MOUSE_DRAG_RELEASED, fullPDRSource, result)); } } } private void updateCursor(Cursor newCursor) { if (currCursor != newCursor) { if (currCursor != null) { currCursor.deactivate(); } if (newCursor != null) { newCursor.activate(); } currCursor = newCursor; } } public void updateCursorFrame() { final CursorFrame newCursorFrame = (currCursor != null) ? currCursor.getCurrentFrame() : Cursor.DEFAULT.getCurrentFrame(); if (currCursorFrame != newCursorFrame) { if (Scene.this.peer != null) { Scene.this.peer.setCursor(newCursorFrame); } currCursorFrame = newCursorFrame; } } private PickResult pickNode(PickRay pickRay) { PickResultChooser r = new PickResultChooser(); Scene.this.getRoot().pickNode(pickRay, r); return r.toPickResult(); } } /* ************************************************************************* * * * Event Dispatch * * * **************************************************************************/ // PENDING_DOC_REVIEW /** * Specifies the event dispatcher for this scene. When replacing the value * with a new {@code EventDispatcher}, the new dispatcher should forward * events to the replaced dispatcher to keep the scene's default event * handling behavior. */ private ObjectProperty eventDispatcher; public final void setEventDispatcher(EventDispatcher value) { eventDispatcherProperty().set(value); } public final EventDispatcher getEventDispatcher() { return eventDispatcherProperty().get(); } public final ObjectProperty eventDispatcherProperty() { initializeInternalEventDispatcher(); return eventDispatcher; } private SceneEventDispatcher internalEventDispatcher; // Delegates requests from platform input method to the focused // node's one, if any. class InputMethodRequestsDelegate implements ExtendedInputMethodRequests { @Override public Point2D getTextLocation(int offset) { InputMethodRequests requests = getClientRequests(); if (requests != null) { return requests.getTextLocation(offset); } else { return new Point2D(0, 0); } } @Override public int getLocationOffset(int x, int y) { InputMethodRequests requests = getClientRequests(); if (requests != null) { return requests.getLocationOffset(x, y); } else { return 0; } } @Override public void cancelLatestCommittedText() { InputMethodRequests requests = getClientRequests(); if (requests != null) { requests.cancelLatestCommittedText(); } } @Override public String getSelectedText() { InputMethodRequests requests = getClientRequests(); if (requests != null) { return requests.getSelectedText(); } return null; } @Override public int getInsertPositionOffset() { InputMethodRequests requests = getClientRequests(); if (requests != null && requests instanceof ExtendedInputMethodRequests) { return ((ExtendedInputMethodRequests)requests).getInsertPositionOffset(); } return 0; } @Override public String getCommittedText(int begin, int end) { InputMethodRequests requests = getClientRequests(); if (requests != null && requests instanceof ExtendedInputMethodRequests) { return ((ExtendedInputMethodRequests)requests).getCommittedText(begin, end); } return null; } @Override public int getCommittedTextLength() { InputMethodRequests requests = getClientRequests(); if (requests != null && requests instanceof ExtendedInputMethodRequests) { return ((ExtendedInputMethodRequests)requests).getCommittedTextLength(); } return 0; } private InputMethodRequests getClientRequests() { Node focusOwner = getFocusOwner(); if (focusOwner != null) { return focusOwner.getInputMethodRequests(); } return null; } } @Override public final void addEventHandler( final EventType eventType, final EventHandler eventHandler) { getInternalEventDispatcher().getEventHandlerManager() .addEventHandler(eventType, eventHandler); } @Override public final void removeEventHandler( final EventType eventType, final EventHandler eventHandler) { getInternalEventDispatcher().getEventHandlerManager() .removeEventHandler(eventType, eventHandler); } @Override public final void addEventFilter( final EventType eventType, final EventHandler eventFilter) { getInternalEventDispatcher().getEventHandlerManager() .addEventFilter(eventType, eventFilter); } @Override public final void removeEventFilter( final EventType eventType, final EventHandler eventFilter) { getInternalEventDispatcher().getEventHandlerManager() .removeEventFilter(eventType, eventFilter); } /** * Sets the handler to use for this event type. There can only be one such * handler specified at a time. This handler is guaranteed to be called * first. This is used for registering the user-defined onFoo event * handlers. * * @param the specific event class of the handler * @param eventType the event type to associate with the given eventHandler * @param eventHandler the handler to register, or null to unregister * @throws NullPointerException if the event type is null */ protected final void setEventHandler( final EventType eventType, final EventHandler eventHandler) { getInternalEventDispatcher().getEventHandlerManager() .setEventHandler(eventType, eventHandler); } private SceneEventDispatcher getInternalEventDispatcher() { initializeInternalEventDispatcher(); return internalEventDispatcher; } final void initializeInternalEventDispatcher() { if (internalEventDispatcher == null) { internalEventDispatcher = createInternalEventDispatcher(); eventDispatcher = new SimpleObjectProperty<>( this, "eventDispatcher", internalEventDispatcher); } } private SceneEventDispatcher createInternalEventDispatcher() { return new SceneEventDispatcher(this); } /** * Registers the specified mnemonic. * * @param m The mnemonic */ public void addMnemonic(Mnemonic m) { getInternalEventDispatcher().getKeyboardShortcutsHandler() .addMnemonic(m); } /** * Unregisters the specified mnemonic. * * @param m The mnemonic */ public void removeMnemonic(Mnemonic m) { getInternalEventDispatcher().getKeyboardShortcutsHandler() .removeMnemonic(m); } final void clearNodeMnemonics(Node node) { getInternalEventDispatcher().getKeyboardShortcutsHandler() .clearNodeMnemonics(node); } /** * Gets the list of mnemonics for this {@code Scene}. * * @return the list of mnemonics */ public ObservableMap> getMnemonics() { return getInternalEventDispatcher().getKeyboardShortcutsHandler() .getMnemonics(); } /** * Gets the list of accelerators for this {@code Scene}. * * @return the list of accelerators */ public ObservableMap getAccelerators() { return getInternalEventDispatcher().getKeyboardShortcutsHandler() .getAccelerators(); } @Override public EventDispatchChain buildEventDispatchChain( EventDispatchChain tail) { if (eventDispatcher != null) { final EventDispatcher eventDispatcherValue = eventDispatcher.get(); if (eventDispatcherValue != null) { tail = tail.prepend(eventDispatcherValue); } } if (getWindow() != null) { tail = getWindow().buildEventDispatchChain(tail); } return tail; } /* ************************************************************************* * * * Context Menus * * * **************************************************************************/ /** * Defines a function to be called when a mouse button has been clicked * (pressed and released) on this {@code Scene}. * @since JavaFX 2.1 */ private ObjectProperty> onContextMenuRequested; public final void setOnContextMenuRequested(EventHandler value) { onContextMenuRequestedProperty().set(value); } public final EventHandler getOnContextMenuRequested() { return onContextMenuRequested == null ? null : onContextMenuRequested.get(); } public final ObjectProperty> onContextMenuRequestedProperty() { if (onContextMenuRequested == null) { onContextMenuRequested = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onContextMenuRequested"; } }; } return onContextMenuRequested; } /* ************************************************************************* * * * Mouse Handling * * * **************************************************************************/ /** * Defines a function to be called when a mouse button has been clicked * (pressed and released) on this {@code Scene}. */ private ObjectProperty> onMouseClicked; public final void setOnMouseClicked(EventHandler value) { onMouseClickedProperty().set(value); } public final EventHandler getOnMouseClicked() { return onMouseClicked == null ? null : onMouseClicked.get(); } public final ObjectProperty> onMouseClickedProperty() { if (onMouseClicked == null) { onMouseClicked = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(MouseEvent.MOUSE_CLICKED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onMouseClicked"; } }; } return onMouseClicked; } /** * Defines a function to be called when a mouse button is pressed * on this {@code Scene} and then dragged. */ private ObjectProperty> onMouseDragged; public final void setOnMouseDragged(EventHandler value) { onMouseDraggedProperty().set(value); } public final EventHandler getOnMouseDragged() { return onMouseDragged == null ? null : onMouseDragged.get(); } public final ObjectProperty> onMouseDraggedProperty() { if (onMouseDragged == null) { onMouseDragged = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(MouseEvent.MOUSE_DRAGGED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onMouseDragged"; } }; } return onMouseDragged; } /** * Defines a function to be called when the mouse enters this {@code Scene}. */ private ObjectProperty> onMouseEntered; public final void setOnMouseEntered(EventHandler value) { onMouseEnteredProperty().set(value); } public final EventHandler getOnMouseEntered() { return onMouseEntered == null ? null : onMouseEntered.get(); } public final ObjectProperty> onMouseEnteredProperty() { if (onMouseEntered == null) { onMouseEntered = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(MouseEvent.MOUSE_ENTERED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onMouseEntered"; } }; } return onMouseEntered; } /** * Defines a function to be called when the mouse exits this {@code Scene}. */ private ObjectProperty> onMouseExited; public final void setOnMouseExited(EventHandler value) { onMouseExitedProperty().set(value); } public final EventHandler getOnMouseExited() { return onMouseExited == null ? null : onMouseExited.get(); } public final ObjectProperty> onMouseExitedProperty() { if (onMouseExited == null) { onMouseExited = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(MouseEvent.MOUSE_EXITED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onMouseExited"; } }; } return onMouseExited; } /** * Defines a function to be called when mouse cursor moves within * this {@code Scene} but no buttons have been pushed. */ private ObjectProperty> onMouseMoved; public final void setOnMouseMoved(EventHandler value) { onMouseMovedProperty().set(value); } public final EventHandler getOnMouseMoved() { return onMouseMoved == null ? null : onMouseMoved.get(); } public final ObjectProperty> onMouseMovedProperty() { if (onMouseMoved == null) { onMouseMoved = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(MouseEvent.MOUSE_MOVED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onMouseMoved"; } }; } return onMouseMoved; } /** * Defines a function to be called when a mouse button * has been pressed on this {@code Scene}. */ private ObjectProperty> onMousePressed; public final void setOnMousePressed(EventHandler value) { onMousePressedProperty().set(value); } public final EventHandler getOnMousePressed() { return onMousePressed == null ? null : onMousePressed.get(); } public final ObjectProperty> onMousePressedProperty() { if (onMousePressed == null) { onMousePressed = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(MouseEvent.MOUSE_PRESSED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onMousePressed"; } }; } return onMousePressed; } /** * Defines a function to be called when a mouse button * has been released on this {@code Scene}. */ private ObjectProperty> onMouseReleased; public final void setOnMouseReleased(EventHandler value) { onMouseReleasedProperty().set(value); } public final EventHandler getOnMouseReleased() { return onMouseReleased == null ? null : onMouseReleased.get(); } public final ObjectProperty> onMouseReleasedProperty() { if (onMouseReleased == null) { onMouseReleased = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(MouseEvent.MOUSE_RELEASED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onMouseReleased"; } }; } return onMouseReleased; } /** * Defines a function to be called when drag gesture has been * detected. This is the right place to start drag and drop operation. */ private ObjectProperty> onDragDetected; public final void setOnDragDetected(EventHandler value) { onDragDetectedProperty().set(value); } public final EventHandler getOnDragDetected() { return onDragDetected == null ? null : onDragDetected.get(); } public final ObjectProperty> onDragDetectedProperty() { if (onDragDetected == null) { onDragDetected = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(MouseEvent.DRAG_DETECTED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onDragDetected"; } }; } return onDragDetected; } /** * Defines a function to be called when a full press-drag-release gesture * progresses within this {@code Scene}. * @since JavaFX 2.1 */ private ObjectProperty> onMouseDragOver; public final void setOnMouseDragOver(EventHandler value) { onMouseDragOverProperty().set(value); } public final EventHandler getOnMouseDragOver() { return onMouseDragOver == null ? null : onMouseDragOver.get(); } public final ObjectProperty> onMouseDragOverProperty() { if (onMouseDragOver == null) { onMouseDragOver = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(MouseDragEvent.MOUSE_DRAG_OVER, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onMouseDragOver"; } }; } return onMouseDragOver; } /** * Defines a function to be called when a full press-drag-release gesture * ends within this {@code Scene}. * @since JavaFX 2.1 */ private ObjectProperty> onMouseDragReleased; public final void setOnMouseDragReleased(EventHandler value) { onMouseDragReleasedProperty().set(value); } public final EventHandler getOnMouseDragReleased() { return onMouseDragReleased == null ? null : onMouseDragReleased.get(); } public final ObjectProperty> onMouseDragReleasedProperty() { if (onMouseDragReleased == null) { onMouseDragReleased = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(MouseDragEvent.MOUSE_DRAG_RELEASED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onMouseDragReleased"; } }; } return onMouseDragReleased; } /** * Defines a function to be called when a full press-drag-release gesture * enters this {@code Scene}. * @since JavaFX 2.1 */ private ObjectProperty> onMouseDragEntered; public final void setOnMouseDragEntered(EventHandler value) { onMouseDragEnteredProperty().set(value); } public final EventHandler getOnMouseDragEntered() { return onMouseDragEntered == null ? null : onMouseDragEntered.get(); } public final ObjectProperty> onMouseDragEnteredProperty() { if (onMouseDragEntered == null) { onMouseDragEntered = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(MouseDragEvent.MOUSE_DRAG_ENTERED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onMouseDragEntered"; } }; } return onMouseDragEntered; } /** * Defines a function to be called when a full press-drag-release gesture * exits this {@code Scene}. * @since JavaFX 2.1 */ private ObjectProperty> onMouseDragExited; public final void setOnMouseDragExited(EventHandler value) { onMouseDragExitedProperty().set(value); } public final EventHandler getOnMouseDragExited() { return onMouseDragExited == null ? null : onMouseDragExited.get(); } public final ObjectProperty> onMouseDragExitedProperty() { if (onMouseDragExited == null) { onMouseDragExited = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(MouseDragEvent.MOUSE_DRAG_EXITED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onMouseDragExited"; } }; } return onMouseDragExited; } /* ************************************************************************* * * * Gestures Handling * * * **************************************************************************/ /** * Defines a function to be called when a scrolling gesture is detected. * @since JavaFX 2.2 */ private ObjectProperty> onScrollStarted; public final void setOnScrollStarted(EventHandler value) { onScrollStartedProperty().set(value); } public final EventHandler getOnScrollStarted() { return onScrollStarted == null ? null : onScrollStarted.get(); } public final ObjectProperty> onScrollStartedProperty() { if (onScrollStarted == null) { onScrollStarted = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(ScrollEvent.SCROLL_STARTED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onScrollStarted"; } }; } return onScrollStarted; } /** * Defines a function to be called when user performs a scrolling action. */ private ObjectProperty> onScroll; public final void setOnScroll(EventHandler value) { onScrollProperty().set(value); } public final EventHandler getOnScroll() { return onScroll == null ? null : onScroll.get(); } public final ObjectProperty> onScrollProperty() { if (onScroll == null) { onScroll = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(ScrollEvent.SCROLL, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onScroll"; } }; } return onScroll; } /** * Defines a function to be called when a scrolling gesture ends. * @since JavaFX 2.2 */ private ObjectProperty> onScrollFinished; public final void setOnScrollFinished(EventHandler value) { onScrollFinishedProperty().set(value); } public final EventHandler getOnScrollFinished() { return onScrollFinished == null ? null : onScrollFinished.get(); } public final ObjectProperty> onScrollFinishedProperty() { if (onScrollFinished == null) { onScrollFinished = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(ScrollEvent.SCROLL_FINISHED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onScrollFinished"; } }; } return onScrollFinished; } /** * Defines a function to be called when a rotating gesture is detected. * @since JavaFX 2.2 */ private ObjectProperty> onRotationStarted; public final void setOnRotationStarted(EventHandler value) { onRotationStartedProperty().set(value); } public final EventHandler getOnRotationStarted() { return onRotationStarted == null ? null : onRotationStarted.get(); } public final ObjectProperty> onRotationStartedProperty() { if (onRotationStarted == null) { onRotationStarted = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(RotateEvent.ROTATION_STARTED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onRotationStarted"; } }; } return onRotationStarted; } /** * Defines a function to be called when user performs a rotating action. * @since JavaFX 2.2 */ private ObjectProperty> onRotate; public final void setOnRotate(EventHandler value) { onRotateProperty().set(value); } public final EventHandler getOnRotate() { return onRotate == null ? null : onRotate.get(); } public final ObjectProperty> onRotateProperty() { if (onRotate == null) { onRotate = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(RotateEvent.ROTATE, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onRotate"; } }; } return onRotate; } /** * Defines a function to be called when a rotating gesture ends. * @since JavaFX 2.2 */ private ObjectProperty> onRotationFinished; public final void setOnRotationFinished(EventHandler value) { onRotationFinishedProperty().set(value); } public final EventHandler getOnRotationFinished() { return onRotationFinished == null ? null : onRotationFinished.get(); } public final ObjectProperty> onRotationFinishedProperty() { if (onRotationFinished == null) { onRotationFinished = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(RotateEvent.ROTATION_FINISHED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onRotationFinished"; } }; } return onRotationFinished; } /** * Defines a function to be called when a zooming gesture is detected. * @since JavaFX 2.2 */ private ObjectProperty> onZoomStarted; public final void setOnZoomStarted(EventHandler value) { onZoomStartedProperty().set(value); } public final EventHandler getOnZoomStarted() { return onZoomStarted == null ? null : onZoomStarted.get(); } public final ObjectProperty> onZoomStartedProperty() { if (onZoomStarted == null) { onZoomStarted = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(ZoomEvent.ZOOM_STARTED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onZoomStarted"; } }; } return onZoomStarted; } /** * Defines a function to be called when user performs a zooming action. * @since JavaFX 2.2 */ private ObjectProperty> onZoom; public final void setOnZoom(EventHandler value) { onZoomProperty().set(value); } public final EventHandler getOnZoom() { return onZoom == null ? null : onZoom.get(); } public final ObjectProperty> onZoomProperty() { if (onZoom == null) { onZoom = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(ZoomEvent.ZOOM, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onZoom"; } }; } return onZoom; } /** * Defines a function to be called when a zooming gesture ends. * @since JavaFX 2.2 */ private ObjectProperty> onZoomFinished; public final void setOnZoomFinished(EventHandler value) { onZoomFinishedProperty().set(value); } public final EventHandler getOnZoomFinished() { return onZoomFinished == null ? null : onZoomFinished.get(); } public final ObjectProperty> onZoomFinishedProperty() { if (onZoomFinished == null) { onZoomFinished = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(ZoomEvent.ZOOM_FINISHED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onZoomFinished"; } }; } return onZoomFinished; } /** * Defines a function to be called when an upward swipe gesture * happens in this scene. * @since JavaFX 2.2 */ private ObjectProperty> onSwipeUp; public final void setOnSwipeUp(EventHandler value) { onSwipeUpProperty().set(value); } public final EventHandler getOnSwipeUp() { return onSwipeUp == null ? null : onSwipeUp.get(); } public final ObjectProperty> onSwipeUpProperty() { if (onSwipeUp == null) { onSwipeUp = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(SwipeEvent.SWIPE_UP, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onSwipeUp"; } }; } return onSwipeUp; } /** * Defines a function to be called when an downward swipe gesture * happens in this scene. * @since JavaFX 2.2 */ private ObjectProperty> onSwipeDown; public final void setOnSwipeDown(EventHandler value) { onSwipeDownProperty().set(value); } public final EventHandler getOnSwipeDown() { return onSwipeDown == null ? null : onSwipeDown.get(); } public final ObjectProperty> onSwipeDownProperty() { if (onSwipeDown == null) { onSwipeDown = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(SwipeEvent.SWIPE_DOWN, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onSwipeDown"; } }; } return onSwipeDown; } /** * Defines a function to be called when an leftward swipe gesture * happens in this scene. * @since JavaFX 2.2 */ private ObjectProperty> onSwipeLeft; public final void setOnSwipeLeft(EventHandler value) { onSwipeLeftProperty().set(value); } public final EventHandler getOnSwipeLeft() { return onSwipeLeft == null ? null : onSwipeLeft.get(); } public final ObjectProperty> onSwipeLeftProperty() { if (onSwipeLeft == null) { onSwipeLeft = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(SwipeEvent.SWIPE_LEFT, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onSwipeLeft"; } }; } return onSwipeLeft; } /** * Defines a function to be called when an rightward swipe gesture * happens in this scene. * @since JavaFX 2.2 */ private ObjectProperty> onSwipeRight; public final void setOnSwipeRight(EventHandler value) { onSwipeRightProperty().set(value); } public final EventHandler getOnSwipeRight() { return onSwipeRight == null ? null : onSwipeRight.get(); } public final ObjectProperty> onSwipeRightProperty() { if (onSwipeRight == null) { onSwipeRight = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(SwipeEvent.SWIPE_RIGHT, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onSwipeRight"; } }; } return onSwipeRight; } /* ************************************************************************* * * * Touch Handling * * * **************************************************************************/ /** * Defines a function to be called when a new touch point is pressed. * @since JavaFX 2.2 */ private ObjectProperty> onTouchPressed; public final void setOnTouchPressed(EventHandler value) { onTouchPressedProperty().set(value); } public final EventHandler getOnTouchPressed() { return onTouchPressed == null ? null : onTouchPressed.get(); } public final ObjectProperty> onTouchPressedProperty() { if (onTouchPressed == null) { onTouchPressed = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(TouchEvent.TOUCH_PRESSED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onTouchPressed"; } }; } return onTouchPressed; } /** * Defines a function to be called when a touch point is moved. * @since JavaFX 2.2 */ private ObjectProperty> onTouchMoved; public final void setOnTouchMoved(EventHandler value) { onTouchMovedProperty().set(value); } public final EventHandler getOnTouchMoved() { return onTouchMoved == null ? null : onTouchMoved.get(); } public final ObjectProperty> onTouchMovedProperty() { if (onTouchMoved == null) { onTouchMoved = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(TouchEvent.TOUCH_MOVED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onTouchMoved"; } }; } return onTouchMoved; } /** * Defines a function to be called when a new touch point is pressed. * @since JavaFX 2.2 */ private ObjectProperty> onTouchReleased; public final void setOnTouchReleased(EventHandler value) { onTouchReleasedProperty().set(value); } public final EventHandler getOnTouchReleased() { return onTouchReleased == null ? null : onTouchReleased.get(); } public final ObjectProperty> onTouchReleasedProperty() { if (onTouchReleased == null) { onTouchReleased = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(TouchEvent.TOUCH_RELEASED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onTouchReleased"; } }; } return onTouchReleased; } /** * Defines a function to be called when a touch point stays pressed and * still. * @since JavaFX 2.2 */ private ObjectProperty> onTouchStationary; public final void setOnTouchStationary(EventHandler value) { onTouchStationaryProperty().set(value); } public final EventHandler getOnTouchStationary() { return onTouchStationary == null ? null : onTouchStationary.get(); } public final ObjectProperty> onTouchStationaryProperty() { if (onTouchStationary == null) { onTouchStationary = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(TouchEvent.TOUCH_STATIONARY, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onTouchStationary"; } }; } return onTouchStationary; } /* * This class provides reordering and ID mapping of particular touch points. * Platform may report arbitrary touch point IDs and they may be reused * during one gesture. This class keeps track of it and provides * sequentially sorted IDs, unique in scope of a gesture. * * Some platforms report always small numbers, these take fast paths through * the algorithm, directly indexing an array. Bigger numbers take a slow * path using a hash map. * * The algorithm performance was measured and it doesn't impose * any significant slowdown on the event delivery. */ private static class TouchMap { private static final int FAST_THRESHOLD = 10; int[] fastMap = new int[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; Map slowMap = new HashMap<>(); List order = new LinkedList<>(); List removed = new ArrayList<>(10); int counter = 0; int active = 0; public int add(long id) { counter++; active++; if (id < FAST_THRESHOLD) { fastMap[(int) id] = counter; } else { slowMap.put(id, counter); } order.add(counter); return counter; } public void remove(long id) { // book the removal - it needs to be done after all touch points // of an event are processed - see cleanup() removed.add(id); } public int get(long id) { if (id < FAST_THRESHOLD) { int result = fastMap[(int) id]; if (result == 0) { throw new RuntimeException("Platform reported wrong " + "touch point ID"); } return result; } else { try { return slowMap.get(id); } catch (NullPointerException e) { throw new RuntimeException("Platform reported wrong " + "touch point ID"); } } } public int getOrder(int id) { return order.indexOf(id); } // returns true if gesture finished (no finger is touched) public boolean cleanup() { for (long id : removed) { active--; order.remove(Integer.valueOf(get(id))); if (id < FAST_THRESHOLD) { fastMap[(int) id] = 0; } else { slowMap.remove(id); } if (active == 0) { // gesture finished counter = 0; } } removed.clear(); return active == 0; } } /* ************************************************************************* * * * Drag and Drop Handling * * * **************************************************************************/ private ObjectProperty> onDragEntered; public final void setOnDragEntered(EventHandler value) { onDragEnteredProperty().set(value); } public final EventHandler getOnDragEntered() { return onDragEntered == null ? null : onDragEntered.get(); } /** * Defines a function to be called when drag gesture * enters this {@code Scene}. * @return function to be called when drag gesture enters this scene */ public final ObjectProperty> onDragEnteredProperty() { if (onDragEntered == null) { onDragEntered = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(DragEvent.DRAG_ENTERED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onDragEntered"; } }; } return onDragEntered; } private ObjectProperty> onDragExited; public final void setOnDragExited(EventHandler value) { onDragExitedProperty().set(value); } public final EventHandler getOnDragExited() { return onDragExited == null ? null : onDragExited.get(); } /** * Defines a function to be called when drag gesture * exits this {@code Scene}. * @return the function to be called when drag gesture exits this scene */ public final ObjectProperty> onDragExitedProperty() { if (onDragExited == null) { onDragExited = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(DragEvent.DRAG_EXITED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onDragExited"; } }; } return onDragExited; } private ObjectProperty> onDragOver; public final void setOnDragOver(EventHandler value) { onDragOverProperty().set(value); } public final EventHandler getOnDragOver() { return onDragOver == null ? null : onDragOver.get(); } /** * Defines a function to be called when drag gesture progresses * within this {@code Scene}. * @return the function to be called when drag gesture progresses within * this scene */ public final ObjectProperty> onDragOverProperty() { if (onDragOver == null) { onDragOver = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(DragEvent.DRAG_OVER, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onDragOver"; } }; } return onDragOver; } // Do we want DRAG_TRANSFER_MODE_CHANGED event? // private ObjectProperty> onDragTransferModeChanged; // // public final void setOnDragTransferModeChanged(EventHandler value) { // onDragTransferModeChangedProperty().set(value); // } // // public final EventHandler getOnDragTransferModeChanged() { // return onDragTransferModeChanged == null ? null : onDragTransferModeChanged.get(); // } // // /** // * Defines a function to be called this {@code Scene} if it is a potential // * drag-and-drop target when the user takes action to change the intended // * {@code TransferMode}. // * The user can change the intended {@link TransferMode} by holding down // * or releasing key modifiers. // */ // public ObjectProperty> onDragTransferModeChangedProperty() { // if (onDragTransferModeChanged == null) { // onDragTransferModeChanged = new SimpleObjectProperty>() { // // @Override // protected void invalidated() { // setEventHandler(DragEvent.DRAG_TRANSFER_MODE_CHANGED, get()); // } // }; // } // return onDragTransferModeChanged; // } private ObjectProperty> onDragDropped; public final void setOnDragDropped(EventHandler value) { onDragDroppedProperty().set(value); } public final EventHandler getOnDragDropped() { return onDragDropped == null ? null : onDragDropped.get(); } /** * Defines a function to be called when the mouse button is released * on this {@code Scene} during drag and drop gesture. Transfer of data from * the {@link DragEvent}'s {@link DragEvent#getDragboard() dragboard} should * happen in this function. * @return the function to be called when the mouse button is released on * this scene during drag and drop gesture */ public final ObjectProperty> onDragDroppedProperty() { if (onDragDropped == null) { onDragDropped = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(DragEvent.DRAG_DROPPED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onDragDropped"; } }; } return onDragDropped; } private ObjectProperty> onDragDone; public final void setOnDragDone(EventHandler value) { onDragDoneProperty().set(value); } public final EventHandler getOnDragDone() { return onDragDone == null ? null : onDragDone.get(); } /** * Defines a function to be called when this {@code Scene} is a * drag and drop gesture source after its data has * been dropped on a drop target. The {@code transferMode} of the * event shows what just happened at the drop target. * If {@code transferMode} has the value {@code MOVE}, then the source can * clear out its data. Clearing the source's data gives the appropriate * appearance to a user that the data has been moved by the drag and drop * gesture. A {@code transferMode} that has the value {@code NONE} * indicates that no data was transferred during the drag and drop gesture. * Positional data for the {@code DragEvent} is invalid. Valid positional * data for the {@code DragEvent} is presented in the * {@link #onDragDroppedProperty() onDragDropped} event handler. * @return the function to be called when this scene is a drag and drop * gesture source after its data has been dropped on a drop target */ public final ObjectProperty> onDragDoneProperty() { if (onDragDone == null) { onDragDone = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(DragEvent.DRAG_DONE, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onDragDone"; } }; } return onDragDone; } /** * Confirms a potential drag and drop gesture that is recognized over this * {@code Scene}. * Can be called only from a DRAG_DETECTED event handler. The returned * {@link Dragboard} is used to transfer data during * the drag and drop gesture. Placing this {@code Scene}'s data on the * {@link Dragboard} also identifies this {@code Scene} as the source of * the drag and drop gesture. * More detail about drag and drop gestures is described in the overivew * of {@link DragEvent}. * * @see DragEvent * @param transferModes The supported {@code TransferMode}(s) of this {@code Node} * @return A {@code Dragboard} to place this {@code Scene}'s data on * @throws IllegalStateException if drag and drop cannot be started at this * moment (it's called outside of {@code DRAG_DETECTED} event handling). */ public Dragboard startDragAndDrop(TransferMode... transferModes) { return startDragAndDrop(this, transferModes); } /** * Starts a full press-drag-release gesture with this scene as gesture * source. This method can be called only from a {@code DRAG_DETECTED} mouse * event handler. More detail about dragging gestures can be found * in the overview of {@link MouseEvent} and {@link MouseDragEvent}. * * @see MouseEvent * @see MouseDragEvent * @throws IllegalStateException if the full press-drag-release gesture * cannot be started at this moment (it's called outside of * {@code DRAG_DETECTED} event handling). * @since JavaFX 2.1 */ public void startFullDrag() { startFullDrag(this); } Dragboard startDragAndDrop(EventTarget source, TransferMode... transferModes) { Toolkit.getToolkit().checkFxUserThread(); if (dndGesture == null || (dndGesture.dragDetected != DragDetectedState.PROCESSING)) { throw new IllegalStateException("Cannot start drag and drop " + "outside of DRAG_DETECTED event handler"); } Set set = EnumSet.noneOf(TransferMode.class); for (TransferMode tm : InputEventUtils.safeTransferModes(transferModes)) { set.add(tm); } return dndGesture.startDrag(source, set); } void startFullDrag(EventTarget source) { Toolkit.getToolkit().checkFxUserThread(); if (dndGesture.dragDetected != DragDetectedState.PROCESSING) { throw new IllegalStateException("Cannot start full drag " + "outside of DRAG_DETECTED event handler"); } if (dndGesture != null) { dndGesture.startFullPDR(source); return; } throw new IllegalStateException("Cannot start full drag when " + "mouse button is not pressed"); } /* ************************************************************************* * * * Keyboard Handling * * * **************************************************************************/ /** * Defines a function to be called when some {@code Node} of this * {@code Scene} has input focus and a key has been pressed. The function * is called only if the event hasn't been already consumed during its * capturing or bubbling phase. */ private ObjectProperty> onKeyPressed; public final void setOnKeyPressed(EventHandler value) { onKeyPressedProperty().set(value); } public final EventHandler getOnKeyPressed() { return onKeyPressed == null ? null : onKeyPressed.get(); } public final ObjectProperty> onKeyPressedProperty() { if (onKeyPressed == null) { onKeyPressed = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(KeyEvent.KEY_PRESSED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onKeyPressed"; } }; } return onKeyPressed; } /** * Defines a function to be called when some {@code Node} of this * {@code Scene} has input focus and a key has been released. The function * is called only if the event hasn't been already consumed during its * capturing or bubbling phase. */ private ObjectProperty> onKeyReleased; public final void setOnKeyReleased(EventHandler value) { onKeyReleasedProperty().set(value); } public final EventHandler getOnKeyReleased() { return onKeyReleased == null ? null : onKeyReleased.get(); } public final ObjectProperty> onKeyReleasedProperty() { if (onKeyReleased == null) { onKeyReleased = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(KeyEvent.KEY_RELEASED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onKeyReleased"; } }; } return onKeyReleased; } /** * Defines a function to be called when some {@code Node} of this * {@code Scene} has input focus and a key has been typed. The function * is called only if the event hasn't been already consumed during its * capturing or bubbling phase. */ private ObjectProperty> onKeyTyped; public final void setOnKeyTyped( EventHandler value) { onKeyTypedProperty().set( value); } public final EventHandler getOnKeyTyped( ) { return onKeyTyped == null ? null : onKeyTyped.get(); } public final ObjectProperty> onKeyTypedProperty( ) { if (onKeyTyped == null) { onKeyTyped = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(KeyEvent.KEY_TYPED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onKeyTyped"; } }; } return onKeyTyped; } /* ************************************************************************* * * * Input Method Handling * * * **************************************************************************/ /** * Defines a function to be called when this {@code Node} * has input focus and the input method text has changed. If this * function is not defined in this {@code Node}, then it * receives the result string of the input method composition as a * series of {@code onKeyTyped} function calls. *

* When the {@code Node} loses the input focus, the JavaFX runtime * automatically commits the existing composed text if any. */ private ObjectProperty> onInputMethodTextChanged; public final void setOnInputMethodTextChanged( EventHandler value) { onInputMethodTextChangedProperty().set( value); } public final EventHandler getOnInputMethodTextChanged() { return onInputMethodTextChanged == null ? null : onInputMethodTextChanged.get(); } public final ObjectProperty> onInputMethodTextChangedProperty() { if (onInputMethodTextChanged == null) { onInputMethodTextChanged = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(InputMethodEvent.INPUT_METHOD_TEXT_CHANGED, get()); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "onInputMethodTextChanged"; } }; } return onInputMethodTextChanged; } /* * This class represents a picked target - either node, or scne, or null. * It provides functionality needed for the targets and covers the fact * that they are different kinds of animals. */ private static class TargetWrapper { private Scene scene; private Node node; private PickResult result; /** * Fills the list with the target and all its parents (including scene) */ public void fillHierarchy(final List list) { list.clear(); Node n = node; while(n != null) { list.add(n); final Parent p = n.getParent(); n = p != null ? p : n.getSubScene(); } if (scene != null) { list.add(scene); } } public EventTarget getEventTarget() { return node != null ? node : scene; } public Cursor getCursor() { Cursor cursor = null; if (node != null) { cursor = node.getCursor(); Node n = node.getParent(); while (cursor == null && n != null) { cursor = n.getCursor(); final Parent p = n.getParent(); n = p != null ? p : n.getSubScene(); } } return cursor; } public void clear() { set(null, null); result = null; } public void setNodeResult(PickResult result) { if (result != null) { this.result = result; final Node n = result.getIntersectedNode(); set(n, n.getScene()); } } // Pass null scene if the mouse is outside of the window content public void setSceneResult(PickResult result, Scene scene) { if (result != null) { this.result = result; set(null, scene); } } public PickResult getResult() { return result; } public void copy(TargetWrapper tw) { node = tw.node; scene = tw.scene; result = tw.result; } private void set(Node n, Scene s) { node = n; scene = s; } } /* *********************************************************************** * * * * * * *************************************************************************/ private static final Object USER_DATA_KEY = new Object(); // A map containing a set of properties for this scene private ObservableMap properties; /** * Returns an observable map of properties on this node for use primarily * by application developers. * * @return an observable map of properties on this node for use primarily * by application developers * * @since JavaFX 8u40 */ public final ObservableMap getProperties() { if (properties == null) { properties = FXCollections.observableMap(new HashMap<>()); } return properties; } /** * Tests if Scene has properties. * @return true if node has properties. * * @since JavaFX 8u40 */ public boolean hasProperties() { return properties != null && !properties.isEmpty(); } /** * Convenience method for setting a single Object property that can be * retrieved at a later date. This is functionally equivalent to calling * the getProperties().put(Object key, Object value) method. This can later * be retrieved by calling {@link Scene#getUserData()}. * * @param value The value to be stored - this can later be retrieved by calling * {@link Scene#getUserData()}. * * @since JavaFX 8u40 */ public void setUserData(Object value) { getProperties().put(USER_DATA_KEY, value); } /** * Returns a previously set Object property, or null if no such property * has been set using the {@link Scene#setUserData(java.lang.Object)} method. * * @return The Object that was previously set, or null if no property * has been set or if null was set. * * @since JavaFX 8u40 */ public Object getUserData() { return getProperties().get(USER_DATA_KEY); } /* ************************************************************************* * * * Component Orientation Properties * * * **************************************************************************/ @SuppressWarnings("removal") private static final NodeOrientation defaultNodeOrientation = AccessController.doPrivileged( (PrivilegedAction) () -> Boolean.getBoolean("javafx.scene.nodeOrientation.RTL")) ? NodeOrientation.RIGHT_TO_LEFT : NodeOrientation.INHERIT; /** * Node orientation describes the flow of visual data within a node. * In the English speaking world, visual data normally flows from * left-to-right. In an Arabic or Hebrew world, visual data flows * from right-to-left. This is consistent with the reading order * of text in both worlds. * * @defaultValue if the system property {@code javafx.scene.nodeOrientation.RTL} is {@code true}, * {@code NodeOrientation.RIGHT_TO_LEFT}, otherwise {@code NodeOrientation.INHERIT} * @since JavaFX 8.0 */ private ObjectProperty nodeOrientation; private EffectiveOrientationProperty effectiveNodeOrientationProperty; private NodeOrientation effectiveNodeOrientation; public final void setNodeOrientation(NodeOrientation orientation) { nodeOrientationProperty().set(orientation); } public final NodeOrientation getNodeOrientation() { return nodeOrientation == null ? defaultNodeOrientation : nodeOrientation.get(); } public final ObjectProperty nodeOrientationProperty() { if (nodeOrientation == null) { nodeOrientation = new StyleableObjectProperty(defaultNodeOrientation) { @Override protected void invalidated() { sceneEffectiveOrientationInvalidated(); getRoot().applyCss(); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "nodeOrientation"; } @Override public CssMetaData getCssMetaData() { //TODO - not yet supported throw new UnsupportedOperationException("Not supported yet."); } }; } return nodeOrientation; } public final NodeOrientation getEffectiveNodeOrientation() { if (effectiveNodeOrientation == null) { effectiveNodeOrientation = calcEffectiveNodeOrientation(); } return effectiveNodeOrientation; } /** * The effective node orientation of a scene resolves the inheritance of * node orientation, returning either left-to-right or right-to-left. * @return the effective node orientation of this scene * @since JavaFX 8.0 */ public final ReadOnlyObjectProperty effectiveNodeOrientationProperty() { if (effectiveNodeOrientationProperty == null) { effectiveNodeOrientationProperty = new EffectiveOrientationProperty(); } return effectiveNodeOrientationProperty; } private void parentEffectiveOrientationInvalidated() { if (getNodeOrientation() == NodeOrientation.INHERIT) { sceneEffectiveOrientationInvalidated(); } } private void sceneEffectiveOrientationInvalidated() { effectiveNodeOrientation = null; if (effectiveNodeOrientationProperty != null) { effectiveNodeOrientationProperty.invalidate(); } getRoot().parentResolvedOrientationInvalidated(); } private NodeOrientation calcEffectiveNodeOrientation() { NodeOrientation orientation = getNodeOrientation(); if (orientation == NodeOrientation.INHERIT) { Window window = getWindow(); if (window != null) { Window parent = null; if (window instanceof Stage) { parent = ((Stage)window).getOwner(); } else if (window instanceof EmbeddedWindow win) { return win.getNodeOrientation(); } else { if (window instanceof PopupWindow) { parent = ((PopupWindow)window).getOwnerWindow(); } } if (parent != null) { Scene scene = parent.getScene(); if (scene != null) return scene.getEffectiveNodeOrientation(); } } return NodeOrientation.LEFT_TO_RIGHT; } return orientation; } private final class EffectiveOrientationProperty extends ReadOnlyObjectPropertyBase { @Override public NodeOrientation get() { return getEffectiveNodeOrientation(); } @Override public Object getBean() { return Scene.this; } @Override public String getName() { return "effectiveNodeOrientation"; } public void invalidate() { fireValueChangedEvent(); } } private Map accMap; Accessible removeAccessible(Node node) { if (accMap == null) return null; return accMap.remove(node); } void addAccessible(Node node, Accessible acc) { if (accMap == null) { accMap = new HashMap<>(); } accMap.put(node, acc); } private void disposeAccessibles() { if (accMap != null) { for (Map.Entry entry : accMap.entrySet()) { Node node = entry.getKey(); Accessible acc = entry.getValue(); if (node.accessible != null) { /* This node has already been initialized to another scene. * Note an accessible can be returned to the node before the * pulse if getAccessible() is called. In which case it must * already being removed from accMap. */ if (node.accessible == acc) { System.err.println("[A11y] 'node.accessible == acc' should never happen."); } if (node.getScene() == this) { System.err.println("[A11y] 'node.getScene() == this' should never happen."); } acc.dispose(); } else { if (node.getScene() == this) { node.accessible = acc; } else { acc.dispose(); } } } accMap.clear(); } } private Accessible accessible; Accessible getAccessible() { /* * The accessible for the Scene should never be * requested when the peer is not set. * This can only happen in a error case where a * descender of this Scene was not disposed and * it still being used by the AT client and trying * to reach to the top level window. */ if (peer == null) return null; if (accessible == null) { accessible = Application.GetApplication().createAccessible(); accessible.setEventHandler(new Accessible.EventHandler() { @SuppressWarnings("removal") @Override public AccessControlContext getAccessControlContext() { return getPeer().getAccessControlContext(); } @Override public Object getAttribute(AccessibleAttribute attribute, Object... parameters) { switch (attribute) { case CHILDREN: { Parent root = getRoot(); if (root != null) { return FXCollections.observableArrayList(root); } break; } case TEXT: { Window w = getWindow(); if (w instanceof Stage) { return ((Stage)w).getTitle(); } break; } case NODE_AT_POINT: { Window window = getWindow(); /* is this screen to scene translation correct ? not considering camera ? */ Point2D pt = (Point2D)parameters[0]; PickResult res = pick(pt.getX() - getX() - window.getX(), pt.getY() - getY() - window.getY()); if (res != null) { Node node = res.getIntersectedNode(); if (node != null) return node; } return getRoot();//not sure } case ROLE: { if (getRoot() != null && getRoot().getAccessibleRole() == AccessibleRole.DIALOG) { return AccessibleRole.DIALOG; } else { return AccessibleRole.PARENT; } } case SCENE: return Scene.this; case FOCUS_NODE: { if (transientFocusContainer != null) { return transientFocusContainer.queryAccessibleAttribute(AccessibleAttribute.FOCUS_NODE); } return getFocusOwner(); } default: } return super.getAttribute(attribute, parameters); } }); PlatformImpl.accessibilityActiveProperty().set(true); } return accessible; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy