SWT to JavaFX migration – Eclipse RCP Cookbook

No Comments

When developing an Eclipse RCP application in the past, you needed to use the Standard Widget Toolkit (SWT) as UI toolkit for creating and rendering the user interface. With Eclipse 4 you are not restricted to SWT anymore. The architecture separated the application model and the rendering, which allows to create an Eclipse based application using a different UI toolkit. Currently there are implementations for JavaFX and Vaadin available.

The following recipe shows the steps necessary to migrate a simple Eclipse 4 application from SWT to JavaFX using the e(fx)clipse tooling and runtime. It is based on the basic recipe introduced in a previous blog post and can also be used as basis for further recipes.

Cookware

Ingredients

This recipe uses the Eclipse RCP Cookbook – Basic Recipe. To get started fast with this recipe, we have prepared the basic recipe for you on GitHub.

If you want to use the prepared basic recipe to follow the migration from SWT to JavaFX, import the project by cloning the Git repository:

  • File → Import → Git → Projects from Git
  • Click Next
  • Select Clone URI
  • Enter URI https://github.com/fipro78/e4-cookbook-basic-recipe.git
  • Click Next
  • Select the master branch
  • Click Next
  • Choose a directory where you want to store the checked out sources
  • Click Next
  • Select Import existing projects
  • Click Next
  • Click Finish

Preparation

Step 1: Update the Target Platform

  • Open the target definition de.codecentric.eclipse.tutorial.target.target in the project de.codecentric.eclipse.tutorial.target
  • Remove the Software Site http://download.eclipse.org/releases/luna/ by selecting it in the Locations section and then clicking Remove
  • Add a new Software Site by clicking Add… in the Locations section
    • Select Software Site
    • Software Site for the e(fx)clipse 1.2.0 release build
      http://download.eclipse.org/efxclipse/runtime-released/1.2.0/site
    • Expand FX Target and check Target Platform Feature
      Note: RCP e4 Target Platform Feature only contains fx plugins for helping adding JavaFX content to e4-SWT-RCP applications
    • Uncheck Include required software
    • Click Finish 
  • Activate the target platform by clicking Set as Target Platform in the upper right corner of the Target Definition Editor

Efxclipse_basic_target_definition

Step 2: Migrate the Application Project

  • Update the application model
    The application model itself is UI toolkit independent. But the binding addons are not. Therefore the default SWT binding addons need to be replaced with the corresponding JavaFX counterpart.

    • Open the file Application.e4xmi in the project de.codecentric.eclipse.tutorial.app
    • Expand the Addons tree node under Application
    • Remove the addon org.eclipse.e4.ui.bindings.service
      (org.eclipse.e4.ui.bindings.BindingServiceAddon)
    • Add a new addon
      • ID: org.eclipse.fx.ui.keybindings.e4.service
      • Class:
        bundleclass://org.eclipse.fx.ui.keybindings.e4/org.eclipse.fx.ui.keybindings.e4.BindingServiceAddon
    • Remove the addon org.eclipse.e4.ui.workbench.bindings.model
      (org.eclipse.e4.ui.workbench.swt.util.BindingProcessingAddon)
    • Add a new addon
      • ID: org.eclipse.fx.ui.keybindings.e4.model
      • Class:
        bundleclass://org.eclipse.fx.ui.keybindings.e4/org.eclipse.fx.ui.keybindings.e4.BindingProcessingAddon
  • Update the bundle dependencies
    • Open the file MANIFEST.MF in the project de.codecentric.eclipse.tutorial.app
    • Switch to the Dependencies tab
    • Remove the following bundles from the Required Plug-ins
      • javax.inject
      • org.eclipse.core.runtime
      • org.eclipse.swt
      • org.eclipse.jface
    • Add the following bundles to the Required Plug-ins
      • org.eclipse.fx.ui.workbench.fx
      • org.eclipse.fx.ui.theme
      • org.eclipse.fx.ui.di
      • org.eclipse.fx.ui.services
      • org.eclipse.e4.core.services
      • org.eclipse.e4.core.di.extensions
    • Ensure the following bundles are set
      • org.eclipse.e4.ui.model.workbench
      • org.eclipse.e4.core.di
      • org.eclipse.e4.ui.di
      • org.eclipse.e4.core.contexts
      • org.eclipse.e4.ui.workbench
      • org.eclipse.e4.ui.services
    • Add the following packages to the Imported Packages
      • javax.annotation (1.0.0)
      • javax.inject (1.0.0)
      • javafx.*
        Efxclipse_basic_app_dependencies
  • Update the extension points
    • Open the file plugin.xml in the project de.codecentric.eclipse.tutorial.app
    • Switch to the Extensions tab
    • Update product extension point
      • Set application to org.eclipse.fx.ui.workbench.fx.application
    • Add a new property to the product extension point
      • Right click on the product extension point → property
      • name applicationXMI
      • value de.codecentric.eclipse.tutorial.app/Application.e4xmi

Note that e(fx)clipse does only support theme based CSS styling. Therefore the applicationCSS property of the product extension point does not have an effect in e(fx)clipse 1.2.0 and can be removed. The recommended way to configure a theme is via declarative service as explained here. Since there is no CSS styling in the basic recipe, we don’t have to perform any migration actions here.

Step 3: Migrate the Plug-in Project

  • Update the bundle dependencies
    • Open the file MANIFEST.MF in the project de.codecentric.eclipse.tutorial.inverter
    • Switch to the Dependencies tab
    • Remove the following bundles from the Required Plug-ins
      • org.eclipse.swt
      • org.eclipse.jface
    • Add the following packages to the Imported Packages
      • javafx.*

In the basic recipe, the plug-in project is the only place where we directly get in touch with JavaFX. In the part implementation, UI toolkit controls are used to create the visible part of the application. Therefore the InverterPart needs to be re-implemented for the usage of JavaFX.

The first thing to look at are the containers. A JavaFX application basically consists out of two containers, the Stage which is the main/top-level container, and the Scene which is the background container for UI elements, that can be exchanged on the Stage. Within a Scene, UI elements are arranged in a hierarchical scene graph, that typically has a layout pane as the root node.

Using the e(fx)clipse runtime, the Stage and the Scene are managed by the renderer. Within a part we start with a layout pane where the UI elements can be placed on. By default this is javafx.scene.layout.BorderPane. This can be adjusted by setting a tag to the part definition in the application model. The list of available tags can be found in the Eclipse Wiki.

In SWT layouts are managed by creating a layout instance and setting it to a control. In the basic recipe based on SWT the parent org.eclipse.swt.widgets.Composite is injected and a org.eclipse.swt.layout.GridLayout is set it. To show a one-to-one migration, we will also use a grid layout in the JavaFX version. As the layout is specified by the node itself, we use the javafx.scene.layout.GridPane as parent container, were the other controls will be added to. Note that our simple layout could also be achieved using for example a combination of wrapped javafx.scene.layout.VBox and javafx.scene.layout.HBox instances.

  •  Specify javafx.scene.layout.GridPane as the root container of the InverterPart
    • Open the file Application.e4xmi in the project de.codecentric.eclipse.tutorial.app
    • Select the part in the application model
      Application → Windows and Dialogs → Trimmed Window → Controls → PartSashContainer → Part
    • Switch to the Supplementary tab on the right side of the part details
    • Add the tag Container:GridPane via Tags input field
  • Open the InverterPart in the project de.codecentric.eclipse.tutorial.inverter
    • Change the type of the parent parameter for the postConstruct() method from Composite to GridPane
    • Delete the line that sets the GridLayout, since the GridPane is already our layout pane
    • Exchange the control implementations. Use the default constructor for every JavaFX control.
      • org.eclipse.swt.widgets.Label → javafx.scene.control.Label
      • org.eclipse.swt.widgets.Text → javafx.scene.control.TextField
      • org.eclipse.swt.widgets.Button → javafx.scene.control.Button
    • Set the layout constraints for the controls
      • While in SWT the column and row position of a control is determined by the insertion order when it is added to the parent control, you need to specify the column and row position in a JavaFX GridPane explicitly. This can be done by setting a constraint using one of the static GridPane#setContraints() helper methods, or by using one of the GridPane#add() convenience methods for adding a control to a GridPane instance.
      • Another difference in specifying a layout is, that in JavaFX the layout pane knows how to layout the children in detail, while in SWT the layout configuration (e.g. whether a control should grab all the remaining space) is configured on the control itself.
        For example, the following SWT GridData configuration tells that the input control should fill the available space and grab the extra horizontal space when the layout resizes.

        GridDataFactory.fillDefaults().grab(true, false).applyTo(input);

        To achieve the same in JavaFX we create and set a constraint for the input control with the following lines of code.

        GridPane.setConstraints(input, 1, 0);
        GridPane.setHgrow(input, Priority.ALWAYS);
        GridPane.setMargin(input, new Insets(5.0));
      • Specify the action that should be performed on button click
        • In SWT you set listeners to a control to be able to react on an event. Most of these listeners handle several different events in different methods, e.g. a org.eclipse.swt.events.MouseListener has methods to react on mouseDown(), mouseUp() and mouseDoubleClick(). In JavaFX the same is achieved by setting an javafx.event.EventHandler for an explicit event via various setOnXxx() methods. As javafx.event.EventHandler is a functional interface, it has exactly one method for handling an event. This allows the usage of Java lambda expressions for registering an javafx.event.EventHandler.
          In SWT you react on a click on a org.eclipse.swt.widgets.Button by adding a org.eclipse.swt.events.SelectionListener:

          button.addSelectionListener(new SelectionAdapter() {
          	@Override
          	public void widgetSelected(SelectionEvent e) {
          		output.setText(StringInverter.invert(input.getText()));
          	}
          });

          For JavaFX exchange this with the following line of code:

          button.setOnMouseClicked((e) -> 
          	output.setText(StringInverter.invert(input.getText())));
        • Also exchange the org.eclipse.swt.events.KeyListener on the input field with the following javafx.event.EventHandler
          input.setOnKeyPressed(event -> {
          	if (KeyCode.ENTER.equals(event.getCode())) {
          		output.setText(StringInverter.invert(input.getText()));
          	}
          });
      • Add the children to the layout pane
        parent.getChildren().addAll(inputLabel, input, button, outputLabel, output);

The finished part might look similar to the following snippet:

package de.codecentric.eclipse.tutorial.inverter.part;
 
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
 
import javax.annotation.PostConstruct;
 
import de.codecentric.eclipse.tutorial.inverter.helper.StringInverter;
 
public class InverterPart {
 
	@PostConstruct
	public void postConstruct(GridPane parent) {
		Label inputLabel = new Label();
		inputLabel.setText("String to revert:");
		GridPane.setConstraints(inputLabel, 0, 0);
		GridPane.setMargin(inputLabel, new Insets(5.0));
 
		final TextField input = new TextField();
		GridPane.setConstraints(input, 1, 0);
		GridPane.setHgrow(input, Priority.ALWAYS);
		GridPane.setMargin(input, new Insets(5.0));
 
		Button button = new Button();
		button.setText("Revert");
		GridPane.setConstraints(button, 2, 0);
		GridPane.setMargin(button, new Insets(5.0));
 
		Label outputLabel = new Label();
		outputLabel.setText("Inverted String:");
		GridPane.setConstraints(outputLabel, 0, 1);
		GridPane.setMargin(outputLabel, new Insets(5.0));
 
		final Label output = new Label();
		GridPane.setConstraints(output, 1, 1);
		GridPane.setColumnSpan(output, 2);
		GridPane.setHgrow(output, Priority.ALWAYS);
		GridPane.setMargin(output, new Insets(5.0));
 
		button.setOnMouseClicked((e) -> 
			output.setText(StringInverter.invert(input.getText())));
 
		input.setOnKeyPressed(event -> {
			if (KeyCode.ENTER.equals(event.getCode())) {
				output.setText(StringInverter.invert(input.getText()));
			}
		});
 
		// don't forget to add children to gridpane
		parent.getChildren().addAll(
				inputLabel, input, button, outputLabel, output);
	}
}

Step 4: Migrate the Product Configuration

  • Open the file de.codecentric.eclipse.tutorial.app.product in the project de.codecentric.eclipse.tutorial.product
  • Switch to the Overview tab
    • Uncheck The product includes native launcher artifacts
    • Update the Product Definition
    • Application: org.eclipse.fx.ui.workbench.fx.application
  • Switch to the Dependencies tab
    •  Remove
      • org.eclipse.e4.rcp
      • org.eclipse.emf.ecore
      • org.eclipse.emf.common
    • Add
      • org.eclipse.fx.runtime.e4fx.feature
  • Switch to the Launching tab
    • Add -nosplash to the Program Arguments
    • Add -Dosgi.framework.extensions=org.eclipse.fx.osgi to the VM Arguments

Step 5: Taste

  • Start the application from within the IDE
    • Open the Product Configuration in the de.codecentric.eclipse.tutorial.product project
    • Select the Overview tab
    • Click Launch an Eclipse Application in the Testing section
  • Export the application and start the deliverable
    • Open the Product Configuration in the de.codecentric.eclipse.tutorial.product project
    • Select the Overview tab
    • Click Eclipse Product export wizard in the Exporting section
    • Select a directory to export to in the Destination section of the export wizard
    • Leave the other options unchanged
    • Click Finish
    • After the export is done, open the directory the application was exported to and start the application by executing the following command on the command line
      java -jar plugins/org.eclipse.equinox.launcher_$VERSION.jar

In both cases the application should look similar to the following screenshot.

Eclipse4_basic_fx_result

  • Troubleshooting
    • In case the application does not start, showing for example ClassCastExceptions, ensure to clean the workspace and the configuration directory.
      • Run → Run Configurations → Select the run configuration for the application (Eclipse Applicationde.codecentric.eclipse.tutorial.app.product)
      • On the Main tab check Clear in the Workspace Data section
      • On the Configuration tab check Clear the configuration area before launching in the Configuration Area section

Step 6: Delivery

With the above setup, the export will not generate an executable. We have chosen this setup because the preferred way to export a runnable JavaFX application is by using the JavaFX Packager Tool. On Windows and Linux the PDE export in Eclipse also works, so you basically have two options to create an application that can be delivered with an executable:

  • Create an export via PDE export (only Windows and Linux)
    • Open the target definition de.codecentric.eclipse.tutorial.target.target in the project de.codecentric.eclipse.tutorial.target
      • Add a new Software Site
      • Add a new Software Site by clicking Add… in the Locations section
      • Software Site http://download.eclipse.org/releases/luna
      • Disable Group by Category and filter for Eclipse
      • Select Eclipse Platform Launcher Executables
      • Activate the target platform by clicking Set as Target Platform in the upper right corner of the Target Definition Editor
    • Open the file de.codecentric.eclipse.tutorial.app.product in the project de.codecentric.eclipse.tutorial.product
      • Switch to the Overview tab
      • Check The product includes native launcher artifacts
    • Export and verify that the exported product contains an executable that starts the application
  • Create a self contained application package via JavaFX Packager Tool
    • To package the application ensure that you already exported the application like shown in Step 5.
    • To use the JavaFX Packager Tool you need to create a build.xml file. A good starting point is the build.xml created by the e4 JavaFX application wizard
    • Create a temporary e4 JavaFX project
      • File → New → Other → JavaFX → OSGI → e4 Application projects
      • Enter temp as Bundle-ID-Prefix
      • Enter temp as Product Name
      • Ensure Tycho Build Stubs is checked
      • Ensure Native Packaging is checked
    • Create a new release engineering project
      • File → New → Project → General → Project
      • Set name to de.codecentric.eclipse.tutorial.releng
    • Copy the following files from the temporary application releng project to the newly created releng project
      • org.eclipse.fx.ide.ant.jar
      • build.xml
    • Modify the build.xml
      • Change the eclipseapp-dir property to the directory the application was exported to
      • Also update the fileset with the id equinox-launcher to point to that directory
        You can reuse the eclipse-app-dir property
        <fileset id="equinox-launcher" dir="${eclipse-app-dir}">
      • Update the application name in fx:application and fx:info
    • Execute the build
      • Right click on build.xml → Run As → Ant Build
    • Refresh the de.codecentric.eclipse.tutorial.releng project
      • the sub-directory deploy/bundles contains a deliverable application with an executable
    • Delete the temporary created projects

You can also find the finished migration on GitHub.

I’m curious to hear if the migration steps work for you and what further recipes you are interested in or if you faced any issues regarding such a migration. Feel free to contact me via email or comments section.

Comment

Your email address will not be published. Required fields are marked *