/*
* Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package javafx.stage;
import com.sun.javafx.util.
Utils;
import com.sun.javafx.event.
DirectEvent;
import java.util.
ArrayList;
import java.util.
List;
import javafx.beans.
InvalidationListener;
import javafx.beans.
Observable;
import javafx.beans.property.
BooleanProperty;
import javafx.beans.property.
BooleanPropertyBase;
import javafx.beans.property.
ObjectProperty;
import javafx.beans.property.
ReadOnlyDoubleProperty;
import javafx.beans.property.
ReadOnlyDoubleWrapper;
import javafx.beans.property.
SimpleBooleanProperty;
import javafx.beans.property.
SimpleObjectProperty;
import javafx.beans.value.
ChangeListener;
import javafx.beans.value.
ObservableValue;
import javafx.collections.
ObservableList;
import javafx.event.
Event;
import javafx.event.
EventHandler;
import javafx.geometry.
BoundingBox;
import javafx.geometry.
Bounds;
import javafx.geometry.
Rectangle2D;
import javafx.scene.
Group;
import javafx.scene.
Node;
import javafx.scene.
Parent;
import javafx.scene.
Scene;
import com.sun.javafx.event.
EventHandlerManager;
import com.sun.javafx.event.
EventRedirector;
import com.sun.javafx.event.
EventUtil;
import com.sun.javafx.perf.
PerformanceTracker;
import com.sun.javafx.scene.
SceneHelper;
import com.sun.javafx.stage.
FocusUngrabEvent;
import com.sun.javafx.stage.
PopupWindowPeerListener;
import com.sun.javafx.stage.
WindowCloseRequestHandler;
import com.sun.javafx.stage.
WindowEventDispatcher;
import com.sun.javafx.tk.
Toolkit;
import java.security.
AllPermission;
import javafx.beans.property.
ObjectPropertyBase;
import javafx.beans.property.
ReadOnlyObjectProperty;
import javafx.beans.property.
ReadOnlyObjectWrapper;
import javafx.beans.value.
WeakChangeListener;
import javafx.event.
EventTarget;
import javafx.event.
EventType;
import javafx.scene.input.
KeyCombination;
import javafx.scene.input.
KeyEvent;
import javafx.scene.input.
MouseEvent;
import javafx.scene.input.
ScrollEvent;
import javafx.scene.layout.
Background;
import javafx.scene.layout.
Pane;
/**
* PopupWindow is the parent for a variety of different types of popup
* based windows including {@link Popup} and {@link javafx.scene.control.Tooltip}
* and {@link javafx.scene.control.ContextMenu}.
* <p>
* A PopupWindow is a secondary window which has no window decorations or title bar.
* It doesn't show up in the OS as a top-level window. It is typically
* used for tool tip like notification, drop down boxes, menus, and so forth.
* <p>
* The PopupWindow <strong>cannot be shown without an owner</strong>.
* PopupWindows require that an owner window exist in order to be shown. However,
* it is possible to create a PopupWindow ahead of time and simply set the owner
* (or change the owner) before first being made visible. Attempting to change
* the owner while the PopupWindow is visible will result in an IllegalStateException.
* <p>
* The PopupWindow encapsulates much of the behavior and functionality common to popups,
* such as the ability to close when the "esc" key is pressed, or the ability to
* hide all child popup windows whenever this window is hidden. These abilities can
* be enabled or disabled via properties.
* @since JavaFX 2.0
*/
public abstract class
PopupWindow extends
Window {
/**
* A private list of all child popups.
*/
private final
List<
PopupWindow>
children = new
ArrayList<
PopupWindow>();
/**
* Keeps track of the bounds of the content, and adjust the position and
* size of the popup window accordingly. This way as the popup content
* changes, the window will be changed to match.
*/
private final
InvalidationListener popupWindowUpdater =
new
InvalidationListener() {
@
Override
public void
invalidated(final
Observable observable) {
cachedExtendedBounds = null;
cachedAnchorBounds = null;
updateWindow(
getAnchorX(),
getAnchorY());
}
};
/**
* RT-28454: When a parent node or parent window we are associated with is not
* visible anymore, possibly because the scene was not valid anymore, we should hide.
*/
private
ChangeListener<
Boolean>
changeListener = (
observable,
oldValue,
newValue) -> {
if (
oldValue && !
newValue) {
hide();
}
};
private
WeakChangeListener<
Boolean>
weakOwnerNodeListener = new
WeakChangeListener(
changeListener);
public
PopupWindow() {
final
Pane popupRoot = new
Pane();
popupRoot.
setBackground(
Background.
EMPTY);
popupRoot.
getStyleClass().
add("popup");
final
Scene scene =
SceneHelper.
createPopupScene(
popupRoot);
scene.
setFill(null);
super.setScene(
scene);
popupRoot.
layoutBoundsProperty().
addListener(
popupWindowUpdater);
popupRoot.
boundsInLocalProperty().
addListener(
popupWindowUpdater);
scene.
rootProperty().
addListener(
new
InvalidationListener() {
private
Node oldRoot =
scene.
getRoot();
@
Override
public void
invalidated(final
Observable observable) {
final
Node newRoot =
scene.
getRoot();
if (
oldRoot !=
newRoot) {
if (
oldRoot != null) {
oldRoot.
layoutBoundsProperty()
.
removeListener(
popupWindowUpdater);
oldRoot.
boundsInLocalProperty()
.
removeListener(
popupWindowUpdater);
oldRoot.
getStyleClass().
remove("popup");
}
if (
newRoot != null) {
newRoot.
layoutBoundsProperty()
.
addListener(
popupWindowUpdater);
newRoot.
boundsInLocalProperty()
.
addListener(
popupWindowUpdater);
newRoot.
getStyleClass().
add("popup");
}
oldRoot =
newRoot;
cachedExtendedBounds = null;
cachedAnchorBounds = null;
updateWindow(
getAnchorX(),
getAnchorY());
}
}
});
}
/**
* Gets the observable, modifiable list of children which are placed in this
* PopupWindow.
*
* @return the PopupWindow content
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@
Deprecated
protected
ObservableList<
Node>
getContent() {
final
Parent rootNode =
getScene().
getRoot();
if (
rootNode instanceof
Group) {
return ((
Group)
rootNode).
getChildren();
}
if (
rootNode instanceof
Pane) {
return ((
Pane)
rootNode).
getChildren();
}
throw new
IllegalStateException(
"The content of the Popup can't be accessed");
}
/**
* The window which is the parent of this popup. All popups must have an
* owner window.
*/
private
ReadOnlyObjectWrapper<
Window>
ownerWindow =
new
ReadOnlyObjectWrapper<
Window>(this, "ownerWindow");
public final
Window getOwnerWindow() {
return
ownerWindow.
get();
}
public final
ReadOnlyObjectProperty<
Window>
ownerWindowProperty() {
return
ownerWindow.
getReadOnlyProperty();
}
/**
* The node which is the owner of this popup. All popups must have an
* owner window but are not required to be associated with an owner node.
* If an autohide Popup has an owner node, mouse press inside the owner node
* doesn't cause the Popup to hide.
*/
private
ReadOnlyObjectWrapper<
Node>
ownerNode =
new
ReadOnlyObjectWrapper<
Node>(this, "ownerNode");
public final
Node getOwnerNode() {
return
ownerNode.
get();
}
public final
ReadOnlyObjectProperty<
Node>
ownerNodeProperty() {
return
ownerNode.
getReadOnlyProperty();
}
/**
* Note to subclasses: the scene used by PopupWindow is very specifically
* managed by PopupWindow. This method is overridden to throw
* UnsupportedOperationException. You cannot specify your own scene.
*
* @param scene
*/
@
Override protected final void
setScene(
Scene scene) {
throw new
UnsupportedOperationException();
}
/**
* This convenience variable indicates whether, when the popup is shown,
* it should automatically correct its position such that it doesn't end
* up positioned off the screen.
* @defaultValue true
*/
private
BooleanProperty autoFix =
new
BooleanPropertyBase(true) {
@
Override
protected void
invalidated() {
handleAutofixActivation(
isShowing(),
get());
}
@
Override
public
Object getBean() {
return
PopupWindow.this;
}
@
Override
public
String getName() {
return "autoFix";
}
};
public final void
setAutoFix(boolean
value) {
autoFix.
set(
value); }
public final boolean
isAutoFix() { return
autoFix.
get(); }
public final
BooleanProperty autoFixProperty() { return
autoFix; }
/**
* Specifies whether Popups should auto hide. If a popup loses focus and
* autoHide is true, then the popup will be hidden automatically.
* <p>
* The only exception is when owner Node is specified using {@link #show(javafx.scene.Node, double, double)}.
* Focusing owner Node will not hide the PopupWindow.
* </p>
* @defaultValue false
*/
private
BooleanProperty autoHide =
new
BooleanPropertyBase() {
@
Override
protected void
invalidated() {
handleAutohideActivation(
isShowing(),
get());
}
@
Override
public
Object getBean() {
return
PopupWindow.this;
}
@
Override
public
String getName() {
return "autoHide";
}
};
public final void
setAutoHide(boolean
value) {
autoHide.
set(
value); }
public final boolean
isAutoHide() { return
autoHide.
get(); }
public final
BooleanProperty autoHideProperty() { return
autoHide; }
/**
* Called after autoHide is run.
*/
private
ObjectProperty<
EventHandler<
Event>>
onAutoHide =
new
SimpleObjectProperty<
EventHandler<
Event>>(this, "onAutoHide");
public final void
setOnAutoHide(
EventHandler<
Event>
value) {
onAutoHide.
set(
value); }
public final
EventHandler<
Event>
getOnAutoHide() { return
onAutoHide.
get(); }
public final
ObjectProperty<
EventHandler<
Event>>
onAutoHideProperty() { return
onAutoHide; }
/**
* Specifies whether the PopupWindow should be hidden when an unhandled escape key
* is pressed while the popup has focus.
* @defaultValue true
*/
private
BooleanProperty hideOnEscape =
new
SimpleBooleanProperty(this, "hideOnEscape", true);
public final void
setHideOnEscape(boolean
value) {
hideOnEscape.
set(
value); }
public final boolean
isHideOnEscape() { return
hideOnEscape.
get(); }
public final
BooleanProperty hideOnEscapeProperty() { return
hideOnEscape; }
/**
* Specifies whether the event, which caused the Popup to hide, should be
* consumed. Having the event consumed prevents it from triggering some
* additional UI response in the Popup's owner window.
* @defaultValue true
* @since JavaFX 2.2
*/
private
BooleanProperty consumeAutoHidingEvents =
new
SimpleBooleanProperty(this, "consumeAutoHidingEvents",
true);
public final void
setConsumeAutoHidingEvents(boolean
value) {
consumeAutoHidingEvents.
set(
value);
}
public final boolean
getConsumeAutoHidingEvents() {
return
consumeAutoHidingEvents.
get();
}
public final
BooleanProperty consumeAutoHidingEventsProperty() {
return
consumeAutoHidingEvents;
}
/**
* Show the popup.
* @param owner The owner of the popup. This must not be null.
* @throws NullPointerException if owner is null
* @throws IllegalArgumentException if the specified owner window would
* create cycle in the window hierarchy
*/
public void
show(
Window owner) {
validateOwnerWindow(
owner);
showImpl(
owner);
}
/**
* Shows the popup at the specified location on the screen. The popup window
* is positioned in such way that its anchor point ({@see #anchorLocation})
* is displayed at the specified {@code anchorX} and {@code anchorY}
* coordinates.
* <p>
* The popup is associated with the specified owner node. The {@code Window}
* which contains the owner node at the time of the call becomes an owner
* window of the displayed popup.
* </p>
* <p>
* Note that when {@link #autoHideProperty()} is set to true, mouse press on the owner Node
* will not hide the PopupWindow.
* </p>
*
* @param ownerNode The owner Node of the popup. It must not be null
* and must be associated with a Window.
* @param anchorX the x position of the popup anchor in screen coordinates
* @param anchorY the y position of the popup anchor in screen coordinates
* @throws NullPointerException if ownerNode is null
* @throws IllegalArgumentException if the specified owner node is not
* associated with a Window or when the window would create cycle
* in the window hierarchy
*/
public void
show(
Node ownerNode, double
anchorX, double
anchorY) {
if (
ownerNode == null) {
throw new
NullPointerException("The owner node must not be null");
}
final
Scene ownerNodeScene =
ownerNode.
getScene();
if ((
ownerNodeScene == null)
|| (
ownerNodeScene.
getWindow() == null)) {
throw new
IllegalArgumentException(
"The owner node needs to be associated with a window");
}
final
Window newOwnerWindow =
ownerNodeScene.
getWindow();
validateOwnerWindow(
newOwnerWindow);
this.
ownerNode.
set(
ownerNode);
// RT-28454 PopupWindow should disappear when owner node is not visible
if (
ownerNode != null) {
ownerNode.
visibleProperty().
addListener(
weakOwnerNodeListener);
}
updateWindow(
anchorX,
anchorY);
showImpl(
newOwnerWindow);
}
/**
* Shows the popup at the specified location on the screen. The popup window
* is positioned in such way that its anchor point ({@see #anchorLocation})
* is displayed at the specified {@code anchorX} and {@code anchorY}
* coordinates.
*
* @param ownerWindow The owner of the popup. This must not be null.
* @param anchorX the x position of the popup anchor in screen coordinates
* @param anchorY the y position of the popup anchor in screen coordinates
* @throws NullPointerException if ownerWindow is null
* @throws IllegalArgumentException if the specified owner window would
* create cycle in the window hierarchy
*/
public void
show(
Window ownerWindow, double
anchorX, double
anchorY) {
validateOwnerWindow(
ownerWindow);
updateWindow(
anchorX,
anchorY);
showImpl(
ownerWindow);
}
private void
showImpl(final
Window owner) {
// Update the owner field
this.
ownerWindow.
set(
owner);
if (
owner instanceof
PopupWindow) {
((
PopupWindow)
owner).
children.
add(this);
}
// RT-28454 PopupWindow should disappear when owner node is not visible
if (
owner != null) {
owner.
showingProperty().
addListener(
weakOwnerNodeListener);
}
final
Scene sceneValue =
getScene();
SceneHelper.
parentEffectiveOrientationInvalidated(
sceneValue);
// RT-28447
final
Scene ownerScene =
getRootWindow(
owner).
getScene();
if (
ownerScene != null) {
if (
ownerScene.
getUserAgentStylesheet() != null) {
sceneValue.
setUserAgentStylesheet(
ownerScene.
getUserAgentStylesheet());
}
sceneValue.
getStylesheets().
setAll(
ownerScene.
getStylesheets());
if (
sceneValue.
getCursor() == null) {
sceneValue.
setCursor(
ownerScene.
getCursor());
}
}
// It is required that the root window exist and be visible to show the popup.
if (
getRootWindow(
owner).
isShowing()) {
// We do show() first so that the width and height of the
// popup window are initialized. This way the x,y location of the
// popup calculated below uses the right width and height values for
// its calculation. (fix for part of RT-10675).
show();
}
}
/**
* Hide this Popup and all its children
*/
@
Override public void
hide() {
for (
PopupWindow c :
children) {
if (
c.
isShowing()) {
c.
hide();
}
}
children.
clear();
super.hide();
// RT-28454 when popup hides, remove listeners; these are added when the popup shows.
if (
getOwnerWindow() != null)
getOwnerWindow().
showingProperty().
removeListener(
weakOwnerNodeListener);
if (
getOwnerNode() != null)
getOwnerNode().
visibleProperty().
removeListener(
weakOwnerNodeListener);
}
/**
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@
Deprecated
@
Override protected void
impl_visibleChanging(boolean
visible) {
super.impl_visibleChanging(
visible);
PerformanceTracker.
logEvent("PopupWindow.storeVisible for [PopupWindow]");
Toolkit toolkit =
Toolkit.
getToolkit();
if (
visible && (
impl_peer == null)) {
// Setup the peer
StageStyle popupStyle;
try {
final
SecurityManager securityManager =
System.
getSecurityManager();
if (
securityManager != null) {
securityManager.
checkPermission(new
AllPermission());
}
popupStyle =
StageStyle.
TRANSPARENT;
} catch (final
SecurityException e) {
popupStyle =
StageStyle.
UNDECORATED;
}
impl_peer =
toolkit.
createTKPopupStage(this,
popupStyle,
getOwnerWindow().
impl_getPeer(),
acc);
peerListener = new
PopupWindowPeerListener(
PopupWindow.this);
}
}
private
Window rootWindow;
/**
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@
Deprecated
@
Override protected void
impl_visibleChanged(boolean
visible) {
super.impl_visibleChanged(
visible);
final
Window ownerWindowValue =
getOwnerWindow();
if (
visible) {
rootWindow =
getRootWindow(
ownerWindowValue);
startMonitorOwnerEvents(
ownerWindowValue);
// currently we consider popup window to be focused when it is
// visible and its owner window is focused (we need to track
// that through listener on owner window focused property)
// a better solution would require some focus manager, which can
// track focus state across multiple windows
bindOwnerFocusedProperty(
ownerWindowValue);
setFocused(
ownerWindowValue.
isFocused());
handleAutofixActivation(true,
isAutoFix());
handleAutohideActivation(true,
isAutoHide());
} else {
stopMonitorOwnerEvents(
ownerWindowValue);
unbindOwnerFocusedProperty(
ownerWindowValue);
setFocused(false);
handleAutofixActivation(false,
isAutoFix());
handleAutohideActivation(false,
isAutoHide());
rootWindow = null;
}
PerformanceTracker.
logEvent("PopupWindow.storeVisible for [PopupWindow] finished");
}
/**
* Specifies the x coordinate of the popup anchor point on the screen. If
* the {@code anchorLocation} is set to {@code WINDOW_TOP_LEFT} or
* {@code WINDOW_BOTTOM_LEFT} the {@code x} and {@code anchorX} values will
* be identical.
*
* @since JavaFX 8.0
*/
private final
ReadOnlyDoubleWrapper anchorX =
new
ReadOnlyDoubleWrapper(this, "anchorX",
Double.
NaN);
public final void
setAnchorX(final double
value) {
updateWindow(
value,
getAnchorY());
}
public final double
getAnchorX() {
return
anchorX.
get();
}
public final
ReadOnlyDoubleProperty anchorXProperty() {
return
anchorX.
getReadOnlyProperty();
}
/**
* Specifies the y coordinate of the popup anchor point on the screen. If
* the {@code anchorLocation} is set to {@code WINDOW_TOP_LEFT} or
* {@code WINDOW_TOP_RIGHT} the {@code y} and {@code anchorY} values will
* be identical.
*
* @since JavaFX 8.0
*/
private final
ReadOnlyDoubleWrapper anchorY =
new
ReadOnlyDoubleWrapper(this, "anchorY",
Double.
NaN);
public final void
setAnchorY(final double
value) {
updateWindow(
getAnchorX(),
value);
}
public final double
getAnchorY() {
return
anchorY.
get();
}
public final
ReadOnlyDoubleProperty anchorYProperty() {
return
anchorY.
getReadOnlyProperty();
}
/**
* Specifies the popup anchor point which is used in popup positioning. The
* point can be set to a corner of the popup window or a corner of its
* content. In this context the content corners are derived from the popup
* root node's layout bounds.
* <p>
* In general changing of the anchor location won't change the current
* window position. Instead of that, the {@code anchorX} and {@code anchorY}
* values are recalculated to correspond to the new anchor point.
* </p>
* @since JavaFX 8.0
*/
private final
ObjectProperty<
AnchorLocation>
anchorLocation =
new
ObjectPropertyBase<
AnchorLocation>(
AnchorLocation.
WINDOW_TOP_LEFT) {
@
Override
protected void
invalidated() {
cachedAnchorBounds = null;
updateWindow(
windowToAnchorX(
getX()),
windowToAnchorY(
getY()));
}
@
Override
public
Object getBean() {
return
PopupWindow.this;
}
@
Override
public
String getName() {
return "anchorLocation";
}
};
public final void
setAnchorLocation(final
AnchorLocation value) {
anchorLocation.
set(
value);
}
public final
AnchorLocation getAnchorLocation() {
return
anchorLocation.
get();
}
public final
ObjectProperty<
AnchorLocation>
anchorLocationProperty() {
return
anchorLocation;
}
/**
* Anchor location constants for popup anchor point selection.
*
* @since JavaFX 8.0
*/
public enum
AnchorLocation {
/** Represents top left window corner. */
WINDOW_TOP_LEFT(0, 0, false),
/** Represents top right window corner. */
WINDOW_TOP_RIGHT(1, 0, false),
/** Represents bottom left window corner. */
WINDOW_BOTTOM_LEFT(0, 1, false),
/** Represents bottom right window corner. */
WINDOW_BOTTOM_RIGHT(1, 1, false),
/** Represents top left content corner. */
CONTENT_TOP_LEFT(0, 0, true),
/** Represents top right content corner. */
CONTENT_TOP_RIGHT(1, 0, true),
/** Represents bottom left content corner. */
CONTENT_BOTTOM_LEFT(0, 1, true),
/** Represents bottom right content corner. */
CONTENT_BOTTOM_RIGHT(1, 1, true);
private final double
xCoef;
private final double
yCoef;
private final boolean
contentLocation;
private
AnchorLocation(final double
xCoef, final double
yCoef,
final boolean
contentLocation) {
this.
xCoef =
xCoef;
this.
yCoef =
yCoef;
this.
contentLocation =
contentLocation;
}
double
getXCoef() {
return
xCoef;
}
double
getYCoef() {
return
yCoef;
}
boolean
isContentLocation() {
return
contentLocation;
}
};
@
Override
void
setXInternal(final double
value) {
updateWindow(
windowToAnchorX(
value),
getAnchorY());
}
@
Override
void
setYInternal(final double
value) {
updateWindow(
getAnchorX(),
windowToAnchorY(
value));
}
@
Override
void
notifyLocationChanged(final double
newX, final double
newY) {
super.notifyLocationChanged(
newX,
newY);
anchorX.
set(
windowToAnchorX(
newX));
anchorY.
set(
windowToAnchorY(
newY));
}
private
Bounds cachedExtendedBounds;
private
Bounds cachedAnchorBounds;
private
Bounds getExtendedBounds() {
if (
cachedExtendedBounds == null) {
final
Parent rootNode =
getScene().
getRoot();
cachedExtendedBounds =
union(
rootNode.
getLayoutBounds(),
rootNode.
getBoundsInLocal());
}
return
cachedExtendedBounds;
}
private
Bounds getAnchorBounds() {
if (
cachedAnchorBounds == null) {
cachedAnchorBounds =
getAnchorLocation().
isContentLocation()
?
getScene().
getRoot()
.
getLayoutBounds()
:
getExtendedBounds();
}
return
cachedAnchorBounds;
}
private void
updateWindow(final double
newAnchorX,
final double
newAnchorY) {
final
AnchorLocation anchorLocationValue =
getAnchorLocation();
final
Parent rootNode =
getScene().
getRoot();
final
Bounds extendedBounds =
getExtendedBounds();
final
Bounds anchorBounds =
getAnchorBounds();
final double
anchorXCoef =
anchorLocationValue.
getXCoef();
final double
anchorYCoef =
anchorLocationValue.
getYCoef();
final double
anchorDeltaX =
anchorXCoef *
anchorBounds.
getWidth();
final double
anchorDeltaY =
anchorYCoef *
anchorBounds.
getHeight();
double
anchorScrMinX =
newAnchorX -
anchorDeltaX;
double
anchorScrMinY =
newAnchorY -
anchorDeltaY;
if (
autofixActive) {
final
Screen currentScreen =
Utils.
getScreenForPoint(
newAnchorX,
newAnchorY);
final
Rectangle2D screenBounds =
Utils.
hasFullScreenStage(
currentScreen)
?
currentScreen.
getBounds()
:
currentScreen.
getVisualBounds();
if (
anchorXCoef <= 0.5) {
// left side of the popup is more important, try to keep it
// visible if the popup width is larger than screen width
anchorScrMinX =
Math.
min(
anchorScrMinX,
screenBounds.
getMaxX()
-
anchorBounds.
getWidth());
anchorScrMinX =
Math.
max(
anchorScrMinX,
screenBounds.
getMinX());
} else {
// right side of the popup is more important
anchorScrMinX =
Math.
max(
anchorScrMinX,
screenBounds.
getMinX());
anchorScrMinX =
Math.
min(
anchorScrMinX,
screenBounds.
getMaxX()
-
anchorBounds.
getWidth());
}
if (
anchorYCoef <= 0.5) {
// top side of the popup is more important
anchorScrMinY =
Math.
min(
anchorScrMinY,
screenBounds.
getMaxY()
-
anchorBounds.
getHeight());
anchorScrMinY =
Math.
max(
anchorScrMinY,
screenBounds.
getMinY());
} else {
// bottom side of the popup is more important
anchorScrMinY =
Math.
max(
anchorScrMinY,
screenBounds.
getMinY());
anchorScrMinY =
Math.
min(
anchorScrMinY,
screenBounds.
getMaxY()
-
anchorBounds.
getHeight());
}
}
final double
windowScrMinX =
anchorScrMinX -
anchorBounds.
getMinX()
+
extendedBounds.
getMinX();
final double
windowScrMinY =
anchorScrMinY -
anchorBounds.
getMinY()
+
extendedBounds.
getMinY();
// update popup dimensions
setWidth(
extendedBounds.
getWidth());
setHeight(
extendedBounds.
getHeight());
// update transform
rootNode.
setTranslateX(-
extendedBounds.
getMinX());
rootNode.
setTranslateY(-
extendedBounds.
getMinY());
// update popup position
// don't set Window.xExplicit unnecessarily
if (!
Double.
isNaN(
windowScrMinX)) {
super.setXInternal(
windowScrMinX);
}
// don't set Window.yExplicit unnecessarily
if (!
Double.
isNaN(
windowScrMinY)) {
super.setYInternal(
windowScrMinY);
}
// set anchor x, anchor y
anchorX.
set(
anchorScrMinX +
anchorDeltaX);
anchorY.
set(
anchorScrMinY +
anchorDeltaY);
}
private
Bounds union(final
Bounds bounds1, final
Bounds bounds2) {
final double
minX =
Math.
min(
bounds1.
getMinX(),
bounds2.
getMinX());
final double
minY =
Math.
min(
bounds1.
getMinY(),
bounds2.
getMinY());
final double
maxX =
Math.
max(
bounds1.
getMaxX(),
bounds2.
getMaxX());
final double
maxY =
Math.
max(
bounds1.
getMaxY(),
bounds2.
getMaxY());
return new
BoundingBox(
minX,
minY,
maxX -
minX,
maxY -
minY);
}
private double
windowToAnchorX(final double
windowX) {
final
Bounds anchorBounds =
getAnchorBounds();
return
windowX -
getExtendedBounds().
getMinX()
+
anchorBounds.
getMinX()
+
getAnchorLocation().
getXCoef()
*
anchorBounds.
getWidth();
}
private double
windowToAnchorY(final double
windowY) {
final
Bounds anchorBounds =
getAnchorBounds();
return
windowY -
getExtendedBounds().
getMinY()
+
anchorBounds.
getMinY()
+
getAnchorLocation().
getYCoef()
*
anchorBounds.
getHeight();
}
/**
*
* Gets the root (non PopupWindow) Window for the provided window.
*
* @param win the Window for which to get the root window
*/
private static
Window getRootWindow(
Window win) {
// should be enough to traverse PopupWindow hierarchy here to get to the
// first non-popup focusable window
while (
win instanceof
PopupWindow) {
win = ((
PopupWindow)
win).
getOwnerWindow();
}
return
win;
}
void
doAutoHide() {
// There is a timing problem here. I would like to have this isVisible
// check, such that we don't send an onAutoHide event if it was already
// invisible. However, visible is already false by the time this method
// gets called, when done by certain code paths.
// if (isVisible()) {
// hide this popup
hide();
if (
getOnAutoHide() != null) {
getOnAutoHide().
handle(new
Event(this, this,
Event.
ANY));
}
// }
}
@
Override
WindowEventDispatcher createInternalEventDispatcher() {
return new
WindowEventDispatcher(new
PopupEventRedirector(this),
new
WindowCloseRequestHandler(this),
new
EventHandlerManager(this));
}
@
Override
Window getWindowOwner() {
return
getOwnerWindow();
}
private void
startMonitorOwnerEvents(final
Window ownerWindowValue) {
final
EventRedirector parentEventRedirector =
ownerWindowValue.
getInternalEventDispatcher()
.
getEventRedirector();
parentEventRedirector.
addEventDispatcher(
getEventDispatcher());
}
private void
stopMonitorOwnerEvents(final
Window ownerWindowValue) {
final
EventRedirector parentEventRedirector =
ownerWindowValue.
getInternalEventDispatcher()
.
getEventRedirector();
parentEventRedirector.
removeEventDispatcher(
getEventDispatcher());
}
private
ChangeListener<
Boolean>
ownerFocusedListener;
private void
bindOwnerFocusedProperty(final
Window ownerWindowValue) {
ownerFocusedListener =
(
observable,
oldValue,
newValue) ->
setFocused(
newValue);
ownerWindowValue.
focusedProperty().
addListener(
ownerFocusedListener);
}
private void
unbindOwnerFocusedProperty(final
Window ownerWindowValue) {
ownerWindowValue.
focusedProperty().
removeListener(
ownerFocusedListener);
ownerFocusedListener = null;
}
private boolean
autofixActive;
private void
handleAutofixActivation(final boolean
visible,
final boolean
autofix) {
final boolean
newAutofixActive =
visible &&
autofix;
if (
autofixActive !=
newAutofixActive) {
autofixActive =
newAutofixActive;
if (
newAutofixActive) {
Screen.
getScreens().
addListener(
popupWindowUpdater);
updateWindow(
getAnchorX(),
getAnchorY());
} else {
Screen.
getScreens().
removeListener(
popupWindowUpdater);
}
}
}
private boolean
autohideActive;
private void
handleAutohideActivation(final boolean
visible,
final boolean
autohide) {
final boolean
newAutohideActive =
visible &&
autohide;
if (
autohideActive !=
newAutohideActive) {
// assert rootWindow != null;
autohideActive =
newAutohideActive;
if (
newAutohideActive) {
rootWindow.
increaseFocusGrabCounter();
} else {
rootWindow.
decreaseFocusGrabCounter();
}
}
}
private void
validateOwnerWindow(final
Window owner) {
if (
owner == null) {
throw new
NullPointerException("Owner window must not be null");
}
if (
wouldCreateCycle(
owner, this)) {
throw new
IllegalArgumentException(
"Specified owner window would create cycle"
+ " in the window hierarchy");
}
if (
isShowing() && (
getOwnerWindow() !=
owner)) {
throw new
IllegalStateException(
"Popup is already shown with different owner window");
}
}
private static boolean
wouldCreateCycle(
Window parent, final
Window child) {
while (
parent != null) {
if (
parent ==
child) {
return true;
}
parent =
parent.
getWindowOwner();
}
return false;
}
static class
PopupEventRedirector extends
EventRedirector {
private static final
KeyCombination ESCAPE_KEY_COMBINATION =
KeyCombination.
keyCombination("Esc");
private final
PopupWindow popupWindow;
public
PopupEventRedirector(final
PopupWindow popupWindow) {
super(
popupWindow);
this.
popupWindow =
popupWindow;
}
@
Override
protected void
handleRedirectedEvent(final
Object eventSource,
final
Event event) {
if (
event instanceof
KeyEvent) {
handleKeyEvent((
KeyEvent)
event);
return;
}
final
EventType<?>
eventType =
event.
getEventType();
if (
eventType ==
MouseEvent.
MOUSE_PRESSED
||
eventType ==
ScrollEvent.
SCROLL) {
handleAutoHidingEvents(
eventSource,
event);
return;
}
if (
eventType ==
FocusUngrabEvent.
FOCUS_UNGRAB) {
handleFocusUngrabEvent();
return;
}
}
private void
handleKeyEvent(final
KeyEvent event) {
if (
event.
isConsumed()) {
return;
}
final
Scene scene =
popupWindow.
getScene();
if (
scene != null) {
final
Node sceneFocusOwner =
scene.
getFocusOwner();
final
EventTarget eventTarget =
(
sceneFocusOwner != null) ?
sceneFocusOwner :
scene;
if (
EventUtil.
fireEvent(
eventTarget, new
DirectEvent(
event.
copyFor(
popupWindow,
eventTarget)))
== null) {
event.
consume();
return;
}
}
if ((
event.
getEventType() ==
KeyEvent.
KEY_PRESSED)
&&
ESCAPE_KEY_COMBINATION.
match(
event)) {
handleEscapeKeyPressedEvent(
event);
}
}
private void
handleEscapeKeyPressedEvent(final
Event event) {
if (
popupWindow.
isHideOnEscape()) {
popupWindow.
doAutoHide();
if (
popupWindow.
getConsumeAutoHidingEvents()) {
event.
consume();
}
}
}
private void
handleAutoHidingEvents(final
Object eventSource,
final
Event event) {
// we handle mouse pressed only for the immediate parent window,
// where we can check whether the mouse press is inside of the owner
// control or not, we will force possible child popups to close
// by sending the FOCUS_UNGRAB event
if (
popupWindow.
getOwnerWindow() !=
eventSource) {
return;
}
if (
popupWindow.
isAutoHide() && !
isOwnerNodeEvent(
event)) {
// the mouse press is outside of the owner control,
// fire FOCUS_UNGRAB to child popups
Event.
fireEvent(
popupWindow, new
FocusUngrabEvent());
popupWindow.
doAutoHide();
if (
popupWindow.
getConsumeAutoHidingEvents()) {
event.
consume();
}
}
}
private void
handleFocusUngrabEvent() {
if (
popupWindow.
isAutoHide()) {
popupWindow.
doAutoHide();
}
}
private boolean
isOwnerNodeEvent(final
Event event) {
final
Node ownerNode =
popupWindow.
getOwnerNode();
if (
ownerNode == null) {
return false;
}
final
EventTarget eventTarget =
event.
getTarget();
if (!(
eventTarget instanceof
Node)) {
return false;
}
Node node = (
Node)
eventTarget;
do {
if (
node ==
ownerNode) {
return true;
}
node =
node.
getParent();
} while (
node != null);
return false;
}
}
}