javafx.scene.Scene Maven / Gradle / Ivy
Show all versions of openjfx-78-backport Show documentation
/*
* Copyright (c) 2010, 2013, 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.javafx.scene.SceneHelper;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.sun.javafx.runtime.SystemProperties;
import com.sun.javafx.scene.input.PickResultChooser;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.DefaultProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyObjectPropertyBase;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventDispatchChain;
import javafx.event.EventDispatcher;
import javafx.event.EventHandler;
import javafx.event.EventTarget;
import javafx.event.EventType;
import javafx.geometry.Point2D;
import javafx.geometry.Point3D;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.GestureEvent;
import javafx.scene.input.InputMethodEvent;
import javafx.scene.input.InputMethodRequests;
import javafx.scene.input.InputMethodTextRun;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.Mnemonic;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseDragEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.PickResult;
import javafx.scene.input.RotateEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.input.SwipeEvent;
import javafx.scene.input.TouchEvent;
import javafx.scene.input.TouchPoint;
import javafx.scene.input.TransferMode;
import javafx.scene.input.ZoomEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.stage.Window;
import javafx.util.Duration;
import com.sun.javafx.Logging;
import com.sun.javafx.Utils;
import com.sun.javafx.beans.annotations.Default;
import com.sun.javafx.collections.TrackableObservableList;
import com.sun.javafx.css.StyleManager;
import javafx.css.StyleableObjectProperty;
import javafx.css.CssMetaData;
import com.sun.javafx.cursor.CursorFrame;
import com.sun.javafx.event.EventQueue;
import com.sun.javafx.geom.PickRay;
import com.sun.javafx.geom.Rectangle;
import com.sun.javafx.geom.Vec3d;
import com.sun.javafx.geom.transform.BaseTransform;
import com.sun.javafx.geom.transform.GeneralTransform3D;
import sun.util.logging.PlatformLogger;
import com.sun.javafx.perf.PerformanceTracker;
import com.sun.javafx.robot.impl.FXRobotHelper;
import com.sun.javafx.scene.CssFlags;
import com.sun.javafx.scene.SceneEventDispatcher;
import com.sun.javafx.scene.input.InputEventUtils;
import com.sun.javafx.scene.traversal.Direction;
import com.sun.javafx.scene.traversal.TraversalEngine;
import com.sun.javafx.tk.TKDragGestureListener;
import com.sun.javafx.tk.TKDragSourceListener;
import com.sun.javafx.tk.TKDropTargetListener;
import com.sun.javafx.tk.TKPulseListener;
import com.sun.javafx.tk.TKScene;
import com.sun.javafx.tk.TKSceneListener;
import com.sun.javafx.tk.TKScenePaintListener;
import com.sun.javafx.tk.TKStage;
import com.sun.javafx.tk.Toolkit;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import javafx.application.Platform;
import javafx.application.ConditionalFeature;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.geometry.Bounds;
import javafx.geometry.Orientation;
import javafx.scene.image.WritableImage;
import javafx.stage.PopupWindow;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Callback;
import static com.sun.javafx.logging.PulseLogger.PULSE_LOGGING_ENABLED;
import static com.sun.javafx.logging.PulseLogger.PULSE_LOGGER;
import com.sun.javafx.scene.input.KeyCodeMap;
import com.sun.javafx.sg.PGLightBase;
import javafx.geometry.NodeOrientation;
/**
* 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.
*
*
* Scene objects must be constructed and modified on the
* JavaFX Application Thread.
*
*
* 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 boolean depthBuffer = false;
private int dirtyBits;
private final AccessControlContext acc = AccessController.getContext();
private Camera defaultCamera;
//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 IllegalStateException if this constructor is called on a thread
* other than the JavaFX Application Thread.
* @throws NullPointerException if root is null
*/
public Scene(Parent root) {
this(root, -1, -1, Color.WHITE, false);
}
//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 IllegalStateException if this constructor is called on a thread
* other than the JavaFX Application Thread.
* @throws NullPointerException if root is null
*/
public Scene(Parent root, double width, double height) {
this(root, width, height, Color.WHITE, false);
}
/**
* Creates a Scene for a specific root Node with a fill.
*
* @param root The parent
* @param fill The fill
*
* @throws IllegalStateException if this constructor is called on a thread
* other than the JavaFX Application Thread.
* @throws NullPointerException if root is null
*/
public Scene(Parent root, @Default("javafx.scene.paint.Color.WHITE") Paint fill) {
this(root, -1, -1, fill, false);
}
/**
* 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 IllegalStateException if this constructor is called on a thread
* other than the JavaFX Application Thread.
* @throws NullPointerException if root is null
*/
public Scene(Parent root, double width, double height,
@Default("javafx.scene.paint.Color.WHITE") Paint fill) {
this(root, width, height, fill, false);
}
/**
* 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.
*
* @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 IllegalStateException if this constructor is called on a thread
* other than the JavaFX Application Thread.
* @throws NullPointerException if root is null
*
* @see javafx.scene.Node#setDepthTest(DepthTest)
*/
public Scene(Parent root, @Default("-1") double width, @Default("-1") double height, boolean depthBuffer) {
this(root, width, height, Color.WHITE, depthBuffer);
}
/**
* 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.
*
* @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 flag
*
* The depthBuffer and antiAliasing flags are conditional feature and the default
* value for both are false. See
* {@link javafx.application.ConditionalFeature#SCENE3D ConditionalFeature.SCENE3D}
* for more information.
*
* @throws IllegalStateException if this constructor is called on a thread
* other than the JavaFX Application Thread.
* @throws NullPointerException if root is null
*
* @see javafx.scene.Node#setDepthTest(DepthTest)
* @since JavaFX 8.0
*/
public Scene(Parent root, @Default("-1") double width, @Default("-1") double height,
boolean depthBuffer, boolean antiAliasing) {
// TODO: 3D - Support scene anti-aliasing using MSAA.
this(root, width, height, Color.WHITE, depthBuffer);
// NOTE: this block will be removed once implement anti-aliasing
if (antiAliasing) {
String logname = Scene.class.getName();
PlatformLogger.getLogger(logname).warning("3D anti-aliasing is "
+ "not supported yet.");
}
if ((depthBuffer || antiAliasing)
&& !Platform.isSupported(ConditionalFeature.SCENE3D)) {
String logname = Scene.class.getName();
PlatformLogger.getLogger(logname).warning("System can't support "
+ "ConditionalFeature.SCENE3D");
// TODO: 3D - ignore depthBuffer and antiAliasing at rendering time
}
}
private Scene(Parent root, double width, double height,
@Default("javafx.scene.paint.Color.WHITE") Paint fill,
boolean depthBuffer) {
if (root == null) {
throw new NullPointerException("Root cannot be null");
}
if (depthBuffer && !Platform.isSupported(ConditionalFeature.SCENE3D)) {
String logname = Scene.class.getName();
PlatformLogger.getLogger(logname).warning("System can't support "
+ "ConditionalFeature.SCENE3D");
}
Toolkit.getToolkit().checkFxUserThread();
setRoot(root);
init(width, height, depthBuffer);
setFill(fill);
}
static {
PerformanceTracker.setSceneAccessor(new PerformanceTracker.SceneAccessor() {
public void setPerfTracker(Scene scene, PerformanceTracker tracker) {
synchronized (trackerMonitor) {
scene.tracker = tracker;
}
}
public PerformanceTracker getPerfTracker(Scene scene) {
synchronized (trackerMonitor) {
return scene.tracker;
}
}
});
FXRobotHelper.setSceneAccessor(new FXRobotHelper.FXRobotSceneAccessor() {
public void processKeyEvent(Scene scene, KeyEvent keyEvent) {
scene.impl_processKeyEvent(keyEvent);
}
public void processMouseEvent(Scene scene, MouseEvent mouseEvent) {
scene.impl_processMouseEvent(mouseEvent);
}
public void processScrollEvent(Scene scene, ScrollEvent scrollEvent) {
scene.processGestureEvent(scrollEvent, scene.scrollGesture);
}
public ObservableList getChildren(Parent parent) {
return parent.getChildren(); //was impl_getChildren
}
public Object renderToImage(Scene scene, Object platformImage) {
return scene.snapshot(null).impl_getPlatformImage();
}
});
SceneHelper.setSceneAccessor(
new SceneHelper.SceneAccessor() {
@Override
public void setPaused(boolean paused) {
Scene.paused = paused;
}
@Override
public void parentEffectiveOrientationInvalidated(
final Scene scene) {
scene.parentEffectiveOrientationInvalidated();
}
@Override
public Camera getEffectiveCamera(Scene scene) {
return scene.getEffectiveCamera();
}
@Override
public void setSceneDelta(Scene scene,
double deltaX,
double deltaY) {
scene.setSceneDelta(deltaX, deltaY);
}
});
}
// 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;
// Flag set by the Toolkit when we are paused for JMX debugging
private static boolean paused = false;
/**
* 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;
}
/**
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@Deprecated
public static void impl_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) {
Toolkit.getToolkit().checkFxUserThread();
if (dirtyNodes == null || dirtyNodesSize == 0) {
if (impl_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.impl_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
// Node.impl_processCSS(boolean)
sceneRoot.impl_clearDirty(com.sun.javafx.scene.DirtyBits.NODE_CSS);
sceneRoot.processCSS();
}
}
/**
* List of dirty layout roots.
* When a parent is either marked as a layout root or is unmanaged and it
* has its needsLayout flag set to true, then that node is added to this set
* so that it can be laid out on the next pulse without requiring its
* ancestors to be laid out.
*/
private Set dirtyLayoutRootsA = new LinkedHashSet(10);
private Set dirtyLayoutRootsB = new LinkedHashSet(10);
private Set dirtyLayoutRoots = dirtyLayoutRootsA;
/**
* Add the specified parent to this scene's dirty layout list.
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@Deprecated
public void addToDirtyLayoutList(Parent p) {
// If the current size of the list is 0 then we will need to schedule
// a pulse event because a layout pass is needed.
if (dirtyLayoutRoots.isEmpty()) {
Toolkit.getToolkit().requestNextPulse();
}
// Add the node.
dirtyLayoutRoots.add(p);
}
/**
* Remove the specified parent from this scene's dirty layout list.
*/
void removeFromDirtyLayoutList(Parent p) {
dirtyLayoutRoots.remove(p);
}
private void doLayoutPass() {
// sometimes a layout pass with cause scene-graph changes (bounds/structure)
// that leave some branches needing further layout, so pass through roots twice
layoutDirtyRoots();
layoutDirtyRoots();
// we don't want to spin too long in layout, so if there are still dirty
// roots, we'll leave those for next pulse.
if (dirtyLayoutRoots.size() > 0) {
PlatformLogger logger = Logging.getLayoutLogger();
if (logger.isLoggable(PlatformLogger.FINER)) {
logger.finer("after layout pass, "+dirtyLayoutRoots.size()+" layout root nodes still dirty");
}
Toolkit.getToolkit().requestNextPulse();
}
}
private void layoutDirtyRoots() {
if (dirtyLayoutRoots.size() > 0) {
PlatformLogger logger = Logging.getLayoutLogger();
Set temp = dirtyLayoutRoots;
if (dirtyLayoutRoots == dirtyLayoutRootsA) {
dirtyLayoutRoots = dirtyLayoutRootsB;
} else {
dirtyLayoutRoots = dirtyLayoutRootsA;
}
for (Parent parent : temp) {
if (parent.getScene() == this && parent.isNeedsLayout()) {
if (logger.isLoggable(PlatformLogger.FINE)) {
logger.fine("<<< START >>> root = "+parent.toString());
}
parent.layout();
if (logger.isLoggable(PlatformLogger.FINE)) {
logger.fine("<<< END >>> root = "+parent.toString());
}
}
}
temp.clear();
}
}
/**
* The peer of this scene
*
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@Deprecated
private TKScene impl_peer;
/**
* Get Scene's peer
*
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@Deprecated
public TKScene impl_getPeer() {
return impl_peer;
}
/**
* The scene pulse listener that gets called on toolkit pulses
*/
ScenePulseListener scenePulseListener = new ScenePulseListener();
/**
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@Deprecated
public TKPulseListener impl_getScenePulseListener() {
if (SystemProperties.isDebug()) {
return scenePulseListener;
}
return null;
}
/**
* Return true if this {@code Scene} is anti-aliased otherwise false.
* @since JavaFX 8.0
*/
public boolean isAntiAliasing() {
//TODO: 3D - Implement this method.
return false; // For now
}
/**
* The {@code Window} for this {@code Scene}
*/
private ReadOnlyObjectWrapper window;
private 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();
getKeyHandler().windowForSceneChanged(oldWindow, newWindow);
if (oldWindow != null) {
impl_disposePeer();
}
if (newWindow != null) {
impl_initPeer();
}
parentEffectiveOrientationInvalidated();
oldWindow = newWindow;
}
@Override
public Object getBean() {
return Scene.this;
}
@Override
public String getName() {
return "window";
}
};
}
return window;
}
/**
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@Deprecated
public void impl_setWindow(Window value) {
setWindow(value);
}
/**
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@Deprecated
public void impl_initPeer() {
assert impl_peer == null;
Window window = getWindow();
// impl_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 = window.impl_getPeer();
if (windowPeer == null) {
// This is fine, the window is not visible. impl_initPeer() will
// be called again later, when the window is being shown.
return;
}
PerformanceTracker.logEvent("Scene.initPeer started");
impl_setAllowPGAccess(true);
Toolkit tk = Toolkit.getToolkit();
impl_peer = windowPeer.createTKScene(isDepthBufferInteral());
PerformanceTracker.logEvent("Scene.initPeer TKScene created");
impl_peer.setSecurityContext(acc);
impl_peer.setTKSceneListener(new ScenePeerListener());
impl_peer.setTKScenePaintListener(new ScenePeerPaintListener());
PerformanceTracker.logEvent("Scene.initPeer TKScene set");
impl_peer.setRoot(getRoot().impl_getPGNode());
impl_peer.setFillPaint(getFill() == null ? null : tk.getPaint(getFill()));
getEffectiveCamera().impl_updatePG();
impl_peer.setCamera(getEffectiveCamera().getPlatformCamera());
impl_peer.markDirty();
PerformanceTracker.logEvent("Scene.initPeer TKScene initialized");
impl_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(impl_peer, EnumSet.allOf(TransferMode.class), dragGestureListener);
}
tk.enableDrop(impl_peer, new DropTargetListener());
tk.installInputMethodRequests(impl_peer, new InputMethodRequestsDelegate());
PerformanceTracker.logEvent("Scene.initPeer finished");
}
/**
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@Deprecated
public void impl_disposePeer() {
if (impl_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 impl_disposePeer()
// has already been called.
return;
}
PerformanceTracker.logEvent("Scene.disposePeer started");
Toolkit tk = Toolkit.getToolkit();
tk.removeSceneTkPulseListener(scenePulseListener);
impl_peer.dispose();
impl_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) {
_root.impl_transformsChanged();
}
if (_root.isResizable()) {
_root.resize(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()) {
_root.resize(_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;
}
private double sceneDeltaX;
private double sceneDeltaY;
private void setSceneDelta(final double deltaX, final double deltaY) {
if ((sceneDeltaX != deltaX) || (sceneDeltaY != deltaY)) {
setX(getX() - sceneDeltaX + deltaX);
setY(getY() - sceneDeltaY + deltaY);
sceneDeltaX = deltaX;
sceneDeltaY = deltaY;
}
}
// 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);
}
/**
* 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, but what is painted behind it will
* depend on the platform. The default value is the color white.
*
* @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.setImpl_traversalEngine(null);
}
oldRoot = _value;
if (_value.getImpl_traversalEngine() == null) {
_value.setImpl_traversalEngine(new TraversalEngine(_value, true));
}
_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;
}
private void doLayoutPassWithoutPulse(int maxAttempts) {
for (int i = 0; dirtyLayoutRoots.size() > 0 && i != maxAttempts; ++i) {
layoutDirtyRoots();
}
}
void setNeedsRepaint() {
if (this.impl_peer != null) {
impl_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
doLayoutPassWithoutPulse(3);
if (!paused) {
getRoot().updateBounds();
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 xMax = (int)Math.ceil(x + w);
int yMax = (int)Math.ceil(y + h);
int width = Math.max(xMax - xMin, 1);
int height = Math.max(yMax - yMin, 1);
if (wimg == null) {
wimg = new WritableImage(width, height);
} else {
width = (int)wimg.getWidth();
height = (int)wimg.getHeight();
}
impl_setAllowPGAccess(true);
context.x = xMin;
context.y = yMin;
context.width = width;
context.height = height;
context.transform = transform;
context.depthBuffer = depthBuffer;
context.root = root.impl_getPGNode();
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);
camera.impl_updatePG();
context.camera = camera.getPlatformCamera();
} else {
context.camera = null;
}
Toolkit.WritableImageAccessor accessor = Toolkit.getWritableImageAccessor();
context.platformImage = accessor.getTkImageLoader(wimg);
impl_setAllowPGAccess(false);
Object tkImage = tk.renderToImage(context);
accessor.loadTkImage(wimg, tkImage);
if (camera != null) {
impl_setAllowPGAccess(true);
camera.setViewWidth(cameraViewWidth);
camera.setViewHeight(cameraViewHeight);
camera.impl_updatePG();
impl_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.impl_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, isDepthBufferInteral(),
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;
static void addSnapshotRunnable(Runnable r) {
Toolkit.getToolkit().checkFxUserThread();
if (snapshotPulseListener == null) {
snapshotRunnableListA = new ArrayList();
snapshotRunnableListB = new ArrayList();
snapshotRunnableList = snapshotRunnableListA;
snapshotPulseListener = new TKPulseListener() {
@Override public void pulse() {
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);
}
snapshotRunnableList.add(r);
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) {
if (!paused) {
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 = new Runnable() {
@Override public void run() {
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);
}
// lets us know when initialized so our triggers can be a bit more effective
boolean initialized = false;
// This does not push changes to peer because cursor updates are pushed on mouse events
/**
* 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 SimpleObjectProperty(this, "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().impl_reapplyCSS();
}
};
/**
* Gets an observable list 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.
*
* @return the list of stylesheets to use with this scene
*/
public final ObservableList getStylesheets() { return stylesheets; }
/**
* Retrieves the depth buffer attribute for this scene.
* @return the depth buffer attribute.
*/
public final boolean isDepthBuffer() {
return depthBuffer;
}
boolean isDepthBufferInteral() {
if (!Platform.isSupported(ConditionalFeature.SCENE3D)) {
return false;
}
return depthBuffer;
}
private void init(double width, double height, boolean depthBuffer) {
if (width >= 0) {
widthSetByUser = width;
setWidth((float)width);
}
if (height >= 0) {
heightSetByUser = height;
setHeight((float)height);
}
sizeInitialized = (widthSetByUser >= 0 && heightSetByUser >= 0);
this.depthBuffer = depthBuffer;
init();
}
private void init() {
if (PerformanceTracker.isLoggingEnabled()) {
PerformanceTracker.logEvent("Scene.init for [" + this + "]");
}
mouseHandler = new MouseHandler();
clickGenerator = new ClickGenerator();
initialized = true;
if (PerformanceTracker.isLoggingEnabled()) {
PerformanceTracker.logEvent("Scene.init for [" + this + "] - finished");
}
}
private 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();
boolean computeWidth = false;
boolean computeHeight = false;
double rootWidth = widthSetByUser;
double rootHeight = heightSetByUser;
if (widthSetByUser < 0) {
rootWidth = root.prefWidth(heightSetByUser >= 0 ? heightSetByUser : -1);
rootWidth = root.boundedSize(rootWidth,
root.minWidth(heightSetByUser >= 0 ? heightSetByUser : -1),
root.maxWidth(heightSetByUser >= 0 ? heightSetByUser : -1));
computeWidth = true;
}
if (heightSetByUser < 0) {
rootHeight = root.prefHeight(widthSetByUser >= 0 ? widthSetByUser : -1);
rootHeight = root.boundedSize(rootHeight,
root.minHeight(widthSetByUser >= 0 ? widthSetByUser : -1),
root.maxHeight(widthSetByUser >= 0 ? widthSetByUser : -1));
computeHeight = true;
}
if (root.getContentBias() == Orientation.HORIZONTAL) {
if (heightSetByUser < 0) {
rootHeight = root.boundedSize(
root.prefHeight(rootWidth),
root.minHeight(rootWidth),
root.maxHeight(rootWidth));
computeHeight = true;
}
} else if (root.getContentBias() == Orientation.VERTICAL) {
if (widthSetByUser < 0) {
rootWidth = root.boundedSize(
root.prefWidth(rootHeight),
root.minWidth(rootHeight),
root.maxWidth(rootHeight));
computeWidth = true;
}
}
root.resize(rootWidth, rootHeight);
doLayoutPass();
if (computeWidth) {
setWidth(root.isResizable()? root.getLayoutX() + root.getTranslateX() + root.getLayoutBounds().getWidth() :
root.getBoundsInParent().getMaxX());
} else {
setWidth(widthSetByUser);
}
if (computeHeight) {
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");
}
/**
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@Deprecated
public void impl_preferredSize() {
preferredSize();
}
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 {
EventTarget 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();
/**
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
// SB-dependency: RT-22747 has been filed to track this
@Deprecated
public void impl_processMouseEvent(MouseEvent e) {
if (e.getEventType() == MouseEvent.MOUSE_CLICKED) {
// Ignore click generated by platform, we are generating
// smarter clicks here by ClickGenerator
return;
}
mouseHandler.process(e);
}
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;
} 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 = 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) {
t.impl_reset();
}
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,
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 *
* *
**************************************************************************/
/*
* We cannot initialize keyHandler in init because some of the triggers
* access it before the init block.
* No clue why def keyHandler = bind lazy {KeyHandler{scene:this};}
* does not compile.
*/
private KeyHandler keyHandler = null;
private KeyHandler getKeyHandler() {
if (keyHandler == null) {
keyHandler = new KeyHandler();
}
return keyHandler;
}
/**
* 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;
}
/**
* This is a map from focusTraversable nodes within this scene
* to instances of a traversal engine. The traversal engine is
* either the instance for the scene itself, or for a Parent
* nested somewhere within this scene.
*
* This has package access for testing purposes.
*/
Map traversalRegistry; // Map
/**
* Searches up the scene graph for a Parent with a traversal engine.
*/
private TraversalEngine lookupTraversalEngine(Node node) {
Parent p = node.getParent();
while (p != null) {
if (p.getImpl_traversalEngine() != null) {
return p.getImpl_traversalEngine();
}
p = p.getParent();
}
// This shouldn't ever occur, since walking up the tree
// should always find the Scene's root, which always has
// a traversal engine. But if for some reason we get here,
// just return the root's traversal engine.
return getRoot().getImpl_traversalEngine();
}
/**
* Registers a traversable node with a traversal engine
* on this scene.
*/
void registerTraversable(Node n) {
initializeInternalEventDispatcher();
final TraversalEngine te = lookupTraversalEngine(n);
if (te != null) {
if (traversalRegistry == null) {
traversalRegistry = new HashMap();
}
traversalRegistry.put(n, te);
te.reg(n);
}
}
/**
* Unregisters a traversable node from this scene.
*/
void unregisterTraversable(Node n) {
final TraversalEngine te = (TraversalEngine) traversalRegistry.remove(n);
if (te != null) {
te.unreg(n);
}
}
/**
* Traverses focus from the given node in the given direction.
*/
void traverse(Node node, Direction dir) {
/*
** if the registry is null then there are no
** registered traversable nodes in this scene
*/
if (traversalRegistry != null) {
TraversalEngine te = (TraversalEngine) traversalRegistry.get(node);
if (te == null) {
te = lookupTraversalEngine(node);
}
te.trav(node, dir);
}
}
/**
* 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() {
getRoot().getImpl_traversalEngine().getTopLeftFocusableNode();
}
/**
* 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);
}
/**
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
// SB-dependency: RT-24668 has been filed to track this
@Deprecated
public void impl_processKeyEvent(KeyEvent e) {
if (dndGesture != null) {
if (!dndGesture.processKey(e)) {
dndGesture = null;
}
}
getKeyHandler().process(e);
}
void requestFocus(Node node) {
getKeyHandler().requestFocus(node);
}
private Node oldFocusOwner;
/**
* 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 ReadOnlyObjectWrapper focusOwner = new ReadOnlyObjectWrapper(this, "focusOwner") {
@Override
protected void invalidated() {
if (oldFocusOwner != null) {
((Node.FocusedProperty) oldFocusOwner.focusedProperty()).store(false);
}
Node value = get();
if (value != null) {
((Node.FocusedProperty) value.focusedProperty()).store(keyHandler.windowFocused);
if (value != oldFocusOwner) {
value.getScene().impl_enableInputMethodEvents(
value.getInputMethodRequests() != null
&& value.getOnInputMethodTextChanged() != null);
}
}
if (oldFocusOwner != null) {
((Node.FocusedProperty) oldFocusOwner.focusedProperty()).notifyListeners();
}
if (value != null) {
((Node.FocusedProperty) value.focusedProperty()).notifyListeners();
}
PlatformLogger logger = Logging.getFocusLogger();
if (logger.isLoggable(PlatformLogger.FINE)) {
logger.fine("Changed focus from "
+ oldFocusOwner + " to " + value);
}
oldFocusOwner = value;
}
};
public final Node getFocusOwner() {
return focusOwner.get();
}
public final ReadOnlyObjectProperty focusOwnerProperty() {
return focusOwner.getReadOnlyProperty();
}
// For testing.
void focusCleanup() {
scenePulseListener.focusCleanup();
}
private void processInputMethodEvent(InputMethodEvent e) {
Node node = getFocusOwner();
if (node != null) {
node.fireEvent(e);
}
}
/**
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@Deprecated
public void impl_enableInputMethodEvents(boolean enable) {
if (impl_peer != null) {
impl_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() {
return !isFocusDirty()
&& (getRoot().cssFlag == CssFlags.CLEAN)
&& dirtyLayoutRoots.isEmpty();
}
/**
* 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 (impl_peer != null) {
Toolkit.getToolkit().requestNextPulse();
}
}
/**
* Set the specified dirty bit
*/
private void setDirty(DirtyBits dirtyBit) {
dirtyBits |= dirtyBit.getMask();
}
/**
* Test the specified dirty bit
*/
private boolean isDirty(DirtyBits dirtyBit) {
return ((dirtyBits & dirtyBit.getMask()) != 0);
}
/**
* Test whether the dirty bits are empty
*/
private boolean isDirtyEmpty() {
return dirtyBits == 0;
}
/**
* Clear all dirty bits
*/
private void clearDirty() {
dirtyBits = 0;
}
private enum DirtyBits {
FILL_DIRTY,
ROOT_DIRTY,
CAMERA_DIRTY,
LIGHTS_DIRTY;
private int mask;
private DirtyBits() {
mask = 1 << ordinal();
}
public final int getMask() { return mask; }
}
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;
Object peerLights[] = impl_peer.getLights();
if (!lights.isEmpty() || (peerLights != null)) {
if (lights.isEmpty()) {
impl_peer.setLights(null);
} else {
if (peerLights == null || peerLights.length < lights.size()) {
peerLights = new PGLightBase[lights.size()];
}
int i = 0;
for (; i < lights.size(); i++) {
peerLights[i] = lights.get(i).impl_getPGNode();
}
// Clear the rest of the list
while (i < peerLights.length && peerLights[i] != null) {
peerLights[i++] = null;
}
impl_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.impl_syncPGNode();
}
}
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.impl_syncPGNode();
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)) {
impl_peer.setRoot(getRoot().impl_getPGNode());
}
if (isDirty(DirtyBits.FILL_DIRTY)) {
Toolkit tk = Toolkit.getToolkit();
impl_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)) {
cam.impl_updatePG();
impl_peer.setCamera(cam.getPlatformCamera());
}
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);
Scene.this.focusInitial();
} else if (!oldOwner.isCanReceiveFocus()) {
Scene.this.requestFocus(null);
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");
}
if (PULSE_LOGGING_ENABLED) {
long start = System.currentTimeMillis();
Scene.this.doCSSPass();
PULSE_LOGGER.fxMessage(start, System.currentTimeMillis(), "CSS Pass");
start = System.currentTimeMillis();
Scene.this.doLayoutPass();
PULSE_LOGGER.fxMessage(start, System.currentTimeMillis(), "Layout Pass");
} else {
Scene.this.doCSSPass();
Scene.this.doLayoutPass();
}
boolean dirty = dirtyNodes == null || dirtyNodesSize != 0 || !isDirtyEmpty();
if (dirty) {
getRoot().updateBounds();
if (impl_peer != null) {
try {
long start = PULSE_LOGGING_ENABLED ? System.currentTimeMillis() : 0;
impl_peer.waitForRenderingToComplete();
impl_peer.waitForSynchronization();
if (PULSE_LOGGING_ENABLED) {
PULSE_LOGGER.fxMessage(start, System.currentTimeMillis(), "Waiting for previous rendering");
}
start = PULSE_LOGGING_ENABLED ? System.currentTimeMillis() : 0;
// synchronize scene properties
syncLights();
synchronizeSceneProperties();
// Run the synchronizer
synchronizeSceneNodes();
Scene.this.mouseHandler.pulse();
// Tell the scene peer that it needs to repaint
impl_peer.markDirty();
if (PULSE_LOGGING_ENABLED) {
PULSE_LOGGER.fxMessage(start, System.currentTimeMillis(), "Copy state to render graph");
}
} finally {
impl_peer.releaseSynchronization();
}
} else {
long start = PULSE_LOGGING_ENABLED ? System.currentTimeMillis() : 0;
synchronizeSceneProperties();
synchronizeSceneNodes();
Scene.this.mouseHandler.pulse();
if (PULSE_LOGGING_ENABLED) {
PULSE_LOGGER.fxMessage(start, System.currentTimeMillis(), "Synchronize with null peer");
}
}
}
// required for image cursor created from animated image
Scene.this.mouseHandler.updateCursorFrame();
focusCleanup();
if (firstPulse) {
if (PerformanceTracker.isLoggingEnabled()) {
PerformanceTracker.logEvent("Scene - first repaint - layout complete");
AccessController.doPrivileged(new PrivilegedAction
* 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 super InputMethodEvent> value) {
onInputMethodTextChangedProperty().set( value);
}
public final EventHandler super InputMethodEvent> 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;
}
}
/***************************************************************************
* *
* Component Orientation Properties *
* *
**************************************************************************/
private static final NodeOrientation defaultNodeOrientation =
AccessController.doPrivileged(
new PrivilegedAction() {
@Override public Boolean run() {
return Boolean.getBoolean("javafx.scene.nodeOrientation.RTL");
}
}) ? NodeOrientation.RIGHT_TO_LEFT : NodeOrientation.INHERIT;
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();
}
/**
* Property holding NodeOrientation.
*
* 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. The default value is left-to-right.
*
*
* @return NodeOrientation
* @since JavaFX 8.0
*/
public final ObjectProperty nodeOrientationProperty() {
if (nodeOrientation == null) {
nodeOrientation = new StyleableObjectProperty(defaultNodeOrientation) {
@Override
protected void invalidated() {
sceneEffectiveOrientationInvalidated();
getRoot().impl_reapplyCSS();
}
@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.
* @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 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();
}
}
}