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