# HG changeset patch # User kcr # Date 1408054555 25200 # Node ID 3479a4c4c48dd3c7c70ffc32f7af6db6ff09b8b1 # Parent afbc7b22b7abd44f7303aff7fe9e1ab6b8bed73a RT-12643: Add alert dialogs diff --git a/modules/controls/src/main/java/com/sun/javafx/scene/control/skin/ButtonBarSkin.java b/modules/controls/src/main/java/com/sun/javafx/scene/control/skin/ButtonBarSkin.java new file mode 100644 --- /dev/null +++ b/modules/controls/src/main/java/com/sun/javafx/scene/control/skin/ButtonBarSkin.java @@ -0,0 +1,297 @@ +/* + * Copyright (c) 2014, 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 com.sun.javafx.scene.control.skin; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javafx.collections.ListChangeListener; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonBar.ButtonData; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; + +import com.sun.javafx.scene.control.behavior.BehaviorBase; +import com.sun.javafx.scene.control.behavior.KeyBinding; +import com.sun.javafx.scene.control.skin.BehaviorSkinBase; + +public class ButtonBarSkin extends BehaviorSkinBase> { + + /************************************************************************** + * + * Static fields + * + **************************************************************************/ + + private static final double GAP_SIZE = 10; + + private static final String CATEGORIZED_TYPES = "LRHEYNXBIACO"; //$NON-NLS-1$ + + // represented as a ButtonType + public static final String BUTTON_DATA_PROPERTY = "controlfx.button.type"; //$NON-NLS-1$ + + // allows to exclude button from uniform resizing + public static final String BUTTON_SIZE_INDEPENDENCE = "controlfx.button.size.indepenence"; //$NON-NLS-1$ + + // pick an arbitrary number + private static final double DO_NOT_CHANGE_SIZE = Double.MAX_VALUE - 100; + + + /************************************************************************** + * + * fields + * + **************************************************************************/ + + private HBox layout; + + + + /************************************************************************** + * + * Constructors + * + **************************************************************************/ + + public ButtonBarSkin(final ButtonBar control) { + super(control, new BehaviorBase<>(control, Collections. emptyList())); + + this.layout = new HBox(GAP_SIZE) { + @Override + protected void layoutChildren() { + // has to be called first or layout is not correct somtimes + resizeButtons(); + super.layoutChildren(); + } + }; + this.layout.setAlignment(Pos.CENTER); + this.layout.getStyleClass().add("container"); + getChildren().add(layout); + + layoutButtons(); + + control.getButtons().addListener(new ListChangeListener() { + @Override public void onChanged(ListChangeListener.Change change) { + layoutButtons(); + } + }); + + registerChangeListener(control.buttonOrderProperty(), "BUTTON_ORDER"); //$NON-NLS-1$ + registerChangeListener(control.buttonMinWidthProperty(), "BUTTON_MIN_WIDTH"); //$NON-NLS-1$ + } + + + /************************************************************************** + * + * Overriding public API + * + **************************************************************************/ + + @Override protected void handleControlPropertyChanged(String p) { + super.handleControlPropertyChanged(p); + + if ("BUTTON_ORDER".equals(p)) { //$NON-NLS-1$ + layoutButtons(); + } else if ("BUTTON_MIN_WIDTH".equals(p)) { //$NON-NLS-1$ +// layoutButtons(); + resizeButtons(); + } + } + + + + /************************************************************************** + * + * Implementation + * + **************************************************************************/ + + private void layoutButtons() { + final ButtonBar buttonBar = getSkinnable(); + final List buttons = buttonBar.getButtons(); + final double buttonMinWidth = buttonBar.getButtonMinWidth(); + + Map> buttonMap = buildButtonMap(buttons); + String buttonOrder = getSkinnable().getButtonOrder(); + + if (buttonOrder == null || buttonOrder.isEmpty()) { + throw new IllegalStateException("ButtonBar buttonOrder string can not be null or empty"); //$NON-NLS-1$ + } + + char[] buttonOrderArr = buttonOrder.toCharArray(); + layout.getChildren().clear(); + + int buttonIndex = 0; // to determine edge cases + Spacer spacer = Spacer.NONE; + + for (int i = 0; i < buttonOrderArr.length; i++) { + char type = buttonOrderArr[i]; + boolean edgeCase = buttonIndex <= 0 && buttonIndex >= buttons.size()-1; + boolean hasChildren = ! layout.getChildren().isEmpty(); + if (type == '+') { + spacer = spacer.replace(Spacer.DYNAMIC); + } else if (type == '_' && hasChildren) { + spacer = spacer.replace(Spacer.FIXED); + } else { + List buttonList = buttonMap.get(String.valueOf(type).toUpperCase()); + if (buttonList != null) { + + spacer.add(layout,edgeCase); + + for (Node btn: buttonList) { + sizeButton(btn, buttonMinWidth, DO_NOT_CHANGE_SIZE, Double.MAX_VALUE); + + layout.getChildren().add(btn); + HBox.setHgrow(btn, Priority.NEVER); + buttonIndex++; + } + spacer = spacer.replace(Spacer.NONE); + } + } + } + + } + + // Button sizing. If buttonUniformSize is true button size = max(buttonMinSize, max(all button pref sizes)) + // otherwise button size = max(buttonBar.buttonMinSize, button pref size) + private void resizeButtons() { + final ButtonBar buttonBar = getSkinnable(); + double buttonMinWidth = buttonBar.getButtonMinWidth(); + final List buttons = buttonBar.getButtons(); + + // determine the widest button + double widest = buttonMinWidth; + for (Node button : buttons) { + if (!ButtonBar.isButtonUniformSize(button)) { + widest = Math.max(button.prefWidth(-1), widest); + } + } + + // set the width of all buttons + for (Node button : buttons) { + if (!ButtonBar.isButtonUniformSize(button)) { + sizeButton(button, DO_NOT_CHANGE_SIZE, widest, DO_NOT_CHANGE_SIZE); + } + } + } + + private void sizeButton(Node btn, double min, double pref, double max) { + if (btn instanceof Region) { + Region regionBtn = (Region)btn; + + if (min != DO_NOT_CHANGE_SIZE) { + regionBtn.setMinWidth(min); + } + if (pref != DO_NOT_CHANGE_SIZE) { + regionBtn.setPrefWidth(pref); + } + if (max != DO_NOT_CHANGE_SIZE) + regionBtn.setMaxWidth(max); + } + } + + private String getButtonType(Node btn) { + ButtonData buttonType = (ButtonData) btn.getProperties().get(BUTTON_DATA_PROPERTY); + + if (buttonType == null) { + // just assume it is ButtonType.OTHER + buttonType = ButtonData.OTHER; + } + + String typeCode = buttonType.getTypeCode(); + typeCode = typeCode.length() > 0? typeCode.substring(0,1): ""; //$NON-NLS-1$ + return CATEGORIZED_TYPES.contains(typeCode.toUpperCase())? typeCode : ButtonData.OTHER.getTypeCode(); + } + + private Map> buildButtonMap( List buttons ) { + Map> buttonMap = new HashMap<>(); + for (Node btn : buttons) { + if ( btn == null ) continue; + String type = getButtonType(btn); + List typedButtons = buttonMap.get(type); + if ( typedButtons == null ) { + typedButtons = new ArrayList(); + buttonMap.put(type, typedButtons); + } + typedButtons.add( btn ); + } + return buttonMap; + } + + + + /************************************************************************** + * + * Support classes / enums + * + **************************************************************************/ + + private enum Spacer { + FIXED { + @Override protected Node create(boolean edgeCase) { + if ( edgeCase ) return null; + Region spacer = new Region(); + spacer.setMinWidth(GAP_SIZE); + HBox.setHgrow(spacer, Priority.NEVER); + return spacer; + } + }, + DYNAMIC { + @Override protected Node create(boolean edgeCase) { + Region spacer = new Region(); + spacer.setMinWidth(edgeCase ? 0 : GAP_SIZE); + HBox.setHgrow(spacer, Priority.ALWAYS); + return spacer; + } + + @Override public Spacer replace(Spacer spacer) { + return FIXED == spacer? this: spacer; + } + }, + NONE; + + protected Node create(boolean edgeCase) { + return null; + } + + public Spacer replace(Spacer spacer) { + return spacer; + } + + public void add(Pane pane, boolean edgeCase) { + Node spacer = create(edgeCase); + if (spacer != null) { + pane.getChildren().add(spacer); + } + } + } +} diff --git a/modules/controls/src/main/java/javafx/scene/control/Alert.java b/modules/controls/src/main/java/javafx/scene/control/Alert.java new file mode 100644 --- /dev/null +++ b/modules/controls/src/main/java/javafx/scene/control/Alert.java @@ -0,0 +1,418 @@ +/* + * Copyright (c) 2014, 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.Optional; + +import com.sun.javafx.scene.control.skin.AccordionSkin; +import com.sun.javafx.scene.control.skin.resources.ControlResources; +import javafx.beans.InvalidationListener; +import javafx.beans.NamedArg; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.scene.Node; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; + +/** + * The Alert class subclasses the {@link Dialog} class, and provides support for a number + * of pre-built dialog types that can be easily shown to users to prompt for a + * response. Therefore, for many users, the Alert class is the most suited class + * for their needs (as opposed to using {@link Dialog} directly). Alternatively, + * users who want to prompt a user for text input or to make a choice from a list + * of options would be better served by using {@link TextInputDialog} and + * {@link ChoiceDialog}, respectively. + * + *

When creating an Alert instance, users must pass in an {@link AlertType} + * enumeration value. It is by passing in this value that the Alert instance will + * configure itself appropriately (by setting default values for many of the + * {@link Dialog} properties, including {@link #titleProperty() title}, + * {@link #headerTextProperty() header}, and {@link #graphicProperty() graphic}, + * as well as the default {@link #getButtonTypes() buttons} that are expected in + * a dialog of the given type. + * + *

To instantiate (but not yet show) an Alert, simply use code such as the following: + * {@code Alert alert = new Alert(AlertType.CONFIRMATION, "Are you sure you want to format your system?");} + * + *

Once an Alert is instantiated, we must show it. More often than not, alerts + * (and dialogs in general) are shown in a modal and blocking fashion. 'Modal' + * means that the dialog prevents user interaction with the owning application + * whilst it is showing, and 'blocking' means that code execution stops at the + * point in which the dialog is shown. This means that you can show a dialog, + * await the user response, and then continue running the code that directly + * follows the show call, giving developers the ability to immediately deal with + * the user input from the dialog (if relevant). + * + *

JavaFX dialogs are modal by default (you can change this via the + * {@link #initModality(javafx.stage.Modality)} API). To specify whether you want + * blocking or non-blocking dialogs, developers simply choose to call + * {@link #showAndWait()} or {@link #show()} (respectively). By default most + * developers should choose to use {@link #showAndWait()}, given the ease of + * coding in these situations. Shown below is three code snippets, showing three + * equally valid ways of showing the Alert dialog that was specified above: + * + *

Option 1: The 'traditional' approach + *

{@code Optional result = alert.showAndWait();
+ * if (result.get() == ButtonType.OK) {
+ *     formatSystem();
+ * }}
+ * + *

Option 2: The traditional + Optional approach + *

{@code alert.showAndWait().ifPresent(response -> {
+ *     if (response == ButtonType.OK) {
+ *         formatSystem();
+ *     }
+ * });}
+ * + *

Option 3: The fully lambda approach + *

{@code alert.showAndWait()
+ *      .filter(response -> response == ButtonType.OK)
+ *      .ifPresent(response -> formatSystem());
+ * }
+ * + *

There is no better or worse option of the three listed above, so developers + * are encouraged to work to their own style preferences. The purpose of showing + * the above is to help introduce developers to the {@link Optional} API, which + * is new in Java 8 and may be foreign to many developers. + * + * @see Dialog + * @see AlertType + * @see TextInputDialog + * @see ChoiceDialog + * @since JavaFX 8u40 + */ +public class Alert extends Dialog { + + /************************************************************************** + * + * Static enums + * + **************************************************************************/ + + /** + * An enumeration containing the available, pre-built alert types that + * the {@link Alert} class can use to pre-populate various properties. + */ + public static enum AlertType { + /** + * The NONE alert type has the effect of not setting any default properties + * in the Alert. + */ + NONE, + + /** + * The INFORMATION alert type configures the Alert dialog to appear in a + * way that suggests the content of the dialog is informing the user of + * a piece of information. This includes an 'information' image, an + * appropriate title and header, and just an OK button for the user to + * click on to dismiss the dialog. + */ + INFORMATION, + + /** + * The WARNING alert type configures the Alert dialog to appear in a + * way that suggests the content of the dialog is warning the user about + * some fact or action. This includes a 'warning' image, an + * appropriate title and header, and just an OK button for the user to + * click on to dismiss the dialog. + */ + WARNING, + + /** + * The CONFIRMATION alert type configures the Alert dialog to appear in a + * way that suggests the content of the dialog is seeking confirmation from + * the user. This includes a 'confirmation' image, an + * appropriate title and header, and both OK and Cancel buttons for the + * user to click on to dismiss the dialog. + */ + CONFIRMATION, + + /** + * The ERROR alert type configures the Alert dialog to appear in a + * way that suggests that something has gone wrong. This includes an + * 'error' image, an appropriate title and header, and just an OK button + * for the user to click on to dismiss the dialog. + */ + ERROR + } + + + + /************************************************************************** + * + * Fields + * + **************************************************************************/ + + private WeakReference dialogPaneRef; + + private boolean installingDefaults = false; + private boolean hasCustomButtons = false; + private boolean hasCustomTitle = false; + private boolean hasCustomGraphic = false; + private boolean hasCustomHeaderText = false; + + private final InvalidationListener graphicListener = o -> { + if (!installingDefaults) hasCustomGraphic = true; + }; + + private final InvalidationListener headerTextListener = o -> { + if (!installingDefaults) hasCustomHeaderText = true; + }; + + private final InvalidationListener titleListener = o -> { + if (!installingDefaults) hasCustomTitle = true; + }; + + private final ListChangeListener buttonsListener = change -> { + if (!installingDefaults) hasCustomButtons = true; + }; + + + + /************************************************************************** + * + * Constructors + * + **************************************************************************/ + + /** + * Creates an alert with the given AlertType (refer to the {@link AlertType} + * documentation for clarification over which one is most appropriate). + * + *

By passing in an AlertType, default values for the + * {@link #titleProperty() title}, {@link #headerTextProperty() headerText}, + * and {@link #graphicProperty() graphic} properties are set, as well as the + * relevant {@link #getButtonTypes() buttons} being installed. Once the Alert + * is instantiated, developers are able to modify the values of the alert as + * desired. + * + *

It is important to note that the one property that does not have a + * default value set, and which therefore the developer must set, is the + * {@link #contentTextProperty() content text} property (or alternatively, + * the developer may call {@code alert.getDialogPane().setContent(Node)} if + * they want a more complex alert). If the contentText (or content) properties + * are not set, there is no useful information presented to end users. + */ + public Alert(@NamedArg("AlertType") AlertType alertType) { + this(alertType, ""); + } + + /** + * Creates an alert with the given contentText and AlertType (refer to the + * {@link AlertType} documentation for clarification over which one is most + * appropriate). + * + *

By passing in an AlertType, default values for the + * {@link #titleProperty() title}, {@link #headerTextProperty() headerText}, + * and {@link #graphicProperty() graphic} properties are set, as well as the + * relevant {@link #getButtonTypes() buttons} being installed. Once the Alert + * is instantiated, developers are able to modify the values of the alert as + * desired. + */ + public Alert(@NamedArg("AlertType") AlertType alertType, + @NamedArg("contentText") String contentText) { + this(alertType, contentText, (ButtonType[])null); + } + + /** + * Creates an alert with the given contentText, ButtonTypes, and AlertType + * (refer to the {@link AlertType} documentation for clarification over which + * one is most appropriate). + * + *

By passing in a variable number of ButtonType arguments, the developer + * is directly overriding the default buttons that will be displayed in the + * dialog, replacing the pre-defined buttons with whatever is specified in the + * varargs array. + * + *

By passing in an AlertType, default values for the + * {@link #titleProperty() title}, {@link #headerTextProperty() headerText}, + * and {@link #graphicProperty() graphic} properties are set. Once the Alert + * is instantiated, developers are able to modify the values of the alert as + * desired. + */ + public Alert(@NamedArg("AlertType") AlertType alertType, + @NamedArg("contentText") String contentText, + ButtonType... buttons) { + super(); + final DialogPane dialogPane = getDialogPane(); + dialogPane.setContentText(contentText); + + dialogPaneRef = new WeakReference<>(dialogPane); + + hasCustomButtons = buttons != null && buttons.length > 0; + if (hasCustomButtons) { + for (ButtonType btnType : buttons) { + dialogPane.getButtonTypes().addAll(btnType); + } + } + + setAlertType(alertType); + + // listening to property changes on Dialog and DialogPane + dialogPaneProperty().addListener(o -> updateListeners()); + titleProperty().addListener(titleListener); + updateListeners(); + } + + + + /************************************************************************** + * + * Properties + * + **************************************************************************/ + + /** + * When creating an Alert instance, users must pass in an {@link AlertType} + * enumeration value. It is by passing in this value that the Alert instance will + * configure itself appropriately (by setting default values for many of the + * {@link Dialog} properties, including {@link #titleProperty() title}, + * {@link #headerTextProperty() header}, and {@link #graphicProperty() graphic}, + * as well as the default {@link #getButtonTypes() buttons} that are expected in + * a dialog of the given type. + */ + // --- alertType + private final ObjectProperty alertType = new SimpleObjectProperty(null) { + protected void invalidated() { + String newTitle = ""; + String newHeader = ""; + Node newGraphic = null; + ButtonType[] newButtons = new ButtonType[] { ButtonType.OK }; + switch (getAlertType()) { + case NONE: { + newButtons = new ButtonType[] { }; + break; + } + case INFORMATION: { + newTitle = ControlResources.getString("Dialog.info.title"); + newHeader = ControlResources.getString("Dialog.info.header"); + + // TODO extract out to CSS + newGraphic = new ImageView(new Image(AccordionSkin.class.getResource("modena/dialog-information.png").toExternalForm())); + break; + } + case WARNING: { + newTitle = ControlResources.getString("Dialog.warning.title"); + newHeader = ControlResources.getString("Dialog.warning.header"); + + // TODO extract out to CSS + newGraphic = new ImageView(new Image(AccordionSkin.class.getResource("modena/dialog-warning.png").toExternalForm())); + break; + } + case ERROR: { + newTitle = ControlResources.getString("Dialog.error.title"); + newHeader = ControlResources.getString("Dialog.error.header"); + + // TODO extract out to CSS + newGraphic = new ImageView(new Image(AccordionSkin.class.getResource("modena/dialog-error.png").toExternalForm())); + break; + } + case CONFIRMATION: { + newTitle = ControlResources.getString("Dialog.confirm.title"); + newHeader = ControlResources.getString("Dialog.confirm.header"); + + // TODO extract out to CSS + newGraphic = new ImageView(new Image(AccordionSkin.class.getResource("modena/dialog-confirm.png").toExternalForm())); + newButtons = new ButtonType[] { ButtonType.OK, ButtonType.CANCEL }; + break; + } + } + + installingDefaults = true; + if (!hasCustomTitle) setTitle(newTitle); + if (!hasCustomHeaderText) setHeaderText(newHeader); + if (!hasCustomGraphic) setGraphic(newGraphic); + if (!hasCustomButtons) getButtonTypes().setAll(newButtons); + installingDefaults = false; + } + }; + + public final AlertType getAlertType() { + return alertType.get(); + } + + public final void setAlertType(AlertType alertType) { + this.alertType.setValue(alertType); + } + + public final ObjectProperty alertTypeProperty() { + return alertType; + } + + + /** + * Returns an {@link ObservableList} of all {@link ButtonType} instances that + * are currently set inside this Alert instance. A ButtonType may either be one + * of the pre-defined types (e.g. {@link ButtonType#OK}), or it may be a + * custom type (created via the {@link ButtonType#ButtonType(String)} or + * {@link ButtonType#ButtonType(String, javafx.scene.control.ButtonBar.ButtonData)} + * constructors. + * + *

Readers should refer to the {@link ButtonType} class documentation for more details, + * but at a high level, each ButtonType instance is converted to + * a Node (although most commonly a {@link Button}) via the (overridable) + * {@link DialogPane#createButton(ButtonType)} method on {@link DialogPane}. + */ + // --- buttonTypes + public final ObservableList getButtonTypes() { + return getDialogPane().getButtonTypes(); + } + + + + /************************************************************************** + * + * Private Implementation + * + **************************************************************************/ + + private void updateListeners() { + DialogPane oldPane = dialogPaneRef.get(); + + if (oldPane != null) { + oldPane.graphicProperty().removeListener(graphicListener); + oldPane.headerTextProperty().removeListener(headerTextListener); + oldPane.getButtonTypes().removeListener(buttonsListener); + } + + // listen to changes to properties that would be changed by alertType being + // changed, so that we only change values that are still at their default + // value (i.e. the user hasn't changed them, so we are free to set them + // to a new default value when the alertType changes). + + DialogPane newPane = getDialogPane(); + if (newPane != null) { + newPane.graphicProperty().addListener(graphicListener); + newPane.headerTextProperty().addListener(headerTextListener); + newPane.getButtonTypes().addListener(buttonsListener); + } + + dialogPaneRef = new WeakReference(newPane); + } +} diff --git a/modules/controls/src/main/java/javafx/scene/control/ButtonBar.java b/modules/controls/src/main/java/javafx/scene/control/ButtonBar.java new file mode 100644 --- /dev/null +++ b/modules/controls/src/main/java/javafx/scene/control/ButtonBar.java @@ -0,0 +1,612 @@ +/* + * Copyright (c) 2014, 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 javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.Node; +import javafx.scene.layout.HBox; + +import com.sun.javafx.Utils; +import com.sun.javafx.scene.control.skin.ButtonBarSkin; +import com.sun.javafx.scene.traversal.Algorithm; +import com.sun.javafx.scene.traversal.Direction; +import com.sun.javafx.scene.traversal.ParentTraversalEngine; +import com.sun.javafx.scene.traversal.TraversalContext; + +/** + * A ButtonBar is essentially a {@link HBox}, with the additional functionality + * for operating system specific button placement. In other words, any Node may + * be annotated with its type (via the {@link ButtonBar#setButtonData(Node, ButtonData)} + * method, placed inside a ButtonBar (via the {@link #getButtons()} list), and will + * then be positioned relative to all other nodes in the button list based on their + * type, as well as the overarching {@link #buttonOrderProperty() button order} + * specified for the ButtonBar. + * + * Uniform button sizing + *

By default all buttons are uniformly sized in a ButtonBar, meaning that all + * buttons take the width of the widest button. It is possible to opt-out of this + * on a per-button basis, but calling the {@link #setButtonUniformSize(Node, boolean)} method with + * a boolean value of false. + * + *

If a button is excluded from uniform sizing, it is both excluded from + * being resized away from its preferred size, and also excluded from the + * measuring process, so its size will not influence the maximum size calculated + * for all buttons in the ButtonBar. + * + *

Screenshots

+ *

Because a ButtonBar comes with built-in support for Windows, Mac OS + * and Linux, there are three screenshots shown below, with the same buttons + * laid out on each of the three operating systems. + * + *

+ * Windows:

+ * Mac OS:

+ * Linux:

+ * + *

Code Samples

+ *

Instantiating and using the ButtonBar is simple, simply do the following: + * + *

+ * {@code
+ * // Create the ButtonBar instance 
+ * ButtonBar buttonBar = new ButtonBar();
+ * 
+ * // Create the buttons to go into the ButtonBar
+ * Button yesButton = new Button("Yes");
+ * ButtonBar.setButtonData(yesButton, ButtonData.YES); 
+ * 
+ * Button noButton = new Button("No");
+ * ButtonBar.setButtonData(noButton, ButtonData.NO);
+ * 
+ * // Add buttons to the ButtonBar
+ * buttonBar.getButtons().addAll(yesButton, noButton);
+ * }
+ * + *

The code sample above will position the Yes and No buttons relative to the + * users operating system. This means that on Windows and Linux the Yes button + * will come before the No button, whereas on Mac OS it'll be No and then Yes. + * + *

In most cases the OS-specific layout is the best choice, but in cases + * where you want a custom layout, this is achieved be modifying the + * {@link #buttonOrderProperty() button order property}. These are cryptic-looking + * strings that are shorthand representations for the button order. The built-in + * orders for Windows, Mac OS and Linux are: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Windows:L_E+U+FBXI_YNOCAH_R
Mac OS:L_HE+U+FBIX_NCYOA_R
Linux:L_HE+UNYACBXIO_R
+ * + *

You should refer to the {@link ButtonData} enumeration for a description of + * what each of these characters mean. However, if your ButtonBar only consisted + * of {@link ButtonData#YES} and {@link ButtonData#NO} buttons, you always + * wanted the yes buttons before the no buttons, and you wanted the buttons to + * be {@link ButtonData#BIG_GAP right-aligned}, you could do the following: + * + *

+ * {@code
+ * // Create the ButtonBar instance 
+ * ButtonBar buttonBar = new ButtonBar();
+ * 
+ * // Set the custom button order
+ * buttonBar.setButtonOrder("+YN"); 
+ * }
+ * + * @see ButtonData + * @since JavaFX 8u40 + */ +public class ButtonBar extends Control { + + // TODO add support for BUTTON_ORDER_NONE + // TODO test and document what happens with unexpected button order strings + + /************************************************************************** + * + * Static fields + * + **************************************************************************/ + + /** + * The default button ordering on Windows. + */ + public static final String BUTTON_ORDER_WINDOWS = "L_E+U+FBXI_YNOCAH_R"; //$NON-NLS-1$ + + /** + * The default button ordering on Mac OS. + */ + public static final String BUTTON_ORDER_MAC_OS = "L_HE+U+FBIX_NCYOA_R"; //$NON-NLS-1$ + + /** + * The default button ordering on Linux (specifically, GNOME). + */ + public static final String BUTTON_ORDER_LINUX = "L_HE+UNYACBXIO_R"; //$NON-NLS-1$ + + /** + * A button ordering string that specifies there is no button ordering. In + * other words, buttons will be placed in the order that exist in the + * {@link #getButtons()} list. + */ + public static final String BUTTON_ORDER_NONE = ""; //$NON-NLS-1$ + + + + /************************************************************************** + * + * Static enumerations + * + **************************************************************************/ + + /** + * An enumeration of all available button types. By designating every button + * in a {@link ButtonBar} as one of these types, the buttons will be + * appropriately positioned relative to all other buttons in the ButtonBar. + * + *

For details on the button order code for each button type, refer to + * the javadoc comment for that type. + */ + public static enum ButtonData { + /** + * Buttons with this style tag will statically end up on the left end of the bar. + * + *

Button order code: L + */ + LEFT("L",false,false), //$NON-NLS-1$ + + /** + * Buttons with this style tag will statically end up on the right end of the bar. + * + *

Button order code: R + */ + RIGHT("R", false, false), //$NON-NLS-1$ + + /** + * A tag for the "help" button that normally is supposed to be on the right. + * + *

Button order code: H + */ + HELP("H", false, false ), //$NON-NLS-1$ + + /** + * A tag for the "help2" button that normally is supposed to be on the left. + * + *

Button order code: E + */ + HELP_2("E", false, false), //$NON-NLS-1$ + + /** + * A tag for the "yes" button. + * + *

Is default button: True + *

Button order code: Y + */ + YES("Y", false, true), //$NON-NLS-1$ + + /** + * A tag for the "no" button. + * + *

Is cancel button: True + *

Button order code: N + */ + NO("N", true, false), //$NON-NLS-1$ + + /** + * A tag for the "next" or "forward" button. + * + *

Is default button: True + *

Button order code: X + */ + NEXT_FORWARD("X", false, true), //$NON-NLS-1$ + + /** + * A tag for the "back" or "previous" button. + * + *

Button order code: B + */ + BACK_PREVIOUS("B", false, false), //$NON-NLS-1$ + + /** + * A tag for the "finish". + * + *

Is default button: True + *

Button order code: I + */ + FINISH("I", false, true), //$NON-NLS-1$ + + /** + * A tag for the "apply" button. + * + *

Button order code: A + */ + APPLY("A", false, false), //$NON-NLS-1$ + + /** + * A tag for the "cancel" or "close" button. + * + *

Is cancel button: True + *

Button order code: C + */ + CANCEL_CLOSE("C", true, false), //$NON-NLS-1$ + + /** + * A tag for the "ok" or "done" button. + * + *

Is default button: True + *

Button order code: O + */ + OK_DONE("O", false, true), //$NON-NLS-1$ + + /** + * All Uncategorized, Other, or "Unknown" buttons. Tag will be "other". + * + *

Button order code: U + */ + OTHER("U", false, false), //$NON-NLS-1$ + + + /** + * A glue push gap that will take as much space as it can and at least + * an "unrelated" gap. (Platform dependent) + * + *

Button order code: + + */ + BIG_GAP("+", false, false), //$NON-NLS-1$ + + /** + * An "unrelated" gap. (Platform dependent) + * + *

Button order code: _ (underscore) + */ + SMALL_GAP("_", false, false); //$NON-NLS-1$ + + private final String typeCode; + + private final boolean cancelButton; + private final boolean defaultButton; + + private ButtonData(String type, boolean cancelButton, boolean defaultButton) { + this.typeCode = type; + this.cancelButton = cancelButton; + this.defaultButton = defaultButton; + } + + /** + * Returns the single character code used to represent the ButtonData + * type in the {@link ButtonBar#buttonOrderProperty() button order} string. + */ + public String getTypeCode() { + return typeCode; + } + + /** + * Indicates whether buttons created from the ButtonData type should be + * the 'cancel' button in the user interface. This typically means that + * the button will respond to the escape key press, even if the button does + * not have focus. + * + *

ButtonData types that can be the cancel button have a comment stating + * this in their javadoc. + */ + public final boolean isCancelButton() { + return cancelButton; + } + + /** + * Indicates whether buttons created from the ButtonData type should be + * the 'default' button in the user interface. This typically means that + * the button will respond to enter key presses, even if the button does + * not have focus. + * + *

ButtonData types that can be the default button have a comment stating + * this in their javadoc. + */ + public final boolean isDefaultButton() { + return defaultButton; + } + } + + + /** + * Sets the given ButtonData on the given button. If this button is + * subsequently placed in a {@link ButtonBar} it will be placed in the + * correct position relative to all other buttons in the bar. + * + * @param button The button to tag with the given type. + * @param buttonData The ButtonData to designate the button as. + */ + public static void setButtonData(Node button, ButtonData buttonData) { + button.getProperties().put(ButtonBarSkin.BUTTON_DATA_PROPERTY, buttonData); + } + + /** + * Returns the previously set ButtonData property on the given button. If this + * was never set, this method will return null. + * + * @param button The button to return the previously set ButtonData for. + */ + public static ButtonData getButtonData(Node button) { + return (ButtonData) button.getProperties().get(ButtonBarSkin.BUTTON_DATA_PROPERTY); + } + + /** + * By default all buttons are uniformly sized in a ButtonBar, meaning that all + * buttons take the width of the widest button. It is possible to opt-out of this + * on a per-button basis, but calling the setButtonUniformSize method with + * a boolean value of false. + * + *

If a button is excluded from uniform sizing, it is both excluded from + * being resized away from its preferred size, and also excluded from the + * measuring process, so its size will not influence the maximum size calculated + * for all buttons in the ButtonBar. + * + * @param button The button to include / exclude from uniform sizing. + * @param uniformSize Boolean true to force uniform sizing on the button, + * false to exclude the button from uniform sizing. + */ + public static void setButtonUniformSize(Node button, boolean uniformSize) { + // we store the false, but remove the true (as the isButtonUniformSize + // method returns true by default) + if (uniformSize) { + button.getProperties().remove(ButtonBarSkin.BUTTON_SIZE_INDEPENDENCE); + } else { + button.getProperties().put(ButtonBarSkin.BUTTON_SIZE_INDEPENDENCE, uniformSize); + } + } + + /** + * Returns whether the given node is part of the uniform sizing calculations + * or not. By default all nodes that have not opted out (via + * {@link #setButtonUniformSize(Node, boolean)}) will return true here. + */ + public static boolean isButtonUniformSize(Node button) { + return (boolean) button.getProperties().getOrDefault(ButtonBarSkin.BUTTON_SIZE_INDEPENDENCE, true); + } + + + + /************************************************************************** + * + * Private fields + * + **************************************************************************/ + + private ObservableList buttons = FXCollections.observableArrayList(); + + + + /************************************************************************** + * + * Constructors + * + **************************************************************************/ + + /** + * Creates a default ButtonBar instance using the default properties for + * the users operating system. + */ + public ButtonBar() { + this(null); + } + + /** + * Creates a ButtonBar with the given button order (refer to + * {@link #buttonOrderProperty()} for more information). + * + * @param buttonOrder The button order to use in this button bar instance. + */ + public ButtonBar(final String buttonOrder) { + getStyleClass().add("button-bar"); //$NON-NLS-1$ + + final boolean buttonOrderEmpty = buttonOrder == null || buttonOrder.isEmpty(); + + if (Utils.isMac()) { + setButtonOrder(buttonOrderEmpty ? BUTTON_ORDER_MAC_OS : buttonOrder); + setButtonMinWidth(70); + } else if (Utils.isUnix()) { + setButtonOrder(buttonOrderEmpty ? BUTTON_ORDER_LINUX : buttonOrder); + setButtonMinWidth(85); + } else { + // windows by default + setButtonOrder(buttonOrderEmpty ? BUTTON_ORDER_WINDOWS : buttonOrder); + setButtonMinWidth(75); + } + + // fix for issue where initial focus / tab navigation got lost on the ButtonBar - + // this code moves the focus onto the correct button inside the ButtonBar + focusedProperty().addListener(new InvalidationListener() { + @Override public void invalidated(Observable o) { + if (! isFocused()) return; + + boolean focusSet = false; + for (Node button : getButtons()) { + if (button instanceof Button && ((Button)button).isDefaultButton()) { + button.requestFocus(); + focusSet = true; + break; + } + } + + // if we are here there is no default button, so for now we + // will simply give focus to the first button (this can + // definitely be improved in the future!) + if (! focusSet && ! getButtons().isEmpty()) { + getButtons().get(0).requestFocus(); + } + } + }); + + ParentTraversalEngine engine = new ParentTraversalEngine(this, new Algorithm() { + @Override public Node selectLast(TraversalContext context) { + return getButtons().get(0); + } + + @Override public Node selectFirst(TraversalContext context) { + return getButtons().get(getButtons().size() - 1); + } + + @Override public Node select(Node node, Direction direction, TraversalContext context) { + if (ButtonBar.this.equals(node)) { + if (direction == null || direction.equals(Direction.NEXT)) { + // Sends the focus to the first button in the button bar + return getButtons().get(0); + } else if (direction.equals(Direction.PREVIOUS)) { + // Sends the focus to the last button in the button bar + return getButtons().get(getButtons().size() - 1); + } + } + return null; + } + }); + setImpl_traversalEngine(engine); + // end of focus / traversal fix + } + + + + /************************************************************************** + * + * Public API + * + **************************************************************************/ + + /** + * {@inheritDoc} + */ + @Override protected Skin createDefaultSkin() { + return new ButtonBarSkin(this); + } + + /** + * Placing buttons inside this ObservableList will instruct the ButtonBar + * to position them relative to each other based on their specified + * {@link ButtonData}. To set the ButtonData for a button, simply call + * {@link ButtonBar#setButtonData(Node, ButtonData)}, passing in the + * relevant ButtonData. + * + * @return A list containing all buttons currently in the button bar, and + * allowing for further buttons to be added or removed. + */ + public final ObservableList getButtons() { + return buttons; + } + + + + /************************************************************************** + * + * Properties + * + **************************************************************************/ + + // --- Button order + /** + * The order for the typical buttons in a standard button bar. It is + * one letter per button type, and the applicable options are part of + * {@link ButtonData}. Default button orders for operating systems are also + * available: {@link #BUTTON_ORDER_WINDOWS}, {@link #BUTTON_ORDER_MAC_OS}, + * {@link #BUTTON_ORDER_LINUX}. + */ + public final StringProperty buttonOrderProperty() { + return buttonOrderProperty; + } + private final StringProperty buttonOrderProperty = + new SimpleStringProperty(this, "buttonOrder"); //$NON-NLS-1$ + + /** + * Sets the {@link #buttonOrderProperty() button order} + * @param buttonOrder The currently set button order, which by default will + * be the OS-specific button order. + */ + public final void setButtonOrder(String buttonOrder) { + buttonOrderProperty.set(buttonOrder); + } + + /** + * Returns the current {@link #buttonOrderProperty() button order}. + * @return The current {@link #buttonOrderProperty() button order}. + */ + public final String getButtonOrder() { + return buttonOrderProperty.get(); + } + + + // --- button min width + /** + * Specifies the minimum width of all buttons placed in this button bar. + */ + public final DoubleProperty buttonMinWidthProperty() { + return buttonMinWidthProperty; + } + private final DoubleProperty buttonMinWidthProperty = + new SimpleDoubleProperty(this, "buttonMinWidthProperty"); //$NON-NLS-1$ + + /** + * Sets the minimum width of all buttons placed in this button bar. + */ + public final void setButtonMinWidth(double value) { + buttonMinWidthProperty.set(value); + } + + /** + * Returns the minimum width of all buttons placed in this button bar. + */ + public final double getButtonMinWidth() { + return buttonMinWidthProperty.get(); + } + + + + /************************************************************************** + * + * Implementation + * + **************************************************************************/ + + + + + /************************************************************************** + * + * Support classes / enums + * + **************************************************************************/ + +} diff --git a/modules/controls/src/main/java/javafx/scene/control/ButtonType.java b/modules/controls/src/main/java/javafx/scene/control/ButtonType.java new file mode 100644 --- /dev/null +++ b/modules/controls/src/main/java/javafx/scene/control/ButtonType.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2014, 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 com.sun.javafx.scene.control.skin.resources.ControlResources; + +import javafx.beans.NamedArg; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar.ButtonData; + +/** + * The ButtonType class is used as part of the JavaFX {@link Dialog} API (more + * specifically, the {@link DialogPane} API) to specify which buttons should be + * shown to users in the dialogs. Refer to the {@link DialogPane} class javadoc + * for more information on how to use this class. + * + * @see Alert + * @see Dialog + * @see DialogPane + * @since JavaFX 8u40 + */ +public final class ButtonType { + + /** + * A pre-defined {@link ButtonType} that displays "Apply" and has a + * {@link ButtonData} of {@link ButtonData#APPLY}. + */ + public static final ButtonType APPLY = new ButtonType( + ControlResources.getString("Dialog.apply.button"), ButtonData.APPLY); + + /** + * A pre-defined {@link ButtonType} that displays "OK" and has a + * {@link ButtonData} of {@link ButtonData#OK_DONE}. + */ + public static final ButtonType OK = new ButtonType( + ControlResources.getString("Dialog.ok.button"), ButtonData.OK_DONE); + + /** + * A pre-defined {@link ButtonType} that displays "Cancel" and has a + * {@link ButtonData} of {@link ButtonData#CANCEL_CLOSE}. + */ + public static final ButtonType CANCEL = new ButtonType( + ControlResources.getString("Dialog.cancel.button"), ButtonData.CANCEL_CLOSE); + + /** + * A pre-defined {@link ButtonType} that displays "Close" and has a + * {@link ButtonData} of {@link ButtonData#CANCEL_CLOSE}. + */ + public static final ButtonType CLOSE = new ButtonType( + ControlResources.getString("Dialog.close.button"), ButtonData.CANCEL_CLOSE); + + /** + * A pre-defined {@link ButtonType} that displays "Yes" and has a + * {@link ButtonData} of {@link ButtonData#YES}. + */ + public static final ButtonType YES = new ButtonType( + ControlResources.getString("Dialog.yes.button"), ButtonData.YES); + + /** + * A pre-defined {@link ButtonType} that displays "No" and has a + * {@link ButtonData} of {@link ButtonData#NO}. + */ + public static final ButtonType NO = new ButtonType( + ControlResources.getString("Dialog.no.button"), ButtonData.NO); + + /** + * A pre-defined {@link ButtonType} that displays "Finish" and has a + * {@link ButtonData} of {@link ButtonData#FINISH}. + */ + public static final ButtonType FINISH = new ButtonType( + ControlResources.getString("Dialog.finish.button"), ButtonData.FINISH); + + /** + * A pre-defined {@link ButtonType} that displays "Next" and has a + * {@link ButtonData} of {@link ButtonData#NEXT_FORWARD}. + */ + public static final ButtonType NEXT = new ButtonType( + ControlResources.getString("Dialog.next.button"), ButtonData.NEXT_FORWARD); + + /** + * A pre-defined {@link ButtonType} that displays "Previous" and has a + * {@link ButtonData} of {@link ButtonData#BACK_PREVIOUS}. + */ + public static final ButtonType PREVIOUS = new ButtonType( + ControlResources.getString("Dialog.previous.button"), ButtonData.BACK_PREVIOUS); + + private final String text; + private final ButtonData buttonData; + + + /** + * Creates a ButtonType instance with the given text, and the ButtonData set + * as {@link ButtonData#OTHER}. + * + * @param text The string to display in the text property of controls such + * as {@link Button#textProperty() Button}. + */ + public ButtonType(@NamedArg("text") String text) { + this(text, ButtonData.OTHER); + } + + /** + * Creates a ButtonType instance with the given text, and the ButtonType set + * as specified. + * + * @param text The string to display in the text property of controls such + * as {@link Button#textProperty() Button}. + * @param buttonData The type of Button that should be created from this ButtonType. + */ + public ButtonType(@NamedArg("text") String text, + @NamedArg("buttonData") ButtonData buttonData) { + this.text = text; + this.buttonData = buttonData; + } + + /** + * Returns the ButtonData specified for this ButtonType in the constructor. + */ + public final ButtonData getButtonData() { return this.buttonData; } + + /** + * Returns the text specified for this ButtonType in the constructor; + */ + public final String getText() { return text; } + + /** {@inheritDoc} */ + @Override public String toString() { + return "ButtonType [text=" + getText() + ", buttonData=" + getButtonData() + "]"; + } +} diff --git a/modules/controls/src/main/java/javafx/scene/control/ChoiceDialog.java b/modules/controls/src/main/java/javafx/scene/control/ChoiceDialog.java new file mode 100644 --- /dev/null +++ b/modules/controls/src/main/java/javafx/scene/control/ChoiceDialog.java @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2014, 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.util.Arrays; +import java.util.Collection; + +import com.sun.javafx.scene.control.skin.AccordionSkin; +import com.sun.javafx.scene.control.skin.resources.ControlResources; +import javafx.application.Platform; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.collections.ObservableList; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; + +/** + * A dialog that shows a list of choices to the user, from which they can pick + * one item at most. + * + * @see Dialog + * @param The type of the items to show to the user, and the type that is returned + * via {@link #getResult()} when the dialog is dismissed. + * @since JavaFX 8u40 + */ +public class ChoiceDialog extends Dialog { + + /************************************************************************** + * + * Fields + * + **************************************************************************/ + + private final GridPane grid; + private final Label label; + private final ComboBox comboBox; + private final T defaultChoice; + + + + /************************************************************************** + * + * Constructors + * + **************************************************************************/ + + /** + * Creates a default, empty instance of ChoiceDialog with no set items and a + * null default choice. Users of this constructor will subsequently need to + * call {@link #getItems()} to specify which items to show to the user. + */ + public ChoiceDialog() { + this((T)null, (T[])null); + } + + /** + * Creates a new ChoiceDialog instance with the first argument specifying the + * default choice that should be shown to the user, and all following arguments + * considered a varargs array of all available choices for the user. It is + * expected that the defaultChoice be one of the elements in the choices varargs + * array. If this is not true, then defaultChoice will be set to null and the + * dialog will show with the initial choice set to the first item in the list + * of choices. + * + * @param defaultChoice The item to display as the pre-selected choice in the dialog. + * This item must be contained within the choices varargs array. + * @param choices All possible choices to present to the user. + */ + public ChoiceDialog(T defaultChoice, @SuppressWarnings("unchecked") T... choices) { + this(defaultChoice, Arrays.asList(choices)); + } + + /** + * Creates a new ChoiceDialog instance with the first argument specifying the + * default choice that should be shown to the user, and the second argument + * specifying a collection of all available choices for the user. It is + * expected that the defaultChoice be one of the elements in the choices + * collection. If this is not true, then defaultChoice will be set to null and the + * dialog will show with the initial choice set to the first item in the list + * of choices. + * + * @param defaultChoice The item to display as the pre-selected choice in the dialog. + * This item must be contained within the choices varargs array. + * @param choices All possible choices to present to the user. + */ + public ChoiceDialog(T defaultChoice, Collection choices) { + final DialogPane dialogPane = getDialogPane(); + + // -- grid + this.grid = new GridPane(); + this.grid.setHgap(10); + this.grid.setMaxWidth(Double.MAX_VALUE); + + // -- label + label = DialogPane.createContentLabel(dialogPane.getContentText()); + label.setPrefWidth(Region.USE_COMPUTED_SIZE); + label.textProperty().bind(dialogPane.contentTextProperty()); + + dialogPane.contentTextProperty().addListener(o -> updateGrid()); + + setTitle(ControlResources.getString("Dialog.confirm.title")); + dialogPane.setHeaderText(ControlResources.getString("Dialog.confirm.header")); + + // TODO extract out to CSS + dialogPane.setGraphic(new ImageView(new Image(AccordionSkin.class.getResource("modena/dialog-confirm.png").toExternalForm()))); + dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + + final double MIN_WIDTH = 150; + + comboBox = new ComboBox(); + comboBox.setMinWidth(MIN_WIDTH); + if (choices != null) { + comboBox.getItems().addAll(choices); + } + comboBox.setMaxWidth(Double.MAX_VALUE); + GridPane.setHgrow(comboBox, Priority.ALWAYS); + GridPane.setFillWidth(comboBox, true); + + this.defaultChoice = comboBox.getItems().contains(defaultChoice) ? defaultChoice : null; + + if (defaultChoice == null) { + comboBox.getSelectionModel().selectFirst(); + } else { + comboBox.getSelectionModel().select(defaultChoice); + } + + updateGrid(); + + setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? getSelectedItem() : null); + } + + + + /************************************************************************** + * + * Public API + * + **************************************************************************/ + + /** + * Returns the currently selected item in the dialog. + */ + public final T getSelectedItem() { + return comboBox.getSelectionModel().getSelectedItem(); + } + + /** + * Returns the property representing the currently selected item in the dialog. + */ + public final ReadOnlyObjectProperty selectedItemProperty() { + return comboBox.getSelectionModel().selectedItemProperty(); + } + + /** + * Sets the currently selected item in the dialog. + * @param item The item to select in the dialog. + */ + public final void setSelectedItem(T item) { + comboBox.getSelectionModel().select(item); + } + + /** + * Returns the list of all items that will be displayed to users. This list + * can be modified by the developer to add, remove, or reorder the items + * to present to the user. + */ + public final ObservableList getItems() { + return comboBox.getItems(); + } + + /** + * Returns the default choice that was specified in the constructor. + */ + public final T getDefaultChoice() { + return defaultChoice; + } + + + + /************************************************************************** + * + * Private Implementation + * + **************************************************************************/ + + private void updateGrid() { + grid.getChildren().clear(); + + grid.add(label, 0, 0); + grid.add(comboBox, 1, 0); + getDialogPane().setContent(grid); + + Platform.runLater(() -> comboBox.requestFocus()); + } +} diff --git a/modules/controls/src/main/java/javafx/scene/control/Dialog.java b/modules/controls/src/main/java/javafx/scene/control/Dialog.java new file mode 100644 --- /dev/null +++ b/modules/controls/src/main/java/javafx/scene/control/Dialog.java @@ -0,0 +1,973 @@ +/* + * Copyright (c) 2014, 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.Optional; + +import javafx.beans.InvalidationListener; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.ListChangeListener; +import javafx.css.PseudoClass; +import javafx.event.Event; +import javafx.event.EventDispatchChain; +import javafx.event.EventHandler; +import javafx.event.EventTarget; +import javafx.scene.Node; +import javafx.scene.control.ButtonBar.ButtonData; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.stage.Window; +import javafx.util.Callback; + +import com.sun.javafx.event.EventHandlerManager; + +/** + * A Dialog in JavaFX wraps a {@link DialogPane} and provides the necessary API + * to present it to end users. In JavaFX 8u40, this essentially means that the + * {@link DialogPane} is shown to users inside a {@link Stage}, but future releases + * may offer alternative options (such as 'lightweight' or 'internal' dialogs). + * This API therefore is intentionally ignorant of the underlying implementation, + * and attempts to present a common API for all possible implementations. + * + *

The Dialog class has a single generic type, R, which is used to represent + * the type of the {@link #resultProperty() result} property (and also, how to + * convert from {@link ButtonType} to R, through the use of the + * {@link #resultConverterProperty() result converter} {@link Callback}). + * + *

Critical note: It is critical that all developers who choose + * to create their own dialogs by extending the Dialog class understand the + * importance of the {@link #resultConverterProperty() result converter} property. + * A result converter must always be set, whenever the R type is not + * {@link Void} or {@link ButtonType}. If this is not heeded, developers will find + * that they get ClassCastExceptions in their code, for failure to convert from + * {@link ButtonType} via the {@link #resultConverterProperty() result converter}. + * + *

It is likely that most developers would be better served using either the + * {@link Alert} class (for pre-defined, notification-style alerts), or either of + * the two pre-built dialogs ({@link TextInputDialog} and {@link ChoiceDialog}), + * depending on their needs. + * + *

Once a Dialog is instantiated, the next step is to configure it. Almost + * all properties on Dialog are not related to the content of the Dialog, the + * only exceptions are {@link #contentTextProperty()}, + * {@link #headerTextProperty()}, and {@link #graphicProperty()}, and these + * properties are simply forwarding API onto the respective properties on the + * {@link DialogPane} stored in the {@link #dialogPaneProperty() dialog pane} + * property. These three properties are forwarded from DialogPane for developer + * convenience. For developers wanting to configure their dialog, they will in many + * cases be required to use code along the lines of + * {@code dialog.getDialogPane().setExpandableContent(node)}. + * + *

After configuring these properties, all that remains is to consider whether + * the buttons (created using {@link ButtonType} and the + * {@link DialogPane#createButton(ButtonType)} method) are fully configured. + * 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 DialogPane#lookupButton(ButtonType)} method with the ButtonType + * (assuming it has already been set in the {@link DialogPane#getButtonTypes()} + * list. The returned Node is typically of type {@link Button}, but this depends + * on if the {@link DialogPane#createButton(ButtonType)} method has been overridden. A + * typical approach is therefore along the following lines: + * + *

{@code ButtonType loginButtonType = new ButtonType("Login", ButtonData.OK_DONE);
+ * Dialog dialog = new Dialog<>();
+ * dialog.getDialogPane().getButtonTypes().add(loginButtonType);
+ * boolean disabled = false; // computed based on content of text fields, for example
+ * dialog.getDialogPane().lookupButton(loginButtonType).setDisable(disabled);}
+ * + *

Once a Dialog is instantiated and fully configured, the next step is to + * show it. More often than not, dialogs are shown in a modal and blocking + * fashion. 'Modal' means that the dialog prevents user interaction with the + * owning application whilst it is showing, and 'blocking' means that code + * execution stops at the point in which the dialog is shown. This means that + * you can show a dialog, await the user response, and then continue running the + * code that directly follows the show call, giving developers the ability to + * immediately deal with the user input from the dialog (if relevant). + * + *

JavaFX dialogs are modal by default (you can change this via the + * {@link #initModality(javafx.stage.Modality)} API). To specify whether you want + * blocking or non-blocking dialogs, developers simply choose to call + * {@link #showAndWait()} or {@link #show()} (respectively). By default most + * developers should choose to use {@link #showAndWait()}, given the ease of + * coding in these situations. Shown below is three code snippets, showing three + * equally valid ways of showing a dialog: + * + *

Option 1: The 'traditional' approach + *

{@code Optional result = dialog.showAndWait();
+ * if (result.get() == ButtonType.OK) {
+ *     formatSystem();
+ * }}
+ * + *

Option 2: The traditional + Optional approach + *

{@code dialog.showAndWait().ifPresent(response -> {
+ *     if (response == ButtonType.OK) {
+ *         formatSystem();
+ *     }
+ * });}
+ * + *

Option 3: The fully lambda approach + *

{@code dialog.showAndWait()
+ *      .filter(response -> response == ButtonType.OK)
+ *      .ifPresent(response -> formatSystem());}
+ * + *

There is no better or worse option of the three listed above, so developers + * are encouraged to work to their own style preferences. The purpose of showing + * the above is to help introduce developers to the {@link Optional} API, which + * is new in Java 8 and may be foreign to many developers. + * + *

It is important to understand what happens when a Dialog is closed, and + * also how a Dialog can be closed, especially in abnormal closing situations + * (such as when the 'X' button is clicked in a dialogs title bar, or when + * operating system specific keyboard shortcuts (such as alt-F4 on Windows) + * are entered). Fortunately, the outcome is well-defined in these situations, + * and can be best summarised in the following bullet points: + * + *

    + *
  • JavaFX dialogs can only be closed 'abnormally' (as defined above) in + * two situations: + *
      + *
    1. When the dialog only has one button, or + *
    2. When the dialog has multiple buttons, as long as one of them has + * a {@link ButtonType} whose {@link ButtonData} is of type + * {@link ButtonData#CANCEL_CLOSE}. + *
    + *
  • In all other situations, the dialog will refuse to respond to all + * close requests, remaining open until the user clicks on one of the available + * buttons in the {@link DialogPane} area of the dialog. + *
  • If a dialog is closed abnormally, and if the dialog contains a button + * which has a {@link ButtonData} of {@link ButtonData#CANCEL_CLOSE}, + * the dialog will attempt to set the {@link #resultProperty() result} property + * to whatever value is returned from calling the + * {@link #resultConverterProperty() result converter} with the first matching + * {@link ButtonType}. + *
  • If for any reason the result converter returns null, or if the dialog + * is closed when on one non-cancel button is present, the + * {@link #resultProperty() result} property will be null, and the + * {@link #showAndWait()} method will return {@link Optional#empty()}. This + * later point means that, if you use either of option 2 or option 3 (as + * presented earlier in this class documentation), the + * {@link Optional#ifPresent(java.util.function.Consumer)} lambda will never + * be called, and code will continue executing as if the dialog had not + * returned any value at all. + *
+ * + * @param The return type of the dialog, via the + * {@link #resultProperty() result} property. + * @see Alert + * @see TextInputDialog + * @see ChoiceDialog + * @since JavaFX 8u40 + */ +public class Dialog implements EventTarget { + + /************************************************************************** + * + * Static fields + * + **************************************************************************/ + + + + + /************************************************************************** + * + * Static methods + * + **************************************************************************/ + + + + /************************************************************************** + * + * Private fields + * + **************************************************************************/ + + private final FXDialog dialog; + + // used to indicate upwards from FXDialog implementations into Dialog whether + // the dialog was closed normally (i.e. via buttons) or abnormally (i.e. via + // the top-right cancel button, alt-f4, etc). + boolean closeWasNormal; + + + + /************************************************************************** + * + * Constructors + * + **************************************************************************/ + + /** + * Creates a dialog without a specified owner. + */ + public Dialog() { + this.dialog = new HeavyweightDialog(this); + setDialogPane(new DialogPane()); + initModality(Modality.APPLICATION_MODAL); + setResizable(true); + } + + + + /************************************************************************** + * + * Abstract methods + * + **************************************************************************/ + + + + + /************************************************************************** + * + * Public API + * + **************************************************************************/ + + /** + * Shows the dialog but does not wait for a user response (in other words, + * this brings up a non-blocking dialog). Users of this API must either + * poll the {@link #resultProperty() result property}, or else add a listener + * to the result property to be informed of when it is set. + */ + public final void show() { + Event.fireEvent(this, new DialogEvent(this, DialogEvent.DIALOG_SHOWING)); + dialog.setDialogPane(getDialogPane()); + dialog.sizeToScene(); + Node root = dialog.getRoot(); + root.pseudoClassStateChanged(HEADER_PSEUDO_CLASS, getDialogPane().hasHeader()); + root.pseudoClassStateChanged(NO_HEADER_PSEUDO_CLASS, !getDialogPane().hasHeader()); + + dialog.show(); + + Event.fireEvent(this, new DialogEvent(this, DialogEvent.DIALOG_SHOWN)); + } + + /** + * Shows the dialog and waits for the user response (in other words, brings + * up a blocking dialog, with the returned value the users input). + * + * @return An {@link Optional} that contains the {@link #resultProperty() result}. + * Refer to the {@link Dialog} class documentation for more detail. + */ + public final Optional showAndWait() { + Event.fireEvent(this, new DialogEvent(this, DialogEvent.DIALOG_SHOWING)); + + final DialogPane dialogPane = getDialogPane(); + dialog.setDialogPane(dialogPane); + dialog.sizeToScene(); + Node root = dialog.getRoot(); + root.pseudoClassStateChanged(HEADER_PSEUDO_CLASS, getDialogPane().hasHeader()); + root.pseudoClassStateChanged(NO_HEADER_PSEUDO_CLASS, !getDialogPane().hasHeader()); + + // this is slightly odd - we fire the SHOWN event before the show() + // call, so that users get the event before the dialog blocks + Event.fireEvent(this, new DialogEvent(this, DialogEvent.DIALOG_SHOWN)); + + dialog.showAndWait(); + + return Optional.ofNullable(getResult()); + } + + /** + * Hides the dialog. + */ + public final void close() { + // This code is called just before close, and ONLY in cases where the + // dialog was closed abnormally (as represented by closeWasNormal). + // In these cases, and where the dialog had a cancel button, we call + // into the result converter to see what to do. This is used primarily + // to handle the requirement that the X button has the same result as + // clicking the cancel button. + if (! closeWasNormal) { + R result = getResult(); + if (result == null) { + ButtonType cancelButton = null; + for (ButtonType button : getDialogPane().getButtonTypes()) { + if (button.getButtonData() == ButtonData.CANCEL_CLOSE) { + cancelButton = button; + break; + } + } + + Callback resultConverter = getResultConverter(); + if (cancelButton != null && resultConverter != null) { + result = resultConverter.call(cancelButton); + setResult(result); + } + } + + closeWasNormal = true; + } + + // start normal closing process + Event.fireEvent(this, new DialogEvent(this, DialogEvent.DIALOG_HIDING)); + + DialogEvent closeRequestEvent = new DialogEvent(this, DialogEvent.DIALOG_CLOSE_REQUEST); + Event.fireEvent(this, closeRequestEvent); + if (closeRequestEvent.isConsumed()) { + return; + } + + dialog.close(true); + + Event.fireEvent(this, new DialogEvent(this, DialogEvent.DIALOG_HIDDEN)); + } + + /** + * closes the dialog. + */ + public final void hide() { + close(); + } + + /** + * Specifies the modality for this dialog. This must be done prior to making + * the dialog visible. The modality is one of: Modality.NONE, + * Modality.WINDOW_MODAL, or Modality.APPLICATION_MODAL. + * + * @param modality the modality for this dialog. + * + * @throws IllegalStateException if this property is set after the dialog + * has ever been made visible. + * + * @defaultValue Modality.APPLICATION_MODAL + */ + public final void initModality(Modality modality) { + dialog.initModality(modality); + } + + /** + * Retrieves the modality attribute for this dialog. + * + * @return the modality. + */ + public final Modality getModality() { + return dialog.getModality(); + } + + /** + * Specifies the style for this dialog. This must be done prior to making + * the dialog visible. The style is one of: StageStyle.DECORATED, + * StageStyle.UNDECORATED, StageStyle.TRANSPARENT, StageStyle.UTILITY, + * or StageStyle.UNIFIED. + * + * @param style the style for this dialog. + * + * @throws IllegalStateException if this property is set after the dialog + * has ever been made visible. + * + * @defaultValue StageStyle.DECORATED + */ + public final void initStyle(StageStyle style) { + dialog.initStyle(style); + } + + /** + * Specifies the owner {@link Window} for this dialog, or null for a top-level, + * unowned dialog. This must be done prior to making the dialog visible. + * + * @param window the owner {@link Window} for this dialog. + * + * @throws IllegalStateException if this property is set after the dialog + * has ever been made visible. + * + * @defaultValue null + */ + public final void initOwner(Window window) { + dialog.initOwner(window); + } + + /** + * Retrieves the owner Window for this dialog, or null for an unowned dialog. + * + * @return the owner Window. + */ + public final Window getOwner() { + return dialog.getOwner(); + } + + + + /************************************************************************** + * + * Properties + * + **************************************************************************/ + + // --- dialog Pane + /** + * The root node of the dialog, the {@link DialogPane} contains all visual + * elements shown in the dialog. As such, it is possible to completely adjust + * the display of the dialog by modifying the existing dialog pane or creating + * a new one. + */ + private ObjectProperty dialogPane = new SimpleObjectProperty(this, "dialogPane", new DialogPane()) { + final InvalidationListener expandedListener = o -> { + DialogPane dialogPane = getDialogPane(); + if (dialogPane == null) return; + + final Node content = dialogPane.getExpandableContent(); + final boolean isExpanded = content == null ? false : content.isVisible(); + setResizable(isExpanded); + + Dialog.this.dialog.sizeToScene(); + }; + + WeakReference dialogPaneRef = new WeakReference(null); + + protected void invalidated() { + DialogPane oldDialogPane = dialogPaneRef.get(); + if (oldDialogPane != null) { + // clean up + oldDialogPane.expandedProperty().removeListener(expandedListener); + oldDialogPane.setDialog(null); + } + + final DialogPane newDialogPane = getDialogPane(); + + if (newDialogPane != null) { + newDialogPane.setDialog(Dialog.this); + + // if the buttons change, we dynamically update the dialog + newDialogPane.getButtonTypes().addListener((ListChangeListener) c -> { + newDialogPane.requestLayout(); + }); + newDialogPane.expandedProperty().addListener(expandedListener); + newDialogPane.requestLayout(); + } + + // push the new dialog down into the implementation for rendering + dialog.setDialogPane(newDialogPane); + + dialogPaneRef = new WeakReference(newDialogPane); + } + }; + + public final ObjectProperty dialogPaneProperty() { + return dialogPane; + } + + public final DialogPane getDialogPane() { + return dialogPane.get(); + } + + public final void setDialogPane(DialogPane value) { + dialogPane.set(value); + } + + + // --- content text (forwarded from DialogPane) + /** + * A property representing the content text for the dialog pane. The content text + * is lower precedence than the {@link DialogPane#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 getDialogPane().contentTextProperty(); + } + + /** + * Returns the currently-set content text for this DialogPane. + */ + public final String getContentText() { + return getDialogPane().getContentText(); + } + + /** + * Sets the string to show in the dialog content area. Note that the content text + * is lower precedence than the {@link DialogPane#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) { + getDialogPane().setContentText(contentText); + } + + + // --- header text (forwarded from DialogPane) + /** + * A property representing the header text for the dialog pane. The header text + * is lower precedence than the {@link DialogPane#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. + */ + public final StringProperty headerTextProperty() { + return getDialogPane().headerTextProperty(); + } + + /** + * Returns the currently-set header text for this DialogPane. + */ + public final String getHeaderText() { + return getDialogPane().getHeaderText(); + } + + /** + * Sets the string to show in the dialog header area. Note that the header text + * is lower precedence than the {@link DialogPane#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. + */ + public final void setHeaderText(String headerText) { + getDialogPane().setHeaderText(headerText); + } + + + // --- graphic (forwarded from DialogPane) + /** + * The dialog graphic, presented either in the header, if one is showing, or + * to the left of the {@link DialogPane#contentProperty() content}. + * + * @return An ObjectProperty wrapping the current graphic. + */ + public final ObjectProperty graphicProperty() { + return getDialogPane().graphicProperty(); + } + + public final Node getGraphic() { + return getDialogPane().getGraphic(); + } + + /** + * Sets the dialog graphic, which will be displayed either in the header, if + * one is showing, or to the left of the {@link DialogPane#contentProperty() content}. + * + * @param graphic + * The new dialog graphic, or null if no graphic should be shown. + */ + public final void setGraphic(Node graphic) { + getDialogPane().setGraphic(graphic); + } + + + // --- result + private final ObjectProperty resultProperty = new SimpleObjectProperty() { + protected void invalidated() { + close(); + } + }; + + /** + * A property representing what has been returned from the dialog. A result + * is generated through the {@link #resultConverterProperty() result converter}, + * which is intended to convert from the {@link ButtonType} that the user + * clicked on into a value of type R. Refer to the {@link Dialog} class + * JavaDoc for more details. + */ + public final ObjectProperty resultProperty() { + return resultProperty; + } + + public final R getResult() { + return resultProperty().get(); + } + + public final void setResult(R value) { + this.resultProperty().set(value); + } + + + // --- result converter + private final ObjectProperty> resultConverterProperty + = new SimpleObjectProperty<>(this, "resultConverter"); + + /** + * API to convert the {@link ButtonType} that the user clicked on into a + * result that can be returned via the {@link #resultProperty() result} + * property. This is necessary as {@link ButtonType} represents the visual + * button within the dialog, and do not know how to map themselves to a valid + * result - that is a requirement of the dialog implementation by making use + * of the result converter. In some cases, the result type of a Dialog + * subclass is ButtonType (which means that the result converter can be null), + * but in some cases (where the result type, R, is not ButtonType or Void), + * this callback must be specified. + */ + public final ObjectProperty> resultConverterProperty() { + return resultConverterProperty; + } + + public final Callback getResultConverter() { + return resultConverterProperty().get(); + } + + public final void setResultConverter(Callback value) { + this.resultConverterProperty().set(value); + } + + + // --- showing + /** + * Represents whether the dialog is currently showing. + */ + public final ReadOnlyBooleanProperty showingProperty() { + return dialog.showingProperty(); + } + + /** + * Returns whether or not the dialog is showing. + * + * @return true if dialog is showing. + */ + public final boolean isShowing() { + return showingProperty().get(); + } + + + // --- resizable + /** + * Represents whether the dialog is resizable. + */ + public final BooleanProperty resizableProperty() { + return dialog.resizableProperty(); + } + + /** + * Returns whether or not the dialog is resizable. + * + * @return true if dialog is resizable. + */ + public final boolean isResizable() { + return resizableProperty().get(); + } + + /** + * Sets whether the dialog can be resized by the user. + * Resizable dialogs can also be maximized ( maximize button + * becomes visible) + * + * @param resizable true if dialog should be resizable. + */ + public final void setResizable(boolean resizable) { + resizableProperty().set(resizable); + } + + + // --- width + /** + * Property representing the width of the dialog. + */ + public final ReadOnlyDoubleProperty widthProperty() { + return dialog.widthProperty(); + } + + /** + * Returns the width of the dialog. + */ + public final double getWidth() { + return widthProperty().get(); + } + + /** + * Sets the width of the dialog. + */ + public final void setWidth(double width) { + dialog.setWidth(width); + } + + + // --- height + /** + * Property representing the height of the dialog. + */ + public final ReadOnlyDoubleProperty heightProperty() { + return dialog.heightProperty(); + } + + /** + * Returns the height of the dialog. + */ + public final double getHeight() { + return heightProperty().get(); + } + + /** + * Sets the height of the dialog. + */ + public final void setHeight(double height) { + dialog.setHeight(height); + } + + + // --- title + /** + * Return the titleProperty of the dialog. + */ + public final StringProperty titleProperty(){ + return this.dialog.titleProperty(); + } + + /** + * Return the title of the dialog. + */ + public final String getTitle(){ + return this.dialog.titleProperty().get(); + } + /** + * Change the Title of the dialog. + * @param title + */ + public final void setTitle(String title){ + this.dialog.titleProperty().set(title); + } + + + // --- x + public final double getX() { + return dialog.getX(); + } + + public final void setX(double x) { + dialog.setX(x); + } + + /** + * The horizontal location of this {@code Dialog}. Changing this attribute + * will move the {@code Dialog} horizontally. + */ + public final ReadOnlyDoubleProperty xProperty() { + return dialog.xProperty(); + } + + // --- y + public final double getY() { + return dialog.getY(); + } + + public final void setY(double y) { + dialog.setY(y); + } + + /** + * The vertical location of this {@code Dialog}. Changing this attribute + * will move the {@code Dialog} vertically. + */ + public final ReadOnlyDoubleProperty yProperty() { + return dialog.yProperty(); + } + + + + /*************************************************************************** + * + * Events + * + **************************************************************************/ + + private final EventHandlerManager eventHandlerManager = new EventHandlerManager(this); + + /** {@inheritDoc} */ + @Override public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) { + return tail.prepend(eventHandlerManager); + } + + /** + * Called just prior to the Dialog being shown. + */ + private ObjectProperty> onShowing; + public final void setOnShowing(EventHandler value) { onShowingProperty().set(value); } + public final EventHandler getOnShowing() { + return onShowing == null ? null : onShowing.get(); + } + public final ObjectProperty> onShowingProperty() { + if (onShowing == null) { + onShowing = new SimpleObjectProperty>(this, "onShowing") { + @Override protected void invalidated() { + eventHandlerManager.setEventHandler(DialogEvent.DIALOG_SHOWING, get()); + } + }; + } + return onShowing; + } + + /** + * Called just after the Dialog is shown. + */ + private ObjectProperty> onShown; + public final void setOnShown(EventHandler value) { onShownProperty().set(value); } + public final EventHandler getOnShown() { + return onShown == null ? null : onShown.get(); + } + public final ObjectProperty> onShownProperty() { + if (onShown == null) { + onShown = new SimpleObjectProperty>(this, "onShown") { + @Override protected void invalidated() { + eventHandlerManager.setEventHandler(DialogEvent.DIALOG_SHOWN, get()); + } + }; + } + return onShown; + } + + /** + * Called just prior to the Dialog being hidden. + */ + private ObjectProperty> onHiding; + public final void setOnHiding(EventHandler value) { onHidingProperty().set(value); } + public final EventHandler getOnHiding() { + return onHiding == null ? null : onHiding.get(); + } + public final ObjectProperty> onHidingProperty() { + if (onHiding == null) { + onHiding = new SimpleObjectProperty>(this, "onHiding") { + @Override protected void invalidated() { + eventHandlerManager.setEventHandler(DialogEvent.DIALOG_HIDING, get()); + } + }; + } + return onHiding; + } + + /** + * Called just after the Dialog has been hidden. + * When the {@code Dialog} is hidden, this event handler is invoked allowing + * the developer to clean up resources or perform other tasks when the + * {@link Alert} is closed. + */ + private ObjectProperty> onHidden; + public final void setOnHidden(EventHandler value) { onHiddenProperty().set(value); } + public final EventHandler getOnHidden() { + return onHidden == null ? null : onHidden.get(); + } + public final ObjectProperty> onHiddenProperty() { + if (onHidden == null) { + onHidden = new SimpleObjectProperty>(this, "onHidden") { + @Override protected void invalidated() { + eventHandlerManager.setEventHandler(DialogEvent.DIALOG_HIDDEN, get()); + } + }; + } + return onHidden; + } + + /** + * Called when there is an external request to close this {@code Dialog}. + * The installed event handler can prevent dialog closing by consuming the + * received event. + */ + private ObjectProperty> onCloseRequest; + public final void setOnCloseRequest(EventHandler value) { + onCloseRequestProperty().set(value); + } + public final EventHandler getOnCloseRequest() { + return (onCloseRequest != null) ? onCloseRequest.get() : null; + } + public final ObjectProperty> + onCloseRequestProperty() { + if (onCloseRequest == null) { + onCloseRequest = new SimpleObjectProperty>(this, "onCloseRequest") { + @Override protected void invalidated() { + eventHandlerManager.setEventHandler(DialogEvent.DIALOG_CLOSE_REQUEST, get()); + } + }; + } + return onCloseRequest; + } + + + + /*************************************************************************** + * + * Private implementation + * + **************************************************************************/ + + // This code is called ONLY in the case where the dialog was closed normally + // by the user clicking on a button. The abnormal case is handled in the + // close() method. + @SuppressWarnings("unchecked") + void impl_setResultAndClose(ButtonType cmd) { + Callback resultConverter = getResultConverter(); + + R priorResultValue = getResult(); + R newResultValue = null; + + if (resultConverter == null) { + // The choice to cast cmd to R here was a conscious decision, taking + // into account the choices available to us. Firstly, to summarise the + // issue, at this point here we have a null result converter, and no + // idea how to convert the given ButtonType to R. Our options are: + // + // 1) We could throw an exception here, but this requires that all + // developers who create a dialog set a result converter (at least + // setResultConverter(buttonType -> (R) buttonType)). This is + // non-intuitive and depends on the developer reading documentation. + // + // 2) We could set a default result converter in the resultConverter + // property that does the identity conversion. This saves people from + // having to set a default result converter, but it is a little odd + // that the result converter is non-null by default. + // + // 3) We can cast the button type here, which is what we do. This means + // that the result converter is null by default. + // + // In the case of option 1), developers will receive a NPE when the + // dialog is closed, regardless of how it was closed. In the case of + // option 2) and 3), the user unfortunately receives a ClassCastException + // in their code. This is unfortunate as it is not immediately obvious + // why the ClassCastException occurred, and how to resolve it. However, + // we decided to take this later approach as it prevents the issue of + // requiring all custom dialog developers from having to supply their + // own result converters. + newResultValue = (R) cmd; + } else { + newResultValue = resultConverter.call(cmd); + } + + setResult(newResultValue); + + // fix for the case where we set the same result as what + // was already set. We should still close the dialog, but + // we need to special-case it here, as the result property + // won't fire any event if the value won't change. + if (priorResultValue == newResultValue) { + close(); + } + } + + + + + /*************************************************************************** + * + * Stylesheet Handling + * + **************************************************************************/ + private static final PseudoClass HEADER_PSEUDO_CLASS = + PseudoClass.getPseudoClass("header"); //$NON-NLS-1$ + private static final PseudoClass NO_HEADER_PSEUDO_CLASS = + PseudoClass.getPseudoClass("no-header"); //$NON-NLS-1$ +} diff --git a/modules/controls/src/main/java/javafx/scene/control/DialogEvent.java b/modules/controls/src/main/java/javafx/scene/control/DialogEvent.java new file mode 100644 --- /dev/null +++ b/modules/controls/src/main/java/javafx/scene/control/DialogEvent.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2014, 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 javafx.beans.NamedArg; +import javafx.event.Event; +import javafx.event.EventTarget; +import javafx.event.EventType; + +/** + * Event related to dialog showing/hiding actions. In particular, this event is + * used exclusively by the following methods: + * + *
    + *
  • {@link Dialog#onShowingProperty()} + *
  • {@link Dialog#onShownProperty()} + *
  • {@link Dialog#onHidingProperty()} + *
  • {@link Dialog#onCloseRequestProperty()} + *
  • {@link Dialog#onHiddenProperty()()} + *
+ * + * @see Dialog + * @since JavaFX 8u40 + */ +public class DialogEvent extends Event { + + private static final long serialVersionUID = 20140716L; + + /** + * Common supertype for all dialog event types. + */ + public static final EventType ANY = + new EventType(Event.ANY, "DIALOG"); + + /** + * This event occurs on dialog just before it is shown. + */ + public static final EventType DIALOG_SHOWING = + new EventType(DialogEvent.ANY, "DIALOG_SHOWING"); + + /** + * This event occurs on dialog just after it is shown. + */ + public static final EventType DIALOG_SHOWN = + new EventType(DialogEvent.ANY, "DIALOG_SHOWN"); + + /** + * This event occurs on dialog just before it is hidden. + */ + public static final EventType DIALOG_HIDING = + new EventType(DialogEvent.ANY, "DIALOG_HIDING"); + + /** + * This event occurs on dialog just after it is hidden. + */ + public static final EventType DIALOG_HIDDEN = + new EventType(DialogEvent.ANY, "DIALOG_HIDDEN"); + + /** + * This event is delivered to a + * dialog when there is an external request to close that dialog. If the + * event is not consumed by any installed dialog event handler, the default + * handler for this event closes the corresponding dialog. + */ + public static final EventType DIALOG_CLOSE_REQUEST = + new EventType(DialogEvent.ANY, "DIALOG_CLOSE_REQUEST"); + + /** + * Construct a new {@code Event} with the specified event source, target + * and type. If the source or target is set to {@code null}, it is replaced + * by the {@code NULL_SOURCE_TARGET} value. + * + * @param source the event source which sent the event + * @param eventType the event type + */ + public DialogEvent(final @NamedArg("source") Dialog source, final @NamedArg("eventType") EventType eventType) { + super(source, source, eventType); + } + + /** + * Returns a string representation of this {@code DialogEvent} object. + * @return a string representation of this {@code DialogEvent} object. + */ + @Override public String toString() { + final StringBuilder sb = new StringBuilder("DialogEvent ["); + + sb.append("source = ").append(getSource()); + sb.append(", target = ").append(getTarget()); + sb.append(", eventType = ").append(getEventType()); + sb.append(", consumed = ").append(isConsumed()); + + return sb.append("]").toString(); + } + + @Override public DialogEvent copyFor(Object newSource, EventTarget newTarget) { + return (DialogEvent) super.copyFor(newSource, newTarget); + } + + /** + * Creates a copy of the given event with the given fields substituted. + * @param newSource the new source of the copied event + * @param newTarget the new target of the copied event + * @param type the new eventType + * @return the event copy with the fields substituted + * @since JavaFX 8.0 + */ + public DialogEvent copyFor(Object newSource, EventTarget newTarget, EventType type) { + DialogEvent e = copyFor(newSource, newTarget); + e.eventType = type; + return e; + } + + @SuppressWarnings("unchecked") + @Override public EventType getEventType() { + return (EventType) super.getEventType(); + } +} \ No newline at end of file diff --git a/modules/controls/src/main/java/javafx/scene/control/DialogPane.java b/modules/controls/src/main/java/javafx/scene/control/DialogPane.java new file mode 100644 --- /dev/null +++ b/modules/controls/src/main/java/javafx/scene/control/DialogPane.java @@ -0,0 +1,1060 @@ +/* + * Copyright (c) 2014, 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.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.event.EventHandler; +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.BorderPane; +import javafx.scene.layout.Pane; +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. + * + *

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: + * + *

    + *
  • {@link #createButton(ButtonType)} + *
  • {@link #createDetailsButton()} + *
  • {@link #createButtonBar()} + *
+ * + *

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.setAlignment(Pos.TOP_LEFT); +// label.setTextAlignment(TextAlignment.LEFT); + label.setMaxWidth(Double.MAX_VALUE); + label.setMaxHeight(Double.MAX_VALUE); + label.getStyleClass().add("content"); + label.setWrapText(true); + + // FIXME we don't want to restrict the width, but for now this works ok + // label.setPrefWidth(MAIN_TEXT_WIDTH); +// label.setPrefWidth(360); + + + return label; + } + + + + /************************************************************************** + * + * Private fields + * + **************************************************************************/ + + private final BorderPane 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 BorderPane(); + 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); + if (newGraphic != null) { + 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); + if (newHeader != null) { + updateHeaderArea(); + } + } + }; + + /** + * Node which acts as dialog's header + * + * @return dialog's header + */ + public final Node getHeader() { + return header.get(); + } + + /** + * Assigns dialog's header. Any Node can be used + * + * @param header + * future header + */ + public final void setHeader(Node header) { + this.header.setValue(header); + } + + /** + * Property representing the header area of the dialog. + */ + 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. + */ + 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. + */ + 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); + if (newContent != null) { + 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(); + 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; + dialog.impl_setResultAndClose(buttonType); + }); + + 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(new EventHandler() { + @Override public void handle(ActionEvent ae) { + setExpanded(!isExpanded()); + } + }); + return detailsButton; + } + + /** {@inheritDoc} */ + @Override protected void layoutChildren() { + final boolean hasHeader = hasHeader(); + + final double w = getWidth() - (snappedLeftInset() + snappedRightInset()); + final double h = getHeight() - (snappedTopInset() + snappedBottomInset()); + + 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(h); + + final double headerPrefHeight = hasHeader ? header.prefHeight(w) : 0; + final double buttonBarPrefHeight = buttonBar == null ? 0 : buttonBar.prefHeight(w); + final double expandableContentPrefHeight = isExpanded() ? expandableContent.prefHeight(w) : 0; + final double graphicPrefHeight = hasHeader || graphic == null ? 0 : graphic.prefHeight(w); + final double contentAreaHeight = h - + (topPadding + headerPrefHeight + expandableContentPrefHeight + buttonBarPrefHeight + bottomPadding); + + double x = leftPadding; + double y = topPadding; + + if (! hasHeader) { + if (graphic != null) { + graphic.resizeRelocate(x, y, graphicPrefWidth, graphicPrefHeight); + x += graphicPrefWidth; + } + } else { + header.resizeRelocate(leftPadding, topPadding, w - rightPadding, headerPrefHeight); + y += headerPrefHeight; + } + + content.resizeRelocate(x, y, w - graphicPrefWidth - rightPadding, contentAreaHeight); + y += contentAreaHeight; + + if (expandableContent != null) { + expandableContent.resizeRelocate(leftPadding, y, w - rightPadding, expandableContentPrefHeight); + y += expandableContentPrefHeight; + } + + if (buttonBar != null) { + buttonBar.resizeRelocate(leftPadding, (h - buttonBarPrefHeight - bottomPadding), w - rightPadding, buttonBarPrefHeight); + } + } + + /** {@inheritDoc} */ + @Override protected double computePrefWidth(double height) { + double headerPrefWidth = hasHeader() ? getActualHeader().prefWidth(height) : 0; + double contentPrefWidth = getActualContent().prefWidth(height); + double buttonBarPrefWidth = buttonBar == null ? 0 : buttonBar.prefWidth(height); + double graphicPrefWidth = graphicContainer.prefWidth(height); + + double expandableContentPrefWidth = 0; + final Node expandableContent = getExpandableContent(); + if (isExpanded() && expandableContent != null) { + expandableContentPrefWidth = expandableContent.prefWidth(height); + } + + return snappedLeftInset() + + (hasHeader() ? 0 : graphicPrefWidth) + + Math.max(Math.max(headerPrefWidth, expandableContentPrefWidth), Math.max(contentPrefWidth, buttonBarPrefWidth)) + + snappedRightInset(); + } + + /** {@inheritDoc} */ + @Override protected double computePrefHeight(double width) { + double headerPrefHeight = hasHeader() ? getActualHeader().prefHeight(width) : 0; + double contentPrefHeight = getActualContent().prefHeight(width); + double buttonBarPrefHeight = buttonBar == null ? 0 : buttonBar.prefHeight(width); + double graphicPrefHeight = graphicContainer.prefHeight(width); + + double expandableContentPrefHeight = 0; + final Node expandableContent = getExpandableContent(); + if (isExpanded() && expandableContent != null) { + expandableContentPrefHeight = expandableContent.prefHeight(width); + } + + return snappedTopInset() + + headerPrefHeight + + Math.max(graphicPrefHeight, contentPrefHeight) + + expandableContentPrefHeight + + buttonBarPrefHeight + + snappedBottomInset(); + } + + + + /************************************************************************** + * + * 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 graphicContainer; + } + + private void updateHeaderArea() { + Node header = getHeader(); + if (header != null) { + getChildren().add(header); + + headerTextPanel.setVisible(false); + headerTextPanel.setManaged(false); + } else { + // recreate the headerTextNode and add it to the children list. + headerTextPanel.setMaxWidth(Double.MAX_VALUE); + headerTextPanel.getStyleClass().add("header-panel"); //$NON-NLS-1$ + + // on left of header is the text + Label headerLabel = new Label(getHeaderText()); + headerLabel.setWrapText(true); + headerLabel.setAlignment(Pos.CENTER_LEFT); +// headerLabel.setPrefWidth(Dialog.MIN_DIALOG_WIDTH); + headerTextPanel.setLeft(headerLabel); + BorderPane.setAlignment(headerLabel, Pos.CENTER_LEFT); + + // on the right of the header is a graphic, if one is specified + graphicContainer.getChildren().clear(); + graphicContainer.getStyleClass().add("graphic-container"); //$NON-NLS-1$ + final Node graphic = getGraphic(); + if (graphic != null) { + graphicContainer.getChildren().add(graphic); + } + headerTextPanel.setRight(graphicContainer); + + 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(); + if (contentText != null && !contentText.isEmpty()) { + contentLabel.setText(contentText); + contentLabel.setVisible(true); + contentLabel.setManaged(true); + } + } + } + + 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>(Control.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. + * @since JavaFX 8.0 + */ + public static List> getClassCssMetaData() { + return StyleableProperties.STYLEABLES; + } +} diff --git a/modules/controls/src/main/java/javafx/scene/control/FXDialog.java b/modules/controls/src/main/java/javafx/scene/control/FXDialog.java new file mode 100644 --- /dev/null +++ b/modules/controls/src/main/java/javafx/scene/control/FXDialog.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2014, 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.net.URL; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.StringProperty; +import javafx.scene.Node; +import javafx.stage.Modality; +import javafx.stage.StageStyle; +import javafx.stage.Window; + +abstract class FXDialog { + + /************************************************************************** + * + * Static fields + * + **************************************************************************/ + + + /************************************************************************** + * + * Private fields + * + **************************************************************************/ + + protected Object owner; + + + /************************************************************************** + * + * Constructors + * + **************************************************************************/ + + protected FXDialog() { + // pretty much a no-op, but we expect subclasses to call init(...) once + // they have initialised their abstract property methods. + } + + + + /************************************************************************** + * + * Public API + * + **************************************************************************/ + + + + + /*************************************************************************** + * + * Abstract API + * + **************************************************************************/ + + public abstract void show(); + + public abstract void showAndWait(); + + // This is called by public API, it is a valid, 'normal' way of closing the + // dialog, so we should not be concerned with the result value being null. + public abstract void close(boolean closeWasNormal); + + public abstract void initOwner(Window owner); + + public abstract Window getOwner(); + + public abstract void initModality(Modality modality); + + public abstract Modality getModality(); + + public abstract ReadOnlyBooleanProperty showingProperty(); + + public abstract Window getWindow(); + + public abstract void sizeToScene(); + + // --- x + public abstract double getX(); + public abstract void setX(double x); + public abstract ReadOnlyDoubleProperty xProperty(); + + // --- y + public abstract double getY(); + public abstract void setY(double y); + public abstract ReadOnlyDoubleProperty yProperty(); + + // --- resizable + abstract BooleanProperty resizableProperty(); + + + // --- focused + abstract ReadOnlyBooleanProperty focusedProperty(); + + + // --- title + abstract StringProperty titleProperty(); + + // --- content + public abstract void setDialogPane(DialogPane node); + + // --- root + public abstract Node getRoot(); + + + // --- width + /** + * Property representing the width of the dialog. + */ + abstract ReadOnlyDoubleProperty widthProperty(); + + abstract void setWidth(double width); + + + // --- height + /** + * Property representing the height of the dialog. + */ + abstract ReadOnlyDoubleProperty heightProperty(); + + abstract void setHeight(double height); + + + // stage style + abstract void initStyle(StageStyle style); + abstract StageStyle getStyle(); + + + + + /*************************************************************************** + * + * Implementation + * + **************************************************************************/ + + + + + /*************************************************************************** + * + * Support Classes + * + **************************************************************************/ + + + + /*************************************************************************** + * + * Stylesheet Handling + * + **************************************************************************/ +} diff --git a/modules/controls/src/main/java/javafx/scene/control/HeavyweightDialog.java b/modules/controls/src/main/java/javafx/scene/control/HeavyweightDialog.java new file mode 100644 --- /dev/null +++ b/modules/controls/src/main/java/javafx/scene/control/HeavyweightDialog.java @@ -0,0 +1,296 @@ +/* + * Copyright (c) 2014, 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.util.List; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.ObservableList; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.ButtonBar.ButtonData; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.stage.Modality; +import javafx.stage.Screen; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.stage.Window; + +// Not public API (class is package-protected), so no JavaDoc is required. +class HeavyweightDialog extends FXDialog { + + /************************************************************************** + * + * Private fields + * + **************************************************************************/ + + private final Dialog dialog; + private final Stage stage; + private final Scene scene; + private final StackPane sceneRoot; + + private DialogPane dialogPane; + + + + /************************************************************************** + * + * Constructors + * + **************************************************************************/ + + HeavyweightDialog(Dialog dialog) { + this.dialog = dialog; + this.stage = new Stage() { + @Override public void centerOnScreen() { + double x = getX(); + double y = getY(); + + // if the user has specified an x/y location, use it + if (!Double.isNaN(x) && !Double.isNaN(y)) { + // weird, but if I don't call setX/setY here, the stage + // isn't where I expect it to be (in instances where a single + // dialog is shown and closed multiple times). I expect the + // second showing to be in the place the dialog was when it + // was closed the first time, but on Windows it jumps to the + // top-left of the screen. + setX(x); + setY(y); + return; + } + + Window owner = getOwner(); + if (owner != null) { + Scene scene = owner.getScene(); + + // scene.getY() seems to represent the y-offset from the top of the titlebar to the + // start point of the scene, so it is the titlebar height + final double titleBarHeight = scene.getY(); + + // because Stage does not seem to centre itself over its owner, we + // do it here. + final double dialogWidth = sceneRoot.prefWidth(-1); + final double dialogHeight = sceneRoot.prefHeight(-1); + + if (owner.getX() < 0 || owner.getY() < 0) { + // Fix for #165 + Screen screen = Screen.getPrimary(); // todo something more sensible + double maxW = screen.getVisualBounds().getWidth(); + double maxH = screen.getVisualBounds().getHeight(); + + x = maxW / 2.0 - dialogWidth / 2.0; + y = maxH / 2.0 - dialogHeight / 2.0 + titleBarHeight; + } else { + x = owner.getX() + (scene.getWidth() / 2.0) - (dialogWidth / 2.0); + y = owner.getY() + titleBarHeight + (scene.getHeight() / 2.0) - (dialogHeight / 2.0); + } + + setX(x); + setY(y); + } else { + super.centerOnScreen(); + } + } + }; + stage.setResizable(false); + + stage.setOnCloseRequest(windowEvent -> { + // We only allow the dialog to be closed abnormally (i.e. via the X button) + // when there is a cancel button in the dialog, or when there is only + // one button in the dialog. In all other cases, we disable the ability + // (as best we can) to close a dialog abnormally. + if (dialog.closeWasNormal) return; + + DialogPane dialogPane = dialog.getDialogPane(); + if (dialogPane == null) return; + + List buttons = dialogPane.getButtonTypes(); + if (buttons.size() == 1) return; + + // look for cancel button type + for (ButtonType button : buttons) { + if (button == null) continue; + ButtonData type = button.getButtonData(); + if (type == ButtonData.CANCEL_CLOSE) return; + } + + // if we are here, we consume the event to prevent closing the dialog + windowEvent.consume(); + }); + + sceneRoot = new StackPane(); + sceneRoot.getStyleClass().setAll("dialog"); + + scene = new Scene(sceneRoot); +// scene.setFill(Color.TRANSPARENT); + stage.setScene(scene); + } + + + + /************************************************************************** + * + * Public API + * + **************************************************************************/ + + @Override void initStyle(StageStyle style) { + stage.initStyle(style); + } + + @Override StageStyle getStyle() { + return stage.getStyle(); + } + + @Override public void initOwner(Window window) { + stage.initOwner(window); + } + + @Override public Window getOwner() { + return stage.getOwner(); + } + + @Override public void initModality(Modality modality) { + stage.initModality(modality == null? Modality.APPLICATION_MODAL : modality); + } + + @Override public Modality getModality() { + return stage.getModality(); + } + + @Override public void setDialogPane(DialogPane dialogPane) { + this.dialogPane = dialogPane; +// root.setCenter(dialogPane); + sceneRoot.getChildren().setAll(dialogPane); + + // TODO There is still more work to be done here: + // 1) Handling when the dialog pane pref sizes change dynamically (and resizing the stage) + // 2) Animating the resize (if deemed desirable) + stage.sizeToScene(); + } + + @Override public void show() { + dialogPane.heightProperty().addListener(o -> stage.centerOnScreen()); + stage.show(); + } + + @Override public void showAndWait() { + dialogPane.heightProperty().addListener(o -> stage.centerOnScreen()); + stage.showAndWait(); + } + + boolean isClosing = false; + @Override public void close(boolean closeWasNormal) { + if (isClosing) return; + + isClosing = true; + dialog.closeWasNormal = closeWasNormal; + + // enabling this code has the effect of firing the Dialog.onHiding and onHidden events twice +// if (dialog.isShowing()) { +// dialog.close(); +// } + + if (stage.isShowing()) { + stage.hide(); + } + isClosing = false; + } + + @Override public ReadOnlyBooleanProperty showingProperty() { + return stage.showingProperty(); + } + + @Override public Window getWindow() { + return stage; + } + + @Override public Node getRoot() { + return stage.getScene().getRoot(); + } + + // --- x + @Override public double getX() { + return stage.getX(); + } + + @Override public void setX(double x) { + stage.setX(x); + } + + @Override public ReadOnlyDoubleProperty xProperty() { + return stage.xProperty(); + } + + // --- y + @Override public double getY() { + return stage.getY(); + } + + @Override public void setY(double y) { + stage.setY(y); + } + + @Override public ReadOnlyDoubleProperty yProperty() { + return stage.yProperty(); + } + + @Override ReadOnlyDoubleProperty heightProperty() { + return stage.heightProperty(); + } + + @Override void setHeight(double height) { + stage.setHeight(height); + } + + @Override ReadOnlyDoubleProperty widthProperty() { + return stage.widthProperty(); + } + + @Override void setWidth(double width) { + stage.setWidth(width); + } + + @Override BooleanProperty resizableProperty() { + return stage.resizableProperty(); + } + + @Override StringProperty titleProperty() { + return stage.titleProperty(); + } + + @Override ReadOnlyBooleanProperty focusedProperty() { + return stage.focusedProperty(); + } + + @Override public void sizeToScene() { + stage.sizeToScene(); + } +} diff --git a/modules/controls/src/main/java/javafx/scene/control/TextInputDialog.java b/modules/controls/src/main/java/javafx/scene/control/TextInputDialog.java new file mode 100644 --- /dev/null +++ b/modules/controls/src/main/java/javafx/scene/control/TextInputDialog.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2014, 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 com.sun.javafx.scene.control.skin.AccordionSkin; +import com.sun.javafx.scene.control.skin.resources.ControlResources; +import javafx.application.Platform; +import javafx.beans.NamedArg; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; + +/** + * A dialog that shows a text input control to the user. + * + * @see Dialog + * @since JavaFX 8u40 + */ +public class TextInputDialog extends Dialog { + + /************************************************************************** + * + * Fields + * + **************************************************************************/ + + private final GridPane grid; + private final Label label; + private final TextField textField; + private final String defaultValue; + + + + /************************************************************************** + * + * Constructors + * + **************************************************************************/ + + /** + * Creates a new TextInputDialog without a default value entered into the + * dialog {@link TextField}. + */ + public TextInputDialog() { + this(""); + } + + /** + * Creates a new TextInputDialog with the default value entered into the + * dialog {@link TextField}. + */ + public TextInputDialog(@NamedArg("defaultValue") String defaultValue) { + final DialogPane dialogPane = getDialogPane(); + + // -- textfield + this.textField = new TextField(defaultValue); + this.textField.setMaxWidth(Double.MAX_VALUE); + GridPane.setHgrow(textField, Priority.ALWAYS); + GridPane.setFillWidth(textField, true); + + // -- label + label = DialogPane.createContentLabel(dialogPane.getContentText()); + label.setPrefWidth(Region.USE_COMPUTED_SIZE); + label.textProperty().bind(dialogPane.contentTextProperty()); + + this.defaultValue = defaultValue; + + this.grid = new GridPane(); + this.grid.setHgap(10); + this.grid.setMaxWidth(Double.MAX_VALUE); + + dialogPane.contentTextProperty().addListener(o -> updateGrid()); + + setTitle(ControlResources.getString("Dialog.confirm.title")); + dialogPane.setHeaderText(ControlResources.getString("Dialog.confirm.header")); + + // TODO extract out to CSS + dialogPane.setGraphic(new ImageView(new Image(AccordionSkin.class.getResource("modena/dialog-confirm.png").toExternalForm()))); + dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + + updateGrid(); + + setResultConverter((dialogButton) -> dialogButton == ButtonType.OK ? textField.getText() : null); + } + + + + /************************************************************************** + * + * Public API + * + **************************************************************************/ + + /** + * Returns the {@link TextField} used within this dialog. + */ + public final TextField getEditor() { + return textField; + } + + /** + * Returns the default value that was specified in the constructor. + */ + public final String getDefaultValue() { + return defaultValue; + } + + + + /************************************************************************** + * + * Private Implementation + * + **************************************************************************/ + + private void updateGrid() { + grid.getChildren().clear(); + + grid.add(label, 0, 0); + grid.add(textField, 1, 0); + getDialogPane().setContent(grid); + + Platform.runLater(() -> textField.requestFocus()); + } +} diff --git a/modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/dialog-confirm.png b/modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/dialog-confirm.png new file mode 100644 index 0000000000000000000000000000000000000000..6c4fd8ee97e7f73278c3622d58df7de47500c1d4 GIT binary patch literal 621 zc%17D@N?(olHy`uVBq!ia0vp^1|ZDA1SD@H0#LT=By}Z;C1rt33 zJ>#Bd(*uBp-t%;E42f_*JKfNW#ZkcZwGwwzf&yEUf<#AS)_+?0>1v~Vzt(jvcCF%N`-Q$qg@xwTJM}ycO+7s)g1MS^ z2XD#G!kAgb|6BI9O`rezq21fnIVH?n)@)i?wB*w~RwnCbUyRh3Rhz8ug{tMTDY&;sdT-2-+a5^F?WoDAXKz|ZyKt{8{(ob_(ZyEEl-uTadX-;b9hI0=%zKSH2H}7SC zmbQELj45nuW}h>9!}r1EUS0c(gQr_P7d^92EH6l3#GsS@SZp^?>eJqfp5L!**caq) z3*9nwp>W6_^?rJYD@<);T3K0RV8a3R?gG diff --git a/modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/dialog-error.png b/modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/dialog-error.png new file mode 100644 index 0000000000000000000000000000000000000000..b6754fcd1f5dd0e0f6314446eeeffd7677a0dc93 GIT binary patch literal 396 zc%17D@N?(olHy`uVBq!ia0vp^1|ZDA1SD@H0#LT=By}Z;C1rt33 zJ>#Bd(*uBN{(8DNhD5l(y>82O$U(&Q;fx>c+6CP24xhVr=$2~4ZWiu?_pX`Vh}yw; z@Y32I#@P>D?#*pDyjkMMEawwv-a4%^_^Y2&jskPTTAwQ9yi$2>b;eqK#*JGGIoEsg zFIV(8*tqrijPP4}JJ}!FW#0e5A{^bVe|T|xLU!teAT~`f@o_}8{F}DM z+wPg&u=Zh?^gZ7L*B<@8rN8sxjrW^>9lZ9)vEmDN&RU+JQwmE##MAZc50C#`?fM*K zCi~JIeHBfeYwx=6xm0%AHY2*-eg6I5#qa8WpSkf41)f`WReI4Ce%*WCeP4kA!QkoY K=d#Wzp$P!mm9HNF diff --git a/modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/dialog-fewer-details.png b/modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/dialog-fewer-details.png new file mode 100644 index 0000000000000000000000000000000000000000..3cf470ab5fc3e41d146b23bff0c558a147e1b4e7 GIT binary patch literal 546 zc$@(u0^R+IP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y8E;Eegmrwuz0h38YK~y+Tm6V~nfs3TQ{@@2^=d80kGiW~_ zxn8gGR70)*cDpSW3)3{m-C@l*LABI{%5=0s$iV!a5(T;vn)$(`57DvM>7lqu2!o>yY{ut zFPu&%aT1Z6AWkA$6U0eGYl1k5XwPgm8{hYF=korHIEiQ_xSru~$R*S~2!d)ya8(fP zK5$_evOI8lp2s4gyU>Labm#6Xp_FAA8i5Mh1G4}TzvwH9c%TTV2xvPsCzw3UAA=P5 zb2d2Ana}4)lfeQT8);+a)!4j%E5JoCQl0#LT=By}Z;C1rt33 zJ>#Bd(*uBNrh2+KhD5l(J!i;u$U%f5@U^SpY!j)EeDe<8>#j&RdhMQ)M$;xXk#1JW zm^#6f2c?(KXqZ+8$SCm%wx6))^EI1(^WB}gTe~DWM4fcNJwq diff --git a/modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/dialog-more-details.png b/modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/dialog-more-details.png new file mode 100644 index 0000000000000000000000000000000000000000..51c7587564c91a6cb0d0d1f5636d6920087113f4 GIT binary patch literal 520 zc$@(U0{8uiP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y8E;Eegmrwuz0eMM8K~y+Tos%KAf0#LT=By}Z;C1rt33 zJ>#Bd(*uBp%6YmthD5k0|EQl>8n7^sUC?PUpHhpPtP2NkJo{ezGez~%rys}aWIS4; z^vWXRvq~D91V>?_%Vbr~z~hdeXAAHJO;~i|3`pOV@(H~U6}+AK)E4s{ayfCcF2Ew= z|C~Vfhwdl%xC&%a_`E(ymkOv^?W>qz$#LUDgH`$!#~Xs1nuD0wXEwhwo^c_?6Kwa2 z#|!SK1BDtmUV(JG99U)|P_V&p&HHq>$x57oXC1G=lwCeOBlU .text-field { -fx-background-insets: 1 0 1 0, 1 0 1 0, 3 2 3 2; } + + +/******************************************************************************* + * * + * Dialog * + * * + ******************************************************************************/ + +.dialog { + -fx-padding: -1; +} + +.dialog > .dialog-pane { + -fx-background-color: linear-gradient(#ffffff,#f3f3f4); + -fx-border-width: 1 1 1 1; + -fx-border-color: #b4b4b4 transparent #b4b4b4 transparent; + -fx-font-size: 1.083333em; /*13px;*/ + -fx-text-fill: #292929; +} + +.dialog > .dialog-pane > .button-bar { + -fx-font-size: 1em; +} + +.dialog > .dialog-pane { + -fx-padding: 0; +} + +.dialog > .dialog-pane > .expandable-content { + -fx-padding: 8 8 8 8; +} + +.dialog > .dialog-pane > .button-bar > .container { + -fx-padding: 10; +} + +.dialog > .dialog-pane > .content { + -fx-padding: 10 10 10 10; +} + +.dialog:no-header > .dialog-pane .graphic-container { + -fx-padding: 10 10 0 10; +} + +.dialog:header > .dialog-pane .header-panel { + -fx-padding: 10 14 10 14; + -fx-background-insets: 0 -1 0 -1, 0 -1 1 -1; + -fx-background-color: #b4b4b4, linear-gradient(#e2e2e2,#e2e2e2,#eeeeee); +} + +.dialog:header > .dialog-pane .header-panel .label { + -fx-font-size: 1.249999em; + -fx-wrap-text: true; +} + +.dialog:header > .dialog-pane .header-panel .graphic-container { + -fx-padding: 0 0 0 10; +} + +.dialog > .dialog-pane > .button-bar > .container > .details-button { + -fx-alignment: baseline-left; + -fx-focus-traversable: false; + -fx-padding: 5 5 5 5; + -fx-font-size: 1em; +} + +.dialog > .dialog-pane > .button-bar > .container > .details-button.more { + -fx-graphic: url("dialog-more-details.png"); +} + +.dialog > .dialog-pane > .button-bar > .container > .details-button.less { + -fx-graphic: url("dialog-fewer-details.png"); +} + +.dialog > .dialog-pane > .button-bar > .container > .details-button:hover { + -fx-underline: true; +} diff --git a/modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/resources/controls.properties b/modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/resources/controls.properties --- a/modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/resources/controls.properties +++ b/modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/resources/controls.properties @@ -83,3 +83,32 @@ Accessibility.title.Pagination.NextButton=Select next page Accessibility.title.TabPane.CloseButton=Close Accessibility.title.ToolBar.OverflowButton=Overflow button + +### Dialogs ### + +Dialog.apply.button = Apply +Dialog.ok.button = OK +Dialog.close.button = Close +Dialog.cancel.button = Cancel +Dialog.yes.button = Yes +Dialog.no.button = No +Dialog.finish.button = Finish +Dialog.next.button = Next +Dialog.previous.button = Previous + +Dialog.detail.button.more = Show Details +Dialog.detail.button.less = Hide Details + +### Common Dialogs ### + +Dialog.error.title=Error +Dialog.error.header=Error + +Dialog.info.title=Message +Dialog.info.header=Message + +Dialog.warning.title=Warning +Dialog.warning.header=Warning + +Dialog.confirm.title=Confirmation +Dialog.confirm.header=Confirmation \ No newline at end of file