How to Create a Custom Widget for RAP?

Just like in SWT, you can also create custom widgets to extend the RAP widget set to your needs. Examples range from simple Web2.0 mashups to complex widgets such as custom tab folders, or widgets that display graphs and charts.

There are different types of custom widgets which will be described below. But no matter which type of custom widget you implement, you will end up with an SWT widget that has an SWT-like API and inherits some methods from an SWT super class. Therefore you should make yourself familiar with some important rules of SWT before you get started. We highly recommend to read this article by the creators of SWT:

Different types of custom widgets

These types differ a lot with regard to the features they support, the dependency on a certain platform, and also the effort it takes to create them.

Compound widgets

These are the simplest ones. Compound widgets are compositions of existing SWT widgets. These widgets have to extend Composite. There is no difference between SWT and RAP for compound widgets. Everything that is said about compound widgets in [1] also applies to RAP.

Writing a compound widget does not require any JavaScript knowledge. The widgets are re-usable in desktop applications with SWT.

Self-drawing widgets

These are also simple. Sometimes you might want to need a widget that draws itself. This can be done by extending Canvas. The canvas widget supports drawing in a paint listener. For writing this kind of widgets, you can also follow [1]. Please note that the drawing capabilities of RAP are limited compared to SWT. Especially in Internet Explorer browsers, the performance degrades with the number of drawing operations.

Writing a canvas widget does not require any JavaScript knowledge. The widgets are re-usable in desktop applications with SWT.

Browser-based widgets

These are still rather simple. The SWT Browser widget lets you place any HTML into your application. In RAP, this is no different. Any JavaScript code placed in a Browser widget will end up in an IFrame element. This makes it easy to wrap Web2.0 mashups or include existing components from other JavaScript libraries. Using the SWT BrowserFunction API, you can even send events from the JavaScript code that runs in the browser to the Java code of your widget.

Remember not to subclass Browser but extend Composite instead and wrap the Browser widget. This way, you do not expose the API of the Browser to user of your widget, but instead provide an API that is specific for your widget.

A good example of a “mashup” custom widget for Google Maps can be found in [2]. Another example [3] shows how to include a JQuery UI widget into a browser-based custom widget.

Real native widgets

Just like in SWT, writing real native custom widgets is hard. You need deep knowledge of both RAP and the platform you are developing for, i.e. the browser and any libraries you use. You have to know the JavaScript library of RAP, the communication between client and server, the request lifecycle, and life cycle adapters. Luckily, you get around writing native custom widgets most of the time by using one of the above solutions. Future versions of RAP will simplify the writing of native custom widgets.

Native custom widgets are not re-usable on the desktop unless a specific desktop version is being developed.

Creating native custom widgets

A RAP widget consists of a client part and a server part. Both parts have to be synchronized. There are different tasks to create a new custom widget which will be covered here:

As an exemplary widget we will implement a native Google maps widget. You can find the example code for this widget in the RAP CVS:

dev.eclipse.org/cvsroot/rt/org.eclipse.rap/sandbox/org.eclipse.rap.demo.gmaps

Note: this example is here for historical reasons, today you could do the same using the Browser widget as shown in [2].

Creating the widget

To have a clear line between your application and your widgets we'll create a new plug-in project called org.eclipse.rap.demo.gmaps. We don't need any additional aspects like an RCP application or an activator.

New Plugin Project wizard

The first step is to create a java class - the widget itself - to talk to the developer like every other widget. In our case we will call it GMap.java in place it into the org.eclipse.rap.gmaps package. As super class we use org.eclipse.swt.widgets.Composite to have to have a proper base class which interacts with the rest of SWT. To store the address which will be shown on the map, we create a new field in the GMap class called address and will generate the corresponding setter and getter. The setter - setAdress( String ) - should check for null values and replacing null with an empty string instead. It should look like this in the end:


  public void setAddress( String address ) {
    if( address == null ) {
      this.address = "";
    } else {
      this.address = address;
    }
  }
  

To have an example for the other way around - from client to server - we introduce another field called centerLocation to get the current location of the map when the user moves it around. This is done by adding a new field to the class together with its getter and setter:


  private String centerLocation;

  public void setCenterLocation( String location ) {
    this.centerLocation = location;
  }

  public String getCenterLocation() {
    return this.centerLocation;
  }
  

Additionally we override the setLayout(Layout) method of Composite with an empty method as our custom widget does not contain any other widgets. That's all for now - this is all needed by the developers who want to use our the custom widget. Now it gets interesting as we have to work out the JavaScript side of our widget.

Client-side implementation

On the client side our widget is just a JavaScript class defined with the qooxdoo syntax. As super class we need to use at least qx.ui.core.Widget. To have an easier life we will directly use one of the qooxdoo layout managers as base of our widget. The code for the new qooxdoo class together with qooxdoo's CanvasLayout as super class will look like this:


  qx.Class.define( "org.eclipse.rap.gmaps.GMap", {
  extend: qx.ui.layout.CanvasLayout,
      // ...
  } );
  

The first thing we need to create is the constructor for that class in order to initialize our widget properly. As parameter we have an id which will be passed to the widget by us in a later step. The first line is a base call which is the same as super in a java environment. For now we populate the id to the browsers DOM by adding a new HTML attribute with setHtmlAttribute. You see some Google Maps-specific calls here which are just there to initialize the Google Maps subsystem. See the Google Maps API Documentation for more information. The two event listeners for the qooxdoo widget will take care for the size calculations. This means that whenever the server sets a new size for the widget we care about our widget to lay out everything inside (in this case the map) correctly. The called method _doResize will be implemented later.


  qx.Class.define( "org.eclipse.rap.gmaps.GMap", {
  extend: qx.ui.layout.CanvasLayout,
  
    construct: function( id ) {
      this.base( arguments );
      this.setHtmlAttribute( "id", id );
      this._id = id;
      this._map = null;
      if( GBrowserIsCompatible() ) {
        this._geocoder = new GClientGeocoder();
        this.addEventListener( "changeHeight", this._doResize, this );
        this.addEventListener( "changeWidth", this._doResize, this );
      }
    }
  
  } );
  

To save the address of the widget somewhere we use the property system of qooxdoo. Adding a new property will look like this:


  qx.Class.define( "org.eclipse.rap.gmaps.GMap", {
  extend: qx.ui.layout.CanvasLayout,
      construct: function( id ) {
        // ...
      },  // <-- comma added, see JavaScript syntax reference

      properties : {
        address : {
          init : "",
          apply : "load"
        }
      }
      
    }
  } );
  

Qooxdoo will automatically generate the corresponding setter and getter at runtime for us. So if we want to read the current address of our client-side widget we just need to call getAdress on a our client-side GMap object. As you see in the apply attribute of your address property the method load will be called when the value of the property changes. So let's implement it:


  qx.Class.define( "org.eclipse.rap.gmaps.GMap", {
  extend: qx.ui.layout.CanvasLayout,
      construct: function( id ) {
        // ...
      },

      properties : {
        // ...
      },  // <-- comma added, see JavaScript syntax reference

      members : {
        _doActivate : function() {
            var shell = null;
            var parent = this.getParent();
            while( shell == null && parent != null ) {
                if( parent.classname == "org.eclipse.swt.widgets.Shell" ) {
                    shell = parent;
                }
                parent = parent.getParent();
            }
            if( shell != null ) {
                shell.setActiveChild( this );
            }
        },

        load : function() {
            var current = this.getAddress();
            if( GBrowserIsCompatible() && current != null && current != "" ) {
                qx.ui.core.Widget.flushGlobalQueues();
                if( this._map == null ) {
                    this._map = new GMap2( document.getElementById( this._id ) );
                    this._map.addControl( new GSmallMapControl() );
                    this._map.addControl( new GMapTypeControl() );
                    GEvent.bind( this._map, "click", this, this._doActivate );
                    GEvent.bind( this._map, "moveend", this, this._onMapMove );

                }
                var map = this._map;
                map.clearOverlays();
                this._geocoder.getLatLng(
                current,
                function( point ) {
                    if( !point ) {
                        alert( "'" + current + "' not found" );
                        } else {
                        map.setCenter( point, 13 );
                        var marker = new GMarker( point );
                        map.addOverlay( marker );
                        marker.openInfoWindowHtml( current );
                    }
                }
                );
            }
        },

        _onMapMove : function() {
            if( !org_eclipse_rap_rwt_EventUtil_suspend ) {
                var wm = org.eclipse.swt.WidgetManager.getInstance();
                var gmapId = wm.findIdByWidget( this );
                var center = this._map.getCenter().toString();
                var req = org.eclipse.swt.Request.getInstance();
                req.addParameter( gmapId + ".centerLocation", center );
            }
        },

        _doResize : function() {
            qx.ui.core.Widget.flushGlobalQueues();
            if( this._map != null ) {
                this._map.checkResize();
            }
        }
      }
      
  } );
  

There is a little hack involved which is easily explained. We need to listen to click events in the map and connect them with our _doActivate method to activate the current shell. This is needed because Google Maps API is implemented with an IFrame and current browser generations send events only to their document. The IFrame is handled as a separate document and thus we cannot catch the events to let the shell be activated with the standard mechanism. This is obsolete for custom widgets without IFrames.

The other event listener for the moveend event will trigger a function to send the current location of the map to the server. Therefore we need to get the Id - which is allocated by RAP - of the current widget and obtained via the WidgetManager on the client side. Then we use the current Request object to add a new parameter which will be processed later at the server side. If you want to immediately send the parameter to the server use req.send(). But as we don't need this for now we just add the parameter to the request object and it will be transfered automatically to the server with the next request.

Ok, we're almost done with our client-side implementation. But the key part of the whole widget is still missing: the piece between the server and the client.

Note: Although the RAP client is derived from qooxdoo 0.7, it does not contain the same classes as the original qooxdoo 0.7 library. It was heavily stripped down and adapted. Therefore you should not rely on the qooxdoo documentation and should avoid using other classes from qooxdoo than Terminator, CanvasLayout, the event system and the RWT classes required for communicating with the server. Future versions of RAP will provide a stable API for custom widget development.

Filling the gap between client and server

In our current situation we have already done two important tasks: the server- and the client-side widget. Now we need to connect each other in order to control the widget on the client by calling it on the server (where our application lives). In RAP - more precisely in RWT - we have a concept called the life cycle. With each request from the client the life cycle on the server side will be executed. It is responsible to process all changes and events from the client and in the end it will send back a response to the client what to do (mostly hide/show widgets, update data, etc). The life cycle itself is split up in several phases which are executed in a specific order.

Phase Description
ReadData This is responsible to read the values sent from the client like occurred events. At the end of this phase, all widget attributes are in sync again with the values on the client. The attributes are preserved for later use.
ProcessAction ProcessAction is responsible for dispatching the events to the widgets. Attributes may change in this phase as a response for the events.
Render At the end of the lifecycle every change will be rendered to the client.
Be aware that only values which are different than there preserved ones are send to the client (means: only the delta).

To participate in the life cycle - what is what we want to do with our custom widget - we need to provide a Life Cycle Adapter (LCA). There are two ways to connect the LCA with our widget. On the one hand side we can return it directly when someone asks our widget with getAdapter to provide an LCA. This can be done by implementing the getAdapter method in our own widget like this:


  ...
  public Object getAdapter( Class adapter ) {
    Object result;
    if( adapter == ILifeCycleAdapter.class ) {
      result = new MyCustomWidgetLCA(); // extends AbstractWidgetLCA
    } else {
      result = super.getAdapter( adapter );
    }
    return result;
  }
  ...
  

The preferred way is to let RAP do this itself by creating the LCA class in a package called <widgetpackage>.internal.<widgetname>kit.<widgetname>LCA. The order of the internal does not play the big role. The important thing is to have internal in the package name, the package with the LCA is called <widgetname>kit and the LCA itself is called <widgetname>LCA.
Here a little example:
If our widget is named org.eclipse.rap.gmaps.GMap,
then our LCA should be named org.eclipse.rap.internal.gmaps.gmapkit.GMapLCA.
The LCA class itself must have org.eclipse.rwt.lifecycle.AbstractWidgetLCA as the super class and implement its abstract methods. We'll just show you a little overview of the methods and their role and then go further to implement a working LCA for our GMaps widget.

Method name Description
renderInitialization(Widget) This method is called to initialize your widget. Normally you will tell RAP which client-side class to use and how it is initialized (think of style bits)
preserveValues(Widget) Here we have to preserve our values so we can see at the end of the lifecycle if something has changes during the process action phase.
renderChanges(Widget) That's the most interesting part. We need to sync the attributes of the widget on the server with the the client-side implementation by sending the changes to the client.
renderDispose(Widget) You can tidy up several things here before the widget gets disposed of on the client.

After having a brief overview of the principles of the Life Cycle Adapter let's start by implementing the LCA for our GMap widget. After creating the LCA class - which extends AbstractWidgetLCA - we will fill the interesting methods with some logic. First comes the initialization.


  public void renderInitialization( Widget widget ) throws IOException {
    JSWriter writer = JSWriter.getWriterFor( widget );
    String id = WidgetUtil.getId( widget );
    writer.newWidget( "org.eclipse.rap.gmaps.GMap", new Object[] { id } );
    writer.set( "overflow", "hidden" );
    ControlLCAUtil.writeStyleFlags( ( GMap )widget );
  }
  

The JSWriter provides methods to generate JavaScript code that updates the client-side state of our widget. Each widget has its own JSWriter instance so that RAP can decide to which widget the call belongs. As you can see in the snippet, JSWriter#newWidget is called to create a new widget on the client side. The second parameters is an array of Objects which are passed to the constructor of the qooxdoo class (see above). With JSWriter#set you can easily set different attributes of your qooxdoo object. Note that there are overloaded methods of set that take care that JavaScript code is only generated if necessary. That means if a certain property was left unchanged during the request processing, no JavaScript code is generated.

The next step is to preserve the relevant properties of our widget. After that the set-methods from JSWriter can determine whether it is necessary to actually generate JavaScript code. As there is only the address property which could change, this is straight forward.


  private static final String PROP_ADDRESS = "address";

  public void preserveValues( Widget widget ) {
    // Preserve properties that are inherited from Control
    ControlLCAUtil.preserveValues( ( Control )widget );

    // Preserve the 'address' property
    IWidgetAdapter adapter = WidgetUtil.getAdapter( widget );
    adapter.preserve( PROP_ADDRESS, ( ( GMap )widget ).getAddress() );

    // Only needed for custom variants (theming)
    WidgetLCAUtil.preserveCustomVariant( widget );
  }
  

First we use ControlLCAUtil#preserveValues to preserve all properties that are inherited by Control (tooltip, tabindex, size, etc.). So we only need to care about the things implemented in our widget. We just request a so-called IWidgetAdapter which is responsible for different aspects in the lifecycle of a widget. In this case we only use it to preserve the address value with a predefined key (PROP_ADRESS) to associate it later.
The last line calling preserveCustomVariant is only added for the sake of completeness. Variants are a way to have different looks of the same widget and is part of the theme engine. Please see the Prepare Custom Widgets for Theming article for further information.

The following step is one of most interesting parts of your lifecycle adapter - the renderChanges method. As said before it is responsible to write every change to the outgoing stream which is executed on the client. Let's take a look at the implementation:


  private static final String JS_PROP_ADDRESS = "address";

  public void renderChanges( Widget widget ) throws IOException {
    GMap gmap = ( GMap )widget;
    ControlLCAUtil.writeChanges( gmap );
    JSWriter writer = JSWriter.getWriterFor( widget );
    writer.set( PROP_ADDRESS, JS_PROP_ADDRESS, gmap.getAddress() );

    // only needed for custom variants (theming)
    WidgetLCAUtil.writeCustomVariant( widget );
  }
  

Again we use the ControlLCAUtil to write the changes which are implemented on Control and thus we should not care what's behind it (for those who really want to know it - it's the same as preserving the values like tooltip, tabindex, size, etc). Like in the widget initialization we have to render something and therefore we need to obtain the corresponding JSWriter instance for our widget. We need to use JSWriter#set to set a specific attribute to the widget instance on the client-side. There are many different set implementations available for every need. The one used here is simple: We pass the key for the preserved value to the method so RAP can check if there has something changed since the last time it was preserved, we pass the name of the client-side attribute which gets transformed into "set*" and last but not least the value for this setter. As we can see, the name of the JavaScript attribute (JS_PROP_ADRESS) will call the method setAdress on the corresponding widget instance on the client. If you wonder where this method is, take a look at the qooxdoo property system. The address property of our widget will be transformed by qooxdoo into a set* and get* methods at runtime.

Now we need to process the actions transfered to the server (at least if there are any). We see in the GMap.js that if the user moves the map a new parameter called centerLocation will be attached to the current request and transfered to the server. To read and process it the readData method of the LCA is used.


  private static final String PARAM_CENTER = "centerLocation";

  public void readData( Widget widget ) {
    GMap map = ( GMap )widget;
    String location = WidgetLCAUtil.readPropertyValue( map, PARAM_CENTER );
    map.setCenterLocation( location );
  }
  

As you can see, it's really easy. You just need to ask the WidgetLCAUtil#readPropertyValue for the parameter and pass it to the server-side widget implementation. If you wonder why the center location does not get written in the renderChanges method we implemented above: This will be your task at the end of the tutorial. Normally you won't have public methods for attributes which are not changeable programmatically. For this you would use an adapter or another mechanism to implement the setter behind the scenes. At the end of the tutorial it is your task hide the public setCenterLocation method of the GMap widget or - even better - to implement the rendering of the center location yourself.

The last step is to implement the way our widget is disposed on the client. Normally there is no need to care for anything else and this is also the case with our GMap widget. If you really have to do any other stuff like calling specific methods before the widget is disposed you should do it here.


  public void renderDispose( Widget widget ) throws IOException {
    JSWriter writer = JSWriter.getWriterFor( widget );
    writer.dispose();
  }
  

There are now two additional methods called createResetHandlerCalls and getTypePoolId. These were introduced by a mechanism that helps to soften the massive memory consumption on the client. Many of the client-side widgets are not thrown away anymore, but kept in an object pool for later reuse. Especially with long-running applications in the Internet Explorer browser, this can make a huge difference. Please note that this topic is work in progress and, despite extensive testing, might lead to errors under different circumstances. We recommend not to use this in your custom widgets.

Registering the JavaScript files

Loading the client application (with our widget) in a browser will still lead to problems as nobody knows about the JavaScript resources and where to find them. We can fix this by using the org.eclipse.rap.ui.resources extension point. We add two new extensions, one for our custom widget JavaScript file and one for the external JavaScript library of Google Maps.


  <plugin>
    <extension
      id="org.eclipse.rap.gmaps.gmap"
      point="org.eclipse.rap.ui.resources">
        <resource class="org.eclipse.rap.gmaps.GMapResource"/>
        <resource class="org.eclipse.rap.gmaps.GMapAPIResource"/>
    </extension>
  </plugin>
  

Both classes refer to an implementation of org.eclipse.rwt.resources.IResource. The first one - our custom widget itself - is really easy to implement. We just need to tell RAP that it is a JavaScript file, where it can find the file and which charset to use. So the IResource> implementation for the widget JavaScript could look like this:


  public class GMapResource implements IResource {

    public String getCharset() {
      return "UTF-8";
    }

    public ClassLoader getLoader() {
      return this.getClass().getClassLoader();
    }

    public RegisterOptions getOptions() {
      return RegisterOptions.VERSION_AND_COMPRESS;
    }

    public String getLocation() {
      return "org/eclipse/rap/gmaps/GMap.js";
    }

    public boolean isJSLibrary() {
      return true;
    }

    public boolean isExternal() {
      return false;
    }
  }
  

For the charset we need to return a string to describe the charset. If you're not sure you can use of the constants defined in org.eclipse.rwt.internal.util.HTML but be aware that this is internal. The getOptions method specifies if the file should be delivered with any special treatment. Possible ways are NONE, VERSION, COMPRESS and VERSION_AND_COMPRESS. VERSION means that RAP will append a hash value of the file itself to tell the browser if he should use an already cached version or reload the file from the server. With the COMPRESS option RAP will remove all unnecessary stuff like blank lines or comments from the JavaScript file in order to save bandwidth and parse time. Remote files like our next task - the Google Maps library - are handled a little bit different.


  public class GMapAPIResource implements IResource {

    private static final String KEY_SYSTEM_PROPERTY = "org.eclipse.rap.gmaps.key";

    // key for localhost rap development on port 9090
    private static final String KEY_LOCALHOST
      = "ABQIAAAAjE6itH-9WA-8yJZ7sZwmpRQz5JJ2zPi3YI9JDWBFF"
      + "6NSsxhe4BSfeni5VUSx3dQc8mIEknSiG9EwaQ";

    private String location;

    public String getCharset() {
      return "UTF-8";
    }

    public ClassLoader getLoader() {
      return this.getClass().getClassLoader();
    }

    public RegisterOptions getOptions() {
      return RegisterOptions.VERSION;
    }

    public String getLocation() {
      if( location == null ) {
        String key = System.getProperty( KEY_SYSTEM_PROPERTY );
        if( key == null ) {
          key = KEY_LOCALHOST;
        }
        location = "http://maps.google.com/maps?file=api&v=2&key=" + key;
      }
      return location;
    }

    public boolean isJSLibrary() {
      return true;
    }

    public boolean isExternal() {
      return true;
    }
  }
  

To tell RAP to load external resources we just need to return true in isExternal. The location should be a valid URL where the resource resides. In this case it's loaded from the Google Maps server with a special API key. This is specific for the Google Maps widget and is not needed by other widgets. If you're planning to use or extend this sample widget we encourage you to get your own API key for Google as this key will only work on http://localhost:9090/rap. With every other combination of host, port or servletName you need to obtain a new key. Additionally we implemented a system property to define the key without recompiling the widget.
We've just finished our custom widget and our project structure should look like this if we have done everything right.

Project structure

Test the widget

In order to test the widget we will create a new plug-in project with the following entrypoint:


  public class GMapDemo implements IEntryPoint {

    public int createUI() {
      Display display = new Display();
      Shell shell = new Shell( display, SWT.SHELL_TRIM );
      shell.setLayout( new FillLayout() );
      shell.setText( "GMaps Demo" );

      GMap map = new GMap( shell, SWT.NONE );
      map.setAddress( "Stephanienstraße 20, Karlsruhe" );

      shell.setSize( 300, 300 );
      shell.open();
      while( !shell.isDisposed() ) {
        if( !display.readAndDispatch() ) {
          display.sleep();
        }
      }
      return 0;
    }
  }
  

Don't forget to add org.eclipse.rap.ui and the GMap widget project as dependencies. Then we need to register our entry point via the org.eclipse.rap.ui.entrypoint. If everything went well our demo should look like this after starting the server:

Widget demo

But there was more: the center location we sent to the server. To read it out, we extend the sample with a new button:


  public class GMapDemo implements IEntryPoint {

    public int createUI() {
      Display display = new Display();
      final Shell shell = new Shell( display, SWT.SHELL_TRIM );
      shell.setLayout( new FillLayout() );
      shell.setText( "GMaps Demo" );

      GMap map = new GMap( shell, SWT.NONE );
      map.setAddress( "Stephanienstraße 20, Karlsruhe" );

      Button button = new Button( shell, SWT.PUSH );
      button.setText( "Tell me where I am!" );
      button.addSelectionListener( new SelectionAdapter() {
        public void widgetSelected( SelectionEvent e ) {
          String msg = "You are here: " + map.getCenterLocation()
          MessageDialog.openInformation( shell, "GMap Demo", msg );
        }
      } );

      shell.setSize( 300, 300 );
      shell.open();
      while( !shell.isDisposed() ) {
        if( !display.readAndDispatch() ) {
          display.sleep();
        }
      }
      return 0;
    }
  }
  

After a click on the button the current location will be sent to the server (together with the selection event of the button), applied to the widget with the help of our LCA and shown by the MessageDialog. Great!

And now?

You completed your first custom widget - congratulations!
Before implementing a heavy custom control we like to give you some tasks to play around with the GMap widget.

We are delight in seeing what you can do with this little widget. If you have problems, take a look at all the LCAs already provided by RAP. It's not black magic.

Troubleshooting

Client debugging

Browser-based JavaScript debugging tools such as Firebug can be very helpful. To enable client debugging, remember to start your application in Debug mode. This setting preserves the original formatting of the JavaScript code delivered to the client, whereas it is compressed and obfuscated in the Standard mode. You can enable the Debug mode directly in the RAP Launcher (see option Client-side library variant) or by setting the system property -Dorg.eclipse.rwt.clientLibraryVariant=DEBUG as VM parameter.

You can use the console API to add temporary logging output to your JavaScript code.