GWT fu, Part 2: Beyond the basics

Implement advanced Google Web Toolkit features

Google Web Toolkit (GWT) lets you implement desktop-like applications that run in a browser. In the second half of a two-part series, David Geary shows you how to use some of the more advanced aspects of GWT, including sinking events, using timers, and previewing events.

Share:

David Geary, President, Clarity Training, Inc.

David GearyAuthor, speaker, and consultant David Geary is the president of Clarity Training, Inc., where he teaches developers to implement Web applications using GWT and JavaServer Faces (JSF). He was on the JSTL 1.0 and JSF 1.0/2.0 Expert Groups, co-authored Sun's Web Developer Certification Exam, and has contributed to open source projects, including Apache Struts and Apache Shale. David's Graphic Java Swing was one of the best-selling Java books of all time, and Core JSF (co-written with Cay Horstman), is the best-selling JSF book. David is also the author of GWT Solutions, and he speaks regularly at conferences and user groups. He has been a regular on the NFJS tour since 2003, has taught courses at Java University, and is a three-time JavaOne rock star.



20 October 2009

Also available in Chinese Japanese

With a Swing-like API, GWT lets you implement rich user interfaces that run in a browser, without any additional software such as Java™ Web Start. In Part 1 of this two-part article, I showed you some GWT fundamentals, including using widgets, invoking remote procedure calls (RPCs), and implementing composite widgets.

In this article, I pick up where Part 1 left off to illustrate some of the more advanced aspects of GWT:

  • Dialog boxes
  • Sinking events and manipulating Document Object Model (DOM) element attributes
  • Image loading (and busy cursors)
  • Modules
  • Event previews
  • Timers

The places application (redux)

In Part 1, I introduced the places application, shown in Figure 1:

Figure 1. The places application
Screen shot of GWT application

When the application starts, it loads six addresses from a database and shows them in the list box on the top left. When you click on one of those addresses, the application loads the selected address into the grid to the right of the list box. Subsequently, when you click the grid's Show button, the application accesses map and weather information for the address from Yahoo! Web Services and shows that information in a dialog box. In the dialog box, the map and weather information reside in a horizontal split panel that you can adjust as shown in Figure 1.

In Part 1, I discussed loading addresses from the database, displaying them in the list box, and populating the grid with the selected item from the list box. Listing 9 in Part 1 shows the places application code in the state that I left it in at the end of that article. Here, I focus on the dialog box — an instance of PlaceDialog — and a custom viewport widget that contains the map. After making two RPCs to the server to fetch map and weather information from Yahoo! Web Services, the application creates an instance of PlaceDialog, sets the dialog's position, and shows the dialog:

PlaceDialog dialog = new PlaceDialog(addressGrid.getAddress(), urls, weatherHTML);
dialog.setPopupPosition(200, 200);
dialog.show();

The implementation of the dialog box and its enclosed viewport will give you the opportunity to explore some of GWT's more advanced aspects.


Dialog boxes

For the rest of this article, I'll continue implementing the places application. (You can download the source code for the complete application.) The first thing I'll do is implement PlacesDialog so that it simply shows the weather information for the selected place, as shown in Figure 2:

Figure 2. A first cut at the places dialog
Screen shot of GWT dialog boxes showing weather information for the selected place

The code for my first cut of PlacesDialog is shown in Listing 1:

Listing 1. PlacesDialog.java, take 1
package com.clarity.client;
package com.clarity.client;

import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.HTML;

public class PlacesDialog extends DialogBox {    

  public PlacesDialog(Address address, String[] imageUrls, String weather) {
    super(false, false); // no auto-hide, and no modal

    setText(address.getAddress() + " " + address.getCity() + ", " + address.getState());
      
    HTML weatherHTML = new HTML();
    weatherHTML.setHTML(weather);
      
    setWidget(weatherHTML);
  }
}

Listing 1 is pretty simple. PlacesDialog extends DialogBox and contains an HTML widget that shows the weather information fetched from the Web service. From the PlacesDialog constructor, I invoke the superclass constructor to create a dialog box that is not modal and does not auto-hide when the user clicks outside of the dialog box.

There's one problem, though, with the dialog box shown in Listing 1. You cannot bring a dialog to the front by clicking on it. For example, in Figure 2, you can never get the San Francisco dialog on top of the Buffalo dialog. To implement that feature, I need to sink mouse events for the dialog box and handle mouse-down events.

Sinking events and manipulating DOM element attributes

In the updated listing of the PlacesDialog class in Listing 2, I sink mouse events in the dialog box with a call to sinkEvents(). After that call to sinkEvents(), GWT will call the dialog box's onBrowserEvent() method when a mouse event occurs in the dialog box. In that method, I increase the z index for the dialog box's associated DOM element with a call to DOM.setIntStyleAttribute().

Listing 2. PlacesDialog.java, take 2
package com.clarity.client;

import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.HTML;

public class PlacesDialog extends DialogBox {
  private static int z;
    
  public PlacesDialog(Address address, String[] imageUrls, String weather) {
    setText(address.getAddress() + " " + address.getCity() + ", " + address.getState());
      

    HTML weatherHTML = new HTML();
    weatherHTML.setHTML(weather);
      
    setWidget(weatherHTML);
      
    sinkEvents(Event.MOUSEEVENTS);
  }
    
  public void onBrowserEvent(Event event) {
   if (event.getTypeInt() == Event.ONMOUSEDOWN) {
      DOM.setIntStyleAttribute(PlacesDialog.this.getElement(), "zIndex", z++);    
    }
    super.onBrowserEvent(event);
  }
}

With the dialog box in Listing 2, you can click on a dialog to bring it to the front.

Image loading and busy cursors

In Listing 3, I round out the implementation of the PlacesDialog class by adding a horizontal split panel and putting the map on the left and weather on the right:

Listing 3. PlacesDialog.java, take 3
package com.clarity.client;

import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.LoadEvent;
import com.google.gwt.event.dom.client.LoadHandler;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HorizontalSplitPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.Panel;

import com.clarity.widgets.client.Viewport;

public class PlacesDialog extends DialogBox {
    private static int z;
    private static final String[] zoomLevelItems = { 
      "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12" 
    };
 
    private final Viewport viewport = new Viewport();
    private final Image[] images = new Image[12];
    private String[] imageUrls;
    
    final ListBox zoomLevels = new ListBox();
    
    public PlacesDialog(Address address, String[] imageUrls, String weather) {
      super(false, false); // no auto-hide, and no modal
      setText(address.getAddress() + " " 
          + address.getCity() + ", " 
          + address.getState());
      
      this.imageUrls = imageUrls;
      
      HorizontalSplitPanel hsp = new HorizontalSplitPanel();
      hsp.setPixelSize(600, 350);
      hsp.add(createMapPanel());
      hsp.add(createWeatherPanel(weather));
      
      zoomLevels.addChangeHandler(new ChangeHandler() {
        public void onChange(ChangeEvent event) {
          String v = zoomLevels.getItemText(zoomLevels.getSelectedIndex());
          setZoom(new Integer(v).intValue() - 1);
        }
      });
      
      setWidget(hsp);      
      sinkEvents(Event.MOUSEEVENTS);
    }

    public void onBrowserEvent(Event event) {
      if (event.getTypeInt() == Event.ONMOUSEDOWN) {
        DOM.setIntStyleAttribute(PlacesDialog.this.getElement(), "zIndex", z++);    
      }
      super.onBrowserEvent(event);
    }
    
    private Panel createMapPanel() {
      AbsolutePanel viewportPanel = new AbsolutePanel();
      
      for (String item : zoomLevelItems) {
        zoomLevels.addItem(item);
      }
      
      images[0] = new Image();
      images[0].setUrl(imageUrls[0]);
      
      viewport.setView(images[0]);
      
      viewportPanel.add(viewport);
     viewportPanel.add(zoomLevels, 10, 10);  // 10, 10 are x,y coordinates from ulhc
      
      DOM.setIntStyleAttribute(zoomLevels.getElement(), "zIndex", 
      DOM.getIntStyleAttribute(viewport.getElement(), "zIndex") + 1);
      
      return viewportPanel;
    }
    
    private HTML createWeatherPanel(String weather) {
      HTML weatherHTML = new HTML();
      weatherHTML.setHTML(weather);
      return weatherHTML;
    }
   
    public void setZoom(final int index) {
      if (images[index] == null) {
        images[index] = new Image();
        images[index].addLoadHandler(new LoadHandler() {
          public void onLoad(LoadEvent event) {
            zoomLevels.setEnabled(true);
            viewport.removeStyleName("waitCursor");
          }
        });
      zoomLevels.setEnabled(false);
      viewport.addStyleName("waitCursor");
    }
    images[index].setUrl(imageUrls[index]);
    viewport.setView(images[index]);
  }
}

In Listing 3, I create a viewport in the createMapPanel() method. As you can see from the imports, that viewport is an instance of com.clarity.widgets.client.Viewport. I add the viewport, and the zoom-level pull-down, to an instance of AbsolutePanel. Absolute panels let you position the widgets they contain at pixel locations — notice that I position the zoom-level pull-down 10 pixels from the top and 10 pixels from the left of the upper left-hand corner of the panel.

As Figure 3 illustrates, when you select a zoom level, the application changes the cursor to a wait cursor while the application loads the image for the selected zoom level. When the image associated with the selected zoom level is loaded, the application changes the cursor back to its original state.

Figure 3. Loading an image
Screen shot of an image of a map

Listing 4 is an excerpt from Listing 3 that shows how I monitor loading of the image and manipulate the cursor accordingly:

Listing 4. Loading images
package com.clarity.client;
public class PlacesDialog extends DialogBox {
    private static int z;
    private static final String[] zoomLevelItems = { 
      "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12" 
    };
 
    private final Viewport viewport = new Viewport();
    private final Image[] images = new Image[12];
    private String[] imageUrls;
    
    final ListBox zoomLevels = new ListBox();
    
    public PlacesDialog(Address address, String[] imageUrls, String weather) {
      ...
      zoomLevels.addChangeHandler(new ChangeHandler() {
        public void onChange(ChangeEvent event) {
          String v = zoomLevels.getItemText(zoomLevels.getSelectedIndex());
          setZoom(new Integer(v).intValue() - 1);
        }
      });
      ...
    }

    ...
   
    public void setZoom(final int index) {
      if (images[index] == null) {
        zoomLevels.setEnabled(false);
       viewport.addStyleName("waitCursor");

        images[index] = new Image();

        images[index].addLoadHandler(new LoadHandler() {
          public void onLoad(LoadEvent event) {
            zoomLevels.setEnabled(true);
           viewport.removeStyleName("waitCursor");
          }
        });
    }
    images[index].setUrl(imageUrls[index]);
    viewport.setView(images[index]);
  }

In Listing 4, I add a change handler to the zoom pull-down. (See Part 1 for more information on event handlers.) That change handler calls the setZoom() method, which checks to see if the image has been previously loaded; if not, setZoom() adds a load handler to the image, changes the cursor to the wait cursor, and disables the zoom pull-down. Subsequently, when the image is loaded, and GWT calls the load handler's onLoad method, the load handler restores the cursor to its original representation and enables the zoom pull-down. The waitCursor CSS style is defined in the application's style sheet, like this:

.waitCursor {
  cursor: wait;
}

Modules

As I discussed in Part 1, every GWT application is a module. However, the inverse is not true: every module is not a GWT application. Modules are a reuse mechanism, and a GWT application can use as many other modules as it likes.

Modules are simply some artifacts — perhaps custom GWT widgets, some JavaScript, and some CSS, for example — along with a module-definition XML file. For example, I put the Viewport class in a module of its own. Figure 4 shows the source files for the Widgets module.

Figure 4. Source for the Widgets module
Image of source of Widegets module

The module shown in Figure 4 is about as simple as a module gets. It has a module-configuration file (Widgets.gwt.xml) and the viewport's implementation (Viewport.java). The configuration file, shown in Listing 5, is also about as simple as it gets:

Listing 5. Widgets.gwt.xml
<module rename-to='widgets'>
  <inherits name='com.google.gwt.user.User'/>
</module>

The Widgets module inherits the GWT User module, which contains the core code for GWT.

After creating the module configuration file and implementing the Viewport class, I created a JAR file containing the Widgets module, as shown in Figure 5:

Figure 5. The Widgets module's JAR
Image of Widget module's JAR

To use the Widgets module in the places application, I do two things:

  • Put the Widgets JAR file on my classpath.
  • Inherit the Widgets module in the places XML configuration file.

Modules let you package custom widgets, and their associated artifacts, and make them available for other developers to use. An application can inherit as many modules as it needs, and modules can contain other modules; in fact, you can nest modules as deeply as you want.


Event previews

The Viewport class lets you drag the viewport's view, as illustrated in Figure 6:

Figure 6. Dragging the map in the viewport
Screen shot of the click and drag feature on a map

As you know by now, GWT comes with mouse handlers and an absolute panel that lets you place widgets at pixel locations in the panel. If you combine mouse handlers and absolute panels, you can drag widgets in an absolute panel, as illustrated in Figure 6. However, if you try to drag an image, the browser will interfere with your dragging, as shown in Figure 7:

Figure 7. The browser interfering with image dragging
Screen shot of browser interference with click and drag feature

All browsers that I'm familiar with let you drag images, and if you try to drag an image around in an absolute panel, the browser will try to help you out by dragging the image as shown in Figure 7. In this case, however, I don't want the browser's help dragging the image because it interferes with my image dragging.

What I need is a way to tell the browser not to interfere with my image dragging. The way to do that is by previewing events, as shown in Listing 6:

Listing 6. Previewing events
  public class Viewport extends AbsolutePanel {
    ...
    private HandlerRegistration handlerRegistration = null;

    private Event.NativePreviewHandler preventDefaultMouseEvents = 
      new Event.NativePreviewHandler() {
      public void onPreviewNativeEvent(NativePreviewEvent event) {
        if (event.getTypeInt() == Event.ONMOUSEDOWN 
            || event.getTypeInt() == Event.ONMOUSEMOVE) {
          event.getNativeEvent().preventDefault();        
        }
      }
    };

    addDomHandler(new MouseOverHandler() {
      public void onMouseOver(MouseOverEvent event) {
        handlerRegistration = Event.addNativePreviewHandler(preventDefaultMouseEvents);
      }
    }, MouseOverEvent.getType());

    addDomHandler(new MouseOutHandler() {
      public void onMouseOut(MouseOutEvent event) {
        if (handlerRegistration != null) {
          handlerRegistration.removeHandler();
        }
      }
    }, MouseOutEvent.getType());
    ...
  }
}

In Listing 6, I implement a native preview handler, which, as its name implies, lets me preview events before GWT or the browser get a crack at them. In the event preview, I prevent the browser from reacting to mouse-down and mouse-move events by invoking preventDefault() for a native event. That method prevents the browser from its default reaction to the mouse events — thus the name of the method, preventDefault().

When the cursor enters the viewport, I add the native preview handler to the top of GWT's event stack. And when the cursor leaves the viewport, I remove the preview, which returns event handling to normal. So, while the cursor is in the viewport, GWT prevents the browser from carrying out its default reaction to mouse-down and mouse-move events, thereby keeping the browser from interfering with my image dragging.


Timers

If you drag the mouse in a viewport for less than a half second, the viewport animates the map in the direction you moved the mouse, at a speed relative to how many pixels you covered while you were dragging. That animation continues until you click the mouse in the viewport. I perform that animation with a GWT timer, as shown in Listing 7:

Listing 7. Animating the viewport's view with a GWT timer
private static final int TIMER_REPEAT_INTERVAL = 50;
private static final int SPEED_FACTOR_MULTIPLIER = 20;

...

timer = new Timer() {
  public void run() {
    // Calculate new X and Y locations for the
    // mouse panel, and reposition it.
    int newX = getWidgetLeft(view) - (int) (unitVectorX * speedFactor);
    int newY = getWidgetTop(view) - (int) (unitVectorY * speedFactor);

    repositionView(newX, newY);
  }
};

...

addDomHandler(new MouseUpHandler() {
  public void onMouseUp(MouseUpEvent event) {
    if (deltaTime < gestureTimeThreshold && 
       (deltaX > gesturePixelThreshold || deltaY > gesturePixelThreshold)) {
  
      speedFactor = ((deltaX + deltaY) / (timeUp - timeDown)) * SPEED_FACTOR_MULTIPLIER;
      timer.scheduleRepeating(TIMER_REPEAT_INTERVAL);
      timerRunning = true;
    }      
  }
}
...

addDomHandler(new MouseDownHandler() {
 public void onMouseDown(MouseDownEvent event) {
    System.out.println("mouse down " + event.getX() + ", " + event.getY());
    int x = event.getX(), y = event.getY();

    if (isGesturesEnabled() && timerRunning) {
      // On a mouse down, if the timer is running, stop it.
      timerRunning = false;
      timer.cancel();
    }
} 
...
private void repositionView(int newX, int newY) {
  // Check to see if the view scrolled out of sight;
  // if so, bring it back in view
  if (newX > 0) {
    newX = 0;
    unitVectorX = 0 - unitVectorX;
  } else if (newX < 0 - view.getOffsetWidth() + getOffsetWidth()) {
    newX = 0 - view.getOffsetWidth() + getOffsetWidth();
    unitVectorX = 0 - unitVectorX;
  }

  if (newY > 0) {
    newY = 0;
    unitVectorY = 0 - unitVectorY;
  } else if (newY < 0 - view.getOffsetHeight() + getOffsetHeight()) {
    newY = 0 - view.getOffsetHeight() + getOffsetHeight();
    unitVectorY = 0 - unitVectorY;
  }
    
  if (isRestrictDragVertical())
    setWidgetPosition(view, xstart, newY);
  else if (isRestrictDragHorizontal())
    setWidgetPosition(view, newX, ystart);
  else
    setWidgetPosition(view, newX, newY);
}

Listing 7 is pretty straightforward. I implement a timer that calculates the new position for the map within the viewport, and then calls a helper method — repositionView() — to reposition the map.

In the viewport's mouse-up handler, I call the timer's scheduleRepeating method, which starts the timer and invokes it periodically at the TIMER_REPEAT_INTERVAL (50 milliseconds). Finally, when the mouse subsequently goes down in the viewport and the timer is running, I call the timer's cancel() method to stop the timer.

The Viewport class, at around 350 lines, is too long to list in this article, but you can take a look at the code if you download the source for the places application.


Conclusion

GWT is an exciting framework because it lets you implement anything you can imagine. With a powerful, Swing-like API, you can create rich client user interfaces that run in a browser. In this two-part article, I've shown you both the basics of GWT and some of the more advanced aspects, such as using timers and event previews. After reading this series and downloading the sample application, you are now equipped to implement your own rich user interfaces with GWT.

Although I've covered some key basic and advanced aspects of GWT in this short series, as you can imagine there's a lot more to the framework, including its recent integration with Google App Engine. Future versions of GWT will include more exciting features, including built-in support for drag-and-drop. Check out the Resources and stay tuned!


Download

DescriptionNameSize
Source code for the places applicationj-gwtfu-part1-code690KB

Resources

Learn

Get products and technologies

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=438569
ArticleTitle=GWT fu, Part 2: Beyond the basics
publish-date=10202009