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:
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
org.eclipse.e4.ui.bindings.BindingServiceAddon
org.eclipse.e4.ui.workbench.swt.util.BindingProcessingAddon
with
org.eclipse.fx.ui.keybindings.e4.BindingServiceAddon
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 to IServiceConstants.ACTIVE_SHELL
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
.
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.
(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:
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.