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