Monthly Archives: March 2015

ScalaFX – Alerts and Dialogs

One of the new features on ScalaFX 8.0.40 are Alerts and Dialogs. Dialog API allows for opening a dialog window and returning result from the user. The result can be as simple as the type of button used to close the dialog. Custom dialog allows for returning an arbitrary result.

We will present examples of ScalaFX Alerts and Dialogs based on examples presented in JavaFX Dialogs blog post.

Simple Alerts

Information Alert

There are several predefined dialogs called alerts that can be easily presented to the user. A simplest alert can be shown with a single line of code:

 new Alert(AlertType.Information, "Hello Dialogs!!!").showAndWait()

013 - simple information alert

In general use you will typically customize it a bit more:

new Alert(AlertType.Information) {
  initOwner(stage)
  title = "Information Dialog"
  headerText = "Look, an Information Dialog"
  contentText = "I have a great message for you!"
}.showAndWait()

013 - information alert

initOwner() specifies the owner for a dialog. It is not required, but specifying an owner is a good style. It allows the dialog to use the same icon as the owner. It also will let block the parent when dialog is shown modal.

Warning Alert

new Alert(AlertType.Warning) {
  initOwner(stage)
  title = "Warning Dialog"
  headerText = "Look, an Warning Dialog."
  contentText = "Careful with the next step!"
}.showAndWait()

013 - warning alert

Error Alert

new Alert(AlertType.Error) {
  initOwner(stage)
  title = "Error Dialog"
  headerText = "Look, an Error Dialog."
  contentText = "Ooops, there was an error!"
}.showAndWait()

013 - error alert

Confirmation Alert

Alerts and dialogs can be used to query user for information. Every alert returns type of button that was pressed to close the dialog window. A simplest form is a confirmation dialog that indicates whether user pressed OK or Cancel buttons. Strictly speaking a dialog returns an Option containing type of button pressed or None.

// Create and show confirmation alert
val alert = new Alert(AlertType.Confirmation) {
  initOwner(stage)
  title = "Confirmation Dialog"
  headerText = "Look, a Confirmation Dialog."
  contentText = "Are you ok with this?"
}

val result = alert.showAndWait()

// React to user's selectioon
result match {
  case Some(ButtonType.OK) => println("OK")
  case _                   => println("Cancel or closed")
}

013 - confirmation alert

You can create a dialog returning any content, an example will be shown later. First let see how to use custom buttons in an alert.

Alerts with Custom Buttons

We can customize the button in an alert by defining ButtonType objects and passing them to Alert’s buttonTypes property. Notice that we overwrite the content of the property (rather than append to it):

    val ButtonTypeOne = new ButtonType("One")
    val ButtonTypeTwo = new ButtonType("Two")
    val ButtonTypeThree = new ButtonType("Three")

    val alert = new Alert(AlertType.Confirmation) {
      initOwner(stage)
      title = "Confirmation Dialog with Custom Actions"
      headerText = "Look, a Confirmation Dialog with Custom Actions."
      contentText = "Choose your option."
      // Note that we override here default dialog buttons, OK and Cancel, with new ones.
      buttonTypes = Seq(ButtonTypeOne, ButtonTypeTwo, ButtonTypeThree, ButtonType.Cancel)
    }

    val result = alert.showAndWait()

    result match {
      case Some(ButtonTypeOne)   => println("... user chose "One"")
      case Some(ButtonTypeTwo)   => println("... user chose "Two"")
      case Some(ButtonTypeThree) => println("... user chose "Three"")
      case _                     => println("... user chose CANCEL or closed the dialog")
    }

013 - confirmation custom alert

Alerts with Custom Content

You are not limited to simple text in an alert. You can add your custom content. For instance, there is no predefined Alert for showing exceptions, but you can add your own implementation:

// Create expandable Exception.
val exceptionText = {
  val ex = new FileNotFoundException("Could not find file blabla.txt")
  val sw = new StringWriter()
  val pw = new PrintWriter(sw)
  ex.printStackTrace(pw)
  sw.toString
}
val label = new Label("The exception stacktrace was:")
 val textArea = new TextArea {
  text = exceptionText
  editable = false
  wrapText = true
  maxWidth = Double.MaxValue
  maxHeight = Double.MaxValue
  vgrow = Priority.Always
  hgrow = Priority.Always
}
val expContent = new GridPane {
  maxWidth = Double.MaxValue
  add(label, 0, 0)
  add(textArea, 0, 1)
}

new Alert(AlertType.Error) {
  initOwner(stage)
  title = "Exception Dialog"
  headerText = "Look, an Exception Dialog."
  contentText = "Could not find file blabla.txt!"
  // Set expandable Exception into the dialog pane.
  dialogPane().expandableContent = expContent
}.showAndWait()

013 - exception alert

Text Input Dialog

You can get simple text input using the TextInputDialog. It works similar to alerts, but it returns an Option containing the text entered by the use (alerts return the button pressed):

val dialog = new TextInputDialog(defaultValue = "walter") {
  initOwner(stage)
  title = "Text Input Dialog"
  headerText = "Look, a Text Input Dialog."
  contentText = "Please enter your name:"
}

val result = dialog.showAndWait()

result match {
  case Some(name) => println("Your name: " + name)
  case None       => println("Dialog was canceled.")
}

013 - text input dialog

Choice Dialog

There is also a predefined dialog for selecting from a list of available choices: ChoiceDialog.The list can be collection of arbitrary objects. The choice dialog will return Option selected by the user. You create the dialog by specifying default choice and the collection of available choices.

val choices = Seq("a", "b", "c")

val dialog = new ChoiceDialog(defaultChoice = "b", choices = choices) {
  initOwner(stage)
  title = "Choice Dialog"
  headerText = "Look, a Choice Dialog."
  contentText = "Choose your letter:"
}

val result = dialog.showAndWait()

result match {
  case Some(choice) => println("Your choice: " + choice)
  case None         => println("No selection")
}

013 - choice dialog

Custom Dialog

Custom dialogs are created using Dialog class. Below is an example of a login dialog. Class Result defines result returned by the dialog. The dialog contains a custom graphic, two input fields (“Username” and “Password”), and custom buttons (“Login” and “Cancel”).

case class Result(username: String, password: String)

// Create the custom dialog.
val dialog = new Dialog[Result]() {
  initOwner(stage)
  title = "Login Dialog"
  headerText = "Look, a Custom Login Dialog"
}

// Set the button types.
val loginButtonType = new ButtonType("Login", ButtonData.OKDone)
dialog.dialogPane().buttonTypes = Seq(loginButtonType, ButtonType.Cancel)

// Create the username and password labels and fields.
val username = new TextField() {
  promptText = "Username"
}
val password = new PasswordField() {
  promptText = "Password"
}

val grid = new GridPane() {
  hgap = 10
  vgap = 10
  padding = Insets(20, 100, 10, 10)

  add(new Label("Username:"), 0, 0)
  add(username, 1, 0)
  add(new Label("Password:"), 0, 1)
  add(password, 1, 1)
}

// Enable/Disable login button depending on whether a username was entered.
val loginButton = dialog.dialogPane().lookupButton(loginButtonType)
loginButton.disable = true

// Do some validation (disable when username is empty).
username.text.onChange { (_, _, newValue) => 
  loginButton.disable = newValue.trim().isEmpty
}

dialog.dialogPane().content = grid

// Request focus on the username field by default.
Platform.runLater(username.requestFocus())

// When the login button is clicked, convert the result to a username-password-pair.
dialog.resultConverter = dialogButton =>
  if (dialogButton == loginButtonType) Result(username.text(), password.text())
  else null

val result = dialog.showAndWait()

result match {
  case Some(Result(u, p)) => println("Username=" + u + ", Password=" + p)
  case None               => println("Dialog returned: None")
}

013 - custom login dialog

Summary

ScalaFX 8.0.40-R8 brings support for Alerts and Dialogs. There are several predefined dialogs: Information, Warning, Error, Confirmation, Text Input, and Choice. Predefined dialog allow some level of customization of their content and buttons. Source code for the examples of pre-defined dialogs, including customization, are in DialogsDemo.

Completely customized dialogs can be created using Dialog class. Source code for a custom login dialog is in LoginDialogDemo.

To use ScalaFX 8.0.40 add following to your SBT:

libraryDependencies += "org.scalafx" %% "scalafx" % “8.0.40-R8”
Advertisements

ScalaFX 8u40 – TextFormatter – part 2

JavaFX 8.0_u40 brings several enhancements, the new ScalaFX release 8.0.40 supports them the. The new Spinner and the basic use of TextFormatter were described in previous blogs. ScalaFX use of Dialogs and Alerts will be described in following posts.

TextFormatter restricts and formats the text that can be displayed and typed in a TextInputControl. TextFormatter has one or both:

  • Filter – intercepts and can modify or reject text changes,
  • ValueConverter – a StringConverter that converts between the values and text. There are several predefined converters in scalafx.util.converter package.

In the first part we presented a simple scenario where we specify a string converter to display TextField content as currency. Here we will look at an example that uses a filter and a converter together.

Imagine, for the sake of this example, that we have a TextField that should contain one part of the text that is fixed and the rest that can be freely edited. User types messages in that text field and presses Enter to send. The restriction is that that text field always contains a prompt: > in front of the message:

011 - input

User should be able to freely edit the message, but should not be able to delete the prompt or move caret on or in front of the prompt. As in the previous example a converter is used to ensure that there is a prompt in front of the message when content of the text field is set. However the converter is not activated while user is editing content of a text input field, only when the value is set. TextFormatter’s filter gives you ability to intercept and modify any change done to the text content. It is called as changes happen before they are applied or displayed on the screen. The filter is a function that takes as an input change event; it can modify that it before it returns it back:

def filter(change:Change) : Change

In our case we will look for changes that would remove prompt or misplace the caret.

val prompt = "> "
...  
val filter: (Change) => Change = { change: Change =>
  // Restore prompt if part was deleted
  if (change.controlNewText.length <= prompt.length) {
    change.text = prompt.substring(change.controlNewText.length)
  }
  // Restore caret position if it moved over the prompt
  if (change.anchor < prompt.length) { 
    change.anchor = prompt.length
  }
  if (change.caretPosition < prompt.length) {
    change.caretPosition = prompt.length
  }
  change
}

As in the previous example we will use a custom text converter that will add prompt when text is set:

case class Message(text: String) {
  override def toString = s""$text""
}

val converter = new StringConverter[Message] {
  override def fromString(s: String): Message = {
    val r = if (s.startsWith(prompt)) s.substring(prompt.length)
            else s
      Message(r)
  }
  override def toString(v: Message): String = prompt + v.text
}

Now we use our converter and filter to create a text formatter, with some default content:

val formatter = new TextFormatter[Message](converter, Message("hello"), filter)

And the text field with our text formatter and action that sends the messages when Enter is pressed.

val textField = new TextField {
  text = prompt
  textFormatter = formatter
  onAction = (a: ActionEvent) => {
    val str = text()
    val message = converter.fromString(str) + "\n"
    outputTextArea.text = message + outputTextArea.text()
    text() = ""
  }
}

Here is the complete example:

011 - input-output

import scalafx.Includes._
import scalafx.application.JFXApp
import scalafx.application.JFXApp.PrimaryStage
import scalafx.event.ActionEvent
import scalafx.geometry.Insets
import scalafx.scene.Scene
import scalafx.scene.control.TextFormatter.Change
import scalafx.scene.control.{Label, TextArea, TextField, TextFormatter}
import scalafx.scene.layout.{BorderPane, VBox}
import scalafx.util.StringConverter

object TextFormatterWithChangeFilterDemo extends JFXApp {

  case class Message(text: String) {
    override def toString = '"' + text + '"'
  }

  val prompt = "> "

  val converter = new StringConverter[Message] {
    override def fromString(s: String): Message = {
      val r =
        if (s.startsWith(prompt)) s.substring(prompt.length)
        else s
      Message(r)
    }
    override def toString(v: Message): String = {
      prompt + v.text
    }
  }

  // Filter the change restoring prompt if it was removed and 
  // correcting caret position
  val filter: (Change) => Change = { change: Change =>
    // Restore prompt if part was deleted
    if (change.controlNewText.length <= prompt.length) {
      change.text = prompt.substring(change.controlNewText.length)
    }
    // Restore caret position if it moved over the prompt
    if (change.anchor < prompt.length) change.anchor = prompt.length
    if (change.caretPosition < prompt.length) change.caretPosition = prompt.length
    change
  }
  val formatter = new TextFormatter[Message](converter, Message("hello"), filter)

  val outputTextArea = new TextArea {
    editable = false
    focusTraversable = false
  }

  val textField = new TextField {
    text = prompt
    textFormatter = formatter
    onAction = (a: ActionEvent) => {
      val str = text()
      val message =
        if (outputTextArea.text().length == 0)
          converter.fromString(str)
        else
          converter.fromString(str) + "n"
      outputTextArea.text = message + outputTextArea.text()
      text() = ""
    }
  }

  stage = new PrimaryStage {
    scene = new Scene(300, 200) {
      title = "TextFormatter Demo"
      root = new VBox {
        spacing = 6
        padding = Insets(10)
        children = Seq(
          new BorderPane {
            top = textField
            center = outputTextArea
          }
        )
      }
    }
  }
}

The TextFormatter support is available in latest SNAPSHOT releases of ScalaFX. To use it add following to your SBT build file:

libraryDependencies += "org.scalafx" %% "scalafx" % “8.0.40-SNAPSHOT”