| <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> |
| <html> |
| <head> |
| <meta name="copyright" content="Copyright (c) 2007-2008 Innoopract Informationssysteme GmbH. This page is made available under license. For full details see the LEGAL in the documentation book that contains this page." > |
| <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> |
| <meta http-equiv="Content-Style-Type" content="text/css"> |
| <title>Custom widgets</title> |
| <link rel="stylesheet" href="../style.css" charset="ISO-8859-1" type="text/css"> |
| </head> |
| <body> |
| <h1>How to create a custom widget?</h1> |
| |
| <p>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.</p> |
| |
| <p> |
| 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). |
| </p> |
| |
| <p> |
| There are different tasks to create a new custom widget which will be covered here: |
| <ul> |
| <li>create a widget which will be used on the server</li> |
| <li>create the javascript libraries needed by the custom widget</li> |
| <li>create an adapter to connect the widget with the RAP lifecycle</li> |
| <li>register the javascript files</li> |
| </ul> |
| </p> |
| |
| <p>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.</p> |
| |
| <p> |
| <em>Note</em>: You can find the example code for this widget in the RAP CVS: |
| <code>dev.eclipse.org/cvsroot/rt/org.eclipse.rap/sandbox/org.eclipse.rap.demo.gmaps</code>. |
| </p> |
| |
| <h3>Creating the widget</h3> |
| |
| <p> |
| To have a clear line between your application and your widgets we'll create a new plug-in |
| project called <code>org.eclipse.rap.demo.gmaps</code>. We don't need any additional |
| aspects like an RCP application or an activator. |
| </p> |
| <img src="../../images/custom-widget/newproject.png"> |
| |
| <p> |
| 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 <code>GMap.java</code> in place it into the |
| <code>org.eclipse.rap.gmaps</code> package. As super class we use <code>org.eclipse.swt.widgets.Composite</code> |
| 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 <code>GMap</code> class called <code>address</code> |
| and will generate the corresponding setter and getter. The setter - <code>setAdress( String )</code> - should |
| check for <code>null</code> values and replacing <code>null</code> with an empty string instead. It |
| should look like this in the end: |
| <pre> |
| <code> |
| public void setAddress( String address ) { |
| if( address == null ) { |
| this.address = ""; |
| } else { |
| this.address = address; |
| } |
| } |
| </code> |
| </pre> |
| <p> |
| To have an example for the other way around - from client to server - we introduce another |
| field called <code>centerLocation</code> 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: |
| </p> |
| <pre> |
| <code> |
| private String centerLocation; |
| |
| public void setCenterLocation( String location ) { |
| this.centerLocation = location; |
| } |
| |
| public String getCenterLocation() { |
| return this.centerLocation; |
| } |
| </code> |
| </pre> |
| <p> |
| Additionally we override the <code>setLayout(Layout)</code> method of <code>Composite</code> |
| 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. |
| </p> |
| |
| <h3>Client-side implementation</h3> |
| |
| <p> |
| On the client side our widget is just a javascript class defined with the |
| <a href="http://qooxdoo.org/documentation/0.7/class_declaration">qooxdoo syntax</a>. |
| As super class we need to use at least |
| <a href="http://demo.qooxdoo.org/current/apiviewer/#qx.ui.core.Widget">qx.ui.core.Widget</a>. |
| To have an easier life we will directly use one of the |
| <a href="http://qooxdoo.org/documentation/0.7/layout_manager">qooxdoo layout managers</a> |
| as base of our widget. The code for the new qooxdoo class together with qooxdoos |
| <code>CanvasLayout</code> as super class will look like this: |
| <pre> |
| <code> |
| qx.Class.define( "org.eclipse.rap.gmaps.GMap", { |
| extend: qx.ui.layout.CanvasLayout, |
| // ... |
| } ); |
| </code> |
| </pre> |
| 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 <code>id</code> 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 <code>id</code> to the browsers DOM by adding a new HTML attribute with <code>setHtmlAttribute</code>. |
| You see some Google Maps-specific calls here which are just there to initialize the Google Maps |
| subsystem. See the |
| <a href="http://code.google.com/apis/ajax/documentation">Google Maps API Documentation</a> |
| for more information. |
| The two <a href="http://qooxdoo.org/documentation/0.7/using_events">event listeners</a> 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 <code>_doResize</code> will be implemented later. |
| <pre> |
| <code> |
| qx.Class.define( "org.eclipse.rap.gmaps.GMap", { |
| extend: qx.ui.layout.CanvasLayout, |
| <b> |
| 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 ); |
| } |
| } |
| </b> |
| } ); |
| </code> |
| </pre> |
| |
| To save the address of the widget somewhere we use the |
| <a href="http://qooxdoo.org/documentation/0.7/properties">property system</a> of qooxdoo. |
| Adding a new property will look like this: |
| <pre> |
| <code> |
| qx.Class.define( "org.eclipse.rap.gmaps.GMap", { |
| extend: qx.ui.layout.CanvasLayout, |
| construct: function( id ) { |
| // ... |
| }<b>, // <-- comma added, see javascript syntax reference |
| |
| properties : { |
| address : { |
| init : "", |
| apply : "load" |
| } |
| } |
| </b> |
| } |
| } ); |
| </code> |
| </pre> |
| 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 |
| <code>getAdress</code> on a our client-side GMap object. As you see in the <code>apply</code> attribute |
| of your address property the method <code>load</code> will be called when the value of the |
| property changes. So let's implement it: |
| |
| <pre> |
| <code> |
| qx.Class.define( "org.eclipse.rap.gmaps.GMap", { |
| extend: qx.ui.layout.CanvasLayout, |
| construct: function( id ) { |
| // ... |
| }, |
| |
| properties : { |
| // ... |
| }<b>, // <-- 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(); |
| } |
| } |
| } |
| </b> |
| } ); |
| </code> |
| </pre> |
| <p> |
| 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 <code>_doActivate</code> 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. |
| </p> |
| <p> |
| The other event listener for the <code>moveend</code> 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 <code>Request</code> 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 <code>req.send()</code>. 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. |
| </p> |
| <p> |
| 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. |
| </p> |
| <p style="border:1px dotted grey;"><strong>Note:</strong> The qooxdoo build |
| which gets delivered with RAP is <strong>not</strong> the same qooxdoo you'll |
| find on their <a href="http://qooxdoo.org">webpage</a>. 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.</p> |
| |
| <h3>Filling the gap between client and server</h3> |
| <p> |
| 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 <em>life cycle</em>. 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. |
| </p> |
| <table border="1"> |
| <tr> |
| <th>Phase</th> |
| <th>Description</th> |
| </tr> |
| <tr> |
| <td>ReadData</td> |
| <td>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.</td> |
| </tr> |
| <tr> |
| <td>ProcessAction</td> |
| <td>ProcessAction is responsible for dispatching the events to the widgets. Attributes may |
| change in this phase as a response for the events.</td> |
| </tr> |
| <tr> |
| <td>Render</td> |
| <td>At the end of the lifecycle every change will be rendered to the client.<br /> |
| Be aware that only values which are different than there preserved ones are send to |
| the client (means: only the delta).</td> |
| </tr> |
| </table> |
| <p> |
| 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 <code>getAdapter</code> to provide an LCA. This can be done by implementing the |
| <code>getAdapter</code> method in our own widget like this: |
| <pre> |
| <code> |
| ... |
| public Object getAdapter( Class adapter ) { |
| Object result; |
| if( adapter == ILifeCycleAdapter.class ) { |
| result = new MyCustomWidgetLCA(); // extends AbstractWidgetLCA |
| } else { |
| result = super.getAdapter( adapter ); |
| } |
| return result; |
| } |
| ... |
| </pre> |
| </code> |
| 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.<br /> |
| Here a little example:<br /> |
| If our widget is named <code>org.eclipse.rap.gmaps.GMap</code>, |
| <br /> |
| then our LCA should be named <code>org.eclipse.rap.internal.gmaps.gmapkit.GMapLCA</code>. |
| <br /> |
| The LCA class itself must have <code>org.eclipse.rwt.lifecycle.AbstractWidgetLCA</code> |
| 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. |
| </p> |
| <table border="1"> |
| <tr> |
| <th>Method name</th> |
| <th>Description</th> |
| </tr> |
| <tr> |
| <td><code>renderInitialization(Widget)</code></td> |
| <td>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)</td> |
| </tr> |
| <tr> |
| <td><code>preserveValues(Widget)</code></td> |
| <td>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.</td> |
| </tr> |
| <tr> |
| <td><code>renderChanges(Widget)</code></td> |
| <td>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.</td> |
| </tr> |
| <tr> |
| <td><code>renderDispose(Widget)</code></td> |
| <td>You can tidy up several things here before the widget gets disposed |
| of on the client.</td> |
| </tr> |
| </table> |
| <p> |
| 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. |
| </p> |
| <pre> |
| <code> |
| 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 ); |
| } |
| </code> |
| </pre> |
| <p> |
| The <code>JSWriter</code> provides methods to generate Javasccript code that |
| updates the client-side state of our widget. |
| Each widget has its own <code>JSWriter</code> instance so that RAP can decide |
| to which widget the call belongs. |
| As you can see in the snippet, <code>JSWriter#newWidget</code> is called to create a new widget |
| on the client side. The second parameters is an array of <code>Object</code>s which are |
| passed to the constructor of the qooxdoo class (see above). |
| With <code>JSWriter#set</code> you can easily set different attributes of your qooxdoo object. |
| Note that there are overloaded methods of <code>set</code> that take care |
| that Javascript code is only generated if <em>necessary</em>. That means if a |
| certain property was left unchanged during the request processing, no Javascript |
| code is generated. |
| </p> |
| <p> |
| The next step is to preserve the relevant properties of our widget. After that |
| the <code>set</code>-methods from <code>JSWriter</code> can determine wether |
| it is necessary to actually generate Javascript code. |
| As there is only the <code>address</code> property which could change, this |
| is straight forward. |
| </p> |
| <pre> |
| <code> |
| 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' propety |
| IWidgetAdapter adapter = WidgetUtil.getAdapter( widget ); |
| adapter.preserve( PROP_ADDRESS, ( ( GMap )widget ).getAddress() ); |
| |
| // Only needed for custom variants (themeing) |
| WidgetLCAUtil.preserveCustomVariant( widget ); |
| } |
| </code> |
| </pre> |
| <p> |
| First we use <code>ControlLCAUtil#preserveValues</code> to preserve all |
| properties that are inherited by <code>Control</code> (tooltip, tabindex, |
| size, etc.). |
| So we only need to care about the things implemented in our widget. |
| We just request a socalled <code>IWidgetAdapter</code> which is responsible for different |
| aspects in the lifecycle of a widget. In this case we only use it to preserve |
| the <code>address</code> value |
| with a predefined key (<code>PROP_ADRESS</code>) to associate it later.<br /> |
| The last line calling <code>preserveCustomVariant</code> 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 <a href="theming-custom.html">Prepare Custom |
| Widgets for Theming</a> article for further information. |
| </p> |
| <p> |
| The following step is one of most interesting parts of your lifecycle adapter - the |
| <code>renderChanges</code> 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: |
| |
| </p> |
| <pre> |
| <code> |
| 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 ); |
| } |
| </code> |
| </pre> |
| |
| <p> |
| Again we use the <code>ControlLCAUtil</code> to write the changes which are implemented on |
| <code>Control</code> 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 <code>JSWriter</code> instance for our widget. We need to use |
| <code>JSWriter#set</code> to set a specific attribute to the widget instance on the client-side. |
| There are many different <code>set</code> 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 (<code>JS_PROP_ADRESS</code>) will call the method |
| <code>setAdress</code> 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. |
| </p> |
| <p> |
| Now we need to process the actions transfered to the server (at least if there are any). |
| We see in the <code>GMap.js</code> that if the user moves the map a new parameter called |
| <code>centerLocation</code> will be attached to the current request and transfered to the |
| server. To read and process it the <code>readData</code> method of the LCA is used. |
| </p> |
| <pre> |
| <code> |
| 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 ); |
| } |
| </code> |
| </pre> |
| <p> |
| As you can see, it's really easy. You just need to ask the <code>WidgetLCAUtil#readPropertyValue</code> |
| 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 <code>renderChanges</code> 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 <code>setCenterLocation</code> |
| method of the <code>GMap</code> widget or - even better - to implement the rendering of |
| the center location yourself. |
| </p> |
| <p> |
| 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. |
| </p> |
| <pre> |
| <code> |
| public void renderDispose( Widget widget ) throws IOException { |
| JSWriter writer = JSWriter.getWriterFor( widget ); |
| writer.dispose(); |
| } |
| </code> |
| </pre> |
| <p> |
| There are now two additional methods called <code>createResetHandlerCalls</code> and |
| <code>getTypePoolId</code>. 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. |
| </p> |
| |
| <h3>Registering the javascript files</h3> |
| 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 <code>org.eclipse.rap.ui.resources</code> extension point. We add two new extensions, |
| one for our custom widget javascript file and one for the external javascript library of Google |
| Maps. |
| </p> |
| <pre> |
| <code> |
| <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> |
| </code> |
| </pre> |
| <p> |
| Both classes refer to an implementation of <code>org.eclipse.rwt.resources.IResource</code>. |
| 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 |
| <code>IResource></code> implementation for the widget javascript could look like this: |
| </p> |
| <pre> |
| <code> |
| 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; |
| } |
| } |
| </code> |
| </pre> |
| <p> |
| 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 <code>org.eclipse.rwt.internal.util.HTML</code> but be |
| aware that this is internal. The <code>getOptions</code> method specifies if the file should |
| be delivered with any special treatment. Possible ways are <code>NONE</code>, <code>VERSION</code>, |
| <code>COMPRESS</code> and <code>VERSION_AND_COMPRESS</code>. <code>VERSION</code> 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 <code>COMPRESS</code> 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. |
| </p> |
| <pre> |
| <code> |
| 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 = "http://maps.google.com/maps?file=api&v=2&key=" + key; |
| } |
| return location; |
| } |
| |
| public boolean isJSLibrary() { |
| return true; |
| } |
| |
| public boolean isExternal() { |
| return true; |
| } |
| } |
| </code> |
| </pre> |
| <p> |
| To tell RAP to load external resources we just need to return <code>true</code> in |
| <code>isExternal</code>. 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 <a href="http://www.google.com/apis/maps/signup.html"> |
| own API key</a> 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. |
| <br /> |
| We've just finished our custom widget and our project structure should look like this if we have done everything right. |
| </p> |
| <img src="../../images/custom-widget/project.png"> |
| |
| <h3>Test the widget</h3> |
| <p> |
| In order to test the widget we will create a new plug-in project with the following entrypoint: |
| </p> |
| <pre> |
| <code> |
| 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; |
| } |
| } |
| </code> |
| </pre> |
| <p> |
| Don't forget to add <code>org.eclipse.rap.ui</code> and the GMap widget project as dependencies. |
| Then we need to register our entry point via the <code>org.eclipse.rap.ui.entrypoint</code>. If |
| everything went well our demo should look like this after starting the server: |
| </p> |
| <img src="../../images/custom-widget/demo.png"> |
| <p> |
| But there was more: the center location we sended to the server. To read it out, we extend the sample |
| with a new button: |
| </p> |
| <pre> |
| <code> |
| 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; |
| } |
| } |
| </code> |
| </pre> |
| <p> |
| 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! |
| </p> |
| |
| <h3>And now?</h3> |
| <p> |
| You completed your first custom widget - congratulations!<br /> |
| Before implementing a heavy custom control we like to give you some tasks to play around |
| with the GMap widget. |
| <ul> |
| <li>Implement the rendering for the center location to programmatically set it</li> |
| <li>Add support for <a href="http://www.google.com/apis/maps/documentation/overlays.html">markers</a></li> |
| <li>Implement other nice things by connecting Google Maps with RAP</li> |
| </ul> |
| 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. |
| </p> |
| |
| <h3>Troubleshooting</h3> |
| <p> |
| 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. |
| <p> |
| <h4>Client library variants</h4> |
| 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 <code>-Dorg.eclipse.rwt.clientLibraryVariant=DEBUG</code> as |
| VM parameter. |
| <h4>Client log level</h4> |
| <p> |
| To see debug messages and exceptions which occured during Javascript execution |
| you can turn on the client-side debugging mode with |
| <code>-Dorg.eclipse.rwt.clientLogLevel=<LEVEL></code> where <code><LEVEL></code> |
| is one of the following values: |
| </p> |
| <ul> |
| <li>OFF</li> |
| <li>ALL</li> |
| <li>WARNING</li> |
| <li>INFO</li> |
| <li>SEVERE</li> |
| <li>FINE</li> |
| <li>FINER</li> |
| <li>FINEST</li> |
| </ul> |
| <p> |
| When developing custom widget we recommend to use the level <code>ALL</code> to |
| see every debug message. |
| </p> |
| </body> |
| </html> |