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