ORIGINAL DRAFT

There are numerous cases in which the progress of a sequence of events need to be conveyed to end users in a manner that clarifies what’s going on. This is a common requirement for installation programs, diagnostic software, automated updates, and protocols, to name just a few. This month, we’ll build a simple component that makes it easy to show users the progress and history of a sequence of events.

Figure 1: JSequence shows progress as the
fourth item is being processed.

Figure 1: JSequence shows progress as the fourth item is being processed.

As always, we want to maximize flexibility and reusability while keeping the design as simple as possible. There are a some questions that come to mind when considering how to have the items in a sequence report their progress. We want this to be as uncomplicated as possible, but there are several way to approach this.

Since we want to report progress using a JProgressBar for each item, we could pass a reference to it and leave it up to developers to set the minimum, maximum and current values as processing continues. With this approach the sequence of events would be handled completely outside the JSequence component, which would then merely acts as a view to show what was going on. That turns out to be more work than we’d like to impose on developers.

What we really want is a simple mechanism that would let us register sequence items with JSequence and have them report their progress as JSequence triggers each in turn. To do this, we need a notification mechanism, typical of a good model, and a class with default behavior that we can inherit from to simplify the development process. Developers need only implementing an interface to qualify a class as a sequence item.

Here’s what the interface looks like:

public interface SequenceItem
{
  public void addSequenceListener(SequenceListener listener);
  public void removeSequenceListener(SequenceListener listener);
  public String getText();
  public void execute();
}

Notice that we support adding and removing listeners and allow a text value to be retrieved. The text is used to show users a description of the item. The execute method is called when the item’s turn comes up in the sequence.

The SequenceListener interface is also simple:

public interface SequenceListener
{
  public void progressReady(SequenceEvent event);
  public void progressUpdate(SequenceEvent event);
  public void progressDone(SequenceEvent event);
}

JSequence will receive notification when an item’s execution starts with a progressReady message. As progress continues, several progressUpdate messages will be received and progressDone should be called at the end of the item’s execution. The execute method should be a non-blocking call, such that it returns immediately, typically starting a thread to do the work it needs to accomplish. It’s only when the progressDone message is received that the next item’s execute method will be called.

We won’t list the SequenceEvent class, which merely extends the Swing ChangeEvent class and adds two arguments, one to maintains the state and another to hold an integer value. The states are declared in constants named: STATE_READY, STATE_UPDATE, and STATE_DONE, mapping directly to the methods of the SequenceListener interface. The values represent different things, depending on the state.

With STATE_READY, the value must be the maximum value of the progress bar. With STATE_UPDATE, the value is the current relative value. It is assumed that the progress bar’s minimum value is zero and that the maximum value was passed when the progressReady listener method was triggered. The progressUpdate method will typically be called several times as progress continues. With STATE_DONE, we expect the maximum value again. This serves to confirm that we are in fact at the last possible value and to make sure that the associated progress bar is at the maximum value when we’re done.

Figure 2: JSequence classes can be divided into
SequenceItem objects, notification infrastructure and the visual representation classes.

Figure 2: JSequence classes can be divided into SequenceItem objects, notification infrastructure and the visual representation classes.

When you look at the class diagram in Figure 2, you’ll recognize the SequenceItem interface, SequenceListener and SequenceEvent classes we’ve already talked about. SequenceItemList is there to contain a list of SequenceItem objects in the order they will be executed. Three methods are exposed: addSequenceItem, removeSequenceItem and getSequenceItem, the last of which is used by JSequence to fetch the next SequenceItem when it’s ready.

The SequenceItemView is shown in Listing 1. It takes responsibly for drawing a given item’s progress, complete with the text description and state icons. Let’s take a closer look.

The SequenceItemView is effectively a JLabel and JProgressBar component. The code provides instance variables for each as well as a set of predefined ImageIcon instances to represent blank, ready, update and done icons. We expect to find these in a subdirectory called images.

The constructor initializes our JLabel with a blank icon and empty string. We put the label in the NORTH part of a BorderLayout and a JProgressBar in the CENTER. For esthetic reasons, we add an empty border for spacing, make the label and progress bar transparent to use a single background color for the view and use an empty border to place the progress bar slightly indented to the right. This helps align the text and progress bar to the right of the icon.

Notice that the last thing the constructor does is make the component invisible. This is important, because we intend to show the views only when it is time for the SequenceItem to execute.

When the item becomes visible in a scroll pane, it may be positioned below the scrolling area, so we implement a scrollToVisible method that uses the scrollRectToVisible method in a JViewport. For this to work, however, we have to determine if there is a JViewport in this objects parentage and then make a suitable call. Notice that we proactively avoid making the call when the rectangle is already visible to avoid flickering or other unexpected display problems.

The next three methods implement the SequenceListener interface. We respond to a progressReady call by setting the text from the SequenceItem in the JLabel, setting the ready icon and then initializing the progress bar with suitable minimum, maximum and current value settings. This is where we make the component visible and call scrollToVisible to be sure any necessary scrolling takes place.

The progressUpdate method makes sure the update icon is active and updates the progress bar value with the current value of the SequenceEvent. Just in case the view is not yet visible we call scrollToVisible again. Since the method checks to be sure the code is not needlessly executed, this is a small price to pay to ensure proper visibility.

The final method from the SequenceListener interface is progressDone, which sets the progress bar to its maximum value, sets the icon to done and waits 100 milliseconds to be sure the interface has a chance to draw the final state of the view. You’ll notice that there is no flow of control to move the sequence forward in the SequenceItemView. That gets taken care of in the JSequence component itself.

Let’s take a look at the AbstractSequenceItem class, in Listing 2, which provides the basic infrastructure for building a SequenceItem subclass. Because we use a SequenceItem interface, developers are free to implement these methods directly, but I expect you’ll typically want to keep all that the extra work to a minimum.

A SequenceItem has very little in the way of state to maintain, only the text needs to be retrieved when the getText method is called. The constructor reflects this. Most of the work it does is meant to handle SequenceListener registration, removal and firing the three events supported by a SequenceListener.

We use an ArrayList to manage listeners. The add and remove methods are simple enough and require no explanation. The fireProgressReady, fireProgressUpdate and fireProgressDone methods are all very similar, with the only differences being the state and value of the SequenceEvent and the listener method being called. Each clones the listener list before walking through it and sending the same event to each listener.

Although it’s not listed, you’ll find a SequenceItemTest class when you download the code from www.javapro.com. SequenceItemTest uses a Swing timer to step through a phony sequence to test the code and inherits from AbstractSequenceItem. If you take a look at the actionPerformed method, which is triggered on each timer tick, you’ll see how easy it is to fire off SequenceListener events as you process things in your own code.

JSequence is the main class for this component. You can take a look at the code in Listing 3. The constructor expects an instance of SequenceList, which is a collection of SequenceItem objects in the order you want to trigger them. From the SequenceList, JSequence will build a set of SequenceItemView objects and put them in a BoxLayout. You’ll find a JSequenceTest class online that demonstrates JSequence usage.

During the loop that creates the views, we register both the SequenceItemView and the JSequence classes as implementations of the SequenceListener interface, ready to receive notification from SequenceItem objects. The SequenceItemView, as we saw earlier does most of the work with SequenceEvents, but JSequence watches for the progressDone message and moves on to the next item when it receives it.

You’ll notice a getScrollPane method, the purpose of which may not seem obvious. If you put JSequence, which uses the BoxLayout, directly in a JScrollPane, you’ll find that the view expands to fill the shape of the scroll pane. Since what you really want is to put JSequence at the top and let it expand to fill space going down, as new items are executed, what you really need is to put JSequence at the NORTH end of a BorderLayout first. To make this easy, you can call getScrollPane instead of doing to work separately. To use it, create a JSequence instance and put the JScrollPane returned by getScrollPane in your application’s layout.

JSequence provides a simple infrastructure that allows you to show progress through a number of sequential processes. JSequence can be given any series of objects that implement the SequenceItem interface. It will step though each items, showing users ongoing progress along the way. Each item will be shown in a list view, allowing users to look back at each completed item, so they can see what’s been done at any point in the process.

When multiple, sequential processes must be executed, it becomes especially critical that users receive appropriate feedback. Without it, their expectations are ill managed and they may look for relief by clicking elsewhere on your interface or typing away without guidance. JSequence can help you avoid these problems, showing you each step along the way.

Listing 1

import java.awt.*;
import javax.swing.*;

public class JSequence extends JPanel
  implements SequenceListener
{
  protected SequenceList list;
  protected int current = 0;

  public JSequence(SequenceList list)
  {
    setBackground(Color.white);
    setOpaque(false);
    this.list = list;
    setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
    int count = list.getSequenceItemCount();
    for (int i = 0; i < count; i++)
    {
      SequenceItemView view = new SequenceItemView();
      SequenceItem item = list.getSequenceItem(i);
      item.addSequenceListener(this);
      item.addSequenceListener(view);
      add(view);
    }
  }
  
  public void execute()
  {
    int count = list.getSequenceItemCount();
    if (count > 0)
    {
      current = 0;
      list.getSequenceItem(0).execute();
    }
  }

  public void progressReady(SequenceEvent event) {}
  public void progressUpdate(SequenceEvent event) {}
  public void progressDone(SequenceEvent event)
  {
    int count = list.getSequenceItemCount();
    current++;
    if (current < count)
    {
      list.getSequenceItem(current).execute();
    }
  }
  
  public JScrollPane getScrollPane()
  {
    JPanel panel = new JPanel(new BorderLayout());
    panel.setBackground(getBackground());
    panel.add(BorderLayout.NORTH, this);
    return new JScrollPane(panel);
  }
}

Listing 2

import java.util.*;

public abstract class AbstractSequenceItem
  implements SequenceItem
{
  protected String text;
  protected ArrayList listeners = new ArrayList();	
  
  public AbstractSequenceItem(String text)
  {
    this.text = text;
  }
  
  public String getText()
  {
    return text;
  }
  
  public void addSequenceListener(SequenceListener listener)
  {
    listeners.add(listener);
  }

  public void removeSequenceListener(SequenceListener listener)
  {
    listeners.remove(listener);
  }
  
  public void fireProgressReady(int max)
  {
    ArrayList list = (ArrayList)listeners.clone();
    SequenceEvent event = new SequenceEvent(
      this, SequenceEvent.STATE_READY, max);
    for (int i = 0; i < list.size(); i++)
    {
      SequenceListener listener =
        (SequenceListener)list.get(i);
      listener.progressReady(event);
    }
  }

  public void fireProgressUpdate(int val)
  {
    ArrayList list = (ArrayList)listeners.clone();
    SequenceEvent event = new SequenceEvent(
      this, SequenceEvent.STATE_UPDATE, val);
    for (int i = 0; i < list.size(); i++)
    {
      SequenceListener listener =
        (SequenceListener)list.get(i);
      listener.progressUpdate(event);
    }
  }
  
  public void fireProgressDone(int max)
  {
    ArrayList list = (ArrayList)listeners.clone();
    SequenceEvent event = new SequenceEvent(
      this, SequenceEvent.STATE_DONE, max);
    for (int i = 0; i < list.size(); i++)
    {
      SequenceListener listener =
        (SequenceListener)list.get(i);
      listener.progressDone(event);
    }
  }
}

Listing 3

import java.awt.*;
import javax.swing.*;

public class SequenceItemView extends JPanel
  implements SequenceListener
{
  protected JLabel label;
  protected JProgressBar bar;
  
  protected static final ImageIcon
    BLANK_ICON = new ImageIcon("images/blank.gif");
  protected static final ImageIcon
    READY_ICON = new ImageIcon("images/ready.gif");
  protected static final ImageIcon
    UPDATE_ICON = new ImageIcon("images/update.gif");
  protected static final ImageIcon
    DONE_ICON = new ImageIcon("images/done.gif");

  public SequenceItemView()
  {
    setOpaque(false);
    setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
    setLayout(new BorderLayout());
    label = new JLabel("", BLANK_ICON, JLabel.LEFT);
    label.setOpaque(false);
    add(BorderLayout.NORTH, label);
    
    bar = new JProgressBar();
    bar.setOpaque(false);
    bar.setBorder(BorderFactory.createEmptyBorder(2, 20, 2, 5));
    add(BorderLayout.CENTER, bar);
    setVisible(false);
  }

  public void scrollToVisible()
  {
    Container ancestor = SwingUtilities.
      getAncestorOfClass(JViewport.class, this);
    if (ancestor != null)
    {
      JViewport viewport = (JViewport)ancestor;
      Rectangle rect = viewport.getViewRect();
      Rectangle bounds = getBounds();
      if (!rect.contains(bounds))
      {
        viewport.scrollRectToVisible(bounds);
      }
    }
  }
  
  public void progressReady(SequenceEvent event)
  {
    Object source = event.getSource();
    if (source instanceof SequenceItem)
    {
      SequenceItem item = (SequenceItem)source;
      label.setText(item.getText());
    }
    label.setIcon(READY_ICON);
    bar.setMinimum(0);
    bar.setMaximum(event.getValue());
    setVisible(true);
    scrollToVisible();
  }

  public void progressUpdate(SequenceEvent event)
  {
    label.setIcon(UPDATE_ICON);
    bar.setValue(event.getValue());
    scrollToVisible();
  }

  public void progressDone(SequenceEvent event)
  {
    bar.setValue(bar.getMaximum());
    label.setIcon(DONE_ICON);
    sleep(100);
  }

  public static void sleep(int time)
  {
    try
    {
      Thread.sleep(time);
    }
    catch (InterruptedException e) {}
  }
}