HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

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 Factory 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 lists of different icons. 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 complementary 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 alternative views in an application. This example demonstrates the basic mechanics only. 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 alternative situations. The ListLayout is also usable outside this context. It lays out its children in a vertical sequence, adjusting position and/or width, but not height. We try to promote reuse and remove 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 we can use it outside this component if we want to. You can think of this arrangement as 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 this 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 getPreferredSize method calls getPreferredTabSize to determine what the largest button component dimensions are and then takes the insets into account. Notice that there is no restriction 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 make 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 that encapsulates the behavior. Listing 4 shows 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 shows how the ContextPanelTest class lets you view three context panels with colored panels in each.

Figure 3
Figure 3:

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 noting that we use the BasicArrowButton from the JFC. This is an undocumented class. There's no risk it will disappear anytime soon since it's heavily used by components like JScrollBar and JComboBox, but it's 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 and 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 whether 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 horizontally, 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. Our intention is primarily 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 mouseExited 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 interested only in the BOTH setting in this context. Notice that it's generally wiser to build a generic layout manager than one that 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 track of the bottom position and lays one component under the previous component until none remain. This is useful primarily 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. You already know everything that's happening under the hood, of course, 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)

Okay, that should be easy enough for 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. It's 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 interchangeably in this context, in toolbars or in dropdown menus.

In Closing
This is the first installment of a column I hope will bring you insight and provide building blocks 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.

About the Author
Claude Duguay has been programming since 1980. In 1988 he founded LogiCraft Corporation, and he currently leads the development team at Atrieva Corp. You can contact him with questions and comments at [email protected]

	

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 < 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 < tabs.size(); i++) 
  { 
    comp = (Component)tabs.elementAt(i); 
    size = comp.getPreferredSize(); 
    if (size.width > w) w = size.width; 
    if (size.height > 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 < 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 < 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 < 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("Center", 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 >= view.height) 
  { 
    add("South", 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 > view.height) 
      add("South", south); 
    if (top.y < 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 > view.height) 
      add("North", north); 
    int max = pane.height - view.height; 
    if (top.y > (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 < 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)) > -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("LargeIcon")); 
  button.addActionListener(action); 
  view.add(button); 
  doLayout(); 
} 
  
      

Download Assoicated Source Files (Zip file format - 31.3 KB)
 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.