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