JavaFX: Mavenize-FX

As part of learning JavaFX I decided to put a nice UI onto my existing ‘mavenize’ tool. This is currently a command line tool and does what it says in tin, so to speak. To find out more about this you can read the original post here. I modified the original component to allow a ‘listener’ to attach itself to the processing section. The mavenize process calls back into the listener to notify of what it was currently doing. I wanted the UI to reflect the changes being made (not as simple as it sounds as we will discover).

What I wanted for the user interface was:

So here it is:

jfx1

I will discuss each feature in turn and what was interesting about it. Here is a rough class diagram of how the various components are connected.

mavenize_class

I have numbered the parts of the interface I want to focus on.

jfx2

Here goes…

(1) Inputs

These are the source and target directories. They are standard TextField controls and are populated from the choice of folder made using the java directory chooser. They are not populated on startup. You will need to choose a folder for each. It can’t be the same folder. If you do not populate or make them the same you will get an Alert box popping up.

jfx3

This is one thing which I was a bit non-plussed about. There doesn’t seem to be a standard Alert box for JavaFX. I ended up rolling my own from suggestions I found on StackOverflow. I have some ideas to improve my implementation to make it much nicer. I always liked the Flex alert which defocussed the whole screen and had a message in the middle. I think it would be nice to have something like that.

(2) Browse Buttons.

When these are clicked we use the folder chooser.


// At the top of the controller class.
@FXML
private TextField sourceInput;

@FXML
private Button sourceButton;

// The method.
public void sourceButtonAction(ActionEvent event)
{
	Window window = getWindow(sourceButton);

	if (window != null)
	{
		File directory = directoryChooser.showDialog(window);

		if (directory != null)
		{
			sourceInput.setText(directory.getPath());
		}
	}
}

The DirectoryChooser object is created in the constructor and shared between the source and target button handlers. Straightforward stuff.

(3) Process button.

This is an interesting one and brings in a bit of an overlap to the table view. When this button is pressed it will validate the UI inputs and if fine then it will call a method from the MavenizeClient class.

public void activateButtonAction(ActionEvent event)
{
	logger.debug("activateButtonAction");

	String sourcePath = sourceInput.getText();
	String targetPath = targetInput.getText();
	String versionText = versionInput.getText();

	if (sourcePath == null || sourcePath.isEmpty())
	{
		// Alert
		Alert alert = new Alert(stage, ApplicationMessages.MSG_ERROR_INVALID_SOURCE);

		alert.showAndWait();
	}
	else if (targetPath == null || targetPath.isEmpty())
	{
		// alert
		Alert alert = new Alert(stage, ApplicationMessages.MSG_ERROR_INVALID_TARGET);

		alert.showAndWait();
	}
	else if (versionText == null || versionText.isEmpty())
	{
		// alert
		Alert alert = new Alert(stage, ApplicationMessages.MSG_ERROR_INVALID_VERSION);

		alert.showAndWait();
	}
	else if (sourcePath.equals(targetPath))
	{
		Alert alert = new Alert(stage, ApplicationMessages.MSG_ERROR_INVALID_PATHS);

		alert.showAndWait();
	}
	else
	{
		// Process
		mavenizeClient.process(sourceInput.getText(), targetInput.getText(), versionInput.getText(), packageCombo.getValue());
	}
}

The MavenizeClient will start the background service which performs our potentially long running process.

public boolean process(String sourcePath, String targetPath, String version, String packaging)
{
	boolean status = true;

	if (sourcePath == null || sourcePath.length() == 0 || targetPath == null || targetPath.length() == 0)
	{
		throw new IllegalArgumentException();
	}
	else
	{
		mavenizeService.setSourcePath(sourcePath);
		mavenizeService.setTargetPath(targetPath);
		mavenizeService.setVersion(version);
		mavenizeService.setPackaging(packaging);

		mavenizeService.reset();

		mavenizeService.start();
	}

	return status;
}

Note the call to ‘reset’ before we call ‘start’ on the service. We need to this as each run has an internal status. Once complete we cannot call ‘start’ again without resetting the status.

The service has an ‘active’ property which we ultimately ‘bind’ the button ‘enabled’ status to so that as long as the service is running the button will be disabled. This is the power of binding and is a very elegant means to do this sort of thing.

(4) Table View.

This binding worked fine between the process button and the service active status. Not so for the TableView and the associated ObservableList. Here are the main points:

Now, I wasn’t actually expecting this to  work. Can you really update a bound list in a background thread? Well, yes so it seems..sort of.  The problem is this – the TableView does update (just to be on the safe side I used a synchronised version of the ObservableList) but it is not consistent. The TabelView was updating or not updating the list on a purely random basis. In short, we can’t expect the view to refresh itself when we are adding stuff to the bound list in a thread which is not the UI thread. It works perfectly if you fill and update the data items in the UI thread.

After a lot of head scratching I implemented a method to force the view to update. This must be kicked off using the runLater mechanism otherwise it would be modifying the scene view directly and throw a massive wobbly. It gets called whenever an item is added to the list or modified. This is a bit of a crufty solution but it works! If you examine the class diagram above you will see that the service is given a reference to the ImplementsRefresh interface.

@Override
public void refresh()
{
	Platform.runLater(new Runnable()
	{
		public void run()
		{
			ObservableList<TableColumn<ProjectResult, ?>> columns = dataTable.getColumns();
			TableColumn<ProjectResult, ?> column = columns.get(0);

			if (column != null)
			{
				column.setVisible(false);
				column.setVisible(true);
			}
		}
	});

}

Binding a list to a table in this way is a really cool feature.

(5) Icon cell.

So I am thinking that I want is a column with icons which will change according to a bound property in the data item.

Here is the data item

package com.netthreads.javafx.mavenize.model;

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

/**
 * Project result bean.
 *
 */
public class ProjectResult
{
	public static final String ATTR_GROUP_ID = "groupId";
	public static final String ATTR_ARTIFACT_ID = "artifactId";
	public static final String ATTR_FILE_PATH = "filePath";
	public static final String ATTR_FILE_COUNT = "fileCount";
	public static final String ATTR_STATUS = "status";
	public static final String ATTR_WORKING = "working";

	public static final String TITLE_GROUP_ID = "groupId";
	public static final String TITLE_ARTIFACT_ID = "artifactId";
	public static final String TITLE_FILE_PATH = "File Path";
	public static final String TITLE_FILE_COUNT = "File Count";
	public static final String TITLE_STATUS = "Status";
	public static final String TITLE_WORKING = "~";

	public static final String STATUS_CREATE = "Creating";
	public static final String STATUS_COPY = "Copying";
	public static final String STATUS_FILE = "Add File";
	public static final String STATUS_POM = "Pom";

	public static final int WORKING_READY = 0;
	public static final int WORKING_BUSY = 1;
	public static final int WORKING_DONE = 2;

	private StringProperty groupIdProperty;
	private StringProperty artifactIdProperty;
	private StringProperty filePathProperty;
	private IntegerProperty fileCountProperty;
	private StringProperty statusProperty;
	private IntegerProperty workingProperty;

	/**
	 * Construct results.
	 *
	 */
	public ProjectResult()
	{
		groupIdProperty = new SimpleStringProperty(this, ATTR_GROUP_ID);
		artifactIdProperty = new SimpleStringProperty(this, ATTR_ARTIFACT_ID);
		filePathProperty = new SimpleStringProperty(this, ATTR_FILE_PATH);
		fileCountProperty = new SimpleIntegerProperty(this, ATTR_FILE_COUNT);
		statusProperty = new SimpleStringProperty(this, ATTR_STATUS);
		workingProperty = new SimpleIntegerProperty(this, ATTR_WORKING);

		groupIdProperty.set("");
		artifactIdProperty.set("");
		filePathProperty.set("");
		fileCountProperty.set(0);
		statusProperty.set("");
		workingProperty.set(WORKING_READY);
	}

	public final String getGroupId()
	{
		return groupIdProperty.get();
	}

	public final void setGroupId(String groupId)
	{
		this.groupIdProperty.set(groupId);
	}

	public final String getArtifactId()
	{
		return artifactIdProperty.get();
	}

	public final void setArtifactId(String artifactId)
	{
		this.artifactIdProperty.set(artifactId);
	}

	public final String getFilePath()
	{
		return filePathProperty.get();
	}

	public final void setFilePath(String filePath)
	{
		this.filePathProperty.set(filePath);
	}

	public final int getFileCount()
	{
		return fileCountProperty.get();
	}

	public final void setFileCount(int fileCount)
	{
		this.fileCountProperty.set(fileCount);
	}

	public String getStatus()
	{
		return statusProperty.get();
	}

	public void setStatus(String status)
	{
		this.statusProperty.set(status);
	}

	public int getWorking()
	{
		return workingProperty.get();
	}

	public void setWorking(int working)
	{
		this.workingProperty.set(working);
	}

	/**
	 * Properties.
	 *
	 */

	/**
	 * Return property.
	 *
	 * @return The property.
	 */
	public final StringProperty groupIdProperty()
	{
		return groupIdProperty;
	}

	/**
	 * Return property.
	 *
	 * @return The property.
	 */
	public StringProperty artifactIdProperty()
	{
		return groupIdProperty;
	}

	/**
	 * Return property.
	 *
	 * @return The property.
	 */
	public StringProperty filePathProperty()
	{
		return filePathProperty;
	}

	/**
	 * Return property.
	 *
	 * @return The property.
	 */
	public IntegerProperty fileCountProperty()
	{
		return fileCountProperty;
	}

	/**
	 * Return property.
	 *
	 * @return The property.
	 */
	public IntegerProperty workingProperty()
	{
		return workingProperty;
	}

}

So the table column definition looks like this:

// Working indicator
TableColumn<ProjectResult, Integer> workingCol = new TableColumn<ProjectResult, Integer>(ProjectResult.TITLE_WORKING);
workingCol.setCellValueFactory(new PropertyValueFactory<ProjectResult, Integer>(ProjectResult.ATTR_WORKING));

// Custom Cell factory converts index to image.
workingCol.setCellFactory(new Callback<TableColumn<ProjectResult, Integer>, TableCell<ProjectResult, Integer>>()
{
	@Override
	public TableCell<ProjectResult, Integer> call(TableColumn<ProjectResult, Integer> item)
	{
		WorkingTableCell cell = new WorkingTableCell();
		return cell;
	}
});

The WorkingTableCell class takes the integer value you set in the workingProperty of the ProjectResult data object and sets the appropriate icon.


package com.netthreads.javafx.mavenize.controller;

import java.io.InputStream;

import javafx.scene.control.TableCell;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;

import com.netthreads.javafx.mavenize.app.ApplicationStyles;
import com.netthreads.javafx.mavenize.model.ProjectResult;

/**
 * Working status custom cell.
 *
 */
public class WorkingTableCell extends TableCell<ProjectResult, Integer>
{
	private String[] ICONS =
	{
	        "/bullet_white.png", "/bullet_red.png", "/bullet_green.png"
	};

	private ImageView imageView;
	private HBox hBox;

	/**
	 * Construct cell image holder.
	 *
	 */
	public WorkingTableCell()
	{
		imageView = new ImageView();

		hBox = new HBox();
		hBox.getChildren().add(imageView);
		hBox.getStyleClass().add(ApplicationStyles.STYLE_WORKING_STATUS_CELL);
	}

	/**
	 * This will take the value and lookup the appropriate icon for display in
	 * the cell.
	 */
	@Override
	protected void updateItem(Integer item, boolean empty)
	{
		super.updateItem(item, empty);

		if (!empty)
		{
			if (item < ICONS.length)
			{
				String iconName = ICONS[item];

				InputStream stream = getClass().getResourceAsStream(iconName);
				Image goImage = new Image(stream);

				imageView.setImage(goImage);

				setGraphic(hBox);
			}
		}
	}

}

I wanted the icon image to be centred in the cell. I’m unsure if there is a programmatic way to do this but I figured I coud do it by defining a style and setting it to the holding HBox.

.workingStatusCell {
	-fx-alignment : center;
	-fx-fill-height : true;
}

And that’s it. I am going to publish the code onto either Google code or github but in the meantime here is the code. As usual Apache 2.0 licence.

JavaFX: Pivot and JavaFX

I have been trying out both Pivot and JavaFX as means to write a user interface for my proposed tool to route OSC messages to Midi (see previous post).

Rather than jump straight in with something which needs lots of features I decided to try something simple. My mavenize tool needs a front end so I thought that would be a good candidate. Mavenize takes a source and target directory and produces a ‘mavenized’ version of the source project in the target folder. It will create the appropriate folders and move all the files into the appropriate place. It is the dogs-bollocks and I have used it a lot to save me lots of boring graft. Stop being a mug and just download the command line version from here and better still there is a version with a proper UI coming in the next post.

Since I was once a big Flex user I decided to look at the two SDK’s available for Java which use a ‘flex’-like approach. These are Apache Pivot and JavaFX that is now part of the standard Java 7 SDK. Both of these have the UI layout and components defined in an XML file and the logic for the UI in the code. Also, these SDK’s embody the concept of a scene-graph for assembling the interface components which at least promises to deliver something more elegant than the somewhat old-skool collection of composite objects which make up Swing.

pivot1

I put a basic Pivot version together fairly quickly. I am not entirely sure why they need to have their own version of the collection classes, I am not going to speculate as I am sure there is a good reason. This put me off for a start. Does the world need another set of collection classes beyond those tried and trusted from the JDK? This is a desktop app and they were always going to have to use the JDK. As you can see from the screenshot below there was also a custom file chooser dialog. Somewhat frustrating.

pivot2

Also, as I was trying to write a desktop application I was a bit confused by the data model that the table view used. It turned out to be a list of maps which was fine but I couldn’t find a single example of how to use this in a programmatic way outside the bxml examples. I figured it out the end but was left with a sense that this is glaring gap in their otherwise exhaustive set of examples. If you try to use a framework and suddenly find yourself rushing towards an edge case that is a bad sign.

I am not going to spend much time on my Pivot version of the application. The framework is usable but pales into insignificance against JavaFX in terms of ease of use.

So, then I decided to take a look at JavaFX. I have to say I am pleasantly surprised. The scene graph and concept of data binding is very close to Flex. So much so that I think I might be a bit miffed if I was Adobe. There is a lot of (ahem) influence from Flex in the feature set. The scene builder tool is probably one of the nicest UI builder tools I have ever used with lots of cleverness to help you layout your user interface. Here is screenshot of the finished tool.

javafx1

There is a whole bunch of JavaFX goodness here where I have solved the issue of updating the table view from a background service and have a nice custom cell with selectable images. I put the last one in because I knew I would need it for my proposed OSC router application.

I am going to outline what I did in the next post.