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 * Closes this {@code Dialog}. 348 * This call is equivalent to {@link #hide}. 349 */ 350 public final void close() { 351 if (isClosing) return; 352 isClosing = true; 353 354 final R result = getResult(); 355 356 // if the result is null and we do not have permission to close the 357 // dialog, then we cancel the close request before any events are 358 // even fired 359 if (result == null && ! dialog.requestPermissionToClose(this)) { 360 isClosing = false; 361 return; 362 } 363 364 // if we are here we have permission to close the dialog. However, we 365 // may not have a result set to return to the user. Therefore, we need 366 // to handle that before the dialog closes (especially in case the 367 // dialog is blocking, in which case having a null result is really going 368 // to mess up users). 369 // 370 // In cases where the result is null, and where the dialog has a cancel 371 // button, we call into the result converter to see what to do. This is 372 // used primarily to handle the requirement that the X button has the 373 // same result as clicking the cancel button. 374 // 375 // A 'cancel button' can mean two different things (although they may 376 // be the same thing): 377 // 1) A button whose ButtonData is of type CANCEL_CLOSE. 378 // 2) A button whose ButtonData returns true for isCancelButton(). 379 if (result == null) { 380 ButtonType cancelButton = null; 381 382 // we do two things here. We are primarily looking for a button with 383 // ButtonData.CANCEL_CLOSE. If we find one, we use it as the result. 384 // However, if we don't find one, we can also use any button that 385 // is a cancel button. 386 for (ButtonType button : getDialogPane().getButtonTypes()) { 387 ButtonData buttonData = button.getButtonData(); 388 if (buttonData == null) continue; 389 390 if (buttonData == ButtonData.CANCEL_CLOSE) { 391 cancelButton = button; 392 break; 393 } 394 if (buttonData.isCancelButton()) { 395 cancelButton = button; 396 } 397 } 398 399 setResultAndClose(cancelButton, false); 400 } 401 402 // start normal closing process 403 Event.fireEvent(this, new DialogEvent(this, DialogEvent.DIALOG_HIDING)); 404 405 DialogEvent closeRequestEvent = new DialogEvent(this, DialogEvent.DIALOG_CLOSE_REQUEST); 406 Event.fireEvent(this, closeRequestEvent); 407 if (closeRequestEvent.isConsumed()) { 408 isClosing = false; 409 return; 410 } 411 412 dialog.close(); 413 414 Event.fireEvent(this, new DialogEvent(this, DialogEvent.DIALOG_HIDDEN)); 415 416 isClosing = false; 417 } 418 419 /** 420 * Hides this {@code Dialog}. 421 */ 422 public final void hide() { 423 close(); 424 } 425 426 /** 427 * Specifies the modality for this dialog. This must be done prior to making 428 * the dialog visible. The modality is one of: Modality.NONE, 429 * Modality.WINDOW_MODAL, or Modality.APPLICATION_MODAL. 430 * 431 * @param modality the modality for this dialog. 432 * 433 * @throws IllegalStateException if this property is set after the dialog 434 * has ever been made visible. 435 * 436 * @defaultValue Modality.APPLICATION_MODAL 437 */ 438 public final void initModality(Modality modality) { 439 dialog.initModality(modality); 440 } 441 442 /** 443 * Retrieves the modality attribute for this dialog. 444 * 445 * @return the modality. 446 */ 447 public final Modality getModality() { 448 return dialog.getModality(); 449 } 450 451 /** 452 * Specifies the style for this dialog. This must be done prior to making 453 * the dialog visible. The style is one of: StageStyle.DECORATED, 454 * StageStyle.UNDECORATED, StageStyle.TRANSPARENT, StageStyle.UTILITY, 455 * or StageStyle.UNIFIED. 456 * 457 * @param style the style for this dialog. 458 * 459 * @throws IllegalStateException if this property is set after the dialog 460 * has ever been made visible. 461 * 462 * @defaultValue StageStyle.DECORATED 463 */ 464 public final void initStyle(StageStyle style) { 465 dialog.initStyle(style); 466 } 467 468 /** 469 * Specifies the owner {@link Window} for this dialog, or null for a top-level, 470 * unowned dialog. This must be done prior to making the dialog visible. 471 * 472 * @param window the owner {@link Window} for this dialog. 473 * 474 * @throws IllegalStateException if this property is set after the dialog 475 * has ever been made visible. 476 * 477 * @defaultValue null 478 */ 479 public final void initOwner(Window window) { 480 dialog.initOwner(window); 481 } 482 483 /** 484 * Retrieves the owner Window for this dialog, or null for an unowned dialog. 485 * 486 * @return the owner Window. 487 */ 488 public final Window getOwner() { 489 return dialog.getOwner(); 490 } 491 492 493 494 /************************************************************************** 495 * 496 * Properties 497 * 498 **************************************************************************/ 499 500 // --- dialog Pane 501 /** 502 * The root node of the dialog, the {@link DialogPane} contains all visual 503 * elements shown in the dialog. As such, it is possible to completely adjust 504 * the display of the dialog by modifying the existing dialog pane or creating 505 * a new one. 506 */ 507 private ObjectProperty<DialogPane> dialogPane = new SimpleObjectProperty<DialogPane>(this, "dialogPane", new DialogPane()) { 508 final InvalidationListener expandedListener = o -> { 509 DialogPane dialogPane = getDialogPane(); 510 if (dialogPane == null) return; 511 512 final Node content = dialogPane.getExpandableContent(); 513 final boolean isExpanded = content == null ? false : content.isVisible(); 514 setResizable(isExpanded); 515 516 Dialog.this.dialog.sizeToScene(); 517 }; 518 519 final InvalidationListener headerListener = o -> { 520 updatePseudoClassState(); 521 }; 522 523 WeakReference<DialogPane> dialogPaneRef = new WeakReference<>(null); 524 525 @Override 526 protected void invalidated() { 527 DialogPane oldDialogPane = dialogPaneRef.get(); 528 if (oldDialogPane != null) { 529 // clean up 530 oldDialogPane.expandedProperty().removeListener(expandedListener); 531 oldDialogPane.headerProperty().removeListener(headerListener); 532 oldDialogPane.headerTextProperty().removeListener(headerListener); 533 oldDialogPane.setDialog(null); 534 } 535 536 final DialogPane newDialogPane = getDialogPane(); 537 538 if (newDialogPane != null) { 539 newDialogPane.setDialog(Dialog.this); 540 541 // if the buttons change, we dynamically update the dialog 542 newDialogPane.getButtonTypes().addListener((ListChangeListener<ButtonType>) c -> { 543 newDialogPane.requestLayout(); 544 }); 545 newDialogPane.expandedProperty().addListener(expandedListener); 546 newDialogPane.headerProperty().addListener(headerListener); 547 newDialogPane.headerTextProperty().addListener(headerListener); 548 549 updatePseudoClassState(); 550 newDialogPane.requestLayout(); 551 } 552 553 // push the new dialog down into the implementation for rendering 554 dialog.setDialogPane(newDialogPane); 555 556 dialogPaneRef = new WeakReference<DialogPane>(newDialogPane); 557 } 558 }; 559 560 public final ObjectProperty<DialogPane> dialogPaneProperty() { 561 return dialogPane; 562 } 563 564 public final DialogPane getDialogPane() { 565 return dialogPane.get(); 566 } 567 568 public final void setDialogPane(DialogPane value) { 569 dialogPane.set(value); 570 } 571 572 573 // --- content text (forwarded from DialogPane) 574 /** 575 * A property representing the content text for the dialog pane. The content text 576 * is lower precedence than the {@link DialogPane#contentProperty() content node}, meaning 577 * that if both the content node and the contentText properties are set, the 578 * content text will not be displayed in a default DialogPane instance. 579 * @return the property representing the content text for the dialog pane 580 */ 581 public final StringProperty contentTextProperty() { 582 return getDialogPane().contentTextProperty(); 583 } 584 585 /** 586 * Returns the currently-set content text for this DialogPane. 587 * @return the currently-set content text for this DialogPane 588 */ 589 public final String getContentText() { 590 return getDialogPane().getContentText(); 591 } 592 593 /** 594 * Sets the string to show in the dialog content area. Note that the content text 595 * is lower precedence than the {@link DialogPane#contentProperty() content node}, meaning 596 * that if both the content node and the contentText properties are set, the 597 * content text will not be displayed in a default DialogPane instance. 598 * @param contentText the string to show in the dialog content area 599 */ 600 public final void setContentText(String contentText) { 601 getDialogPane().setContentText(contentText); 602 } 603 604 605 // --- header text (forwarded from DialogPane) 606 /** 607 * A property representing the header text for the dialog pane. The header text 608 * is lower precedence than the {@link DialogPane#headerProperty() header node}, meaning 609 * that if both the header node and the headerText properties are set, the 610 * header text will not be displayed in a default DialogPane instance. 611 * @return a property representing the header text for the dialog pane 612 */ 613 public final StringProperty headerTextProperty() { 614 return getDialogPane().headerTextProperty(); 615 } 616 617 /** 618 * Returns the currently-set header text for this DialogPane. 619 * @return the currently-set header text for this DialogPane 620 */ 621 public final String getHeaderText() { 622 return getDialogPane().getHeaderText(); 623 } 624 625 /** 626 * Sets the string to show in the dialog header area. Note that the header text 627 * is lower precedence than the {@link DialogPane#headerProperty() header node}, meaning 628 * that if both the header node and the headerText properties are set, the 629 * header text will not be displayed in a default DialogPane instance. 630 * @param headerText the string to show in the dialog header area 631 */ 632 public final void setHeaderText(String headerText) { 633 getDialogPane().setHeaderText(headerText); 634 } 635 636 637 // --- graphic (forwarded from DialogPane) 638 /** 639 * The dialog graphic, presented either in the header, if one is showing, or 640 * to the left of the {@link DialogPane#contentProperty() content}. 641 * 642 * @return An ObjectProperty wrapping the current graphic. 643 */ 644 public final ObjectProperty<Node> graphicProperty() { 645 return getDialogPane().graphicProperty(); 646 } 647 648 public final Node getGraphic() { 649 return getDialogPane().getGraphic(); 650 } 651 652 /** 653 * Sets the dialog graphic, which will be displayed either in the header, if 654 * one is showing, or to the left of the {@link DialogPane#contentProperty() content}. 655 * 656 * @param graphic 657 * The new dialog graphic, or null if no graphic should be shown. 658 */ 659 public final void setGraphic(Node graphic) { 660 getDialogPane().setGraphic(graphic); 661 } 662 663 664 // --- result 665 private final ObjectProperty<R> resultProperty = new SimpleObjectProperty<R>() { 666 protected void invalidated() { 667 close(); 668 } 669 }; 670 671 /** 672 * A property representing what has been returned from the dialog. A result 673 * is generated through the {@link #resultConverterProperty() result converter}, 674 * which is intended to convert from the {@link ButtonType} that the user 675 * clicked on into a value of type R. Refer to the {@link Dialog} class 676 * JavaDoc for more details. 677 * @return a property representing what has been returned from the dialog 678 */ 679 public final ObjectProperty<R> resultProperty() { 680 return resultProperty; 681 } 682 683 public final R getResult() { 684 return resultProperty().get(); 685 } 686 687 public final void setResult(R value) { 688 this.resultProperty().set(value); 689 } 690 691 692 // --- result converter 693 private final ObjectProperty<Callback<ButtonType, R>> resultConverterProperty 694 = new SimpleObjectProperty<>(this, "resultConverter"); 695 696 /** 697 * API to convert the {@link ButtonType} that the user clicked on into a 698 * result that can be returned via the {@link #resultProperty() result} 699 * property. This is necessary as {@link ButtonType} represents the visual 700 * button within the dialog, and do not know how to map themselves to a valid 701 * result - that is a requirement of the dialog implementation by making use 702 * of the result converter. In some cases, the result type of a Dialog 703 * subclass is ButtonType (which means that the result converter can be null), 704 * but in some cases (where the result type, R, is not ButtonType or Void), 705 * this callback must be specified. 706 * @return the API to convert the {@link ButtonType} that the user clicked on 707 */ 708 public final ObjectProperty<Callback<ButtonType, R>> resultConverterProperty() { 709 return resultConverterProperty; 710 } 711 712 public final Callback<ButtonType, R> getResultConverter() { 713 return resultConverterProperty().get(); 714 } 715 716 public final void setResultConverter(Callback<ButtonType, R> value) { 717 this.resultConverterProperty().set(value); 718 } 719 720 721 // --- showing 722 /** 723 * Represents whether the dialog is currently showing. 724 * @return the property representing whether the dialog is currently showing 725 */ 726 public final ReadOnlyBooleanProperty showingProperty() { 727 return dialog.showingProperty(); 728 } 729 730 /** 731 * Returns whether or not the dialog is showing. 732 * 733 * @return true if dialog is showing. 734 */ 735 public final boolean isShowing() { 736 return showingProperty().get(); 737 } 738 739 740 // --- resizable 741 /** 742 * Represents whether the dialog is resizable. 743 * @return the property representing whether the dialog is resizable 744 */ 745 public final BooleanProperty resizableProperty() { 746 return dialog.resizableProperty(); 747 } 748 749 /** 750 * Returns whether or not the dialog is resizable. 751 * 752 * @return true if dialog is resizable. 753 */ 754 public final boolean isResizable() { 755 return resizableProperty().get(); 756 } 757 758 /** 759 * Sets whether the dialog can be resized by the user. 760 * Resizable dialogs can also be maximized ( maximize button 761 * becomes visible) 762 * 763 * @param resizable true if dialog should be resizable. 764 */ 765 public final void setResizable(boolean resizable) { 766 resizableProperty().set(resizable); 767 } 768 769 770 // --- width 771 /** 772 * Property representing the width of the dialog. 773 * @return the property representing the width of the dialog 774 */ 775 public final ReadOnlyDoubleProperty widthProperty() { 776 return dialog.widthProperty(); 777 } 778 779 /** 780 * Returns the width of the dialog. 781 * @return the width of the dialog 782 */ 783 public final double getWidth() { 784 return widthProperty().get(); 785 } 786 787 /** 788 * Sets the width of the dialog. 789 * @param width the width of the dialog 790 */ 791 public final void setWidth(double width) { 792 dialog.setWidth(width); 793 } 794 795 796 // --- height 797 /** 798 * Property representing the height of the dialog. 799 * @return the property representing the height of the dialog 800 */ 801 public final ReadOnlyDoubleProperty heightProperty() { 802 return dialog.heightProperty(); 803 } 804 805 /** 806 * Returns the height of the dialog. 807 * @return the height of the dialog 808 */ 809 public final double getHeight() { 810 return heightProperty().get(); 811 } 812 813 /** 814 * Sets the height of the dialog. 815 * @param height the height of the dialog 816 */ 817 public final void setHeight(double height) { 818 dialog.setHeight(height); 819 } 820 821 822 // --- title 823 /** 824 * Return the titleProperty of the dialog. 825 * @return the titleProperty of the dialog 826 */ 827 public final StringProperty titleProperty(){ 828 return this.dialog.titleProperty(); 829 } 830 831 /** 832 * Return the title of the dialog. 833 * @return the title of the dialog 834 */ 835 public final String getTitle(){ 836 return this.dialog.titleProperty().get(); 837 } 838 /** 839 * Change the Title of the dialog. 840 * @param title the Title of the dialog 841 */ 842 public final void setTitle(String title){ 843 this.dialog.titleProperty().set(title); 844 } 845 846 847 // --- x 848 public final double getX() { 849 return dialog.getX(); 850 } 851 852 public final void setX(double x) { 853 dialog.setX(x); 854 } 855 856 /** 857 * The horizontal location of this {@code Dialog}. Changing this attribute 858 * will move the {@code Dialog} horizontally. 859 * @return the horizontal location of this {@code Dialog} 860 */ 861 public final ReadOnlyDoubleProperty xProperty() { 862 return dialog.xProperty(); 863 } 864 865 // --- y 866 public final double getY() { 867 return dialog.getY(); 868 } 869 870 public final void setY(double y) { 871 dialog.setY(y); 872 } 873 874 /** 875 * The vertical location of this {@code Dialog}. Changing this attribute 876 * will move the {@code Dialog} vertically. 877 * @return the vertical location of this {@code Dialog} 878 */ 879 public final ReadOnlyDoubleProperty yProperty() { 880 return dialog.yProperty(); 881 } 882 883 884 885 /*************************************************************************** 886 * 887 * Events 888 * 889 **************************************************************************/ 890 891 private final EventHandlerManager eventHandlerManager = new EventHandlerManager(this); 892 893 /** {@inheritDoc} */ 894 @Override public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) { 895 return tail.prepend(eventHandlerManager); 896 } 897 898 /** 899 * Called just prior to the Dialog being shown. 900 */ 901 private ObjectProperty<EventHandler<DialogEvent>> onShowing; 902 public final void setOnShowing(EventHandler<DialogEvent> value) { onShowingProperty().set(value); } 903 public final EventHandler<DialogEvent> getOnShowing() { 904 return onShowing == null ? null : onShowing.get(); 905 } 906 public final ObjectProperty<EventHandler<DialogEvent>> onShowingProperty() { 907 if (onShowing == null) { 908 onShowing = new SimpleObjectProperty<EventHandler<DialogEvent>>(this, "onShowing") { 909 @Override protected void invalidated() { 910 eventHandlerManager.setEventHandler(DialogEvent.DIALOG_SHOWING, get()); 911 } 912 }; 913 } 914 return onShowing; 915 } 916 917 /** 918 * Called just after the Dialog is shown. 919 */ 920 private ObjectProperty<EventHandler<DialogEvent>> onShown; 921 public final void setOnShown(EventHandler<DialogEvent> value) { onShownProperty().set(value); } 922 public final EventHandler<DialogEvent> getOnShown() { 923 return onShown == null ? null : onShown.get(); 924 } 925 public final ObjectProperty<EventHandler<DialogEvent>> onShownProperty() { 926 if (onShown == null) { 927 onShown = new SimpleObjectProperty<EventHandler<DialogEvent>>(this, "onShown") { 928 @Override protected void invalidated() { 929 eventHandlerManager.setEventHandler(DialogEvent.DIALOG_SHOWN, get()); 930 } 931 }; 932 } 933 return onShown; 934 } 935 936 /** 937 * Called just prior to the Dialog being hidden. 938 */ 939 private ObjectProperty<EventHandler<DialogEvent>> onHiding; 940 public final void setOnHiding(EventHandler<DialogEvent> value) { onHidingProperty().set(value); } 941 public final EventHandler<DialogEvent> getOnHiding() { 942 return onHiding == null ? null : onHiding.get(); 943 } 944 public final ObjectProperty<EventHandler<DialogEvent>> onHidingProperty() { 945 if (onHiding == null) { 946 onHiding = new SimpleObjectProperty<EventHandler<DialogEvent>>(this, "onHiding") { 947 @Override protected void invalidated() { 948 eventHandlerManager.setEventHandler(DialogEvent.DIALOG_HIDING, get()); 949 } 950 }; 951 } 952 return onHiding; 953 } 954 955 /** 956 * Called just after the Dialog has been hidden. 957 * When the {@code Dialog} is hidden, this event handler is invoked allowing 958 * the developer to clean up resources or perform other tasks when the 959 * {@link Alert} is closed. 960 */ 961 private ObjectProperty<EventHandler<DialogEvent>> onHidden; 962 public final void setOnHidden(EventHandler<DialogEvent> value) { onHiddenProperty().set(value); } 963 public final EventHandler<DialogEvent> getOnHidden() { 964 return onHidden == null ? null : onHidden.get(); 965 } 966 public final ObjectProperty<EventHandler<DialogEvent>> onHiddenProperty() { 967 if (onHidden == null) { 968 onHidden = new SimpleObjectProperty<EventHandler<DialogEvent>>(this, "onHidden") { 969 @Override protected void invalidated() { 970 eventHandlerManager.setEventHandler(DialogEvent.DIALOG_HIDDEN, get()); 971 } 972 }; 973 } 974 return onHidden; 975 } 976 977 /** 978 * Called when there is an external request to close this {@code Dialog}. 979 * The installed event handler can prevent dialog closing by consuming the 980 * received event. 981 */ 982 private ObjectProperty<EventHandler<DialogEvent>> onCloseRequest; 983 public final void setOnCloseRequest(EventHandler<DialogEvent> value) { 984 onCloseRequestProperty().set(value); 985 } 986 public final EventHandler<DialogEvent> getOnCloseRequest() { 987 return (onCloseRequest != null) ? onCloseRequest.get() : null; 988 } 989 public final ObjectProperty<EventHandler<DialogEvent>> 990 onCloseRequestProperty() { 991 if (onCloseRequest == null) { 992 onCloseRequest = new SimpleObjectProperty<EventHandler<DialogEvent>>(this, "onCloseRequest") { 993 @Override protected void invalidated() { 994 eventHandlerManager.setEventHandler(DialogEvent.DIALOG_CLOSE_REQUEST, get()); 995 } 996 }; 997 } 998 return onCloseRequest; 999 } 1000 1001 1002 1003 /*************************************************************************** 1004 * 1005 * Private implementation 1006 * 1007 **************************************************************************/ 1008 1009 // This code is called both in the normal and in the abnormal case (i.e. 1010 // both when a button is clicked and when the user forces a window closed 1011 // with keyboard OS-specific shortchuts or OS-native titlebar buttons). 1012 @SuppressWarnings("unchecked") 1013 void setResultAndClose(ButtonType cmd, boolean close) { 1014 Callback<ButtonType, R> resultConverter = getResultConverter(); 1015 1016 R priorResultValue = getResult(); 1017 R newResultValue = null; 1018 1019 if (resultConverter == null) { 1020 // The choice to cast cmd to R here was a conscious decision, taking 1021 // into account the choices available to us. Firstly, to summarise the 1022 // issue, at this point here we have a null result converter, and no 1023 // idea how to convert the given ButtonType to R. Our options are: 1024 // 1025 // 1) We could throw an exception here, but this requires that all 1026 // developers who create a dialog set a result converter (at least 1027 // setResultConverter(buttonType -> (R) buttonType)). This is 1028 // non-intuitive and depends on the developer reading documentation. 1029 // 1030 // 2) We could set a default result converter in the resultConverter 1031 // property that does the identity conversion. This saves people from 1032 // having to set a default result converter, but it is a little odd 1033 // that the result converter is non-null by default. 1034 // 1035 // 3) We can cast the button type here, which is what we do. This means 1036 // that the result converter is null by default. 1037 // 1038 // In the case of option 1), developers will receive a NPE when the 1039 // dialog is closed, regardless of how it was closed. In the case of 1040 // option 2) and 3), the user unfortunately receives a ClassCastException 1041 // in their code. This is unfortunate as it is not immediately obvious 1042 // why the ClassCastException occurred, and how to resolve it. However, 1043 // we decided to take this later approach as it prevents the issue of 1044 // requiring all custom dialog developers from having to supply their 1045 // own result converters. 1046 newResultValue = (R) cmd; 1047 } else { 1048 newResultValue = resultConverter.call(cmd); 1049 } 1050 1051 setResult(newResultValue); 1052 1053 // fix for the case where we set the same result as what 1054 // was already set. We should still close the dialog, but 1055 // we need to special-case it here, as the result property 1056 // won't fire any event if the value won't change. 1057 if (close && priorResultValue == newResultValue) { 1058 close(); 1059 } 1060 } 1061 1062 1063 1064 1065 /*************************************************************************** 1066 * 1067 * Stylesheet Handling 1068 * 1069 **************************************************************************/ 1070 private static final PseudoClass HEADER_PSEUDO_CLASS = 1071 PseudoClass.getPseudoClass("header"); //$NON-NLS-1$ 1072 private static final PseudoClass NO_HEADER_PSEUDO_CLASS = 1073 PseudoClass.getPseudoClass("no-header"); //$NON-NLS-1$ 1074 1075 private void updatePseudoClassState() { 1076 DialogPane dialogPane = getDialogPane(); 1077 if (dialogPane != null) { 1078 final boolean hasHeader = getDialogPane().hasHeader(); 1079 dialogPane.pseudoClassStateChanged(HEADER_PSEUDO_CLASS, hasHeader); 1080 dialogPane.pseudoClassStateChanged(NO_HEADER_PSEUDO_CLASS, !hasHeader); 1081 } 1082 } 1083 }