Open source Java projects: JFXtras

Utilities and add-ons for the JavaFX Script programming language

JavaFX is a rapidly maturing technology, but its capabilities are still limited. In this installment of Open source Java projects, Jeff Friesen introduces you to JFXtras, utilities and add-ons that fill in useful features that are absent in JavaFX 1.0.

JavaFX 1.0 is missing a lot of functionality, especially in regard to UI components and layouts. To address these deficiencies, developer Stephen Chin initiated the JFXtras project in late 2008. He released JFXtras 0.1 shortly after JavaFX 1.0 debuted. This initial JFXtras release introduced dialog boxes, more layouts, a unit-testing framework, and asynchronous thread support to JavaFX.

The JFXtras 0.2 milestone, released in January 2009, adds a new library of custom shapes via integration with Andres Almiray's jSilhouette Project, and it enhances 0.1's Grid layout, unit-testing framework, and more. With 0.3's release on February 17 (shortly after this article's completion), JFXtras is becoming an increasingly significant part of the JavaFX landscape.

This article focuses on JFXtras 0.2. After introducing you to the project's software distribution, I demonstrate how to use the software via the NetBeans and command-line versions of JavaFX. I then explore JFXtras' various features, ranging from asynchronous thread support and a custom shapes library to unit testing and utilities classes.

Open source licenses

Each of the Java projects covered in this series is subject to an open source license, which you should understand before integrating the project with your own projects. JFXtras is subject to the updated Berkeley Software Distribution (BSD) License. It is not subject to the simplified variant of this license or to any other license.

Getting started with JFXtras

The JFXtras project site's downloads page provides links for downloading JFXtras-0.2.jar and JFXtras 0.2.zip. If you're interested in the custom shapes library, you must download the ZIP file, which includes JFXtras-0.2.jar and two other JAR files that provide the jSilhouette scene graph infrastructure for custom shapes. If custom shapes are of no interest to you, you can download JFXtras-0.2.jar instead, but I recommend downloading JFXtras 0.2.zip. This archive's JFXtras 0.2 home directory contains the ChangeLog.txt, JFXtras-0.2.jar, and LICENSE.txt files. It also contains javadoc (API documentation), lib (jsilhouette-geom-0.3.jar and jsilhouette-scene-0.3.jar, custom shapes support JARs), src (JFXtras source code), and test (scripts for unit-testing JFXtras) subdirectories.

In addition to JFXtras 0.2, you need to install either NetBeans IDE 6.5 with JavaFX 1.0 or the JavaFX 1.0 SDK (if not already installed). Also, Windows platforms should have Java SE 6u10 or a higher update installed, whereas Macintosh platforms will benefit from Java 10.5 Update 2 (1.6.0_07). I developed this article's code with both JavaFX SDKs and Java SE 6u12 on a Windows XP SP3 platform.

A simple script

Before exploring JFXtras, let's play with a couple of examples, to familiarize you with developing scripts in the context of the NetBeans and command-line versions of JavaFX 1.0. For our first example, check out Listing 1.

Listing 1. Main.fx for a CharSeq project

/*
 * Main.fx
 */

package charseq;

import org.jfxtras.util.*;

def digits = SequenceUtil.characterSequence ("0", "9");
println (digits) // Output: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

This script invokes the SequenceUtil class's characterSequence() function to generate a sequence of single-character Strings from 0 through 9. You'll learn about characterSequence() and other SequenceUtil functions later in the article.

You'll probably prefer to work in the NetBeans environment, so complete the following steps to compile and run this script using NetBeans IDE 6.5 with JavaFX 1.0:

  1. Use the New Project wizard to introduce a CharSeq project with charseq.Main as the main file.
  2. Replace the generated skeletal Main.fx with Listing 1.
  3. Right-click CharSeq in the Projects window and select Properties from the pop-up menu.
  4. Select the Libraries category on the resulting Project Properties dialog box.
  5. Click the Add JAR/Folder button.
  6. Locate the appropriate directory containing JFXtras-0.2.jar and add this JAR to the Libraries list.
  7. Dismiss the Project Properties dialog.
  8. Click the green triangle button on the main toolbar, or press F6 to run the script.

If you would rather work at the command line, complete the following (Windows-oriented) steps to compile and run this script using the command-line version of JavaFX:

  1. Create a charseq subdirectory of the current directory.
  2. Copy Listing 1 to a Main.fx file and store this file in charseq.
  3. Copy JFXtras-0.2.jar to the current directory.
  4. Compile Main.fx via javafxc -cp JFXtras-0.2.jar charseq\Main.fx.
  5. Run the script via javafx -cp JFXtras-0.2.jar;. charseq.Main.

A shape-oriented script

When working with JFXtras, you'll often only need to access JFXtras-0.2.jar. However, if you're planning to work with custom shapes, as is the case with Listing 2, you'll also need to access jsilhouette-geom-0.3.jar and jsilhouette-scene-0.3.jar.

Listing 2. Main.fx for an Asterisk project

/*
* Main.fx
 */

package asterisk;

import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import org.jfxtras.scene.shape.Asterisk;

Stage
{
    title: "Centered Asterisk"
    width: 250
    height: 250

    var sceneRef: Scene
    scene: sceneRef = Scene
    {
        content: Asterisk
        {
            centerX: bind sceneRef.width/2.0
            centerY: bind sceneRef.height/2.0
            radius: 80
            width: 20
            beams: 5
            roundness: 0.5
            fill: Color.RED
        }
    }
}

This script creates a scene consisting of a single five-legged red asterisk shape whose center is always bound to the center of the scene. You'll learn about Asterisk and other custom shape classes later in this article.

Custom shapes glitches

If you're working on a Windows platform and using JavaFX 1.0 with Java SE 6u7 (the earliest JRE that JavaFX runs on for Windows), you may notice some strange glitches with JFXtras' custom shapes. For example, when I tried to run Listing 2 with Java SE 6u7, the asterisk was not painted properly, nor was it centered in the client area.

Follow the steps below to build and run this script from NetBeans:

  1. Use the New Project wizard to introduce an Asterisk project with asterisk.Main as the main file.
  2. Replace the generated skeletal Main.fx with Listing 2.
  3. Right-click Asterisk in the Projects window and select Properties from the pop-up menu.
  4. Select the Libraries category on the resulting Project Properties dialog box.
  5. Click the Add JAR/Folder button.
  6. Locate the appropriate directory containing JFXtras-0.2.jar and add this JAR to the Libraries list.
  7. Click the Add JAR/Folder button a second time.
  8. Change to the lib subdirectory containing jsilhouette-geom-0.3.jar and add this JAR to the Libraries list.
  9. Click the Add JAR/Folder button a third time.
  10. Add jsilhouette-scene-0.3.jar to the Libraries list.
  11. Dismiss the Project Properties dialog.
  12. Click the green triangle button on the main toolbar, or press F6 to run the script.

Figure 1 shows the resulting window with its asterisk shape.

The asterisk always appears in the window's center no matter how you resize the window
Figure 1. The asterisk always appears in the window's center no matter how you resize the window.

Follow the steps below to build and run this script using the command-line version of JavaFX:

  1. Create an asterisk subdirectory of the current directory.
  2. Copy Listing 2 to a Main.fx file and store this file in asterisk.
  3. Copy JFXtras-0.2.jar to the current directory.
  4. Create lib as a subdirectory of the current directory.
  5. Copy jsilhouette-geom-0.3.jar and jsilhouette-scene-0.3.jar to lib.
  6. Compile Main.fx via javafxc -cp JFXtras-0.2.jar asterisk\Main.fx.
  7. Run the script via javafx -cp JFXtras-0.2.jar;. asterisk.Main.

Exploring JFXtras

JFXtras 0.2 consists of nearly 50 classes organized into 10 packages, with each package name beginning with the org.jfxtras prefix. This section briefly examines some of these classes. For complete details, check out the API documentation in JFXtras 0.2.zip's javadoc directory, or point your browser to the  online Javadoc.

Asynchronous thread support

JavaFX Script is a single-threaded language. On Swing-based platforms, the event-dispatching thread runs JavaFX code. When this thread is delayed, such as when it's waiting for a Web-based document to download, the user interface's responsiveness suffers. To overcome this problem, you'll want to perform time-consuming operations on a background thread.

The nature of JavaFX's implementation makes it potentially dangerous to subclass Java's Thread class and start a background thread based on this subclass. Instead, you'll typically need to subclass JavaFX's AbstractAsyncOperation class (see James Clarke's JavaFX Async operations blog post for an example) if its RemoteTextDocument subclass doesn't meet your needs.

Subclassing AbstractAsyncOperation is somewhat cumbersome, especially when you find yourself coding part of the subclass's implementation in Java (to avoid unintended conflicts with binding, triggers, and the rest of the JavaFX runtime). Fortunately, you can avoid this challenge by taking advantage of JFXtras' org.jfxtras.async.JFXWorker class, which offers a general-purpose solution.

JFXWorker uses Java SE 6's SwingWorker class to execute an asynchronous operation on a background thread and make the result available on the event-dispatching thread. The asynchronous operation is described via a function assigned to JFXWorker's inBackground attribute. When the operation completes, JFXWorker invokes the function assigned to the onDone attribute.

The function assigned to inBackground returns an object (or null), which is then passed as an argument to onDone's function after inBackground's function successfully completes. However, if inBackground's function throws an exception, the function assigned to the onFailure attribute is invoked instead of onDone's function.

Listing 3 demonstrates JFXWorker's usefulness in the context of searching all files for a specific text string.

Listing 3. Main.fx for a TextSrch project

/*
 * Main.fx
 */

package textsrch;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;

import java.lang.Thread;

import java.nio.CharBuffer;

import java.util.regex.Pattern;

import javafx.ext.swing.SwingButton;
import javafx.ext.swing.SwingLabel;
import javafx.ext.swing.SwingList;
import javafx.ext.swing.SwingListItem;
import javafx.ext.swing.SwingScrollPane;
import javafx.ext.swing.SwingTextField;

import javafx.scene.Cursor;
import javafx.scene.Scene;

import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;

import javafx.scene.paint.Color;

import javafx.stage.Stage;

import org.jfxtras.async.JFXWorker;

class FindModel
{
    // The following attribute is populated (on a JFXWorker background thread)
    // with the SwingListItem-wrapped paths of matching files during a search.
    // It's also accessed by the UI (via binding) on the event-dispatching
    // thread to present these paths in a Swing listbox.

    var items: SwingListItem [];

    // The following attribute is used to dynamically disable the Search button
    // and enable the Stop button once a search begins, and do the reverse once
    // a search ends. More importantly, it's used to prevent updating a
    // SwingList component's items attribute (via binding) until the search
    // ends. This is done to avoid a potential thread-synchronization problem.

    var searching: Boolean;

    // The rest of these attributes should not be accessed from outside this
    // class. If I factored out this class into its own file, I would make
    // sure that only the two attributes above could be accessed.

    var worker: JFXWorker;

    def LIMIT = 500; // Store no more than LIMIT items in items, to avoid
                     // exhausting heap memory and generating an
                     // OutOfMemoryError. How might this happen? Imagine a
                     // scenario where you have 10 roots to search, and they
                     // have a combined total of 10 million matching files --
                     // we're searching for the empty string (which always
                     // matches), for example. Also, suppose the average path
                     // length for these files is 50 characters. We would need
                     // approximately one billion bytes of heap memory to store
                     // all of these paths in items.

    var counter: Integer;

    def BUFSIZE = 60000; // 40000-60000 seems to be optimal on my platform.
    def buffer = CharBuffer.allocate (BUFSIZE);

    function find (text: String): Void
    {
        searching = true;
        worker = JFXWorker
        {
            inBackground: function ()
            {
                // This code executes on a background thread.

                delete items;
                counter = 0;

                // This script is coded to search all files on all roots. As an
                // exercise, improve the UI and the following code to allow the
                // user to choose a range of roots, and a range of a given root
                // to search.

                def roots = File.listRoots ();
                for (i in [0..<sizeof roots])
                     findall (roots [i], text);

                return null
            }

            onDone: function (result): Void
            {
                // This code executes on the event-dispatching thread.

                searching = false;
            }
        }
    }

    function kill (): Void
    {
        worker.cancel ()
    }

    // The rest of these functions should not be accessed from outside this
    // class. If I factored out this class into its own file, I would make
    // sure that only the two functions above could be accessed.

    function find (filename: String, srchtext: String): Boolean
    {
        // All files match the empty string.

        if (srchtext.length () == 0)
            return true;

        // Compile the search text as a regex pattern (for performance).
        // Treat all special regex characters as if they were literals,
        // and also select case-insensitive pattern matching.

        def pattern = Pattern.compile (srchtext, Pattern.LITERAL+
                                       Pattern.CASE_INSENSITIVE);

        var fr: FileReader;

        try
        {
            fr = new FileReader (filename);

            // Prepare for initial read.

            buffer.clear ();

            while (true)
            {
                // Read up to BUFSIZE characters into the buffer. If the
                // return value is -1, no characters have been read.
                // Therefore, we can safely conclude that the file is
                // either empty (if this is the first read) or that the
                // search text is not present in the file.

                if (fr.read (buffer) == -1)
                    return false;

                // Confine pattern match to only those characters that have
                // been read -- not to the entire buffer.

                buffer.flip ();

                // Extract a matcher and attempt to find the search text in
                // the buffer.

                def matcher = pattern.matcher (buffer);
                if (matcher.find ())
                    return true;

                // Because the text was not found, continue the search after
                // copying srchtext.length ()-1 characters from the end
                // of the buffer to the start of the buffer. The search
                // starts with these characters, which might actually be the
                // beginning of the search text.

                buffer.limit (buffer.capacity ());
                buffer.position (buffer.capacity()-srchtext.length ()+1);
                buffer.compact()
            }
        }
        catch (e: IOException)
        {
        }
        finally
        {
            if (fr != null)
                try { fr.close () } catch (e: IOException) {}
        }

        return false
    }

    function findall (file: File, srchtext: String): Void
    {
        var files: File [] = file.listFiles ();
        for (i in [0..<sizeof files])
        {
            if (Thread.currentThread ().isInterrupted ())
                return;

            if (counter == LIMIT)
                return;

            if (files [i].isDirectory ())
                findall (files [i], srchtext)
            else
            if (find (files [i].getPath (), srchtext))
            {
                insert SwingListItem { text: files [i].getPath () }
                    into items;
                counter++
            }
        }
        delete files
    }
}

Stage
{
    def model = FindModel {}

    title: "Text Search"

    width: 400
    height: 300
    resizable: false

    var sceneRef: Scene
    scene: sceneRef = Scene
    {
        fill: Color.GOLD

        var vboxRef: VBox
        content: vboxRef = VBox
        {
            var input: SwingTextField
            content:
            [
                HBox
                {
                    content:
                    [
                        SwingLabel
                        {
                            text: "Enter search text"
                        }
                        input = SwingTextField
                        {
                            columns: 23
                        }
                    ]

                    spacing: 10
                }
                HBox
                {
                    content:
                    [
                        SwingButton
                        {
                            action: function (): Void
                            {
                                model.find (input.text)
                            }

                            enabled: bind not model.searching

                            text: "Search"
                        }
                        SwingButton
                        {
                            action: function (): Void
                            {
                                model.searching = false;
                                model.kill ()
                            }

                            enabled: bind model.searching

                            text: "Stop"
                        }
                    ]

                    spacing: 10
                }
                SwingLabel
                {
                    text: "Results"
                }
                SwingScrollPane
                {
                    cursor: bind if (model.searching)
                                     Cursor.WAIT
                                 else
                                     Cursor.DEFAULT

                    view: SwingList
                    {
                        items: bind if (model.searching)
                                        [
                                            SwingListItem
                                            {
                                                text: "Scanning files..."
                                            }
                                        ]
                                    else
                                        model.items
                    }

                    width: bind sceneRef.width-2*vboxRef.spacing
                }
            ]

            spacing: 10

            translateX: bind (sceneRef.width-vboxRef.boundsInLocal.width)/2
            translateY: bind (sceneRef.height-vboxRef.boundsInLocal.height)/2
        }
    }
}

Listing 3 describes a text-search application that lets you input search text via a text field, initiate a search by clicking the Search button, stop a search by clicking the Stop button, and view a list of files containing the search text via a scrollable list. Figure 2 shows this application's user interface.

The list displays a 'Scanning files... ' message until the search completes.
Figure 2. The list displays a "Scanning files..." message until the search completes. (Click to enlarge.)

Listing 3 uses a FindModel class to encapsulate search-specific attributes and functions. The items attribute stores a sequence of SwingListItems; new SwingListItems are appended to this sequence as a search progresses. The searching attribute is set to true when a search begins, cleared to false when a search ends, and is used to enable or disable the buttons.

When the user clicks the Search button, a JFXWorker is created and the function assigned to inBackground begins to run. This function invokes FindModel's findall() function for each of the file system's roots (drive letters on Windows platforms). The findall() function recursively invokes itself and a find() helper function to accomplish its work.

Instead of returning an object for the onDone function to process, the inBackground function relies on findall() updating items, and on the SwingList object assigned to SwingScrollPane's view attribute binding the model's items attribute to its items attribute (but only if the model's searching attribute is set to false).

When you click Stop to cancel a long-running search, its action function invokes JFXWorker's public cancel(): Void function. This function sets the background thread's interrupted state, which the findall() function constantly checks (via java.lang.Thread.currentThread ().isInterrupted ()). When this method returns true, findall() exits and the search ends.

Custom shapes library

JFXtras 0.2 features a custom shapes library for introducing arrows, balloons, crosses, Reuleaux triangles, Lauburu, and other shapes not available in JavaFX 1.0 to your user interfaces. You can explore these shapes via JFXtras' ShapesDemoFX demo program: javafx -cp JFXtras-0.2.jar org.jfxtras.scene.shape.ShapesDemoFX (the current directory must include lib with its jSilhouette JARs).

Figure 3 shows this program's user interface.

When you move the mouse over one of the shape names in the left column, you can see several variations of that shape in the right column.
Figure 3. When you move the mouse over one of the shape names in the left column, you can see several variations of that shape in the right column. (Click to enlarge.)

The custom shapes library consists of 18 classes in the org.jfxtras.scene.shape package: Almond, Arrow, Asterisk, Astroid, Balloon, Cross, Donut, ETriangle, ITriangle, Lauburu, MultiRoundRectangle, Rays, RegularPolygon, ResizableRectangle, ReuleauxTriangle, RoundPin, RTriangle, and Star2. Let's take a closer look at Asterisk.

Asterisk subclasses javafx.scene.shape.Shape, which subclasses javafx.scene.Node. (The same is true for the other classes except for ResizableRectangle.) In addition to inheriting from Shape and Node, Asterisk provides the following seven attributes (all of type Number):

  • angle specifies the number of degrees of rotation; it defaults to 0.0.
  • beams specifies the number of beams (or arms) emanating from the asterisk's center; it defaults to 5.0.
  • centerX specifies the X coordinate (in pixels) of the asterisk's center; it defaults to 0.0.
  • centerY specifies the Y coordinate (in pixels) of the asterisk's center; it defaults to 0.0.
  • radius specifies the asterisk's radius (in pixels); it defaults to 40.0.
  • roundness specifies the roundness of the asterisk's outside corners. This value ranges from 0.0 through 1.0 (the closer to 1.0, the rounder the corners); it defaults to 0.0.
  • width specifies the thickness of the arm; it defaults to 30.0.

Listing 2 demonstrated Asterisk by using this class to display a red asterisk shape in the center of the stage's scene. Try changing the various attribute values in this listing's Asterisk object literal, and also introduce the angle attribute. Although you can assign a positive value to angle, can you also assign a negative value?

Dialog boxes

JavaFX's javafx.ext.swing package lacks support for dialog boxes. JFXtras recognizes this deficiency by providing the org.jfxtras.stage package with its JFXStage and JFXDialog classes. JFXStage subclasses javafx.stage.Stage, and JFXDialog subclasses JFXStage.

JFXStage offers window capabilities that are absent from Stage. At present, this class only provides support for always-on-top windows, via the alwaysOnTop attribute (of type Boolean). Set this attribute to true, and the stage's window appears over other windows (except when minimized). This article's code archive provides an Asterisk2 NetBeans project that demonstrates this feature.

JFXDialog offers owned or ownerless, modal or modeless dialog boxes that can pack their contents so that these contents just fit the window. Because JFXDialog subclasses JFXStage, you can choose to have the dialog box remain above other windows (by setting alwaysOnTop to true) whenever the user switches focus to another application's window.

JFXDialog's attributes include modal, owner, and packed:

  • modal (of type Boolean) lets you block user input in the owner window until the dialog is dismissed, when set to true. This attribute defaults to false.
  • owner (of type Stage) lets you specify the dialog's owner window. This attribute defaults to null (ownerless window).
  • packed (of type Boolean) lets you tell JavaFX to calculate the minimum window size that will allow the dialog window to present all of its content, when set to true. This attribute is only recognized when the dialog is created (future dialog size changes are ignored). Also, this attribute defaults to false.

I've created a TextSrch2 NetBeans project (as an extension to the previous TextSrch project) that demonstrates JFXDialog. Listing 4 excerpts this project's Help class.

Listing 4. The Help class from a TextSrch2 project's Main.fx file

class Help
{
    function showHelp (owner: Stage): Void
    {
        def helpText = "<html>"
                       "Welcome to Text Search!<br><br>"
                       "Enter the search text in the text field.<br>"
                       "Click <b>Search</b> to initiate the search.<br>"
                       "Click <b>Stop</b> to interrupt a search.<br>"
                       "Click <b>Help</b> to display this help.<br>"
                       "<br>"
                       "The paths of all files that contain<br>"
                       "the search text appear in the list."
                       "</html>";

        var dialogRef: JFXDialog;
        dialogRef = JFXDialog
        {
            title: "Text Search Help"

            owner: owner
            modal: true
            packed: true
            resizable: false

            scene: Scene
            {
                content: VBox
                {
                    content:
                    [
                        SwingLabel
                        {
                            text: helpText
                        }
                        SwingButton
                        {
                            action: function (): Void
                            {
                                dialogRef.close ()
                            }

                            text: "OK"
                        }
                    ]

                    spacing: 10
                }
            }
        }
    }
}

Listing 4 assigns an owner to the owner attribute, specifies a modal window by assigning true to modal, and also assigns true to packed so that the window's contents will just fit into the window. The listing also assigns false to the inherited resizable attribute to prevent the dialog box window from being resized. (The components are not repacked.)

In response to being clicked, the Help button's action function executes Help {}.showHelp (stageRef) to display the dialog box, whose contents appear in Figure 4.

The text-search window and dialog box are initially centered on the screen.
Figure 4. The text-search window and dialog box are initially centered on the screen. (Click to enlarge.)

Grid layout

Apart from HBox and VBox, JavaFX 1.0 is lacking in layout classes. Once again, JFXtras comes to the rescue by providing the org.jfxtras.scene.layout.Grid class (for achieving form-based layouts) and its org.jfxtras.scene.layout.Cell, org.jfxtras.scene.layout.GridConstraints, org.jfxtras.scene.layout.Row, and other helper classes.

Grid's attributes include:

  • border (of type Number) specifies the size (in pixels) of the empty and transparent border that surrounds the grid. This attribute defaults to 3.0.
  • hgap (of type Number) specifies the width (in pixels) of empty space between adjacent horizontal cells. This attribute defaults to 3.0.
  • rows (of type Row[]) specifies the rows of nodes that are laid out by the grid.
  • vgap (of type Number) specifies the height (in pixels) of empty space between adjacent vertical cells. This attribute defaults to 3.0.

The Row class provides a single attribute, cells (of type Object[]), which contains the nodes to be laid out in a row. Each entry in this sequence is either a JavaFX node (such as an instance of the javafx.scene.text.Text or javafx.scene.shape.Circle class) or a Cell instance that wraps a node for additional layout control. The Cell class wraps a node (via its content attribute, of type Node) to provide extra control over how the node is positioned and oriented in the grid. This control includes alignment, span, grow, and the ability to override javafx.scene.layout.Resizable properties. Some of Cell's attributes are:

  • halign (of type org.jfxtras.scene.layout.HorizontalAlignment) specifies the horizontal alignment of the cell's contents. This attribute is one of GridConstraints.CENTER (horizontally align to the center of the container), GridConstraints.HFILL (horizontally fill the container with the cell contents), GridConstraints.LEFT (horizontally align to the left of the container), or GridConstraints.RIGHT (horizontally align to the right of the container).
  • hgrow (of type org.jfxtras.scene.layout.Grow) specifies the priority for allocating unused horizontal space. This attribute is one of Grow.ALWAYS (cell always tries to grow, sharing unused space equally with other cells whose hgrow attribute is set to Grow.ALWAYS), Grow.NEVER (don't grow horizontally -- the default), and Grow.SOMETIMES (cell tries to grow, and gets an equal share of unused space if there are no other cells whose hgrow attribute is set to Grow.ALWAYS).
  • hspan (of type Integer) specifies the number of columns occupied by this cell.
  • valign (of type org.jfxtras.scene.layout.VerticalAlignment) specifies the vertical alignment of the cell's contents. This attribute is one of GridConstraints.BASELINE (vertically align the portion above the baseline of the cell to the center of the container), GridConstraints.BOTTOM (vertically align to the bottom of the container), GridConstraints.MIDDLE (vertically align to the center of the container), GridConstraints.TOP (vertically align to the top of the container), and GridConstraints.VFILL (vertically fill the container with the cell contents).
  • vgrow (of type Grow) specifies the priority for allocating unused space vertically. It takes the same values and default as hgrow.

I've created a TextSrch3 NetBeans project (as an extension to the previous TextSrch2 project) that demonstrates Grid and related classes. Listing 5 excerpts this project's Help class.

Listing 5. The Help class from a TextSrch3 project's Main.fx file

class Help
{
    function showHelp (owner: Stage): Void
    {
        def helpText = "<html>"
                       "Welcome to Text Search!<br><br>"
                       "Enter the search text in the text field.<br>"
                       "Click <b>Search</b> to initiate the search.<br>"
                       "Click <b>Stop</b> to interrupt a search.<br>"
                       "Click <b>Help</b> to display this help.<br>"
                       "<br>"
                       "The paths of all files that contain<br>"
                       "the search text appear in the list."
                       "</html>";

        var dialogRef: JFXDialog;
        dialogRef = JFXDialog
        {
            title: "Text Search Help"

            owner: owner
            modal: true
            packed: true
            resizable: false

            scene: Scene
            {
                content: Grid
                {
                    border: 15

                    rows:
                    [
                        Row
                        {
                            cells: SwingLabel
                            {
                                text: helpText
                            }
                        }
                        Row
                        {
                            cells: Cell
                            {
                                content: SwingButton
                                {
                                    action: function (): Void
                                    {
                                        dialogRef.close ()
                                    }

                                    text: "OK"
                                }

                                horizontalAlignment: GridConstraints.CENTER
                            }
                        }
                    ]

                    vgap: 10
                }
            }
        }
    }
}

Listing 5 initializes Grid's border attribute to specify an empty border around the dialog box's content. It also initializes Grid's vgap attribute to place some empty space between the text and the button. The rows attribute is populated with two Rows, with the second row consisting of a single Cell that centers its wrapped SwingButton.

Figure 5 shows the visually improved dialog box.

The grid simplifies component centering, provides a border, and provides a vertical gap between the text and the button.
Figure 5. The grid simplifies component centering, provides a border, and provides a vertical gap between the text and the button. (Click to enlarge.)

Additional layouts

The org.jfxtras.scene.layout package provides two other layout classes: Deck and EmptyBorder. By stacking nodes on top of each other, Deck is reminiscent of the java.awt.CardLayout class (which stacks components) in the Abstract Window Toolkit (AWT). EmptyBorder subclasses Deck, letting you surround the stacked nodes with empty space.

Language-oriented classes

The org.jfxtras.lang package provides JFXObject and JFXException classes for (respectively) providing base functionality that is generally useful to all JavaFX objects (such as returning an object's class type), and for supporting the declarative construction of runtime exceptions (that is, classes extending java.lang.RuntimeException). Listing 6 demonstrates these classes.

Listing 6. Main.fx for a LangDemo project

/*
 * Main.fx
 */
package langdemo;

import org.jfxtras.lang.JFXException;
import org.jfxtras.lang.JFXObject;

class NegativeBalanceException extends JFXException
{
}

class Account extends JFXObject
{
    var balance: Number on replace oldValue = newValue
    {
        if (newValue < 0.0)
        {
            balance = oldValue;
            throw NegativeBalanceException
            {
                message: "attempt to set balance to {newValue}"
            }
        }
    }

    function deposit (amount: Number): Void
    {
        balance += amount;
    }

    function withdraw (amount: Number): Void
    {
        balance -= amount;
    }

    override function toString (): String
    {
        "balance = {balance}"
    }
}

def acct = Account { balance: 1000.0 }
println (acct);
println (acct.getJFXClass ());
acct.deposit (1000.0);
println (acct);
acct.withdraw (100.0);
println (acct);
acct.withdraw (1901.0)

Listing 6 invokes JFXObject's public getJFXClass(): javafx.reflect.FXClassType function to return the acct object's class type, which subsequently outputs. This listing also subclasses JFXException to provide a suitably named exception class, and declaratively initializes an instance of this class prior to throwing the exception when an attempt is made to withdraw too much money.

This script generates the following output:

balance = 1000.0
class langdemo.Main.Account
balance = 2000.0
balance = 1900.0
Exception in trigger:
langdemo.Main$NegativeBalanceException: attempt to set balance to -1.0
        at langdemo.Main$Account$1.onChange(Main.fx:21)

Native menus

JFXtras provides, via org.jfxtras.menu's six classes, declarative access to AWT-based menus for integrating pop-up menus into your applications. You can use the classes below to create pop-up menus for use with the system tray, to create and show context-sensitive pop-up menus for different nodes, and so on:

  • NativePopupMenu provides a JavaFX wrapper for the java.awt.PopupMenu class. This class provides a parent attribute (of type java.awt.Component) that identifies the pop-up menu's AWT component. Invoke this class's public show(origin: java.awt.Component, x: Integer, y: Integer): Void function to show the pop-up menu at the specified coordinates relative to the origin component.
  • NativeMenu provides a JavaFX wrapper for the java.awt.Menu class and serves as NativePopupMenu's parent. This class provides an items attribute (of type NativeMenuEntry[]) that identifies the menu entries associated with this menu.
  • NativeMenuEntry serves as the base class for NativeMenuItem.
  • NativeMenuItem provides a JavaFX wrapper for the java.awt.MenuItem class and serves as the parent of the NativeMenu class (and also the NativeCheckboxMenuItem class). This class provides an action attribute (of type function(): Void) for specifying the function that's executed when this menu item is clicked. The class also provides a text attribute (of type String) that specifies the menu item's displayed text.
  • NativeCheckboxMenuItem provides a JavaFX wrapper for the java.awt.CheckboxMenuItem class. This class provides a selected attribute (of type Boolean) that specifies this menu item's checked state.

I've created a TextSrch4 NetBeans project (as an extension to the previous TextSrch3 project) that demonstrates NativePopupMenu and NativeMenuItem. Listing 7 excerpts this project's createMenu() function.

Listing 7. The createMenu() function from a TextSrch4 project's Main.fx file

function createMenu (parent: Component): NativePopupMenu
{
    NativePopupMenu
    {
        items:
        [
            NativeMenuItem
            {
                action: function (): Void
                {
                    Help {}.showHelp (stageRef)
                }

                text: "Help..."
            }
        ]

        parent: parent
    }
}

Listing 7's createMenu() function constructs and returns a pop-up menu with a single Help... menu item (as an alternative to a Help button) for activating the help dialog box. Whenever this menu item is clicked, its action function instantiates the Help class and invokes that class's showHelp() function to present the help dialog box shown previously in Figure 5.

This menu is created via def menu = createMenu (stageRef.getWindow ());, which appears in the script after the stage's declaration. This positioning is necessary to ensure that the stage (an instance of JFXStage) is visible so that its public getWindow(): java.awt.Window function can return a non-null AWT parent for the native pop-up menu.

To ensure that the pop-up menu can be triggered from anywhere on the scene, I inserted a transparent rectangle behind all other scene content. As Listing 8 shows, I then assigned functions to this Rectangle object's onMousePressed and onMouseReleased attributes to show the pop-up menu whenever they detect a menu-trigger.

Listing 8. The menu-trigger detection and display logic from a TextSrch4 project's Main.fx file

onMousePressed: function (me: MouseEvent): Void
{
    if (me.popupTrigger)
        menu.show (stageRef.getWindow (), me.x, me.y)
}

onMouseReleased: function (me: MouseEvent): Void
{
    if (me.popupTrigger)
        menu.show (stageRef.getWindow (), me.x, me.y)
}

Because pop-up menus are triggered differently on different platforms, isPopupTrigger is checked in both of the functions assigned to onMousePressed and mouseReleased, to ensure proper cross-platform behavior.

Figure 6 reveals the pop-up menu for activating the help dialog box.

Right-click (on Windows platforms, if the left and right mouse buttons haven't been reversed) to activate the pop-up menu.
Figure 6. Right-click (on Windows platforms, if the left and right mouse buttons haven't been reversed) to activate the pop-up menu. (Click to enlarge.)

More advanced menu example

Stephen Chin demonstrates the native menu classes in his  DockDialog.fx source code, which is part of WidgetFX.

Unit testing

Recognizing the importance of unit testing, JFXtras provides a declarative unit-testing framework that lets you write JavaFX tests in JavaFX. This testing framework is modeled after best-of-class, behavior-driven, and fluent testing patterns to make tests easier to read and maintain. It consists of the following classes, located in the org.jfxtras.test package:

  • Test is the base class for creating declarative, fluent, behavior-driven tests.
  • Assumption describes a condition that must be met before a test can be run.
  • Expectation specifies a declarative assertion that causes a test to fail if it's not true.
  • Expect provides several convenience functions (such as public equalTo(expected: java.lang.Object): Expectation) that return standard Expectations.
  • ExpectationException is thrown when an expectation is not met.
  • TestResults records the number of failed, passed, and skipped tests.

I've created a BasicTests NetBeans project that demonstrates this unit-testing framework in terms of its Test and Expect classes. Listing 9 presents this project's Main.fx script.

Listing 9. Main.fx for a BasicTests project

/*
 * Main.fx
 */

package basictests;

import org.jfxtras.test.Expect;
import org.jfxtras.test.Test;

Test
{
     say: "factorialGood should return 24 when passed 4"
     do: function () { factorialGood (4) }
     expect: Expect.equalTo (24)
}
.perform ();

println (" ");

Test
{
     say: "factorialBad should return 24 when passed 4"
     do: function () { factorialBad (4) }
     expect: Expect.equalTo (24)
}
.perform ();

function factorialGood (n: Integer): Integer
{
    if (n == 0) 1 else n*factorialGood (n-1)
}

function factorialBad (n: Integer): Integer
{
    if (n < 0) 1 else n*factorialBad (n-1)
}

Listing 9 tests two functions that return factorials. Each test is established by creating a Test object, assigning appropriate values to Test's say (of type String), do (of type function(): Object), and expect (of type Expectation[]) attributes, and invoking Test's public perform(): Test function to run the test. This function outputs test results after running the test.

The String assigned to say describes what the test should accomplish. The function assigned to do contains the code to be tested. It should return a value that can be checked by the expect clause, which consists of a sequence of Expectations; the test fails if any Expectation is not true. Any exceptions thrown from the function are counted towards a test failure unless defined via Test's expectException attribute.

This script generates the following output:

test: factorialGood should return 24 when passed 4.
Test Results: 1 passed, 0 failed, 0 skipped.
Test run was successful!

test: factorialBad should return 24 when passed 4.
TEST FAILURE:
Expected: equal to "24"
  Actual: 0
Test Results: 0 passed, 1 failed, 0 skipped.
TEST RUN FAILED WITH ERRORS.  See above for cause of failures.

Utilities classes

Finally, JFXtras provides a pair of utilities classes in its org.jfxtras.util package. The GeometryUtil class provides functions for converting between JavaFX and Java versions of the Point2D and Rectangle2D classes, whereas the SequenceUtil class provides the following sequence-oriented functions:

  • public characterSequence(start: java.lang.String, end: java.lang.String): <any>[] generates and returns a sequence of one-character strings, starting with the first character of the start string and ending with the first character of the end string.
  • public concat(seq: java.lang.String[]): java.lang.String concatenates all elements in a sequence of Strings into a single String, which is returned.
  • public fold(ident: Number, seq: Number[], func: com.sun.javafx.functions.Function2): Number performs what is known in functional languages as a left fold operation. This operation consists of passing ident's value and seq's first element to reduce function func, which combines these two inputs into a single output. This output value and seq's second element are passed to func, resulting in a new output value. This output value and seq's third value are passed to func, and so on until there are no elements left in seq. The fold() function returns the final output value. Check out Wikipedia's Fold (higher-order function) entry to learn more about this operation.
  • public fold(ident: Integer, seq: Integer[], func: com.sun.javafx.functions.Function2): Integer is equivalent to the former function, but is typed for Integers.
  • public fold(ident: java.lang.Object, seq: java.lang.Object[], func: com.sun.javafx.functions.Function2): java.lang.Object is equivalent to the former function, but is typed for java.lang.Objects.
  • public join(seq: java.lang.String[], delimiter: java.lang.String): java.lang.String joins all of seq's String elements into a single String, with the specified delimiter appearing between successive Strings. The resulting String is returned.
  • public sum(seq: Number[]): Number adds all elements of a Numbers sequence and returns the total.
  • public sum(seq: Integer[]): Integer adds all elements of an Integers sequence and returns the total.

Listing 1 presented an example of SequenceUtil's characterSequence() function. Listing 10 provides another example of this function, and also demonstrates most of the other functions.

Listing 10. Main.fx for a UtilDemo project

/*
 * Main.fx
 */

package utildemo;

import org.jfxtras.util.SequenceUtil;

def letters = SequenceUtil.characterSequence ("dog", "bone");
println (letters);
println (SequenceUtil.concat (letters));
println (SequenceUtil.join (letters, ", "));

def grades = [ 69, 23, 46, 58 ];
println ("Average = {SequenceUtil.sum (grades)/sizeof grades}");

// What is the sum of the series 1+1/2+1/4+1/8+1/16+... (forever)?

var numbers: Number[];
for (i in [0..100])
     insert 1.0/java.lang.Math.pow (2, i) into numbers;
println ("Sum = {SequenceUtil.sum (numbers)}");

// Use a fold function to calculate 5! (factorial).

println (SequenceUtil.fold (1, [2, 3, 4, 5], multiply));

function multiply (a: Integer, b: Integer): Integer
{
    a*b
}

This script generates the following output:

[ d, c, b ]
dcb
d, c, b
Average = 49
Sum = 2.0
120

In conclusion

JFXtras offers considerable value for your JavaFX projects; I wouldn't be surprised to see JFXtras features appearing in future releases of JavaFX. Now that you've finished this article's brief tour of JFXtras 0.2, I recommend digging deeper into its unit-testing framework and other features. And don't forget to check out JFXtras 0.3, which supports MiGLayout and is compatible with JavaFX 1.1!

Jeff Friesen is a freelance software developer and educator who specializes in Java technology. Check out his javajeff.mb.ca website to discover all of his published Java articles and more. His book Beginning Java SE 6 Platform: From Novice to Professional (Apress, October 2007) explores most of the new features introduced in Java SE 6

Learn more about this topic

  • Download the sample code for this article.
  • Visit the JFXtras project site for downloads and documentation.
  • JFXtras 0.2's custom shapes are based on the jSilhouette collection.
  • JFXtras creator Stephen Chin was one of the developers interviewed for "Client-side Java's evolutionary leap" (Jeff Friesen, JavaWorld, January 2009) a roundtable on the current state and rapid evolution of JavaFX and other client-side technologies.
  • Jump into JavaFX is a six-part series introducing JavaFX, from early experiments with the Preview SDK through JavaFX 1.0:
  • Unwrapping JavaFX 1.0 is a JavaWorld podcast with Sun's Param Singh and John Burkey on the eve of the release of JavaFX 1.0. Also read about the release of JavaFX Mobile in February 2009.
  • Read about the BSD License
  • "JavaFX Async operations" (James Clark, Clarkeman's Weblog, December 2008) discusses subclassing JavaFX's AbstractAsyncOperation class.
  • Read Wikipedia's entries on Reuleaux triangle and Lauburu, two of the shapes supported in JFXtras.
  • Stephen Chin demonstrates the JFXtras native menu classes in DockDialog.fx, part of WidgetFX.
  • Learn more about fold higher-order function in Wikipedia.

More from JavaWorld

Join the discussion
Be the first to comment on this article. Our Commenting Policies