Java User-Interface

Design

Interactive Java applets for Web searching

Jeffrey Kay

Jeffrey specializes in software design and development for IBM. He can be contacted at jeffrey_kay@ vnet.ibm.com.



IBM infoMarket is one of the first major web-based search services to implement a user interface written entirely in Java. This Java interface accomplishes two things. First, it provides advanced capabilities similar to those available in stand-alone applications. Second, the software demonstrates Java's capabilities in a serious and significant application. Some of the most interesting Java software on the Internet is presented as more of a window decoration than a primary interface. This Java interface brings users functionality previously found only in software nat ively installed on the users' computers.

IBM infoMarket (http://www.infomarket.ibm.com/) lets users search multiple source databases simultaneously. These databases are categorized by subject area and name, providing a way to select which sources are searched. Users may perform simple searches by entering keywords or enter a more-advanced Boolean query. The search results in a "hit" list that returns the title of the content, an exhibit, date, author, relevance rank, and price.

The original IBM infoMarket interface uses HTML forms. The simple search screen includes an edit box for keywords and the source list coded as a series of check-box list elements. The search returns a hit list as an HTML page, 25 hits at a time. The original interface, including the generation of the search screen, source list, hit list, and user-requested data reductions, is implemented almost entirely as CGI scripts.

While HTML and CGI comprise a viable approach, a more interactive interface enhances usability. The HTML version requires users to page forward and backward to refine a query because the search page and the results page are separate. And because the results are all page oriented, the hit list is limited to a quantity that is reasonably visible on a screen.

The design goals of the Java version were to improve the overall usability and to increase functionality. The hierarchical nature of the sources is easily represented by a hierarchical listbox with expanding and collapsing entries. This listbox allows users to see just topic areas or to delve deeper into the available sources for a particular topic area. Users can even mix and match topic areas and particular sources if it makes sense for a query.

Users can experiment with different ways of formulating queries by toggling between the simple search and advanced search. The search-entry fields retain the terms as users try different searches to see which results in the best hit list.

By displaying the results as a multicolumn listbox or grid control, users can access a wider range of functions, including sorting and selecting. In addition, a multicolumn listbox can hold many more lines than might make sense for HTML-based output.

In the end, the user interface should adopt the style of most interactive applications, with the search query, source list, and results list residing on the same screen. Users then have all of the information in view when searching.

Figure 1 is the enhanced, Java-based IBM infoMarket user interface. The search-word entry portion of the window flips between the simple search and the advanced search. This source list appears to the right in an expanding and collapsing entry listbox. The results are in the bottom part of the panel in a single multicolumn listbox. In the center of the listbox are the buttons used to control the operations and a message label indicating the status of the operation.

Basic Applet Structure

The basic applet structure is a single flow from the entry of search terms to the review of results. The interface to the web server is implemented through CGI scripts. In early implementations, a CGI script parsed the HTML pages returned by our production system, but later versions of the software use CGI scripts that return "raw" output in a format easily parsed in the Java code. All CGI interfaces are implemented using the GET method to avoid the additional code necessary to implement a POST in Java.

The sources listbox is a derivation of the standard Java ListBox class. The listbox keeps track of which list elements are expandable and which are not. In addition, the titles of the sources are matched with the terms that are returned as part of the CGI call to the web server when executing a search.

The entire screen display is implemented using a series of panels. The outermost panel and all but two of the inner panels use the GridBagLayout Manager. Although it is the most complicated layout manager in the Java Abstract Windows Toolkit (AWT), the GridBagLayout Manager is also the most functional when the user interface requires precise layout instructions. It allows each component in the panel to be placed precisely with respect to its neighbors. The only panels that do not use the GridBagLayout are the Message panel (BorderLayout) and the Button panel (FlowLayout). Figure 2 shows each of the panels in the display.

To easily switch between the simple and advanced search options, each entry screen is built into a separate panel. When pressing the toggle button (designated "Simple search" or "Advanced search" depending on what is currently displayed), the applet quickly rebuilds the interface using the appropriate display panel. The rebuilding of the display is also aided by having each of the main interface components in its own panel, reducing the complexity of the code. Figure 3 is a subsection of the search screen with the advanced-search panel visible.

Threads

Threads play a minor role in the applet, implementing the "cancel search" functionality and the asynchronous collection of status information during a search. The use of threads allows the applet to perform some minor functions without polluting object encapsulations in the code.

The cancel search function uses a thread to allow the search operation to run independently. The applet can then continue processing messages in the message queue, a necessary function when user input is required. When a search starts (a user presses the "Search" button), the search thread starts and the Search button is retitled "Cancel search." All other buttons are disabled. If the Cancel search button is pressed, the search thread is stopped using a Thread.stop() function call.

The status functionality is implemented in a similar fashion. The search code is fully encapsulated in a single object, a derivation of the multicolumn listbox class. That class initiates the search CGI call, retrieves the results, and displays them. The search thread, while running, polls that object every half second to find out how many elements it currently has in the list. This method informs the user that something is happening while the search results are retrieved, and does not burden the Java applet significantly.

The Multicolumn Listbox

The most interesting class in the applet is the multicolumn listbox (MCList). While not the most ideal or efficient implementation of a basic grid control, the code employs many of the more subtle Java language elements. In particular, a C++ software developer may not be familiar with some of these techniques because they are unavailable in C++. It is also the one place in the code that extensive work had to be done to ensure compatibility across platforms.

The multicolumn listbox is implemented as the public class MCList (available electronically), which encapsulates three subcomponents: horizontal and vertical scroll bars (MCScrollbar) and MCCanvas, an extension of the Canvas class. In addition, two public interface classes allow a developer to implement "owner-drawing" and "owner-defined sorting" objects that can be used in a cell.

MCList

MCList is an extension of the Panel class that implements many of the "pass-through" function calls. Since most of the functionality of the class resides in MCCanvas, MCList must expose certain function calls to provide a single interface point. An alternate approach would have been to build most of the functionality into MCList directly, but the encapsulation of the drawing routines in the MCCanvas class reduces the complexity of the code and takes advantage of the layout capabilities in Java. The constructors in MCList set up the number of rows and columns that the listbox must initially display. While the number of rows is variable, there are no provisions in this implementation to add columns. The validate() and setFont() functions have been overridden to ensure that the horizontal scroll bar limits are set properly. The vertical scroll bar increments by row count rather than pixel count, so its limits are only updated when the row count changes.

MCScrollbar

Perhaps the most frustrating thing about Java is its relative infancy. MCScrollbar is a class designed to address just that issue. Because of the variations in Java implementations, the limits and line- and page-increment values of the default scroll bars were inconsistent. To work around these problems, the MCScrollbar class overrides several of the Scrollbar class functions and replaces them with equivalent functions that work better across platforms.

MCPainx and MCSortable Interface Classes

Interface classes define functional interfaces that allow an object to provide functionality without having to define a new superclass. Specifically, the MCSortable and MCPaintable interface classes (Listings One and Two, respectively) allow an object stored in a cell to provide functions for painting and sorting itself even though the object's base class is Object. These classes are used to display the icon in the header row, to draw the description column with bold and plain fonts, and to sort columns alphabetically or numerically. The MCImage and TitleAbstract classes, private classes implemented as part of the main program, demonstrate the use of these classes (Listing Three).

The MCImage class defines an extension to the Object class that implements MCPaintable to draw a picture in a cell area. The MCPaintable class requires that the paint() function be implemented, which in this case just draws the image on the supplied Graphics object. Because the class is derived from the base Object class, it can be used as the contents of a cell.

The TitleAbstract class is more complicated, implementing both the MCPaintable and MCSortable interfaces. This is as close to multiple inheritance as Java permits. The TitleAbstract class extends Object (as required for use with MCList) and implements the toString() function to return an appropriate value. The paint() function draws the title string in bold while the abstract part is drawn using a plain text face. The lessthan() and greaterthan() functions compare the joined title and abstract to the string value of any other object.

MCCanvas

MCCanvas is the centerpiece of the multicolumn listbox. It draws the rows and maintains the values of the cells. Each cell value must be derived from the Object class and all values are stored in Vectors, one per column. The column headers are also stored in a single Vector and one additional Vector is defined to store the sort index. Because the widths of the columns are fixed, a single array of integers is defined to hold the width of each column. The width of a column is defined in characters based on the average width of the characters in the selected font.

As rows are added, cell values are set to zero-length Strings. The cell values are set individually using the setValue() function, where the arguments are the row, the column, and the Object to be stored in the cell. As cells are set, they are painted using the paintCell() function. This function is called directly, rather than using a repaint() call, to update the display synchronously.

The paint() function itself is split into several function calls so that individual parts of the display can be drawn as needed or as part of a global update. The paint() function calls the drawing routines for all of the display components:

The paintHeaders() function does not call paintCell() even though paintCell() duplicates much of paintHeaders()'s functionality. The paintHeaders() function does some additional manipulation of the cell contents before it is displayed (it adds "**" to the primary sort-column header), preventing the use of paintCell() in its present form.

The paintCell() function draws the contents of each cell. Because of the possibility of a cell implementing its own painting routine, paintCell() must check the object before it is drawn to see if it implements the MCPaintable interface class. It does this using the instanceof operator, an operator that can test the object to see what classes it instantiates. If the object is an instance of the MCPaintable class, it is recast and its paint() function is called. If the object is just a string, it is automatically fit into the cell area using word wrapping. Figure 4, an excerpt from paintCell(), shows this code.

The paintCell() function uses the create() method of the Graphics class to create subsections for drawing. This is required because only one clipping rectangle can be established for a Graphics object. When paintCell() is called, a new Graphics object is created based on the original Graphics object. This new Graphics object clips to the dimensions of the cell and is used for all subsequent graphics operations.

Sorting is implemented in MCCanvas using a low-tech insertion sort. Though there are many container classes provided with Java, no sorting functions are provided. The sort() function is called explicitly by the owner of the MCList object. During a sort, each object is tested with instanceof to see if it implements MCSortable; if it does, the lessthan() and greaterthan() functions of MCSortable are used for comparisons. If not, the object is sorted based on the value of the toString() function.

Lessons Learned

I encountered several problems using Java as a development language and environment. The good news, however, is that these are not inherent problems; almost all stem from Java's relative immaturity. As Java matures, many of these problems should go away.

The development platform was the Java Development Kit (JDK) Version 1.02 under Windows 95 with Netscape Navigator as the target platform. I wrote all of the code by hand using an editor and a make program, and used the JDK Appletviewer for development testing. When it became apparent that Navigator and the JDK Appletviewer did not function exactly the same, I was forced to use Navigator more extensively and earlier in the process than I desired. In most cases, the Appletviewer did a better job displaying the applet than Netscape Navigator.

Netscape Navigator 2.02 showed symptoms of memory problems. When the multicolumn listbox reached 100 elements, Netscape Navigator crashed. As a result, I reset my sights for Navigator 3.0 and beyond.

The showDocument() function is the Java function that retrieves documents in the search-results list. Because of a bug in early betas of Navigator 3, any document retrieved that was processed by a helper application crashed the browser. I developed a kludge using JavaScript. Instead of using the showDocument() function directly, showDocument() calls a JavaScript function and passes the document URL to that function. This changed the call from

showDocument("http:// xxx.xxx.xxx.xxx/ xxx.xxx"); to

showDocument("javascript:showdoc(\"http:// xxx.xxx.xxx.xxx/xxx.xxx\"));.

Of course, this prevented other browsers from retrieving documents, since only Navigator currently implements JavaScript. Navigator 3.0 beta 5 cured this problem.

Scroll bars continue to be a source of frustration. Each platform seems to have a different idea about the scroll-bar limits and page- and line-increment values. Some betas of Navigator had a bug that would not allow the page- and line-increment values to be changed from their defaults of 10 and 1, respectively. I coded MCScrollbar to eliminate this inconsistency by trapping most of the scroll-bar value sets.

Double clicking on a Canvas also proved troublesome. The handleEvent() function in MCList has several workarounds to address this problem. This problem initially manifested itself on UNIX/X-Windows and Macintosh platforms, where the double click could not be trapped, but even Navigator 3.0 beta 5 for Windows 95 exhibited this problem. The initial workaround counted Event.MOUSE_DOWN events, but this also proved inconsistent. A second workaround counted Event.MOUSE_UP events and this worked better. There are still double-click problems on some platforms.

A strange problem with the setFont() function occurred while building the paint() routine for the TitleAbstract class. Apparently, doing a setFont() while clicking on a scroll bar could cause a thread (or the main thread) to freeze. I attempted to correct the problem by disabling the multicolumn listbox during screen painting. This worked in most cases. Fortunately, it appears that this problem has vanished from later betas of Navigator 3.0.

Font problems across platforms also seem to exist. Java uses the "a-point-a-pixel" model: One point equals one pixel. On some Navigator implementations, the font metrics returned from a selected font were occasionally incorrect, sometimes even twice that of the selected font. The text on the screen looked okay and demonstrated that the system was returning the correct font size, but the font metrics that the Java Virtual Machine returned were wrong. I found no particular workaround that was acceptable.

On Navigator 3.0 beta 5, invalidating the windows did not always result in a repaint() call. I corrected this by adding a repaint() call after the invalidate(). In addition, to prevent flickering on screen updates, the update() function of a component is overridden to just call paint(). This prevents the update() function from clearing the component area.

Conclusion

The IBM infoMarket Java user interface demonstrates great potential as a primary user interface. It improves usability and increases functionality. As the Java environment matures, this user interface and others like it will drive the World Wide Web into the universe of interactive applications.

Figure 1: IBM infoMarket service Java search screen.

Figure 2: Panels of the main display (highlighted in blue).

Figure 3: Subsection of search screen showing advanced search panel.

Figure 4: Code excerpt from paintCell() showing the use of instanceof.

if (values[col].elementAt(realRow(row)) instanceof MCPaintable) {
MCPaintable x = (MCPaintable)values[col].elementAt(realRow(row));
x.paint(newg,this,widths[col]*aveWidth+Inset,Inset);
}

Listing One

import java.awt.*;
import java.awt.image.*;
import java.applet.*;
import java.net.*;
import java.util.*;
import java.io.*;

// Code is provided "AS IS" for example purposes only, without warranty 

public interface MCPaintable {
    abstract void paint(Graphics g, Object o,int width, int inset);
}

Listing Two

import java.awt.*;
import java.awt.image.*;
import java.applet.*;
import java.net.*;
import java.util.*;
import java.io.*;

// Code provided "AS IS" for example purposes only, without warranty 
public interface MCSortable {
    abstract boolean lessthan(Object o);
    abstract boolean greaterthan(Object o);
}

Listing Three

import java.awt.*;
import java.awt.image.*;
import java.applet.*;
import java.net.*;
import java.util.*;
import java.io.*;

// Code provided "AS IS" for example purposes only, without warranty 

class MCImage extends Object implements MCPaintable {
    Image image;
    public MCImage(Image i) {
        image = i;
    }
    public void paint(Graphics g, Object o,int width,int inset) {
        if (g == null) return;
        if (o instanceof ImageObserver) {
            g.drawImage(image,inset,0,(ImageObserver)o);
        }
    }
    public String toString() {
        return "Cryptolope";
    }
}
class TitleAbstract extends Object implements MCPaintable, MCSortable {
    String Title = "";
    String Abstract = "";

    public TitleAbstract(String t, String a) {
        Title = t;
        Abstract = a;
    }
    public String toString() {
        return Title + " " + Abstract;
    }
    public boolean lessthan(Object o) {
        return toString().compareTo(o.toString()) < 0;
    }
    public boolean greaterthan(Object o) {
        return toString().compareTo(o.toString()) > 0;
    }
    // void wrapString(Graphics g,String s,int w)
    
    public void paint(Graphics g, Object o,int width, int inset) {
        if (g == null) return;
        width -= inset;
        if (width <= 0) return; // hidden row
        String temp = "";
        FontMetrics fm = null;
        Font f = g.getFont();
        
        int col = 0;  // x coordinate
        int y = 0;  // y coordinate
        int lineheight = 0; // lineheight
        
        Font newFont = new Font(f.getName(),Font.BOLD,f.getSize());

        for(int i=0;i<2;i++) {
            StringTokenizer st = null;
            if (i == 0) {
                g.setFont(newFont);
                st = new StringTokenizer(Title);
                fm = g.getFontMetrics();
                y = fm.getMaxAscent();
            }
            else {
                g.setFont(f);
                fm = g.getFontMetrics();
                st = new StringTokenizer(Abstract);
            }
            lineheight = fm.getMaxAscent() + fm.getMaxDescent();
            while (st.hasMoreTokens()) {
                String tok = st.nextToken();
                if (temp == "") {
                    temp = tok;
                }
                else {
                    if (fm.stringWidth(temp + " " + tok) < width - col) {
                        temp += " " + tok;
                   }
                    else {
                      if ((col + fm.stringWidth(temp) > width) && (col != 0)) {
                          col = 0;
                          y += lineheight;
                          if (fm.stringWidth(temp + " " + tok) < width - col) {
                                g.drawString(temp,col,y);
                                col = 0;
                                y += lineheight;
                                temp = tok;
                            }
                        }
                        else {
                            g.drawString(temp,col+inset,y);
                            col = 0;
                            y += lineheight;
                            temp = tok;
                        }
                    }
                }
            }
            if (temp != "") {
                g.drawString(temp,col+inset,y);
                col += fm.stringWidth(temp)+fm.stringWidth("XX");
                temp = "";
                if (col > width) {
                    col = 0;
                    y += lineheight;
                }
            }
        }
    }
}