ORIGINAL DRAFT

One of my favorite ways to tackle development efforts is what you might call a breadth first investigation. By doing complete passes across layers of a development project, you get to see the commonalties and often do a better job of identifying reusable components. Development efforts are rarely subject to a single approach, of course, but looking across the problem space gives you the kind of perspective you need to identify the commonalities. It is precisely this process that leads us to this months visual component.

It’s rather surprising how often you might have to present a user with a list or table that could benefit from being organizable. By this, I mean organizing the order of elements or the interchange of elements between two collections. Ideally, you’d want to be able to drop any JList or JTable into a component that could handle the behavior automatically. That’s the premise behind JOrganizer. Doing this with JList turns out to be straight forward, but we’ll add the same functionality to JTable components, imposing a single minimum requirement - that the table use a DefaultTableModel. Even this would be too constraining if we didn’t add the ability to drop in any component that implements a suitable interface. We’ll develop default implementations for lists and tables just to make it as easy as possible to use our solution, right out of the box.

Figure 1: JOrganizer with a pair of JList instances.

Figure 1: JOrganizer with a pair of JList instances.

You can see what the JOrganizer looks like when used with JList components in Figure 1. The plus and minus buttons let you add and remove elements. The up and down arrow buttons allow the order of the elements in the list to be changed. We’ll make this set of capabilities optionally available since you won’t always want to use them. We’ll also make it possible to use the ordering panel outside JOrganizer. The central buttons let you move elements between the two lists, depending on what’s selected. This behavior is extremely useful when you want to allow the user to move elements between two collections. For example, you could make FTP file transfer selections, add or remove tools from an inventory of available options, or you could manage visible table fields and the order in which they are presented. Since JOrganizer manipulates the elements in a model, you can attach any behavior you like by just listening to model events.

Organizable Lists

One of our design objectives is always the same in this column - to maximize flexibility and reusability. One of our design options would be to impose a set of interfaces on a ListModel and.or TableModel to accomplish our goals. Unfortunately, there are many implementations that already extend these models and introduce their own behaviors and this would reduce the likelyhood that a typical JList or JTable could simply be dropped into our component. Instead, we’ll impose a simple interface and provide a couple of default implementations that can operate on any JList and any JTable with a DefaultTableModel. This ensures compatibility with a standard Swing list or table, and most of the JList or JTable extensions you might be inclined to design.

As you might expect, our solution can’t possibly deal with every contingency, so you will have to subclass one of these models yourself or extend the respective organizers to handle specialized models. This is a reasonable design tradeoff in that the solution covers most cases without requiring any programming effort. It also supports custom models with only a small coding investment. Ideally, however, we want to be able to drop in custom components without requiring a major investment. To make this possible, we’ll use a simple interface called OrganizableList, which looks like this:

public interface OrganizableList
{
	public int getElementCount();
	public Object[] getSelectionList();
	public void insertSelectionList(Object[] values);
	public void moveSelectedUp();
	public void moveSelectedDown();
	public void removeSelected();
	public ListSelectionModel getSelectionModel();
	public JComponent getComponent();
}

You can implement this interface directly in your model if you like, or in a wrapper class or custom component, but we’ll provide a pair of element organizers called OrganizableListPane and OrganizableTablePane to handle the common cases. The OrganizableListPane requires a JList argument in its constructor and the OrganizableTablePane requires a JTable argument in its constructor. We’ll provide a layer of abstraction that plugs these in automatically when you create a new JOrganizer instance. We’ll also provide JOrganizer constructor variants that let you drop in arbitrary components, so long as they implement the OrganizableList interface. Let’s take a quick look at our default implementations.

Listing 1 shows the OrganizableListPane implementation. The constructor expects a title, for labeling purposes, a JList instance, an instance of the OrganizerInput interface and an integer that holds a bittmapped flag for which buttons are to be displayed. We store a reference to the list and to the selection model, add a label to the NORTH of a BorderLayout, with the list in the CENTER and an instance of OrganizerToolbar to the SOUTH, so long as the flags warrant using a toolbar. The getElements method is an internal utility methods that fetches a full list of elements from JList. We’ll use this later in the code a few times. All it does is walk the ListModel, adding each element to a Vector instance.

The getElementCount and getSelectionModel methods are self-explanatory and required by the OrganizableList, as are the rest of the methods in this class. The insertSelectionList method adds elements to the JList. To do this, we get the full list using the getElements method and add elements at the currently selected offset. The offset is determined by the minimum selection index and adjusted in case it’s out of range. JList supports a setListData method that lets us set all the elements with a the Vector instance we just manipulated. The last step sets the new selection range, based on the insertion point and the length of the array of objects to be inserted.

The getSelectionList method delegates its request to the JList instance and returns all the elements that are currently selected in the form of an array. The moveSelectedUp and moveSelectedDown methods are almost identical, swapping pairs of selected elements to move them up or down, respectively. The order of processing is important and reversed in the moveSelectedDown method. After resetting the list values, we move the selection interval up or down, respectively. The removeSelected method gets a Vector with the getElements method and walks the list, removing elements in the selection range. We intentionally walk the list backward so that removed elements don’t move all remaining elements up the list before each subsequent deletion takes place. Out final method, getComponent, returns a reference to the component itself.

Listing 2 shows the OrganizableTablePane implementation which is only slightly more complicated. The constructor is almost identical to the OrganizableListPane constructor but throws an exception if the table model is not the expected DefaultTableModel. A trio of utility methods let follow The getColumnNames method gets a Vector of column names. The getRowVector method returns a Vector representation for any row in the table model. The getElements method delegates its work to the tableModel and is used in the same way as in the OrganizableListPane. Methods like getElementCount, getSelectionModel, getComponent are trivial and self-explanatory. The rest of the methods are virtually identical to the equivalent methods in OrganizableListPane but they do not have to set the data Vector in a separate call, since they operate directly on a reference retrieved by from the model with the getDataVector method.

Managing the order in which the elements of a JList or a JTable are presented requires nothing more than the methods we’ve implemented in our ElementOrganizer interface. We’ll implement this behavior using only two classes and an interface. The OrganizerButton class merely extends JButton and sets a few parameters for convenience. The OrganizerConstants interface declares a few constants that bit map integer option values to allow us to decide whether the up and down buttons, the insert and delete buttons are active. They include variants for NONE and ALL for convenience.

To suport insertion of new entries, we provide the OrganizerInput interface, which looks like this:

public interface OrganizerInput
{
	public Object[] getInputData(Component parent);
}

We provide two default implementations, OrganizerListInput and OrganizerTableInput, for convenience. The OrganizerListInput pops up a JOptionPane asking for a single line of text to be inserted into the list. The OrganizerTableInput implementation simply returns an empty Vector object for the table. Given that double-clicking on a JTable cell lets you edit the entries by default, this makes sense. In any case, returning a null value from getInputData will cause no action to be taken. You can make this behavior work to your advantage by providing custom OrganizerInput implementations that allow the user to cancel their action at any time.

Listing 3 shows the OrganizerToolbar code which does most of the single list or table management work. The constructor expects an instance of OrganizableList, an OrganizerInput object and an integer value with the button flags. Based on the flag settings, we set up as many of the buttons as required, adding listeners and disabling the buttons until list selections are made. To watch for list selections, we implement the ListSelectionInterface. When buttons are pressed, we catch the ActionEvent and call appropriate methods in the OrganizableList interface to make changes to the list or table.

CenterLayout Manager

One of the things we’ll need to do in our JOrganizer component is center the buttons between two lists or tables. There aren’t any layout managers that do a good job prioritizing the center component in either core Java or the Swing extensions. What we’d like is something that reverses the behavior of a BorderLayout. Figure 2 shows the forces at work in a BorderLayout manager. The North and South components are pulled west and east to fill the available area, and the center component is pushed outward in all directions to fill any remaining space after the surrounding components have been laid out. Figure 3 shows what we want our CenterLayout to do by default.

Figure 2: BorderLayout.

Figure 2: BorderLayout.

Figure 3: CenterLayout - Vertical.

Figure 3: CenterLayout - Vertical.

Figure 4: Center Layout - Horizontal.

Figure 4: Center Layout - Horizontal.

As you can see, the North, South, East and West components are pushed toward the center, into any space remaining after the Center component has been laid out. Like the BorderLayout, the North and South components are pushed west and east as well. We’ll add some flexibility in our CenterLayout by permitting a flag to control whether the North and South expand to the edges or the West and East. Figure 4 shows CenterLayout with the orientation flag set to HORIZONTAL. The default behavior is VERTICAL.

Listing 4 shows the CenterLayout class, which extends the AbstractLayout class. AbstractLayout implements all the default behaviors required by layout managers, leaving the important ones for the subclass. The three methods we need to implement are typically preferredLayoutSize, minimumLayoutSize and layoutContainer. In this case, we also implement addLayoutComponent and removeLayoutComponent because we store the north, south, east, west and center components in specific variables. The preferredLayoutSize and minimumLayoutSize methods calculate the preferred and minimum layout sizes based on the size each component that was added to the parent component. These methods account for missing components and the orientation that might be used.

Figures 3 and 4 show the two orientation behaviors. This is a choice I always wished were available in BorderLayout, which adds a lot of control in the way components are organized. The layoutContainer method does the actual work of resizing the components. The center component position is first calculated and the surrounding components are resized according to the remaining space. Depending on the orientation, the north and south or the west and east components will be processed first. In each case, missing components are accounted for and suitably ignored.

JOrganizer Component

With all our elements at hand, we’re ready for the final assembly. Figure 5 shows the JOrganizer at work with JTable elements and only the TOOLBAR_UPDOWN arrow flag assigned. Listing 5 shows the code for JOrganizer. We provide 3 constructor variants. The first two accept two titles and JList or JTable arguments, along with an integer flag for the order management buttons. The flags are applied to both lists or tables, which are added to OrganizableListPane or OrganizableTablePane instances, respectively. We use the default OrganizerListInput and OrganizerTableInput, for each list or table, repectively. The third constructor gives you more control, allowing you to use OrganizerList implementations more directly.

JOrganizer implements the ActionListener and ListSelectionListener interfaces to catch events from the buttons and lists/tables respectively. We use the valueChanged events to decide which buttons should be active.

Figure 5: JOrganizer with a pair of JTable instances.

Figure 5: JOrganizer with a pair of JTable instances.

When the active buttons are pressed, we respond to the actionPerformed event by using the OrganizableList interface to remove selected elements from one list or table and add them to the other list or table. Once we’ve done this, we merely need to reset the selection to reflect our changes. When list elements or table rows are moved, they are inserted at the currently selected position. The selection in the source list or table is reset to a single entry for convenience.

The main benefit of this implementations is the ease with which you can implement list ordering solutions. Buttons automatically becoment enabled or disabled by using the ListSelectionListener associated with the component being managed. You can even decide which buttons are displayed by setting a few simple flags.

Summary

You might have noticed the SINGLE_INTERVAL_SELECTION setting in the OrganizableListPane and the OrganizableTablePane implementations earlier. These exist primarily to constrain the selection problem. With this setting, you can only select one or more contiguous elements. You can solve this problem by creating subclasses for the DefaultListSelectionModel that let you control selections more directly but I chose not to do so because it would have required specific ListSelectionModel implementations to be used. If you find this limitation too constraining, you can extend the code to deal with multiple interval selections easily enough.

The JOrganizer components are reusable in many circumstances. You can use the OrganizableListPane and OrganizableTablePane implementations outside JOrganizer to support controlling the order of list elements with a simple button interface. The constructor lets you decide which buttons are active, putting control in your hands at development time. The CenterLayout manager is extremely reusable, anywhere you might use another layout manager, and just as easily. Finally, JOrganizer provides the ability to manage two list-oriented components and move elements between them with little or no programming. Our use of interfaces ensures flexibility so you can easily use any component that implements the OrganizableList interface. I hope you find it useful.

Listing 1

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

public class OrganizableListPane extends JPanel
  implements OrganizableList, OrganizerConstants
{
  protected JList list;
  protected OrganizerToolbar toolbar;
  protected ListSelectionModel selectionModel;
  
  public OrganizableListPane(String title, JList list,
    OrganizerInput input, int orderButtons)
  {
    this.list = list;
    selectionModel = list.getSelectionModel();
    selectionModel.setSelectionMode(
      ListSelectionModel.SINGLE_INTERVAL_SELECTION);
    setLayout(new BorderLayout(4, 4));
    
    if (title != null)
      add(BorderLayout.NORTH, new JLabel(title));
    
    add(BorderLayout.CENTER, new JScrollPane(list));
    
    if (orderButtons != TOOLBAR_NONE)
    {
      add(BorderLayout.SOUTH, toolbar =
        new OrganizerToolbar(this, input, orderButtons));
      selectionModel.addListSelectionListener(toolbar);
    }
  }
  
  protected Vector getElements()
  {
    Vector vector = new Vector();
    ListModel model = list.getModel();
    for (int i = 0; i < model.getSize(); i++)
    {
      vector.add(model.getElementAt(i));
    }
    return vector;
  }
  
  public int getElementCount()
  {
    return list.getModel().getSize();
  }
  
  public ListSelectionModel getSelectionModel()
  {
    return selectionModel;
  }

  public void insertSelectionList(Object[] values)
  {
    int pos = selectionModel.getMinSelectionIndex();
    if (pos < 0) pos = getElementCount();
    
    Vector vector = getElements();
    for (int i = 0; i < values.length; i++)
    {
      vector.add(pos + i, values[i]);
    }
    list.setListData(vector);
    selectionModel.setSelectionInterval(
      pos, pos + values.length - 1);
  }
  
  public Object[] getSelectionList()
  {
    return list.getSelectedValues();
  }

  public void moveSelectedUp()
  {
    Vector vector = getElements();
    for (int i = 0; i < vector.size(); i++)
    {
      if (selectionModel.isSelectedIndex(i))
      {			
        Object one = vector.elementAt(i - 1);
        Object two = vector.elementAt(i);
        vector.setElementAt(two, i - 1);
        vector.setElementAt(one, i);
      }
    }
    list.setListData(vector);
    int anchor = selectionModel.getAnchorSelectionIndex() - 1;
    int lead = selectionModel.getLeadSelectionIndex() - 1;
    selectionModel.setSelectionInterval(anchor, lead);
  }

  public void moveSelectedDown()
  {
    Vector vector = getElements();
    for (int i = vector.size() - 1; i >= 0; i--)
    {
      if (selectionModel.isSelectedIndex(i))
      {			
        Object one = vector.elementAt(i);
        Object two = vector.elementAt(i + 1);
        vector.setElementAt(two, i);
        vector.setElementAt(one, i + 1);
      }
    }
    list.setListData(vector);
    int anchor = selectionModel.getAnchorSelectionIndex() + 1;
    int lead = selectionModel.getLeadSelectionIndex() + 1;
    selectionModel.setSelectionInterval(anchor, lead);
  }
  
  public void removeSelected()
  {
    int min = selectionModel.getMinSelectionIndex();
    Vector vector = getElements();
    int size = getElementCount() - 1;
    for (int i = size; i >= 0; i--)
    {
      if (selectionModel.isSelectedIndex(i))
      {			
        vector.removeElementAt(i);
      }
    }
    list.setListData(vector);
    size = getElementCount() - 1;
    if (min < 0) min = 0;
    if (min > size) min = size;
    selectionModel.setSelectionInterval(min, min);
  }
  
  public JComponent getComponent()
  {
    return this;
  }
}

Listing 2

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

public class OrganizableTablePane extends JPanel
  implements OrganizableList, OrganizerConstants
{
  protected JTable table;
  protected DefaultTableModel tableModel;
  protected OrganizerToolbar toolbar;
  protected ListSelectionModel selectionModel;

  public OrganizableTablePane(String title, JTable table, 
    OrganizerInput input, int orderButtons)
  {
    if (!(table.getModel() instanceof DefaultTableModel))
      throw new IllegalArgumentException("DefaultTableModel required");

    this.table = table;
    selectionModel = table.getSelectionModel();
    selectionModel.setSelectionMode(
      ListSelectionModel.SINGLE_INTERVAL_SELECTION);
    tableModel = (DefaultTableModel)table.getModel();
    setLayout(new BorderLayout(4, 4));
    
    if (title != null)
      add(BorderLayout.NORTH, new JLabel(title));
  
    add(BorderLayout.CENTER, new JScrollPane(table));
    
    if (orderButtons != TOOLBAR_NONE)
    {
      add(BorderLayout.SOUTH, toolbar =
        new OrganizerToolbar(this, input, orderButtons));
      table.getSelectionModel().addListSelectionListener(toolbar);
    }
  }
  
  protected Vector getColumnNames()
  {
    int count = tableModel.getColumnCount();
    Vector vector = new Vector(count);
    for (int i = 0; i < count; i++)
      vector.addElement(tableModel.getColumnName(i));
    return vector;
  }
  
  protected Vector getRowVector(int row)
  {
    int count = tableModel.getColumnCount();
    Vector list = new Vector(count);
    for (int i = 0; i < count; i++)
      list.addElement(tableModel.getValueAt(row, i));
    return list;
  }
  
  protected Vector getElements()
  {
    return tableModel.getDataVector();
  }
  
  public int getElementCount()
  {
    return tableModel.getRowCount();
  }
  
  public ListSelectionModel getSelectionModel()
  {
    return selectionModel;
  }
  
  public void insertSelectionList(Object[] values)
  {
    int pos = selectionModel.getMinSelectionIndex();
    if (pos < 0) pos = getElementCount();
    for (int i = 0; i < values.length; i++)
    {
      tableModel.insertRow(pos + i, (Vector)values[i]);
    }
    selectionModel.setSelectionInterval(
      pos, pos + values.length - 1);
  }
  
  public Object[] getSelectionList()
  {
    Vector rows = tableModel.getDataVector();
    Vector vector = new Vector();
    for (int i = 0; i < rows.size(); i++)
    {
      if (selectionModel.isSelectedIndex(i))
        vector.addElement(getRowVector(i));
    }
    int size = vector.size();
    Object[] list = new Object[size];
    for (int i = 0; i < size; i++)
      list[i] = vector.elementAt(i);
    return list;
  }

  public void moveSelectedUp()
  {
    Vector vector = getElements();
    for (int i = 0; i < vector.size(); i++)
    {
      if (selectionModel.isSelectedIndex(i))
      {			
        Object one = vector.elementAt(i - 1);
        Object two = vector.elementAt(i);
        vector.setElementAt(two, i - 1);
        vector.setElementAt(one, i);
      }
    }
    int anchor = selectionModel.getAnchorSelectionIndex() - 1;
    int lead = selectionModel.getLeadSelectionIndex() - 1;
    selectionModel.setSelectionInterval(anchor, lead);
  }

  public void moveSelectedDown()
  {
    Vector vector = getElements();
    for (int i = vector.size() - 1; i >= 0; i--)
    {
      if (selectionModel.isSelectedIndex(i))
      {			
        Object one = vector.elementAt(i + 1);
        Object two = vector.elementAt(i);
        vector.setElementAt(two, i + 1);
        vector.setElementAt(one, i);
      }
    }
    int anchor = selectionModel.getAnchorSelectionIndex() + 1;
    int lead = selectionModel.getLeadSelectionIndex() + 1;
    selectionModel.setSelectionInterval(anchor, lead);
  }

  public void removeSelected()
  {
    int min = selectionModel.getMinSelectionIndex() - 1;
    int size = getElementCount() - 1;
    for (int i = size; i >= 0; i--)
    {
      if (selectionModel.isSelectedIndex(i))
      {			
        tableModel.removeRow(i);
      }
    }
    size = getElementCount() - 1;
    if (min < 0) min = 0;
    if (min > size) min = size;
    selectionModel.setSelectionInterval(min, min);
  }
  
  public JComponent getComponent()
  {
    return this;
  }
}

Listing 3

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

public class OrganizerToolbar extends JPanel
  implements OrganizerConstants, ActionListener,
    ListSelectionListener
{
  protected OrganizerInput input;
  protected JButton insertButton, deleteButton, upButton, downButton;
  protected ListSelectionModel selectionModel;
  protected OrganizableList organizer;
  
  public OrganizerToolbar(OrganizableList organizer,
    OrganizerInput input, int orderButtons)
  {
    this.organizer = organizer;
    this.input = input;
    selectionModel = organizer.getSelectionModel();
    
    int count = 0;
    if (isFlagSet(orderButtons, TOOLBAR_UPDOWN)) count += 2;
    if (isFlagSet(orderButtons, TOOLBAR_INSERT)) count++;
    if (isFlagSet(orderButtons, TOOLBAR_DELETE)) count++;
    setLayout(new GridLayout(1, count, 2, 2));
    
    if (isFlagSet(orderButtons, TOOLBAR_INSERT))
    {
      add(insertButton = new OrganizerButton("Insert"));
      insertButton.addActionListener(this);
    }
    if (isFlagSet(orderButtons, TOOLBAR_DELETE))
    {
      add(deleteButton = new OrganizerButton("Delete"));
      deleteButton.addActionListener(this);
      deleteButton.setEnabled(false);
    }	
    if (isFlagSet(orderButtons, TOOLBAR_UPDOWN))
    {
      add(upButton = new OrganizerButton("Up"));
      add(downButton = new OrganizerButton("Down"));
      upButton.addActionListener(this);
      downButton.addActionListener(this);
      upButton.setEnabled(false);
      downButton.setEnabled(false);
    }
  }
  
  protected boolean isFlagSet(int orderButtons, int flag)
  {
    return (orderButtons & flag) == flag;
  }

  public void valueChanged(ListSelectionEvent event)
  {
    boolean empty = selectionModel.isSelectionEmpty();
    int min = selectionModel.getMinSelectionIndex();
    int max = selectionModel.getMaxSelectionIndex();
    int size = organizer.getElementCount() - 1;
    if (upButton != null)
      upButton.setEnabled(!empty && min > 0);
    if (downButton != null)
      downButton.setEnabled(!empty && max < size);
    if (deleteButton != null)
      deleteButton.setEnabled(!empty);
  }
  
  public void actionPerformed(ActionEvent event)
  {
    Object source = event.getSource();
    if (source == insertButton)
    {
      Object[] insert = input.getInputData(getParent());
      if (insert != null)
        organizer.insertSelectionList(insert);
    }
    if (source == deleteButton)
    {
      organizer.removeSelected();
    }
    if (source == upButton)
    {
      organizer.moveSelectedUp();
    }
    if (source == downButton)
    {
      organizer.moveSelectedDown();
    }
  }
}

Listing 4

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

public class CenterLayout extends AbstractLayout
  implements LayoutManager2, Serializable
{
  public static final String CENTER = "CENTER";
  public static final String NORTH = "NORTH";
  public static final String SOUTH = "SOUTH";
  public static final String WEST = "WEST";
  public static final String EAST = "EAST";
  
  public static final int VERTICAL = 0;
  public static final int HORIZONTAL = 1;
  
  protected Component center, north, south, east, west;
  protected int hgap, vgap;
  protected int orientation;
  
  public CenterLayout()
  {
    this(0, 0, VERTICAL);
  }

  public CenterLayout(int orientation)
  {
    this(0, 0, orientation);
  }

  public CenterLayout(int hgap, int vgap)
  {
    this(hgap, vgap, VERTICAL);
  }
  
  public CenterLayout(int hgap, int vgap, int orientation)
  {
    this.hgap = hgap;
    this.vgap = vgap;
    this.orientation = orientation;
  }

  public void addLayoutComponent(Component comp, Object constraint)
  {
    if (constraint == null) center = comp;
    if (constraint == CENTER) center = comp;
    if (constraint == NORTH) north = comp;
    if (constraint == SOUTH) south = comp;
    if (constraint == WEST) west = comp;
    if (constraint == EAST) east = comp;
  }

  public void removeLayoutComponent(Component comp)
  {
    if (center == comp) center = null;
    if (north == comp) north = null;
    if (south == comp) south = null;
    if (west == comp) west = null;
    if (east == comp) east = null;
  }

  public Dimension preferredLayoutSize(Container container)
  {
    Insets insets = container.getInsets();
    Dimension centerSize = center == null ?
      new Dimension(0, 0) : center.getPreferredSize();
    Dimension westSize = new Dimension(0, 0);
    Dimension eastSize = new Dimension(0, 0);
    Dimension northSize = new Dimension(0, 0);
    Dimension southSize = new Dimension(0, 0);
    if (west != null) westSize = west.getPreferredSize();
    if (east != null) eastSize = east.getPreferredSize();
    if (north != null) northSize = north.getPreferredSize();
    if (south != null) southSize = south.getPreferredSize();
  
    int height = 0;
    int width = 0;
    int insetsWidth = insets.left + insets.right;
    int insetsHeight = insets.top + insets.bottom;
    if (orientation == VERTICAL)
    {
      height = northSize.height + centerSize.height + southSize.height;
      width = westSize.width + centerSize.width + eastSize.width;
      height = Math.max(westSize.width, height);
      height = Math.max(eastSize.width, height);
    }
    else
    {
      height = northSize.height + centerSize.height + southSize.height;
      width = westSize.width + centerSize.width + eastSize.width;
      width = Math.max(northSize.width, width);
      width = Math.max(southSize.width, width);
    }
    return new Dimension(insetsWidth + width, insetsHeight + height);
  }

  public Dimension minimumLayoutSize(Container container)
  {
    Insets insets = container.getInsets();
    Dimension centerSize = center == null ?
      new Dimension(0, 0) : center.getPreferredSize();
    Dimension westSize = new Dimension(0, 0);
    Dimension eastSize = new Dimension(0, 0);
    Dimension northSize = new Dimension(0, 0);
    Dimension southSize = new Dimension(0, 0);
    if (west != null) westSize = west.getMinimumSize();
    if (east != null) eastSize = east.getMinimumSize();
    if (north != null) northSize = north.getMinimumSize();
    if (south != null) southSize = south.getMinimumSize();
  
    int height = 0;
    int width = 0;
    int insetsWidth = insets.left + insets.right;
    int insetsHeight = insets.top + insets.bottom;
    if (orientation == VERTICAL)
    {
      height = northSize.height + centerSize.height + southSize.height;
      width = westSize.width + centerSize.width + eastSize.width;
      height = Math.max(westSize.width, height);
      height = Math.max(eastSize.width, height);
    }
    else
    {
      height = northSize.height + centerSize.height + southSize.height;
      width = westSize.width + centerSize.width + eastSize.width;
      width = Math.max(northSize.width, width);
      width = Math.max(southSize.width, width);
    }
    return new Dimension(insetsWidth + width, insetsHeight + height);
  }

  public void layoutContainer(Container container)
  {
    Dimension size = container.getSize();
    Insets insets = container.getInsets();
    Dimension centerSize = center == null ?
      new Dimension(0, 0) : center.getPreferredSize();
    int width = size.width - (insets.left + insets.right);
    int height = size.height - (insets.top + insets.bottom);
    int x = insets.left;
    int y = insets.top;
    int w = centerSize.width;
    int h = centerSize.height;
    x += (width - w) / 2;
    y += (height - h) / 2;
    
    int x0 = insets.left;
    int y0 = insets.right;
    int x1 = x - hgap;
    int y1 = y - vgap;
    int x2 = x + w + hgap;
    int y2 = y + h + hgap;
    int x3 = size.width - insets.right;
    int y3 = size.height - insets.bottom;
    
    if (center != null)
      center.setBounds(x, y, w, h);
    
    if (orientation == VERTICAL)
    {
      if (west != null)
        west.setBounds(x0, y0, x1 - x0, y3 - y0);
      if (east != null)
        east.setBounds(x2, y0, x3 - x2, y3 - y0);
      
      if (north != null)
        north.setBounds(x, y0, w, y1 - y0);
      if (south != null)
        south.setBounds(x, y2, w, y3 - y2);
    }
    else
    {
      if (north != null)
        north.setBounds(x0, y0, x3 - x0, y1 - y0);
      if (south != null)
        south.setBounds(x0, y2, x3 - x0, y3 - y2);
      if (west != null)
        west.setBounds(x0, y, x1 - x0, h);
      if (east != null)
        east.setBounds(x2, y, x3 - x2, h);
    }
  }

  public static void main(String[] args)
  {
    JFrame frame = new JFrame();
    frame.getContentPane().setLayout(new BorderLayout());
    
    JPanel panel = new JPanel(new CenterLayout(9, 9, CenterLayout.VERTICAL));
    panel.setBorder(BorderFactory.createEmptyBorder(9, 9, 9, 9));
    JButton center = new JButton("Center");
    center.setPreferredSize(new Dimension(100, 100));
    panel.add(CenterLayout.CENTER, center);
    panel.add(CenterLayout.NORTH, new JButton("North"));
    panel.add(CenterLayout.SOUTH, new JButton("South"));
    panel.add(CenterLayout.WEST, new JButton("West"));
    panel.add(CenterLayout.EAST, new JButton("East"));
    
    frame.getContentPane().add(panel);
    frame.setSize(300, 300);
    frame.setVisible(true);
  }
}

Listing 5

import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;

public class JOrganizer extends JPanel
  implements ActionListener, ListSelectionListener
{
  protected OrganizableList westOrganizer, eastOrganizer;
  ListSelectionModel westSelectionModel, eastSelectionModel;
  protected JButton westButton, eastButton;
  
  public JOrganizer(
    String westTitle, JList westList,
    String eastTitle, JList eastList,
    int orderButtons)
  {
    this(
      new OrganizableListPane(westTitle, westList,
        new OrganizerListInput(), orderButtons),
      new OrganizableListPane(eastTitle, eastList,
        new OrganizerListInput(), orderButtons));
  }
  
  public JOrganizer(
    String westTitle, JTable westTable,
    String eastTitle, JTable eastTable,
    int orderButtons)
  {
    this(
      new OrganizableTablePane(westTitle, westTable,
        new OrganizerTableInput(), orderButtons),
      new OrganizableTablePane(eastTitle, eastTable,
      new OrganizerTableInput(), orderButtons));
  }
  
  public JOrganizer(
    OrganizableList westOrganizer,
    OrganizableList eastOrganizer)
  {
    this.westOrganizer = westOrganizer;
    this.eastOrganizer = eastOrganizer;
    westSelectionModel = westOrganizer.getSelectionModel();
    eastSelectionModel = eastOrganizer.getSelectionModel();
    westSelectionModel.addListSelectionListener(this);
    eastSelectionModel.addListSelectionListener(this);

    JComponent source = westOrganizer.getComponent();
    JComponent target = eastOrganizer.getComponent();
    
    setLayout(new CenterLayout(8, 8));
    JPanel buttons = new JPanel(new GridLayout(2, 1, 4, 4));
    buttons.add(eastButton =
      makeButton("icons/RightIcon.GIF", ">>"));
    buttons.add(westButton =
      makeButton("icons/LeftIcon.GIF", "<<"));
    add(CenterLayout.CENTER, buttons);

    JPanel westPanel = new JPanel(new BorderLayout());
    westPanel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
    westPanel.add(BorderLayout.CENTER, source);
    add(CenterLayout.WEST, westPanel);
    
    JPanel eastPanel = new JPanel(new BorderLayout());
    eastPanel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
    eastPanel.add(BorderLayout.CENTER, target);
    add(CenterLayout.EAST, eastPanel);
    
    eastButton.addActionListener(this);
    westButton.addActionListener(this);
    eastButton.setEnabled(false);
    westButton.setEnabled(false);
  }
  
  public JButton makeButton(String iconFileName, String alternative)
  {
    if ((new File(iconFileName)).exists())
      return new JButton(new ImageIcon(iconFileName));
    return new JButton(alternative);
  }
  
  public void valueChanged(ListSelectionEvent event)
  {
    Object source = event.getSource();
    if (source == westSelectionModel)
    {
      boolean empty = westSelectionModel.isSelectionEmpty();
      eastButton.setEnabled(!empty);
    }
    if (source == eastSelectionModel)
    {
      boolean empty = eastSelectionModel.isSelectionEmpty();
      westButton.setEnabled(!empty);
    }
  }
  
  public void actionPerformed(ActionEvent event)
  {
    Object source = event.getSource();
    if (source == eastButton)
    {
      Object[] selections = westOrganizer.getSelectionList();
      eastOrganizer.insertSelectionList(selections);
      westOrganizer.removeSelected();
      
      int size = westOrganizer.getElementCount();
      int index = westSelectionModel.getMinSelectionIndex();
      if (index >= size) index--;
      if (index >= 0)
        westSelectionModel.setSelectionInterval(index, index);
    }
    if (source == westButton)
    {
      Object[] selections = eastOrganizer.getSelectionList();
      westOrganizer.insertSelectionList(selections);
      eastOrganizer.removeSelected();

      int size = eastOrganizer.getElementCount();
      int index = eastSelectionModel.getMinSelectionIndex();
      if (index >= size) index--;
      if (index >= 0)
        eastSelectionModel.setSelectionInterval(index, index);
    }
  }
}