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