1 /*
   2  * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package javafx.scene.control;
  26 
  27 import java.lang.ref.WeakReference;
  28 import java.util.ArrayList;
  29 import java.util.Collections;
  30 import java.util.List;
  31 import java.util.Map;
  32 import java.util.WeakHashMap;
  33 
  34 import com.sun.javafx.scene.control.skin.Utils;
  35 import com.sun.javafx.scene.control.skin.resources.ControlResources;
  36 import javafx.beans.DefaultProperty;
  37 import javafx.beans.InvalidationListener;
  38 import javafx.beans.property.BooleanProperty;
  39 import javafx.beans.property.ObjectProperty;
  40 import javafx.beans.property.SimpleBooleanProperty;
  41 import javafx.beans.property.SimpleObjectProperty;
  42 import javafx.beans.property.SimpleStringProperty;
  43 import javafx.beans.property.StringProperty;
  44 import javafx.beans.value.WritableValue;
  45 import javafx.collections.FXCollections;
  46 import javafx.collections.ListChangeListener;
  47 import javafx.collections.ObservableList;
  48 import javafx.css.CssMetaData;
  49 import javafx.css.StyleOrigin;
  50 import javafx.css.Styleable;
  51 import javafx.css.StyleableObjectProperty;
  52 import javafx.css.StyleableProperty;
  53 import javafx.css.StyleableStringProperty;
  54 import javafx.event.ActionEvent;
  55 import javafx.geometry.Pos;
  56 import javafx.scene.Node;
  57 import javafx.scene.control.ButtonBar.ButtonData;
  58 import javafx.scene.image.Image;
  59 import javafx.scene.image.ImageView;
  60 import javafx.scene.layout.ColumnConstraints;
  61 import javafx.scene.layout.GridPane;
  62 import javafx.scene.layout.Pane;
  63 import javafx.scene.layout.Priority;
  64 import javafx.scene.layout.Region;
  65 import javafx.scene.layout.StackPane;
  66 
  67 import com.sun.javafx.css.StyleManager;
  68 import javafx.css.converter.StringConverter;
  69 
  70 /**
  71  * DialogPane should be considered to be the root node displayed within a
  72  * {@link Dialog} instance. In this role, the DialogPane is responsible for the
  73  * placement of {@link #headerProperty() headers}, {@link #graphicProperty() graphics},
  74  * {@link #contentProperty() content}, and {@link #getButtonTypes() buttons}.
  75  * The default implementation of DialogPane (that is, the DialogPane class itself)
  76  * handles the layout via the normal {@link #layoutChildren()} method. This
  77  * method may be overridden by subclasses wishing to handle the layout in an
  78  * alternative fashion).
  79  *
  80  * <p>In addition to the {@link #headerProperty() header} and
  81  * {@link #contentProperty() content} properties, there exists
  82  * {@link #headerTextProperty() header text} and
  83  * {@link #contentTextProperty() content text} properties. The way the *Text
  84  * properties work is that they are a lower precedence compared to the Node
  85  * properties, but they are far more convenient for developers in the common case,
  86  * as it is likely the case that a developer more often than not simply wants to
  87  * set a string value into the header or content areas of the DialogPane.
  88  *
  89  * <p>It is important to understand the implications of setting non-null values
  90  * in the {@link #headerProperty() header} and {@link #headerTextProperty() headerText}
  91  * properties. The key points are as follows:
  92  *
  93  * <ol>
  94  *   <li>The {@code header} property takes precedence over the {@code headerText}
  95  *       property, so if both are set to non-null values, {@code header} will be
  96  *       used and {@code headerText} will be ignored.</li>
  97  *   <li>If {@code headerText} is set to a non-null value, and a
  98  *       {@link #graphicProperty() graphic} has also been set, the default position
  99  *       for the graphic shifts from being located to the left of the content area
 100  *       to being to the right of the header text.</li>
 101  *   <li>If {@code header} is set to a non-null value, and a
 102  *       {@link #graphicProperty() graphic} has also been set, the graphic is
 103  *       removed from its default position (to the left of the content area),
 104  *       and <strong>is not</strong> placed to the right of the custom header
 105  *       node. If the graphic is desired, it should be manually added in to the
 106  *       layout of the custom header node manually.</li>
 107  * </ol>
 108  *
 109  * <p>DialogPane operates on the concept of {@link ButtonType}. A ButtonType is
 110  * a descriptor of a single button that should be represented visually in the
 111  * DialogPane. Developers who create a DialogPane therefore must specify the
 112  * button types that they want to display, and this is done via the
 113  * {@link #getButtonTypes()} method, which returns a modifiable
 114  * {@link ObservableList}, which users can add to and remove from as desired.
 115  *
 116  * <p>The {@link ButtonType} class defines a number of pre-defined button types,
 117  * such as {@link ButtonType#OK} and {@link ButtonType#CANCEL}. Many users of the
 118  * JavaFX dialogs API will find that these pre-defined button types meet their
 119  * needs, particularly due to their built-in support for
 120  * {@link ButtonData#isDefaultButton() default} and
 121  * {@link ButtonData#isCancelButton() cancel} buttons, as well as the benefit of
 122  * the strings being translated into all languages which JavaFX is translated to.
 123  * For users that want to define their own {@link ButtonType} (most commonly to
 124  * define a button with custom text), they may do so via the constructors available
 125  * on the {@link ButtonType} class.
 126  *
 127  * <p>Developers will quickly find that the amount of configurability offered
 128  * via the {@link ButtonType} class is minimal. This is intentional, but does not
 129  * mean that developers can not modify the buttons created by the {@link ButtonType}
 130  * that have been specified. To do this, developers simply call the
 131  * {@link #lookupButton(ButtonType)} method with the ButtonType (assuming it has
 132  * already been set in the {@link #getButtonTypes()} list. The returned Node is
 133  * typically of type {@link Button}, but this depends on if the
 134  * {@link #createButton(ButtonType)} method has been overridden.
 135  *
 136  * <p>The DialogPane class offers a few methods that can be overridden by
 137  * subclasses, to more easily enable custom functionality. These methods include
 138  * the following:
 139  *
 140  * <ul>
 141  *   <li>{@link #createButton(ButtonType)}
 142  *   <li>{@link #createDetailsButton()}
 143  *   <li>{@link #createButtonBar()}
 144  * </ul>
 145  *
 146  * <p>These methods are documented, so please take note of the expectations
 147  * placed on any developer who wishes to override these methods with their own
 148  * functionality.
 149  *
 150  * @see Dialog
 151  * @since JavaFX 8u40
 152  */
 153 @DefaultProperty("buttonTypes")
 154 public class DialogPane extends Pane {
 155 
 156     /**************************************************************************
 157      *
 158      * Static fields
 159      *
 160      **************************************************************************/
 161 
 162     /**
 163      * Creates a Label node that works well within a Dialog.
 164      * @param text The text to display
 165      */
 166     static Label createContentLabel(String text) {
 167         Label label = new Label(text);
 168         label.setMaxWidth(Double.MAX_VALUE);
 169         label.setMaxHeight(Double.MAX_VALUE);
 170         label.getStyleClass().add("content");
 171         label.setWrapText(true);
 172         label.setPrefWidth(360);
 173         return label;
 174     }
 175 
 176 
 177 
 178     /**************************************************************************
 179      *
 180      * Private fields
 181      *
 182      **************************************************************************/
 183 
 184     private final GridPane headerTextPanel;
 185     private final Label contentLabel;
 186     private final StackPane graphicContainer;
 187     private final Node buttonBar;
 188 
 189     private final ObservableList<ButtonType> buttons = FXCollections.observableArrayList();
 190 
 191     private final Map<ButtonType, Node> buttonNodes = new WeakHashMap<>();
 192 
 193     private Node detailsButton;
 194 
 195     // this is not a property - we have a package-scope setDialog method that
 196     // sets this field. It is set by Dialog if the DialogPane is set inside a Dialog.
 197     private Dialog<?> dialog;
 198 
 199 
 200 
 201     /**************************************************************************
 202      *
 203      * Constructors
 204      *
 205      **************************************************************************/
 206 
 207     /**
 208      * Creates a new DialogPane instance with a style class of 'dialog-pane'.
 209      */
 210     public DialogPane() {
 211         getStyleClass().add("dialog-pane");
 212 
 213         headerTextPanel = new GridPane();
 214         getChildren().add(headerTextPanel);
 215 
 216         graphicContainer = new StackPane();
 217 
 218         contentLabel = createContentLabel("");
 219         getChildren().add(contentLabel);
 220 
 221         buttonBar = createButtonBar();
 222         if (buttonBar != null) {
 223             getChildren().add(buttonBar);
 224         }
 225 
 226         buttons.addListener((ListChangeListener<ButtonType>) c -> {
 227             while (c.next()) {
 228                 if (c.wasRemoved()) {
 229                     for (ButtonType cmd : c.getRemoved()) {
 230                         buttonNodes.remove(cmd);
 231                     }
 232                 }
 233                 if (c.wasAdded()) {
 234                     for (ButtonType cmd : c.getAddedSubList()) {
 235                         if (! buttonNodes.containsKey(cmd)) {
 236                             buttonNodes.put(cmd, createButton(cmd));
 237                         }
 238                     }
 239                 }
 240             }
 241         });
 242     }
 243 
 244 
 245 
 246     /**************************************************************************
 247      *
 248      * Properties
 249      *
 250      **************************************************************************/
 251 
 252     // --- graphic
 253     private final ObjectProperty<Node> graphicProperty = new StyleableObjectProperty<Node>() {
 254         // The graphic is styleable by css, but it is the
 255         // imageUrlProperty that handles the style value.
 256         @Override public CssMetaData getCssMetaData() {
 257             return StyleableProperties.GRAPHIC;
 258         }
 259 
 260         @Override public Object getBean() {
 261             return DialogPane.this;
 262         }
 263 
 264         @Override public String getName() {
 265             return "graphic";
 266         }
 267 
 268         WeakReference<Node> graphicRef = new WeakReference<>(null);
 269 
 270         protected void invalidated() {
 271             Node oldGraphic = graphicRef.get();
 272             if (oldGraphic != null) {
 273                 getChildren().remove(oldGraphic);
 274             }
 275 
 276             Node newGraphic = getGraphic();
 277             graphicRef = new WeakReference<>(newGraphic);
 278             updateHeaderArea();
 279         }
 280     };
 281 
 282     /**
 283      * The dialog graphic, presented either in the header, if one is showing, or
 284      * to the left of the {@link #contentProperty() content}.
 285      *
 286      * @return An ObjectProperty wrapping the current graphic.
 287      */
 288     public final ObjectProperty<Node> graphicProperty() {
 289         return graphicProperty;
 290     }
 291 
 292     public final Node getGraphic() {
 293         return graphicProperty.get();
 294     }
 295 
 296     /**
 297      * Sets the dialog graphic, which will be displayed either in the header, if
 298      * one is showing, or to the left of the {@link #contentProperty() content}.
 299      *
 300      * @param graphic
 301      *            The new dialog graphic, or null if no graphic should be shown.
 302      */
 303     public final void setGraphic(Node graphic) {
 304         this.graphicProperty.set(graphic);
 305     }
 306 
 307 
 308     // --- imageUrl (this is NOT public API, except via CSS)
 309     // Note that this code is a copy/paste from Labeled
 310     private StyleableStringProperty imageUrl = null;
 311     /**
 312      * The imageUrl property is set from CSS and then the graphic property is
 313      * set from the invalidated method. This ensures that the same image isn't
 314      * reloaded.
 315      */
 316     private StyleableStringProperty imageUrlProperty() {
 317         if (imageUrl == null) {
 318             imageUrl = new StyleableStringProperty() {
 319                 //
 320                 // If imageUrlProperty is invalidated, this is the origin of the style that
 321                 // triggered the invalidation. This is used in the invalidated() method where the
 322                 // value of super.getStyleOrigin() is not valid until after the call to set(v) returns,
 323                 // by which time invalidated will have been called.
 324                 // This value is initialized to USER in case someone calls set on the imageUrlProperty, which
 325                 // is possible:
 326                 //     CssMetaData metaData = ((StyleableProperty)dialogPane.graphicProperty()).getCssMetaData();
 327                 //     StyleableProperty prop = metaData.getStyleableProperty(dialogPane);
 328                 //     prop.set(someUrl);
 329                 //
 330                 // TODO: Note that prop != dialogPane, which violates the contract between StyleableProperty and CssMetaData.
 331                 //
 332                 StyleOrigin origin = StyleOrigin.USER;
 333 
 334                 @Override
 335                 public void applyStyle(StyleOrigin origin, String v) {
 336                     this.origin = origin;
 337 
 338                     // Don't want applyStyle to throw an exception which would leave this.origin set to the wrong value
 339                     if (graphicProperty == null || graphicProperty.isBound() == false) super.applyStyle(origin, v);
 340 
 341                     // Origin is only valid for this invocation of applyStyle, so reset it to USER in case someone calls set.
 342                     this.origin = StyleOrigin.USER;
 343                 }
 344 
 345                 @Override
 346                 protected void invalidated() {
 347                     // need to call super.get() here since get() is overridden to return the graphicProperty's value
 348                     final String url = super.get();
 349 
 350                     if (url == null) {
 351                         ((StyleableProperty<Node>)(WritableValue<Node>)graphicProperty()).applyStyle(origin, null);
 352                     } else {
 353                         // RT-34466 - if graphic's url is the same as this property's value, then don't overwrite.
 354                         final Node graphicNode = DialogPane.this.getGraphic();
 355                         if (graphicNode instanceof ImageView) {
 356                             final ImageView imageView = (ImageView)graphicNode;
 357                             final Image image = imageView.getImage();
 358                             if (image != null) {
 359                                 final String imageViewUrl = image.getUrl();
 360                                 if (url.equals(imageViewUrl)) return;
 361                             }
 362 
 363                         }
 364 
 365                         final Image img = StyleManager.getInstance().getCachedImage(url);
 366 
 367                         if (img != null) {
 368                             //
 369                             // Note that it is tempting to try to re-use existing ImageView simply by setting
 370                             // the image on the current ImageView, if there is one. This would effectively change
 371                             // the image, but not the ImageView which means that no graphicProperty listeners would
 372                             // be notified. This is probably not what we want.
 373                             //
 374 
 375                             //
 376                             // Have to call applyStyle on graphicProperty so that the graphicProperty's
 377                             // origin matches the imageUrlProperty's origin.
 378                             //
 379                             ((StyleableProperty<Node>)(WritableValue<Node>)graphicProperty()).applyStyle(origin, new ImageView(img));
 380                         }
 381                     }
 382                 }
 383 
 384                 @Override
 385                 public String get() {
 386                     //
 387                     // The value of the imageUrlProperty is that of the graphicProperty.
 388                     // Return the value in a way that doesn't expand the graphicProperty.
 389                     //
 390                     final Node graphic = getGraphic();
 391                     if (graphic instanceof ImageView) {
 392                         final Image image = ((ImageView)graphic).getImage();
 393                         if (image != null) {
 394                             return image.getUrl();
 395                         }
 396                     }
 397                     return null;
 398                 }
 399 
 400                 @Override
 401                 public StyleOrigin getStyleOrigin() {
 402                     //
 403                     // The origin of the imageUrlProperty is that of the graphicProperty.
 404                     // Return the origin in a way that doesn't expand the graphicProperty.
 405                     //
 406                     return graphicProperty != null ? ((StyleableProperty<Node>)(WritableValue<Node>)graphicProperty).getStyleOrigin() : null;
 407                 }
 408 
 409                 @Override
 410                 public Object getBean() {
 411                     return DialogPane.this;
 412                 }
 413 
 414                 @Override
 415                 public String getName() {
 416                     return "imageUrl";
 417                 }
 418 
 419                 @Override
 420                 public CssMetaData<DialogPane,String> getCssMetaData() {
 421                     return StyleableProperties.GRAPHIC;
 422                 }
 423 
 424             };
 425         }
 426         return imageUrl;
 427     }
 428 
 429 
 430     // --- header
 431     private final ObjectProperty<Node> header = new SimpleObjectProperty<Node>(null) {
 432         WeakReference<Node> headerRef = new WeakReference<>(null);
 433         @Override protected void invalidated() {
 434             Node oldHeader = headerRef.get();
 435             if (oldHeader != null) {
 436                 getChildren().remove(oldHeader);
 437             }
 438 
 439             Node newHeader = getHeader();
 440             headerRef = new WeakReference<>(newHeader);
 441             updateHeaderArea();
 442         }
 443     };
 444 
 445     /**
 446      * Node which acts as the dialog pane header.
 447      *
 448      * @return the header of the dialog pane.
 449      */
 450     public final Node getHeader() {
 451         return header.get();
 452     }
 453 
 454     /**
 455      * Assigns the dialog pane header. Any Node can be used.
 456      *
 457      * @param header The new header of the DialogPane.
 458      */
 459     public final void setHeader(Node header) {
 460         this.header.setValue(header);
 461     }
 462 
 463     /**
 464      * Property representing the header area of the dialog pane. Note that if this
 465      * header is set to a non-null value, that it will take up the entire top
 466      * area of the DialogPane. It will also result in the DialogPane switching its
 467      * layout to the 'header' layout - as outlined in the {@link DialogPane} class
 468      * javadoc.
 469      */
 470     public final ObjectProperty<Node> headerProperty() {
 471         return header;
 472     }
 473 
 474 
 475 
 476     // --- header text
 477     private final StringProperty headerText = new SimpleStringProperty(this, "headerText") {
 478         @Override protected void invalidated() {
 479             updateHeaderArea();
 480             requestLayout();
 481         }
 482     };
 483 
 484     /**
 485      * Sets the string to show in the dialog header area. Note that the header text
 486      * is lower precedence than the {@link #headerProperty() header node}, meaning
 487      * that if both the header node and the headerText properties are set, the
 488      * header text will not be displayed in a default DialogPane instance.
 489      *
 490      * <p>When headerText is set to a non-null value, this will result in the
 491      * DialogPane switching its layout to the 'header' layout - as outlined in
 492      * the {@link DialogPane} class javadoc.</p>
 493      */
 494     public final void setHeaderText(String headerText) {
 495         this.headerText.set(headerText);
 496     }
 497 
 498     /**
 499      * Returns the currently-set header text for this DialogPane.
 500      */
 501     public final String getHeaderText() {
 502         return headerText.get();
 503     }
 504 
 505     /**
 506      * A property representing the header text for the dialog pane. The header text
 507      * is lower precedence than the {@link #headerProperty() header node}, meaning
 508      * that if both the header node and the headerText properties are set, the
 509      * header text will not be displayed in a default DialogPane instance.
 510      *
 511      * <p>When headerText is set to a non-null value, this will result in the
 512      * DialogPane switching its layout to the 'header' layout - as outlined in
 513      * the {@link DialogPane} class javadoc.</p>
 514      */
 515     public final StringProperty headerTextProperty() {
 516         return headerText;
 517     }
 518 
 519 
 520     // --- content
 521     private final ObjectProperty<Node> content = new SimpleObjectProperty<Node>(null) {
 522         WeakReference<Node> contentRef = new WeakReference<>(null);
 523         @Override protected void invalidated() {
 524             Node oldContent = contentRef.get();
 525             if (oldContent != null) {
 526                 getChildren().remove(oldContent);
 527             }
 528 
 529             Node newContent = getContent();
 530             contentRef = new WeakReference<>(newContent);
 531             updateContentArea();
 532         }
 533     };
 534 
 535     /**
 536      * Returns the dialog content as a Node (even if it was set as a String
 537      * using {@link #setContentText(String)} - this was simply transformed into a
 538      * {@link Node} (most probably a {@link Label}).
 539      *
 540      * @return dialog's content
 541      */
 542     public final Node getContent() {
 543         return content.get();
 544     }
 545 
 546     /**
 547      * Assign dialog content. Any Node can be used
 548      *
 549      * @param content
 550      *            dialog's content
 551      */
 552     public final void setContent(Node content) {
 553         this.content.setValue(content);
 554     }
 555 
 556     /**
 557      * Property representing the content area of the dialog.
 558      */
 559     public final ObjectProperty<Node> contentProperty() {
 560         return content;
 561     }
 562 
 563 
 564     // --- content text
 565     private final StringProperty contentText = new SimpleStringProperty(this, "contentText") {
 566         @Override protected void invalidated() {
 567             updateContentArea();
 568             requestLayout();
 569         }
 570     };
 571 
 572     /**
 573      * Sets the string to show in the dialog content area. Note that the content text
 574      * is lower precedence than the {@link #contentProperty() content node}, meaning
 575      * that if both the content node and the contentText properties are set, the
 576      * content text will not be displayed in a default DialogPane instance.
 577      */
 578     public final void setContentText(String contentText) {
 579         this.contentText.set(contentText);
 580     }
 581 
 582     /**
 583      * Returns the currently-set content text for this DialogPane.
 584      */
 585     public final String getContentText() {
 586         return contentText.get();
 587     }
 588 
 589     /**
 590      * A property representing the content text for the dialog pane. The content text
 591      * is lower precedence than the {@link #contentProperty() content node}, meaning
 592      * that if both the content node and the contentText properties are set, the
 593      * content text will not be displayed in a default DialogPane instance.
 594      */
 595     public final StringProperty contentTextProperty() {
 596         return contentText;
 597     }
 598 
 599 
 600     // --- expandable content
 601     private final ObjectProperty<Node> expandableContentProperty = new SimpleObjectProperty<Node>(null) {
 602         WeakReference<Node> expandableContentRef = new WeakReference<>(null);
 603         @Override protected void invalidated() {
 604             Node oldExpandableContent = expandableContentRef.get();
 605             if (oldExpandableContent != null) {
 606                 getChildren().remove(oldExpandableContent);
 607             }
 608 
 609             Node newExpandableContent = getExpandableContent();
 610             expandableContentRef = new WeakReference<Node>(newExpandableContent);
 611             if (newExpandableContent != null) {
 612                 newExpandableContent.setVisible(isExpanded());
 613                 newExpandableContent.setManaged(isExpanded());
 614 
 615                 if (!newExpandableContent.getStyleClass().contains("expandable-content")) { //$NON-NLS-1$
 616                     newExpandableContent.getStyleClass().add("expandable-content"); //$NON-NLS-1$
 617                 }
 618 
 619                 getChildren().add(newExpandableContent);
 620             }
 621         }
 622     };
 623 
 624     /**
 625      * A property that represents the dialog expandable content area. Any Node
 626      * can be placed in this area, but it will only be shown when the user
 627      * clicks the 'Show Details' expandable button. This button will be added
 628      * automatically when the expandable content property is non-null.
 629      */
 630     public final ObjectProperty<Node> expandableContentProperty() {
 631         return expandableContentProperty;
 632     }
 633 
 634     /**
 635      * Returns the dialog expandable content node, if one is set, or null
 636      * otherwise.
 637      */
 638     public final Node getExpandableContent() {
 639         return expandableContentProperty.get();
 640     }
 641 
 642     /**
 643      * Sets the dialog expandable content node, or null if no expandable content
 644      * needs to be shown.
 645      */
 646     public final void setExpandableContent(Node content) {
 647         this.expandableContentProperty.set(content);
 648     }
 649 
 650 
 651     // --- expanded
 652     private final BooleanProperty expandedProperty = new SimpleBooleanProperty(this, "expanded", false) {
 653         protected void invalidated() {
 654             final Node expandableContent = getExpandableContent();
 655 
 656             if (expandableContent != null) {
 657                 expandableContent.setVisible(isExpanded());
 658             }
 659 
 660             requestLayout();
 661         }
 662     };
 663 
 664     /**
 665      * Represents whether the dialogPane is expanded.
 666      */
 667     public final BooleanProperty expandedProperty() {
 668         return expandedProperty;
 669     }
 670 
 671     /**
 672      * Returns whether or not the dialogPane is expanded.
 673      *
 674      * @return true if dialogPane is expanded.
 675      */
 676     public final boolean isExpanded() {
 677         return expandedProperty().get();
 678     }
 679 
 680     /**
 681      * Sets whether the dialogPane is expanded. This only makes sense when there
 682      * is {@link #expandableContentProperty() expandable content} to show.
 683      *
 684      * @param value true if dialogPane should be expanded.
 685      */
 686     public final void setExpanded(boolean value) {
 687         expandedProperty().set(value);
 688     }
 689 
 690 
 691 
 692     /**************************************************************************
 693      *
 694      * Public API
 695      *
 696      **************************************************************************/
 697 
 698     // --- button types
 699     /**
 700      * Observable list of button types used for the dialog button bar area
 701      * (created via the {@link #createButtonBar()} method). Modifying the contents
 702      * of this list will immediately change the buttons displayed to the user
 703      * within the dialog pane.
 704      *
 705      * @return The {@link ObservableList} of {@link ButtonType button types}
 706      *         available to the user.
 707      */
 708     public final ObservableList<ButtonType> getButtonTypes() {
 709         return buttons;
 710     }
 711 
 712     /**
 713      * This method provides a way in which developers may retrieve the actual
 714      * Node for a given {@link ButtonType} (assuming it is part of the
 715      * {@link #getButtonTypes() button types} list).
 716      *
 717      * @param buttonType The {@link ButtonType} for which a Node representation is requested.
 718      * @return The Node used to represent the button type, as created by
 719      *         {@link #createButton(ButtonType)}, and only if the button type
 720      *         is part of the {@link #getButtonTypes() button types} list, otherwise null.
 721      */
 722     public final Node lookupButton(ButtonType buttonType) {
 723         return buttonNodes.get(buttonType);
 724     }
 725 
 726     /**
 727      * This method can be overridden by subclasses to provide the button bar.
 728      * Note that by overriding this method, the developer must take on multiple
 729      * responsibilities:
 730      *
 731      * <ol>
 732      *   <li>The developer must immediately iterate through all
 733      *   {@link #getButtonTypes() button types} and call
 734      *   {@link #createButton(ButtonType)} for each of them in turn.
 735      *   <li>The developer must add a listener to the
 736      *   {@link #getButtonTypes() button types} list, and when this list changes
 737      *   update the button bar as appropriate.
 738      *   <li>Similarly, the developer must watch for changes to the
 739      *   {@link #expandableContentProperty() expandable content} property,
 740      *   adding and removing the details button (created via
 741      *   {@link #createDetailsButton()} method).
 742      * </ol>
 743      *
 744      * <p>The default implementation of this method creates and returns a new
 745      * {@link ButtonBar} instance.
 746      */
 747     protected Node createButtonBar() {
 748         ButtonBar buttonBar = new ButtonBar();
 749         buttonBar.setMaxWidth(Double.MAX_VALUE);
 750 
 751         updateButtons(buttonBar);
 752         getButtonTypes().addListener((ListChangeListener<? super ButtonType>) c -> updateButtons(buttonBar));
 753         expandableContentProperty().addListener(o -> updateButtons(buttonBar));
 754 
 755         return buttonBar;
 756     }
 757 
 758     /**
 759      * This method can be overridden by subclasses to create a custom button that
 760      * will subsequently inserted into the DialogPane button area (created via
 761      * the {@link #createButtonBar()} method, but mostly commonly it is an instance
 762      * of {@link ButtonBar}.
 763      *
 764      * @param buttonType The {@link ButtonType} to create a button from.
 765      * @return A JavaFX {@link Node} that represents the given {@link ButtonType},
 766      *         most commonly an instance of {@link Button}.
 767      */
 768     protected Node createButton(ButtonType buttonType) {
 769         final Button button = new Button(buttonType.getText());
 770         final ButtonData buttonData = buttonType.getButtonData();
 771         ButtonBar.setButtonData(button, buttonData);
 772         button.setDefaultButton(buttonData.isDefaultButton());
 773         button.setCancelButton(buttonData.isCancelButton());
 774         button.addEventHandler(ActionEvent.ACTION, ae -> {
 775             if (ae.isConsumed()) return;
 776             if (dialog != null) {
 777                 dialog.setResultAndClose(buttonType, true);
 778             }
 779         });
 780 
 781         return button;
 782     }
 783 
 784     /**
 785      * This method can be overridden by subclasses to create a custom details button.
 786      *
 787      * <p>To override this method you must do two things:
 788      * <ol>
 789      *   <li>The button will need to have its own code set to handle mouse / keyboard
 790      *       interaction and to toggle the state of the
 791      *       {@link #expandedProperty() expanded} property.
 792      *   <li>If your button changes its visuals based on whether the dialog pane
 793      *       is expanded or collapsed, you should add a listener to the
 794      *       {@link #expandedProperty() expanded} property, so that you may update
 795      *       the button visuals.
 796      * </ol>
 797      */
 798     protected Node createDetailsButton() {
 799         final Hyperlink detailsButton = new Hyperlink();
 800         final String moreText = ControlResources.getString("Dialog.detail.button.more"); //$NON-NLS-1$
 801         final String lessText = ControlResources.getString("Dialog.detail.button.less"); //$NON-NLS-1$
 802 
 803         InvalidationListener expandedListener = o -> {
 804             final boolean isExpanded = isExpanded();
 805             detailsButton.setText(isExpanded ? lessText : moreText);
 806             detailsButton.getStyleClass().setAll("details-button", (isExpanded ? "less" : "more")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
 807         };
 808 
 809         // we call the listener immediately to ensure the state is correct at start up
 810         expandedListener.invalidated(null);
 811         expandedProperty().addListener(expandedListener);
 812 
 813         detailsButton.setOnAction(ae -> setExpanded(!isExpanded()));
 814         return detailsButton;
 815     }
 816 
 817     private double oldHeight = -1;
 818 
 819     /** {@inheritDoc} */
 820     @Override protected void layoutChildren() {
 821         final boolean hasHeader = hasHeader();
 822 
 823         // snapped insets code commented out to resolve RT-39738
 824         final double w = Math.max(minWidth(-1), getWidth());// - (snappedLeftInset() + snappedRightInset());
 825 
 826         final double minHeight = minHeight(w);
 827         final double prefHeight = prefHeight(w);
 828         final double maxHeight = maxHeight(w);
 829         final double currentHeight = getHeight();
 830         final double dialogHeight = dialog == null ? 0 : dialog.dialog.getSceneHeight();
 831         double h;
 832 
 833         if (prefHeight > currentHeight && prefHeight > minHeight && (prefHeight <= dialogHeight || dialogHeight == 0)) {
 834             h = prefHeight;
 835             resize(w, h);
 836         } else {
 837             boolean isDialogGrowing = currentHeight > oldHeight;
 838 
 839             if (isDialogGrowing) {
 840                 double _h = currentHeight < prefHeight ?
 841                         Math.min(prefHeight, currentHeight) : Math.max(prefHeight, dialogHeight);
 842                 h = Utils.boundedSize(_h, minHeight, maxHeight);
 843             } else {
 844                 h = Utils.boundedSize(Math.min(currentHeight, dialogHeight), minHeight, maxHeight);
 845             }
 846             resize(w, h);
 847         }
 848 
 849         h -= (snappedTopInset() + snappedBottomInset());
 850 
 851         oldHeight = h;
 852 
 853         final double leftPadding = snappedLeftInset();
 854         final double topPadding = snappedTopInset();
 855         final double rightPadding = snappedRightInset();
 856 
 857         // create the nodes up front so we can work out sizing
 858         final Node header = getActualHeader();
 859         final Node content = getActualContent();
 860         final Node graphic = getActualGraphic();
 861         final Node expandableContent = getExpandableContent();
 862 
 863         final double graphicPrefWidth = hasHeader || graphic == null ? 0 : graphic.prefWidth(-1);
 864         final double headerPrefHeight = hasHeader ? header.prefHeight(w) : 0;
 865         final double buttonBarPrefHeight = buttonBar == null ? 0 : buttonBar.prefHeight(w);
 866         final double graphicPrefHeight = hasHeader || graphic == null ? 0 : graphic.prefHeight(-1);
 867 
 868         final double expandableContentPrefHeight;
 869         final double contentAreaHeight;
 870         final double contentAndGraphicHeight;
 871 
 872         final double availableContentWidth = w - graphicPrefWidth - leftPadding - rightPadding;
 873 
 874         if (isExpanded()) {
 875             // precedence goes to content and then expandable content
 876             contentAreaHeight = isExpanded() ? content.prefHeight(availableContentWidth) : 0;
 877             contentAndGraphicHeight = hasHeader ? contentAreaHeight : Math.max(graphicPrefHeight, contentAreaHeight);
 878             expandableContentPrefHeight = h - (headerPrefHeight + contentAndGraphicHeight + buttonBarPrefHeight);
 879         } else {
 880             // content gets the lowest precedence
 881             expandableContentPrefHeight = isExpanded() ? expandableContent.prefHeight(w) : 0;
 882             contentAreaHeight = h - (headerPrefHeight + expandableContentPrefHeight + buttonBarPrefHeight);
 883             contentAndGraphicHeight = hasHeader ? contentAreaHeight : Math.max(graphicPrefHeight, contentAreaHeight);
 884         }
 885 
 886         double x = leftPadding;
 887         double y = topPadding;
 888 
 889         if (! hasHeader) {
 890             if (graphic != null) {
 891                 graphic.resizeRelocate(x, y, graphicPrefWidth, graphicPrefHeight);
 892                 x += graphicPrefWidth;
 893             }
 894         } else {
 895             header.resizeRelocate(x, y, w - (leftPadding + rightPadding), headerPrefHeight);
 896             y += headerPrefHeight;
 897         }
 898 
 899         content.resizeRelocate(x, y, availableContentWidth, contentAreaHeight);
 900         y += hasHeader ? contentAreaHeight : contentAndGraphicHeight;
 901 
 902         if (expandableContent != null) {
 903             expandableContent.resizeRelocate(leftPadding, y, w - rightPadding, expandableContentPrefHeight);
 904             y += expandableContentPrefHeight;
 905         }
 906 
 907         if (buttonBar != null) {
 908             buttonBar.resizeRelocate(leftPadding,
 909                                      y,
 910                                      w - (leftPadding + rightPadding),
 911                                      buttonBarPrefHeight);
 912         }
 913     }
 914 
 915     /** {@inheritDoc} */
 916     @Override protected double computeMinWidth(double height) {
 917         double headerMinWidth = hasHeader() ? getActualHeader().minWidth(height) + 10 : 0;
 918         double contentMinWidth = getActualContent().minWidth(height);
 919         double buttonBarMinWidth = buttonBar == null ? 0 : buttonBar.minWidth(height);
 920         double graphicMinWidth = getActualGraphic().minWidth(height);
 921 
 922         double expandableContentMinWidth = 0;
 923         final Node expandableContent = getExpandableContent();
 924         if (isExpanded() && expandableContent != null) {
 925             expandableContentMinWidth = expandableContent.minWidth(height);
 926         }
 927 
 928         double minWidth = snappedLeftInset() +
 929                 (hasHeader() ? 0 : graphicMinWidth) +
 930                 Math.max(Math.max(headerMinWidth, expandableContentMinWidth), Math.max(contentMinWidth, buttonBarMinWidth)) +
 931                 snappedRightInset();
 932 
 933         return snapSize(minWidth);
 934     }
 935 
 936     /** {@inheritDoc} */
 937     @Override protected double computeMinHeight(double width) {
 938         final boolean hasHeader = hasHeader();
 939 
 940         double headerMinHeight = hasHeader ? getActualHeader().minHeight(width) : 0;
 941         double buttonBarMinHeight = buttonBar == null ? 0 : buttonBar.minHeight(width);
 942 
 943         Node graphic = getActualGraphic();
 944         double graphicMinWidth = hasHeader ? 0 : graphic.minWidth(-1);
 945         double graphicMinHeight = hasHeader ? 0 : graphic.minHeight(width);
 946 
 947         // min height of a label is based on one line (wrapping is ignored)
 948         Node content = getActualContent();
 949         double contentAvailableWidth = width == Region.USE_COMPUTED_SIZE ? Region.USE_COMPUTED_SIZE :
 950                 hasHeader ? width : (width - graphicMinWidth);
 951         double contentMinHeight = content.minHeight(contentAvailableWidth);
 952 
 953         double expandableContentMinHeight = 0;
 954         final Node expandableContent = getExpandableContent();
 955         if (isExpanded() && expandableContent != null) {
 956             expandableContentMinHeight = expandableContent.minHeight(width);
 957         }
 958 
 959         double minHeight = snappedTopInset() +
 960                 headerMinHeight +
 961                 Math.max(graphicMinHeight, contentMinHeight) +
 962                 expandableContentMinHeight +
 963                 buttonBarMinHeight +
 964                 snappedBottomInset();
 965 
 966         return snapSize(minHeight);
 967     }
 968 
 969     /** {@inheritDoc} */
 970     @Override protected double computePrefWidth(double height) {
 971         double headerPrefWidth = hasHeader() ? getActualHeader().prefWidth(height) + 10 : 0;
 972         double contentPrefWidth = getActualContent().prefWidth(height);
 973         double buttonBarPrefWidth = buttonBar == null ? 0 : buttonBar.prefWidth(height);
 974         double graphicPrefWidth = getActualGraphic().prefWidth(height);
 975 
 976         double expandableContentPrefWidth = 0;
 977         final Node expandableContent = getExpandableContent();
 978         if (isExpanded() && expandableContent != null) {
 979             expandableContentPrefWidth = expandableContent.prefWidth(height);
 980         }
 981 
 982         double prefWidth = snappedLeftInset() +
 983                (hasHeader() ? 0 : graphicPrefWidth) +
 984                Math.max(Math.max(headerPrefWidth, expandableContentPrefWidth), Math.max(contentPrefWidth, buttonBarPrefWidth)) +
 985                snappedRightInset();
 986 
 987         return snapSize(prefWidth);
 988     }
 989 
 990     /** {@inheritDoc} */
 991     @Override protected double computePrefHeight(double width) {
 992         final boolean hasHeader = hasHeader();
 993 
 994         double headerPrefHeight = hasHeader ? getActualHeader().prefHeight(width) : 0;
 995         double buttonBarPrefHeight = buttonBar == null ? 0 : buttonBar.prefHeight(width);
 996 
 997         Node graphic = getActualGraphic();
 998         double graphicPrefWidth = hasHeader ? 0 : graphic.prefWidth(-1);
 999         double graphicPrefHeight = hasHeader ? 0 : graphic.prefHeight(width);
1000 
1001         Node content = getActualContent();
1002         double contentAvailableWidth = width == Region.USE_COMPUTED_SIZE ? Region.USE_COMPUTED_SIZE :
1003                 hasHeader ? width : (width - graphicPrefWidth);
1004         double contentPrefHeight = content.prefHeight(contentAvailableWidth);
1005 
1006         double expandableContentPrefHeight = 0;
1007         final Node expandableContent = getExpandableContent();
1008         if (isExpanded() && expandableContent != null) {
1009             expandableContentPrefHeight = expandableContent.prefHeight(width);
1010         }
1011 
1012         double prefHeight = snappedTopInset() +
1013                headerPrefHeight +
1014                Math.max(graphicPrefHeight, contentPrefHeight) +
1015                expandableContentPrefHeight +
1016                buttonBarPrefHeight +
1017                snappedBottomInset();
1018 
1019         return snapSize(prefHeight);
1020     }
1021 
1022 
1023 
1024     /**************************************************************************
1025      *
1026      * Private implementation
1027      * @param buttonBar
1028      *
1029      **************************************************************************/
1030 
1031     private void updateButtons(ButtonBar buttonBar) {
1032         buttonBar.getButtons().clear();
1033 
1034         // show details button if expandable content is present
1035         if (hasExpandableContent()) {
1036             if (detailsButton == null) {
1037                 detailsButton = createDetailsButton();
1038             }
1039             ButtonBar.setButtonData(detailsButton, ButtonData.HELP_2);
1040             buttonBar.getButtons().add(detailsButton);
1041             ButtonBar.setButtonUniformSize(detailsButton, false);
1042         }
1043 
1044         boolean hasDefault = false;
1045         for (ButtonType cmd : getButtonTypes()) {
1046             Node button = buttonNodes.computeIfAbsent(cmd, dialogButton -> createButton(cmd));
1047 
1048             // keep only first default button
1049             if (button instanceof Button) {
1050                 ButtonData buttonType = cmd.getButtonData();
1051 
1052                 ((Button)button).setDefaultButton(!hasDefault && buttonType != null && buttonType.isDefaultButton());
1053                 ((Button)button).setCancelButton(buttonType != null && buttonType.isCancelButton());
1054 
1055                 hasDefault |= buttonType != null && buttonType.isDefaultButton();
1056             }
1057             buttonBar.getButtons().add(button);
1058         }
1059     }
1060 
1061     private Node getActualContent() {
1062         Node content = getContent();
1063         return content == null ? contentLabel : content;
1064     }
1065 
1066     private Node getActualHeader() {
1067         Node header = getHeader();
1068         return header == null ? headerTextPanel : header;
1069     }
1070 
1071     private Node getActualGraphic() {
1072         return headerTextPanel;
1073     }
1074 
1075     private void updateHeaderArea() {
1076         Node header = getHeader();
1077         if (header != null) {
1078             if (! getChildren().contains(header)) {
1079                 getChildren().add(header);
1080             }
1081 
1082             headerTextPanel.setVisible(false);
1083             headerTextPanel.setManaged(false);
1084         } else {
1085             final String headerText = getHeaderText();
1086 
1087             headerTextPanel.getChildren().clear();
1088             headerTextPanel.getStyleClass().clear();
1089 
1090             // recreate the headerTextNode and add it to the children list.
1091             headerTextPanel.setMaxWidth(Double.MAX_VALUE);
1092 
1093             if (headerText != null && ! headerText.isEmpty()) {
1094                 headerTextPanel.getStyleClass().add("header-panel"); //$NON-NLS-1$
1095             }
1096 
1097             // on left of header is the text
1098             Label headerLabel = new Label(headerText);
1099             headerLabel.setWrapText(true);
1100             headerLabel.setAlignment(Pos.CENTER_LEFT);
1101             headerLabel.setMaxWidth(Double.MAX_VALUE);
1102             headerLabel.setMaxHeight(Double.MAX_VALUE);
1103             headerTextPanel.add(headerLabel, 0, 0);
1104 
1105             // on the right of the header is a graphic, if one is specified
1106             graphicContainer.getChildren().clear();
1107 
1108             if (! graphicContainer.getStyleClass().contains("graphic-container")) { //$NON-NLS-1$)
1109                 graphicContainer.getStyleClass().add("graphic-container"); //$NON-NLS-1$
1110             }
1111 
1112             final Node graphic = getGraphic();
1113             if (graphic != null) {
1114                 graphicContainer.getChildren().add(graphic);
1115             }
1116             headerTextPanel.add(graphicContainer, 1, 0);
1117 
1118             // column constraints
1119             ColumnConstraints textColumn = new ColumnConstraints();
1120             textColumn.setFillWidth(true);
1121             textColumn.setHgrow(Priority.ALWAYS);
1122             ColumnConstraints graphicColumn = new ColumnConstraints();
1123             graphicColumn.setFillWidth(false);
1124             graphicColumn.setHgrow(Priority.NEVER);
1125             headerTextPanel.getColumnConstraints().setAll(textColumn , graphicColumn);
1126 
1127             headerTextPanel.setVisible(true);
1128             headerTextPanel.setManaged(true);
1129         }
1130     }
1131 
1132     private void updateContentArea() {
1133         Node content = getContent();
1134         if (content != null) {
1135             if (! getChildren().contains(content)) {
1136                 getChildren().add(content);
1137             }
1138 
1139             if (! content.getStyleClass().contains("content")) {
1140                 content.getStyleClass().add("content");
1141             }
1142 
1143             contentLabel.setVisible(false);
1144             contentLabel.setManaged(false);
1145         } else {
1146             final String contentText = getContentText();
1147             final boolean visible = contentText != null && !contentText.isEmpty();
1148             contentLabel.setText(visible ? contentText : "");
1149             contentLabel.setVisible(visible);
1150             contentLabel.setManaged(visible);
1151         }
1152     }
1153 
1154     boolean hasHeader() {
1155         return getHeader() != null || isTextHeader();
1156     }
1157 
1158     private boolean isTextHeader() {
1159         String headerText = getHeaderText();
1160         return headerText != null && !headerText.isEmpty();
1161     }
1162 
1163     boolean hasExpandableContent() {
1164         return getExpandableContent() != null;
1165     }
1166 
1167     void setDialog(Dialog<?> dialog) {
1168         this.dialog = dialog;
1169     }
1170 
1171 
1172 
1173     /***************************************************************************
1174      *                                                                         *
1175      * Stylesheet Handling                                                     *
1176      *                                                                         *
1177      **************************************************************************/
1178 
1179      /**
1180       * @treatAsPrivate implementation detail
1181       */
1182     private static class StyleableProperties {
1183 
1184         private static final CssMetaData<DialogPane,String> GRAPHIC =
1185             new CssMetaData<DialogPane,String>("-fx-graphic",
1186                 StringConverter.getInstance()) {
1187 
1188             @Override
1189             public boolean isSettable(DialogPane n) {
1190                 // Note that we care about the graphic, not imageUrl
1191                 return n.graphicProperty == null || !n.graphicProperty.isBound();
1192             }
1193 
1194             @Override
1195             public StyleableProperty<String> getStyleableProperty(DialogPane n) {
1196                 return n.imageUrlProperty();
1197             }
1198         };
1199 
1200         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
1201         static {
1202             final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Region.getClassCssMetaData());
1203             Collections.addAll(styleables,
1204                 GRAPHIC
1205             );
1206             STYLEABLES = Collections.unmodifiableList(styleables);
1207         }
1208     }
1209 
1210     /**
1211      * @return The CssMetaData associated with this class, which may include the
1212      * CssMetaData of its super classes.
1213      */
1214     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
1215         return StyleableProperties.STYLEABLES;
1216     }
1217 
1218     /** {@inheritDoc} */
1219     @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
1220         return getClassCssMetaData();
1221     }
1222 }