Virtual containers in tree viewers using EMF.Edit

Maybe you already stumbled upon the problem to display model instances with hundreds or even thousands of child elements in a tree based viewer. Displaying all that children as direct successors of their parent (shown in the figure on the left) is confusing and slows down the UI.

An alternative approach is to hook in virtual folders, that contain a subset of the parent’s child elements instead. This approach is shown in the figure on the right and might be familiar to you since it’s also used in the Eclipse ‘Variables’ view.

If you use EMF.Edit based item providers to display your domain objects read on!

The technique shown here is a slightly modified version of the technique explained in chapter 19.2.3 Adding Non-Model Intermediary View Objects of the standard Eclipse Modeling Framework book.

The source code below is used to display the right tree viewer in the illustration above. It’s usual EMF.Edit programming. The only difference is the usage of a PartitionedContentProvider instead of an AdapterFactoryContentProvider.

You find the implementation of PartitionedContentProvider here. It’s packaged into a plugin but feel free to copy that single class into your own project. Overwrite the method getVirtualFolderSize() to limit the maximum number of container’s direct children. If the number exceeds this limit, virtual folders are hooked in instead. If the virtual folder size is -1 virtual folders are not created.


final AdapterFactory adapterFactory = new ComposedAdapterFactory(ComposedAdapterFactory.Descriptor.Registry.INSTANCE);
domain = new AdapterFactoryEditingDomain(adapterFactory, new BasicCommandStack());

viewer = new TreeViewer(parent, SWT.FLAT | SWT.MULTI);

viewer.addDragSupport(DND.DROP_MOVE, new Transfer[] { LocalTransfer.getInstance() }, new ViewerDragAdapter(viewer));
viewer.addDropSupport(DND.DROP_MOVE, new Transfer[] { LocalTransfer.getInstance() }, new EditingDomainViewerDropAdapter(domain, viewer));
viewer.setLabelProvider(new AdapterFactoryLabelProvider(adapterFactory));

// we limit the maximal child elements for containers to 100
viewer.setContentProvider(new PartitionedContentProvider(adapterFactory) {

       @Override
       protected int getVirtualFolderSize(Object folder) {
          return folder instanceof Container ? 100 : DEFAULT_FOLDER_SIZE;
       }
});


Adding Maven artifacts to your target platform

Don’t get me wrong I really like Maven. It’s a great and technically mature build system. Nevertheless I try to avoid it in all my RCP and OSGi projects because all that POM declarations are redundant and unnecessary in some way. Inter bundle dependencies are expressed by means of OSGi manifests and the build process provided by the Eclipse environment respectively the headless PDE build is more than adequate. Obviously this is just a matter of taste and if you use Tycho or other IDEs with the better Maven support, maybe you will not agree.

Anyway there are many existing OSGi bundles (e.g. Jetty, Vaadin, log4j, Knopflerfish) available in Maven Central and manually downloading them together with all their dependencies is a cumbersome task.

So during my experiments with the embedded XULRunner, I tried to find a mechanism that automatically adds bundles from Maven repositories to my active target platform. If you are not familiar with the concept of Eclipse target platforms read this tutorial first.

This is my best solution so far. If you have better ideas please let me know!

Create a simple project named target_platform in your workspace. In the new project create a sub folder named maven_libs. Later this sub folder is the location of all jar files fetched by Maven. Select New->Other->Target Definition and add the maven_libs folder to the list of required locations.

The path ${project_loc:/target_platform}/maven_libs works fine even if the target_platform project is outside the current workspace. Of course you can still add further locations like environment variables, folders or p2 repositories.

Create a pom.xml that lists all required dependencies in your target platform project. Don’t forget to overwrite the default properties of some standard plugins as shown in the code below.


<project>
    <modelVersion>4.0.0</modelVersion>
    <name>Target Platform Project</name>
 
    <groupId>com.javahacks.sample</groupId>
    <artifactId>target_platform</artifactId>
    <version>1.0</version>  
 
    <dependencies>
        <!--Configure dependencies here -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>       
        </dependency>
               ...
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>2.8</version>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>initialize</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>maven_libs</outputDirectory>
                            <overWriteReleases>false</overWriteReleases>
                            <overWriteSnapshots>false</overWriteSnapshots>
                            <overWriteIfNewer>true</overWriteIfNewer>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
 
            <plugin>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.4</version>
                <executions>
                <execution><id>default-jar</id><phase>none</phase></execution>
                </executions>
            </plugin>
 
            <plugin>
                <artifactId>maven-install-plugin</artifactId>
                <version>2.4</version>
                <configuration><skip>true</skip></configuration>
            </plugin>
 
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.12.4</version>
                <configuration><skip>true</skip></configuration>
            </plugin>
 
        </plugins>
 
    </build>
   
</project>

Now run mvn install from within Eclipse or on the command line inside the ‘target_platform’ location to fetch all libraries configured in the POM configuration.

Reopen the target definition and select Set as Target Platform. If you run Maven outside your IDE refresh the target platform project first.

This approach is also useful for the POM dependency resolution explained here. Now you have the same bundle resolution at the build and development time and running the Maven installation to setup the target platform on your build server (without Tycho) is no problem.


Embedded HTML 5 browser in Java

Today HTML 5 is one of the most sophisticated and popular UI technology available. Unfortunately writing business code using Javascript is a mess. If you are used to Java’s type safety, the modularity of OSGi and all that dozens of third party libraries, you know that plain Javascript is a huge step backwards. The Java Virtual Machine and HTML complement each other for many years in web based applications. So why not combine both technologies to create rich desktop applications? A platform independent user interface on top of a platform independent programming language – a perfect match!

I was very excited when I heard about the HTML 5 support in JavaFX. However the ‘Webview’ rendering is very bad and not comparable to most recent web browsers. Fortunately the browser support in SWT was improved with Eclipse Luna and embedding XULRunner 24.x is now possible.

XULRunner is the primary HTML rendering engine used by Firefox and is available on all major operating systems. The WebGL support in XULRunner is great, audio and video playback are implemented. Also the rendering result on different platforms is almost identical so you don’t have to fight with all that different browser quirks as usual in web development.

The demo below shows an SWT based application with embedded XULRunner that renders a simple WebGL scene using three.js.

The SWT browser API supports calling Java from Javascript and vice versa. I added the ‘Take Screenshot’ and ‘Add Image’ button just to demonstrate the interaction ability. The screen capture itself is based on the solution I posted a while ago . The source code for the demo is available here.

After you can not rely on a proper XULRunner installed on your user’s target machine, bundle your application together with the appropriate XULRunner version. For OSGi based applications this is really straightforward and an exemplary implementation can be found here (Sorry I couldn’t integrate the OSX version yet).

The org.mozilla.xulrunner bundle is the platform independent host bundle with helper classes to instantiate XULRunner based browsers. The XULRunner binaries are provided by platform specific fragment bundles, one fragment for each operating system characteristic.

 

XULRunner is not supported for GTK3 so under Linux you have to use a start script to run your application:

 
#!/bin/bash
export SWT_GTK3=0
./yourApp

At the moment I’m playing with different approaches to combine Java and HTML. Single page apps with Java calls using AngularJS and Bootstrap, server centric solution using Vaadin and RAP. So far I have absolutely no idea what works best but I let you know in the near future on this blog.


Integrate Knopflerfish desktop into Equinox

If you are familiar with OSGi development you should already know Knopflerfish an open source OSGi Service Platform. Knopflerfish is an implementation of the OSGi R5 specifications and includes a set of Knopflerfish specific bundles and utilities. An overview of the distribution’s bundles is given here.

The most noteworthy of that additional bundles is the Knopflerfish OSGi Desktop. The desktop is a GUI application that gives a graphical overview of the running OSGi instance. From within the desktop bundles can be started, stopped updated or installed. Additional bundle and service detail information is shown.

The desktop is based on two standard OSGi bundles using Swing without any further dependencies. So it can easily be integrated in Equinox based projects (e.g. RCP applications). The Maven repository and coordinates for that bundles are:


     <repository><url>http://www.knopflerfish.org/maven2/</url></repository>

     <groupId>org.knopflerfish.bundle</groupId>
     <artifactId>desktop</artifactId>
     <version>5.0.1</version>

     <groupId>org.knopflerfish.bundle</groupId>
     <artifactId>desktop-API</artifactId>
     <version>5.0.1</version>

To show the desktop start the org.knopflerfish.bundle.desktop bundle either via autostart in your launch configuration or programmatically at runtime:


    Bundle bundle = Platform.getBundle("org.knopflerfish.bundle.desktop");
    if (bundle.getState() == Bundle.RESOLVED){
        bundle.start(Bundle.START_TRANSIENT);
    }

EMF.Edit Performance

Whenever you are using EMF generated models in rich client applications don’t forget to generate appropriate EMF.Edit item providers for all your domain objects. Even if you are using other UI frameworks than SWT, this will definitely save a lot of time. Especially if your user interface has to react on complex model changes. Complex changes are bulked collection modifications that might trigger refresh operations in table or tree based viewers.

If you are not familiar with the basic concepts of EMF and EMF.Edit the What every Eclipse developer should know about EMF blog post is a good starting point.

As you can see in the illustration below, the AdapterFactoryContentProvider reacts on model changes (actually notified by the wrapped AdapterFactory and a specific ModelItemProvider) and refreshes its associated viewer instance.

 

This works pretty fine in most cases. However if your underlying domain model is very ‘nervous’ with dozens of model updates per second (e.g. stock prices, simulation models, sensor data) frequent refresh operations slow down the user interface. In this case you should try to use a delayed content provider illustrated here:

 

 

Instead of performing dozens of smaller viewer updates it could be better to await a couple of model notifications and perform a full refresh operations afterwards.

The snippet shows an exemplary implementation that I currently use in a customers monitoring application for high frequent simulation models. The provider implementation itself is time based. If the first notification arrives, a full viewer refresh is performed after a short delay (Display.timerExec(..)). Notifications arriving in the meantime can be ignored due to the subsequent full viewer refresh. Delays around 500ms feel a little lazy but the UI reacts still as smooth as expected.

 


import java.util.concurrent.atomic.AtomicBoolean;

import org.eclipse.emf.common.notify.AdapterFactory;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.edit.ui.provider.AdapterFactoryContentProvider;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.widgets.Display;

/**
 * * Use this {@link AdapterFactoryContentProvider} if the viewer's underlying
 * domain model frequently changes and would produce many notification events.
 * In this case it's much more efficient to trigger a full refresh after a short
 * delay.
 */
public class DelayedAdapterFactoryContentProvider extends
		AdapterFactoryContentProvider {

	private static final int DELAY_IN_MS = 500;
	private final AtomicBoolean refresh = new AtomicBoolean(false);
	private Viewer viewer;

	public DelayedAdapterFactoryContentProvider(AdapterFactory adapterFactory) {
		super(adapterFactory);
	}

	@Override
	public void notifyChanged(Notification notification) {

		if (!notification.isTouch() & !refresh.getAndSet(true)) {

			Display.getDefault().asyncExec(new Runnable() {
				@Override
				public void run() {

					Display.getDefault().timerExec(DELAY_IN_MS, new Runnable() {
						@Override
						public void run() {
							refresh.set(false);

							if (viewer.getControl() != null
									&& !viewer.getControl().isDisposed()) {
								viewer.refresh();
							}
						}
					});
				}
			});

		}
	}

	@Override
	public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
		this.viewer = viewer;
		super.inputChanged(viewer, oldInput, newInput);
	}
}


Screen capture with Java and SWT

Creating screenshots with Java is really straightforward. The more complicated part is to implement a convenient way to select a rectangular area on the screen. The snippet below shows how to implement a rectangular clipping selector using SWT and the Tracker widget.

 


import org.eclipse.swt.SWT;
import org.eclipse.swt.events.*;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.widgets.*;

public class ClippingSelector {

    public Rectangle select() {

        final Display display = Display.getDefault();

        //convert desktop to image
        final Image backgroundImage = new Image(display, display.getBounds().width, display.getBounds().height);
        GC gc = new GC(display);
        gc.copyArea(backgroundImage, display.getBounds().x, display.getBounds().y);
        gc.dispose();

        //invisible shell and parent for tracker
        final Shell shell = new Shell(display.getActiveShell(), SWT.NO_BACKGROUND | SWT.ON_TOP);
        shell.setCursor(new Cursor(Display.getCurrent(), SWT.CURSOR_CROSS));
        shell.setBounds(display.getBounds());

        final Rectangle result = new Rectangle(0, 0, 0, 0);

        shell.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseDown(MouseEvent e) {

                Tracker tracker = new Tracker(shell, SWT.RESIZE);
                tracker.setStippled(true);
                tracker.setRectangles(new Rectangle[] { new Rectangle(e.x, e.y, 0, 0) });
                tracker.open();

                Rectangle selection = tracker.getRectangles()[0];

                result.width = selection.width;
                result.height = selection.height;

                result.x = shell.toDisplay(selection.x, selection.y).x;
                result.y = shell.toDisplay(selection.x, selection.y).y;

                shell.dispose();

            }
        });

        shell.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                shell.dispose(); //any key pressed close shell
            }
        });

        shell.addShellListener(new ShellAdapter() {
            @Override
            public void shellDeactivated(ShellEvent e) {
                shell.dispose(); //close shell if another shell is activated
            }

        });

        shell.addPaintListener(new PaintListener() {
            @Override
            public void paintControl(PaintEvent e) {
                e.gc.drawImage(backgroundImage, -1, -1); //paint background image on invisible shell
            }
        });

        shell.open();

        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }

        backgroundImage.dispose();
        shell.dispose();

        return result;

    }

    public static void main(String[] args) {

        final Display display = new Display();
        final Shell shell = new Shell(display);

        final Rectangle rect = new ClippingSelector().select();

        if (rect.height == 0 || rect.width == 0) return;

        //we show the selected area in a new shell. Just for demonstration!
        final Image image = new Image(display, rect);
        final GC gc = new GC(display);
        gc.copyArea(image, rect.x, rect.y);
        gc.dispose();

        shell.setBounds(rect);
        shell.addPaintListener(new PaintListener() {

            @Override
            public void paintControl(PaintEvent e) {
                e.gc.drawImage(image, 0, 0);
            }
        });

        shell.open();
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) display.sleep();
        }
        display.dispose();

    }
}

Follow

Get every new post delivered to your Inbox.