Follow Techotopia on Twitter

On-line Guides
All Guides
eBook Store
iOS / Android
Linux for Beginners
Office Productivity
Linux Installation
Linux Security
Linux Utilities
Linux Virtualization
Linux Kernel
System/Network Admin
Programming
Scripting Languages
Development Tools
Web Development
GUI Toolkits/Desktop
Databases
Mail Systems
openSolaris
Eclipse Documentation
Techotopia.com
Virtuatopia.com

How To Guides
Virtualization
General System Admin
Linux Security
Linux Filesystems
Web Servers
Graphics & Desktop
PC Hardware
Windows
Problem Solutions
Privacy Policy

  




 

 

Eclipse RAP Development Guide
Previous Page Home Next Page

How to create a custom widget?

Creating custom widgets in RAP is a hot topic and we see the need to cover this in an article which you are just reading here. The main problem is the distributed nature of RAP which requires that all components of your custom widget have to play together and have some different aspects than custom widgets in SWT.

You should be aware of the fact that there are two different types of custom widgets. On the one side we have compounded widgets which is just an agglomeration of already existing widgets. As this would be done in the same way like SWT we'll only care about real custom widgets (you could use the term "owner drawn" in this case).

There are different tasks to create a new custom widget which will be covered here:

  • create a widget which will be used on the server
  • create the javascript libraries needed by the custom widget
  • create an adapter to connect the widget with the RAP lifecycle
  • register the javascript files

As an exemplary widget implementation we will use a component to show specific locations in a map. Behind the scenes we will use the public API of Google maps. Let's start with our first task - the server-side widget implementation.

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.

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 qooxdoos 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: The qooxdoo build which gets delivered with RAP is not the same qooxdoo you'll find on their webpage. We stripped it down to have the best mix between needed functionality and size. So there may be classes which are available in plain qooxdoo but not in the version of RAP.

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 splitted up in several phases which are executed in a specific order.

Phase Description
ReadData This is responsible to read the values sended 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 kit and the LCA itself is called 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( "appearance", "composite" );
    writer.set( "overflow", "hidden" );
    ControlLCAUtil.writeStyleFlags( ( GMap )widget );
  }
  
  

Basically the class JSWriter is responsible for transforming our java calls and setters into the corresponding javascript which gets transfered to the client. The JSWriter is also responsible to only write out changes which are different than their preserved value. For each widget you have an own JSWriter instance so 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. This could also be done in Javascript and is just a showcase in here. The next step is to preserve our values therewith RAP can decide in the next request if there has something changed in the meantime. As we just have the address which could change this is relative straight forward.

  
  private static final String PROP_ADDRESS = "address";
  
  public void preserveValues( Widget widget ) {
    ControlLCAUtil.preserveValues( ( Control )widget );
    IWidgetAdapter adapter = WidgetUtil.getAdapter( widget );
    adapter.preserve( PROP_ADDRESS, ( ( GMap )widget ).getAddress() );
    
    // only needed for custom variants (themeing)
    WidgetLCAUtil.preserveCustomVariant( widget );
  }
  
  

First we use ControlLCAUtil#preserveValues to have most of the work done by the framework. It's responsible for preserving values like 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 is a way to have different looks of the same widget and is part of the theme engine provided by RAP. Please see the Prepare Custom Widgets for Theming article for more informations about theming a custom widget.

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 (themeing)
    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 "ISO-8859-1";
    }
  
    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 "ISO-8859-1";
    }
  
    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 = "https://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 https://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.

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:

But there was more: the center location we sended 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 sended 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.

  • Implement the rendering for the center location to programmatically set it
  • Add support for markers
  • Implement other nice things by connecting Google Maps with RAP
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

There may be times where you need to track down a javascript error in your custom widget. As the tooling support for javascript is not that mature as for java we provide some little undertakes to help you debugging your custom widget.

Client library variants

As we do many improvements regarding speed of the underlying qooxdoo library the normally used javascript fragment is compressed and cleaned from any comment in the code. As it is easier to develop with a full-blown version of qooxdoo to debug it you can specify -Dorg.eclipse.rwt.clientLibraryVariant=DEBUG as VM parameter.

Client log level

To see debug messages and exceptions which occured during javascript execution you can turn on the client-side debugging mode with -Dorg.eclipse.rwt.clientLogLevel=<LEVEL> where <LEVEL> is one of the following values:

  • OFF
  • ALL
  • WARNING
  • INFO
  • SEVERE
  • FINE
  • FINER
  • FINEST

When developing custom widget we recommend to use the level ALL to see every debug message.


 
 
  Published under the terms of the Eclipse Public License Version 1.0 ("EPL") Design by Interspire