Migrate E4 RCP from SWT to JavaFX using SWTonJavaFX

In this blog I would like to demonstrate how to migrate an Eclipse4 RCP application from SWT to JavaFX using SWTonJavaFX and how to deal with some of the problems that might arise in a real world application.

Introduction

The goal of this investigation is to migrate the project as quickly and simply as possible. Most importantly, we only want to make changes to the code where absolutely necessary. This basically means that we don’t want to change any of our SWT code (parts, wizards, dialogs, etc.)! To achieve this we will integrate SWTonJavaFX, so we can keep on using the SWT API and migrate to JavaFX on demand.

I set up a small test project de.emsw.e4.migration to simulate some of the concepts used in our larger E4-SWT applications. This is what it looks like:
SWT screenshot

You can clone the git repository and have a look at the code changes. The SWT code will be in branch master and the JavaFX code will be in the branch migration.

Dirk Fauth has recently published a migration tutorial about the basic steps, which will serve as a basis for this investigation. Please note however, that Dirk uses efxclipse 1.2.0 whereas I am using efxclipse 2.0.0. There are some small but important differences!

Target Platform

I will not go into the setup a new target platform, because Dirk has already described a good way to get there in his article. Dirk uses the url to efxclipse 1.2.0, so you will need to change that. 2.0.0 has not been released at the time of writing, but you can use the nightly build site http://download.eclipse.org/efxclipse/runtime-nightly/site.

E(fx)clipse Runtime

Since I wanted to work with the current sources of e(fx)clipse 2.0.0 (and maybe contribute some code while I am into this investigation) I cloned the e(fx)clipse repo and imported the runtime bundles I needed from bundles/runtime/* into the workspace. At our company, we use a manually managed target platform, so we don’t work directly with the provided e(fx)clipse runtime feature anyway.

Note: Since we are using e(fx)clipse 2.0 we don’t need the bundles org.eclipse.fx.osgi and org.eclipse.fx.javafx anymore. For more information why please refer to Tom Schindls post on the forum.

SWTonJavaFX

The bundles for SWTonJavaFX are also in the e(fx)cipse git repository (experimental/swt/*) and can also be imported into the workspace. You need these projects:

  • org.eclipse.fx.runtime.swt
  • org.eclipse.fx.runtime.swt.compat (== org.eclipse.swt)
  • org.eclipse.fx.runtime.swt.e4

The project org.eclipse.fx.runtime.swt contains the actual code implementing the SWT API with JavaFX. The project to watch out for is org.eclipse.fx.runtime.swt.compat, since this is the project that mimics org.eclipse.swt. So you will only want to have either the original bundle org.eclispe.swt or org.eclipse.fx.runtime.swt.compat in your target platform. The bundle org.eclipse.fx.runtime.swt.e4 will register components supporting the migration from SWT.

A side note concerning org.eclipse.fx.runtime.swt.compat: Since I imported it as a project into the workspace, it will automatically be used instead of the original org.eclispe.swt from the target platform definition. If I want to use the original SWT library I only need to close org.eclipse.fx.runtime.swt.compat. This approach can lead to some difficulties though, so if you want to be 100% safe, you should remove org.eclipse.swt from your target platform.

Basic Migration

Now that we have all the libraries we need, we can migrate the actual project. Dirks migration tutorial is a big help, so I will only summarize the effects here. You can also look at the git commit to see all the changes.

Product Definition

The first thing we need to do is change the application entry point in the plugin.xml by changing the attribute application of <product> to:

org.eclipse.fx.ui.workbench.fx.application

(While you are here you can drop the property applicationCSS. This will not be evaluated by efxclipse.)

The application entry point must be changed accordingly in the product configuration (e4migration.product in my test project):

org.eclipse.fx.ui.workbench.fx.application

Bundles

I used a plug-in based product configuration for this demo project, so it is easy to see which bundles need to be replaced.

What we don’t need any more from E4-SWT:

org.eclipse.e4.ui.bindings
org.eclipse.e4.ui.css.core
org.eclipse.e4.ui.css.swt
org.eclipse.e4.ui.css.swt.theme
org.eclipse.e4.ui.widgets
org.eclipse.e4.ui.workbench.addons.swt
org.eclipse.e4.ui.workbench.renderers.swt
org.eclipse.e4.ui.workbench.swt
org.eclipse.e4.ui.workbench3

Additional bundles from e(fx)clipse (including SWTonJavaFX):

org.eclipse.fx.core
org.eclipse.fx.core.databinding
org.eclipse.fx.core.di
org.eclipse.fx.core.di.context
org.eclipse.fx.core.fxml
org.eclipse.fx.osgi.util
org.eclipse.fx.runtime.swt
org.eclipse.fx.runtime.swt.e4
org.eclipse.fx.ui.animation
org.eclipse.fx.ui.controls
org.eclipse.fx.ui.di
org.eclipse.fx.ui.dialogs
org.eclipse.fx.ui.keybindings
org.eclipse.fx.ui.keybindings.e4
org.eclipse.fx.ui.keybindings.generic
org.eclipse.fx.ui.panes
org.eclipse.fx.ui.services
org.eclipse.fx.ui.theme
org.eclipse.fx.ui.workbench.base
org.eclipse.fx.ui.workbench.fx
org.eclipse.fx.ui.workbench.renderers.base
org.eclipse.fx.ui.workbench.renderers.fx
org.eclipse.fx.ui.workbench.services

Because we are using SWTonJavaFX we also don’t need the swt platform fragments, so in my test project for windows I could also get rid of org.eclipse.swt.win32.win32.x86. Remember that we are keeping org.eclipse.swt though, because it is mimicked by org.eclipse.fx.runtime.swt.compat.

At the time of writing, we can also get rid of some dependencies that came with E4-SWT like org.apache.batik.* and org.w3c.*. The only 3rd party dependency that comes with e(fx)clipse at the moment is org.apache.commons.lang.

Launch Configuration

To use JavaFX, you need to tell Equinox to use the extension class loader by adding

-Dorg.osgi.framework.bundle.parent=ext

to the vm arguments. (Note that this differs from efxclipse 1.x where it used to be osgi.framework.extensions=org.eclipse.fx.osgi.)

This is a very important step, otherwise you will see errors about equinox not being able to load javafx.* classes:

java.lang.NoClassDefFoundError: javafx/application/Application
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
    at org.eclipse.osgi.internal.loader.ModuleClassLoader.defineClass(ModuleClassLoader.java:272)
    ...
Caused by: java.lang.ClassNotFoundException: javafx.application.Application cannot be found by org.eclipse.fx.ui.workbench.fx_2.0.0.qualifier
    at org.eclipse.osgi.internal.loader.BundleLoader.findClassInternal(BundleLoader.java:423)

MANIFEST.MF

In my case I didn’t have to make any changes to the MANIFEST.MF at this stage. This is due to the fact that I have only defined plug-in dependencies that are actually needed by the compiler. Everything else you need is added directly to the feature definition (or product definition, like in the test project).

If you are working on your own migration you need to check this and throw out any of the replaced e4 bundles mentioned above.

Note that, with e(fx)clipse 2.0, you should not add javafx.* to the imported packages, as this will be taken care of by using the extension class loader.

Application Model

To migrate the application model (TestApp.e4xmi in my test project) we follow Dirks migration tutorial and swap the two add-ons

  1. org.eclipse.e4.ui.bindings.BindingServiceAddon
  2. org.eclipse.e4.ui.workbench.swt.util.BindingProcessingAddon

with

  1. org.eclipse.fx.ui.keybindings.e4.BindingServiceAddon
  2. org.eclipse.fx.ui.keybindings.e4.BindingProcessingAddon

Frequently Encountered Problems

In this chapter I will explain some other small changes that you might need to make to your code, depending on whether or not you used certain features.

Injection of the active Shell

When it comes to dependency injection org.eclipse.fx.runtime.swt.e4 is your friend. It registers context functions which will supply a Composite or a Display instance if requested. So most of the DI stuff already works as you would expect.

One exception is IServiceConstants.ACTIVE_SHELL. We use this key in our E4-SWT projects all the time in handlers when we want to open a dialog or wizard. To demonstrate, here is the simplified handler in my test project:

public class DialogHandler {
    @Execute
    public void execute(@Named(IServiceConstants.ACTIVE_SHELL) Shell shell) {
        SampleTitleAreaDialog dialog = new SampleTitleAreaDialog(shell);
        dialog.open();
    }
}

The problem here is that IServiceConstants.ACTIVE_SHELL is (despite its SWT-like naming) meant to be independent of the ui framework being used. The DI container will inject an object of the type that the ui framework considers to be its equivalent to Shell. Using JavaFX this will be a Stage.

The consequence of this is that our handler will compile correctly, but will never execute, because there is no Shell available to inject, only a Stage. What we want to have is a Shell wrapper around this Stage, so we can pass it as a parent in our constructor. To get this we need to change the key from IServiceConstants.ACTIVE_SHELL to SWTServiceConstants.ACTIVE_SHELL (you will need a dependency on org.eclipse.fx.runtime.swt.e4 to use this). A context function in will take care of supplying a wrapper Shell around the current Stage.

The migrated version of the handler looks almost the same:

public class DialogHandler {
    @Execute
    public void execute(@Named(SWTServiceConstants.ACTIVE_SHELL) Shell shell) {
        SampleTitleAreaDialog dialog = new SampleTitleAreaDialog(shell);
        dialog.open();
    }
}

Modifying a Widget

There might be some cases where you are doing something to an SWT widget after is has been created by the renderer. A simple example of this is the MaximizeAddon in my test project. The add-on registers an EventHandler which waits until the first MWindow has been created and then gets calls getWidget() on it to retrieve the Shell:

MWindow windowModel = (MWindow) objElement;
Shell theShell = (Shell) windowModel.getWidget();
if (theShell == null)
    return;
theShell.setMaximized(true);

Although this will compile, we will get a ClassCastException at runtime. The simple reason being that the renderer doesn’t return a Shell anymore.

Since we are down to the internals of the renderer, we need to change this code. The efxclipse renderer adds another level of abstraction (WWindow), so we need to perform two steps until we reach the Stage:

MWindow windowModel = (MWindow) objElement;
WWindow<?> windowImpl = (WWindow<?>) windowModel.getWidget();
Stage theStage = (Stage) windowImpl.getWidget();
if (theStage == null)
    return;
theStage.setMaximized(true);

You will need to add a dependency on org.eclipse.fx.ui.workbench.renderers.base to get this to compile.

Menu Mnemonics

A small difference between SWT und JavaFX is how you define a mnemonic key for menus and menu items. Where SWT uses the “&” character JavaFX expects you to use the underscore “_” instead. Hence, if you use “&” to set mnemonics with JavaFX, you will see the “&” turn up in the labels of your menu and the mnemonic will not work.

The direct solution here is to change the labels directly in the e4xmi or properties file where your menu labels are defined. If you want to have a fast hook to change the labels at runtime you could supply your own TranslationService on top of the BundleTranslationProvider. I used the life cycle handler to push my custom implementation into the application context:

@ProcessAdditions
public void processAdditions(MApplication application) {
    TranslationService translationProvider = new BundleTranslationProvider() {
        @Override
        public String translate(String key, String contributorURI) {
            String result = super.translate(key, contributorURI);
            return result.replace('&', '_');
        }
    };
    application.getContext().set(TranslationService.class, translationProvider);
}

Toolbars and ToolControls

Toolbars might look “cut off”, especially if you are using a single Toolbar in a Window Trim. The toolbar will be expanded by adding the tag fillspace to the tags of the Toolbar.
2015-03-30_110019

A tool control might also not layout as you would like. This could be because the renderer uses the JavaFX Group as a container by default. You can configure the tool control to use a HBox instead which will behave nicer if resized. This is done by a tag to the ToolControl in the application model: Container:HBox. (Don’t forget to add the fillspace tag too.)

Utilizing e(fx)clipse Features

Now that our application is running, let’s see how we can get more out of e(fx)clipse.

Add a Theme

A common reason to migrate to JavaFX is to have more control over the visual appearance of the application. Efxclipse supports theme based CSS styling. Tom Schindl described the motives for implementing themes with OSGi services in this article.

If you want to view the whole change right away, you can have a look at the commit.

Adding a Theme as a Service

The first thing we need is an implementation of the interface Theme which can be easily done by extending AbstractTheme (you need to add dependencies to the bundles org.eclipse.fx.ui.services and org.eclipse.fx.ui.theme):

public class DefaultTheme extends AbstractTheme {
    public DefaultTheme() {
        super("default", "Default Theme", DefaultTheme.class.getClassLoader().getResource("css/default.css"));
    }
}

This needs to be setup as an OSGi service using a component definition (for example in OSGI-INF/osgi-DefaultTheme-component.xml):

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="de.emsw.e4.migration.theme.default">
   <implementation class="de.emsw.e4.migration.DefaultTheme"/>
   <service>
      <provide interface="org.eclipse.fx.ui.services.theme.Theme"/>
   </service>
   <reference 
     bind="registerStylesheet" 
     cardinality="0..n" 
     interface="org.eclipse.fx.ui.services.theme.Stylesheet" 
     name="Stylesheet" 
     policy="dynamic" 
     unbind="unregisterStylesheet"/>
</scr:component>

Don’t forget to add the new file to the build.properties and to include it in MANIFEST.MF:

Service-Component: OSGI-INF/osgi-DefaultTheme-component.xml

By the way: The applicationCSS property of the product extension point is not evaluated and can be removed, if you havn’t done so already.

Modifying the CSS File

In my case, I already have the file css/default.css (which is loaded by the DefaultTheme we created before) in my project. Until now it contained css for the SWT renderer. Although this is not valid anymore, we can still reuse the file itself, so let’s open it. To edit CSS for JavaFX you should use the e(fx)clipse Css Editor.
2015-04-01_113955
(If you are asked if you want to add the “Xtest nature” to your project: Say “Yes”.)

If there are already contents in your file, you can delete it all and start from scratch. To demonstrate, let’s set up a simple dark theme as described here:

.root {
    -fx-base: rgb(50, 50, 50);
    -fx-background: rgb(50, 50, 50);
    -fx-control-inner-background:  rgb(50, 50, 50);
}

Aborting the Startup

The test application starts up with a login dialog requesting a username and a password. The original SWT code uses `System.exit(0)’ to abort the startup if the user canceled this operation. The code in the life cycle handler basically looks like this:

@PostContextCreate
public void postContextCreate(IEclipseContext context) {
    LoginService loginService = ContextInjectionFactory.make(LoginService.class, context);
    boolean result = loginService.login();
    if (!result) {
        System.exit(0);
    }
}

I have always considered this to be “unkind” to the platform, so I was happy to see that e(fx)clipse offers a “soft” way to abort the startup by returning a value of LifecycleRV from @PostContextCreate:

@PostContextCreate
public LifecycleRV postContextCreate(IEclipseContext context) {
    LoginService loginService = ContextInjectionFactory.make(LoginService.class, context);
    boolean result = loginService.login();
    if (!result) {
        return LifecycleRV.SHUTDOWN;
    }
    return LifecycleRV.CONTINUE;
}

You can also use this to restart the application after an update with LifecycleRV.RESTART or LifecycleRV.RESTART_CLEAR_STATE if you also want to reset the application model. This and some other new features you get with e(fx)clipse are described on the wiki.

Conclusion

So this is what my test application looks like with JavaFX:
JavaFX screenshot

Of course there is still some work to do, but I hope that this information will help you get started with your own migration.

If you run into problems or missing API with SWTonJavaFX please refer to this discussion about the current status of SWTonJavaFX on the e(fx)clipse forum.

If you have any questions please feel free to leave a comment.