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