Practical JavaFX 2, Part 3: Refactoring Swing JPad's advanced UI features

Migrate Swing JPad's dialogs, clipboard, and drag-and-drop to JavaFX

The refactoring of Swing JPad is well underway, with basic features such as the content pane, menu system, and event handling successfully migrated to JavaFX 2. Now find out how more advanced features such as dialogs and drag-and-drop support map from Swing to JavaFX. Part 3 concludes with Jeff's response to the question of whether JavaFX 2 is production ready, and an opportunity to share your own perspective.

The second part of this article focused on the essential features of a notepad application converted from Swing to JavaFX 2. You've seen how Swing JPad's content pane, menu system, and event handling architecture map to JPadFX. Now, we'll conclude with a look at some of the more advanced features of a notepad application: dialog boxes, a clipboard, and drag-and-drop manipulation.

Dialog boxes

Like the original Swing notepad application, JPadFX presents dialog boxes for selecting a file from the filesystem, showing About information, displaying error messages, and prompting the user for yes/no responses. In Listing 1, JPadFX's start() method instantiates javafx.stage.FileChooser to handle the file-selection task.

Listing 1. Creating and configuring a javafx.stage.FileChooser

fc = new FileChooser();
fc.setInitialDirectory(new File("."));
FileChooser.ExtensionFilter ef1;
ef1 = new FileChooser.ExtensionFilter("TXT documents (*.txt)", "*.txt");
FileChooser.ExtensionFilter ef2;
ef2 = new FileChooser.ExtensionFilter("All Files", "*.*");
fc.getExtensionFilters().addAll(ef1, ef2);

After instantiating FileChooser, start() sets its initial directory to the current one. Because FileChooser offers only a no-argument constructor, the start() method uses fc.setInitialDirectory(new File(".")); instead of a constructor to set the directory.

Continuing, start() creates a pair of extension-based file filters by instantiating FileChooser's nested ExtensionFilter class. It registers them with the file chooser by returning an observable list of extension filters and adding the pair of filters to this list.

JPadFX will use the file chooser to display an Open or Save dialog box by calling FileChooser's void showOpenDialog(Window ownerWindow) or void showSaveDialog(Window ownerWindow) method. Listing 2 demonstrates the former method in the context of JPadFX's doOpenFile(file) method.

Listing 2. Selecting a file to open via showOpenDialog()

if (file == null)
   file = fc.showOpenDialog(stage);
   if (file == null)
      return;
fc.setInitialDirectory(file.getParentFile());

The fc.setInitialDirectory(file.getParentFile()); method ensures that the next file-chooser activation sets the initial directory to the current one.

Unlike JPad, which relies on Swing's JOptionPane class for its About, Alert, and AreYouSure dialog boxes, JPadFX has no JOptionPane equivalent, because JavaFX has yet to provide one. So we'll manually create the About, Alert, and AreYouSure dialog boxes for JPadFX. As you will discover, each dialog box class extends JavaFX's Stage class, which means that a dialog box is nothing more than a secondary stage (as opposed to the primary stage that is passed to the application's start() method).

The About dialog

JPad's About dialog box, which provides information about JPadFX in image and text form, is implemented by the About class. About's source code is shown in Listing 3.

Listing 3. The About class declares only a constructor

public class About extends Stage
{
   public About(Stage owner)
   {
      initOwner(owner);
      initStyle(StageStyle.UNDECORATED);
      initModality(Modality.APPLICATION_MODAL);
      Group root = new Group();
      Image img = new Image(getClass().getResourceAsStream("icon.png"));
      ImageView iv = new ImageView(img);
      double width = iv.layoutBoundsProperty().getValue().getWidth();
      double height = iv.layoutBoundsProperty().getValue().getHeight();
      iv.setX(10.0);
      iv.setY((180.0-height)/2.0);
      root.getChildren().add(iv);
      Text msg1 = new Text("JPadFX 1.0");
      msg1.setFill(Color.WHITE);
      msg1.setFont(new Font("Arial", 20.0));
      msg1.setX(iv.getX()+width);
      msg1.setY(iv.getY()+height/2.0);
      root.getChildren().add(msg1);
      Text msg2 = new Text("by Jeff Friesen");
      msg2.setFill(Color.WHITE);
      msg2.setFont(new Font("Arial", 12.0));
      msg2.setX(msg1.getX());
      msg2.setY(msg1.getY()+20.0);
      root.getChildren().add(msg2);
      Reflection r = new Reflection();
      r.setFraction(1.0);
      root.setEffect(r);
      Scene scene = new Scene(root, 200.0, 180.0, Color.BLACK);
      EventHandler<MouseEvent> ehme;
      ehme = new EventHandler<MouseEvent>()
      {
         @Override
         public void handle(MouseEvent me)
         {
            close();
         }
      };
      scene.setOnMousePressed(ehme);
      setScene(scene);
      setX(owner.getX()+Math.abs(owner.getWidth()-scene.getWidth())/2.0);
      setY(owner.getY()+Math.abs(owner.getHeight()-scene.getHeight())/2.0);
   }
}

In Listing 3, About's constructor takes a Stage argument, which identifies the stage that owns the About dialog box. The About dialog box is owned by the primary stage, so JPadFX passes the primary stage instance to About's constructor via the expression newAbout(stage).show();.

Before the About box is displayed (by calling Stage's void show() method), JavaFX must be informed of this stage's owner. We do this by invoking Stage's void initOwner(Window owner) method with the argument passed to About's constructor.

Next we declare the stage's style and modality. First, we use a void initStyle(StageStyle style) method to inform JavaFX of the desired style for the stage. Calling this method with the javafx.stage.StageStyle enum's UNDECORATED constant tells JavaFX that the stage must not have a border or other decorations.

Next, Stage declares a void initModality(Modality modality) method, which informs JavaFX of the desired modality for the stage. This method is called with the javafx.stage.Modality enum's APPLICATION_MODAL constant, telling JavaFX not to deliver events to any other application window. Both the void initStyle(StageStyle style) and void initModality(Modality modality) methods must be called before the stage is displayed.

Nodes

The constructor next instantiates the javafx.scene.Group class, which is a container node for subsequently created nodes. The first of these nodes is a javafx.scene.image.ImageView instance, which manages a javafx.scene.image.Image instance that describes an image.

Before ImageView is instantiated, Image is instantiated and told to load contents of image.png. Because this PNG file will be stored in a JAR file, it's accessed via the expression getClass().getResourceAsStream("icon.png").

Next, the ImageView node's width and height are obtained to help position the scene. This node is positioned 10 pixels from the left edge of the dialog box and centered vertically. It is then added to the group's observable list. (See Part 2 for a discussion about observable lists.)

The javafx.scene.text package's Text and Font classes are instantiated to describe two text nodes that are displayed with the image. The text is colored white to contrast with the scene's black background. The font is assigned and these nodes are added to the group's observable list.

JavaFX lets you add one or more effects to a node. For example, javafx.scene.effect.Reflection is used to reflect a node. About's constructor instantiates Reflection and calls its void setFraction(double value) method, specifying that all of the image must be visible. The constructor also calls javafx.scene.Node's void setEffect(Effect effect) method on the group node to reflect this node's contents.

The scene is now created and an event handler is registered to respond to mouse-pressed events. When the mouse is pressed over the About stage, Stage's void close() method will be called to close the stage.

Tip: Centering the stage

After assigning the scene to the stage, there's just one thing left to do. Centering the About stage over the primary stage will add a touch of professionalism to our UI. We do this by calling Stage's setX() and setY() property setter methods with the results of our centering calculations, as shown in the final two lines of Listing 3.

Figure 1 shows the About dialog box.

Figure 1. Click anywhere on the About dialog box to close it

The Alert dialog

The Alert dialog box, which displays messages resulting from I/O exceptions, is implemented by the Alert class. Listing 4 reveals Alert's source code.

Listing 4. The Alert class declares only a constructor

public class Alert extends Stage
{
   public Alert(Stage owner, String msg)
   {
      setTitle("Alert");
      initOwner(owner);
      initStyle(StageStyle.UTILITY);
      initModality(Modality.APPLICATION_MODAL);
      Button btnOk = new Button("Ok");
      btnOk.setOnAction(new EventHandler<ActionEvent>()
                        {
                           @Override
                           public void handle(ActionEvent ae)
                           {
                              close();
                           }
                        });
      final Scene scene = new Scene(VBoxBuilder.create()
                                               .children(new Label(msg),
                                                         btnOk)
                                               .alignment(Pos.CENTER)
                                               .padding(new Insets(10))
                                               .spacing(20.0)
                                               .build());
      setScene(scene);
      sizeToScene();
      setResizable(false);
      show(); hide(); // needed to get proper value from scene.getWidth() and
                      // scene.getHeight()
      setX(owner.getX()+Math.abs(owner.getWidth()-scene.getWidth())/2.0);
      setY(owner.getY()+Math.abs(owner.getHeight()-scene.getHeight())/2.0);
   }
}

Alert's constructor takes a Stage argument identifying the stage that owns the Alert dialog box. It also takes a String argument that describes the message to be shown. JPadFX passes the primary stage instance and a suitable message to this constructor.

For example, when an I/O error occurs, JPadFX executes new Alert(stage, "I/O error: "+ioe.getMessage()).show(); to notify the user via a dialog box. The primary stage instance is passed as the first argument; an exception message is passed as the second argument.

The constructor creates a scene consisting of the message and an OK button (whose event handler closes the stage). The javafx.scene.layout.VBoxBuilder class creates a single-column scene consisting of a label containing the message and a button. The scene is centered in the dialog box.

VBoxBuilder is an example of a builder class that lets you create a container by conveniently chaining method calls together. create() instantiates this class. Remaining method calls specify the builder's children, alignment, padding, and spacing. The final build() call creates and returns a javafx.scene.layout.VBox instance, which is a layout container that lays out its children in a single vertical column.

Alert() uses the Scene(Parent root) constructor to specify the scene graph's root node. A width and height are not specified because we want the scene's size to be automatically calculated based on the preferred size of its content.

Window's void sizeToScene() method is called to set the stage window's height/width to match the scene's height/width. Stage's void setResizable(boolean value) method is called with a false argument to prevent the stage from being resized.

1 2 Page 1
Page 1 of 2