/* * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javafx.scene.control; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.WeakHashMap; import com.sun.javafx.scene.control.skin.Utils; import com.sun.javafx.scene.control.skin.resources.ControlResources; import javafx.beans.DefaultProperty; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.WritableValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.css.CssMetaData; import javafx.css.StyleOrigin; import javafx.css.Styleable; import javafx.css.StyleableObjectProperty; import javafx.css.StyleableProperty; import javafx.css.StyleableStringProperty; import javafx.event.ActionEvent; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.ButtonBar.ButtonData; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import com.sun.javafx.css.StyleManager; import com.sun.javafx.css.converters.StringConverter; /** * DialogPane should be considered to be the root node displayed within a * {@link Dialog} instance. In this role, the DialogPane is responsible for the * placement of {@link #headerProperty() headers}, {@link #graphicProperty() graphics}, * {@link #contentProperty() content}, and {@link #getButtonTypes() buttons}. * The default implementation of DialogPane (that is, the DialogPane class itself) * handles the layout via the normal {@link #layoutChildren()} method. This * method may be overridden by subclasses wishing to handle the layout in an * alternative fashion). * *

In addition to the {@link #headerProperty() header} and * {@link #contentProperty() content} properties, there exists * {@link #headerTextProperty() header text} and * {@link #contentTextProperty() content text} properties. The way the *Text * properties work is that they are a lower precedence compared to the Node * properties, but they are far more convenient for developers in the common case, * as it is likely the case that a developer more often than not simply wants to * set a string value into the header or content areas of the DialogPane. * *

It is important to understand the implications of setting non-null values * in the {@link #headerProperty() header} and {@link #headerTextProperty() headerText} * properties. The key points are as follows: * *

    *
  1. The {@code header} property takes precedence over the {@code headerText} * property, so if both are set to non-null values, {@code header} will be * used and {@code headerText} will be ignored.
  2. *
  3. If {@code headerText} is set to a non-null value, and a * {@link #graphicProperty() graphic} has also been set, the default position * for the graphic shifts from being located to the left of the content area * to being to the right of the header text.
  4. *
  5. If {@code header} is set to a non-null value, and a * {@link #graphicProperty() graphic} has also been set, the graphic is * removed from its default position (to the left of the content area), * and is not placed to the right of the custom header * node. If the graphic is desired, it should be manually added in to the * layout of the custom header node manually.
  6. *
* *

DialogPane operates on the concept of {@link ButtonType}. A ButtonType is * a descriptor of a single button that should be represented visually in the * DialogPane. Developers who create a DialogPane therefore must specify the * button types that they want to display, and this is done via the * {@link #getButtonTypes()} method, which returns a modifiable * {@link ObservableList}, which users can add to and remove from as desired. * *

The {@link ButtonType} class defines a number of pre-defined button types, * such as {@link ButtonType#OK} and {@link ButtonType#CANCEL}. Many users of the * JavaFX dialogs API will find that these pre-defined button types meet their * needs, particularly due to their built-in support for * {@link ButtonData#isDefaultButton() default} and * {@link ButtonData#isCancelButton() cancel} buttons, as well as the benefit of * the strings being translated into all languages which JavaFX is translated to. * For users that want to define their own {@link ButtonType} (most commonly to * define a button with custom text), they may do so via the constructors available * on the {@link ButtonType} class. * *

Developers will quickly find that the amount of configurability offered * via the {@link ButtonType} class is minimal. This is intentional, but does not * mean that developers can not modify the buttons created by the {@link ButtonType} * that have been specified. To do this, developers simply call the * {@link #lookupButton(ButtonType)} method with the ButtonType (assuming it has * already been set in the {@link #getButtonTypes()} list. The returned Node is * typically of type {@link Button}, but this depends on if the * {@link #createButton(ButtonType)} method has been overridden. * *

The DialogPane class offers a few methods that can be overridden by * subclasses, to more easily enable custom functionality. These methods include * the following: * *

* *

These methods are documented, so please take note of the expectations * placed on any developer who wishes to override these methods with their own * functionality. * * @see Dialog * @since JavaFX 8u40 */ @DefaultProperty("buttonTypes") public class DialogPane extends Pane { /************************************************************************** * * Static fields * **************************************************************************/ /** * Creates a Label node that works well within a Dialog. * @param text The text to display */ static Label createContentLabel(String text) { Label label = new Label(text); label.setMaxWidth(Double.MAX_VALUE); label.setMaxHeight(Double.MAX_VALUE); label.getStyleClass().add("content"); label.setWrapText(true); label.setPrefWidth(360); return label; } /************************************************************************** * * Private fields * **************************************************************************/ private final GridPane headerTextPanel; private final Label contentLabel; private final StackPane graphicContainer; private final Node buttonBar; private final ObservableList buttons = FXCollections.observableArrayList(); private final Map buttonNodes = new WeakHashMap<>(); private Node detailsButton; // this is not a property - we have a package-scope setDialog method that // sets this field. It is set by Dialog if the DialogPane is set inside a Dialog. private Dialog dialog; /************************************************************************** * * Constructors * **************************************************************************/ /** * Creates a new DialogPane instance with a style class of 'dialog-pane'. */ public DialogPane() { getStyleClass().add("dialog-pane"); headerTextPanel = new GridPane(); getChildren().add(headerTextPanel); graphicContainer = new StackPane(); contentLabel = createContentLabel(""); getChildren().add(contentLabel); buttonBar = createButtonBar(); if (buttonBar != null) { getChildren().add(buttonBar); } buttons.addListener((ListChangeListener) c -> { while (c.next()) { if (c.wasRemoved()) { for (ButtonType cmd : c.getRemoved()) { buttonNodes.remove(cmd); } } if (c.wasAdded()) { for (ButtonType cmd : c.getAddedSubList()) { if (! buttonNodes.containsKey(cmd)) { buttonNodes.put(cmd, createButton(cmd)); } } } } }); } /************************************************************************** * * Properties * **************************************************************************/ // --- graphic private final ObjectProperty graphicProperty = new StyleableObjectProperty() { // The graphic is styleable by css, but it is the // imageUrlProperty that handles the style value. @Override public CssMetaData getCssMetaData() { return StyleableProperties.GRAPHIC; } @Override public Object getBean() { return DialogPane.this; } @Override public String getName() { return "graphic"; } WeakReference graphicRef = new WeakReference<>(null); protected void invalidated() { Node oldGraphic = graphicRef.get(); if (oldGraphic != null) { getChildren().remove(oldGraphic); } Node newGraphic = getGraphic(); graphicRef = new WeakReference<>(newGraphic); updateHeaderArea(); } }; /** * The dialog graphic, presented either in the header, if one is showing, or * to the left of the {@link #contentProperty() content}. * * @return An ObjectProperty wrapping the current graphic. */ public final ObjectProperty graphicProperty() { return graphicProperty; } public final Node getGraphic() { return graphicProperty.get(); } /** * Sets the dialog graphic, which will be displayed either in the header, if * one is showing, or to the left of the {@link #contentProperty() content}. * * @param graphic * The new dialog graphic, or null if no graphic should be shown. */ public final void setGraphic(Node graphic) { this.graphicProperty.set(graphic); } // --- imageUrl (this is NOT public API, except via CSS) // Note that this code is a copy/paste from Labeled private StyleableStringProperty imageUrl = null; /** * The imageUrl property is set from CSS and then the graphic property is * set from the invalidated method. This ensures that the same image isn't * reloaded. */ private StyleableStringProperty imageUrlProperty() { if (imageUrl == null) { imageUrl = new StyleableStringProperty() { // // If imageUrlProperty is invalidated, this is the origin of the style that // triggered the invalidation. This is used in the invalidated() method where the // value of super.getStyleOrigin() is not valid until after the call to set(v) returns, // by which time invalidated will have been called. // This value is initialized to USER in case someone calls set on the imageUrlProperty, which // is possible: // CssMetaData metaData = ((StyleableProperty)dialogPane.graphicProperty()).getCssMetaData(); // StyleableProperty prop = metaData.getStyleableProperty(dialogPane); // prop.set(someUrl); // // TODO: Note that prop != dialogPane, which violates the contract between StyleableProperty and CssMetaData. // StyleOrigin origin = StyleOrigin.USER; @Override public void applyStyle(StyleOrigin origin, String v) { this.origin = origin; // Don't want applyStyle to throw an exception which would leave this.origin set to the wrong value if (graphicProperty == null || graphicProperty.isBound() == false) super.applyStyle(origin, v); // Origin is only valid for this invocation of applyStyle, so reset it to USER in case someone calls set. this.origin = StyleOrigin.USER; } @Override protected void invalidated() { // need to call super.get() here since get() is overridden to return the graphicProperty's value final String url = super.get(); if (url == null) { ((StyleableProperty)(WritableValue)graphicProperty()).applyStyle(origin, null); } else { // RT-34466 - if graphic's url is the same as this property's value, then don't overwrite. final Node graphicNode = DialogPane.this.getGraphic(); if (graphicNode instanceof ImageView) { final ImageView imageView = (ImageView)graphicNode; final Image image = imageView.getImage(); if (image != null) { final String imageViewUrl = image.impl_getUrl(); if (url.equals(imageViewUrl)) return; } } final Image img = StyleManager.getInstance().getCachedImage(url); if (img != null) { // // Note that it is tempting to try to re-use existing ImageView simply by setting // the image on the current ImageView, if there is one. This would effectively change // the image, but not the ImageView which means that no graphicProperty listeners would // be notified. This is probably not what we want. // // // Have to call applyStyle on graphicProperty so that the graphicProperty's // origin matches the imageUrlProperty's origin. // ((StyleableProperty)(WritableValue)graphicProperty()).applyStyle(origin, new ImageView(img)); } } } @Override public String get() { // // The value of the imageUrlProperty is that of the graphicProperty. // Return the value in a way that doesn't expand the graphicProperty. // final Node graphic = getGraphic(); if (graphic instanceof ImageView) { final Image image = ((ImageView)graphic).getImage(); if (image != null) { return image.impl_getUrl(); } } return null; } @Override public StyleOrigin getStyleOrigin() { // // The origin of the imageUrlProperty is that of the graphicProperty. // Return the origin in a way that doesn't expand the graphicProperty. // return graphicProperty != null ? ((StyleableProperty)(WritableValue)graphicProperty).getStyleOrigin() : null; } @Override public Object getBean() { return DialogPane.this; } @Override public String getName() { return "imageUrl"; } @Override public CssMetaData getCssMetaData() { return StyleableProperties.GRAPHIC; } }; } return imageUrl; } // --- header private final ObjectProperty header = new SimpleObjectProperty(null) { WeakReference headerRef = new WeakReference<>(null); @Override protected void invalidated() { Node oldHeader = headerRef.get(); if (oldHeader != null) { getChildren().remove(oldHeader); } Node newHeader = getHeader(); headerRef = new WeakReference<>(newHeader); updateHeaderArea(); } }; /** * Node which acts as the dialog pane header. * * @return the header of the dialog pane. */ public final Node getHeader() { return header.get(); } /** * Assigns the dialog pane header. Any Node can be used. * * @param header The new header of the DialogPane. */ public final void setHeader(Node header) { this.header.setValue(header); } /** * Property representing the header area of the dialog pane. Note that if this * header is set to a non-null value, that it will take up the entire top * area of the DialogPane. It will also result in the DialogPane switching its * layout to the 'header' layout - as outlined in the {@link DialogPane} class * javadoc. */ public final ObjectProperty headerProperty() { return header; } // --- header text private final StringProperty headerText = new SimpleStringProperty(this, "headerText") { @Override protected void invalidated() { updateHeaderArea(); requestLayout(); } }; /** * Sets the string to show in the dialog header area. Note that the header text * is lower precedence than the {@link #headerProperty() header node}, meaning * that if both the header node and the headerText properties are set, the * header text will not be displayed in a default DialogPane instance. * *

When headerText is set to a non-null value, this will result in the * DialogPane switching its layout to the 'header' layout - as outlined in * the {@link DialogPane} class javadoc.

*/ public final void setHeaderText(String headerText) { this.headerText.set(headerText); } /** * Returns the currently-set header text for this DialogPane. */ public final String getHeaderText() { return headerText.get(); } /** * A property representing the header text for the dialog pane. The header text * is lower precedence than the {@link #headerProperty() header node}, meaning * that if both the header node and the headerText properties are set, the * header text will not be displayed in a default DialogPane instance. * *

When headerText is set to a non-null value, this will result in the * DialogPane switching its layout to the 'header' layout - as outlined in * the {@link DialogPane} class javadoc.

*/ public final StringProperty headerTextProperty() { return headerText; } // --- content private final ObjectProperty content = new SimpleObjectProperty(null) { WeakReference contentRef = new WeakReference<>(null); @Override protected void invalidated() { Node oldContent = contentRef.get(); if (oldContent != null) { getChildren().remove(oldContent); } Node newContent = getContent(); contentRef = new WeakReference<>(newContent); updateContentArea(); } }; /** * Returns the dialog content as a Node (even if it was set as a String * using {@link #setContentText(String)} - this was simply transformed into a * {@link Node} (most probably a {@link Label}). * * @return dialog's content */ public final Node getContent() { return content.get(); } /** * Assign dialog content. Any Node can be used * * @param content * dialog's content */ public final void setContent(Node content) { this.content.setValue(content); } /** * Property representing the content area of the dialog. */ public final ObjectProperty contentProperty() { return content; } // --- content text private final StringProperty contentText = new SimpleStringProperty(this, "contentText") { @Override protected void invalidated() { updateContentArea(); requestLayout(); } }; /** * Sets the string to show in the dialog content area. Note that the content text * is lower precedence than the {@link #contentProperty() content node}, meaning * that if both the content node and the contentText properties are set, the * content text will not be displayed in a default DialogPane instance. */ public final void setContentText(String contentText) { this.contentText.set(contentText); } /** * Returns the currently-set content text for this DialogPane. */ public final String getContentText() { return contentText.get(); } /** * A property representing the content text for the dialog pane. The content text * is lower precedence than the {@link #contentProperty() content node}, meaning * that if both the content node and the contentText properties are set, the * content text will not be displayed in a default DialogPane instance. */ public final StringProperty contentTextProperty() { return contentText; } // --- expandable content private final ObjectProperty expandableContentProperty = new SimpleObjectProperty(null) { WeakReference expandableContentRef = new WeakReference<>(null); @Override protected void invalidated() { Node oldExpandableContent = expandableContentRef.get(); if (oldExpandableContent != null) { getChildren().remove(oldExpandableContent); } Node newExpandableContent = getExpandableContent(); expandableContentRef = new WeakReference(newExpandableContent); if (newExpandableContent != null) { newExpandableContent.setVisible(isExpanded()); newExpandableContent.setManaged(isExpanded()); if (!newExpandableContent.getStyleClass().contains("expandable-content")) { //$NON-NLS-1$ newExpandableContent.getStyleClass().add("expandable-content"); //$NON-NLS-1$ } getChildren().add(newExpandableContent); } } }; /** * A property that represents the dialog expandable content area. Any Node * can be placed in this area, but it will only be shown when the user * clicks the 'Show Details' expandable button. This button will be added * automatically when the expandable content property is non-null. */ public final ObjectProperty expandableContentProperty() { return expandableContentProperty; } /** * Returns the dialog expandable content node, if one is set, or null * otherwise. */ public final Node getExpandableContent() { return expandableContentProperty.get(); } /** * Sets the dialog expandable content node, or null if no expandable content * needs to be shown. */ public final void setExpandableContent(Node content) { this.expandableContentProperty.set(content); } // --- expanded private final BooleanProperty expandedProperty = new SimpleBooleanProperty(this, "expanded", false) { protected void invalidated() { final Node expandableContent = getExpandableContent(); if (expandableContent != null) { expandableContent.setVisible(isExpanded()); } requestLayout(); } }; /** * Represents whether the dialogPane is expanded. */ public final BooleanProperty expandedProperty() { return expandedProperty; } /** * Returns whether or not the dialogPane is expanded. * * @return true if dialogPane is expanded. */ public final boolean isExpanded() { return expandedProperty().get(); } /** * Sets whether the dialogPane is expanded. This only makes sense when there * is {@link #expandableContentProperty() expandable content} to show. * * @param value true if dialogPane should be expanded. */ public final void setExpanded(boolean value) { expandedProperty().set(value); } /************************************************************************** * * Public API * **************************************************************************/ // --- button types /** * Observable list of button types used for the dialog button bar area * (created via the {@link #createButtonBar()} method). Modifying the contents * of this list will immediately change the buttons displayed to the user * within the dialog pane. * * @return The {@link ObservableList} of {@link ButtonType button types} * available to the user. */ public final ObservableList getButtonTypes() { return buttons; } /** * This method provides a way in which developers may retrieve the actual * Node for a given {@link ButtonType} (assuming it is part of the * {@link #getButtonTypes() button types} list). * * @param buttonType The {@link ButtonType} for which a Node representation is requested. * @return The Node used to represent the button type, as created by * {@link #createButton(ButtonType)}, and only if the button type * is part of the {@link #getButtonTypes() button types} list, otherwise null. */ public final Node lookupButton(ButtonType buttonType) { return buttonNodes.get(buttonType); } /** * This method can be overridden by subclasses to provide the button bar. * Note that by overriding this method, the developer must take on multiple * responsibilities: * *
    *
  1. The developer must immediately iterate through all * {@link #getButtonTypes() button types} and call * {@link #createButton(ButtonType)} for each of them in turn. *
  2. The developer must add a listener to the * {@link #getButtonTypes() button types} list, and when this list changes * update the button bar as appropriate. *
  3. Similarly, the developer must watch for changes to the * {@link #expandableContentProperty() expandable content} property, * adding and removing the details button (created via * {@link #createDetailsButton()} method). *
* *

The default implementation of this method creates and returns a new * {@link ButtonBar} instance. */ protected Node createButtonBar() { ButtonBar buttonBar = new ButtonBar(); buttonBar.setMaxWidth(Double.MAX_VALUE); updateButtons(buttonBar); getButtonTypes().addListener((ListChangeListener) c -> updateButtons(buttonBar)); expandableContentProperty().addListener(o -> updateButtons(buttonBar)); return buttonBar; } /** * This method can be overridden by subclasses to create a custom button that * will subsequently inserted into the DialogPane button area (created via * the {@link #createButtonBar()} method, but mostly commonly it is an instance * of {@link ButtonBar}. * * @param buttonType The {@link ButtonType} to create a button from. * @return A JavaFX {@link Node} that represents the given {@link ButtonType}, * most commonly an instance of {@link Button}. */ protected Node createButton(ButtonType buttonType) { final Button button = new Button(buttonType.getText()); final ButtonData buttonData = buttonType.getButtonData(); ButtonBar.setButtonData(button, buttonData); button.setDefaultButton(buttonType != null && buttonData.isDefaultButton()); button.setCancelButton(buttonType != null && buttonData.isCancelButton()); button.addEventHandler(ActionEvent.ACTION, ae -> { if (ae.isConsumed()) return; if (dialog != null) { dialog.impl_setResultAndClose(buttonType, true); } }); return button; } /** * This method can be overridden by subclasses to create a custom details button. * *

To override this method you must do two things: *

    *
  1. The button will need to have its own code set to handle mouse / keyboard * interaction and to toggle the state of the * {@link #expandedProperty() expanded} property. *
  2. If your button changes its visuals based on whether the dialog pane * is expanded or collapsed, you should add a listener to the * {@link #expandedProperty() expanded} property, so that you may update * the button visuals. *
*/ protected Node createDetailsButton() { final Hyperlink detailsButton = new Hyperlink(); detailsButton.getStyleClass().setAll("details-button", "more"); //$NON-NLS-1$ //$NON-NLS-2$ final String moreText = ControlResources.getString("Dialog.detail.button.more"); //$NON-NLS-1$ final String lessText = ControlResources.getString("Dialog.detail.button.less"); //$NON-NLS-1$ detailsButton.setText(moreText); expandedProperty().addListener(o -> { final boolean isExpanded = isExpanded(); detailsButton.setText(isExpanded ? lessText : moreText); detailsButton.getStyleClass().setAll("details-button", (isExpanded ? "less" : "more")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ }); detailsButton.setOnAction(ae -> setExpanded(!isExpanded())); return detailsButton; } private double oldHeight = -1; /** {@inheritDoc} */ @Override protected void layoutChildren() { final boolean hasHeader = hasHeader(); // snapped insets code commented out to resolve RT-39738 final double w = Math.max(minWidth(-1), getWidth());// - (snappedLeftInset() + snappedRightInset()); final double minHeight = minHeight(w); final double prefHeight = prefHeight(w); final double maxHeight = maxHeight(w); final double currentHeight = getHeight(); final double dialogHeight = dialog == null ? 0 : dialog.dialog.getSceneHeight(); double h; if (prefHeight > currentHeight && prefHeight > minHeight && (prefHeight <= dialogHeight || dialogHeight == 0)) { h = prefHeight; resize(w, h); } else { boolean isDialogGrowing = currentHeight > oldHeight; if (isDialogGrowing) { double _h = currentHeight < prefHeight ? Math.min(prefHeight, currentHeight) : Math.max(prefHeight, dialogHeight); h = Utils.boundedSize(_h, minHeight, maxHeight); } else { h = Utils.boundedSize(Math.min(currentHeight, dialogHeight), minHeight, maxHeight); } resize(w, h); } h -= (snappedTopInset() + snappedBottomInset()); oldHeight = h; final double leftPadding = snappedLeftInset(); final double topPadding = snappedTopInset(); final double rightPadding = snappedRightInset(); final double bottomPadding = snappedBottomInset(); // create the nodes up front so we can work out sizing final Node header = getActualHeader(); final Node content = getActualContent(); final Node graphic = getActualGraphic(); final Node expandableContent = getExpandableContent(); final double graphicPrefWidth = hasHeader || graphic == null ? 0 : graphic.prefWidth(-1); final double headerPrefHeight = hasHeader ? header.prefHeight(w) : 0; final double buttonBarPrefHeight = buttonBar == null ? 0 : buttonBar.prefHeight(w); final double graphicPrefHeight = hasHeader || graphic == null ? 0 : graphic.prefHeight(-1); final double expandableContentPrefHeight; final double contentAreaHeight; final double contentAndGraphicHeight; final double availableContentWidth = w - graphicPrefWidth - leftPadding - rightPadding; if (isExpanded()) { // precedence goes to content and then expandable content contentAreaHeight = isExpanded() ? content.prefHeight(availableContentWidth) : 0; contentAndGraphicHeight = hasHeader ? contentAreaHeight : Math.max(graphicPrefHeight, contentAreaHeight); expandableContentPrefHeight = h - (headerPrefHeight + contentAndGraphicHeight + buttonBarPrefHeight); } else { // content gets the lowest precedence expandableContentPrefHeight = isExpanded() ? expandableContent.prefHeight(w) : 0; contentAreaHeight = h - (headerPrefHeight + expandableContentPrefHeight + buttonBarPrefHeight); contentAndGraphicHeight = hasHeader ? contentAreaHeight : Math.max(graphicPrefHeight, contentAreaHeight); } double x = leftPadding; double y = topPadding; if (! hasHeader) { if (graphic != null) { graphic.resizeRelocate(x, y, graphicPrefWidth, graphicPrefHeight); x += graphicPrefWidth; } } else { header.resizeRelocate(x, y, w - (leftPadding + rightPadding), headerPrefHeight); y += headerPrefHeight; } content.resizeRelocate(x, y, availableContentWidth, contentAreaHeight); y += hasHeader ? contentAreaHeight : contentAndGraphicHeight; if (expandableContent != null) { expandableContent.resizeRelocate(leftPadding, y, w - rightPadding, expandableContentPrefHeight); y += expandableContentPrefHeight; } if (buttonBar != null) { buttonBar.resizeRelocate(leftPadding, y, w - (leftPadding + rightPadding), buttonBarPrefHeight); } } /** {@inheritDoc} */ @Override protected double computeMinWidth(double height) { double headerMinWidth = hasHeader() ? getActualHeader().minWidth(height) + 10 : 0; double contentMinWidth = getActualContent().minWidth(height); double buttonBarMinWidth = buttonBar == null ? 0 : buttonBar.minWidth(height); double graphicMinWidth = getActualGraphic().minWidth(height); double expandableContentMinWidth = 0; final Node expandableContent = getExpandableContent(); if (isExpanded() && expandableContent != null) { expandableContentMinWidth = expandableContent.minWidth(height); } double minWidth = snappedLeftInset() + (hasHeader() ? 0 : graphicMinWidth) + Math.max(Math.max(headerMinWidth, expandableContentMinWidth), Math.max(contentMinWidth, buttonBarMinWidth)) + snappedRightInset(); return snapSize(minWidth); } /** {@inheritDoc} */ @Override protected double computeMinHeight(double width) { final boolean hasHeader = hasHeader(); double headerMinHeight = hasHeader ? getActualHeader().minHeight(width) : 0; double buttonBarMinHeight = buttonBar == null ? 0 : buttonBar.minHeight(width); Node graphic = getActualGraphic(); double graphicMinWidth = hasHeader ? 0 : graphic.minWidth(-1); double graphicMinHeight = hasHeader ? 0 : graphic.minHeight(width); // min height of a label is based on one line (wrapping is ignored) Node content = getActualContent(); double contentAvailableWidth = width == Region.USE_COMPUTED_SIZE ? Region.USE_COMPUTED_SIZE : hasHeader ? width : (width - graphicMinWidth); double contentMinHeight = content.minHeight(contentAvailableWidth); double expandableContentMinHeight = 0; final Node expandableContent = getExpandableContent(); if (isExpanded() && expandableContent != null) { expandableContentMinHeight = expandableContent.minHeight(width); } double minHeight = snappedTopInset() + headerMinHeight + Math.max(graphicMinHeight, contentMinHeight) + expandableContentMinHeight + buttonBarMinHeight + snappedBottomInset(); return snapSize(minHeight); } /** {@inheritDoc} */ @Override protected double computePrefWidth(double height) { double headerPrefWidth = hasHeader() ? getActualHeader().prefWidth(height) + 10 : 0; double contentPrefWidth = getActualContent().prefWidth(height); double buttonBarPrefWidth = buttonBar == null ? 0 : buttonBar.prefWidth(height); double graphicPrefWidth = getActualGraphic().prefWidth(height); double expandableContentPrefWidth = 0; final Node expandableContent = getExpandableContent(); if (isExpanded() && expandableContent != null) { expandableContentPrefWidth = expandableContent.prefWidth(height); } double prefWidth = snappedLeftInset() + (hasHeader() ? 0 : graphicPrefWidth) + Math.max(Math.max(headerPrefWidth, expandableContentPrefWidth), Math.max(contentPrefWidth, buttonBarPrefWidth)) + snappedRightInset(); return snapSize(prefWidth); } /** {@inheritDoc} */ @Override protected double computePrefHeight(double width) { final boolean hasHeader = hasHeader(); double headerPrefHeight = hasHeader ? getActualHeader().prefHeight(width) : 0; double buttonBarPrefHeight = buttonBar == null ? 0 : buttonBar.prefHeight(width); Node graphic = getActualGraphic(); double graphicPrefWidth = hasHeader ? 0 : graphic.prefWidth(-1); double graphicPrefHeight = hasHeader ? 0 : graphic.prefHeight(width); Node content = getActualContent(); double contentAvailableWidth = width == Region.USE_COMPUTED_SIZE ? Region.USE_COMPUTED_SIZE : hasHeader ? width : (width - graphicPrefWidth); double contentPrefHeight = content.prefHeight(contentAvailableWidth); double expandableContentPrefHeight = 0; final Node expandableContent = getExpandableContent(); if (isExpanded() && expandableContent != null) { expandableContentPrefHeight = expandableContent.prefHeight(width); } double prefHeight = snappedTopInset() + headerPrefHeight + Math.max(graphicPrefHeight, contentPrefHeight) + expandableContentPrefHeight + buttonBarPrefHeight + snappedBottomInset(); return snapSize(prefHeight); } /************************************************************************** * * Private implementation * @param buttonBar * **************************************************************************/ private void updateButtons(ButtonBar buttonBar) { buttonBar.getButtons().clear(); // show details button if expandable content is present if (hasExpandableContent()) { if (detailsButton == null) { detailsButton = createDetailsButton(); } ButtonBar.setButtonData(detailsButton, ButtonData.HELP_2); buttonBar.getButtons().add(detailsButton); ButtonBar.setButtonUniformSize(detailsButton, false); } boolean hasDefault = false; for (ButtonType cmd : getButtonTypes()) { Node button = buttonNodes.computeIfAbsent(cmd, dialogButton -> createButton(cmd)); // keep only first default button if (button instanceof Button) { ButtonData buttonType = cmd.getButtonData(); ((Button)button).setDefaultButton(!hasDefault && buttonType != null && buttonType.isDefaultButton()); ((Button)button).setCancelButton(buttonType != null && buttonType.isCancelButton()); hasDefault |= buttonType != null && buttonType.isDefaultButton(); } buttonBar.getButtons().add(button); } } private Node getActualContent() { Node content = getContent(); return content == null ? contentLabel : content; } private Node getActualHeader() { Node header = getHeader(); return header == null ? headerTextPanel : header; } private Node getActualGraphic() { return headerTextPanel; } private void updateHeaderArea() { Node header = getHeader(); if (header != null) { if (! getChildren().contains(header)) { getChildren().add(header); } headerTextPanel.setVisible(false); headerTextPanel.setManaged(false); } else { final String headerText = getHeaderText(); headerTextPanel.getChildren().clear(); headerTextPanel.getStyleClass().clear(); // recreate the headerTextNode and add it to the children list. headerTextPanel.setMaxWidth(Double.MAX_VALUE); if (headerText != null && ! headerText.isEmpty()) { headerTextPanel.getStyleClass().add("header-panel"); //$NON-NLS-1$ } // on left of header is the text Label headerLabel = new Label(headerText); headerLabel.setWrapText(true); headerLabel.setAlignment(Pos.CENTER_LEFT); headerLabel.setMaxWidth(Double.MAX_VALUE); headerLabel.setMaxHeight(Double.MAX_VALUE); headerTextPanel.add(headerLabel, 0, 0); // on the right of the header is a graphic, if one is specified graphicContainer.getChildren().clear(); if (! graphicContainer.getStyleClass().contains("graphic-container")) { //$NON-NLS-1$) graphicContainer.getStyleClass().add("graphic-container"); //$NON-NLS-1$ } final Node graphic = getGraphic(); if (graphic != null) { graphicContainer.getChildren().add(graphic); } headerTextPanel.add(graphicContainer, 1, 0); // column constraints ColumnConstraints textColumn = new ColumnConstraints(); textColumn.setFillWidth(true); textColumn.setHgrow(Priority.ALWAYS); ColumnConstraints graphicColumn = new ColumnConstraints(); graphicColumn.setFillWidth(false); graphicColumn.setHgrow(Priority.NEVER); headerTextPanel.getColumnConstraints().setAll(textColumn , graphicColumn); headerTextPanel.setVisible(true); headerTextPanel.setManaged(true); } } private void updateContentArea() { Node content = getContent(); if (content != null) { if (! getChildren().contains(content)) { getChildren().add(content); } if (! content.getStyleClass().contains("content")) { content.getStyleClass().add("content"); } contentLabel.setVisible(false); contentLabel.setManaged(false); } else { final String contentText = getContentText(); final boolean visible = contentText != null && !contentText.isEmpty(); contentLabel.setText(visible ? contentText : ""); contentLabel.setVisible(visible); contentLabel.setManaged(visible); } } boolean hasHeader() { return getHeader() != null || isTextHeader(); } private boolean isTextHeader() { String headerText = getHeaderText(); return headerText != null && !headerText.isEmpty(); } boolean hasExpandableContent() { return getExpandableContent() != null; } void setDialog(Dialog dialog) { this.dialog = dialog; } /*************************************************************************** * * * Stylesheet Handling * * * **************************************************************************/ /** * @treatAsPrivate implementation detail */ private static class StyleableProperties { private static final CssMetaData GRAPHIC = new CssMetaData("-fx-graphic", StringConverter.getInstance()) { @Override public boolean isSettable(DialogPane n) { // Note that we care about the graphic, not imageUrl return n.graphicProperty == null || !n.graphicProperty.isBound(); } @Override public StyleableProperty getStyleableProperty(DialogPane n) { return n.imageUrlProperty(); } }; private static final List> STYLEABLES; static { final List> styleables = new ArrayList<>(Region.getClassCssMetaData()); Collections.addAll(styleables, GRAPHIC ); STYLEABLES = Collections.unmodifiableList(styleables); } } /** * @return The CssMetaData associated with this class, which may include the * CssMetaData of its super classes. */ public static List> getClassCssMetaData() { return StyleableProperties.STYLEABLES; } /** {@inheritDoc} */ @Override public List> getCssMetaData() { return getClassCssMetaData(); } }