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