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 }