My Interview with TechTarget.com on JavaFX, Swing, and the NetBeans Rich Client Platform

I sat down with Jan Stafford of Tech Target at JavaOne in September and discussed our experiences with beginning to migrate to JavaFX from Swing while continuing to use the NetBeans Rich Client Platform (RCP) as the foundation for our application.

I discuss some of the pros of using JavaFX and the NetBeans RCP, such as having the ability to develop very polished looking components using effects such as drop shadows, reflections, and gradients  which may have been do-able in Swing, but were extremely painful and time consuming.  These effects however are baked into the JavaFX framework.

The largest negative at this point has been the learning curve with getting up to speed on Cascading Style Sheets.  Since much of the look and feel of the UI can be applied via CSS, it took a bit of work to learn how to use the style sheets to properly style the UI components.  However, the benefit to using CSS is that the work can be given to a UX/UI developer rather than a Java developer, thus truly decoupling the UI development from the business logic development.

A link to the interview on Tech Target’s website is below.

Interview with Tech Target: JavaFX beats Swing: Changing RIA development platform

techTargetInterview

twitter: @RobTerp

Drag and Drop With Custom Components in JavaFX

In my last post I showed how to create custom JavaFX components with Scene Builder and FXML and how to add the custom components to a new Scene.  The screenshot below displays the custom TableView component where each row represents data about a shipping container, with a custom “Add Plan” component on the right side of the UI to add a specified container to a plan.

The functionality of this test application will now be extended so that the user can select a row from the table and drag it over to the “Add Plan” component where they can drop it. When the drop event occurs the application will show a dialog displaying the value of the “VFC #” field from the row that was dragged from the container table.

screen2Additionally, when the user selects the row and begins dragging the cursor, we would like the application to display a translucent image of a shipping container (pictured to the left).  Once the cursor is over the Add Plan component, the component will be set with a drop shadow effect, in addition to changing the cursor to indicate to the user that the Add Plan component will accept the drop

The Drop Source (TestTable class)

The drop source for this example is the TestTable class from the previous post.  I will show sections from this class below, and then will include the entire class at the end of this post for reference.

Below is the TestTable class which contains a TableView with the shipping container information.  The first modification to this class was to add a parentProperty listener so that we know when this component is added to its parent node.  We will need the parent node to register for drag events that will occur when the user drags outside of the bounds of this container.  Once we have the parent we can register the drag listeners.

public class TestTable extends AnchorPane {

    @FXML
    private TableView myTableView;
    private ImageView dragImageView;
    private Parent root;

    public TestTable() {
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/com/lynden/planning/ui/TestTable.fxml"));
        fxmlLoader.setRoot(this);
        fxmlLoader.setController(this);

        try {
            fxmlLoader.load();

            parentProperty().addListener(new ChangeListener() {
                @Override
                public void changed(ObservableValue<? extends Parent> ov, Parent oldP, Parent newP) {
                    root = newP;
                    registerDragEvent();
                }
            });
        } catch (IOException exception) {
            throw new RuntimeException(exception);
        }
    }

The first step in the registerDragEvent() method is to read in the image of the shipping container that we want to display to the user and scale it down to 100×100.  Next, we can register the OnDragDetectedEvent for the TableView.  Once this listener detects a DragEvent it adds the ImageView to the Scene if it hasn’t already been added.  It then sets the opacity, sets MouseTransparent to true, which allows the mouse events to be passed to the components underneath it, and then displays the image.

The DragBoard is then fetched for the TableView, at which time we determine what row was selected, and put the value from the VFC# column as the content of the DragBoard.

 protected void registerDragEvent() {
        Image image = new Image(getClass().getResourceAsStream("/com/lynden/planning/ui/container2.png"));
        dragImageView = new ImageView(image);
        dragImageView.setFitHeight(100);
        dragImageView.setFitWidth(100);

        myTableView.setOnDragDetected(new EventHandler() {
            @Override
            public void handle(MouseEvent t) {

                AnchorPane anchorPane = (AnchorPane) myTableView.getScene().getRoot();

                if (!anchorPane.getChildren().contains(dragImageView)) {
                    anchorPane.getChildren().add(dragImageView);
                }

                dragImageView.setOpacity(0.5);
                dragImageView.toFront();
                dragImageView.setMouseTransparent(true);
                dragImageView.setVisible(true);
                dragImageView.relocate(
                        (int) (t.getSceneX() - dragImageView.getBoundsInLocal().getWidth() / 2),
                        (int) (t.getSceneY() - dragImageView.getBoundsInLocal().getHeight() / 2));

                Dragboard db = myTableView.startDragAndDrop(TransferMode.ANY);
                ClipboardContent content = new ClipboardContent();

                InboundBean inboundBean = (InboundBean) myTableView.getSelectionModel().getSelectedItem();
                content.putString(inboundBean.getVfcNumber());
                db.setContent(content);

                t.consume();
            }
        });

Next, we add the OnDragOver event handler to this component’s Parent.  This listener will re-position the shipping container image as the mouse is dragged around the UI.  If we just add the event handler to the TableView component, the position of the shipping container image will not be updated once the user drags the cursor outside of the TableView’s bounds.

//Add the drag over listener to this component's PARENT, so that the drag over events will be processed even
//after the cursor leaves the bounds of this component.
root.setOnDragOver(new EventHandler() {
    public void handle(DragEvent e) {
	Point2D localPoint = myTableView.getScene().getRoot().sceneToLocal(new Point2D(e.getSceneX(), e.getSceneY()));
	dragImageView.relocate(
		(int) (localPoint.getX() - dragImageView.getBoundsInLocal().getWidth() / 2),
		(int) (localPoint.getY() - dragImageView.getBoundsInLocal().getHeight() / 2));
	e.consume();
    }
});

The last step for the drop source is to add the OnDragDone event handler so that the shipping container image disappears when the user releases the mouse button.

myTableView.setOnDragDone(new EventHandler() {
    public void handle(DragEvent e) {
	dragImageView.setVisible(false);
	e.consume();
    }
});

The Drop Target (TestButton class)

The TestButton class is initialized in a similar fashion to the TestTable class.  The main difference is that the TestButton doesn’t care about its parent node, and we can just register for the drop events straight from the constructor without having to wait for the component’s parent to be set.

public class TestButton extends AnchorPane {

    @FXML
    private AnchorPane myTestButton;

    public TestButton() {
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/com/lynden/planning/ui/TestButton.fxml"));
        fxmlLoader.setRoot(this);
        fxmlLoader.setController(this);

        try {
            fxmlLoader.load();
        } catch (IOException exception) {
            throw new RuntimeException(exception);
        }

        registerDropEvent();
    }

The first thing we would like to do is to register for the OnDragOver event so that we can give feedback to the user that this component is a valid drop target.  The acceptTransferModes(TransferMode.ANY) will change the cursor to indicate the component accepts a drop event as well as adding a drop shadow to the component to provide a bit of highlighting

  
private void registerDropEvent() {

myTestButton.setOnDragOver(new EventHandler() {
    @Override
    public void handle(DragEvent t) {
	t.acceptTransferModes(TransferMode.ANY);
	DropShadow dropShadow = new DropShadow();
	dropShadow.setRadius(5.0);
	dropShadow.setOffsetX(3.0);
	dropShadow.setOffsetY(3.0);
	dropShadow.setColor(Color.color(0.4, 0.5, 0.5));
	myTestButton.setEffect(dropShadow);
	//Don't consume the event.  Let the layers below process the DragOver event as well so that the
	//translucent container image will follow the cursor.
	//t.consume();
    }
});

If the user exits the drop target area without releasing the mouse button the drop shadow effect is cleared from the component by setting the test button’s effect to null.

 
myTestButton.setOnDragExited(new EventHandler() {
    @Override
    public void handle(DragEvent t) {
	myTestButton.setEffect(null);
	t.consume();
    }
});

Finally, if the user does release the mouse button over the target, we will read the content from the Dragboard and display the results in a JOptionPane (used for simplicity sake).

myTestButton.setOnDragDropped(new EventHandler() {
    @Override
    public void handle(DragEvent t) {
	Dragboard db = t.getDragboard();
	final String string = db.getString();
	SwingUtilities.invokeLater(new Runnable() {
	    @Override
	    public void run() {
		JOptionPane.showMessageDialog(null, "Creating a New Plan For Container #: " + string);
	    }
	});
	t.setDropCompleted(true);
    }
});

Results

Below are some screenshots of the drag and drop in action.
Selecting a row and begin dragging displays translucent the shipping container image.

Dragging cursor over the Add Plan component causes the component to be highlighted with a DropShadow effect.
screen4

Releasing the mouse button over the Add Plan component will display a dialog containing the VFC# of the container row that was selected from the TableView.
screen5

For reference I have included both the TestTable and TestButton classes below.

 

twitter: @RobTerp

TestTable class

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.lynden.fx.test;

import com.lynden.fx.InboundBean;
import java.io.IOException;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Point2D;
import javafx.scene.Parent;
import javafx.scene.control.TableView;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.AnchorPane;

/**
 *
 * @author ROBT
 */
public class TestTable extends AnchorPane {

    @FXML
    private TableView myTableView;
    private ImageView dragImageView;
    private Parent root;

    public TestTable() {
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/com/lynden/planning/ui/TestTable.fxml"));
        fxmlLoader.setRoot(this);
        fxmlLoader.setController(this);

        try {
            fxmlLoader.load();

            parentProperty().addListener(new ChangeListener() {
                @Override
                public void changed(ObservableValue<? extends Parent> ov, Parent oldP, Parent newP) {
                    root = newP;
                    registerDragEvent();
                }
            });
        } catch (IOException exception) {
            throw new RuntimeException(exception);
        }
    }

    protected void registerDragEvent() {
        Image image = new Image(getClass().getResourceAsStream("/com/lynden/planning/ui/container2.png"));
        dragImageView = new ImageView(image);
        dragImageView.setFitHeight(100);
        dragImageView.setFitWidth(100);

        myTableView.setOnDragDetected(new EventHandler() {
            @Override
            public void handle(MouseEvent t) {

                AnchorPane anchorPane = (AnchorPane) myTableView.getScene().getRoot();

                if (!anchorPane.getChildren().contains(dragImageView)) {
                    anchorPane.getChildren().add(dragImageView);
                }

                dragImageView.setOpacity(0.5);
                dragImageView.toFront();
                dragImageView.setMouseTransparent(true);
                dragImageView.setVisible(true);
                dragImageView.relocate(
                        (int) (t.getSceneX() - dragImageView.getBoundsInLocal().getWidth() / 2),
                        (int) (t.getSceneY() - dragImageView.getBoundsInLocal().getHeight() / 2));

                Dragboard db = myTableView.startDragAndDrop(TransferMode.ANY);
                ClipboardContent content = new ClipboardContent();

                InboundBean inboundBean = (InboundBean) myTableView.getSelectionModel().getSelectedItem();
                content.putString(inboundBean.getVfcNumber());
                db.setContent(content);

                t.consume();
            }
        });

        //Add the drag over listener to this component's PARENT, so that the drag over events will be processed even
        //after the cursor leaves the bounds of this component.
        root.setOnDragOver(new EventHandler() {
            public void handle(DragEvent e) {
                Point2D localPoint = myTableView.getScene().getRoot().sceneToLocal(new Point2D(e.getSceneX(), e.getSceneY()));
                dragImageView.relocate(
                        (int) (localPoint.getX() - dragImageView.getBoundsInLocal().getWidth() / 2),
                        (int) (localPoint.getY() - dragImageView.getBoundsInLocal().getHeight() / 2));
                e.consume();
            }
        });

        myTableView.setOnDragDone(new EventHandler() {
            public void handle(DragEvent e) {
                dragImageView.setVisible(false);
                e.consume();
            }
        });
    }
}

TestButton Class

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.lynden.fx.test;

import java.io.IOException;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.effect.DropShadow;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;

/**
 *
 * @author ROBT
 */
public class TestButton extends AnchorPane {

    @FXML
    private AnchorPane myTestButton;

    public TestButton() {
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/com/lynden/planning/ui/TestButton.fxml"));
        fxmlLoader.setRoot(this);
        fxmlLoader.setController(this);

        try {
            fxmlLoader.load();
        } catch (IOException exception) {
            throw new RuntimeException(exception);
        }

        registerDropEvent();
    }

    private void registerDropEvent() {

        myTestButton.setOnDragOver(new EventHandler() {
            @Override
            public void handle(DragEvent t) {
                t.acceptTransferModes(TransferMode.ANY);
                DropShadow dropShadow = new DropShadow();
                dropShadow.setRadius(5.0);
                dropShadow.setOffsetX(3.0);
                dropShadow.setOffsetY(3.0);
                dropShadow.setColor(Color.color(0.4, 0.5, 0.5));
                myTestButton.setEffect(dropShadow);
                //Don't consume the event.  Let the layers below process the DragOver event as well so that the
                //translucent container image will follow the cursor.
                //t.consume();
            }
        });

        myTestButton.setOnDragExited(new EventHandler() {
            @Override
            public void handle(DragEvent t) {
                t.acceptTransferModes(TransferMode.ANY);
                myTestButton.setEffect(null);
                t.consume();
            }
        });

        myTestButton.setOnDragDropped(new EventHandler() {
            @Override
            public void handle(DragEvent t) {
                Dragboard db = t.getDragboard();
                final String string = db.getString();
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        JOptionPane.showMessageDialog(null, "Creating a New Plan For Container #: " + string);
                    }
                });
                t.setDropCompleted(true);
            }
        });

    }
}

Creating Custom JavaFX Components with Scene Builder and FXML.

One of the goals of our development with our JavaFX application is to try to keep as much as the UI design as possible within the Scene Builder UI design tool, and the business logic for the application in Java.  One of the things that is not entirely clear is how to create a new component in Scene Builder and then reuse that component within other components created in Scene Builder.

In the example below I illustrate how to create a custom table and ‘Add Plan’ components and then add them to a new Scene using FXML.

The first step is to create a simple component which which act as an “Add Plan” widget on the UI.  The widget contains an AnchorPane, ImageView and Label.

image

For the next step, open up the FXML file and change the first “AnchorPane” declaration to “<fx:root…” as in the example code below.  The type must be the same as the controller/root class which I will illustrate below.  In this case, the root/controller class will extend javafx.scene.layout.AnchorPane

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.effect.*?>
<?import javafx.scene.image.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.paint.*?>
<?import javafx.scene.text.*?>
<fx:root type="javafx.scene.layout.AnchorPane" xmlns:fx="http://javafx.com/fxml">
  <children>
    <AnchorPane fx:id="myTestButton" layoutX="0.0" layoutY="5.0" minHeight="58.0" prefHeight="74.0" prefWidth="129.0244140625" styleClass="main-back">
      <children>
        <ImageView fitHeight="44.0" fitWidth="44.0" layoutX="43.0" layoutY="8.0" pickOnBounds="true" preserveRatio="true">
          <image>
            <Image url="@new-plan.png" preserveRatio="false" smooth="false" />
          </image>
        </ImageView>
        <Label layoutX="43.0" layoutY="52.0" text="Add Plan" textFill="WHITE">
          <font>
            <Font name="System Bold" size="12.0" />
          </font>
        </Label>
      </children>
      <effect>
        <DropShadow />
      </effect>
    </AnchorPane>
  </children>
  </fx:root>

Create a class which will act as both the root of the component as well as its controller.  The class must extend the type that was previously defined in the FXML file.  Use the FXML loader to set the root and controller of the component to “this” class, and then load the component’s FXML file.

/* * To change this template, choose Tools | Templates * and open the template in the editor. */ package com.lynden.fx.test; import com.lynden.ui.util.UIUtilities; import java.io.IOException; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.input.DragEvent; import javafx.scene.input.Dragboard; import javafx.scene.input.TransferMode; import javafx.scene.layout.AnchorPane;

 

/**

* * @author ROBT */ public class TestButton extends AnchorPane { @FXML private AnchorPane myTestButton; public TestButton() {    FXMLLoader fxmlLoader = new FXMLLoader(

getClass().getResource("/com/lynden/planning/ui/TestButton.fxml"));

 

 

fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } }

}

 

Next, the 2nd component is a TableView which contains a collection of beans, and displays their corresponding data.

image

As with the last component, the first <AnchorPane> declaration is changed to <fx:root>

<?xml version="1.0" encoding="UTF-8"?>

<?import com.lynden.fx.*?>
<?import com.lynden.fx.table.*?>
<?import com.lynden.fx.test.*?>
<?import com.lynden.planning.ui.*?>
<?import java.lang.*?>
<?import java.net.*?>
<?import java.util.*?>
<?import javafx.collections.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.control.cell.*?>
<?import javafx.scene.effect.*?>
<?import javafx.scene.image.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.paint.*?>
<?import javafx.scene.shape.*?>
<?import javafx.scene.text.*?>
<?import javafx.util.*?>
<?scenebuilder-classpath-element split-flip-1.0.0.jar?>

<fx:root type="javafx.scene.layout.AnchorPane" xmlns:fx="http://javafx.com/fxml">
  <children>
    <BorderPane layoutX="0.0" layoutY="0.0" prefHeight="400.0" prefWidth="600.0">
      <center>
        <TableView fx:id="myTableView" prefHeight="200.0" prefWidth="200.0">
          <columns>
            <TableColumn prefWidth="42.0" text="">
            </TableColumn>
            <TableColumn prefWidth="110.0" text="Plan">
              <cellValueFactory>
                <PropertyValueFactory property="vfcPlan" />
              </cellValueFactory>
            </TableColumn>
            <TableColumn prefWidth="55.0" text="VFC #">
              <cellValueFactory>
                <PropertyValueFactory property="vfcNumber" />
              </cellValueFactory>
            </TableColumn>
            <TableColumn prefWidth="100.0" text="Location">
              <cellValueFactory>
                <PropertyValueFactory property="location" />
              </cellValueFactory>
            </TableColumn>
            <TableColumn prefWidth="100.0" text="Arrival">
              <cellValueFactory>
                <PropertyValueFactory property="arrivalTime" />
              </cellValueFactory>
            </TableColumn>
            <TableColumn prefWidth="35.0" text="Org">
              <cellValueFactory>
                <PropertyValueFactory property="origin" />
              </cellValueFactory>
            </TableColumn>
            <TableColumn prefWidth="35.0" text="Dst">
              <cellValueFactory>
                <PropertyValueFactory property="destination" />
              </cellValueFactory>
            </TableColumn>
            <TableColumn prefWidth="65.0" text="Shipments">
              <cellValueFactory>
                <PropertyValueFactory property="shipments" />
              </cellValueFactory>
            </TableColumn>
            <TableColumn prefWidth="125.0" text="Consignee">
              <cellValueFactory>
                <PropertyValueFactory property="consignee" />
              </cellValueFactory>
            </TableColumn>
            <TableColumn prefWidth="35.0" text="Haz">
              <cellValueFactory>
                <PropertyValueFactory property="hazMat" />
              </cellValueFactory>
            </TableColumn>
            <TableColumn prefWidth="45.0" text="Temp">
              <cellValueFactory>
                <PropertyValueFactory property="temperature" />
              </cellValueFactory>
            </TableColumn>
          </columns>
          <items>
            <FXCollections fx:factory="observableArrayList">
              <InboundBean arrivalTime="03-11-13 15:00" consignee="FMeyer" destination="ANC" hazMat="N" location="Consignee" origin="TAC" shipments="1" temperature="KFF" vfcNumber="345440" vfcPlan="Fred Meyer" />
              <InboundBean arrivalTime="03-11-13 15:00" consignee="FMeyer" destination="ANC" hazMat="N" location="Yard" origin="TAC" shipments="1" temperature="KFF" vfcNumber="123456" vfcPlan="Fred Meyer" />
              <InboundBean arrivalTime="03-11-13 15:00" consignee="FMeyer" destination="ANC" hazMat="N" location="Trip 12543" origin="TAC" shipments="1" temperature="KFF" vfcNumber="235555" vfcPlan="Fred Meyer" />
              <InboundBean arrivalTime="03-11-13 15:00" consignee="Costco" destination="KNA" hazMat="N" location="Trip 551332" origin="TAC" recovered="false" shipments="1" temperature="KFF" vfcNumber="244000" vfcPlan="KNA" />
              <InboundBean arrivalTime="03-11-13 15:00" consignee="SBS" destination="ANC" hazMat="N" location="Trip 12543" origin="TAC" recovered="false" shipments="1" temperature="KFF" vfcNumber="291007" vfcPlan="Sealand" />
              <InboundBean arrivalTime="03-11-13 15:00" consignee="Costco" destination="FBK" hazMat="N" location="Yard" origin="TAC" recovered="true" shipments="2" temperature="KFF" vfcNumber="291008" vfcPlan="Fairbanks" />
              <InboundBean arrivalTime="03-11-13 15:00" consignee="" destination="KNA" hazMat="N" location="Trip 12543" origin="TAC" recovered="false" shipments="1" temperature="KFF" vfcNumber="234554" vfcPlan="" />
              <InboundBean arrivalTime="03-11-13 15:00" consignee="" destination="ANC" hazMat="N" location="2" origin="TAC" recovered="false" shipments="2" temperature="KFF" vfcNumber="123407" vfcPlan="Darigold" />
              <InboundBean arrivalTime="03-11-13 15:00" consignee="" destination="FBK" hazMat="N" location="Yard" origin="TAC" recovered="true" shipments="1" temperature="KFF" vfcNumber="233458" vfcPlan="Yard" />
              <InboundBean arrivalTime="03-11-13 15:00" consignee="" destination="KNA" hazMat="N" location="" origin="TAC" recovered="false" shipments="1" temperature="KFF" vfcNumber="244340" vfcPlan="Flat" />
              <InboundBean arrivalTime="03-11-13 15:00" consignee="" destination="ANC" hazMat="Y" location="Trip 12543" origin="TAC" recovered="false" shipments="2" temperature="KFF" vfcNumber="222347" vfcPlan="" />
              <InboundBean arrivalTime="03-11-13 15:00" consignee="" destination="FBK" hazMat="N" location="" origin="TAC" recovered="false" shipments="1" temperature="KFF" vfcNumber="312008" vfcPlan="" />
              <InboundBean arrivalTime="03-11-13 15:00" consignee="" destination="KNA" hazMat="N" location="Trip 551332" origin="TAC" recovered="false" shipments="1" temperature="KFF" vfcNumber="313000" vfcPlan="" />
              <InboundBean arrivalTime="03-11-13 15:00" consignee="SBS" destination="ANC" hazMat="N" location="" origin="TAC" recovered="false" shipments="2" temperature="KFF" vfcNumber="334507" vfcPlan="" />
              <InboundBean arrivalTime="03-11-13 15:00" consignee="" destination="FBK" hazMat="N" location="" origin="TAC" recovered="false" shipments="1" temperature="KFF" vfcNumber="244408" vfcPlan="" />
            </FXCollections>
          </items>
        </TableView>
      </center>
    </BorderPane>
  </children>
</fx:root>

 

Next, the corresponding root and controller class is created for the table component.

package com.lynden.fx.test;

import com.lynden.fx.InboundBean; 
import java.io.IOException;
import javafx.event.EventHandler;
import javafx.event.EventType; 
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader; 
import javafx.scene.control.TableView; 
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode; 
import javafx.scene.layout.AnchorPane;

public class TestTable extends AnchorPane {

   @FXML private TableView myTableView; 

   public TestTable() { 
     FXMLLoader fxmlLoader = new FXMLLoader(
      getClass().getResource("/com/lynden/planning/ui/TestTable.fxml"));

      fxmlLoader.setRoot(this); fxmlLoader.setController(this);
       try { 
         fxmlLoader.load();
       } catch (IOException exception) { 
         throw new RuntimeException(exception); 
      } 
   } 
}

Finally, a new Scene can be created which includes both the TestTable and TestButton components that were constructed above.  Unfortunately, custom components can’t be added to the Scene Builder palette, so they must be inserted manually into the FXML file.  You just need to ensure that you have the proper import statements defined, and the TestTable and TestButton components can be inserted into the FXML code just as any native JavaFX component.

<?xml version="1.0" encoding="UTF-8"?>

<?import com.lynden.fx.*?>
<?import com.lynden.fx.table.*?>
<?import com.lynden.fx.test.*?>
<?import com.lynden.planning.ui.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.paint.*?>
<?scenebuilder-classpath-element ../../../../../../../target/classes?>

<AnchorPane id="AnchorPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="766.0" xmlns:fx="http://javafx.com/fxml">
    <children>
        <TestTable layoutX="5.0" layoutY="4.0" />
        <TestButton layoutX="622.0" layoutY="157.0" />
    </children>
</AnchorPane>

When Scene Builder opens the FXML file you will see that both components are displayed on the new Scene

image

That’s it, hopefully this will help others who have been looking at adding custom components to their UIs that are designed with Scene Builder.

twitter: @RobTerp

Using a Custom TableCell Factory to Format a JavaFX table in FXML

We are in the process of designing a table within our JavaFX UI which is being developed using Scene Builder and FXML with CSS for the UI styling.  The table contains information about shipping containers with a requirement that the table row be color coded based on the status of the particular container.  The row will need to be red if the container’s arrival has not yet been planned for, green if its arrival has been planned, but it has not yet arrived, and the default background if the container has been planned for and it has arrived.

I could not find an online tutorial which covered this type of conditional formatting with the UI written in FXML and CSS, so I wanted to document a solution for this problem.

The first step is to define the various formats for the table rows in the application’s CSS file

/* Table text should always be white */

.table-cell { -fx-text-fill: white; -fx-font-size: 14px; } /* Style for default table rows */ .defaultTableStyle { -fx-font-size: 12px; -fx-base: rgb(0,0,0,0); -fx-background-color: linear-gradient( to top, derive(-fx-color, 35.77966101694915%) 0%, derive(-fx-color, 10.05084745762713%) 50%, derive(-fx-color, -8.559322033898283%) 50.5%, derive(-fx-color, -30.0%) 100%); -fx-background-image: url('low_contrast_linen.png'); -fx-background-color: rgba(0,0,0,.75); -fx-border-color: gray; -fx-border-width: 1 1 0 0; }

 

 


/* Format for table rows where container's arrival

has been planned */

.planAssigned { -fx-font-size: 12px; -fx-base: rgb(0,0,0,0); -fx-background-color: #347b34; -fx-border-color: gray; -fx-border-width: 1 1 0 0; } /* Format for row where a plan has not yet been assigned */ .planNotAssigned { -fx-font-size: 12px; -fx-base: rgb(0,0,0,0); -fx-background-color: #983737; -fx-border-color: gray; -fx-border-width: 1 1 0 0; }

/* Format for table row where container has arrived */ .vfcRecovered { -fx-font-size: 12px; -fx-base: rgb(0,0,0,0); -fx-background-color: linear-gradient( to top, derive(-fx-color, 35.77966101694915%) 0%, derive(-fx-color, 10.05084745762713%) 50%, derive(-fx-color, -8.559322033898283%) 50.5%, derive(-fx-color, -30.0%) 100%); -fx-background-image: url('low_contrast_linen.png'); -fx-background-color: rgba(0,0,0,.75); -fx-border-color: gray; -fx-border-width: 1 1 0 0; }

The next step is to create a new custom table cell factory which will be used to select the correct CSS style for the table row based on the container’s status.

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.lynden.fx.table;

import com.lynden.fx.InboundBean;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.util.Callback;

/**
 *
 * @author ROBT
 */
public class FormattedTableCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {

    public FormattedTableCellFactory() {
    }

    @Override
    public TableCell<S, T> call(TableColumn<S, T> p) {
        TableCell<S, T> cell = new TableCell<S, T>() {
            @Override
            protected void updateItem(Object item, boolean empty) {
                // CSS Styles
                String planNotAssignedStyle = "planNotAssigned";
                String planAssignedStyle = "planAssigned";
                String vfcRecoveredStyle = "vfcRecovered";
                String defaultTableStyle = "defaultTableStyle";
                String cssStyle = "";

                InboundBean inboundBean = null;
                if( getTableRow() != null ) {
                    inboundBean = (InboundBean) getTableRow().getItem();
                }

                //Remove all previously assigned CSS styles from the cell.
                getStyleClass().remove(planAssignedStyle);
                getStyleClass().remove(planNotAssignedStyle);
                getStyleClass().remove(vfcRecoveredStyle);
                getStyleClass().remove(defaultTableStyle);

                super.updateItem((T) item, empty);

                //Determine how to format the cell based on the status of the container.
                if( inboundBean == null ) {
                    cssStyle = defaultTableStyle;
                } else if( inboundBean.isRecovered() ) {
                    cssStyle = vfcRecoveredStyle;
                } else if( inboundBean.getVfcPlan() != null && inboundBean.getVfcPlan().length() > 0 ) {
                    cssStyle = planAssignedStyle;
                } else {
                    cssStyle = planNotAssignedStyle;
                }

                //Set the CSS style on the cell and set the cell's text.
                getStyleClass().add(cssStyle);
                if( item != null ) {
                    setText( item.toString()  );
                } else {
                    setText( "" );
                }
            }
        };
        return cell;
    }
}

The final step in the process is to specify the custom factory in the TableView section of the FXML file.  .

First, in the FXML file, define the packages that contain the custom factory classes.

<?xml version="1.0" encoding="UTF-8"?>

<?import com.lynden.fx.*?>
<?import com.lynden.fx.table.*?>

 

The next step is to then specify the FormattedTableCellFactory as the cellFactory (highlighted in red) for the columns in the TableView.

<TableView fx:id="table" prefHeight="451.0" prefWidth="727.0" styleClass="main-back"> <columns> <TableColumn text=""> <cellFactory> <FormattedTableCellFactory /> </cellFactory> </TableColumn> <TableColumn prefWidth="120.0" text="Plan"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="vfcPlan" /> </cellValueFactory> </TableColumn> <TableColumn prefWidth="55.0" text="VFC #"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="vfcNumber" /> </cellValueFactory> </TableColumn> <TableColumn prefWidth="100.0" text="Location"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="location" /> </cellValueFactory> </TableColumn> <TableColumn prefWidth="100.0" text="Arrival"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="arrivalTime" /> </cellValueFactory> </TableColumn> <TableColumn prefWidth="35.0" text="Org"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="origin" /> </cellValueFactory> </TableColumn> <TableColumn prefWidth="35.0" text="Dst"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="destination" /> </cellValueFactory> </TableColumn> <TableColumn prefWidth="65.0" text="Shipments"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="shipments" /> </cellValueFactory> </TableColumn> <TableColumn prefWidth="125.0" text="Consignee"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="consignee" /> </cellValueFactory> </TableColumn> <TableColumn prefWidth="35.0" text="Haz"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="hazMat" /> </cellValueFactory> </TableColumn> <TableColumn prefWidth="45.0" text="Temp"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="temperature" /> </cellValueFactory> </TableColumn> </columns> <items>

<!—Sample data to illustrate cell formatting –-> <FXCollections fx:factory="observableArrayList"> <InboundBean arrivalTime="03-11-13 15:00" consignee="Fred Meyer" destination="ANC" hazMat="N" location="Consignee" origin="TAC" shipments="1" temperature="KFF" vfcNumber="345440" vfcPlan="" /> <InboundBean arrivalTime="03-11-13 14:00" consignee="Fred Meyer" destination="ANC" hazMat="N" location="Yard" origin="TAC" shipments="1" temperature="KFF" vfcNumber="123456" vfcPlan="" /> <InboundBean arrivalTime="03-11-13 19:00" consignee="Fred Meyer" destination="ANC" hazMat="N" location="Trip 12543" origin="TAC" shipments="1" temperature="KFF" vfcNumber="235555" vfcPlan="Fred Meyer" /> <InboundBean recovered="true" arrivalTime="03-12-13 10:00" consignee="Costco" destination="KNA" hazMat="N" location="Trip 551332" origin="TAC" shipments="1" temperature="KFF" vfcNumber="244000" vfcPlan="KNA" /> <InboundBean arrivalTime="05-23-13 15:00" consignee="Lowes" destination="ANC" eta="05-23-13 15:00" hazMat="N" location="Yard" origin="TAC" shipments="5" temperature="KFF" vfcNumber="123456" vfcPlan="Fred Meyer" /> <InboundBean arrivalTime="05-23-13 15:00" consignee="Lowes" destination="ANC" eta="05-23-13 15:00" hazMat="N" location="Yard" origin="TAC" shipments="5" temperature="KFF" vfcNumber="123456" vfcPlan="Fred Meyer" /> <InboundBean arrivalTime="05-23-13 15:00" consignee="Lowes" destination="ANC" eta="05-23-13 15:00" hazMat="N" location="Yard" origin="TAC" shipments="5" temperature="KFF" vfcNumber="123456" vfcPlan="Fred Meyer" /> </FXCollections> </items> </TableView>

That’s it. By utilizing this strategy, cells in the JavaFX TableView can be conditionally formatted using custom cell factories and CSS.  The table below is the product of the strategy that was implemented in the above code.

image

twitter: @RobTerp

Streamlining a Deployment Pipeline with a Custom Jenkins Plug-In

In a previous post (located here) I discussed setting up a deployment pipeline with Jenkins which would retrieve a specified version of one of our web applications from our Nexus repository and deploy it to a specified Glassfish server.  One of the challenges with the Jenkins job was that most of the fields on its deployment page were text fields, allowing users to free form text which was prone to errors.  The original deployment page appears below and allows the user to select the server and application via dropdown box, but leaves all other properties open to free form text.

image1.png (945×640)

The other drawback to the screen above was that each application needed to be listed in a text file which Jenkins would read  in order to populate the Application dropdown box.  This meant that there was a manual step involved with deploying an application that had not been deployed before.

With this in mind I decided to modify the Jenkins Dynamic Parameter Plugin to allow us to auto populate dropdown boxes with valid values based on our environment here at Lynden. I started with this plug-in since it already had functionality to change the contents of dropdown boxes based upon user selections. ie The selection of one dropdown could be used to determine the contents of another dropdown box.  The requirements for this modified plug-in were that it should:

  • Get a list of applications that are in our Nexus release repository
  • Get a list of versions of the selected application from Nexus.
  • Get a list of the application versions that are currently deployed to the Glassfish instance the user has specified for deployment.
  • Enable the user to select a web context to use when deploying the app, or select “Other” if they want to enter a context that does not appear in the list.

The Server dropdown box for this new Deployment plug-in allows the user to select which instance of Glassfish they would like to deploy to.  Each line in the dropdown contains the server name, Glassfish instance name, HTTP port, and admin port of the instance.

JenkinsDeployServerSelect

Once the user has selected a server, they can then select an application from the Application dropdown box.  For this field, the Jenkins plug-in goes out to our Nexus repository and retrieves the names of all of our applications, populating the list with these values.

JenkinsDeployAppSelect

Once the application has been selected, the user can select which version of the application to deploy.  This list of versions is also obtained from Nexus by the plug-in, which displays up to 25 of the most recent versions.

JenkinsDeployAppVersionSelect

Once the user has selected the version to deploy, they can optionally specify a version to undeploy.  The plug-in populates this field by looking at the Glassfish instance that the user selected and querying it to determine if any versions of the specified application are currently deployed to it.  In the example below, HelloWorldWeb 1.1.5 and 1.1.4 are both currently deployed to the Glassfish instance running on “webstg”.  This field is optional, and if a value is selected, the Deploy task will first undeploy the specified version before deploying the new version.

JenkinsDeployAppUndeployVersionSelect

Finally, the user can select a context to use when deploying the application, which is the portion of the URL after the hostname that the app server uses to identify requests for the application.  The two most common context names are included in the drop down, which are <app-name> and <app-name>-<version-number>.  So for example, if HelloWorldWeb-1.1.3 were selected below, the application would have a URL of http://webstg.lynden.com/HelloWorldWeb-1.1.3

JenkinsDeployAppContextSelect

If a different application context is needed, the user can select “Other” from the dropdown box (in the screenshot above), and then enter an app context name in the “Other” text box below the Context drop down box.

JenkinsDeployAppContextOther

At this point the user can select the “Build” button which will invoke an Ant script detailed in the previous post, downloading the binary from Nexus, undeploying the previously deployed version from Glassfish (if specified), deploying the new version of the application to Glassfish, and then saving information about the deployment to a database which can be viewed from a separate web application.


Configuration

Since nearly all the fields are dynamically populated, there isn’t too much work for configuring the job in Jenkins.  Below is the Configuration screen for this deployment job.  There are 2 parameters which must be defined for this job.  The first is the environment this job will deploy to, ie. Test, Staging or Prod. The second is a list of valid Glassfish instances which can be deployed to.  The location of the maven repository and the location of the Glassfish command line interface are defined as system properties in Jenkins.

JenkinsDeployAppConfig

By using a Jenkins plugin to prepopulate the fields which are required for deploying an application we will streamline the deployment process and make it less prone to error.

twitter: @RobTerp

JavaFX is Eye Candy and I Have a Sweet Tooth. (Another Extreme UI Makeover)

In a previous article I showed a redesigned user interface for our Freight Management System (FMS) application using JavaFX as we begin to migrate away from Swing.  We are continuing to design screens for new functionality in FMS, and in this article I will illustrate a makeover I implemented for this new screen using JavaFX, which we had originally had planned to use Swing when the UI was first designed.

The screenshot below shows the original wireframe mock-up of what will be the new “VFC Recovery” screen within our Inbound Planning module of the FMS application.  This module will be used by our warehouse planning personel to display all “Trips” that will be arriving at their terminal.  A trip could be an incoming truck, ship, rail, or flight which has freight destined for the terminal.  On this screen, inbound planners in our warehouses will have the ability to see these incoming trips by arrival date and time.  This is the first layer of nested tables on this tab.  The next layer of tables below the arrival date, is reserved for each trip.  ie in the example below “TOTE*11027” is a ship which is scheduled to arrive on 07-18-12 08:00.  Expanding the trip table, will show the next level of tables, which are the plans for the freight once it arrives at the terminal.  Expanding this table then would show all the shipping containers (called VFCs) that the inbound planners have assigned to their plans, and finally, the VFC rows in this table can be expanded one last time to display all the shipments within the specified container.  As can be seen, the amount of data displayed to the user can get overwhelming pretty quickly.

image
Wireframe mock-up of VFC Recovery screen

The first step in a makeover of the VFC Recovery screen was to try to separate the data in a way that was intuitive for the users to navigate while also giving them the information they need to make their planning decisions.  Below is a picture of what was designed during a whiteboard session.  The inbound trips to the Lynden Transport terminal (Anchorage in this case), would be displayed in a table in the top portion of the screen.  The table would contain a summary of information related to the incoming trip, such as the mode of travel (ie truck, rail, barge, etc), as well as the origin of the trip, and anticipated date/time of the arrival.  The user could select a trip in this table, and the details of the trip would be displayed in a section below the table.  The details section would contain additional information about the trip, as well as contain an “Accordion” component which would have a separate section for each Plan that the inbound planners would be putting together.  The user could then expand one of these sections to view details on all of the containers that had been assigned to a particular plan.

RecoveryDrawingUI
Results of UI design whiteboard session

Once again I used the JavaFX design tool, Scene Builder to construct the new user interface.  In addition, I wanted to make use of the open source JavaFX components that are part of the JFXtras project, which are located here.  Components that are in the JFXtras project unfortunately can’t be added to the component palette in Scene Builder, but it was still easy enough to manually add them to the FXML code that Scene Builder generates, at which point Scene Builder will display them in the application’s UI as if they had been part of the component palette.  (Exactly how I accomplished this could be a future blog post.)

Below is a screenshot of the new UI for the VFC Recovery tab.  I took a bit of inspiration from what I had seen at Grand Central Station in New York, with my goal to make the trip arrivals portion of the UI look something like what you may see in an airport or train station. Two components in the JFXtras library were perfect for this task.  I used the DotMatrixSegment component to create the arrival board’s column headers as well as display the terminal where the trips were arriving at, in this case Anchorage.  I then used the Splitflap component to display the actual summary data about the trip, including arrival date/time, the trip ID, the origin and also the mode.  I found a nice brushed metal background, and used inner and drop shadows to give the board a sense of depth. In the screen shot below, the trip that the user had selected is highlighted by giving the row a green background.

The details section below the arrival board is formatted in a similar fashion to the table found in the previous article’s JavaFX makeover.  I used CSS to create the gradients, rounded corners, borders, background, etc for the section header, accordion component, and data table.

image
JavaFX Mock of VFC Recovery Screen

As mentioned in my previous article, this screen will still reside as a TopComponent within a NetBeans RCP application which will manage our other freight management modules that we will be designing with JavaFX as the UI toolkit.

twitter: @RobTerp

Extreme UI Makeover, JavaFX Edition

We have decided to move forward with JavaFX here at Lynden as we design the next version of our Freight Management System (FMS) application.  The initial version of the application was built on the Netbeans Rich Client Platform and will remain so, but we will be moving from the Swing and Jide (Commercial Swing components) component libraries to JavaFX.  The first application module that we are looking at tackling is called “Inbound Planning”.  This module will be used by warehouse managers at our various terminals to plan how and when to pick up and stage freight that is inbound to their location.

Below is a screenshot of the current FMS application with Planning module that is currently in use.  Warehouse managers run a query for their location and a Jide table displays all shipping containers that are inbound to their location.  Containers are grouped by destination, with each destination being represented by a row in the table.

image

 

Each destination row can then be expanded to reveal a Jide nested table showing all the shipping containers for a particular destination, with each container as a sub row to the destination row.  Finally, each container row can then be expanded to show all the shipments within that specific shipping container, for a total of 3 layers of tables.  In addition, when a user select a shipment, all the details about that shipment are displayed in a separate table in the lower pane of the UI.

image

 

In the next version of the FMS Planning module we will be adding functionality to the application to allow the users to create plans for the incoming containers.  The initial wire frame diagram below was created for the new planning module. The purpose of the new functionality is to allow the manager to see all incoming containers separated by mode of transit, i.e. ship, rail, truck, etc. and then plan for the container’s arrival accordingly by assigning it to a “Unit Plan”.

The top level table is represented by transit mode, which can then be expanded to reveal all the incoming trucks, ships, and trains to the user’s location. For example, the user could expand the “Steamship” row which would show a sub-table showing all scheduled incoming barges to that location.  The manager would like to ensure that all containers are picked up at the shipyard for an incoming voyage and are properly staged at the warehouse for final delivery to the customer. This could be accomplished by expanding the row for a particular voyage which would display sub rows to group the containers by Full-Load, Consolidation, and Transfer shipments.  The user would then expand the appropriate row to see all the containers that fall within one of those three categories.  In order to assign the container to a Unit Plan, the user would click the “Update” button on each row which would display a dialog asking the user which plan to assign the container to, at which point the container would disappear from this screen indicating that the container’s arrival had been planned.

image

 

The feedback that was received regarding this wireframe diagram and corresponding workflow is that this is an overwhelming amount of data to display to the user.  There are 5 levels of nested-tables within this framework and when all are fully expanded it is easy to get lost in the resulting sea of nested rows and columns. 

I was tasked with helping to redesign the UI to make the data a bit more manageable for the users as well as provide an efficient means for them to be able to select the containers they are interested in and assign them to the appropriate Unit Plan as well as give them visibility as to which containers still needed to be assigned to a plan.

The result of a whiteboard session appears below.  Transit modes would be displayed within a row of buttons at the top of the screen which would also show  summary data  regarding the number of shipments within each category.  The user could then select one of the categories and the data would be displayed in a table in the main portion of the UI. For example, if the user selected “Steamship”, the table below would show tabs on its left side, one for each inbound voyage.  When the user selects a voyage, the table would populate with all containers on that voyage that have not yet been assigned to a plan. The tabs would have a progress meter to indicate to the user what percentage of containers on the voyage have been assigned to a plan. An “Accordian” widget would be used to partition the data in the table by container type “Full Load”, “Consolidation” or “Transfer”.  Rather than making the user click a button on each row and display a dialog asking which plan the user wanted to assign the container to, the plans are instead shown in a table on the same screen.  The user could then select one or more container rows and then drag them directly over to the plan they wanted to assign those containers to, at which point the containers would disappear from the table, and the progress meter for the voyage would be updated to reflect the changes.

 

image

 

With the help of the JavaFX design tool, Scene Builder, I was able to design a mock UI of the new Inbound Planning screen. The screenshot below illustrates the “Steamship” mode selected, which changes the button’s color as well as creates a slight glow around the button.The Tote*55433 voyage has been selected and displays all the “Full Load” containers that have not yet been assigned to a plan, but could be dragged over to one of the plans on the right-side table.  The “Sea Land” unit plan is highlighted to illustrated what the table  could look like if the user had dragged any containers over to from the voyage table. (Green and gold are Lynden colors, that which is why the choice of this particular palate).

image

Scene Builder made it extremely easy to add effects such as gradients to the buttons, tabs and unit plan rows to give the components a glossy look.  Also, adding the reflections to the buttons along the top row was a very straightforward task as well as creating the translucent rows of the Unit Plan table.

As previously mentioned, the Inbound Planning module will still reside as a TopComponent within a NetBeans RCP application which will manage our other freight management modules that we will be designing with JavaFX as the UI toolkit as well.

twitter: @RobTerp