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

feature
Mar 6, 201219 mins

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.

Practical JavaFX 2: Read all three parts

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.

Tip: Getting the scene’s width and height

The centering calculations for the Alert dialog box rely on scene.getWidth() and scene.getHeight(), which return the scene’s width and height. But these method calls return 0.0, because explicit width and height values were not specified for the scene. To address this problem, we have to briefly show and then hide the dialog box and its scene, thus prompting scene.getWidth() and scene.getHeight() to return the property values.

Figure 2 shows the Alert dialog box.

Figure 2. Click OK or X on the title bar to close the Alert dialog box

The AreYouSure dialog box, which display a message and prompts the user to press a Yes or No button, is implemented by the AreYouSure class. Listing 5 reveals AreYouSure‘s source code.

Listing 5. The AreYouSure class declares a private field, a constructor, and a pair of methods

public class AreYouSure extends Stage
{
   private EventHandler<ActionEvent> ehaeYes, ehaeNo;
   public AreYouSure(Stage owner, String msg)
   {
      setTitle("Are You Sure?");
      initOwner(owner);
      initStyle(StageStyle.UTILITY);
      initModality(Modality.APPLICATION_MODAL);
      Button btnYes = new Button("Yes");
      btnYes.setOnAction(new EventHandler<ActionEvent>()
                         {
                            @Override
                            public void handle(ActionEvent ae)
                            {
                               if (ehaeYes != null)
                                  ehaeYes.handle(ae);
                               close();
                            }
                         });
      Button btnNo = new Button("No");
      btnNo.setOnAction(new EventHandler<ActionEvent>()
                        {
                           @Override
                           public void handle(ActionEvent ae)
                           {
                              if (ehaeNo != null)
                                 ehaeNo.handle(ae);
                              close();
                           }
                        });
      btnNo.setPrefWidth(60.0);
      btnYes.setPrefWidth(60.0);
      HBoxBuilder hbb;
      hbb = HBoxBuilder.create()
                       .children(btnYes, btnNo)
                       .spacing(10);
      VBoxBuilder vbb;
      vbb = VBoxBuilder.create()
                       .children(new Label(msg), hbb.build())
                       .padding(new Insets(10.0))
                       .spacing(10.0)
                       .alignment(Pos.CENTER);
      Scene scene = new Scene(vbb.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);
   }
   public void setOnYes(EventHandler<ActionEvent> ehae)
   {
      ehaeYes = ehae;
   }
   public void setOnNo(EventHandler<ActionEvent> ehae)
   {
      ehaeNo = ehae;
   }
}

AreYouSure‘s constructor creates a message box that displays a message centered over a pair of Yes and No buttons. The constructor is largely similar to Alert‘s constructor, but with three essential differences:

  1. The setPrefWidth(60.0) call on each button sets the button’s preferred width to 60.0. This ensures that both buttons have the same width, which improves the dialog box’s aesthetics.
  2. The javafx.scene.layout.HBoxBuilder class is used to create a javax.scene.layout.HBox instance that horizontally lays out the Yes and No buttons. This container is then added to the VBox instance returned from VBoxBuilder.build() to ensure a nice-looking UI.
  3. When Swing shows a modal dialog box, the event-dispatch thread doesn’t execute subsequent code until the dialog box is dismissed. Although JavaFX follows this behavior where FileChooser is concerned (FileChooser delegates to the underlying platform’s native file chooser), it permits the JavaFX application thread to continue executing code that follows a call to show a modal stage. We have to proceed with caution when handling the modal dialog box, lest we face unintended consequences.

Tip: Handling modal dialogs

If a user attempted to close the primary stage immediately after showing a modal child stage then the child stage would never be displayed, because the JavaFX thread would execute the close code immediately after executing the code that shows the modal child stage. To address this behavior, we can introduce a pair of fields and methods into AreYouSure that let the application assign separate event handlers to the Yes and No buttons. When the user presses Yes, the Yes button’s event handler executes. Similarly, the No button’s event handler executes when the user presses No. This way, no UI code need follow the call to show the AreYouSure dialog box. The appropriate code will execute via the handler assigned to Yes or No, as shown in Listing 6.

Listing 6. Registering separate event handlers for AreYouSure

EventHandler<ActionEvent> ehae;
ehae = new EventHandler<ActionEvent>()
{
   @Override
   public void handle(ActionEvent ae)
   {
      if (fDirty)
      {
         AreYouSure ays = new AreYouSure(stage, SAVE_CHNGS);
         EventHandler<ActionEvent> ehae1;
         ehae1 = new EventHandler<ActionEvent>()
         {
            @Override
            public void handle(ActionEvent ae)
            {
               if (doSave())
                  doNew();
            }
         };
         ays.setOnYes(ehae1);
         EventHandler<ActionEvent> ehae2;
         ehae2 = new EventHandler<ActionEvent>()
         {
            @Override
            public void handle(ActionEvent ae)
            {
               doNew();
            }
         };
         ays.setOnNo(ehae2);
         ays.show();
         // There should be no UI code here.
      }
      else
         doNew();
   }
};
miNew.setOnAction(ehae);

Figure 3 shows the AreYouSure dialog box.

The system clipboard

JPadFX supports system clipboard access via cut, copy, and paste operations. Much of this support is handled automatically by TextArea, which accesses the system clipboard and performs a cut, copy, or paste operation when it detects a specific key combination, such as Ctrl+C.

TextArea also inherits methods from its javafx.scene.control.TextInputControl superclass to programmatically access the system clipboard; void copy() is one example. Action-event handlers for JPadFX Edit menu items Cut, Copy, and Paste execute appropriate methods (such as ta.copy();) when an item is selected.

JPadFX also needs to directly access the system clipboard so that it can determine when text is available for pasting. When text isn’t available, Edit’s Paste item must be disabled; otherwise, it’s enabled.

The start() method executes the code to access the system clipboard and save its reference in a javafx.scene.input.Clipboard field variable:

cb = Clipboard.getSystemClipboard();

Before this menu is shown, Edit’s event handler executes. Clipboard‘s hasString() method returns true when the system clipboard contains textual data:

miPaste.setDisable(!cb.hasString());

Each time the Edit menu is selected (and before the menu is shown), Clipboard‘s boolean hasString() method is executed on the system clipboard to determine whether the clipboard contains text. If the system clipboard contains text, true returns and the Paste menu item is enabled. If there is no text on the system clipboard then Paste is disabled. (Compare this code to Listing 13 in Part 1, which uses Swing’s setEnabled() method to enable/disable a menu item. JavaFX provides a setDisable() method for this purpose.)

Drag and drop in JavaFX

JPadFX lets you drag files to and drop them on a given text area. Although multiple files can be dragged to and dropped on this control, only the first of these files is opened and its content displayed; the other files are ignored. Listing 7 shows how JPadFX supports drag and drop functionality.

Listing 7. JPadFX responds to drag events

EventHandler<DragEvent> ehde;
ehde = new EventHandler<DragEvent>()
{
   @Override
   public void handle(DragEvent de)
   {
      if (de.getDragboard().hasFiles())
         de.acceptTransferModes(TransferMode.COPY);
      de.consume();
   }
};
ta.setOnDragOver(ehde);
ehde = new EventHandler<DragEvent>()
{
   @Override
   public void handle(DragEvent de)
   {
      final Dragboard db = de.getDragboard();
      boolean success = false;
      if (db.hasFiles())
      {
         if (fDirty)
         {
            AreYouSure ays = new AreYouSure(stage, SAVE_CHNGS);
            EventHandler<ActionEvent> ehae1;
            ehae1 = new EventHandler<ActionEvent>()
            {
               @Override
               public void handle(ActionEvent ae)
               {
                  if (doSave())
                     doOpen(db.getFiles().get(0));
               }
            };
            ays.setOnYes(ehae1);
            EventHandler<ActionEvent> ehae2;
            ehae2 = new EventHandler<ActionEvent>()
            {
               @Override
               public void handle(ActionEvent ae)
               {
                  doOpen(db.getFiles().get(0));
               }
            };
            ays.setOnNo(ehae2);
            ays.show();
         }
         else
            doOpen(db.getFiles().get(0));
         success = true;
      }
      de.setDropCompleted(success);
      de.consume();
   }
};
ta.setOnDragDropped(ehde);

In Listing 7 we first create a javafx.scene.input.DragEvent-parameterized EventHandler instance that responds to “drag over” drag events. It registers this instance with the TextArea by invoking void setOnDragOver(EventHandler<? super DragEvent> value).

The event handler first obtains the javafx.scene.input.Dragboard instance associated with its DragEvent argument by calling DragEvent‘s Dragboard getDragboard() method. (Dragboard subclasses Clipboard, making dragboards drag-and-drop-specific clipboards.) It then invokes Dragboard‘s inherited boolean hasFiles() method to determine if a list of files have been dragged to the textarea.

If files have been dragged, DragEvent‘s void acceptTransferModes(TransferMode... transferModes) method is called to tell the drag-and-drop runtime that the text area lets files be dropped onto it, and signifies to the user that it’s interpreting the drag as a copy operation.

Next, we create a DragEvent-parameterized EventHandler instance that responds to “drag dropped” drag events. It registers this instance with the TextArea by invoking void setOnDragDropped(EventHandler<? super DragEvent> value).

The event-handler first obtains the Dragboard instance and then verifies that the dragboard contains a list of files. If the text area’s contents are dirty, the user is prompted to save changes, after which changes are saved. After invoking DragBoard‘s List<File> getFiles() method to retrieve the list of files, the handler opens the first file and then invokes DragEvent‘s void setDropCompleted(boolean isTransferDone) method to indicate transfer success or failure.

Conclusion to Part 3

Part 1’s introduction presented the following two questions for you to ponder while reading this article:

  1. Is JavaFX 2 a production-ready platform, as Oracle claims?
  2. Are there notable advantages to programming in JavaFX versus Swing?

For many applications, JavaFX 2 is already production-ready. However, this did not prove true for JPadFX, for the following reasons:

  • JavaFX 2 does not support a native undo capability. In order to include this feature in JPadFX, I would have had to rely on Swing, which I chose not to do. Similarly, JavaFX 2 does not support a native printing capability. Once again, I would need to rely on Swing for assistance to add print support to JPadFX. (I didn’t support printing with Swing JPad, but I could have done so.)
  • JavaFX 2 does’t support common dialog boxes for alerts, confirmations, and so on. I had to code my own AreYouSure and Alert dialogs.
  • JavaFX 2 is inconsistent with thread execution whenever a file-chooser modal dialog box or a stage-based modal dialog box is presented. Regarding the file chooser, further execution is suspended until the dialog box has been dismissed. Regarding a stage-based modal dialog box, execution continues while the dialog box is shown. This behavior necessitates the adoption of a cumbersome callback model to delay further UI execution until the dialog stage is closed.

These examples demonstrate that JavaFX 2 still lacks important features, and is occasionally cumbersome to use. That said, in answer to the second question, yes: I believe that there are notable advantages to programming in JavaFX. Consider these advantages:

  • JavaFX 2 provides a rich assortment of features, including animation and effects. Although I didn’t utilize animation for JPadFX, I relied on its reflection effect to create an interesting About dialog box. If I wanted to use reflection in Swing JPad’s About dialog box, I would have had to create a reflected image of the dialog box’s content, and then display this image. This technique would not be possible if I later needed to animate the About box’s content and reflect the animated content.
  • JavaFX 2 provides a simpler and more consistent event-handling framework than the Swing equivalent (which is based on the Abstract Window Toolkit’s event-handling framework).
  • JavaFX 2 provides a unified architecture for writing an application once and then deploying it to various contexts (e.g., embedded in a web browser or run via Java Web Start). Additional contexts will present themselves in the future (e.g., running the same application on a mobile device). To help the application determine its execution context, JavaFX 2 provides host services. As you learned, this feature helps an application adapt to its environment (e.g., JPadFX does not display an Exit menu when it discovers that it is running in a web page — Exit makes no sense in this context.

Just as I could have expanded the previous list, I could also add more items to this list. The point here is that JavaFX 2 does offer many compelling features that are worth investigating and using.

What do you think?

Is JavaFX 2 ready for production? Are you willing to live with the trade-offs in order to begin writing JavaFX applications now? Share your thoughts in the Comments section of this article.

I think that JavaFX 2’s advantages outweigh its current limitations, and I encourage you to consider migrating some of your Swing UIs to JavaFX. Keep in mind that you may have to fall back on Swing for features that are currently not supported by JavaFX 2. However, the need to do so will diminish as JavaFX’s feature set expands. Although JavaFX 2’s threading model results in more cumbersome code to properly deal with modal stage-based dialog boxes, perhaps this limitation will also be addressed in a future JavaFX release.

Jeff Friesen is a freelance tutor and software developer with an emphasis on Java and Android. In addition to writing Java and Android books for Apress, Jeff has written numerous articles on Java and other technologies for JavaWorld, informIT, Java.net, and DevSource. Jeff can be contacted via his website at TutorTutor.ca.