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