1 /*
   2  * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.stage;
  27 
  28 import com.sun.javafx.Utils;
  29 import com.sun.javafx.event.DirectEvent;
  30 import java.util.ArrayList;
  31 import java.util.List;
  32 
  33 import javafx.beans.InvalidationListener;
  34 import javafx.beans.Observable;
  35 import javafx.beans.property.BooleanProperty;
  36 import javafx.beans.property.BooleanPropertyBase;
  37 import javafx.beans.property.ObjectProperty;
  38 import javafx.beans.property.ReadOnlyDoubleProperty;
  39 import javafx.beans.property.ReadOnlyDoubleWrapper;
  40 import javafx.beans.property.SimpleBooleanProperty;
  41 import javafx.beans.property.SimpleObjectProperty;
  42 import javafx.beans.value.ChangeListener;
  43 import javafx.beans.value.ObservableValue;
  44 import javafx.collections.ObservableList;
  45 import javafx.event.Event;
  46 import javafx.event.EventHandler;
  47 import javafx.geometry.BoundingBox;
  48 import javafx.geometry.Bounds;
  49 import javafx.geometry.Rectangle2D;
  50 import javafx.scene.Group;
  51 import javafx.scene.Node;
  52 import javafx.scene.Parent;
  53 import javafx.scene.Scene;
  54 
  55 import com.sun.javafx.event.EventHandlerManager;
  56 import com.sun.javafx.event.EventRedirector;
  57 import com.sun.javafx.event.EventUtil;
  58 import com.sun.javafx.perf.PerformanceTracker;
  59 import com.sun.javafx.scene.SceneHelper;
  60 import com.sun.javafx.stage.FocusUngrabEvent;
  61 import com.sun.javafx.stage.PopupWindowPeerListener;
  62 import com.sun.javafx.stage.WindowCloseRequestHandler;
  63 import com.sun.javafx.stage.WindowEventDispatcher;
  64 import com.sun.javafx.tk.Toolkit;
  65 import javafx.beans.property.ObjectPropertyBase;
  66 import javafx.beans.property.ReadOnlyObjectProperty;
  67 import javafx.beans.property.ReadOnlyObjectWrapper;
  68 import javafx.beans.property.ReadOnlyProperty;
  69 import javafx.beans.value.WeakChangeListener;
  70 import javafx.event.EventTarget;
  71 import javafx.event.EventType;
  72 import javafx.scene.input.KeyCombination;
  73 import javafx.scene.input.KeyEvent;
  74 import javafx.scene.input.MouseEvent;
  75 import javafx.scene.layout.Background;
  76 import javafx.scene.layout.Pane;
  77 
  78 /**
  79  * PopupWindow is the parent for a variety of different types of popup
  80  * based windows including {@link Popup} and {@link javafx.scene.control.Tooltip}
  81  * and {@link javafx.scene.control.ContextMenu}.
  82  * <p>
  83  * A PopupWindow is a secondary window which has no window decorations or title bar.
  84  * It doesn't show up in the OS as a top-level window. It is typically
  85  * used for tool tip like notification, drop down boxes, menus, and so forth.
  86  * <p>
  87  * The PopupWindow <strong>cannot be shown without an owner</strong>.
  88  * PopupWindows require that an owner window exist in order to be shown. However,
  89  * it is possible to create a PopupWindow ahead of time and simply set the owner
  90  * (or change the owner) before first being made visible. Attempting to change
  91  * the owner while the PopupWindow is visible will result in an IllegalStateException.
  92  * <p>
  93  * The PopupWindow encapsulates much of the behavior and functionality common to popups,
  94  * such as the ability to close when the "esc" key is pressed, or the ability to
  95  * hide all child popup windows whenever this window is hidden. These abilities can
  96  * be enabled or disabled via properties.
  97  * @since JavaFX 2.0
  98  */
  99 public abstract class PopupWindow extends Window {
 100     /**
 101      * A private list of all child popups.
 102      */
 103     private final List<PopupWindow> children = new ArrayList<PopupWindow>();
 104 
 105     /**
 106      * Keeps track of the bounds of the content, and adjust the position and
 107      * size of the popup window accordingly. This way as the popup content
 108      * changes, the window will be changed to match.
 109      */
 110     private final InvalidationListener popupWindowUpdater =
 111             new InvalidationListener() {
 112                 @Override
 113                 public void invalidated(final Observable observable) {
 114                     cachedExtendedBounds = null;
 115                     cachedAnchorBounds = null;
 116                     updateWindow(getAnchorX(), getAnchorY());
 117                 }
 118             };
 119 
 120     /**
 121      * RT-28454: When a parent node or parent window we are associated with is not
 122      * visible anymore, possibly because the scene was not valid anymore, we should hide.
 123      */
 124     private ChangeListener<Boolean> changeListener = new ChangeListener<Boolean>() {
 125         @Override public void changed(
 126                 ObservableValue<? extends Boolean> observable, 
 127                 Boolean oldValue, Boolean newValue) {
 128             if (oldValue && !newValue) {
 129                 hide();
 130             }
 131         }
 132     };
 133     
 134     private WeakChangeListener<Boolean> weakOwnerNodeListener = new WeakChangeListener(changeListener);
 135 
 136     public PopupWindow() {
 137         final Pane popupRoot = new Pane();
 138         popupRoot.setBackground(Background.EMPTY);
 139         popupRoot.getStyleClass().add("popup");
 140 
 141         final Scene scene = SceneHelper.createPopupScene(popupRoot);
 142         scene.setFill(null);
 143         super.setScene(scene);
 144 
 145         popupRoot.layoutBoundsProperty().addListener(popupWindowUpdater);
 146         popupRoot.boundsInLocalProperty().addListener(popupWindowUpdater);
 147         scene.rootProperty().addListener(
 148                 new InvalidationListener() {
 149                     private Node oldRoot = scene.getRoot();
 150 
 151                     @Override
 152                     public void invalidated(final Observable observable) {
 153                         final Node newRoot = scene.getRoot();
 154                         if (oldRoot != newRoot) {
 155                             if (oldRoot != null) {
 156                                 oldRoot.layoutBoundsProperty()
 157                                        .removeListener(popupWindowUpdater);
 158                                 oldRoot.boundsInLocalProperty()
 159                                        .removeListener(popupWindowUpdater);
 160                                 oldRoot.getStyleClass().remove("popup");
 161                             }
 162 
 163                             if (newRoot != null) {
 164                                 newRoot.layoutBoundsProperty()
 165                                        .addListener(popupWindowUpdater);
 166                                 newRoot.boundsInLocalProperty()
 167                                        .addListener(popupWindowUpdater);
 168                                 newRoot.getStyleClass().add("popup");
 169                             }
 170 
 171                             oldRoot = newRoot;
 172 
 173                             cachedExtendedBounds = null;
 174                             cachedAnchorBounds = null;
 175                             updateWindow(getAnchorX(), getAnchorY());
 176                         }
 177                     }
 178                 });
 179     }
 180 
 181     /**
 182      * Gets the observable, modifiable list of children which are placed in this
 183      * PopupWindow.
 184      *
 185      * @return the PopupWindow content
 186      * @treatAsPrivate implementation detail
 187      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 188      */
 189     @Deprecated
 190     protected ObservableList<Node> getContent() {
 191         final Parent rootNode = getScene().getRoot();
 192         if (rootNode instanceof Group) {
 193             return ((Group) rootNode).getChildren();
 194         }
 195 
 196         if (rootNode instanceof Pane) {
 197             return ((Pane) rootNode).getChildren();
 198         }
 199 
 200         throw new IllegalStateException(
 201                 "The content of the Popup can't be accessed");
 202     }
 203 
 204     /**
 205      * The window which is the parent of this popup. All popups must have an
 206      * owner window.
 207      */
 208     private ReadOnlyObjectWrapper<Window> ownerWindow =
 209             new ReadOnlyObjectWrapper<Window>(this, "ownerWindow");
 210     public final Window getOwnerWindow() {
 211         return ownerWindow.get();
 212     }
 213     public final ReadOnlyObjectProperty<Window> ownerWindowProperty() {
 214         return ownerWindow.getReadOnlyProperty();
 215     }
 216 
 217     /**
 218      * The node which is the owner of this popup. All popups must have an
 219      * owner window but are not required to be associated with an owner node.
 220      * If an autohide Popup has an owner node, mouse press inside the owner node
 221      * doesn't cause the Popup to hide.
 222      */
 223     private ReadOnlyObjectWrapper<Node> ownerNode =
 224             new ReadOnlyObjectWrapper<Node>(this, "ownerNode");
 225     public final Node getOwnerNode() {
 226         return ownerNode.get();
 227     }
 228     public final ReadOnlyObjectProperty<Node> ownerNodeProperty() {
 229         return ownerNode.getReadOnlyProperty();
 230     }
 231 
 232     /**
 233      * Note to subclasses: the scene used by PopupWindow is very specifically
 234      * managed by PopupWindow. This method is overridden to throw
 235      * UnsupportedOperationException. You cannot specify your own scene.
 236      *
 237      * @param scene
 238      */
 239     @Override protected final void setScene(Scene scene) {
 240         throw new UnsupportedOperationException();
 241     }
 242 
 243     /**
 244      * This convenience variable indicates whether, when the popup is shown,
 245      * it should automatically correct its position such that it doesn't end
 246      * up positioned off the screen.
 247      * @defaultValue true
 248      */
 249     private BooleanProperty autoFix =
 250             new BooleanPropertyBase(true) {
 251                 @Override
 252                 protected void invalidated() {
 253                     handleAutofixActivation(isShowing(), get());
 254                 }
 255 
 256                 @Override
 257                 public Object getBean() {
 258                     return PopupWindow.this;
 259                 }
 260 
 261                 @Override
 262                 public String getName() {
 263                     return "autoFix";
 264                 }
 265             };
 266     public final void setAutoFix(boolean value) { autoFix.set(value); }
 267     public final boolean isAutoFix() { return autoFix.get(); }
 268     public final BooleanProperty autoFixProperty() { return autoFix; }
 269 
 270     /**
 271      * Specifies whether Popups should auto hide. If a popup loses focus and
 272      * autoHide is true, then the popup will be hidden automatically.
 273      * @defaultValue false
 274      */
 275     private BooleanProperty autoHide =
 276             new BooleanPropertyBase() {
 277                 @Override
 278                 protected void invalidated() {
 279                     handleAutohideActivation(isShowing(), get());
 280                 }
 281 
 282                 @Override
 283                 public Object getBean() {
 284                     return PopupWindow.this;
 285                 }
 286 
 287                 @Override
 288                 public String getName() {
 289                     return "autoHide";
 290                 }
 291             };
 292     public final void setAutoHide(boolean value) { autoHide.set(value); }
 293     public final boolean isAutoHide() { return autoHide.get(); }
 294     public final BooleanProperty autoHideProperty() { return autoHide; }
 295 
 296     /**
 297      * Called after autoHide is run.
 298      */
 299     private ObjectProperty<EventHandler<Event>> onAutoHide =
 300             new SimpleObjectProperty<EventHandler<Event>>(this, "onAutoHide");
 301     public final void setOnAutoHide(EventHandler<Event> value) { onAutoHide.set(value); }
 302     public final EventHandler<Event> getOnAutoHide() { return onAutoHide.get(); }
 303     public final ObjectProperty<EventHandler<Event>> onAutoHideProperty() { return onAutoHide; }
 304 
 305     /**
 306      * Specifies whether the PopupWindow should be hidden when an unhandled escape key
 307      * is pressed while the popup has focus.
 308      * @defaultValue true
 309      */
 310     private BooleanProperty hideOnEscape =
 311             new SimpleBooleanProperty(this, "hideOnEscape", true);
 312     public final void setHideOnEscape(boolean value) { hideOnEscape.set(value); }
 313     public final boolean isHideOnEscape() { return hideOnEscape.get(); }
 314     public final BooleanProperty hideOnEscapeProperty() { return hideOnEscape; }
 315 
 316     /**
 317      * Specifies whether the event, which caused the Popup to hide, should be
 318      * consumed. Having the event consumed prevents it from triggering some
 319      * additional UI response in the Popup's owner window.
 320      * @defaultValue true
 321      * @since JavaFX 2.2
 322      */
 323     private BooleanProperty consumeAutoHidingEvents =
 324             new SimpleBooleanProperty(this, "consumeAutoHidingEvents",
 325                                       true);
 326 
 327     public final void setConsumeAutoHidingEvents(boolean value) {
 328         consumeAutoHidingEvents.set(value);
 329     }
 330 
 331     public final boolean getConsumeAutoHidingEvents() {
 332         return consumeAutoHidingEvents.get();
 333     }
 334 
 335     public final BooleanProperty consumeAutoHidingEventsProperty() {
 336         return consumeAutoHidingEvents;
 337     }
 338 
 339     /**
 340      * Show the popup.
 341      * @param owner The owner of the popup. This must not be null.
 342      * @throws NullPointerException if owner is null
 343      * @throws IllegalArgumentException if the specified owner window would
 344      *      create cycle in the window hierarchy
 345      */
 346     public void show(Window owner) {
 347         validateOwnerWindow(owner);
 348         showImpl(owner);
 349     }
 350 
 351     /**
 352      * Shows the popup at the specified location on the screen. The popup window
 353      * is positioned in such way that its anchor point ({@see #anchorLocation})
 354      * is displayed at the specified {@code anchorX} and {@code anchorY}
 355      * coordinates.
 356      * <p>
 357      * The popup is associated with the specified owner node. The {@code Window}
 358      * which contains the owner node at the time of the call becomes an owner
 359      * window of the displayed popup.
 360      * </p>
 361      *
 362      * @param ownerNode The owner Node of the popup. It must not be null
 363      *        and must be associated with a Window.
 364      * @param anchorX the x position of the popup anchor in screen coordinates
 365      * @param anchorY the y position of the popup anchor in screen coordinates
 366      * @throws NullPointerException if ownerNode is null
 367      * @throws IllegalArgumentException if the specified owner node is not
 368      *      associated with a Window or when the window would create cycle
 369      *      in the window hierarchy
 370      */
 371     public void show(Node ownerNode, double anchorX, double anchorY) {
 372         if (ownerNode == null) {
 373             throw new NullPointerException("The owner node must not be null");
 374         }
 375 
 376         final Scene ownerNodeScene = ownerNode.getScene();
 377         if ((ownerNodeScene == null)
 378                 || (ownerNodeScene.getWindow() == null)) {
 379             throw new IllegalArgumentException(
 380                     "The owner node needs to be associated with a window");
 381         }
 382 
 383         final Window newOwnerWindow = ownerNodeScene.getWindow();
 384         validateOwnerWindow(newOwnerWindow);
 385 
 386         this.ownerNode.set(ownerNode);
 387 
 388         // RT-28454 PopupWindow should disappear when owner node is not visible
 389         if (ownerNode != null) {
 390             ownerNode.visibleProperty().addListener(weakOwnerNodeListener);
 391         }
 392 
 393         updateWindow(anchorX, anchorY);
 394         showImpl(newOwnerWindow);
 395     }
 396 
 397     /**
 398      * Shows the popup at the specified location on the screen. The popup window
 399      * is positioned in such way that its anchor point ({@see #anchorLocation})
 400      * is displayed at the specified {@code anchorX} and {@code anchorY}
 401      * coordinates.
 402      *
 403      * @param ownerWindow The owner of the popup. This must not be null.
 404      * @param anchorX the x position of the popup anchor in screen coordinates
 405      * @param anchorY the y position of the popup anchor in screen coordinates
 406      * @throws NullPointerException if ownerWindow is null
 407      * @throws IllegalArgumentException if the specified owner window would
 408      *      create cycle in the window hierarchy
 409      */
 410     public void show(Window ownerWindow, double anchorX, double anchorY) {
 411         validateOwnerWindow(ownerWindow);
 412 
 413         updateWindow(anchorX, anchorY);
 414         showImpl(ownerWindow);
 415     }
 416 
 417     private void showImpl(final Window owner) {
 418         // Update the owner field
 419         this.ownerWindow.set(owner);
 420         if (owner instanceof PopupWindow) {
 421             ((PopupWindow)owner).children.add(this);
 422         }
 423         // RT-28454 PopupWindow should disappear when owner node is not visible
 424         if (owner != null) {
 425             owner.showingProperty().addListener(weakOwnerNodeListener);
 426         }
 427 
 428         final Scene sceneValue = getScene();
 429         SceneHelper.parentEffectiveOrientationInvalidated(sceneValue);            
 430         
 431         // RT-28447
 432         final Scene ownerScene = getRootWindow(owner).getScene();
 433         if (ownerScene != null) {
 434             sceneValue.getStylesheets().setAll(ownerScene.getStylesheets());
 435             if (sceneValue.getCursor() == null) {
 436                 sceneValue.setCursor(ownerScene.getCursor());
 437             }
 438         }
 439 
 440         // It is required that the root window exist and be visible to show the popup.
 441         if (getRootWindow(owner).isShowing()) {
 442             // We do show() first so that the width and height of the
 443             // popup window are initialized. This way the x,y location of the
 444             // popup calculated below uses the right width and height values for
 445             // its calculation. (fix for part of RT-10675).
 446             show();
 447         }
 448     }
 449 
 450     /**
 451      * Hide this Popup and all its children
 452      */
 453     @Override public void hide() {
 454         for (PopupWindow c : children) {
 455             if (c.isShowing()) {
 456                 c.hide();
 457             }
 458         }        
 459         children.clear();
 460         super.hide();
 461         // RT-28454 when popup hides, remove listeners; these are added when the popup shows.
 462         if (getOwnerWindow() != null) getOwnerWindow().showingProperty().removeListener(weakOwnerNodeListener);
 463         if (getOwnerNode() != null) getOwnerNode().visibleProperty().removeListener(weakOwnerNodeListener);
 464     }
 465 
 466     /**
 467      * @treatAsPrivate implementation detail
 468      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 469      */
 470     @Deprecated
 471     @Override protected void impl_visibleChanging(boolean visible) {
 472         super.impl_visibleChanging(visible);
 473         PerformanceTracker.logEvent("PopupWindow.storeVisible for [PopupWindow]");
 474 
 475         Toolkit toolkit = Toolkit.getToolkit();
 476         if (visible && (impl_peer == null)) {
 477             // Setup the peer
 478             impl_peer = toolkit.createTKPopupStage(this, getOwnerWindow().impl_getPeer(), acc);
 479             peerListener = new PopupWindowPeerListener(PopupWindow.this);
 480         }
 481     }
 482 
 483     private Window rootWindow;
 484 
 485     /**
 486      * @treatAsPrivate implementation detail
 487      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 488      */
 489     @Deprecated
 490     @Override protected void impl_visibleChanged(boolean visible) {
 491         super.impl_visibleChanged(visible);
 492 
 493         final Window ownerWindowValue = getOwnerWindow();
 494         if (visible) {
 495             rootWindow = getRootWindow(ownerWindowValue);
 496 
 497             startMonitorOwnerEvents(ownerWindowValue);
 498             // currently we consider popup window to be focused when it is
 499             // visible and its owner window is focused (we need to track
 500             // that through listener on owner window focused property)
 501             // a better solution would require some focus manager, which can
 502             // track focus state across multiple windows
 503             bindOwnerFocusedProperty(ownerWindowValue);
 504             setFocused(ownerWindowValue.isFocused());
 505             handleAutofixActivation(true, isAutoFix());
 506             handleAutohideActivation(true, isAutoHide());
 507         } else {
 508             stopMonitorOwnerEvents(ownerWindowValue);
 509             unbindOwnerFocusedProperty(ownerWindowValue);
 510             setFocused(false);
 511             handleAutofixActivation(false, isAutoFix());
 512             handleAutohideActivation(false, isAutoHide());
 513             rootWindow = null;
 514         }
 515 
 516         PerformanceTracker.logEvent("PopupWindow.storeVisible for [PopupWindow] finished");
 517     }
 518 
 519     /**
 520      * Specifies the x coordinate of the popup anchor point on the screen. If
 521      * the {@code anchorLocation} is set to {@code WINDOW_TOP_LEFT} or
 522      * {@code WINDOW_BOTTOM_LEFT} the {@code x} and {@code anchorX} values will
 523      * be identical.
 524      *
 525      * @since JavaFX 8.0
 526      */
 527     private final ReadOnlyDoubleWrapper anchorX =
 528             new ReadOnlyDoubleWrapper(this, "anchorX", Double.NaN);
 529 
 530     public final void setAnchorX(final double value) {
 531         updateWindow(value, getAnchorY());
 532     }
 533     public final double getAnchorX() {
 534         return anchorX.get();
 535     }
 536     public final ReadOnlyDoubleProperty anchorXProperty() {
 537         return anchorX.getReadOnlyProperty();
 538     }
 539 
 540     /**
 541      * Specifies the y coordinate of the popup anchor point on the screen. If
 542      * the {@code anchorLocation} is set to {@code WINDOW_TOP_LEFT} or
 543      * {@code WINDOW_TOP_RIGHT} the {@code y} and {@code anchorY} values will
 544      * be identical.
 545      *
 546      * @since JavaFX 8.0
 547      */
 548     private final ReadOnlyDoubleWrapper anchorY =
 549             new ReadOnlyDoubleWrapper(this, "anchorY", Double.NaN);
 550 
 551     public final void setAnchorY(final double value) {
 552         updateWindow(getAnchorX(), value);
 553     }
 554     public final double getAnchorY() {
 555         return anchorY.get();
 556     }
 557     public final ReadOnlyDoubleProperty anchorYProperty() {
 558         return anchorY.getReadOnlyProperty();
 559     }
 560 
 561     /**
 562      * Specifies the popup anchor point which is used in popup positioning. The
 563      * point can be set to a corner of the popup window or a corner of its
 564      * content. In this context the content corners are derived from the popup
 565      * root node's layout bounds.
 566      * <p>
 567      * In general changing of the anchor location won't change the current
 568      * window position. Instead of that, the {@code anchorX} and {@code anchorY}
 569      * values are recalculated to correspond to the new anchor point.
 570      * </p>
 571      * @since JavaFX 8.0
 572      */
 573     private final ObjectProperty<AnchorLocation> anchorLocation =
 574             new ObjectPropertyBase<AnchorLocation>(
 575                     AnchorLocation.WINDOW_TOP_LEFT) {
 576                 @Override
 577                 protected void invalidated() {
 578                     cachedAnchorBounds = null;
 579                     updateWindow(windowToAnchorX(getX()),
 580                                  windowToAnchorY(getY()));
 581                 }
 582 
 583                 @Override
 584                 public Object getBean() {
 585                     return PopupWindow.this;
 586                 }
 587 
 588                 @Override
 589                 public String getName() {
 590                     return "anchorLocation";
 591                 }
 592             };
 593     public final void setAnchorLocation(final AnchorLocation value) {
 594         anchorLocation.set(value);
 595     }
 596     public final AnchorLocation getAnchorLocation() {
 597         return anchorLocation.get();
 598     }
 599     public final ObjectProperty<AnchorLocation> anchorLocationProperty() {
 600         return anchorLocation;
 601     }
 602 
 603     /**
 604      * Anchor location constants for popup anchor point selection.
 605      *
 606      * @since JavaFX 8.0
 607      */
 608     public enum AnchorLocation {
 609         /** Represents top left window corner. */
 610         WINDOW_TOP_LEFT(0, 0, false),
 611         /** Represents top right window corner. */
 612         WINDOW_TOP_RIGHT(1, 0, false),
 613         /** Represents bottom left window corner. */
 614         WINDOW_BOTTOM_LEFT(0, 1, false),
 615         /** Represents bottom right window corner. */
 616         WINDOW_BOTTOM_RIGHT(1, 1, false),
 617         /** Represents top left content corner. */
 618         CONTENT_TOP_LEFT(0, 0, true),
 619         /** Represents top right content corner. */
 620         CONTENT_TOP_RIGHT(1, 0, true),
 621         /** Represents bottom left content corner. */
 622         CONTENT_BOTTOM_LEFT(0, 1, true),
 623         /** Represents bottom right content corner. */
 624         CONTENT_BOTTOM_RIGHT(1, 1, true);
 625 
 626         private final double xCoef;
 627         private final double yCoef;
 628         private final boolean contentLocation;
 629 
 630         private AnchorLocation(final double xCoef, final double yCoef,
 631                                final boolean contentLocation) {
 632             this.xCoef = xCoef;
 633             this.yCoef = yCoef;
 634             this.contentLocation = contentLocation;
 635         }
 636 
 637         double getXCoef() {
 638             return xCoef;
 639         }
 640 
 641         double getYCoef() {
 642             return yCoef;
 643         }
 644 
 645         boolean isContentLocation() {
 646             return contentLocation;
 647         }
 648     };
 649 
 650     @Override
 651     void setXInternal(final double value) {
 652         updateWindow(windowToAnchorX(value), getAnchorY());
 653     }
 654 
 655     @Override
 656     void setYInternal(final double value) {
 657         updateWindow(getAnchorX(), windowToAnchorY(value));
 658     }
 659 
 660     @Override
 661     void notifyLocationChanged(final double newX, final double newY) {
 662         super.notifyLocationChanged(newX, newY);
 663         anchorX.set(windowToAnchorX(newX));
 664         anchorY.set(windowToAnchorY(newY));
 665     }
 666 
 667     private Bounds cachedExtendedBounds;
 668     private Bounds cachedAnchorBounds;
 669 
 670     private Bounds getExtendedBounds() {
 671         if (cachedExtendedBounds == null) {
 672             final Parent rootNode = getScene().getRoot();
 673             cachedExtendedBounds = union(rootNode.getLayoutBounds(),
 674                                          rootNode.getBoundsInLocal());
 675         }
 676 
 677         return cachedExtendedBounds;
 678     }
 679 
 680     private Bounds getAnchorBounds() {
 681         if (cachedAnchorBounds == null) {
 682             cachedAnchorBounds = getAnchorLocation().isContentLocation()
 683                                          ? getScene().getRoot()
 684                                                      .getLayoutBounds()
 685                                          : getExtendedBounds();
 686         }
 687 
 688         return cachedAnchorBounds;
 689     }
 690 
 691     private void updateWindow(final double newAnchorX,
 692                               final double newAnchorY) {
 693         final AnchorLocation anchorLocationValue = getAnchorLocation();
 694         final Parent rootNode = getScene().getRoot();
 695         final Bounds extendedBounds = getExtendedBounds();
 696         final Bounds anchorBounds = getAnchorBounds();
 697 
 698         final double anchorXCoef = anchorLocationValue.getXCoef();
 699         final double anchorYCoef = anchorLocationValue.getYCoef();
 700         final double anchorDeltaX = anchorXCoef * anchorBounds.getWidth();
 701         final double anchorDeltaY = anchorYCoef * anchorBounds.getHeight();
 702         double anchorScrMinX = newAnchorX - anchorDeltaX;
 703         double anchorScrMinY = newAnchorY - anchorDeltaY;
 704 
 705         if (autofixActive) {
 706             final Screen currentScreen =
 707                     Utils.getScreenForPoint(newAnchorX, newAnchorY);
 708             final Rectangle2D screenBounds =
 709                     Utils.hasFullScreenStage(currentScreen)
 710                             ? currentScreen.getBounds()
 711                             : currentScreen.getVisualBounds();
 712 
 713             if (anchorXCoef <= 0.5) {
 714                 // left side of the popup is more important, try to keep it
 715                 // visible if the popup width is larger than screen width
 716                 anchorScrMinX = Math.min(anchorScrMinX,
 717                                          screenBounds.getMaxX()
 718                                              - anchorBounds.getWidth());
 719                 anchorScrMinX = Math.max(anchorScrMinX, screenBounds.getMinX());
 720             } else {
 721                 // right side of the popup is more important
 722                 anchorScrMinX = Math.max(anchorScrMinX, screenBounds.getMinX());
 723                 anchorScrMinX = Math.min(anchorScrMinX,
 724                                          screenBounds.getMaxX()
 725                                              - anchorBounds.getWidth());
 726             }
 727 
 728             if (anchorYCoef <= 0.5) {
 729                 // top side of the popup is more important
 730                 anchorScrMinY = Math.min(anchorScrMinY,
 731                                          screenBounds.getMaxY()
 732                                              - anchorBounds.getHeight());
 733                 anchorScrMinY = Math.max(anchorScrMinY, screenBounds.getMinY());
 734             } else {
 735                 // bottom side of the popup is more important
 736                 anchorScrMinY = Math.max(anchorScrMinY, screenBounds.getMinY());
 737                 anchorScrMinY = Math.min(anchorScrMinY,
 738                                          screenBounds.getMaxY()
 739                                              - anchorBounds.getHeight());
 740             }
 741         }
 742 
 743         final double windowScrMinX =
 744                 anchorScrMinX - anchorBounds.getMinX()
 745                               + extendedBounds.getMinX();
 746         final double windowScrMinY =
 747                 anchorScrMinY - anchorBounds.getMinY()
 748                               + extendedBounds.getMinY();
 749 
 750         // update popup dimensions
 751         setWidth(extendedBounds.getWidth());
 752         setHeight(extendedBounds.getHeight());
 753         // update transform
 754         rootNode.setTranslateX(-extendedBounds.getMinX());
 755         rootNode.setTranslateY(-extendedBounds.getMinY());
 756 
 757         // update popup position
 758         // don't set Window.xExplicit unnecessarily
 759         if (!Double.isNaN(windowScrMinX) || !Double.isNaN(getX())) {
 760             super.setXInternal(windowScrMinX);
 761         }
 762         // don't set Window.yExplicit unnecessarily
 763         if (!Double.isNaN(windowScrMinY) || !Double.isNaN(getY())) {
 764             super.setYInternal(windowScrMinY);
 765         }
 766 
 767         // set anchor x, anchor y
 768         anchorX.set(anchorScrMinX + anchorDeltaX);
 769         anchorY.set(anchorScrMinY + anchorDeltaY);
 770     }
 771 
 772     private Bounds union(final Bounds bounds1, final Bounds bounds2) {
 773         final double minX = Math.min(bounds1.getMinX(), bounds2.getMinX());
 774         final double minY = Math.min(bounds1.getMinY(), bounds2.getMinY());
 775         final double maxX = Math.max(bounds1.getMaxX(), bounds2.getMaxX());
 776         final double maxY = Math.max(bounds1.getMaxY(), bounds2.getMaxY());
 777 
 778         return new BoundingBox(minX, minY, maxX - minX, maxY - minY);
 779     }
 780 
 781     private double windowToAnchorX(final double windowX) {
 782         final Bounds anchorBounds = getAnchorBounds();
 783         return windowX - getExtendedBounds().getMinX()
 784                        + anchorBounds.getMinX()
 785                        + getAnchorLocation().getXCoef()
 786                              * anchorBounds.getWidth();
 787     }
 788 
 789     private double windowToAnchorY(final double windowY) {
 790         final Bounds anchorBounds = getAnchorBounds();
 791         return windowY - getExtendedBounds().getMinY()
 792                        + anchorBounds.getMinY()
 793                        + getAnchorLocation().getYCoef()
 794                              * anchorBounds.getHeight();
 795     }
 796 
 797     /**
 798      *
 799      * Gets the root (non PopupWindow) Window for the provided window.
 800      *
 801      * @param win the Window for which to get the root window
 802      */
 803     private static Window getRootWindow(Window win) {
 804         // should be enough to traverse PopupWindow hierarchy here to get to the
 805         // first non-popup focusable window
 806         while (win instanceof PopupWindow) {
 807             win = ((PopupWindow) win).getOwnerWindow();
 808         }
 809         return win;
 810     }
 811 
 812     void doAutoHide() {
 813         // There is a timing problem here. I would like to have this isVisible
 814         // check, such that we don't send an onAutoHide event if it was already
 815         // invisible. However, visible is already false by the time this method
 816         // gets called, when done by certain code paths.
 817 //        if (isVisible()) {
 818         // hide this popup
 819         hide();
 820         if (getOnAutoHide() != null) {
 821             getOnAutoHide().handle(new Event(this, this, Event.ANY));
 822         }
 823 //        }
 824     }
 825 
 826     @Override
 827     WindowEventDispatcher createInternalEventDispatcher() {
 828         return new WindowEventDispatcher(new PopupEventRedirector(this),
 829                                          new WindowCloseRequestHandler(this),
 830                                          new EventHandlerManager(this));
 831 
 832     }
 833 
 834     @Override
 835     Window getWindowOwner() {
 836         return getOwnerWindow();
 837     }
 838 
 839     private void startMonitorOwnerEvents(final Window ownerWindowValue) {
 840         final EventRedirector parentEventRedirector =
 841                 ownerWindowValue.getInternalEventDispatcher()
 842                                 .getEventRedirector();
 843         parentEventRedirector.addEventDispatcher(getEventDispatcher());
 844     }
 845 
 846     private void stopMonitorOwnerEvents(final Window ownerWindowValue) {
 847         final EventRedirector parentEventRedirector =
 848                 ownerWindowValue.getInternalEventDispatcher()
 849                                 .getEventRedirector();
 850         parentEventRedirector.removeEventDispatcher(getEventDispatcher());
 851     }
 852 
 853     private ChangeListener<Boolean> ownerFocusedListener;
 854 
 855     private void bindOwnerFocusedProperty(final Window ownerWindowValue) {
 856         ownerFocusedListener =
 857             new ChangeListener<Boolean>() {
 858                 @Override
 859                 public void changed(
 860                         ObservableValue<? extends Boolean> observable,
 861                         Boolean oldValue, Boolean newValue) {
 862                     setFocused(newValue);
 863                 }
 864             };
 865         ownerWindowValue.focusedProperty().addListener(ownerFocusedListener);
 866     }
 867 
 868     private void unbindOwnerFocusedProperty(final Window ownerWindowValue) {
 869         ownerWindowValue.focusedProperty().removeListener(ownerFocusedListener);
 870         ownerFocusedListener = null;
 871     }
 872 
 873     private boolean autofixActive;
 874     private void handleAutofixActivation(final boolean visible,
 875                                          final boolean autofix) {
 876         final boolean newAutofixActive = visible && autofix;
 877         if (autofixActive != newAutofixActive) {
 878             autofixActive = newAutofixActive;
 879             if (newAutofixActive) {
 880                 Screen.getScreens().addListener(popupWindowUpdater);
 881                 updateWindow(getAnchorX(), getAnchorY());
 882             } else {
 883                 Screen.getScreens().removeListener(popupWindowUpdater);
 884             }
 885         }
 886     }
 887 
 888     private boolean autohideActive;
 889     private void handleAutohideActivation(final boolean visible,
 890                                           final boolean autohide) {
 891         final boolean newAutohideActive = visible && autohide;
 892         if (autohideActive != newAutohideActive) {
 893             // assert rootWindow != null;
 894             autohideActive = newAutohideActive;
 895             if (newAutohideActive) {
 896                 rootWindow.increaseFocusGrabCounter();
 897             } else {
 898                 rootWindow.decreaseFocusGrabCounter();
 899             }
 900         }
 901     }
 902 
 903     private void validateOwnerWindow(final Window owner) {
 904         if (owner == null) {
 905             throw new NullPointerException("Owner window must not be null");
 906         }
 907 
 908         if (wouldCreateCycle(owner, this)) {
 909             throw new IllegalArgumentException(
 910                     "Specified owner window would create cycle"
 911                         + " in the window hierarchy");
 912         }
 913 
 914         if (isShowing() && (getOwnerWindow() != owner)) {
 915             throw new IllegalStateException(
 916                     "Popup is already shown with different owner window");
 917         }
 918     }
 919 
 920     private static boolean wouldCreateCycle(Window parent, final Window child) {
 921        while (parent != null) {
 922            if (parent == child) {
 923                return true;
 924            }
 925 
 926            parent = parent.getWindowOwner();
 927        }
 928 
 929        return false;
 930     }
 931 
 932     static class PopupEventRedirector extends EventRedirector {
 933 
 934         private static final KeyCombination ESCAPE_KEY_COMBINATION =
 935                 KeyCombination.keyCombination("Esc");
 936         private final PopupWindow popupWindow;
 937 
 938         public PopupEventRedirector(final PopupWindow popupWindow) {
 939             super(popupWindow);
 940             this.popupWindow = popupWindow;
 941         }
 942 
 943         @Override
 944         protected void handleRedirectedEvent(final Object eventSource,
 945                 final Event event) {
 946             if (event instanceof KeyEvent) {
 947                 handleKeyEvent((KeyEvent) event);
 948                 return;
 949             }
 950 
 951             final EventType<?> eventType = event.getEventType();
 952 
 953             if (eventType == MouseEvent.MOUSE_PRESSED) {
 954                 handleMousePressedEvent(eventSource, event);
 955                 return;
 956             }
 957 
 958             if (eventType == FocusUngrabEvent.FOCUS_UNGRAB) {
 959                 handleFocusUngrabEvent();
 960                 return;
 961             }
 962         }
 963 
 964         private void handleKeyEvent(final KeyEvent event) {
 965             if (event.isConsumed()) {
 966                 return;
 967             }
 968 
 969             final Scene scene = popupWindow.getScene();
 970             if (scene != null) {
 971                 final Node sceneFocusOwner = scene.getFocusOwner();
 972                 final EventTarget eventTarget =
 973                         (sceneFocusOwner != null) ? sceneFocusOwner : scene;
 974                 if (EventUtil.fireEvent(eventTarget, new DirectEvent(event))
 975                         == null) {
 976                     event.consume();
 977                     return;
 978                 }
 979             }
 980 
 981             if ((event.getEventType() == KeyEvent.KEY_PRESSED)
 982                     && ESCAPE_KEY_COMBINATION.match(event)) {
 983                 handleEscapeKeyPressedEvent(event);
 984             }
 985         }
 986 
 987         private void handleEscapeKeyPressedEvent(final Event event) {
 988             if (popupWindow.isHideOnEscape()) {
 989                 popupWindow.doAutoHide();
 990 
 991                 if (popupWindow.getConsumeAutoHidingEvents()) {
 992                     event.consume();
 993                 }
 994             }
 995         }
 996 
 997         private void handleMousePressedEvent(final Object eventSource,
 998                 final Event event) {
 999             // we handle mouse pressed only for the immediate parent window,
1000             // where we can check whether the mouse press is inside of the owner
1001             // control or not, we will force possible child popups to close
1002             // by sending the FOCUS_UNGRAB event
1003             if (popupWindow.getOwnerWindow() != eventSource) {
1004                 return;
1005             }
1006 
1007             if (popupWindow.isAutoHide() && !isOwnerNodeEvent(event)) {
1008                 // the mouse press is outside of the owner control,
1009                 // fire FOCUS_UNGRAB to child popups
1010                 Event.fireEvent(popupWindow, new FocusUngrabEvent());
1011 
1012                 popupWindow.doAutoHide();
1013 
1014                 if (popupWindow.getConsumeAutoHidingEvents()) {
1015                     event.consume();
1016                 }
1017             }
1018         }
1019 
1020         private void handleFocusUngrabEvent() {
1021             if (popupWindow.isAutoHide()) {
1022                 popupWindow.doAutoHide();
1023             }
1024         }
1025 
1026         private boolean isOwnerNodeEvent(final Event event) {
1027             final Node ownerNode = popupWindow.getOwnerNode();
1028             if (ownerNode == null) {
1029                 return false;
1030             }
1031 
1032             final EventTarget eventTarget = event.getTarget();
1033             if (!(eventTarget instanceof Node)) {
1034                 return false;
1035             }
1036 
1037             Node node = (Node) eventTarget;
1038             do {
1039                 if (node == ownerNode) {
1040                     return true;
1041                 }
1042                 node = node.getParent();
1043             } while (node != null);
1044 
1045             return false;
1046         }
1047     }
1048 }