ORIGINAL DRAFT

The Widget Factory is a series of articles (a regular column) dedicated to showing you how to develop sophisticated user interface components for your Java programs. We build on the foundation provided by Swing, and the Java Foundation Classes, so the only assumption we’ll make is that you can compile and run such programs (this code was tested under JDK 1.1.6 with Swing 1.0.2). This first installment of the Widget Factor explores the development of a component called JOutlookBar which resembles the Microsoft navigation bar provided first in their Outlook mail client, and later in programs like FrontPage, Project and TeamManager.

Figure 1 shows what the finished product will look like. The Alan button is highlighted because the mouse is over it. The Dev, Ops and QA buttons allow you to switch contexts, presenting the user with differing icons lists. You’ll also notice the down arrow inside the folder list, which allows you to scroll down when the list is longer than the display area. A complimentary up arrow button appears if the top part of the list is scrolled up.

Figure 1.

Figure 1.

The JOutlookBar component is primarily intended to present the user with alternate views in an application. This example only demonstrate the basic mechanics. The high-level interface is simple enough. You can add Action objects to the JOutlookBar component, based on arbitrary contexts, and have it become useful immediately.

Decomposition

Sometimes the simplest things are deceivingly simple. Let’s decompose the component and see what makes it tick. Figure 2 shows the various nested pieces. The shading helps distinguish layers. As you can see, there are three kinds of buttons - the context buttons, which provide context navigation; the up and down arrow buttons; and the labeled icon buttons. We also have a number of nested panels and associated layout managers.

Figure 2.

Figure 2.

To manage context switching, and to encourage code reuse, the ContextLayout and ContextPanel handle everything at that particular level, making it possible to use these elements in alternate situations. The ListLayout is also usable outside this context. It lays out its children in vertical sequence, adjusting position and/or width but not their height. We try to promotes reuse and removes unnecessary coupling at every opportunity. The ScrollingPanel works like the JFC ScrollPane but uses arrows at the top and bottom rather than a scroll bar.

Context Management

The ability to switch between contexts is one of the major features in the JOutlookBar. Switching contexts between icon listings and providing visual feedback through button positioning is key to the basic look and feel of this widget. With good design, the coupling can be very loose and the various classes can be reused more effectively.

To keep it all generic, we develop the ContextLayout manager to handle button placement and center component management. We then put user-triggered mechanics into the ContextPanel class so that we can use it outside this compnent if we want to. You can think of this arrangement at the View-Controller separation, where the layout manager handles the view and the ContextPanel handles user interaction.

We won’t spend much time on layout manager design. Instead, check out Practical Layout Managers in the August issue. The key methods here are addComponent, removeComponent, preferredLayoutSize, layoutComponent and setIndex.

Listing 1 shows the code for adding and removing components and for setting the current active index. The context button is stored in the tabs vector and we use the constraint object argument to set the component that will be displayed when the button is pressed. These are stored in the panels vector. The setIndex method sets the current index value and calls layoutComponent to recalculate the layout for display.

Listing 2 shows how the preferred layout size is calculated. The code for getMinimumSize is almost identical. The getPrefferredSize method calls getPreferredTabSize to determine what the largest button component dimensions are and then takes the insets into account. Notice that there is no restrinction on the kind of component you use. Outside the JOutlookBar control, you could just as easily place labels here and change the context directly with the setIndex method.

Listing 3 shows how the components are actually laid out. We call two methods from layoutContainer to keep things more readable. The layoutTabs method decides where the index position is and lays out buttons at the top and bottom of the display area. The layoutCenter method figures out where to put the center component that relates to the currently active index.

The ContextPanel provides a high-level view which encapsulates the behavior. Listing 4 show the source code. The constructor sets the ContextLayout and adds a BevelBorder as a visual enhancement. Two methods are provided to addTab and removeTab, where the internal code handles the registration and removal of button action listeners automatically. A setIndex method abstracts access to the ContextLayout equivalent and the actionPerformed method acts on user button selection.

Figure 3.

Figure 3.

Figure 3 shows how the ContextPanelTest class lets you view three context panels with colored panels in each.

The Scrolling Panel

The ScrollingPanel uses the JViewport paradigm to allow scrolling within the window area. It implements a constructor and only two methods. The setBounds call is intercepted to deal with window resizing and we implement the ActionListener interface to handle button presses. It’s worth nothing that we use the BasicArrowButton from the JFC. This is an undocumented class. There’s no risk it’ll dissapear anytime soon, since its heavily used by components like JScrollBar and JComboBox, but its good to be cautious with anything that’s not part of the official API.

Listing 5 shows code for the ScrollingPanel constructor, setBounds and actionPerformed. The constructor creates the arrow buttons, the main viewport and sets up the panel as an action listener for each button. The setBounds method handles resizing events by resetting the display to the top, removing the north button and adding a south button if necessary. The actionPerformed event handler does the scrolling work. Most of it is a matter of calculating the display area and determining if the arrow buttons are required. If they are, they are added to the north and/or south position(s). Otherwise they are removed. The ScrollingPanel is fully reusable as-is, but if you want to scroll horizontaly, you’ll have to extend the code.

The Icon List

To handle the list of icons in the outlook bar, we subclass JButton to provide a RolloverButton class. Primarily, our intention is to handle mouse over events, so we register as a MouseListener and control the border and color drawing explicitly. The constructor also sets a number of JButton attributes to center the icon and text horizontally and to put the icon above the text vertically. Listing 6 shows the constructor and code for handling the mouseEntered and mouseExisted events.

The ListLayout manager provides the mechanics for placing components vertically above each other, allowing for various sizes if necessary. It allows us to control horizontal justification as well, though we are only interested in the BOTH setting in this context. Notice that its generally wiser to build a generic layout manager that one which is tightly coupled to a specific application. If anything, layout managers are meant to be used in differing contexts, so this is clearly a good design objective.

Listing 7 shows the layoutContainer method for ListLayout. It handles various justification choices, but otherwise simply keeps tack of the bottom position and lays one component under the previous component until none remain. This is primarily useful in containers that scroll vertically, like a list box.

Wraping It Up

The JOutlookBar class wraps everything together so you don’t have to pay attention to the underlying code. Of course, you already know all about what’s happening under the hood, and by now, you’ve probably thought of a few alternate uses for the classes we’ve already discussed. All that aside, we need to make it as easy as possible to use the component, so the interface looks like this:

addIcon(String context, Action action)</P>

Ok, so that should be easy enough to us, right? Listing 8 shows the code for this approach. The context is created only if it has not been seen before, otherwise we look it up and use the existing view. We then create a RolloverButton with the name and large icon value from the Action object. The large icon property is not a default property, so no icon will be present unless this value is associated. Take a look at the code for SelectAction to get a sense of how this works.

If you don’t know anything about Action objects under the JFC, you’ll want to read up on it. Its the preferred method for handling user interface events. You can subclass AbstractAction and implement the ActionListener interface to deal with what happens when the user selects the icon. By setting the text and icon properties in the Action object, you can use them in this context or in toolbars or drop down menus interchangeably.

In Closing

This is the first installment of a column that I hope will bring you insights and provide building blocks that you can use in future user interface design and development projects. We don’t have a lot of room to explore everything in great detail, but often, what you really need is the right idea and a good place to start. The JFC is a great foundation to build on and I hope the widgets coming out of our little factory will answer some of your programming questions. Next month, we’ll develop a JWizard framework that lets you build wizards without having to worry about the mechanics.

Listing 1

protected Vector tabs = new Vector();
protected Vector panels = new Vector();

public void setIndex(Container parent, int index)
{
  this.index = index;
  layoutContainer(parent);
}

public void addLayoutComponent(Component tab, Object panel)
{
  if (panel == null) return;
  tabs.addElement(tab);
  panels.addElement(panel);
}

public void removeLayoutComponent(Component comp)
{
  for (int i = 0; i &lt; tabs.size(); i++)
  {
    if (tabs.elementAt(i) == comp)
    {
      tabs.removeElementAt(i);
      panels.removeElementAt(i);
      return;
    }
  }
}

Listing 2

public Dimension preferredLayoutSize(Container target)
{
  Insets insets = target.getInsets();
  Dimension tab = getPreferredTabSize();
  int h = tab.height *
    (tabs.size() + 1) + (tabs.size() * hgap);
  return new Dimension(
    tab.width + insets.left + insets.right + (hgap * 2),
    h + insets.top + insets.bottom);
}

private Dimension getPreferredTabSize()
{
  int w = 0;
  int h = 0;
  Dimension size;
  Component comp;
  for (int i = 0; i &lt; tabs.size(); i++)
  {
    comp = (Component)tabs.elementAt(i);
    size = comp.getPreferredSize();
    if (size.width &gt; w) w = size.width;
    if (size.height &gt; h) h = size.height;
  }
  return new Dimension(w, h);
}

Listing 3

public void layoutContainer(Container target)
{
  Dimension size = getPreferredTabSize();
  layoutTabs(target, index, size, target.getSize());
  layoutCenter(target, index, size, target.getSize());
}

private void layoutCenter(Container cont, int index, Dimension size, Dimension parent)
{
  Insets insets = cont.getInsets();
  int top = size.height * index + insets.top + 1 +
    vgap * index + vgap;
  int h = parent.height - (size.height + vgap ) *
    tabs.size() - insets.top - insets.bottom - 2 -
    vgap * 2;
		
  if (center != null) cont.remove(center);
  center = (Component)panels.elementAt(index - 1);
		
  center.setBounds(insets.left + 1 + hgap, top,
    parent.width - insets.left -
    insets.right - 2 - (hgap * 2), h);
  cont.add(center);
  center.paintAll(center.getGraphics());
}

private void layoutTabs(Container cont, int index, Dimension size, Dimension parent)
{
  Insets insets = cont.getInsets();
  Component comp;
  // Top tabs
  int top = insets.top + 1 + vgap;
  for (int i = 0; i &lt; index; i++)
  {
    comp = (Component)tabs.elementAt(i);
    comp.setBounds(insets.left + 1 + hgap, top,
      parent.width - insets.left -
      insets.right - 2 - (hgap * 2),
      size.height);
    top += size.height + vgap;
  }
  // Bottom tabs
  top = parent.height - insets.bottom - 1 -
    (size.height + vgap) * (tabs.size() - index);
  for (int i = index; i &lt; tabs.size(); i++)
  {
    comp = (Component)tabs.elementAt(i);
    comp.setBounds(insets.left + 1 + hgap, top,
      parent.width - insets.left -
      insets.right - 2 - (hgap * 2),
      size.height);
    top += size.height + vgap;
  }
}

Listing 4

protected Vector buttons = new Vector();

public ContextPanel()
{ 
  setLayout(new ContextLayout());
  setBorder(new BevelBorder(BevelBorder.LOWERED));
  setPreferredSize(new Dimension(80, 80));
}

public void setIndex(int index)
{
  ((ContextLayout)getLayout()).setIndex(this, index);
}

public void addTab(String name, Component comp)
{
  JButton button = new TabButton(name);
  add(button, comp);
  buttons.addElement(button);
  button.addActionListener(this);
}
	
public void removeTab(JButton button)
{
  button.removeActionListener(this);
  buttons.removeElement(button);
  remove(button);
}
	
public void actionPerformed(ActionEvent event)
{
  Object source = event.getSource();
  for (int i = 0; i &lt; buttons.size(); i++)
  {
    if (source == buttons.elementAt(i))
    {
      setIndex(i + 1);
      return;
    }
  }
}

Listing 5

public ScrollingPanel(Component component)
{
  setLayout(new BorderLayout());
  north = new BasicArrowButton(BasicArrowButton.NORTH);
  south = new BasicArrowButton(BasicArrowButton.SOUTH);
  viewport = new JViewport();
  add(&quot;Center&quot;, viewport);
  viewport.setView(component);
  north.addActionListener(this);
  south.addActionListener(this);
}

public void setBounds(int x, int y, int w, int h)
{
  super.setBounds(x, y, w, h);
  Dimension view = new Dimension(w, h);
  Dimension pane = viewport.getView().getPreferredSize();
  viewport.setViewPosition(new Point(0, 0));
  remove(north);
  if (pane.height &gt;= view.height)
  {
    add(&quot;South&quot;, south);
  }
  else
  {
    remove(south);
  }
  doLayout();
}
	
public void actionPerformed(ActionEvent event)
{
  Dimension view = getSize();
  Dimension pane = viewport.getView().getPreferredSize();
  Point top = viewport.getViewPosition();
  if (event.getSource() == north)
  {
    if (pane.height &gt; view.height)
      add(&quot;South&quot;, south);
    if (top.y &lt; incr)
    {
      viewport.setViewPosition(new Point(0, 0));
      remove(north);
    }
    else
    {
      viewport.setViewPosition(new Point(0, top.y - incr));
    }
    doLayout();
  }
  if (event.getSource() == south)
  {
    if (pane.height &gt; view.height)
      add(&quot;North&quot;, north);
    int max = pane.height - view.height;
    if (top.y &gt; (max - incr))
    {
      remove(south);
      doLayout();
      view = viewport.getExtentSize();
      max = pane.height - view.height;
      viewport.setViewPosition(new Point(0, max));
    }
    else
    {
      viewport.setViewPosition(new Point(0, top.y + incr));
    }
    doLayout();
  }
}

Listing 6

public RolloverButton(String name, Icon icon)
{
  super(name, icon);
  setOpaque(true);
  setBackground(Color.gray);
  setForeground(Color.white);
  setMargin(new Insets(2, 2, 2, 2));
  setBorderPainted(false);
  setFocusPainted(false);
  setVerticalAlignment(TOP);
  setHorizontalAlignment(CENTER);
  setVerticalTextPosition(BOTTOM);
  setHorizontalTextPosition(CENTER);
  addMouseListener(this);
}

public void mouseEntered(MouseEvent event)
{
  setBorderPainted(true);
  setForeground(Color.black);
  setBackground(Color.lightGray);
  repaint();
}

public void mouseExited(MouseEvent event)
{
  setBorderPainted(false);
  setForeground(Color.white);
  setBackground(Color.gray);
  repaint();
}

Listing 7

public void layoutContainer(Container parent)
{
  Insets insets = parent.getInsets();
  int w = parent.getSize().width;
  Component comp;
  Dimension size;
  int position = insets.top;
  int ncomponents = parent.getComponentCount();
  for (int i = 0; i &lt; ncomponents; i++)
  {
    comp = parent.getComponent(i);
    size = comp.getPreferredSize();
    int h = size.height - insets.top - insets.bottom;
    switch (alignment)
    {
      case CENTER:
      {
        int l = (w - size.width) / 2;
        comp.setBounds(insets.left + hgap + l, position,
          size.width - insets.left - insets.right - (hgap * 2), h);
        break;
      }
      case LEFT:
      {
        comp.setBounds(insets.left + hgap, position,
          size.width - insets.left - insets.right - (hgap * 2), h);
        break;
      }
      case RIGHT:
      {
        int l = w - size.width;
        comp.setBounds(insets.left + hgap + l, position,
          size.width - insets.left - insets.right - (hgap * 2), h);
        break;
      }
      default:
      {
        comp.setBounds(insets.left + hgap, position,
          w - insets.left - insets.right - (hgap * 2), h);
        break;
      }
    }
    position += h + vgap;
  }
}

Listing 8

protected Vector names = new Vector();
protected Vector views = new Vector();
	
public void addIcon(String context, Action action)
{
  int index;
  JPanel view;
  if ((index = names.indexOf(context)) &gt; -1)
  {
    view = (JPanel)views.elementAt(index);
  }
  else
  {
    view = new JPanel();
    view.setBackground(Color.gray);
    view.setLayout(new ListLayout());
    names.addElement(context);
    views.addElement(view);
    addTab(context, new ScrollingPanel(view));
  }
  RolloverButton button = new RolloverButton(
    (String)action.getValue(Action.NAME),
    (Icon)action.getValue(&quot;LargeIcon&quot;));
  button.addActionListener(action);
  view.add(button);
  doLayout();
}