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