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         if (sceneValue != null) {
 430             SceneHelper.parentEffectiveOrientationInvalidated(sceneValue);            
 431         }
 432         
 433         // RT-28447
 434         final Scene ownerScene = getRootWindow(owner).getScene();
 435         if (ownerScene != null) {
 436             sceneValue.getStylesheets().setAll(ownerScene.getStylesheets());
 437         }
 438 
 439         // It is required that the root window exist and be visible to show the popup.
 440         if (getRootWindow(owner).isShowing()) {
 441             // We do show() first so that the width and height of the
 442             // popup window are initialized. This way the x,y location of the
 443             // popup calculated below uses the right width and height values for
 444             // its calculation. (fix for part of RT-10675).
 445             show();
 446         }
 447     }
 448 
 449     /**
 450      * Hide this Popup and all its children
 451      */
 452     @Override public void hide() {
 453         for (PopupWindow c : children) {
 454             if (c.isShowing()) {
 455                 c.hide();
 456             }
 457         }        
 458         children.clear();
 459         super.hide();
 460         // RT-28454 when popup hides, remove listeners; these are added when the popup shows.
 461         if (getOwnerWindow() != null) getOwnerWindow().showingProperty().removeListener(weakOwnerNodeListener);
 462         if (getOwnerNode() != null) getOwnerNode().visibleProperty().removeListener(weakOwnerNodeListener);
 463     }
 464 
 465     /**
 466      * @treatAsPrivate implementation detail
 467      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 468      */
 469     @Deprecated
 470     @Override protected void impl_visibleChanging(boolean visible) {
 471         super.impl_visibleChanging(visible);
 472         PerformanceTracker.logEvent("PopupWindow.storeVisible for [PopupWindow]");
 473 
 474         Toolkit toolkit = Toolkit.getToolkit();
 475         if (visible && (impl_peer == null)) {
 476             // Setup the peer
 477             impl_peer = toolkit.createTKPopupStage(this, getOwnerWindow().impl_getPeer(), acc);
 478             peerListener = new PopupWindowPeerListener(PopupWindow.this);
 479         }
 480     }
 481 
 482     private Window rootWindow;
 483 
 484     /**
 485      * @treatAsPrivate implementation detail
 486      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 487      */
 488     @Deprecated
 489     @Override protected void impl_visibleChanged(boolean visible) {
 490         super.impl_visibleChanged(visible);
 491 
 492         final Window ownerWindowValue = getOwnerWindow();
 493         if (visible) {
 494             rootWindow = getRootWindow(ownerWindowValue);
 495 
 496             startMonitorOwnerEvents(ownerWindowValue);
 497             // currently we consider popup window to be focused when it is
 498             // visible and its owner window is focused (we need to track
 499             // that through listener on owner window focused property)
 500             // a better solution would require some focus manager, which can
 501             // track focus state across multiple windows
 502             bindOwnerFocusedProperty(ownerWindowValue);
 503             setFocused(ownerWindowValue.isFocused());
 504             handleAutofixActivation(true, isAutoFix());
 505             handleAutohideActivation(true, isAutoHide());
 506         } else {
 507             stopMonitorOwnerEvents(ownerWindowValue);
 508             unbindOwnerFocusedProperty(ownerWindowValue);
 509             setFocused(false);
 510             handleAutofixActivation(false, isAutoFix());
 511             handleAutohideActivation(false, isAutoHide());
 512             rootWindow = null;
 513         }
 514 
 515         PerformanceTracker.logEvent("PopupWindow.storeVisible for [PopupWindow] finished");
 516     }
 517 
 518     /**
 519      * Specifies the x coordinate of the popup anchor point on the screen. If
 520      * the {@code anchorLocation} is set to {@code WINDOW_TOP_LEFT} or
 521      * {@code WINDOW_BOTTOM_LEFT} the {@code x} and {@code anchorX} values will
 522      * be identical.
 523      *
 524      * @since JavaFX 8.0
 525      */
 526     private final ReadOnlyDoubleWrapper anchorX =
 527             new ReadOnlyDoubleWrapper(this, "anchorX", Double.NaN);
 528 
 529     public final void setAnchorX(final double value) {
 530         updateWindow(value, getAnchorY());
 531     }
 532     public final double getAnchorX() {
 533         return anchorX.get();
 534     }
 535     public final ReadOnlyDoubleProperty anchorXProperty() {
 536         return anchorX.getReadOnlyProperty();
 537     }
 538 
 539     /**
 540      * Specifies the y coordinate of the popup anchor point on the screen. If
 541      * the {@code anchorLocation} is set to {@code WINDOW_TOP_LEFT} or
 542      * {@code WINDOW_TOP_RIGHT} the {@code y} and {@code anchorY} values will
 543      * be identical.
 544      *
 545      * @since JavaFX 8.0
 546      */
 547     private final ReadOnlyDoubleWrapper anchorY =
 548             new ReadOnlyDoubleWrapper(this, "anchorY", Double.NaN);
 549 
 550     public final void setAnchorY(final double value) {
 551         updateWindow(getAnchorX(), value);
 552     }
 553     public final double getAnchorY() {
 554         return anchorY.get();
 555     }
 556     public final ReadOnlyDoubleProperty anchorYProperty() {
 557         return anchorY.getReadOnlyProperty();
 558     }
 559 
 560     /**
 561      * Specifies the popup anchor point which is used in popup positioning. The
 562      * point can be set to a corner of the popup window or a corner of its
 563      * content. In this context the content corners are derived from the popup
 564      * root node's layout bounds.
 565      * <p>
 566      * In general changing of the anchor location won't change the current
 567      * window position. Instead of that, the {@code anchorX} and {@code anchorY}
 568      * values are recalculated to correspond to the new anchor point.
 569      * </p>
 570      * @since JavaFX 8.0
 571      */
 572     private final ObjectProperty<AnchorLocation> anchorLocation =
 573             new ObjectPropertyBase<AnchorLocation>(
 574                     AnchorLocation.WINDOW_TOP_LEFT) {
 575                 @Override
 576                 protected void invalidated() {
 577                     cachedAnchorBounds = null;
 578                     updateWindow(windowToAnchorX(getX()),
 579                                  windowToAnchorY(getY()));
 580                 }
 581 
 582                 @Override
 583                 public Object getBean() {
 584                     return PopupWindow.this;
 585                 }
 586 
 587                 @Override
 588                 public String getName() {
 589                     return "anchorLocation";
 590                 }
 591             };
 592     public final void setAnchorLocation(final AnchorLocation value) {
 593         anchorLocation.set(value);
 594     }
 595     public final AnchorLocation getAnchorLocation() {
 596         return anchorLocation.get();
 597     }
 598     public final ObjectProperty<AnchorLocation> anchorLocationProperty() {
 599         return anchorLocation;
 600     }
 601 
 602     /**
 603      * Anchor location constants for popup anchor point selection.
 604      *
 605      * @since JavaFX 8.0
 606      */
 607     public enum AnchorLocation {
 608         /** Represents top left window corner. */
 609         WINDOW_TOP_LEFT(0, 0, false),
 610         /** Represents top right window corner. */
 611         WINDOW_TOP_RIGHT(1, 0, false),
 612         /** Represents bottom left window corner. */
 613         WINDOW_BOTTOM_LEFT(0, 1, false),
 614         /** Represents bottom right window corner. */
 615         WINDOW_BOTTOM_RIGHT(1, 1, false),
 616         /** Represents top left content corner. */
 617         CONTENT_TOP_LEFT(0, 0, true),
 618         /** Represents top right content corner. */
 619         CONTENT_TOP_RIGHT(1, 0, true),
 620         /** Represents bottom left content corner. */
 621         CONTENT_BOTTOM_LEFT(0, 1, true),
 622         /** Represents bottom right content corner. */
 623         CONTENT_BOTTOM_RIGHT(1, 1, true);
 624 
 625         private final double xCoef;
 626         private final double yCoef;
 627         private final boolean contentLocation;
 628 
 629         private AnchorLocation(final double xCoef, final double yCoef,
 630                                final boolean contentLocation) {
 631             this.xCoef = xCoef;
 632             this.yCoef = yCoef;
 633             this.contentLocation = contentLocation;
 634         }
 635 
 636         double getXCoef() {
 637             return xCoef;
 638         }
 639 
 640         double getYCoef() {
 641             return yCoef;
 642         }
 643 
 644         boolean isContentLocation() {
 645             return contentLocation;
 646         }
 647     };
 648 
 649     @Override
 650     void setXInternal(final double value) {
 651         updateWindow(windowToAnchorX(value), getAnchorY());
 652     }
 653 
 654     @Override
 655     void setYInternal(final double value) {
 656         updateWindow(getAnchorX(), windowToAnchorY(value));
 657     }
 658 
 659     @Override
 660     void notifyLocationChanged(final double newX, final double newY) {
 661         super.notifyLocationChanged(newX, newY);
 662         anchorX.set(windowToAnchorX(newX));
 663         anchorY.set(windowToAnchorY(newY));
 664     }
 665 
 666     private Bounds cachedExtendedBounds;
 667     private Bounds cachedAnchorBounds;
 668 
 669     private Bounds getExtendedBounds() {
 670         if (cachedExtendedBounds == null) {
 671             final Parent rootNode = getScene().getRoot();
 672             cachedExtendedBounds = union(rootNode.getLayoutBounds(),
 673                                          rootNode.getBoundsInLocal());
 674         }
 675 
 676         return cachedExtendedBounds;
 677     }
 678 
 679     private Bounds getAnchorBounds() {
 680         if (cachedAnchorBounds == null) {
 681             cachedAnchorBounds = getAnchorLocation().isContentLocation()
 682                                          ? getScene().getRoot()
 683                                                      .getLayoutBounds()
 684                                          : getExtendedBounds();
 685         }
 686 
 687         return cachedAnchorBounds;
 688     }
 689 
 690     private void updateWindow(final double newAnchorX,
 691                               final double newAnchorY) {
 692         final AnchorLocation anchorLocationValue = getAnchorLocation();
 693         final Parent rootNode = getScene().getRoot();
 694         final Bounds extendedBounds = getExtendedBounds();
 695         final Bounds anchorBounds = getAnchorBounds();
 696 
 697         final double anchorXCoef = anchorLocationValue.getXCoef();
 698         final double anchorYCoef = anchorLocationValue.getYCoef();
 699         final double anchorDeltaX = anchorXCoef * anchorBounds.getWidth();
 700         final double anchorDeltaY = anchorYCoef * anchorBounds.getHeight();
 701         double anchorScrMinX = newAnchorX - anchorDeltaX;
 702         double anchorScrMinY = newAnchorY - anchorDeltaY;
 703 
 704         if (autofixActive) {
 705             final Screen currentScreen =
 706                     Utils.getScreenForPoint(newAnchorX, newAnchorY);
 707             final Rectangle2D screenBounds =
 708                     Utils.hasFullScreenStage(currentScreen)
 709                             ? currentScreen.getBounds()
 710                             : currentScreen.getVisualBounds();
 711 
 712             if (anchorXCoef <= 0.5) {
 713                 // left side of the popup is more important, try to keep it
 714                 // visible if the popup width is larger than screen width
 715                 anchorScrMinX = Math.min(anchorScrMinX,
 716                                          screenBounds.getMaxX()
 717                                              - anchorBounds.getWidth());
 718                 anchorScrMinX = Math.max(anchorScrMinX, screenBounds.getMinX());
 719             } else {
 720                 // right side of the popup is more important
 721                 anchorScrMinX = Math.max(anchorScrMinX, screenBounds.getMinX());
 722                 anchorScrMinX = Math.min(anchorScrMinX,
 723                                          screenBounds.getMaxX()
 724                                              - anchorBounds.getWidth());
 725             }
 726 
 727             if (anchorYCoef <= 0.5) {
 728                 // top side of the popup is more important
 729                 anchorScrMinY = Math.min(anchorScrMinY,
 730                                          screenBounds.getMaxY()
 731                                              - anchorBounds.getHeight());
 732                 anchorScrMinY = Math.max(anchorScrMinY, screenBounds.getMinY());
 733             } else {
 734                 // bottom side of the popup is more important
 735                 anchorScrMinY = Math.max(anchorScrMinY, screenBounds.getMinY());
 736                 anchorScrMinY = Math.min(anchorScrMinY,
 737                                          screenBounds.getMaxY()
 738                                              - anchorBounds.getHeight());
 739             }
 740         }
 741 
 742         final double windowScrMinX =
 743                 anchorScrMinX - anchorBounds.getMinX()
 744                               + extendedBounds.getMinX();
 745         final double windowScrMinY =
 746                 anchorScrMinY - anchorBounds.getMinY()
 747                               + extendedBounds.getMinY();
 748 
 749         // update popup dimensions
 750         setWidth(extendedBounds.getWidth());
 751         setHeight(extendedBounds.getHeight());
 752         // update transform
 753         rootNode.setTranslateX(-extendedBounds.getMinX());
 754         rootNode.setTranslateY(-extendedBounds.getMinY());
 755 
 756         // update popup position
 757         // don't set Window.xExplicit unnecessarily
 758         if (!Double.isNaN(windowScrMinX) || !Double.isNaN(getX())) {
 759             super.setXInternal(windowScrMinX);
 760         }
 761         // don't set Window.yExplicit unnecessarily
 762         if (!Double.isNaN(windowScrMinY) || !Double.isNaN(getY())) {
 763             super.setYInternal(windowScrMinY);
 764         }
 765 
 766         // set anchor x, anchor y
 767         anchorX.set(anchorScrMinX + anchorDeltaX);
 768         anchorY.set(anchorScrMinY + anchorDeltaY);
 769     }
 770 
 771     private Bounds union(final Bounds bounds1, final Bounds bounds2) {
 772         final double minX = Math.min(bounds1.getMinX(), bounds2.getMinX());
 773         final double minY = Math.min(bounds1.getMinY(), bounds2.getMinY());
 774         final double maxX = Math.max(bounds1.getMaxX(), bounds2.getMaxX());
 775         final double maxY = Math.max(bounds1.getMaxY(), bounds2.getMaxY());
 776 
 777         return new BoundingBox(minX, minY, maxX - minX, maxY - minY);
 778     }
 779 
 780     private double windowToAnchorX(final double windowX) {
 781         final Bounds anchorBounds = getAnchorBounds();
 782         return windowX - getExtendedBounds().getMinX()
 783                        + anchorBounds.getMinX()
 784                        + getAnchorLocation().getXCoef()
 785                              * anchorBounds.getWidth();
 786     }
 787 
 788     private double windowToAnchorY(final double windowY) {
 789         final Bounds anchorBounds = getAnchorBounds();
 790         return windowY - getExtendedBounds().getMinY()
 791                        + anchorBounds.getMinY()
 792                        + getAnchorLocation().getYCoef()
 793                              * anchorBounds.getHeight();
 794     }
 795 
 796     /**
 797      *
 798      * Gets the root (non PopupWindow) Window for the provided window.
 799      *
 800      * @param win the Window for which to get the root window
 801      */
 802     private static Window getRootWindow(Window win) {
 803         // should be enough to traverse PopupWindow hierarchy here to get to the
 804         // first non-popup focusable window
 805         while (win instanceof PopupWindow) {
 806             win = ((PopupWindow) win).getOwnerWindow();
 807         }
 808         return win;
 809     }
 810 
 811     void doAutoHide() {
 812         // There is a timing problem here. I would like to have this isVisible
 813         // check, such that we don't send an onAutoHide event if it was already
 814         // invisible. However, visible is already false by the time this method
 815         // gets called, when done by certain code paths.
 816 //        if (isVisible()) {
 817         // hide this popup
 818         hide();
 819         if (getOnAutoHide() != null) {
 820             getOnAutoHide().handle(new Event(this, this, Event.ANY));
 821         }
 822 //        }
 823     }
 824 
 825     @Override
 826     WindowEventDispatcher createInternalEventDispatcher() {
 827         return new WindowEventDispatcher(new PopupEventRedirector(this),
 828                                          new WindowCloseRequestHandler(this),
 829                                          new EventHandlerManager(this));
 830 
 831     }
 832 
 833     @Override
 834     Window getWindowOwner() {
 835         return getOwnerWindow();
 836     }
 837 
 838     private void startMonitorOwnerEvents(final Window ownerWindowValue) {
 839         final EventRedirector parentEventRedirector =
 840                 ownerWindowValue.getInternalEventDispatcher()
 841                                 .getEventRedirector();
 842         parentEventRedirector.addEventDispatcher(getEventDispatcher());
 843     }
 844 
 845     private void stopMonitorOwnerEvents(final Window ownerWindowValue) {
 846         final EventRedirector parentEventRedirector =
 847                 ownerWindowValue.getInternalEventDispatcher()
 848                                 .getEventRedirector();
 849         parentEventRedirector.removeEventDispatcher(getEventDispatcher());
 850     }
 851 
 852     private ChangeListener<Boolean> ownerFocusedListener;
 853 
 854     private void bindOwnerFocusedProperty(final Window ownerWindowValue) {
 855         ownerFocusedListener =
 856             new ChangeListener<Boolean>() {
 857                 @Override
 858                 public void changed(
 859                         ObservableValue<? extends Boolean> observable,
 860                         Boolean oldValue, Boolean newValue) {
 861                     setFocused(newValue);
 862                 }
 863             };
 864         ownerWindowValue.focusedProperty().addListener(ownerFocusedListener);
 865     }
 866 
 867     private void unbindOwnerFocusedProperty(final Window ownerWindowValue) {
 868         ownerWindowValue.focusedProperty().removeListener(ownerFocusedListener);
 869         ownerFocusedListener = null;
 870     }
 871 
 872     private boolean autofixActive;
 873     private void handleAutofixActivation(final boolean visible,
 874                                          final boolean autofix) {
 875         final boolean newAutofixActive = visible && autofix;
 876         if (autofixActive != newAutofixActive) {
 877             autofixActive = newAutofixActive;
 878             if (newAutofixActive) {
 879                 Screen.getScreens().addListener(popupWindowUpdater);
 880                 updateWindow(getAnchorX(), getAnchorY());
 881             } else {
 882                 Screen.getScreens().removeListener(popupWindowUpdater);
 883             }
 884         }
 885     }
 886 
 887     private boolean autohideActive;
 888     private void handleAutohideActivation(final boolean visible,
 889                                           final boolean autohide) {
 890         final boolean newAutohideActive = visible && autohide;
 891         if (autohideActive != newAutohideActive) {
 892             // assert rootWindow != null;
 893             autohideActive = newAutohideActive;
 894             if (newAutohideActive) {
 895                 rootWindow.increaseFocusGrabCounter();
 896             } else {
 897                 rootWindow.decreaseFocusGrabCounter();
 898             }
 899         }
 900     }
 901 
 902     private void validateOwnerWindow(final Window owner) {
 903         if (owner == null) {
 904             throw new NullPointerException("Owner window must not be null");
 905         }
 906 
 907         if (wouldCreateCycle(owner, this)) {
 908             throw new IllegalArgumentException(
 909                     "Specified owner window would create cycle"
 910                         + " in the window hierarchy");
 911         }
 912 
 913         if (isShowing() && (getOwnerWindow() != owner)) {
 914             throw new IllegalStateException(
 915                     "Popup is already shown with different owner window");
 916         }
 917     }
 918 
 919     private static boolean wouldCreateCycle(Window parent, final Window child) {
 920        while (parent != null) {
 921            if (parent == child) {
 922                return true;
 923            }
 924 
 925            parent = parent.getWindowOwner();
 926        }
 927 
 928        return false;
 929     }
 930 
 931     static class PopupEventRedirector extends EventRedirector {
 932 
 933         private static final KeyCombination ESCAPE_KEY_COMBINATION =
 934                 KeyCombination.keyCombination("Esc");
 935         private final PopupWindow popupWindow;
 936 
 937         public PopupEventRedirector(final PopupWindow popupWindow) {
 938             super(popupWindow);
 939             this.popupWindow = popupWindow;
 940         }
 941 
 942         @Override
 943         protected void handleRedirectedEvent(final Object eventSource,
 944                 final Event event) {
 945             if (event instanceof KeyEvent) {
 946                 handleKeyEvent((KeyEvent) event);
 947                 return;
 948             }
 949 
 950             final EventType<?> eventType = event.getEventType();
 951 
 952             if (eventType == MouseEvent.MOUSE_PRESSED) {
 953                 handleMousePressedEvent(eventSource, event);
 954                 return;
 955             }
 956 
 957             if (eventType == FocusUngrabEvent.FOCUS_UNGRAB) {
 958                 handleFocusUngrabEvent();
 959                 return;
 960             }
 961         }
 962 
 963         private void handleKeyEvent(final KeyEvent event) {
 964             if (event.isConsumed()) {
 965                 return;
 966             }
 967 
 968             final Scene scene = popupWindow.getScene();
 969             if (scene != null) {
 970                 final Node sceneFocusOwner = scene.getFocusOwner();
 971                 final EventTarget eventTarget =
 972                         (sceneFocusOwner != null) ? sceneFocusOwner : scene;
 973                 if (EventUtil.fireEvent(eventTarget, new DirectEvent(event))
 974                         == null) {
 975                     event.consume();
 976                     return;
 977                 }
 978             }
 979 
 980             if ((event.getEventType() == KeyEvent.KEY_PRESSED)
 981                     && ESCAPE_KEY_COMBINATION.match(event)) {
 982                 handleEscapeKeyPressedEvent(event);
 983             }
 984         }
 985 
 986         private void handleEscapeKeyPressedEvent(final Event event) {
 987             if (popupWindow.isHideOnEscape()) {
 988                 popupWindow.doAutoHide();
 989 
 990                 if (popupWindow.getConsumeAutoHidingEvents()) {
 991                     event.consume();
 992                 }
 993             }
 994         }
 995 
 996         private void handleMousePressedEvent(final Object eventSource,
 997                 final Event event) {
 998             // we handle mouse pressed only for the immediate parent window,
 999             // where we can check whether the mouse press is inside of the owner
1000             // control or not, we will force possible child popups to close
1001             // by sending the FOCUS_UNGRAB event
1002             if (popupWindow.getOwnerWindow() != eventSource) {
1003                 return;
1004             }
1005 
1006             if (popupWindow.isAutoHide() && !isOwnerNodeEvent(event)) {
1007                 // the mouse press is outside of the owner control,
1008                 // fire FOCUS_UNGRAB to child popups
1009                 Event.fireEvent(popupWindow, new FocusUngrabEvent());
1010 
1011                 popupWindow.doAutoHide();
1012 
1013                 if (popupWindow.getConsumeAutoHidingEvents()) {
1014                     event.consume();
1015                 }
1016             }
1017         }
1018 
1019         private void handleFocusUngrabEvent() {
1020             if (popupWindow.isAutoHide()) {
1021                 popupWindow.doAutoHide();
1022             }
1023         }
1024 
1025         private boolean isOwnerNodeEvent(final Event event) {
1026             final Node ownerNode = popupWindow.getOwnerNode();
1027             if (ownerNode == null) {
1028                 return false;
1029             }
1030 
1031             final EventTarget eventTarget = event.getTarget();
1032             if (!(eventTarget instanceof Node)) {
1033                 return false;
1034             }
1035 
1036             Node node = (Node) eventTarget;
1037             do {
1038                 if (node == ownerNode) {
1039                     return true;
1040                 }
1041                 node = node.getParent();
1042             } while (node != null);
1043 
1044             return false;
1045         }
1046     }
1047 }