/* * 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: * *
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 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. 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. 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 super ButtonType>) 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:
*
*
*
*
*
*/
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