Back Forum Reply New

Open source Java projects: JFXtras part 1

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.

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 scriptBefore 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.
  1. /*
  2. * Main.fx
  3. */

  4. package charseq;

  5. import org.jfxtras.util.*;

  6. def digits = SequenceUtil.characterSequence ("0", "9");
  7. println (digits) // Output: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Copy Code
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:
  • Use the New Project wizard to introduce a CharSeq project with charseq.Main as the main file.
  • Replace the generated skeletal Main.fx with Listing 1.
  • Right-click CharSeq in the Projects window and select Properties from the pop-up menu.
  • Select the Libraries category on the resulting Project Properties dialog box.
  • Click the Add JAR/Folder button.
  • Locate the appropriate directory containing JFXtras-0.2.jar and add this JAR to the Libraries list.
  • Dismiss the Project Properties dialog.
  • 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:
  • Create a charseq subdirectory of the current directory.
  • Copy Listing 1 to a Main.fx file and store this file in charseq.
  • Copy JFXtras-0.2.jar to the current directory.
  • Compile Main.fx via javafxc -cp JFXtras-0.2.jar charseq\Main.fx.
  • Run the script via javafx -cp JFXtras-0.2.jar;. charseq.Main.
A shape-oriented scriptWhen 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.
  1. /*
  2. * Main.fx
  3. */

  4. package asterisk;

  5. import javafx.scene.Scene;
  6. import javafx.scene.paint.Color;
  7. import javafx.stage.Stage;
  8. import org.jfxtras.scene.shape.Asterisk;

  9. Stage
  10. {
  11.     title: "Centered Asterisk"
  12.     width: 250
  13.     height: 250

  14.     var sceneRef: Scene
  15.     scene: sceneRef = Scene
  16.     {
  17.         content: Asterisk
  18.         {
  19.             centerX: bind sceneRef.width/2.0
  20.             centerY: bind sceneRef.height/2.0
  21.             radius: 80
  22.             width: 20
  23.             beams: 5
  24.             roundness: 0.5
  25.             fill: Color.RED
  26.         }
  27.     }
  28. }
Copy Code
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.  
  • Use the New Project wizard to introduce an Asterisk project with asterisk.Main as the main file.
  • Replace the generated skeletal Main.fx with Listing 2.
  • Right-click Asterisk in the Projects window and select Properties from the pop-up menu.
  • Select the Libraries category on the resulting Project Properties dialog box.
  • Click the Add JAR/Folder button.
  • Locate the appropriate directory containing JFXtras-0.2.jar and add this JAR to the Libraries list.
  • Click the Add JAR/Folder button a second time.
  • Change to the lib subdirectory containing jsilhouette-geom-0.3.jar and add this JAR to the Libraries list.
  • Click the Add JAR/Folder button a third time.
  • Add jsilhouette-scene-0.3.jar to the Libraries list.
  • Dismiss the Project Properties dialog.
  • Click the green triangle button on the main toolbar, or press F6 to run the script.
Follow the steps below to build and run this script using the command-line version of JavaFX:
  • Create an asterisk subdirectory of the current directory.
  • Copy Listing 2 to a Main.fx file and store this file in asterisk.
  • Copy JFXtras-0.2.jar to the current directory.
  • Create lib as a subdirectory of the current directory.
  • Copy jsilhouette-geom-0.3.jar and jsilhouette-scene-0.3.jar to lib.
  • Compile Main.fx via javafxc -cp JFXtras-0.2.jar asterisk\Main.fx.
  • Run the script via javafx -cp JFXtras-0.2.jar;. asterisk.Main.
Exploring JFXtrasJFXtras 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 supportJavaFX 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.

  1. /*
  2. * Main.fx
  3. */

  4. package textsrch;

  5. import java.io.File;
  6. import java.io.FileReader;
  7. import java.io.IOException;

  8. import java.lang.Thread;

  9. import java.nio.CharBuffer;

  10. import java.util.regex.Pattern;

  11. import javafx.ext.swing.SwingButton;
  12. import javafx.ext.swing.SwingLabel;
  13. import javafx.ext.swing.SwingList;
  14. import javafx.ext.swing.SwingListItem;
  15. import javafx.ext.swing.SwingScrollPane;
  16. import javafx.ext.swing.SwingTextField;

  17. import javafx.scene.Cursor;
  18. import javafx.scene.Scene;

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

  21. import javafx.scene.paint.Color;

  22. import javafx.stage.Stage;

  23. import org.jfxtras.async.JFXWorker;

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

  30.     var items: SwingListItem [];

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

  36.     var searching: Boolean;

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

  40.     var worker: JFXWorker;

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

  51.     var counter: Integer;

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

  54.     function find (text: String): Void
  55.     {
  56.         searching = true;
  57.         worker = JFXWorker
  58.         {
  59.             inBackground: function ()
  60.             {
  61.                 // This code executes on a background thread.

  62.                 delete items;
  63.                 counter = 0;

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

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

  71.                 return null
  72.             }

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

  76.                 searching = false;
  77.             }
  78.         }
  79.     }

  80.     function kill (): Void
  81.     {
  82.         worker.cancel ()
  83.     }

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

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

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

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

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

  97.         var fr: FileReader;

  98.         try
  99.         {
  100.             fr = new FileReader (filename);

  101.             // Prepare for initial read.

  102.             buffer.clear ();

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

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

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

  114.                 buffer.flip ();

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

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

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

  125.                 buffer.limit (buffer.capacity ());
  126.                 buffer.position (buffer.capacity()-srchtext.length ()+1);
  127.                 buffer.compact()
  128.             }
  129.         }
  130.         catch (e: IOException)
  131.         {
  132.         }
  133.         finally
  134.         {
  135.             if (fr != null)
  136.                 try { fr.close () } catch (e: IOException) {}
  137.         }

  138.         return false
  139.     }

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

  147.             if (counter == LIMIT)
  148.                 return;

  149.             if (files [i].isDirectory ())
  150.                 findall (files [i], srchtext)
  151.             else
  152.             if (find (files [i].getPath (), srchtext))
  153.             {
  154.                 insert SwingListItem { text: files [i].getPath () }
  155.                     into items;
  156.                 counter++
  157.             }
  158.         }
  159.         delete files
  160.     }
  161. }

  162. Stage
  163. {
  164.     def model = FindModel {}

  165.     title: "Text Search"

  166.     width: 400
  167.     height: 300
  168.     resizable: false

  169.     var sceneRef: Scene
  170.     scene: sceneRef = Scene
  171.     {
  172.         fill: Color.GOLD

  173.         var vboxRef: VBox
  174.         content: vboxRef = VBox
  175.         {
  176.             var input: SwingTextField
  177.             content:
  178.             [
  179.                 HBox
  180.                 {
  181.                     content:
  182.                     [
  183.                         SwingLabel
  184.                         {
  185.                             text: "Enter search text"
  186.                         }
  187.                         input = SwingTextField
  188.                         {
  189.                             columns: 23
  190.                         }
  191.                     ]

  192.                     spacing: 10
  193.                 }
  194.                 HBox
  195.                 {
  196.                     content:
  197.                     [
  198.                         SwingButton
  199.                         {
  200.                             action: function (): Void
  201.                             {
  202.                                 model.find (input.text)
  203.                             }

  204.                             enabled: bind not model.searching

  205.                             text: "Search"
  206.                         }
  207.                         SwingButton
  208.                         {
  209.                             action: function (): Void
  210.                             {
  211.                                 model.searching = false;
  212.                                 model.kill ()
  213.                             }

  214.                             enabled: bind model.searching

  215.                             text: "Stop"
  216.                         }
  217.                     ]

  218.                     spacing: 10
  219.                 }
  220.                 SwingLabel
  221.                 {
  222.                     text: "Results"
  223.                 }
  224.                 SwingScrollPane
  225.                 {
  226.                     cursor: bind if (model.searching)
  227.                                      Cursor.WAIT
  228.                                  else
  229.                                      Cursor.DEFAULT

  230.                     view: SwingList
  231.                     {
  232.                         items: bind if (model.searching)
  233.                                         [
  234.                                             SwingListItem
  235.                                             {
  236.                                                 text: "Scanning files..."
  237.                                             }
  238.                                         ]
  239.                                     else
  240.                                         model.items
  241.                     }

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

  245.             spacing: 10

  246.             translateX: bind (sceneRef.width-vboxRef.boundsInLocal.width)/2
  247.             translateY: bind (sceneRef.height-vboxRef.boundsInLocal.height)/2
  248.         }
  249.     }
  250. }
Copy Code
Back Forum