Introduction

We’re all familiar with Dialog boxes, they pop up with some important information, ask you, “Are you sure?”, let us pick a file, or make some other important decision before processing can continue. In the beginning, JavaFX didn’t have Dialogs and we were left to cook up our own solutions or use third party libraries. But they were added around JavaFX 8, and work quite well.

There are places where a Dialog makes a lot of sense, but a lot of places where they result in a bad user experience. First off, most users hate pop-ups and they’re almost always a surprise when the appear. So if you can design your UX such that whatever information would be in a Dialog is conveyed in your main GUI, your users will be happier. For instance, instead of putting up a dialog that indicates that some TextField contains an invalid value, how about configuring your TextField so that the users cannot enter invalid data?

The sample code in this article are written in Kotlin. If you know Java, you should be able to figure out what the Kotlin code is doing with little trouble. If you need a quick primer in Kotlin, you can check out my article, which should tell you everything you need to know.

The Basics

Dialogs really aren’t that complicated, but this is one of those places where the JavaDocs is not a very good learning tool. This means that beginners get confused about how to go about using them. You’ll need to understand how their layout works, the ButtonType madness, and then about “Result Converters”.

Before we get into more advanced concepts, let’s look at the core concepts about how Dialog works…

Button Types

ButtonTypes is the concept that really gets beginners’ heads spinning. It’s meant to abstract the meaning of the Buttons from the implementation of the Buttons, and make it easier to quickly implement a Dialog. Because you don’t have to worry about how the Buttons actually get on the screen, you can treat them like data through the ButtonTypes. Of course, none of this is explained properly, so it just causes a lot of confusion.

Let’s clear that up…

primary

Dialogs are designed to be user interactions inserted into a procedural flow.

Very often, Dialogs are used to allow the user to make some kind of choice between a small number of options - which can even be something as simple as “proceed” or “stop”. To simplify the programming needed to implement these kinds of Dialogs, the designers of JavaFX have given us ButtonTypes and the Enum class ButtonBar.ButtonData.

ButtonType is a POJO with a set of statically predefined values that correspond to the values in ButtonBar.ButtonData. These are values like APPLY, OK, CANCEL and NEXT. You can make your own ButtonTypes if you want, and this allows you to have a button in your Dialog with a customized label but a standard value.

If you’re using Dialogs this way, then you don’t add the Buttons directly to the Dialog, you just add a list of the ButtonTypes that you want to have, and the Dialog takes care of adding the Buttons in the right place, and then it will tell you the ButtonType of the Button that the user clicked. There isn’t any way to directly add a Button to the standard “Button Bar” in a Dialog.

Handling Return Values

Dialogs are primarily intended to be used modally as part of a procedural flow. “Modal” means that the window holding the Dialog is the only active window in the application, and the user cannot interact with any other part of your GUI until they have dismissed the Dialog. The normal way to put a Dialog on the screen is to use Dialog.showAndWait().

Dialog.showAndWait() is very special case in JavaFX coding. It’s almost like using a blocking operation in your code, which you would ordinarily never do on the FXAT. However, Dialog.showAndWait() does not block the FXAT. Events will continue to be processed on the FXAT, transitions and timers will continue to run, and the screen will continue to respond appropriately to window moves, mouse actions and keystrokes.

What will happen, though, is that the code that contains the Dialog.showAndWait() call will stop. No more code will be executed until the Dialog has been closed. At that point, the execution will continue.

And when the execution continues, the Dialog.showAndWait() call will have returned a value. This value is wrapped in Optional, meaning that you’ll have to check to see if there is an actual value. The JavaDocs for Dialog show three methods to handle the Optional with the notation that, “There is no better or worse option of the three…”. That’s utter rubbish. The first approach uses Optional.get() which is an anti-pattern. The third approach, “The fully lambda approach”, is totally counter-intuitive, and probably inspired by the novelty of lambdas at the time that Dialogs were introduced.

Stick to Optional.ifPresent(), and if you want to have some code executed if there is no value returned you should use Optional.ifPresentOrElse().

But why would Dialog.showAndWait() return an Optional?…

Abnormal Closing of the Dialog

Since windows often have that “X” in the top corner, it’s possible to close them without clicking on one of the Buttons. This means that the Dialog can close without the user giving a response, and the way that is handled is to return the value as an Optional.

The JavaDocs refer to this as “abnormally” closing a Dialog. Not all Dialogs can be closed this way, it depends on the nature of the Buttons in the Dialog.

If a Dialog only has one Button, then it can be closed abnormally. If a Dialog has more than one Button, and one of those Buttons has been defined as a “Cancel” Button, then it can also be closed abnormally.

In the standard implementation of Dialogs, the “Cancel” Button is usually treated the same as an abnormal close, and the Dialog will return an empty Optional.

Dialog Utility Subtypes

There are three subtypes of Dialog, each intended to make it easy to perform specific types of common user interaction…

Alert

Alerts are intended to send a message to the user and require them to acknowledge it before moving on. Control of how the Alert will look is by assigning an AlertType to it. Beyond that, you can change the window title, the text which appears beside the icon, the message and the icon itself. Alert extends Dialog<ButtonType> which means that its showAndWait() method will return Optional<ButtonType>

The AlertTypes and How They Look

The JavaDocs don’t give much help about what the different AlertTypes actually do, just some generic description of their intended uses. The only way to see is to try them, so here’s some Kotlin code that will do the trick:

class AlertTest : Application() {
   override fun start(primaryStage: Stage) {
      primaryStage.setScene(Scene(createContent()))
      primaryStage.show()
   }

   private fun createContent(): Region = BorderPane().apply {
      center = Button("Click Me").apply {
         onAction = EventHandler {
            val alert = Alert(AlertType.CONFIRMATION).apply {
               contentText = "Click a Button"
            }
            alert.showAndWait().ifPresentOrElse({ println("This is the result: $it") }, { println("No result") })
         }
      }
      padding = Insets(50.0)
   }
}

fun main() {
   Application.launch(AlertTest::class.java)
}

From this code I generated the following screen snaps, changing the AlertType each time…

AlertType.Confirmation:

Confirmation Alert

This is the only AlertType that generates more than one button, and abnormally closing it will return a “Cancel” value. The idea behind this Alert is usually to allow the user one last chance to abort an operation before it starts.

The next three don’t differ in functionality, just the presentation of the message…

AlertType.Error:

Error Alert

AlertType.Information:

Information Alert

AlertType.Warning:

Warning Alert

AlertType.None:

Empty Alert

This AlertType is useless without some further customization. In fact, it isn’t even possible to close this Dialog at all since there are no Buttons which means it won’t accept an abnormal closing either.

Here’s how you can customize it:

val alert = Alert(AlertType.NONE).apply {
   contentText = "This is contentText"
   headerText = "This is headerText"
   title = "This is the title"
   buttonTypes += listOf(ButtonType.APPLY, ButtonType.FINISH)
   graphic = AlertTest::class.java.getResource("/images/boom.png")?.toExternalForm()?.run { ImageView(this) }?.apply {
      fitWidth = 60.0
      isPreserveRatio = true
   }
}

And that gives you this:

Custom Alert

Of course, you can apply all of these customizations to any of the AlertTypes.

TextInputDialog

TextInputDialog looks very much like an Alert set to AlertType.CONFIRMATION, except that it also has a TextField placed into the content. This is intended to be a very quick way to set up a straight-forward input Dialog with a minimun of extraneous code.

Here’s a simple example:

private fun createContent(): Region = BorderPane().apply {
   center = Button("Click Me").apply {
      onAction = EventHandler {
         val dialog = TextInputDialog("This is the default").apply {
            contentText = "This is a message in\n the content text"
            headerText = "Input Required"
         }
         dialog.showAndWait().ifPresentOrElse({ println("This is the result: $it") }, { println("No result") })
      }
   }
   padding = Insets(50.0)
}

Which looks like this:

TextInputDialog

TextInputDialog extends Dialog<String> so it will return an Optional<String>. The result will be empty if the user clicks the “Cancel” Button or abnormally closes the window. Otherwise it will return the value that is in the TextField.

In addition to the customization values available with Alert you can also get access to the TextField contained in the Dialog via TextInputDialog.getEditor(). This means that you can install a TextFormatter on it.

ChoiceDialog

Like TextInputDialog, ChoiceDialog is inteded to be a “no fuss” way to set up an input Dialog, but using a ComboBox instead of a TextField. You’ll need to provide the list of possible choices, as well as the default…

private fun createContent(): Region = BorderPane().apply {
   val button = Button("Click Me").apply {
      onAction = EventHandler {
         val dialog = ChoiceDialog<String>("Wombat", "Bunny", "Wombat", "Kangaroo", "Panda").apply {
            contentText = "This is a message in\n the content text"
            headerText = "Input Required"
         }
         dialog.showAndWait().ifPresentOrElse({ println("This is the result: $it") }, { println("No result") })
      }
   }
   center = button
   padding = Insets(50.0)
}

Which looks like this:

ChoiceDialog

Unlike TextInputDialog, you cannot grab the input control to customize it. You can, however, still customize the other common elements just like the other standard Dialogs.

Creating Your Own Dialog

While Alert, TextInputDialog and ChoiceDialog are likely to satisfy 90%+ of your use cases for Dialogs, there are times when you’ll need to create something a little bit more complicated and customized. So let’s look at how you go about doing that…

DialogPane

primary

The Dialog class is NOT a screen component. It's best thought of as a controller class.

The screen content of a Dialog is a class called DialogPane, which handles the layout. It has four main components, the “Header”, the “Content”, the “Expandable Content” and the “Button Bar”. If you’ve looked at the screen shots of TextInputDialog or ChoiceDialog earlier in this article then you’ve seen how this structure works.

The “Header” Section

The “Header” section contains two elements; some text and a graphic. These are actually two different Nodes, not the text and graphic of a Labeled type of Node. You can change the content of either of these two Nodes directly if you want. Often, the graphic is an ImageView, but you can change it to any kind of Node that you like.

You can directly change the values held in the default Nodes:

setHeaderText()
This will update the text content of the “Header” section.
setGraphic()
This will update the content of the graphic in the “Header” section. You can put any kind of Node in here.

These are also Properties so you can do the usual binding stuff if that makes sense for your application.

But if you want a completely custom “Header” section…

setHeader()
This will replace the entire “Header” section with your own Node. Once you have done this, you will not be able to use setHeaderText() or setGraphic().

If you haven’t called setHeader() to implement your own custom header Node, then calling getHeader() will return Null. In other words, you cannot use getHeader() to retrieve and manipulate the default header Node.

The “Content” Section

The “Content” section is occupied by an empty Label by default. You can update its text value directly.

setContentText()
This will update the text value in the “Content” section.

This is also a Property so you can do the usual binding stuff if that makes sense for your application. This looks like this:

If you decide that you do not like the default Label implementation for the “Content” section you may replace it with your own Node

setContent()
This will replace the entire “Content” section with your own Node. Once you have done this, you will not be able to use setContentText() any more.

If you haven’t called setContent() to implement your own custom content Node, then calling getContent() will return Null. In other words, you cannot use getContent() to retrieve and manipulate the default content Node.

The “Expandable Content” Section

If you have additional details that users may not always need/want to see, you can put it into the “Expandable Content” section. Unlike the “Header” and the “Content” sections, you cannot access a text value for it, you have to supply a Node via the setExpandableContent() method.

If this Node is non-null, then a “twisty” style arrow with the text, “Show Details” will appear in the “Button Bar”. Clicking on it will expand the Dialog size, and display the Node contained in the “Expandable Content” section. The “twisty” changes to say “Hide Details”.

The “Button Bar”

The “Button Bar” really is a ButtonBar and it sits at the bottom of the DialogPane. There’s no way to directly access it, and the only way to add or remove buttons is to add new ButtonTypes to the list from getButtonTypes().

Delegate Methods in Dialog

There are convenience delegate methods in the Dialog class which call the appropriate method in DialogPane to modify all three of the content values. These are the setters and getters for contentText, headerText and graphic. To actually change the Nodes for any of these elements from the defaults (or to access the default Nodes) you’ll have to get the DialogPane from the Dialog first.

For some unfathomable reason, there’s no delegate method in Dialog to retrieve the list of ButtonTypes. So, once again, you’ll have to grab the DialogPane first.

A Sample

Here’s the code for a customized Dialog with the default Nodes for the “Header” and “Content” sections, but with the values populated. There’s a single Button and “Expandable Content”:

private fun createContent(): Region = BorderPane().apply {
      val button = Button("Click Me").apply {
         onAction = EventHandler {
            val dialog = Dialog<ButtonType>().apply {
               contentText = "This is a message in\n the content text"
               headerText = "The HeaderText"
               title = "Dialog Title"
               graphic = this::class.java.getResource("/images/boom.png")?.toExternalForm()?.run { ImageView(this) }?.apply {
                  fitWidth = 40.0
                  isPreserveRatio = true
               }
               dialogPane.buttonTypes += ButtonType.CLOSE
               dialogPane.expandableContent = Label("""This is the expandable content
                  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
                   Integer tristique malesuada arcu a malesuada. Ut porta
                   et mauris eu pellentesque. Curabitur molestie vel nunc vitae pharetra.
                   In hac habitasse platea dictumst. Donec ut rutrum mauris. In hac habitasse platea dictumst.
                   Donec tristique ac nisl pretium suscipit. Morbi magna quam, convallis ut vehicula et,
                   lacinia nec orci. Quisque commodo neque vitae finibus malesuada. Integer a magna ultricies,
                   consequat velit sit amet, laoreet purus. Pellentesque facilisis aliquam tortor eu eleifend.
                  """)
            }
            dialog.showAndWait().ifPresentOrElse({ println("This is the result: $it") }, { println("No result") })
         }
      }
      center = button
      padding = Insets(50.0)
   }

This looks like this:

Custom Dialog Unexpanded

And expanded it looks like this:

Custom Dialog Expanded

Styling

Here’s the Modena.css entries for Dialog. It will point you to the correct strategy to customize the styling of your Dialog:

/*******************************************************************************
 *                                                                             *
 * Dialog                                                                      *
 *                                                                             *
 ******************************************************************************/

.dialog-pane {
    -fx-background-color: -fx-background;
    -fx-padding: 0;
}

.dialog-pane > .expandable-content {
    -fx-padding: 0.666em; /* 8px */
}

.dialog-pane > .button-bar > .container {
    -fx-padding: 0.833em; /* 10px */
}

.dialog-pane > .content.label {
    -fx-alignment: top-left;
    -fx-padding: 1.333em 0.833em 0 0.833em; /* 16px 10px 0px 10px */
}

.dialog-pane > .content {
    -fx-padding: 0.833em; /* 10 */
}

.dialog-pane:no-header .graphic-container {
    -fx-padding: 0.833em 0 0 0.833em; /* 10px 0px 0px 10px */
}

.dialog-pane:header .header-panel {
    /*-fx-padding: 0.833em 1.166em 0.833em 1.166em; *//* 10px 14px 10px 14px */
    -fx-padding: 0.833em; /* 10px */
    -fx-background-color: -fx-box-border, linear-gradient(-fx-background, derive(-fx-background, 30%));
    -fx-background-insets: 0, 0 0 1 0;
}

.dialog-pane:header .header-panel .label {
    -fx-font-size: 1.167em; /* 14px */
    -fx-wrap-text: true;
}

.dialog-pane:header .header-panel .graphic-container {
    /* This prevents the text in the header running directly into the graphic */
    -fx-padding: 0 0 0 0.833em; /* 0px 0px 0px 10px */
}

.dialog-pane > .button-bar > .container > .details-button {
  -fx-alignment: baseline-left;
  -fx-focus-traversable: false;
  -fx-padding: 0.416em; /* 5px */
}

.dialog-pane > .button-bar > .container > .details-button.more {
    -fx-graphic: url("dialog-more-details.png");
}

.dialog-pane > .button-bar > .container > .details-button.less {
    -fx-graphic: url("dialog-fewer-details.png");
}

.dialog-pane > .button-bar > .container > .details-button:hover {
    -fx-underline: true;
}

You can see that all of the stylings are applied to sub-components of DialogPane, and that you have separate entries for the header, the content, the expandable content and the button bar.

The best strategy for customizing these is to create a separate style class selector for your Dialog and then add it to the DialogPane of your Dialog. You don’t have to customize the base style class you’ve added, but you can add in entries for the sub-components that you want.

This was added to default.css:

.fancy-alert > .content {
   -fx-background-color: lightblue;
}

We’re just adding a light blue background to the “Content” area of the Dialog

private fun createDialog() = Alert(AlertType.CONFIRMATION).apply {
   this::class.java.getResource("/css/default.css")?.toString()?.let { dialogPane.stylesheets += it }
   contentText = "This is a sample message"
   dialogPane.styleClass += "fancy-alert"
}
primary

Dialogs open in their own Scene, so they won't inherit any style sheets that you've added to your main GUI screen.

You’ll have add your stylesheet to the DialogPane which is the root Node of the Scene it opens in.

This results in this:

Styled Alert

This isn’t pretty, but it does show that the content area has been styled. If you replace the standard Nodes for any of the areas, then you can directly add style classes to your custom Nodes, with the caveat that you’ll still need to add the stylesheet to the DialogPane.

The Result Converter

You may have noticed that the TextInputDialog and ChoiceDialog do not return Optional<ButtonType>. TextInputDialog returns Optional<String> and ChoiceDialog is a generic class, return Optional<T> where T is whatever type you declared your ChoiceDialog to be.

So how does Dialog do this?

It uses something called a “Result Converter”.

primary

Understanding how to implement your own Result Converter is the key to creating custom Dialogs that do more interesting things.

A Result Converter is a Callback. A Callback is a functional interface that’s usually invoked deep within some standard element to allow it to do something customized. It’s essentially a “hook” for you to tie in your own code that will be called at some standard place. That standard element will “call back” to your code to figure out some value for it.

Technically, a Result Converter is of type Callback<ButtonType, R>. This means that it will be passed a ButtonType and it will return a value of type R, and that R is going to be the same R that you declared your Dialog to be. You just have to supply the code that says, “Ok, the user clicked ButtonType.DoIt so I need to return this value…”.

A Simple Custom Dialog Example

Now let’s say we’ve written some process that could possibly terminate badly in some way. We’ve captured the crash, and we need to have the user specify how to proceed from here:

Dialog With Conversion

Here we have three custom ButtonTypes, and we need to convert the button that’s clicked into something that’s meaningful to process that’s generated the Dialog. In this case we’re going to us the Enum RecoveryAction that we’ve created. So we’re going to use a Dialog<RecoveryAction>….

class CustomDialogExample : Application() {
   override fun start(primaryStage: Stage) {
      primaryStage.setScene(Scene(createContent()))
      primaryStage.show()
   }

   private fun createContent(): Region = BorderPane().apply {
      val button = Button("Click Me").apply {
         onAction = EventHandler {
            createDialog().showAndWait().ifPresentOrElse({ println("This is the result: $it") }, { println("No result") })
         }
      }
      center = button
      padding = Insets(50.0)
   }

   private fun createDialog() = Dialog<RecoveryAction>().apply {
      val dump = "Core Dump"
      val support = "Contact Support"
      val ignore = "Who Cares?"
      resultConverter = Callback { buttonType ->
         when (buttonType.text) {
            dump -> RecoveryAction.CORE_DUMP
            support -> RecoveryAction.CALL_SUPPORT
            ignore -> RecoveryAction.IGNORE
            else -> null
         }
      }
      headerText = "The Program has Bombed"
      title = "Oops!!!"
      graphic = this::class.java.getResource("/images/boom.png")?.toExternalForm()?.run { ImageView(this) }?.apply {
         fitWidth = 40.0
         isPreserveRatio = true
      }
      dialogPane.buttonTypes += listOf(ButtonType(dump), ButtonType(support), ButtonType(ignore))
      contentText = """Something horrible has happened.
Now you need to pick how you would like to handle the issue.
                     """
   }
}

enum class RecoveryAction { CORE_DUMP, CALL_SUPPORT, IGNORE }

fun main() {
   Application.launch(CustomDialogExample::class.java)
}

This is a very straight-forward use of the ButtonType -> RecoveryAction result converter. We just check the text associated with the ButtonType that the user clicked, and convert that into the Enum. Usually, the idea of using the text on the Button is a pretty horrible idea, but ButtonType doesn’t give a lot of options if you want to use the Buttons to select the action.

A More Integrated Example

As you’ve seen from TextInputDialog and ChoiceDialog, it’s possible to put data collection Nodes into Dialog and then access them from your Result Converter to return something that’s not just dependent on the Button that was clicked. This opens up a whole world of possibilities, as your Dialog can now contain all manner of layouts and can work just like any screen, and then you can gather up results and pass them on to your process.

Let’s move the choice from the Buttons and into the content of the Dialog. In this case, we’ll use RadioButtons:

Converted Dialog With RadioButtons

Unlike the previous version, it’s now possible to have an empty answer, because we have two buttons and one of them is a “Cancel” type. You can see that when that happens, we just return Null from the result converter. In the result converter, we check the ToggleGroup to see which RadioButton was selected. This all works because the Result Converter and the RadioButtons and the ToggleGroup were all defined in the same scope…

class CustomDialogExample : Application() {
   override fun start(primaryStage: Stage) {
      primaryStage.setScene(Scene(createContent()))
      primaryStage.show()
   }

   private fun createContent(): Region = BorderPane().apply {
      val button = Button("Click Me").apply {
         onAction = EventHandler {
            createDialog().showAndWait().ifPresentOrElse({ println("This is the result: $it") }, { println("No result") })
         }
      }
      center = button
      padding = Insets(50.0)
   }

   private fun createDialog() = Dialog<RecoveryAction>().apply {
      val dumpRB = RadioButton("Core Dump")
      val supportRB = RadioButton("Contact Support")
      val ignoreRB = RadioButton("Who Cares?")
      val toggleGroup = ToggleGroup().apply {
         toggles += listOf<Toggle>(dumpRB, supportRB, ignoreRB)
      }
      dialogPane.content = FlowPane(dumpRB, supportRB, ignoreRB).apply {
         hgap = 20.0
      }
      resultConverter = Callback { buttonType ->
         if (buttonType == ButtonType.OK) {
            when (toggleGroup.selectedToggle) {
               dumpRB -> RecoveryAction.CORE_DUMP
               supportRB -> RecoveryAction.CALL_SUPPORT
               ignoreRB -> RecoveryAction.IGNORE
               else -> null
            }
         } else null
      }
      headerText = "The Program has Bombed"
      title = "Oops!!!"
      graphic = this::class.java.getResource("/images/boom.png")?.toExternalForm()?.run { ImageView(this) }?.apply {
         fitWidth = 40.0
         isPreserveRatio = true
      }
      dialogPane.buttonTypes += listOf(ButtonType.CLOSE, ButtonType.OK)
      dialogPane.expandableContent = Label("""Something horrible has happened.
Now you need to pick how you would like to handle the issue.
                     """)
   }
}

enum class RecoveryAction { CORE_DUMP, CALL_SUPPORT, IGNORE }

fun main() {
   Application.launch(CustomDialogExample::class.java)
}

Since we have some actual content in the Dialog, the text description has been moved into the “Expanded Content” area.

This is still a pretty simple example, but you should be able to see that you could use the “Content” section of a Dialog as the View in an MVCI framework. You can have a Model and the Result Converter would be a method call in your Interactor. The Controller would be responsible for instantiating the actual Dialog and pass that back through getView(). If you did something like that, then the sky’s the limit for the kind of things that you could put into a Dialog.

Summary

Dialogs are primarily useful in two ways:

  1. When you have a process that needs to be interrupted to interact with the user.
  2. When you some kind of supplementary data entry that you don’t want to clutter your main GUI.

Of the two, the first is probably the most common. When you just need to send a message to the user, then your best route is to use one of the Alert versions of Dialog. When you need more complicated data collection from the user, then you’re probably going to have to design a custom Dialog to do the work.

Categories:

Updated: