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 }