1 /*
   2  * Copyright (c) 2014, 2019, 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 package javafx.scene.control;
  26 
  27 import java.lang.ref.WeakReference;
  28 import java.util.Optional;
  29 
  30 import javafx.beans.InvalidationListener;
  31 import javafx.beans.property.BooleanProperty;
  32 import javafx.beans.property.ObjectProperty;
  33 import javafx.beans.property.ReadOnlyBooleanProperty;
  34 import javafx.beans.property.ReadOnlyDoubleProperty;
  35 import javafx.beans.property.SimpleObjectProperty;
  36 import javafx.beans.property.StringProperty;
  37 import javafx.collections.ListChangeListener;
  38 import javafx.css.PseudoClass;
  39 import javafx.event.Event;
  40 import javafx.event.EventDispatchChain;
  41 import javafx.event.EventHandler;
  42 import javafx.event.EventTarget;
  43 import javafx.scene.Node;
  44 import javafx.scene.control.ButtonBar.ButtonData;
  45 import javafx.stage.Modality;
  46 import javafx.stage.Stage;
  47 import javafx.stage.StageStyle;
  48 import javafx.stage.Window;
  49 import javafx.util.Callback;
  50 
  51 import com.sun.javafx.event.EventHandlerManager;
  52 import com.sun.javafx.tk.Toolkit;
  53 
  54 /**
  55  * A Dialog in JavaFX wraps a {@link DialogPane} and provides the necessary API
  56  * to present it to end users. In JavaFX 8u40, this essentially means that the
  57  * {@link DialogPane} is shown to users inside a {@link Stage}, but future releases
  58  * may offer alternative options (such as 'lightweight' or 'internal' dialogs).
  59  * This API therefore is intentionally ignorant of the underlying implementation,
  60  * and attempts to present a common API for all possible implementations.
  61  *
  62  * <p>The Dialog class has a single generic type, R, which is used to represent
  63  * the type of the {@link #resultProperty() result} property (and also, how to
  64  * convert from {@link ButtonType} to R, through the use of the
  65  * {@link #resultConverterProperty() result converter} {@link Callback}).
  66  *
  67  * <p><strong>Critical note:</strong> It is critical that all developers who choose
  68  * to create their own dialogs by extending the Dialog class understand the
  69  * importance of the {@link #resultConverterProperty() result converter} property.
  70  * A result converter must always be set, whenever the R type is not
  71  * {@link Void} or {@link ButtonType}. If this is not heeded, developers will find
  72  * that they get ClassCastExceptions in their code, for failure to convert from
  73  * {@link ButtonType} via the {@link #resultConverterProperty() result converter}.
  74  *
  75  * <p>It is likely that most developers would be better served using either the
  76  * {@link Alert} class (for pre-defined, notification-style alerts), or either of
  77  * the two pre-built dialogs ({@link TextInputDialog} and {@link ChoiceDialog}),
  78  * depending on their needs.
  79  *
  80  * <p>Once a Dialog is instantiated, the next step is to configure it. Almost
  81  * all properties on Dialog are not related to the content of the Dialog, the
  82  * only exceptions are {@link #contentTextProperty()},
  83  * {@link #headerTextProperty()}, and {@link #graphicProperty()}, and these
  84  * properties are simply forwarding API onto the respective properties on the
  85  * {@link DialogPane} stored in the {@link #dialogPaneProperty() dialog pane}
  86  * property. These three properties are forwarded from DialogPane for developer
  87  * convenience. For developers wanting to configure their dialog, they will in many
  88  * cases be required to use code along the lines of
  89  * {@code dialog.getDialogPane().setExpandableContent(node)}.
  90  *
  91  * <p>After configuring these properties, all that remains is to consider whether
  92  * the buttons (created using {@link ButtonType} and the
  93  * {@link DialogPane#createButton(ButtonType)} method) are fully configured.
  94  * Developers will quickly find that the amount of configurability offered
  95  * via the {@link ButtonType} class is minimal. This is intentional, but does not
  96  * mean that developers can not modify the buttons created by the {@link ButtonType}
  97  * that have been specified. To do this, developers simply call the
  98  * {@link DialogPane#lookupButton(ButtonType)} method with the ButtonType
  99  * (assuming it has already been set in the {@link DialogPane#getButtonTypes()}
 100  * list. The returned Node is typically of type {@link Button}, but this depends
 101  * on if the {@link DialogPane#createButton(ButtonType)} method has been overridden. A
 102  * typical approach is therefore along the following lines:
 103  *
 104  * <pre> {@code ButtonType loginButtonType = new ButtonType("Login", ButtonData.OK_DONE);
 105  * Dialog<String> dialog = new Dialog<>();
 106  * dialog.setTitle("Login Dialog");
 107  * dialog.setContentText("Would you like to log in?");
 108  * dialog.getDialogPane().getButtonTypes().add(loginButtonType);
 109  * boolean disabled = false; // computed based on content of text fields, for example
 110  * dialog.getDialogPane().lookupButton(loginButtonType).setDisable(disabled);
 111  * dialog.showAndWait();}</pre>
 112  *
 113  * <img src="doc-files/Dialog.png" alt="Image of the Dialog control">
 114  *
 115  * <p>Once a Dialog is instantiated and fully configured, the next step is to
 116  * show it. More often than not, dialogs are shown in a modal and blocking
 117  * fashion. 'Modal' means that the dialog prevents user interaction with the
 118  * owning application whilst it is showing, and 'blocking' means that code
 119  * execution stops at the point in which the dialog is shown. This means that
 120  * you can show a dialog, await the user response, and then continue running the
 121  * code that directly follows the show call, giving developers the ability to
 122  * immediately deal with the user input from the dialog (if relevant).
 123  *
 124  * <p>JavaFX dialogs are modal by default (you can change this via the
 125  * {@link #initModality(javafx.stage.Modality)} API). To specify whether you want
 126  * blocking or non-blocking dialogs, developers simply choose to call
 127  * {@link #showAndWait()} or {@link #show()} (respectively). By default most
 128  * developers should choose to use {@link #showAndWait()}, given the ease of
 129  * coding in these situations. Shown below is three code snippets, showing three
 130  * equally valid ways of showing a dialog:
 131  *
 132  * <p><strong>Option 1: The 'traditional' approach</strong>
 133  * <pre> {@code Optional<ButtonType> result = dialog.showAndWait();
 134  * if (result.isPresent() && result.get() == ButtonType.OK) {
 135  *     formatSystem();
 136  * }}</pre>
 137  *
 138  * <p><strong>Option 2: The traditional + Optional approach</strong>
 139  * <pre> {@code dialog.showAndWait().ifPresent(response -> {
 140  *     if (response == ButtonType.OK) {
 141  *         formatSystem();
 142  *     }
 143  * });}</pre>
 144  *
 145  * <p><strong>Option 3: The fully lambda approach</strong>
 146  * <pre> {@code dialog.showAndWait()
 147  *       .filter(response -> response == ButtonType.OK)
 148  *       .ifPresent(response -> formatSystem());}</pre>
 149  *
 150  * <p>There is no better or worse option of the three listed above, so developers
 151  * are encouraged to work to their own style preferences. The purpose of showing
 152  * the above is to help introduce developers to the {@link Optional} API, which
 153  * is new in Java 8 and may be foreign to many developers.
 154  *
 155  * <h3>Dialog Validation / Intercepting Button Actions</h3>
 156  *
 157  * <p>In some circumstances it is desirable to prevent a dialog from closing
 158  * until some aspect of the dialog becomes internally consistent (e.g. a form
 159  * inside the dialog has all fields in a valid state). To do this, users of the
 160  * dialogs API should become familiar with the
 161  * {@link DialogPane#lookupButton(ButtonType)} method. By passing in a
 162  * {@link javafx.scene.control.ButtonType ButtonType} (that has already been set
 163  * in the {@link DialogPane#getButtonTypes() button types} list), users will be
 164  * returned a Node that is typically of type {@link Button} (but this depends
 165  * on if the {@link DialogPane#createButton(ButtonType)} method has been
 166  * overridden). With this button, users may add an event filter that is called
 167  * before the button does its usual event handling, and as such users may
 168  * prevent the event handling by {@code consuming} the event. Here's a simplified
 169  * example:
 170  *
 171  * <pre> {@code final Button btOk = (Button) dlg.getDialogPane().lookupButton(ButtonType.OK);
 172  * btOk.addEventFilter(ActionEvent.ACTION, event -> {
 173  *     if (!validateAndStore()) {
 174  *         event.consume();
 175  *     }
 176  * });}</pre>
 177  *
 178  * <h3>Dialog Closing Rules</h3>
 179  *
 180  * <p>It is important to understand what happens when a Dialog is closed, and
 181  * also how a Dialog can be closed, especially in abnormal closing situations
 182  * (such as when the 'X' button is clicked in a dialogs title bar, or when
 183  * operating system specific keyboard shortcuts (such as alt-F4 on Windows)
 184  * are entered). Fortunately, the outcome is well-defined in these situations,
 185  * and can be best summarised in the following bullet points:
 186  *
 187  * <ul>
 188  *   <li>JavaFX dialogs can only be closed 'abnormally' (as defined above) in
 189  *   two situations:
 190  *     <ol>
 191  *       <li>When the dialog only has one button, or
 192  *       <li>When the dialog has multiple buttons, as long as one of them meets
 193  *       one of the following requirements:
 194  *       <ol>
 195  *           <li>The button has a {@link ButtonType} whose {@link ButtonData} is of type
 196  *           {@link ButtonData#CANCEL_CLOSE}.</li>
 197  *           <li>The button has a {@link ButtonType} whose {@link ButtonData} returns true
 198  *           when {@link ButtonData#isCancelButton()} is called.</li>
 199  *       </ol>
 200  *     </ol>
 201  *   <li>In all other situations, the dialog will refuse to respond to all
 202  *   close requests, remaining open until the user clicks on one of the available
 203  *   buttons in the {@link DialogPane} area of the dialog.
 204  *   <li>If a dialog is closed abnormally, and if the dialog contains a button
 205  *   which meets one of the two criteria above, the dialog will attempt to set
 206  *   the {@link #resultProperty() result} property to whatever value is returned
 207  *   from calling the {@link #resultConverterProperty() result converter} with
 208  *   the first matching {@link ButtonType}.
 209  *   <li>If for any reason the result converter returns null, or if the dialog
 210  *   is closed when only one non-cancel button is present, the
 211  *   {@link #resultProperty() result} property will be null, and the
 212  *   {@link #showAndWait()} method will return {@link Optional#empty()}. This
 213  *   later point means that, if you use either of option 2 or option 3 (as
 214  *   presented earlier in this class documentation), the
 215  *   {@link Optional#ifPresent(java.util.function.Consumer)} lambda will never
 216  *   be called, and code will continue executing as if the dialog had not
 217  *   returned any value at all.
 218  * </ul>
 219  *
 220  * @param <R> The return type of the dialog, via the
 221  *            {@link #resultProperty() result} property.
 222  * @see Alert
 223  * @see TextInputDialog
 224  * @see ChoiceDialog
 225  * @since JavaFX 8u40
 226  */
 227 public class Dialog<R> implements EventTarget {
 228 
 229     /**************************************************************************
 230      *
 231      * Static fields
 232      *
 233      **************************************************************************/
 234 
 235 
 236 
 237 
 238     /**************************************************************************
 239      *
 240      * Static methods
 241      *
 242      **************************************************************************/
 243 
 244 
 245 
 246     /**************************************************************************
 247      *
 248      * Private fields
 249      *
 250      **************************************************************************/
 251 
 252      FXDialog dialog;
 253 
 254     private boolean isClosing;
 255 
 256 
 257 
 258     /**************************************************************************
 259      *
 260      * Constructors
 261      *
 262      **************************************************************************/
 263 
 264     /**
 265      * Creates a dialog without a specified owner.
 266      */
 267     public Dialog() {
 268         this.dialog = new HeavyweightDialog(this);
 269         setDialogPane(new DialogPane());
 270         initModality(Modality.APPLICATION_MODAL);
 271     }
 272 
 273 
 274 
 275     /**************************************************************************
 276      *
 277      * Abstract methods
 278      *
 279      **************************************************************************/
 280 
 281 
 282 
 283 
 284     /**************************************************************************
 285      *
 286      * Public API
 287      *
 288      **************************************************************************/
 289 
 290     /**
 291      * Shows the dialog but does not wait for a user response (in other words,
 292      * this brings up a non-blocking dialog). Users of this API must either
 293      * poll the {@link #resultProperty() result property}, or else add a listener
 294      * to the result property to be informed of when it is set.
 295      * @throws IllegalStateException if this method is called on a thread
 296      *     other than the JavaFX Application Thread.
 297      */
 298     public final void show() {
 299         Toolkit.getToolkit().checkFxUserThread();
 300 
 301         Event.fireEvent(this, new DialogEvent(this, DialogEvent.DIALOG_SHOWING));
 302         if (Double.isNaN(getWidth()) && Double.isNaN(getHeight())) {
 303             dialog.sizeToScene();
 304         }
 305 
 306         dialog.show();
 307 
 308         Event.fireEvent(this, new DialogEvent(this, DialogEvent.DIALOG_SHOWN));
 309     }
 310 
 311     /**
 312      * Shows the dialog and waits for the user response (in other words, brings
 313      * up a blocking dialog, with the returned value the users input).
 314      * <p>
 315      * This method must be called on the JavaFX Application thread.
 316      * Additionally, it must either be called from an input event handler or
 317      * from the run method of a Runnable passed to
 318      * {@link javafx.application.Platform#runLater Platform.runLater}.
 319      * It must not be called during animation or layout processing.
 320      * </p>
 321      *
 322      * @return An {@link Optional} that contains the {@link #resultProperty() result}.
 323      *         Refer to the {@link Dialog} class documentation for more detail.
 324      * @throws IllegalStateException if this method is called on a thread
 325      *     other than the JavaFX Application Thread.
 326      * @throws IllegalStateException if this method is called during
 327      *     animation or layout processing.
 328      */
 329     public final Optional<R> showAndWait() {
 330         Toolkit.getToolkit().checkFxUserThread();
 331 
 332         if (!Toolkit.getToolkit().canStartNestedEventLoop()) {
 333             throw new IllegalStateException("showAndWait is not allowed during animation or layout processing");
 334         }
 335 
 336         Event.fireEvent(this, new DialogEvent(this, DialogEvent.DIALOG_SHOWING));
 337         if (Double.isNaN(getWidth()) && Double.isNaN(getHeight())) {
 338             dialog.sizeToScene();
 339         }
 340 
 341 
 342         // this is slightly odd - we fire the SHOWN event before the show()
 343         // call, so that users get the event before the dialog blocks
 344         Event.fireEvent(this, new DialogEvent(this, DialogEvent.DIALOG_SHOWN));
 345 
 346         dialog.showAndWait();
 347 
 348         return Optional.ofNullable(getResult());
 349     }
 350 
 351     /**
 352      * Closes this {@code Dialog}.
 353      * This call is equivalent to {@link #hide}.
 354      */
 355     public final void close() {
 356         if (isClosing) return;
 357         isClosing = true;
 358 
 359         final R result = getResult();
 360 
 361         // if the result is null and we do not have permission to close the
 362         // dialog, then we cancel the close request before any events are
 363         // even fired
 364         if (result == null && ! dialog.requestPermissionToClose(this)) {
 365             isClosing = false;
 366             return;
 367         }
 368 
 369         // if we are here we have permission to close the dialog. However, we
 370         // may not have a result set to return to the user. Therefore, we need
 371         // to handle that before the dialog closes (especially in case the
 372         // dialog is blocking, in which case having a null result is really going
 373         // to mess up users).
 374         //
 375         // In cases where the result is null, and where the dialog has a cancel
 376         // button, we call into the result converter to see what to do. This is
 377         // used primarily to handle the requirement that the X button has the
 378         // same result as clicking the cancel button.
 379         //
 380         // A 'cancel button' can mean two different things (although they may
 381         // be the same thing):
 382         // 1) A button whose ButtonData is of type CANCEL_CLOSE.
 383         // 2) A button whose ButtonData returns true for isCancelButton().
 384         if (result == null) {
 385             ButtonType cancelButton = null;
 386 
 387             // we do two things here. We are primarily looking for a button with
 388             // ButtonData.CANCEL_CLOSE. If we find one, we use it as the result.
 389             // However, if we don't find one, we can also use any button that
 390             // is a cancel button.
 391             for (ButtonType button : getDialogPane().getButtonTypes()) {
 392                 ButtonData buttonData = button.getButtonData();
 393                 if (buttonData == null) continue;
 394 
 395                 if (buttonData == ButtonData.CANCEL_CLOSE) {
 396                     cancelButton = button;
 397                     break;
 398                 }
 399                 if (buttonData.isCancelButton()) {
 400                     cancelButton = button;
 401                 }
 402             }
 403 
 404             setResultAndClose(cancelButton, false);
 405         }
 406 
 407         // start normal closing process
 408         Event.fireEvent(this, new DialogEvent(this, DialogEvent.DIALOG_HIDING));
 409 
 410         DialogEvent closeRequestEvent = new DialogEvent(this, DialogEvent.DIALOG_CLOSE_REQUEST);
 411         Event.fireEvent(this, closeRequestEvent);
 412         if (closeRequestEvent.isConsumed()) {
 413             isClosing = false;
 414             return;
 415         }
 416 
 417         dialog.close();
 418 
 419         Event.fireEvent(this, new DialogEvent(this, DialogEvent.DIALOG_HIDDEN));
 420 
 421         isClosing = false;
 422     }
 423 
 424     /**
 425      * Hides this {@code Dialog}.
 426      */
 427     public final void hide() {
 428         close();
 429     }
 430 
 431     /**
 432      * Specifies the modality for this dialog. This must be done prior to making
 433      * the dialog visible. The modality is one of: Modality.NONE,
 434      * Modality.WINDOW_MODAL, or Modality.APPLICATION_MODAL.
 435      *
 436      * @param modality the modality for this dialog.
 437      *
 438      * @throws IllegalStateException if this property is set after the dialog
 439      * has ever been made visible.
 440      *
 441      * @defaultValue Modality.APPLICATION_MODAL
 442      */
 443     public final void initModality(Modality modality) {
 444         dialog.initModality(modality);
 445     }
 446 
 447     /**
 448      * Retrieves the modality attribute for this dialog.
 449      *
 450      * @return the modality.
 451      */
 452     public final Modality getModality() {
 453         return dialog.getModality();
 454     }
 455 
 456     /**
 457      * Specifies the style for this dialog. This must be done prior to making
 458      * the dialog visible. The style is one of: StageStyle.DECORATED,
 459      * StageStyle.UNDECORATED, StageStyle.TRANSPARENT, StageStyle.UTILITY,
 460      * or StageStyle.UNIFIED.
 461      *
 462      * @param style the style for this dialog.
 463      *
 464      * @throws IllegalStateException if this property is set after the dialog
 465      * has ever been made visible.
 466      *
 467      * @defaultValue StageStyle.DECORATED
 468      */
 469     public final void initStyle(StageStyle style) {
 470         dialog.initStyle(style);
 471     }
 472 
 473     /**
 474      * Specifies the owner {@link Window} for this dialog, or null for a top-level,
 475      * unowned dialog. This must be done prior to making the dialog visible.
 476      *
 477      * @param window the owner {@link Window} for this dialog.
 478      *
 479      * @throws IllegalStateException if this property is set after the dialog
 480      * has ever been made visible.
 481      *
 482      * @defaultValue null
 483      */
 484     public final void initOwner(Window window) {
 485         dialog.initOwner(window);
 486     }
 487 
 488     /**
 489      * Retrieves the owner Window for this dialog, or null for an unowned dialog.
 490      *
 491      * @return the owner Window.
 492      */
 493     public final Window getOwner() {
 494         return dialog.getOwner();
 495     }
 496 
 497 
 498 
 499     /**************************************************************************
 500      *
 501      * Properties
 502      *
 503      **************************************************************************/
 504 
 505     // --- dialog Pane
 506     /**
 507      * The root node of the dialog, the {@link DialogPane} contains all visual
 508      * elements shown in the dialog. As such, it is possible to completely adjust
 509      * the display of the dialog by modifying the existing dialog pane or creating
 510      * a new one.
 511      */
 512     private ObjectProperty<DialogPane> dialogPane = new SimpleObjectProperty<DialogPane>(this, "dialogPane", new DialogPane()) {
 513         final InvalidationListener expandedListener = o -> {
 514             DialogPane dialogPane = getDialogPane();
 515             if (dialogPane == null) return;
 516 
 517             final Node content = dialogPane.getExpandableContent();
 518             final boolean isExpanded = content == null ? false : content.isVisible();
 519             setResizable(isExpanded);
 520 
 521             Dialog.this.dialog.sizeToScene();
 522         };
 523 
 524         final InvalidationListener headerListener = o -> {
 525             updatePseudoClassState();
 526         };
 527 
 528         WeakReference<DialogPane> dialogPaneRef = new WeakReference<>(null);
 529 
 530         @Override
 531         protected void invalidated() {
 532             DialogPane oldDialogPane = dialogPaneRef.get();
 533             if (oldDialogPane != null) {
 534                 // clean up
 535                 oldDialogPane.expandedProperty().removeListener(expandedListener);
 536                 oldDialogPane.headerProperty().removeListener(headerListener);
 537                 oldDialogPane.headerTextProperty().removeListener(headerListener);
 538                 oldDialogPane.setDialog(null);
 539             }
 540 
 541             final DialogPane newDialogPane = getDialogPane();
 542 
 543             if (newDialogPane != null) {
 544                 newDialogPane.setDialog(Dialog.this);
 545 
 546                 // if the buttons change, we dynamically update the dialog
 547                 newDialogPane.getButtonTypes().addListener((ListChangeListener<ButtonType>) c -> {
 548                     newDialogPane.requestLayout();
 549                 });
 550                 newDialogPane.expandedProperty().addListener(expandedListener);
 551                 newDialogPane.headerProperty().addListener(headerListener);
 552                 newDialogPane.headerTextProperty().addListener(headerListener);
 553 
 554                 updatePseudoClassState();
 555                 newDialogPane.requestLayout();
 556             }
 557 
 558             // push the new dialog down into the implementation for rendering
 559             dialog.setDialogPane(newDialogPane);
 560 
 561             dialogPaneRef = new WeakReference<DialogPane>(newDialogPane);
 562         }
 563     };
 564 
 565     public final ObjectProperty<DialogPane> dialogPaneProperty() {
 566         return dialogPane;
 567     }
 568 
 569     public final DialogPane getDialogPane() {
 570         return dialogPane.get();
 571     }
 572 
 573     public final void setDialogPane(DialogPane value) {
 574         dialogPane.set(value);
 575     }
 576 
 577 
 578     // --- content text (forwarded from DialogPane)
 579     /**
 580      * A property representing the content text for the dialog pane. The content text
 581      * is lower precedence than the {@link DialogPane#contentProperty() content node}, meaning
 582      * that if both the content node and the contentText properties are set, the
 583      * content text will not be displayed in a default DialogPane instance.
 584      * @return the property representing the content text for the dialog pane
 585      */
 586     public final StringProperty contentTextProperty() {
 587         return getDialogPane().contentTextProperty();
 588     }
 589 
 590     /**
 591      * Returns the currently-set content text for this DialogPane.
 592      * @return the currently-set content text for this DialogPane
 593      */
 594     public final String getContentText() {
 595         return getDialogPane().getContentText();
 596     }
 597 
 598     /**
 599      * Sets the string to show in the dialog content area. Note that the content text
 600      * is lower precedence than the {@link DialogPane#contentProperty() content node}, meaning
 601      * that if both the content node and the contentText properties are set, the
 602      * content text will not be displayed in a default DialogPane instance.
 603      * @param contentText the string to show in the dialog content area
 604      */
 605     public final void setContentText(String contentText) {
 606         getDialogPane().setContentText(contentText);
 607     }
 608 
 609 
 610     // --- header text (forwarded from DialogPane)
 611     /**
 612      * A property representing the header text for the dialog pane. The header text
 613      * is lower precedence than the {@link DialogPane#headerProperty() header node}, meaning
 614      * that if both the header node and the headerText properties are set, the
 615      * header text will not be displayed in a default DialogPane instance.
 616      * @return a property representing the header text for the dialog pane
 617      */
 618     public final StringProperty headerTextProperty() {
 619         return getDialogPane().headerTextProperty();
 620     }
 621 
 622     /**
 623      * Returns the currently-set header text for this DialogPane.
 624      * @return the currently-set header text for this DialogPane
 625      */
 626     public final String getHeaderText() {
 627         return getDialogPane().getHeaderText();
 628     }
 629 
 630     /**
 631      * Sets the string to show in the dialog header area. Note that the header text
 632      * is lower precedence than the {@link DialogPane#headerProperty() header node}, meaning
 633      * that if both the header node and the headerText properties are set, the
 634      * header text will not be displayed in a default DialogPane instance.
 635      * @param headerText the string to show in the dialog header area
 636      */
 637     public final void setHeaderText(String headerText) {
 638         getDialogPane().setHeaderText(headerText);
 639     }
 640 
 641 
 642     // --- graphic (forwarded from DialogPane)
 643     /**
 644      * The dialog graphic, presented either in the header, if one is showing, or
 645      * to the left of the {@link DialogPane#contentProperty() content}.
 646      *
 647      * @return An ObjectProperty wrapping the current graphic.
 648      */
 649     public final ObjectProperty<Node> graphicProperty() {
 650         return getDialogPane().graphicProperty();
 651     }
 652 
 653     public final Node getGraphic() {
 654         return getDialogPane().getGraphic();
 655     }
 656 
 657     /**
 658      * Sets the dialog graphic, which will be displayed either in the header, if
 659      * one is showing, or to the left of the {@link DialogPane#contentProperty() content}.
 660      *
 661      * @param graphic
 662      *            The new dialog graphic, or null if no graphic should be shown.
 663      */
 664     public final void setGraphic(Node graphic) {
 665         getDialogPane().setGraphic(graphic);
 666     }
 667 
 668 
 669     // --- result
 670     private final ObjectProperty<R> resultProperty = new SimpleObjectProperty<R>() {
 671         protected void invalidated() {
 672             close();
 673         }
 674     };
 675 
 676     /**
 677      * A property representing what has been returned from the dialog. A result
 678      * is generated through the {@link #resultConverterProperty() result converter},
 679      * which is intended to convert from the {@link ButtonType} that the user
 680      * clicked on into a value of type R. Refer to the {@link Dialog} class
 681      * JavaDoc for more details.
 682      * @return a property representing what has been returned from the dialog
 683      */
 684     public final ObjectProperty<R> resultProperty() {
 685         return resultProperty;
 686     }
 687 
 688     public final R getResult() {
 689         return resultProperty().get();
 690     }
 691 
 692     public final void setResult(R value) {
 693         this.resultProperty().set(value);
 694     }
 695 
 696 
 697     // --- result converter
 698     private final ObjectProperty<Callback<ButtonType, R>> resultConverterProperty
 699         = new SimpleObjectProperty<>(this, "resultConverter");
 700 
 701     /**
 702      * API to convert the {@link ButtonType} that the user clicked on into a
 703      * result that can be returned via the {@link #resultProperty() result}
 704      * property. This is necessary as {@link ButtonType} represents the visual
 705      * button within the dialog, and do not know how to map themselves to a valid
 706      * result - that is a requirement of the dialog implementation by making use
 707      * of the result converter. In some cases, the result type of a Dialog
 708      * subclass is ButtonType (which means that the result converter can be null),
 709      * but in some cases (where the result type, R, is not ButtonType or Void),
 710      * this callback must be specified.
 711      * @return the API to convert the {@link ButtonType} that the user clicked on
 712      */
 713     public final ObjectProperty<Callback<ButtonType, R>> resultConverterProperty() {
 714         return resultConverterProperty;
 715     }
 716 
 717     public final Callback<ButtonType, R> getResultConverter() {
 718         return resultConverterProperty().get();
 719     }
 720 
 721     public final void setResultConverter(Callback<ButtonType, R> value) {
 722         this.resultConverterProperty().set(value);
 723     }
 724 
 725 
 726     // --- showing
 727     /**
 728      * Represents whether the dialog is currently showing.
 729      * @return the property representing whether the dialog is currently showing
 730      */
 731     public final ReadOnlyBooleanProperty showingProperty() {
 732         return dialog.showingProperty();
 733     }
 734 
 735     /**
 736      * Returns whether or not the dialog is showing.
 737      *
 738      * @return true if dialog is showing.
 739      */
 740     public final boolean isShowing() {
 741         return showingProperty().get();
 742     }
 743 
 744 
 745     // --- resizable
 746     /**
 747      * Represents whether the dialog is resizable.
 748      * @return the property representing whether the dialog is resizable
 749      */
 750     public final BooleanProperty resizableProperty() {
 751         return dialog.resizableProperty();
 752     }
 753 
 754     /**
 755      * Returns whether or not the dialog is resizable.
 756      *
 757      * @return true if dialog is resizable.
 758      */
 759     public final boolean isResizable() {
 760         return resizableProperty().get();
 761     }
 762 
 763     /**
 764      * Sets whether the dialog can be resized by the user.
 765      * Resizable dialogs can also be maximized ( maximize button
 766      * becomes visible)
 767      *
 768      * @param resizable true if dialog should be resizable.
 769      */
 770     public final void setResizable(boolean resizable) {
 771         resizableProperty().set(resizable);
 772     }
 773 
 774 
 775     // --- width
 776     /**
 777      * Property representing the width of the dialog.
 778      * @return the property representing the width of the dialog
 779      */
 780     public final ReadOnlyDoubleProperty widthProperty() {
 781         return dialog.widthProperty();
 782     }
 783 
 784     /**
 785      * Returns the width of the dialog.
 786      * @return the width of the dialog
 787      */
 788     public final double getWidth() {
 789         return widthProperty().get();
 790     }
 791 
 792     /**
 793      * Sets the width of the dialog.
 794      * @param width the width of the dialog
 795      */
 796     public final void setWidth(double width) {
 797         dialog.setWidth(width);
 798     }
 799 
 800 
 801     // --- height
 802     /**
 803      * Property representing the height of the dialog.
 804      * @return the property representing the height of the dialog
 805      */
 806     public final ReadOnlyDoubleProperty heightProperty() {
 807         return dialog.heightProperty();
 808     }
 809 
 810     /**
 811      * Returns the height of the dialog.
 812      * @return the height of the dialog
 813      */
 814     public final double getHeight() {
 815         return heightProperty().get();
 816     }
 817 
 818     /**
 819      * Sets the height of the dialog.
 820      * @param height the height of the dialog
 821      */
 822     public final void setHeight(double height) {
 823         dialog.setHeight(height);
 824     }
 825 
 826 
 827     // --- title
 828     /**
 829      * Return the titleProperty of the dialog.
 830      * @return the titleProperty of the dialog
 831      */
 832     public final StringProperty titleProperty(){
 833         return this.dialog.titleProperty();
 834     }
 835 
 836     /**
 837      * Return the title of the dialog.
 838      * @return the title of the dialog
 839      */
 840     public final String getTitle(){
 841         return this.dialog.titleProperty().get();
 842     }
 843     /**
 844      * Change the Title of the dialog.
 845      * @param title the Title of the dialog
 846      */
 847     public final void setTitle(String title){
 848         this.dialog.titleProperty().set(title);
 849     }
 850 
 851 
 852     // --- x
 853     public final double getX() {
 854         return dialog.getX();
 855     }
 856 
 857     public final void setX(double x) {
 858         dialog.setX(x);
 859     }
 860 
 861     /**
 862      * The horizontal location of this {@code Dialog}. Changing this attribute
 863      * will move the {@code Dialog} horizontally.
 864      * @return the horizontal location of this {@code Dialog}
 865      */
 866     public final ReadOnlyDoubleProperty xProperty() {
 867         return dialog.xProperty();
 868     }
 869 
 870     // --- y
 871     public final double getY() {
 872         return dialog.getY();
 873     }
 874 
 875     public final void setY(double y) {
 876         dialog.setY(y);
 877     }
 878 
 879     /**
 880      * The vertical location of this {@code Dialog}. Changing this attribute
 881      * will move the {@code Dialog} vertically.
 882      * @return the vertical location of this {@code Dialog}
 883      */
 884     public final ReadOnlyDoubleProperty yProperty() {
 885         return dialog.yProperty();
 886     }
 887 
 888 
 889 
 890     /***************************************************************************
 891      *
 892      * Events
 893      *
 894      **************************************************************************/
 895 
 896     private final EventHandlerManager eventHandlerManager = new EventHandlerManager(this);
 897 
 898     /** {@inheritDoc} */
 899     @Override public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
 900         return tail.prepend(eventHandlerManager);
 901     }
 902 
 903     /**
 904      * Called just prior to the Dialog being shown.
 905      */
 906     private ObjectProperty<EventHandler<DialogEvent>> onShowing;
 907     public final void setOnShowing(EventHandler<DialogEvent> value) { onShowingProperty().set(value); }
 908     public final EventHandler<DialogEvent> getOnShowing() {
 909         return onShowing == null ? null : onShowing.get();
 910     }
 911     public final ObjectProperty<EventHandler<DialogEvent>> onShowingProperty() {
 912         if (onShowing == null) {
 913             onShowing = new SimpleObjectProperty<EventHandler<DialogEvent>>(this, "onShowing") {
 914                 @Override protected void invalidated() {
 915                     eventHandlerManager.setEventHandler(DialogEvent.DIALOG_SHOWING, get());
 916                 }
 917             };
 918         }
 919         return onShowing;
 920     }
 921 
 922     /**
 923      * Called just after the Dialog is shown.
 924      */
 925     private ObjectProperty<EventHandler<DialogEvent>> onShown;
 926     public final void setOnShown(EventHandler<DialogEvent> value) { onShownProperty().set(value); }
 927     public final EventHandler<DialogEvent> getOnShown() {
 928         return onShown == null ? null : onShown.get();
 929     }
 930     public final ObjectProperty<EventHandler<DialogEvent>> onShownProperty() {
 931         if (onShown == null) {
 932             onShown = new SimpleObjectProperty<EventHandler<DialogEvent>>(this, "onShown") {
 933                 @Override protected void invalidated() {
 934                     eventHandlerManager.setEventHandler(DialogEvent.DIALOG_SHOWN, get());
 935                 }
 936             };
 937         }
 938         return onShown;
 939     }
 940 
 941     /**
 942      * Called just prior to the Dialog being hidden.
 943      */
 944     private ObjectProperty<EventHandler<DialogEvent>> onHiding;
 945     public final void setOnHiding(EventHandler<DialogEvent> value) { onHidingProperty().set(value); }
 946     public final EventHandler<DialogEvent> getOnHiding() {
 947         return onHiding == null ? null : onHiding.get();
 948     }
 949     public final ObjectProperty<EventHandler<DialogEvent>> onHidingProperty() {
 950         if (onHiding == null) {
 951             onHiding = new SimpleObjectProperty<EventHandler<DialogEvent>>(this, "onHiding") {
 952                 @Override protected void invalidated() {
 953                     eventHandlerManager.setEventHandler(DialogEvent.DIALOG_HIDING, get());
 954                 }
 955             };
 956         }
 957         return onHiding;
 958     }
 959 
 960     /**
 961      * Called just after the Dialog has been hidden.
 962      * When the {@code Dialog} is hidden, this event handler is invoked allowing
 963      * the developer to clean up resources or perform other tasks when the
 964      * {@link Alert} is closed.
 965      */
 966     private ObjectProperty<EventHandler<DialogEvent>> onHidden;
 967     public final void setOnHidden(EventHandler<DialogEvent> value) { onHiddenProperty().set(value); }
 968     public final EventHandler<DialogEvent> getOnHidden() {
 969         return onHidden == null ? null : onHidden.get();
 970     }
 971     public final ObjectProperty<EventHandler<DialogEvent>> onHiddenProperty() {
 972         if (onHidden == null) {
 973             onHidden = new SimpleObjectProperty<EventHandler<DialogEvent>>(this, "onHidden") {
 974                 @Override protected void invalidated() {
 975                     eventHandlerManager.setEventHandler(DialogEvent.DIALOG_HIDDEN, get());
 976                 }
 977             };
 978         }
 979         return onHidden;
 980     }
 981 
 982     /**
 983      * Called when there is an external request to close this {@code Dialog}.
 984      * The installed event handler can prevent dialog closing by consuming the
 985      * received event.
 986      */
 987     private ObjectProperty<EventHandler<DialogEvent>> onCloseRequest;
 988     public final void setOnCloseRequest(EventHandler<DialogEvent> value) {
 989         onCloseRequestProperty().set(value);
 990     }
 991     public final EventHandler<DialogEvent> getOnCloseRequest() {
 992         return (onCloseRequest != null) ? onCloseRequest.get() : null;
 993     }
 994     public final ObjectProperty<EventHandler<DialogEvent>>
 995             onCloseRequestProperty() {
 996         if (onCloseRequest == null) {
 997             onCloseRequest = new SimpleObjectProperty<EventHandler<DialogEvent>>(this, "onCloseRequest") {
 998                 @Override protected void invalidated() {
 999                     eventHandlerManager.setEventHandler(DialogEvent.DIALOG_CLOSE_REQUEST, get());
1000                 }
1001             };
1002         }
1003         return onCloseRequest;
1004     }
1005 
1006 
1007 
1008     /***************************************************************************
1009      *
1010      * Private implementation
1011      *
1012      **************************************************************************/
1013 
1014     // This code is called both in the normal and in the abnormal case (i.e.
1015     // both when a button is clicked and when the user forces a window closed
1016     // with keyboard OS-specific shortcuts or OS-native titlebar buttons).
1017     @SuppressWarnings("unchecked")
1018     void setResultAndClose(ButtonType cmd, boolean close) {
1019         Callback<ButtonType, R> resultConverter = getResultConverter();
1020 
1021         R priorResultValue = getResult();
1022         R newResultValue = null;
1023 
1024         if (resultConverter == null) {
1025             // The choice to cast cmd to R here was a conscious decision, taking
1026             // into account the choices available to us. Firstly, to summarise the
1027             // issue, at this point here we have a null result converter, and no
1028             // idea how to convert the given ButtonType to R. Our options are:
1029             //
1030             // 1) We could throw an exception here, but this requires that all
1031             // developers who create a dialog set a result converter (at least
1032             // setResultConverter(buttonType -> (R) buttonType)). This is
1033             // non-intuitive and depends on the developer reading documentation.
1034             //
1035             // 2) We could set a default result converter in the resultConverter
1036             // property that does the identity conversion. This saves people from
1037             // having to set a default result converter, but it is a little odd
1038             // that the result converter is non-null by default.
1039             //
1040             // 3) We can cast the button type here, which is what we do. This means
1041             // that the result converter is null by default.
1042             //
1043             // In the case of option 1), developers will receive a NPE when the
1044             // dialog is closed, regardless of how it was closed. In the case of
1045             // option 2) and 3), the user unfortunately receives a ClassCastException
1046             // in their code. This is unfortunate as it is not immediately obvious
1047             // why the ClassCastException occurred, and how to resolve it. However,
1048             // we decided to take this later approach as it prevents the issue of
1049             // requiring all custom dialog developers from having to supply their
1050             // own result converters.
1051             newResultValue = (R) cmd;
1052         } else {
1053             newResultValue = resultConverter.call(cmd);
1054         }
1055 
1056         setResult(newResultValue);
1057 
1058         // fix for the case where we set the same result as what
1059         // was already set. We should still close the dialog, but
1060         // we need to special-case it here, as the result property
1061         // won't fire any event if the value won't change.
1062         if (close && priorResultValue == newResultValue) {
1063             close();
1064         }
1065     }
1066 
1067 
1068 
1069 
1070     /***************************************************************************
1071      *
1072      * Stylesheet Handling
1073      *
1074      **************************************************************************/
1075     private static final PseudoClass HEADER_PSEUDO_CLASS =
1076             PseudoClass.getPseudoClass("header"); //$NON-NLS-1$
1077     private static final PseudoClass NO_HEADER_PSEUDO_CLASS =
1078             PseudoClass.getPseudoClass("no-header"); //$NON-NLS-1$
1079 
1080     private void updatePseudoClassState() {
1081         DialogPane dialogPane = getDialogPane();
1082         if (dialogPane != null) {
1083             final boolean hasHeader = getDialogPane().hasHeader();
1084             dialogPane.pseudoClassStateChanged(HEADER_PSEUDO_CLASS,     hasHeader);
1085             dialogPane.pseudoClassStateChanged(NO_HEADER_PSEUDO_CLASS, !hasHeader);
1086         }
1087     }
1088 }