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