Open source Java projects: JFXtras

Utilities and add-ons for the JavaFX Script programming language

1 2 3 4 Page 2
Page 2 of 4

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.

1 2 3 4 Page 2
Page 2 of 4