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 com.sun.javafx.css.converters.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.impl_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.impl_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(buttonType != null && buttonData.isDefaultButton());
 773         button.setCancelButton(buttonType != null && buttonData.isCancelButton());
 774         button.addEventHandler(ActionEvent.ACTION, ae -> {
 775             if (ae.isConsumed()) return;
 776             if (dialog != null) {
 777                 dialog.impl_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         final double bottomPadding = snappedBottomInset();
 857         
 858         // create the nodes up front so we can work out sizing
 859         final Node header = getActualHeader();
 860         final Node content = getActualContent();
 861         final Node graphic = getActualGraphic();
 862         final Node expandableContent = getExpandableContent();
 863 
 864         final double graphicPrefWidth = hasHeader || graphic == null ? 0 : graphic.prefWidth(-1);
 865         final double headerPrefHeight = hasHeader ? header.prefHeight(w) : 0;
 866         final double buttonBarPrefHeight = buttonBar == null ? 0 : buttonBar.prefHeight(w);
 867         final double graphicPrefHeight = hasHeader || graphic == null ? 0 : graphic.prefHeight(-1);
 868 
 869         final double expandableContentPrefHeight;
 870         final double contentAreaHeight;
 871         final double contentAndGraphicHeight;
 872 
 873         final double availableContentWidth = w - graphicPrefWidth - leftPadding - rightPadding;
 874 
 875         if (isExpanded()) {
 876             // precedence goes to content and then expandable content
 877             contentAreaHeight = isExpanded() ? content.prefHeight(availableContentWidth) : 0;
 878             contentAndGraphicHeight = hasHeader ? contentAreaHeight : Math.max(graphicPrefHeight, contentAreaHeight);
 879             expandableContentPrefHeight = h - (headerPrefHeight + contentAndGraphicHeight + buttonBarPrefHeight);
 880         } else {
 881             // content gets the lowest precedence
 882             expandableContentPrefHeight = isExpanded() ? expandableContent.prefHeight(w) : 0;
 883             contentAreaHeight = h - (headerPrefHeight + expandableContentPrefHeight + buttonBarPrefHeight);
 884             contentAndGraphicHeight = hasHeader ? contentAreaHeight : Math.max(graphicPrefHeight, contentAreaHeight);
 885         }
 886         
 887         double x = leftPadding;
 888         double y = topPadding;
 889         
 890         if (! hasHeader) {
 891             if (graphic != null) {
 892                 graphic.resizeRelocate(x, y, graphicPrefWidth, graphicPrefHeight);
 893                 x += graphicPrefWidth;
 894             }
 895         } else {
 896             header.resizeRelocate(x, y, w - (leftPadding + rightPadding), headerPrefHeight);
 897             y += headerPrefHeight;
 898         }
 899 
 900         content.resizeRelocate(x, y, availableContentWidth, contentAreaHeight);
 901         y += hasHeader ? contentAreaHeight : contentAndGraphicHeight;
 902         
 903         if (expandableContent != null) {
 904             expandableContent.resizeRelocate(leftPadding, y, w - rightPadding, expandableContentPrefHeight);
 905             y += expandableContentPrefHeight;
 906         }
 907         
 908         if (buttonBar != null) {
 909             buttonBar.resizeRelocate(leftPadding,
 910                                      y,
 911                                      w - (leftPadding + rightPadding),
 912                                      buttonBarPrefHeight);
 913         }
 914     }
 915 
 916     /** {@inheritDoc} */
 917     @Override protected double computeMinWidth(double height) {
 918         double headerMinWidth = hasHeader() ? getActualHeader().minWidth(height) + 10 : 0;
 919         double contentMinWidth = getActualContent().minWidth(height);
 920         double buttonBarMinWidth = buttonBar == null ? 0 : buttonBar.minWidth(height);
 921         double graphicMinWidth = getActualGraphic().minWidth(height);
 922 
 923         double expandableContentMinWidth = 0;
 924         final Node expandableContent = getExpandableContent();
 925         if (isExpanded() && expandableContent != null) {
 926             expandableContentMinWidth = expandableContent.minWidth(height);
 927         }
 928 
 929         double minWidth = snappedLeftInset() +
 930                 (hasHeader() ? 0 : graphicMinWidth) +
 931                 Math.max(Math.max(headerMinWidth, expandableContentMinWidth), Math.max(contentMinWidth, buttonBarMinWidth)) +
 932                 snappedRightInset();
 933 
 934         return snapSize(minWidth);
 935     }
 936 
 937     /** {@inheritDoc} */
 938     @Override protected double computeMinHeight(double width) {
 939         final boolean hasHeader = hasHeader();
 940 
 941         double headerMinHeight = hasHeader ? getActualHeader().minHeight(width) : 0;
 942         double buttonBarMinHeight = buttonBar == null ? 0 : buttonBar.minHeight(width);
 943 
 944         Node graphic = getActualGraphic();
 945         double graphicMinWidth = hasHeader ? 0 : graphic.minWidth(-1);
 946         double graphicMinHeight = hasHeader ? 0 : graphic.minHeight(width);
 947 
 948         // min height of a label is based on one line (wrapping is ignored)
 949         Node content = getActualContent();
 950         double contentAvailableWidth = width == Region.USE_COMPUTED_SIZE ? Region.USE_COMPUTED_SIZE :
 951                 hasHeader ? width : (width - graphicMinWidth);
 952         double contentMinHeight = content.minHeight(contentAvailableWidth);
 953 
 954         double expandableContentMinHeight = 0;
 955         final Node expandableContent = getExpandableContent();
 956         if (isExpanded() && expandableContent != null) {
 957             expandableContentMinHeight = expandableContent.minHeight(width);
 958         }
 959 
 960         double minHeight = snappedTopInset() +
 961                 headerMinHeight +
 962                 Math.max(graphicMinHeight, contentMinHeight) +
 963                 expandableContentMinHeight +
 964                 buttonBarMinHeight +
 965                 snappedBottomInset();
 966 
 967         return snapSize(minHeight);
 968     }
 969     
 970     /** {@inheritDoc} */
 971     @Override protected double computePrefWidth(double height) {
 972         double headerPrefWidth = hasHeader() ? getActualHeader().prefWidth(height) + 10 : 0;
 973         double contentPrefWidth = getActualContent().prefWidth(height);
 974         double buttonBarPrefWidth = buttonBar == null ? 0 : buttonBar.prefWidth(height);
 975         double graphicPrefWidth = getActualGraphic().prefWidth(height);
 976         
 977         double expandableContentPrefWidth = 0;
 978         final Node expandableContent = getExpandableContent();
 979         if (isExpanded() && expandableContent != null) {
 980             expandableContentPrefWidth = expandableContent.prefWidth(height);
 981         }
 982         
 983         double prefWidth = snappedLeftInset() +
 984                (hasHeader() ? 0 : graphicPrefWidth) +
 985                Math.max(Math.max(headerPrefWidth, expandableContentPrefWidth), Math.max(contentPrefWidth, buttonBarPrefWidth)) +
 986                snappedRightInset();
 987 
 988         return snapSize(prefWidth);
 989     }
 990 
 991     /** {@inheritDoc} */
 992     @Override protected double computePrefHeight(double width) {
 993         final boolean hasHeader = hasHeader();
 994 
 995         double headerPrefHeight = hasHeader ? getActualHeader().prefHeight(width) : 0;
 996         double buttonBarPrefHeight = buttonBar == null ? 0 : buttonBar.prefHeight(width);
 997 
 998         Node graphic = getActualGraphic();
 999         double graphicPrefWidth = hasHeader ? 0 : graphic.prefWidth(-1);
1000         double graphicPrefHeight = hasHeader ? 0 : graphic.prefHeight(width);
1001 
1002         Node content = getActualContent();
1003         double contentAvailableWidth = width == Region.USE_COMPUTED_SIZE ? Region.USE_COMPUTED_SIZE :
1004                 hasHeader ? width : (width - graphicPrefWidth);
1005         double contentPrefHeight = content.prefHeight(contentAvailableWidth);
1006 
1007         double expandableContentPrefHeight = 0;
1008         final Node expandableContent = getExpandableContent();
1009         if (isExpanded() && expandableContent != null) {
1010             expandableContentPrefHeight = expandableContent.prefHeight(width);
1011         }
1012         
1013         double prefHeight = snappedTopInset() +
1014                headerPrefHeight + 
1015                Math.max(graphicPrefHeight, contentPrefHeight) + 
1016                expandableContentPrefHeight + 
1017                buttonBarPrefHeight + 
1018                snappedBottomInset();
1019 
1020         return snapSize(prefHeight);
1021     }
1022     
1023     
1024     
1025     /**************************************************************************
1026      * 
1027      * Private implementation
1028      * @param buttonBar 
1029      * 
1030      **************************************************************************/
1031     
1032     private void updateButtons(ButtonBar buttonBar) {
1033         buttonBar.getButtons().clear();
1034         
1035         // show details button if expandable content is present
1036         if (hasExpandableContent()) {
1037             if (detailsButton == null) {
1038                 detailsButton = createDetailsButton();
1039             }
1040             ButtonBar.setButtonData(detailsButton, ButtonData.HELP_2);
1041             buttonBar.getButtons().add(detailsButton);
1042             ButtonBar.setButtonUniformSize(detailsButton, false);
1043         }
1044 
1045         boolean hasDefault = false;
1046         for (ButtonType cmd : getButtonTypes()) {
1047             Node button = buttonNodes.computeIfAbsent(cmd, dialogButton -> createButton(cmd));
1048             
1049             // keep only first default button
1050             if (button instanceof Button) {
1051                 ButtonData buttonType = cmd.getButtonData();
1052                 
1053                 ((Button)button).setDefaultButton(!hasDefault && buttonType != null && buttonType.isDefaultButton());
1054                 ((Button)button).setCancelButton(buttonType != null && buttonType.isCancelButton());
1055                 
1056                 hasDefault |= buttonType != null && buttonType.isDefaultButton();
1057             }
1058             buttonBar.getButtons().add(button);
1059         }
1060     }
1061     
1062     private Node getActualContent() {
1063         Node content = getContent();
1064         return content == null ? contentLabel : content;
1065     }
1066     
1067     private Node getActualHeader() {
1068         Node header = getHeader();
1069         return header == null ? headerTextPanel : header;
1070     }
1071     
1072     private Node getActualGraphic() {
1073         return headerTextPanel;
1074     }
1075     
1076     private void updateHeaderArea() {
1077         Node header = getHeader();
1078         if (header != null) {
1079             if (! getChildren().contains(header)) {
1080                 getChildren().add(header);
1081             }
1082             
1083             headerTextPanel.setVisible(false);
1084             headerTextPanel.setManaged(false);
1085         } else {
1086             final String headerText = getHeaderText();
1087 
1088             headerTextPanel.getChildren().clear();
1089             headerTextPanel.getStyleClass().clear();
1090 
1091             // recreate the headerTextNode and add it to the children list.
1092             headerTextPanel.setMaxWidth(Double.MAX_VALUE);
1093 
1094             if (headerText != null && ! headerText.isEmpty()) {
1095                 headerTextPanel.getStyleClass().add("header-panel"); //$NON-NLS-1$
1096             }
1097     
1098             // on left of header is the text
1099             Label headerLabel = new Label(headerText);
1100             headerLabel.setWrapText(true);
1101             headerLabel.setAlignment(Pos.CENTER_LEFT);
1102             headerLabel.setMaxWidth(Double.MAX_VALUE);
1103             headerLabel.setMaxHeight(Double.MAX_VALUE);
1104             headerTextPanel.add(headerLabel, 0, 0);
1105 
1106             // on the right of the header is a graphic, if one is specified
1107             graphicContainer.getChildren().clear();
1108 
1109             if (! graphicContainer.getStyleClass().contains("graphic-container")) { //$NON-NLS-1$)
1110                 graphicContainer.getStyleClass().add("graphic-container"); //$NON-NLS-1$
1111             }
1112 
1113             final Node graphic = getGraphic();
1114             if (graphic != null) {
1115                 graphicContainer.getChildren().add(graphic);
1116             }
1117             headerTextPanel.add(graphicContainer, 1, 0);
1118 
1119             // column constraints
1120             ColumnConstraints textColumn = new ColumnConstraints();
1121             textColumn.setFillWidth(true);
1122             textColumn.setHgrow(Priority.ALWAYS);
1123             ColumnConstraints graphicColumn = new ColumnConstraints();
1124             graphicColumn.setFillWidth(false);
1125             graphicColumn.setHgrow(Priority.NEVER);
1126             headerTextPanel.getColumnConstraints().setAll(textColumn , graphicColumn);
1127             
1128             headerTextPanel.setVisible(true);
1129             headerTextPanel.setManaged(true);
1130         }
1131     }
1132     
1133     private void updateContentArea() {
1134         Node content = getContent();
1135         if (content != null) {
1136             if (! getChildren().contains(content)) {
1137                 getChildren().add(content);
1138             }
1139             
1140             if (! content.getStyleClass().contains("content")) {
1141                 content.getStyleClass().add("content");
1142             }
1143             
1144             contentLabel.setVisible(false);
1145             contentLabel.setManaged(false);
1146         } else {
1147             final String contentText = getContentText();
1148             final boolean visible = contentText != null && !contentText.isEmpty();
1149             contentLabel.setText(visible ? contentText : "");
1150             contentLabel.setVisible(visible);
1151             contentLabel.setManaged(visible);
1152         }
1153     }
1154     
1155     boolean hasHeader() {
1156         return getHeader() != null || isTextHeader();
1157     }
1158 
1159     private boolean isTextHeader() {
1160         String headerText = getHeaderText();
1161         return headerText != null && !headerText.isEmpty();
1162     }
1163     
1164     boolean hasExpandableContent() {
1165         return getExpandableContent() != null;
1166     }
1167     
1168     void setDialog(Dialog<?> dialog) {
1169         this.dialog = dialog;
1170     }
1171     
1172     
1173     
1174     /***************************************************************************
1175      *                                                                         *
1176      * Stylesheet Handling                                                     *
1177      *                                                                         *
1178      **************************************************************************/
1179 
1180      /**
1181       * @treatAsPrivate implementation detail
1182       */
1183     private static class StyleableProperties {
1184         
1185         private static final CssMetaData<DialogPane,String> GRAPHIC = 
1186             new CssMetaData<DialogPane,String>("-fx-graphic",
1187                 StringConverter.getInstance()) {
1188 
1189             @Override
1190             public boolean isSettable(DialogPane n) {
1191                 // Note that we care about the graphic, not imageUrl
1192                 return n.graphicProperty == null || !n.graphicProperty.isBound();
1193             }
1194 
1195             @Override
1196             public StyleableProperty<String> getStyleableProperty(DialogPane n) {
1197                 return n.imageUrlProperty();
1198             }
1199         };
1200         
1201         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
1202         static {
1203             final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Region.getClassCssMetaData());
1204             Collections.addAll(styleables,
1205                 GRAPHIC
1206             );
1207             STYLEABLES = Collections.unmodifiableList(styleables);
1208         }
1209     }
1210 
1211     /**
1212      * @return The CssMetaData associated with this class, which may include the
1213      * CssMetaData of its super classes.
1214      */
1215     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
1216         return StyleableProperties.STYLEABLES;
1217     }
1218 
1219     /** {@inheritDoc} */
1220     @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
1221         return getClassCssMetaData();
1222     }
1223 }