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