org.controlsfx.dialog.Dialogs Maven / Gradle / Ivy
Show all versions of controlsfx Show documentation
/**
* Copyright (c) 2013, 2014 ControlsFX
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of ControlsFX, any associated website, nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.controlsfx.dialog;
import static impl.org.controlsfx.i18n.Localization.asKey;
import static impl.org.controlsfx.i18n.Localization.getString;
import static impl.org.controlsfx.i18n.Localization.localize;
import static org.controlsfx.dialog.Dialog.ACTION_CANCEL;
import static org.controlsfx.dialog.Dialog.ACTION_NO;
import static org.controlsfx.dialog.Dialog.ACTION_OK;
import static org.controlsfx.dialog.Dialog.ACTION_YES;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import javafx.application.Platform;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.transformation.FilteredList;
import javafx.concurrent.Worker;
import javafx.concurrent.Worker.State;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.PasswordField;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.SelectionModel;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.effect.Effect;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.RowConstraints;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.FontPosture;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.stage.Window;
import javafx.util.Callback;
import javafx.util.Pair;
import org.controlsfx.control.ButtonBar;
import org.controlsfx.control.ButtonBar.ButtonType;
import org.controlsfx.control.action.Action;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
/**
* A simple (yet flexible) API for showing the most common forms of (modal) UI
* dialogs. This class contains a fluent API to make building and customizing
* the pre-built dialogs really easy, but for those developers who want complete
* control, you may be interested in instead using the {@link Dialog} class
* (which is what all of these pre-built dialogs use as well).
*
* A dialog consists of a number of sections, and the pre-built dialogs in
* this class modify these sections as required. Refer to the {@link Dialog}
* class documentation for more detail, but a brief overview is provided in
* the following section.
*
*
Anatomy of a Dialog
*
* A dialog consists of the following sections:
*
*
* - Title,
*
- System buttons (min, max, close),
*
- Masthead,
*
- Content,
*
- Expandable content,
*
- Button bar
*
*
* This is more easily demonstrated in the diagram shown below:
*
*
*
*
*
* The system buttons are hidden, only "close" button is visible by default.
* "Maximize" button is only available when dialog becomes resizable.
* This happens for example with the Exception dialog when details are expanded.
*
* Screenshots
* To better explain the dialogs, here is a table showing the default look
* of all available pre-built dialogs when run on Windows (the button placement
* in dialogs uses the {@link ButtonBar} control, so the buttons vary in order
* based on the operating system in which the dialog is shown):
*
*
*
*
*
* Without Masthead
* With Masthead
*
*
* Information
*
*
*
*
* Confirmation
*
*
*
*
* Warning
*
*
*
*
* Error
*
*
*
*
* Exception
*
*
*
*
* Exception (Expanded)
*
*
*
*
* Exception (new window)
*
*
*
* Text Input
*
*
*
*
* Choice Input
(ChoiceBox/ComboBox)
*
*
*
*
* Command Link
*
*
*
*
* Font Selector
*
*
*
* Progress
*
*
*
*
*
* Styled Dialogs
*
* The ControlsFX dialogs API supports displaying dialogs with either a
* consistent cross-platform titlebar area, or by using the titlebar of the users
* operating system, or without a titlebar. All of the screenshots above are taken
* using the cross-platform style, whereas the screenshots below are the
* same dialog code being rendered using different dialog styles.
*
*
* A dialog has one of the following styles:
*
* - {@link Dialog#STYLE_CLASS_CROSS_PLATFORM} - a dialog with a cross-platform title bar.
* - {@link Dialog#STYLE_CLASS_NATIVE} - a dialog with a native title bar.
* - {@link Dialog#STYLE_CLASS_UNDECORATED} - a dialog without a title bar.
*
*
* If no style is specified, the dialogs will be rendered using the default
* {@link Dialog#STYLE_CLASS_CROSS_PLATFORM} style.
*
* To enable this in the Dialogs fluent API, simply call {@link #styleClass(String)}
* when creating the dialog.
*
* Here are the screenshots of dialogs using different dialog styles:
*
*
*
*
* Platform
* DialogStyle
* Screenshot
*
*
* Cross-Platform (default)
* {@code DialogStyle.CROSS_PLATFORM_DARK}
*
*
*
* Mac OS X
* {@code DialogStyle.NATIVE}
*
*
*
* Windows 8
* {@code DialogStyle.NATIVE}
*
*
*
* Linux (Ubuntu)
* {@code DialogStyle.NATIVE}
*
*
*
* Linux (Ubuntu)
* {@code DialogStyle.UNDECORATED}
*
*
*
*
* Heavyweight vs Lightweight Dialogs
*
* The ControlsFX dialogs API supports a distinction between heavyweight and
* lightweight dialogs. In short, a heavyweight dialog is rendered in its own
* JavaFX window, allowing for it to appear outside the bounds of the application.
* This is the most common style of dialog, and is therefore the default behavior
* when creating dialogs in ControlsFX. However, in some case, lightweight dialogs
* make more sense, so the following paragraphs will detail when you might want
* to use lightweight dialogs.
*
*
Lightweight dialogs are rendered within the scenegraph (and can't leave
* the window). Other than this limitation, lightweight dialogs otherwise render
* exactly the same as heavyweight dialogs (except a lightweight dialog normally
* inserts an opaque overlay into the scene so that the dialog sticks out
* visually). Lightweight dialogs are commonly useful in environments where a
* windowing system is unavailable (e.g. tablet devices), and also when you only
* want to block execution (and access to) a portion of your user interface. For
* example, you could create a lightweight dialog with an owner of a single
* {@link Tab} in a {@link TabPane}, and this will only block on that one tab -
* all other tabs will continue to be interactive and execute as per usual.
*
*
One limitation of lightweight dialogs is that it is not possible to use
* the native titlebar feature. If you call both {@link #lightweight()} and
* {@link #styleClass(String)} with {@link Dialog#STYLE_CLASS_NATIVE} option,
* the call to enable lightweight takes precedence over the use of the
* native style, so you will end up seeing what is shown in the screenshot
* below (that is, a cross-platform-looking dialog that is lightweight).
*
*
To make a dialog lightweight, you simply call {@link #lightweight()} when
* constructing the dialog using this Dialogs API. If you are using the
* {@link Dialog} class instead, then the decision between heavyweight and
* lightweight dialogs must be made in the
* {@link Dialog#Dialog(Object, String, boolean)} constructor.
*
*
Shown below are screenshots of a lightweight dialog whose owner is the
* Tab. This means that whilst the first tab is blocked for input until the
* dialog is dismissed by the user, the rest of the UI (including going to other
* tabs) remains interactive):
*
*
Lightweight dialog whose style is {@link Dialog#STYLE_CLASS_CROSS_PLATFORM}
*
*
*
* Lightweight dialog whose style is {@link Dialog#STYLE_CLASS_UNDECORATED}
*
*
*
* Java 8 API elements
*
* Many methods in this class are taking advantage of {@link Optional}. There are many benefits of this, but the main one
* is that there no more checking for null and thus no possibility of NullPointerExceptions. Here some things you can do with
* {@link Optional}s:
*
*
{@code
* optional.isPresent(value->{...}) //perform an operation only if there is a value
* optional.orElse(default) //get the value of optional with default if it does not exists
* optional.map( value -> {...} ).orElse(default) //convert to other type with default
* }
* and many more - check {@link Optional} API.
* {@link Optional} can be thought of as a stream collection of one or none elements, allowing for functional approach -
* chaining methods without keeping state. This especially beneficial in concurrent environments.
*
* Code Examples
*
* The code below will setup and show a confirmation dialog:
*
*
{@code
* Action response = Dialogs.create()
* .owner( isOwnerSelected ? stage : null)
* .title("You do want dialogs right?")
* .masthead(isMastheadVisible() ? "Just Checkin'" : null)
* .message( "I was a bit worried that you might not want them, so I wanted to double check.")
* .showConfirm();}
*
* The most important point to note about the dialogs is that they are modal,
* which means that they stop the application code from progressing until the
* dialog is closed. Because of this, it is very easy to retrieve the users input:
* when the user closes the dialog (e.g. by clicking on one of the buttons), the
* dialog will be hidden, and their response will be returned from the
* show method that was called to bring the dialog up in the
* first place. In other words, following on from the code sample above, you
* might do the following:
*
*
* {@code
* if (response == Dialog.ACTION_YES) {
* // ... submit user input
* } else {
* // ... user cancelled, reset form to default
* }}
*
* The following code is an example of setting up a CommandLink dialog:
*
*
{@code
* List links = Arrays.asList(
* new CommandLink("Add a network that is in the range of this computer",
* "This shows you a list of networks that are currently available and lets you connect to one."),
* new CommandLink("Manually create a network profile",
* "This creates a new network profile or locates an existing one and saves it on your computer"),
* new CommandLink("Create an ad hoc network",
* "This creates a temporary network for sharing files or and Internet connection"));
*
* Action response = Dialogs.create()
* .owner(cbSetOwner.isSelected() ? stage : null)
* .title("Manually connect to wireless network")
* .masthead(isMastheadVisible() ? "Manually connect to wireless network": null)
* .message("How do you want to add a network?")
* .showCommandLinks( links.get(1), links );}
*
* @see Dialog
* @see Action
* @see Action
* @see Optional
*/
@Deprecated
public final class Dialogs {
/**
* USE_DEFAULT can be passed in to {@link #title(String)} and {@link #masthead(String)} methods
* to specify that the default text for the dialog should be used, where the default text is
* specific to the type of dialog being shown.
*/
public static final String USE_DEFAULT = "$$$"; //$NON-NLS-1$
private Object owner;
private String title = USE_DEFAULT;
private Node graphic;
private String message;
private String masthead;
private boolean lightweight;
private Set actions = new LinkedHashSet<>();
private Effect backgroundEffect;
private List styleClasses;
/**
* Creates the initial dialog
* @return dialog instance
*/
public static Dialogs create() {
return new Dialogs();
}
private Dialogs() {}
/**
* Assigns the owner of the dialog. If an owner is specified, the dialog will
* block input to the owner and all parent owners. If no owner is specified,
* or if owner is null, the dialog will block input to the entire application.
*
* @param owner The dialog owner.
* @return dialog instance.
*/
public Dialogs owner(final Object owner) {
this.owner = owner;
return this;
}
/**
* Assigns dialog's title
* @param title dialog title
* @return dialog instance.
*/
public Dialogs title(final String title) {
this.title = title;
return this;
}
/**
* Assigns dialog's graphic
* @param graphic dialog graphic
* @return dialog instance.
*/
public Dialogs graphic(final Node graphic) {
this.graphic = graphic;
return this;
}
/**
* Assigns dialog's instructions
* @param message dialog message
* @return dialog instance.
*/
public Dialogs message(final String message) {
this.message = message;
return this;
}
/**
* Assigns dialog's masthead
* @param masthead dialog masthead
* @return dialog instance.
*/
public Dialogs masthead(final String masthead) {
this.masthead = masthead;
return this;
}
/**
* Completely replaces standard actions with provided ones.
* @param actions new dialog actions
* @return dialog instance.
*/
public Dialogs actions( Collection extends Action> actions) {
this.actions.clear();
this.actions.addAll(actions);
return this;
}
/**
* Completely replaces standard actions with provided ones.
* @param actions new dialog actions
* @return dialog instance.
*/
public Dialogs actions( Action... actions) {
return actions( Arrays.asList(actions));
}
/**
* Sets the effect which should be applied to background components when dialog appears.
* If not set or set to null, default semi-transparent panel will be used
* Good example of effect to use is GaussianBlur
* Currently works only with lightweight dialogs.
* @param effect to be applied
* @return dialog instance
*/
public Dialogs backgroundEffect(Effect effect) {
this.backgroundEffect = effect;
return this;
}
/**
* Specifies that the dialog should become lightweight, which means it is
* rendered within the scene graph (and can't leave the window). Lightweight
* dialogs are commonly useful in environments where a windowing system is unavailable
* (e.g. tablet devices), and also when you only want to block execution
* (and access to) a portion of your user interface. For example, you could
* create a lightweight dialog with an owner of a single {@link Tab} in a
* {@link TabPane}, and this will only block on that one tab - all other
* tabs will continue to be interactive and execute as per usual.
*
* @return dialog instance.
*/
public Dialogs lightweight() {
this.lightweight = true;
return this;
}
/**
* Specifies that the dialog should use the given style class, which allows
* for custom styling via CSS. There are three built-in style classes that
* cover most use cases, these are:
*
*
* - {@link Dialog#STYLE_CLASS_CROSS_PLATFORM}
* - {@link Dialog#STYLE_CLASS_NATIVE}
* - {@link Dialog#STYLE_CLASS_UNDECORATED}
*
*
* By default, dialogs use the cross-platform style class, to give a
* consistent look across all operating systems.
*
*
This method can be called multiple times, with each style class being
* added to the final dialog instance.
*
* @param styleClass The style class to add to the dialog.
* @return dialog instance.
*/
public Dialogs styleClass(String styleClass) {
if (styleClasses == null) {
styleClasses = new ArrayList<>();
}
styleClasses.add(styleClass);
return this;
}
/**
* Shows information dialog.
*/
public void showInformation() {
showSimpleContentDialog(Type.INFORMATION);
}
/**
* Shows confirmation dialog.
* @return action used to close dialog.
*/
public Action showConfirm() {
return showSimpleContentDialog(Type.CONFIRMATION);
}
/**
* Shows warning dialog
* @return action used to close dialog
*/
public Action showWarning() {
return showSimpleContentDialog(Type.WARNING);
}
/**
* Show error dialog
* @return action used to close dialog
*/
public Action showError() {
return showSimpleContentDialog(Type.ERROR);
}
/**
* Shows exception dialog with expandable stack trace.
* @param exception exception to present
* @return action used to close dialog
*/
public Action showException(Throwable exception) {
Dialog dlg = buildDialog(Type.ERROR);
dlg.setContent(message != null && ! message.isEmpty() ? message : exception.getMessage());
dlg.setExpandableContent(buildExceptionDetails(exception));
if ( !actions.isEmpty()) {
dlg.getActions().clear();
dlg.getActions().addAll(actions);
}
return dlg.show();
}
/**
* Shows exception dialog with a button to open the exception text in a
* new window.
* @param exception exception to present
* @return action used to close dialog
*/
public Action showExceptionInNewWindow(final Throwable exception) {
Dialog dlg = buildDialog(Type.ERROR);
dlg.setContent(message != null && ! message.isEmpty() ? message : exception.getMessage());
dlg.getActions().clear();
Action openExceptionAction = new Action(localize(asKey("exception.button.label")), ae -> { //$NON-NLS-1$
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
exception.printStackTrace(pw);
String moreDetails = sw.toString();
new ExceptionDialogImpl((Window)owner, moreDetails).show();
});
ButtonBar.setType(openExceptionAction, ButtonType.HELP_2);
dlg.getActions().add(openExceptionAction);
if ( !actions.isEmpty()) {
dlg.getActions().addAll(actions);
}
return dlg.show();
}
/**
* Shows dialog with one text field
* @param defaultValue text field default value
* @return Optional of text from input field if OK action is used otherwise {@link Optional#empty()}
*/
public Optional showTextInput(String defaultValue) {
Dialog dlg = buildDialog(Type.INPUT);
final TextField textField = new TextField(defaultValue);
dlg.setContent(buildInputContent(textField));
return Optional.ofNullable( dlg.show() == ACTION_OK ? textField.getText() : null );
}
/**
* Shows dialog with one text field
* @return Optional of text from input field or {@link Optional#empty()} if dialog is cancelled
*/
public Optional showTextInput() {
return showTextInput(""); //$NON-NLS-1$
}
/**
* Show a dialog with one combobox filled with provided choices. The combobox selection
* will be set to a default value if one is provided.
* @param defaultChoice default combobox selection
* @param choices dialog choices
* @return Optional of selected choice or {@link Optional#empty()} if dialog is cancelled.
*/
public Optional showChoices(T defaultChoice, Collection choices) {
Dialog dlg = buildDialog(Type.INPUT);
final double MIN_WIDTH = 150;
SelectionModel selectionModel=null;
if (choices.size() > 10) {
// use ComboBox
ComboBox comboBox = new ComboBox<>();
comboBox.setMinWidth(MIN_WIDTH);
comboBox.getItems().addAll(choices);
selectionModel = comboBox.getSelectionModel();
dlg.setContent(buildInputContent(comboBox));
} else {
// use ChoiceBox
ChoiceBox choiceBox = new ChoiceBox<>();
choiceBox.setMinWidth(MIN_WIDTH);
choiceBox.getItems().addAll(choices);
selectionModel = choiceBox.getSelectionModel();
dlg.setContent(buildInputContent(choiceBox));
}
if (defaultChoice==null) {
selectionModel.selectFirst();
} else {
selectionModel.select(defaultChoice);
}
return Optional.ofNullable( dlg.show() == ACTION_OK ? selectionModel.getSelectedItem() : null);
}
/**
* Show a dialog with one combobox filled with provided choices
* @param choices dialog choices
* @return Optional of selected choice or Optinal.EMPTY if dialog is cancelled
*/
public Optional showChoices(Collection choices) {
return showChoices(null, choices);
}
/**
* Show a dialog with one combobox filled with provided choices
* @param choices dialog choices
* @return Optional of selected choice or {@link Optional#empty()} if dialog is cancelled
*/
public Optional showChoices(@SuppressWarnings("unchecked") T... choices) {
return showChoices(Arrays.asList(choices));
}
/**
* Show a dialog filled with provided command links. Command links are just a {@link DialogAction}s and used instead of button bar and represent
* a set of available 'radio' buttons. "Command link" action uses content of its 'text' property as a title of a command link and content of 'long text'
* property as a description of the command link.
* @param links list of command links presented in specified sequence
* @return action used to close dialog (it is either one of command links or CANCEL)
*/
public Action showCommandLinks(List links) {
final Dialog dlg = buildDialog(Type.INFORMATION);
dlg.setContent(message);
Node messageNode = dlg.getContent();
messageNode.getStyleClass().add("command-link-message"); //$NON-NLS-1$
final int gapSize = 10;
final List