by Michael Shoffner

Stepping through a site navigator applet

how-to
Jan 1, 199726 mins

Enhance your Web site with a convenient, hierarchial interface

Navigation tools can be used to improve the accessibility of a Web site by providing a clean, simple interface through which visitors can locate the deepest, darkest recesses of the site, without having to negotiate a maze of links, or having to wait for a search engine to imagine the most improbable page that happens to have a particular search string in it.

This article includes the following sections:

Overview of the problem

The navigator applet operates by opening a window displaying a list of the Web site contents. Users can easily navigate this list and select pages; the navigator displays further information about each selected page, and offers the user an option of opening up the selected page in a browser window.

Design-wise, the navigator applet should be

  • Accessible โ€” a user should be able to launch the navigator with a minimum of effort.
  • Non-intrusive โ€” the navigatorโ€™s presence in a Web site should not affect visitors who donโ€™t want to use it.
  • Easy to use โ€” a novice should be able to use the navigator.
  • Efficient โ€” code size should be kept to a minimum to reduce download time.
  • Customizable โ€” the navigator should be easy to configure for different Web sites.

In the rest of this article, we will first describe the solution that we create, and then explain the workings of classes that comprise this solution.

Using navigator

The navigator is fairly easy to use. Clicking on the navigator applet brings up the navigator window, which displays a hierarchical view of its Web site.

Elements of the navigator tree are either folders or documents. Clicking on a closed folder will open the folder, revealing the contents of the folder (more documents and folders). Clicking on an open folder will close the folder.

When you click on any element of the tree, a brief description is displayed at the bottom of the navigator window. If you wish to view the selected page, simply click โ€œGo!โ€ (or double-click on the item). Folders and documents can both correspond to pages on the Web site.

Applet parameters

There are two aspects to configuring the navigator applet: the applet parameters and the site map file. Hereโ€™s a rundown of the supported applet parameters:

  • bgcolor โ€” the applet background color, e.g. โ€œ#ffffffโ€; default gray
  • image โ€” the applet background image, e.g. โ€œmySextant.gifโ€; default โ€œimages/sextant.gifโ€
  • target โ€” the target frame name, e.g. โ€œaSubFrameโ€; default โ€œ_newโ€. Used to drive frames; otherwise use the default for a new browser window
  • title โ€” the navigator window title, e.g. โ€œMyย Siteโ€; default โ€œSextantย Navigatorโ€
  • map โ€” the site map file, e.g. โ€œmyMap.txtโ€; default โ€œsiteMap.txtโ€ (donโ€™t call it โ€œsite.mapโ€ or Apache Web servers will attempt to parse it as an image map file)
  • doc โ€” the document image, e.g. โ€œmyDoc.gifโ€; default โ€œimages/doc.gifโ€
  • dir โ€” the closed folder image, e.g. โ€œmyDir.gifโ€; default โ€œimages/dir.gifโ€
  • open โ€” the open folder image, e.g. โ€œmyOpenDir.gifโ€; default โ€œimages/openDir.gifโ€

For example, the parameters for the sample applet at the top of this page are:

<applet width=73 height=61 code="Sextant">
<param name="bgcolor" value="#ffffff">
<param name="image"   value="images/jwNav.gif">
<param name="title"   value="JavaWorld Site Map">
<param name="map"     value="/javaworld/jw-12-site-map.txt">
<param name="doc"     value="images/doc.gif">
<param name="dir"     value="images/dir.gif">
<param name="open"    value="images/openDir.gif">
</applet>

Site map file

Rather than choosing an elegant, rich, and parenthesized format for the site map file, we use an ugly, simple, but convenient format. While the former may be more aesthetically pleasing, the latter has the advantage of being easy to create and easy to parse.

Each entry in the map file must be on a separate line; each line consists of three parts separated by pipes (|). The first part is the URL for the navigator entry, the second part is the name that will appear in the navigator tree, and the third part is a description that will be displayed at the bottom of the navigator window. The tree structure is specified by preceding each line by a number of hyphens (-) that indicate the tree depth of the entry in the tree.

| |

All entries in the site map file with no preceding hyphens will thus appear at root of the navigator tree. If you want an entry to be contained within a folder, simply place it following the parent and add one more hyphen. The site map for the adjacent navigator tree follows:

http://prominence.com/ | Prominence Dot Com | Prominence Dot Com Inc.
-http://prominence.com/java/ | Java Applets | ...
--http://prominence.com/java/poetry | Electromagnetic Poetry | ...
--http://prominence.com/java/doctor | Therapy | ...
-http://prominence.com/course/ | Intensive Java | ...
--http://prominence.com/course/outline.html | Outline | ...
http://www.att.com/ | AT&T | ...
http://www.dorchester.com/ | Dorchester | ...
-http://www.doorknob.com/ | Goonyards | ...

Overview of navigator classes

The navigator applet that we develop consists of four separate classes. The first, Sextant, is the actual navigator applet. This applet is embedded in a Web page, and displays an image that the user can click on to bring up the navigator.

The next class, Navigator, is the navigator window; this is a standalone Frame that displays a list of the Web site contents.

Instead of using the built-in List class for displaying a list of the Web site contents, we use our own custom widget. Two classes perform this task: Page

a simple Canvas, displays a hierarchical view of the site contents using a helper class, Element which represents the entries in the site list.

The details of these classes are given in the following sections. If you wish to skip this, simply follow this link.

Class Sextant

Class Sextant

The Sextant class is an applet that displays a background image, and brings up a navigator window when the user clicks on it. We used a similar technique described in an earlier JavaWorld article, using the Class.forName().newInstance() method to delay the loading of the extra navigator classes until the user actually clicks on the applet.

import java.awt.*;
import java.applet.*;
public class Sextant extends Applet {

This class extends Applet so that it can be embedded in a Web page.

  Image image;
  Frame frame;

The image variable holds the background image for this applet; the frame variable holds a reference to the actual navigator window when it is created.

  public void init () {
    System.out.println ("Sextant Navigator, merlin@prominence.com, http://prominence.com/n" +
                        "Copyright (c) 1996 Prominence Dot Com Inc. All Rights Reserved.");
    String bgcolor = getParameter ("bgcolor");
    if (bgcolor != null) {
      try {
        setBackground (new Color (Integer.parseInt (bgcolor.substring (1), 16)));
      } catch (NumberFormatException ex) {
        System.out.println ("Invalid format for bgcolor: " + bgcolor);
      }
    }
    image = getImage (getDocumentBase (),
                      getParameter ("image", "images/sextant.gif"));

In the init() method, we set a background color based on the โ€œbgcolorโ€ parameter, and pick up a background image based on the โ€œimageโ€ parameter. We donโ€™t create a navigator window here; we will delay creating the navigator until the user clicks on the applet.

  public String getParameter (String param, String def) {
    String result = getParameter (param);
    if (result != null)
      return result;
    else
      return def;
  }

This method returns the specified applet parameter, or the specified default value if the parameter is not defined.

  public void paint (Graphics g) {
    if (image != null) {
      int x = image.getWidth (this), y = image.getHeight (this);
      g.drawImage (image, (size ().width - x) / 2, (size ().height - y) / 2, this);
    }
  }

The paint() method displays the background image in the center of the applet.

  public boolean mouseDown (Event e, int x, int y) {
    if (frame == null) {
      showStatus ("Loading...");
      try {
        frame = (Frame) Class.forName ("Navigator").newInstance ();
      } catch (ClassNotFoundException ex) {
        System.out.println (ex);
      } catch (InstantiationException ex) {
        System.out.println (ex);
      } catch (IllegalAccessException ex) {
        System.out.println (ex);
      }
      frame.postEvent (new Event (frame, -1, this));
      frame.pack ();
    }
    frame.show ();
    return super.mouseDown (e, x, y);
  }
}

The mouseDown() method is perhaps the only interesting method in this class. If the frame variable is null then we must create a new navigator window. We display a status message to indicate that something is happening, and then we use Class.forName().newInstance() to create an instance of the Navigator class, catching the exceptions that can be thrown.

By using this mechanism to create the navigator, the Java runtime will not automatically download the Navigator class when the user visits a Web page with this applet. This makes the page load faster, and reduces the impact on visitors who donโ€™t wish to use the applet.

The Navigator class requires a reference to our applet in order to be able to pick up parameters and easily download images. We send the navigator a new Event, through its postEvent() method, with this as the argument of the event. This indirectly supplies the navigator with a reference to our applet, and again, bypasses the need for the Navigator class to be downloaded automatically with this class.

After we have created the navigator, we can pack and show the window. The pack() method resizes the navigator window to its preferred size.

  public void stop () {
    if (frame != null)
      frame.hide ();
  }

The navigator operates by calling the showDocument() method of class AppletContext. When the user leaves a page with a particular applet present, most browsers proceed to ignore any further requests that it makes to display new pages; this typically is accompanied by the browser calling the appletโ€™s stop() method.

Because our navigator will be inactive after the stop() method is called, we hide the window to prevent the users from becoming frustrated. This may be inappropriate for some browsers that stop applets that are simply no longer visible, but if this becomes a problem then we can simply remove this method.

Class Navigator

Class Navigator

The Navigator class is a frame that contains a navigator tree with an associated scrollbar, a label that displays further information about the currently selected navigator item, and a button marked โ€œGo!โ€.

This class automatically downloads the site map file in a separate thread using the URL class; download errors are displayed in the information label.

import java.awt.*;
import java.net.*;
import java.io.*;
import java.util.*;
public class Navigator extends Frame implements Runnable {

This class is a standalone Frame; we implement the Runnable interface in order to download the site map in a separate thread.

  Sextant parent;
  Page page;
  Scrollbar bar;
  Label status;
  Button go;
  String target;

The parent variable refers to the parent applet; page is the navigator page; bar is the navigatorโ€™s scrollbar. Information about the current selection is displayed in the status label; the go button causes the current selection to be displayed in a browser frame identified by target.

  public Navigator () {
    super ("Sextant Navigator");
    setBackground (Color.black);
    setLayout (new BorderLayout (1, 1));
    Panel top = new Panel ();
    top.setBackground (Color.white);
    top.setLayout (new BorderLayout ());
    add ("Center", top);
    bar = new Scrollbar (Scrollbar.VERTICAL);
    top.add ("East", bar);
    page = new Page (bar);
    page.setFont (new Font ("Helvetica", Font.BOLD, 14));
    top.add ("Center", page);
    Panel bottom = new Panel ();
    bottom.setBackground (Color.white);
    bottom.setLayout (new BorderLayout ());
    add ("South", bottom);
    status = new Label ("Loading...");
    status.setForeground (Color.blue);
    status.setFont (new Font ("Helvetica", Font.PLAIN, 14));
    bottom.add ("Center", status);
    go = new Button ("Go!");
    go.setFont (new Font ("Helvetica", Font.BOLD, 14));
    bottom.add ("East", go);
  }

In the constructor we lay out the user interface. The layout is somewhat unusual in an effort to achieve a black line separating the top and bottom of the user interface.

We set the frame background to black and set a BorderLayout with a single-pixel inter-component spacing. We then add a white panel with some components to the top, and another white panel with some components to the bottom. The layout manager leaves a single-pixel gap between the panels, through which the black background shows.

  public void init (Sextant parent) {
    this.parent = parent;
    target = parent.getParameter ("target", "_new");
    if (parent.getParameter ("title") != null)
      setTitle (parent.getParameter ("title"));
    page.init (parent);
    new Thread (this).start ();
  }

The init() method is called by handleEvent() when the Sextant class posts the initialization event. We pick up any relevant applet parameters, initialize the navigator page and then start a new thread to download the site map file.

  public void run () {
    try {
      URL url = new URL (parent.getDocumentBase (),
                         parent.getParameter ("map", "siteMap.txt"));
      InputStream i = url.openStream ();
      DataInputStream dI = new DataInputStream (i);
      String line;
      while ((line = dI.readLine ()) != null) {
        StringTokenizer tokens = new StringTokenizer (line, "|");
        try {
          String lineUrl = tokens.nextToken ().trim (),
            lineName = tokens.nextToken ().trim (),
            lineComment = tokens.nextToken ().trim ();
          int depth = 0;
          while (lineUrl.charAt (depth) == '-')
            ++ depth;
          page.addLine (depth, new URL (parent.getDocumentBase (), lineUrl.substring (depth)), lineName, lineComment);
        } catch (Exception ex) {
          System.out.println ("Misformatted line: " + line);
        }
      }
      dI.close ();
      status.setText ("Loaded.");
      page.layout ();
      page.repaint ();
    } catch (IOException ex) {
      status.setText (ex.toString ());
    }
  }

In this method we use the URL class to download the site map file. We download the file line-by-line using a DataInputStream, and use a StringTokenizer to split each line into parts separated by pipes (|). We then strip leading hyphens (-) from the URL to determine the tree depth of the entry, and add the new entry to the navigator page through its addLine() method.

Any exceptions that arise as a result of a misformatted line (too few components) or a misformatted URL are printed to the console. Any exceptions that arise while downloading the map file are displayed in status. Once we have finished downloading a map file we repaint and relayout the navigator page; this initializes the scrollbar values.

  public boolean handleEvent (Event e) {
    if ((e.id == -1) && (e.target == this)) {
      init ((Sextant) e.arg);
    } else if ((e.id == Event.WINDOW_DESTROY) && (e.target == this)) {
      hide ();
    } else if ((e.id == Event.ACTION_EVENT) && (e.target == go)) {
      URL url = page.currentURL ();
      if (url != null)
        parent.getAppletContext ().showDocument (url, target);
    } else if ((e.id == Event.LIST_SELECT) && (e.target == page)) {
      status.setText ((String) e.arg);
    } else if ((e.id == Event.ACTION_EVENT) && (e.target == page)) {
      parent.getAppletContext ().showDocument ((URL) e.arg, target);
    } else if ((e.target == bar) && (e.id >= Event.SCROLL_LINE_UP) && (e.id <= Event.SCROLL_ABSOLUTE)) {
      page.repaint ();
    }
    return super.handleEvent (e);
  }

The handleEvent() method handles all GUI interaction. The first event is created by the Sextant class to initialize the navigator. We also handle WINDOW_DESTROY events to close the window and an ACTION_EVENT event from the go button to display the currently selected page in a browser window.

We display the page using the appletโ€™s getAppletContext().showDocument() method; the target variable allows us to drive another frame, or, as with the default, to open a new browser window with the specified page.

When the user selects an entry in the navigator page, a LIST_SELECT event is passed up with the selection information; when they double-click on an entry, an ACTION_EVENT event is passed up with the target URL and we display the page as usual.

When the user moves the scrollbar we simply repaint the navigator page; it automatically picks up the new scrollbar value.

  public Dimension preferredSize () {
    return new Dimension (320, 240);
  }

We finally provide a preferredSize() method that specifies the size at which the frame should initially be opened.

Class Page

Class Page

This class is a custom graphic widget that displays a navigator tree. It makes use of the Element class to represent elements of the tree, and supports a Scrollbar for scrolling through the tree entries.

This class and the next Element class are not the most salubrious pieces of code ever written; they violate various principles of object-oriented programming, however in my defense I would like to quote a local professor:

For a successful software engineering methodology, pragmatics must take precedence over elegance, for Nature cannot be impressed.

โ€” Cogginsโ€™ Law of Pragmatic Software Engineering
import java.awt.*;
import java.util.*;
import java.net.*;
public class Page extends Canvas {

This class is a Canvas; this is the usual base class for custom graphical widgets.

  Sextant parent;
  Scrollbar bar;
  Image doc, dir, open;
  Element root, selected;
  Stack tree = new Stack ();
  int treeDepth = 0;

The parent variable refers to the parent applet; bar refers to the companion scrollbar in the parent container, and the three images used by the navigator are held in doc, dir, and open.

The root of the navigator tree is held in the root variable; the current selection is held in selected, and the tree and treeDepth variables are used in the construction of the navigator tree.

  public Page (Scrollbar bar) {
    this.bar = bar;
  }

In the constructor, we just obtain a reference to the companion scrollbar.

  public void init (Sextant parent) {
    this.parent = parent;
    doc = parent.getImage (parent.getDocumentBase (),
                           parent.getParameter ("doc", "images/doc.gif"));
    prepareImage (doc, this);
    dir = parent.getImage (parent.getDocumentBase (),
                           parent.getParameter ("dir", "images/dir.gif"));
    prepareImage (dir, this);
    open = parent.getImage (parent.getDocumentBase (),
                            parent.getParameter ("open", "images/openDir.gif"));
    prepareImage (open, this);
  }

The init() method initializes the navigator images; we get the images with the getImage() method, and then begin their download by calling the prepareImage() method,

  int docW, docH, dirW, dirH, openW, openH, textH, textA;
  void getDimensions () {
    docW = doc.getWidth (this);
    docH = doc.getHeight (this);
    dirW = dir.getWidth (this);
    dirH = dir.getHeight (this);
    openW = open.getWidth (this);
    openH = open.getHeight (this);
    textH = getFontMetrics (getFont ()).getHeight ();
    textA = getFontMetrics (getFont ()).getAscent ();
  }

This method is used to determine some dimensions that are relevant to the navigator applet; they are, respectively, the dimensions of all of the images, and the height and ascent of the current navigator display font.

  public void layout () {
    if ((size ().height > 0) && (root != null)) {
      getDimensions ();
      int h = root.getHeight () + 3;
      bar.setValues (bar.getValue (), size ().height, 0, h - size ().height);
      bar.setLineIncrement (textH);
      bar.setPageIncrement (size ().height);
    }
  }

This method is called to lay out this component โ€” typically, when the applet is resized. If the navigator contents are non-null and the applet has a non-zero height, then we reset the scrollbar values.

We first call getDimensions() to pick up the dimensions of the navigator components; the results of this are then used by the getHeight() method of class Element to determine the height of the navigator tree. We then set the scrollbar values appropriately; we keep the same position, adjust the current scrollbar window to be the height of this component and set the maximum value to match the end of the tree. We also supply appropriate new line and page increment values.

  public void addLine (int depth, URL url, String name, String comment) {
    Element element = new Element (url, name, comment, this);
    if (root == null) {
      if (depth != 0)
        throw new RuntimeException ("Initial depth non-zero.");
      else
        tree.push (root = element);
    } else {
      if (depth == treeDepth) {
        tree.push (((Element) tree.pop ()).sibling = element);
      } else if (depth > treeDepth) {
        if (depth > treeDepth + 1)
         throw new RuntimeException ("Depth change error: " + name);
        else
          tree.push (((Element) tree.peek ()).offspring = element);
      } else if (depth < treeDepth) {
        for (int i = depth; i < treeDepth; ++ i)
          tree.pop ();
        tree.push (((Element) tree.pop ()).sibling = element);
      }
      treeDepth = depth;
    }
  }

Hereโ€™s where it gets hairy. This method adds an entry to the navigator tree.

We first create a new entry with the specified parameters. If root is null, then weโ€™re starting a new tree. We ensure that depth (the tree depth of the new entry) is 0; the first item in a tree must be at the root level. We then assign root to be the new element, and push this onto tree. The tree stack is a stack corresponding to the current location in the navigator tree; this is used while building the tree structure. At any time, the elements on the stack correspond to a path from the most-recently-added element to the root of the tree.

Otherwise, if the tree depth of the new element is the same as the depth of the current location in the tree (treeDepth) then this element is a sibling (brother/sister) of the current element. We pop the old element off the tree, assign its sibling reference to point to the new element, and then push the new element onto the tree.

If the tree depth of the new element is greater than that of the current location in the tree, then we are adding a child. We ensure that we are jumping just one level in the tree (you canโ€™t have a grand-child before you have a child), assign the current tree element a new offspring, and push the new element onto the stack.

Finally, if the tree depth of the new element is less than that of the current location, then we are adding a sibling to an ancestor; we pop elements off the stack until we reach the appropriate level, and then add a sibling as before.

We finally assign the new depth of the current location in the tree appropriately.

Note that the exceptions that we throw are caught by the Navigator class and displayed in the status label.

  public URL currentURL () {
    if (selected == null)
      return null;
    else
      return selected.url;
  }

This method returns the URL of the currently selected item, or null if none is selected.

  public void paint (Graphics g) {
    if (root != null) {
      getDimensions ();
      root.paint (g, -1, -bar.getValue (), new Point (10, 2 - bar.getValue ()));
    }
  }

This method paints the navigator tree; we call getDimensions() on behalf of the Element class, and then call rootโ€˜s paint() method.

We supply parameters g, the Graphics context onto which to draw, an โ€œoldโ€ x and y coordinate that is off the edge of the canvas, and a โ€œnewโ€ Point location at which the first tree element will be drawn. See the Element classโ€™s paint() method for details.

Note that we draw the navigator at a vertical position -bar.getValue(); as you move the scrollbar down, we draw the navigator higher and higher. Hence by repainting the page whenever the scrollbar is moved, the navigator appears to scroll.

  public boolean mouseDown (Event ev, int x, int y) {
    if (root != null) {
      boolean doRepaint = false;
      getDimensions ();
      Point xy = new Point (10, 2 - bar.getValue ());
      Element l = root.locate (x, y, xy);
      if ((l != null) && (l.offspring != null) && (x < xy.x + openW)) {
        l.open = !l.open;
        layout ();
        doRepaint = true;
      }
      if (selected != l) {
        if (selected != null)
          selected.selected = false;
        selected = l;
        if (selected != null)
          selected.selected = true;
        doRepaint = true;
        postEvent (new Event (this, Event.LIST_SELECT, (selected != null) ? selected.comment : ""));
      } else if ((selected != null) && (ev.clickCount == 2) &&
                 ((selected.offspring == null) || (x >= xy.x + openW))) {
        postEvent (new Event (this, Event.ACTION_EVENT, l.url));
      }
      if (doRepaint)
        repaint ();
    }
    return super.mouseDown (ev, x, y);
  }

Yet more hair; this time, to process mouse clicks. As always, we call getDimensions(). We then call rootโ€˜s locate() method; this method locates the navigator entry containing the specified mouse location, and updates xy to be the canvas position of this entry.

If an element was located and the element has children (itโ€™s a folder) and the click was over the elementโ€™s folder icon, then we open the folder and repaint and layout the tree. We use the doRepaint variable to ensure that we donโ€™t potentially repaint twice, as this causes undue flicker on certain platforms.

If the user has selected a new entry, then we deselect the old entry, set the current selection to the new entry, select the new entry, repaint the navigator, and post a LIST_SELECT event that displays information about the new selection.

Otherwise, if the user has double-clicked on an entry, and they are not double-clicking on its folder icon, then we post an ACTION_EVENT event that displays the selected URL.

Class Element

The final class in this saga, the Element class, represents elements of the navigator tree. Recursive methods are supplied to traverse the tree.

import java.awt.*;
import java.util.*;
import java.net.*;
public class Element {

The Element class is a simple datatype that includes an entryโ€™s URL, name, information, and other internal data, as well as pointers that describe the tree structure.

  Page parent;
  URL url;
  String name, comment;
  boolean open, selected;
  Element sibling, offspring;

The parent variable refers to the parent page; url, name and comment are the entryโ€™s details. The open variable indicates that a folder is open, and the selected variable indicates that an entry is selected.

Element pointers

Each element in the tree has two pointers (references) to maintain the tree structure; sibling refers to the next tree element at the same level, and offspring refers to the first child of this element. If you follow your offspringโ€™s sibling list, then you can determine all of your immediate children.

An element tree
The corresponding tree structure
  public Element (URL url, String name, String comment, Page parent) {
    this.parent = parent;
    this.url = url;
    this.name = name;
    this.comment = comment;
  }

In the constructor, we initialize the entryโ€™s state.

  int myHeight () {
    int h = (offspring == null) ? parent.docH :
      (open) ? parent.openH : parent.dirH;
    return (parent.textH > h) ? parent.textH : h + 1;
  }

This method returns the height of this entry, not including its children. This is the maximum of the height of its current image (it is a document if it has no children, otherwise it is an open or closed folder) and the height of the text font. Note that all of these values are pulled from the parent; they are the values that are initialized by Pageโ€˜s getDimensions() method.

  public int getHeight () {
    int h = myHeight ();
    if (open && (offspring != null))
      h += offspring.getHeight ();
    if (sibling != null)
      return h + sibling.getHeight ();
    else
      return h;
  }

This method returns the height of this entry, including its children (if it is an open folder) and siblings. Thus the result is the height of this entry plus the height of its first offspring (which includes the height of any further offspring) plus the height of its sibling (which includes the height of any further siblings).

  Element locate (int mX, int mY, Point xy) {
    int x = xy.x, y = xy.y, h = myHeight (),
      w = (offspring == null) ? parent.docW : (open) ? parent.openW : parent.dirW;
    if ((mX >= x) && (mY >= y) && (mY < y + h) &&
        (mX < x + w + 4 + parent.getFontMetrics (parent.getFont ()).stringWidth (name)))
      return this;
    xy.translate (0, h);
    if (open && (offspring != null)) {
      xy.translate (w + 4, 0);
      Element l = offspring.locate (mX, mY, xy);
      if (l != null)
        return l;
      xy.translate (-(w + 4), 0);
    }
    if (sibling != null)
      return sibling.locate (mX, mY, xy);
    else
      return null;
  }

This method locates the entry that contains the specified mouse click mX, mY. The Point xy is a reference to the canvas coordinates of this entry.

If this entry contains the click, then we appropriately return this.

Otherwise, we translate xy down by the height of this entry (because the next item will be drawn below it), and if it is an open folder then we search its offspring. We translate xy across by the appropriate amount (slightly more that the width of the folder icon) and call offspringโ€˜s locate() method. If the result of this is non-null, we return the located entry; otherwise, we translate xy back and search this entryโ€™s sibling.

  void paint (Graphics g, int oX, int oY, Point xy) {
    int x = xy.x, y = xy.y, h = myHeight (),
      w = (offspring == null) ? parent.docW :
                                (open) ? parent.openW : parent.dirW;
    xy.translate (0, h);
    if ((oY < parent.size ().height) && (y + h / 2 > 0))
      g.drawLine (oX, oY, oX, y + h / 2);
    if ((y < parent.size ().height) && (y + h > 0)) {
      g.drawLine (oX, y + h / 2, x + w / 2, y + h / 2);
      Color c = g.getColor ();
      if (selected)
        g.setColor (Color.red);
      g.drawString (name, x + w + 4, y + (h - parent.textH) / 2 + parent.textA);
      if (selected)
        g.setColor (c);
    }
    if (open && (offspring != null)) {
      xy.translate (w + 4, 0);
      offspring.paint (g, x + w / 2, y + h / 2, xy);
      xy.translate (-(w + 4), 0);
    }
    if ((y < parent.size ().height) && (y + h > 0)) {
      if (offspring == null)
        g.drawImage (parent.doc, x, y + (h - parent.docH) / 2, parent);
      else if (open)
        g.drawImage (parent.open, x, y + (h - parent.openH) / 2, parent);
      else
        g.drawImage (parent.dir, x, y + (h - parent.dirH) / 2, parent);
    }
    if (sibling != null)
      sibling.paint (g, oX, y + h / 2, xy);
  }
}
Offspring skeleton lines
Sibling skeleton lines

The paint() method is similar to the previous method in the manner that it recurses the tree. We wish to draw the skeleton lines that trace the tree hierarchy, draw the relevant icon for this entry, and draw the text for the entry. The parameters oX and oY are used to draw the skeleton lines from this entry to its parent. For the first offspring of an entry, they refer to the center of the parentโ€™s icon; for subsequent siblings, they join up to the previous siblingโ€™s skeleton lines.

We first draw a vertical skeleton line from this element to its parent or previous sibling (to optimize redraw, we do this only if it intersects the display area). We next draw a horizontal skeleton line to the center of this entryโ€™s icon. We switch to red if this entry is selected, and then draw the text for this entry.

Next, we recursively called offspringโ€˜s paint() method as we recursed the locate() method. After this returns, we draw the appropriate icon for this entry and then call siblingโ€˜s paint() method.

The order in which we assemble this drawing may seem strange; why not just draw the icon with the rest of the stuff for this entry? If we did this, then our offspringโ€™s vertical skeleton line would overlap our own icon. The ordering of the other drawing commands is unimportant.

A navigator applet can make a Web site much more accessible because it presents a simple, friendly user interface that can bypass the maze of links that constitutes a typical Web site. We could have used the standard AWT components to implement this applet, but we can effect a much more pleasing and much more powerful interface by developing our own custom widgets.

The cost of using custom widgets is typically in download time, however this entire applet weighs in at a little under 14KB; just over half the size of last monthโ€™s JavaWorld image map.

There are some obvious extensions to the user interface that might be worth considering. The first is to optimize the redraw code to minimize flicker; this is a particular problem when scrolling a large navigator tree. In addition, support for the arrow keys and page up / page down would be convenient.

Generating a site map for a continually changing Web site can be a burden, however the possibility of automating this task exists. A variant of Laurence Vanhelsuweโ€™s Java Web crawler (JavaWorld, November 1996) could be used to crawl your own Web site automatically and create a sitemap file based on the titles of the documents it finds.

Michael Shoffner has extensive experience in cryptography, networking, and computer graphics and has published and lectured on these topics. He is the vice president of technology at Prominence Dot Com, a North Carolina company formed to create interactive products for the Web.