ORIGINAL DRAFT

One of the most common development requirements is the ability to store and modify configuration parameters. Java provides a Properties class which is useful for storing application settings. To make these properties easy for users to modify, programmers typically build a dialog interface that exposes each of the various fields and values individually. This can be complicated, and often leads to a system that requires time-consuming code changes when variables are added or removed.

This month, we’ll try to alleviate some of these problems with the JConfigure widget. This component automates most of the work you would normally have to do and uses an open architecture to make it as flexible as possible. Our widget presents users with an explorer-style interface, allowing them to edit property fields based on a hiearchy of information. Firgure 1 shows JConfigure in action.

Figure 1: JConfigure at Work.

Figure 1: JConfigure at Work.

By default, the fields are presented using JTextField editors, but a template mechanism allows you to define custom field editors by implementing a simple PropertyField interface. JConfigure is entirely data-driven, adapting to handle new fields or property files without typically requiring any code changes. The only code you should need to write will be for custom edit fields.

Application settings come in many forms. Under Windows, for example, they are commonly held in ".ini" files or in the Windows registry. Under UNIX, you’ll often find them stored as environment variables or ".conf" files. With Java programs, the standard choice is a ".properties" file. Java Properties use a dot-delimited path convention, supporting a hierarchical name space, which we can parse into a tree structure automatically.

User interface designs have started moving toward an explorer-style view for property settings. The older, and still perfectly valid, style was to use tabbed panels. This is still one of the best ways to present settings to the user, but may become cluttered if you have too many categories. Explorer-style tree navigation keeps the interface well organized, supporting easy viewing and editing of almost any number of categories and settings.

Tree Modeling

Each of the entries in a properties file can represent a path with each path element delimited by a dot (period) character. We consider the last element to be the field name, so the path we’re interested in contains all but that last element. We use the JTree control to display the resulting hierarchy. Listing 1 shows ConfigureTree, which merely subclasses JTree in order to make setting the default size and model easier.

The ConfigureTreeModel class in Listing 2 shows how we extend the JFC DefaultTreeModel and add the ability to set and retrieve entire paths. This makes it easier to manage dot-delimited paths rather than individual elements. The constructor sets "properties" as the root node. The addPath method adds child nodes by traversing existing nodes and appending new ones to the child list when appropriate. We use the StringTokenizer class to split the path into pieces and do each lookup, adding any necessary tree nodes as we traverse the path.

The ConfigureTreeModel also implements the getPath method, which returns a linear list of paths in the same order they are stored in the tree model. We do this by traversing the nodes, accumulating path segments along the way. The paths are returned in the form of a Vector. We always maintain order when loading and saving property files so that the presentation can be controlled more effectively.

Listing 3 shows the ConfigureTreeNode class. These are the nodes we use in the ConfigureModel to store path elements. We extend the DefaultMutableTreeNode class and add the ability to get the node name and return a child node by name. The getName method casts the user object back into a string to save time when we need to access it. The getChild method returns a ConfigureTreeNode by taking the name argument and looking at each child node until a match is found.

View Layout

Figure 2 shows the way panels and layout managers are brought together to work our magic. We’ve already talked about the tree, model and node classes. Listing 4 shows the TitleBar class, which is nothing more than an extended JLabel class with a few presets to control the font, spacing and colors. We use this class to display the current panel name, making the output context easier to understand.

Figure 2: JConfigure Layout.

Figure 2: JConfigure Layout.

The DeckLayout manager was covered in an earlier article, so I’ll keep this brief. The class essentially replicates the code from the Java CardLayout manager, updating deprecated calls to their modern equivalents and disabling or enabling child components as required. This fixes a few shortcomings in the CardLayout manager which inappropriately allows focus traversal through invisible components. The net effect is much cleaner and requires no effort on the programmer’s part in order to manage proper focus traversal.

We develop a new layout manager named FieldLayout to handle label/field pairing. You may find this layout manager useful in many of your own applications, since numerous programs have to deal with the same issues. FieldLayout expects prompt and editor component pairs, though you can use it with any valid component. We extend the AbstractLayout class, which was also presented in earlier articles, cutting down the amount of coding we need to do.

The FieldLayout code, presented in Listing 5, implements the minimumLayoutSize, preferredLayoutSize and layoutComponent methods. The first two merely calculate the required size, while layoutComponent does the actual layout work. In each case, we calculate the maximum label width and consider the two-column format of our display to be directed by that value.

Accounting for the margin and the horizontal gap, we resize each component to the maximum label width in the left column and the remaining available width in the right column. The vertical size, accounting for the margin and vertical gap, is the maximum height of the two components on a given line. We also account for situations where an odd number of components are entered and no right component exists for a given row.

While the FieldLayout manager is completely generic, making no assumptions about the components displayed, the FieldPanel provides specific methods for presenting field information. Listing 6 shows the FieldPanel code. We set the layout manager to FieldLayout with vertical and horizontal gaps, and add an EmptyBorder to help with spacing.

The FieldPanel implements two addField methods. Each one creates a JLabel control with the field name provide in the first argument. The second argument is either a string, in which case we create a JTextField element, or a Component argument, which permits additional flexibility. We use this variation to insert custom editors.

FieldPanel implements two additional methods to retrieve information when we need to save the data. The getFieldNames method returns a string array with each of the field names. The getFieldValues returns a string array with the field values. The two arrays are guaranteed to be returned in the same order they were created, so we can reconstruct the properties file when its time to save it.

Custom Editors

To maximize the flexibility of our design, we support customizable edit fields. Listing 7 shows the PropertyField interface each must implement. The LayoutPanel is smart enough to recognize PropetyField components and uses their getValue method to retrieve values. The JConfigure class handles instantiation. The PropertyField interface must be implemented by a Component class that exposes the setValue and getValue methods, along with a zero-argument constructor.

We implement the obvious default PropertyField class in Listing 8. The PropertyTextField class extends JTextField and simply adds the setValue and getValue methods by calling the setText and getText methods in JTextField. Listing 9 shows the PropertyBooleanField class, which extends JComboBox and sets the values to true or false. The setValue and getValue methods use the JComboBox setSelectedItem and getSelectedItem to expose the required interface. Figure 3 shows what these editors look like.

Figure 3: Custom Boolean Fields.

Figure 3: Custom Boolean Fields.

The two previous classes show how easy it is to implement PropertyField classes. Listing 10 shows another, PropertyRectangleField, to demonstrate how you can create complex field editors with relative ease. The PropertyRectangleField class uses four JLabel and four JTextField components arranged in a 4 by 2 GridLayout on a JPanel. The setValue and getValue methods, implementing the PropertyField interface, set the JTextField values based on the parsed comma-delimited property string and return an appropriate string based on the values in those text fields. Figure 4 shows a trio of rectangle fields in JConfigure.

Figure 4: Custom Rectangle Fields.

Figure 4: Custom Rectangle Fields.

The JConfigure Class

The JConfigure class ties all the previously mentioned classes together. The code is presented in Listing 11. The constructor creates the components we’ll need to display our tree navigator, title bar and field panels. The properties file name is the only argument you need to provide. JConfigure will automatically load and parse the properties and display the interface for you. If a file with the same prefix and a ".template" extension is found, it will also be read automatically.

The constructor code sets the leaf node icon for the tree and creates a BevelBorder to place around the title bar. The colors are explicitly set to keep the border thin, setting the inner part of the two-pixel border to the same color as the background. We set the panel border to an EmptyBorder with 5 pixels all the way around. We add the ConfigureTree to the WEST and a JPanel in the CENTER part of the panel’s BorderLayout. The CENTER panel also adds a TitleBar to the NORTH and a new JPanel with the DeckLayout in its own center. Figure 2 shows how the internal components are organized.

We call on a pair of methods to read the template and property files. The readTemplate method merely opens a stream and uses the Properties object load method to read the content into a Hashtable before closing the stream. The readProperties method is more complicated because we want to preserve the order of elements in our properties file. The readProperties method expects a reference to the ConfigureTree components and the file name to be read.

Like readTemplate, readProperties delegates much of its work to another method. We open a BufferedReader stream and read each line independently before closing the file. Each line is parsed by the processLine method and the data is inserted in appropriate locations. We use the ConfigureTree addPath method to keep the tree model up to date and use an addField method to deal with each path tail and associated value.

Because we have multiple cards in the DeckLayout panel, we keep track of the path each one is associated with by using a Hashtable, assigned to the deck member variable. If a panel already exists for a given path, we simply add a field, otherwise we add another FieldPanel to the deck. As each field is processed, we check to see if there’s a template entry associated with it. After creating a field prompt, we set the PropertyField, based on either the template entry we found or the default PropertyTextField. This late binding is accomplished at runtime, using the Class.forName(name).newInstance() combination.

The JConfigure class exposes a pair of save methods. The first assumes you want to save the file to the same file name used at construction time. For integrity reasons, we first rename the existing properties file, changing the extension to ".previous". This gives us a rollback position if something goes wrong. The alternative save method lets you specify the output file name and does not provide this extra safety feature, assuming the original is still available for recovery purposes, if necessary.

The output file content is labeled with a JConfigure statement that includes the current date and time, formatted by the DateFormat class. We then walk the tree model paths after asking for them with the ConfigureTree getPath method. This is guaranteed to be in the right order but may include subpaths that have no associated FieldPanel, so we check each one and then walk the elements in each FieldPanel using the getFieldNames and getFieldValues methods. In each case, we rebuild the full path and write it to the BufferedWriter until we run out of properties.

To switch between views, the JConfigure panel is registered as a TreeSelectionListener. To support this, the valueChanged method responds to user selections by calling getSelectionPath and activating any existing FieldPanel that’s stored in the deck Hashtable under that path. If no path exists, we leave the existing FieldPanel where it is. We use a final method called treePath to rebuild the path from the TreePath structure provided by the JFC.

Listing 12 shows the JConfigureTest class, which wraps a JFrame around the JConfigure component and adds an OK and Cancel button. If the user clicks the OK button, we call the JConfigure save method. If the user clicks Cancel, we ignore the changes and save nothing. This is representative of the way you would typically use JConfigure. Since JConfigure extends the JPanel class, it can easily be used in any window or dialog box.

Summary

The JConfigure widget provides considerable flexibility when it comes to manipulating property files. These files often represent the persistent states and customized settings implemented by your software. Having the ability to present an adaptive generic interface to the user is attractive. The user can automatically navigate and edit properties with minimal programming effort. Furthermore, the model is extensible. You can provide custom editors for any field and support constraints or field validation through a very simple interface. Enjoy.

Listing 1

public class ConfigureTree extends JTree
{
  public ConfigureTree(ConfigureTreeModel model)
  {
    setPreferredSize(new Dimension(150, 150));
    setShowsRootHandles(true);
    setRootVisible(false);
    setModel(model);
  }
}

Listing 2

public class ConfigureTreeModel extends DefaultTreeModel
{
  public ConfigureTreeModel()
  {
    super(new ConfigureTreeNode("properties"));
  }
  
  public void addPath(String path)
  {
    StringTokenizer tokenizer =
      new StringTokenizer(path, ".", false);
    
    ConfigureTreeNode node, next;
    node = (ConfigureTreeNode)getRoot();
    while (tokenizer.hasMoreTokens())
    {
      String name = tokenizer.nextToken();
      next = node.getChild(name);
      if (next == null)
      {
        next = new ConfigureTreeNode(name);
        node.add(next);
      }
      node = next;
    }
  }

  public Vector getPaths()
  {
    Vector vector = new Vector();
    getPaths("", vector, (ConfigureTreeNode)getRoot());
    return vector;
  }

  public void getPaths(String path, Vector vector,
    ConfigureTreeNode node)
  {
    int count = node.getChildCount();
    ConfigureTreeNode child;
    for (int i = 0; i < count; i++)
    {
      child = (ConfigureTreeNode)node.getChildAt(i);
      String next, name = child.getName();
      if (path.length() == 0) next = name; 
      else next = path + "." + name;
      vector.addElement(next);
      getPaths(next, vector, child);
    }
  }
}

Listing 3

public class ConfigureTreeNode extends DefaultMutableTreeNode
{
  public ConfigureTreeNode(String name)
  {
    super(name);
  }

  public String getName()
  {
    return (String)getUserObject();
  }

  public ConfigureTreeNode getChild(String name)
  {
    ConfigureTreeNode node;
    Enumeration enum = children();
    while (enum.hasMoreElements())
    {
      node = (ConfigureTreeNode)enum.nextElement();
      if (node.getName().equals(name)) return node;
    }
    return null;
  }
}

<B>Listing 4</B>

```Java
public class TitleBar extends JLabel
{
  public TitleBar(String text, Icon icon)
  {
    super(text, icon, JLabel.LEFT);
    setFont(new Font(getFont().getName(), Font.PLAIN, 16));
    setPreferredSize(new Dimension(30, 30));
    setBorder(new EmptyBorder(5, 5, 5, 5));
    setBackground(Color.gray);
    setForeground(Color.white);
    setOpaque(true);
  }
}

Listing 5

public class FieldLayout extends AbstractLayout
{
  public FieldLayout() {}
  
  public FieldLayout(int hgap, int vgap)
  {
    super(hgap, vgap);
  }
  
  public Dimension minimumLayoutSize(Container target)
  {
    int left = 0, right = 0, height = 0;
    Insets insets = target.getInsets();
    int ncomponents = target.getComponentCount();
    for (int i = 0; i &lt; ncomponents; i += 2)
    {
      Component label = target.getComponent(i);
      int w1 = label.getMinimumSize().width;
      int h1 = label.getMinimumSize().height;
      if (w1 &gt; left) left = w1;
      if (i + 1 &lt; ncomponents)
      {
        Component field = target.getComponent(i + 1);
        int w2 = field.getMinimumSize().width;
        int h2 = field.getMinimumSize().height;
        if (w2 &gt; right) right = w2;
        height += Math.max(h1, h2) + hgap;
      }
      else height += h1;
    }
    return new Dimension(
      insets.left + insets.right + left + right + vgap,
      insets.top + insets.bottom + height - hgap);
  }

  public Dimension preferredLayoutSize(Container target)
  {
    int left = 0, right = 0, height = 0;
    Insets insets = target.getInsets();
    int ncomponents = target.getComponentCount();
    for (int i = 0; i &lt; ncomponents; i += 2)
    {
      Component label = target.getComponent(i);
      int w1 = label.getPreferredSize().width;
      int h1 = label.getPreferredSize().height;
      if (w1 &gt; left) left = w1;
      if (i + 1 &lt; ncomponents)
      {
        Component field = target.getComponent(i + 1);
        int w2 = field.getPreferredSize().width;
        int h2 = field.getPreferredSize().height;
        if (w2 &gt; right) right = w2;
        height += Math.max(h1, h2) + hgap;
      }
      else height += h1;
    }
    return new Dimension(
      insets.left + insets.right + left + right + vgap,
      insets.top + insets.bottom + height - hgap);
  }
  
  public void layoutContainer(Container target)
  {
    int left = 0;
    int height = 0;
    Insets insets = target.getInsets();
    int ncomponents = target.getComponentCount();
    // Pre-calculate left position
    for (int i = 0; i &lt; ncomponents; i += 2)
    {
      Component label = target.getComponent(i);
      int w = label.getPreferredSize().width;
      if (w &gt; left) left = w;
    }
    int right = target.getSize().width - left
      - insets.left - insets.right - hgap;
    int vpos = insets.top;
    for (int i = 0; i &lt; ncomponents; i += 2)
    {
      Component label = target.getComponent(i);
      int h1 = label.getPreferredSize().height;
      int h2 = 0;
      Component field = null;
      if (i + 1 &lt; ncomponents)
      {
        field = target.getComponent(i + 1);
        h2 = field.getPreferredSize().height;
      }
      int h = Math.max(h1, h2);
      label.setBounds(insets.left, vpos, left, h);
      if (field != null)
        field.setBounds(insets.left + left + hgap, vpos, right, h);
      vpos += h + hgap;
    }
  }
}

Listing 6

public class FieldPanel extends JPanel
{
  protected String path;
  
  public FieldPanel(String path)
  {
    this.path = path;
    setLayout(new FieldLayout(4, 4));
    setBorder(new EmptyBorder(4, 4, 4, 4));
  }
  
  public void addEntry(String prompt, String value)
  {
    add(new JLabel(prompt));
    add(new JTextField(value));
  }

  public void addEntry(String prompt, Component comp)
  {
    add(new JLabel(prompt));
    add(comp);
  }
  
  public String[] getFieldNames()
  {
    int count = getComponentCount() / 2;
    String[] list = new String[count];
    JLabel label;
    for (int i = 0; i &lt; count; i++)
    {
      label = (JLabel)getComponent(i * 2);
      list[i] = label.getText();
    }
    return list;
  }

  public String[] getFieldValues()
  {
    int count = getComponentCount() / 2;
    String[] list = new String[count];
    for (int i = 0; i &lt; count; i++)
    {
      Component component = getComponent(i * 2 + 1);
      if (component instanceof PropertyField)
      {
        PropertyField field = (PropertyField)component;
        list[i] = field.getValue();
      }
      else
      {
        JTextField field = (JTextField)component;
        list[i] = field.getText();
      }
    }
    return list;
  }
}

Listing 7

public interface PropertyField
{
  public String getValue();
  public void setValue(String value);
}

Listing 8

public class PropertyTextField extends JTextField
  implements PropertyField
{
  public PropertyTextField()
  {
    super();
  }
  
  public String getValue()
  {
    return getText();
  }
  
  public void setValue(String value)
  {
    setText(value);
  }
}

Listing 9

public class PropertyBooleanField extends JComboBox
  implements PropertyField
{
  private static String[] list = {&quot;true&quot;, &quot;false&quot;};
  
  public PropertyBooleanField()
  {
    super(list);
  }
  
  public String getValue()
  {
    return (String)getSelectedItem();
  }
  
  public void setValue(String value)
  {
    setSelectedItem(value);
  }
}

Listing 10

public class PropertyRectangleField extends JPanel
  implements PropertyField
{
  JTextField left, top, width, height;
  
  public PropertyRectangleField()
  {
    setBorder(new EtchedBorder());
    setLayout(new GridLayout(2, 4));
    add(new JLabel(&quot;Left: &quot;, JLabel.RIGHT));
    add(left = new JTextField(3));
    add(new JLabel(&quot;Top: &quot;, JLabel.RIGHT));
    add(top = new JTextField(3));
    add(new JLabel(&quot;Width: &quot;, JLabel.RIGHT));
    add(width = new JTextField(3));
    add(new JLabel(&quot;Height: &quot;, JLabel.RIGHT));
    add(height = new JTextField(3));
  }
  
  public String getValue()
  {
    return left.getText() + &quot;,&quot; +
      top.getText() + &quot;,&quot; +
      width.getText() + &quot;,&quot; +
      height.getText();
  }
  
  public void setValue(String value)
  {
    StringTokenizer tokenizer =
      new StringTokenizer(value, &quot;,&quot;, false);
    left.setText(tokenizer.nextToken());
    top.setText(tokenizer.nextToken());
    width.setText(tokenizer.nextToken());
    height.setText(tokenizer.nextToken());
  }
}

Listing 11

public class JConfigure extends JPanel
  implements TreeSelectionListener
{
  protected JTree tree;
  protected JLabel title;
  protected JPanel deck;
  protected String filename;
  protected DeckLayout layout = new DeckLayout();
  protected Hashtable decks = new Hashtable();
  protected ConfigureTreeModel model;
  protected Properties template = new Properties();
  
  public JConfigure(String filename)
    throws IOException
  {
    this.filename = filename;
    ImageIcon icon = new ImageIcon(&quot;Task.gif&quot;);
    UIManager.put(&quot;Tree.leafIcon&quot;, icon);
    Border border = new BevelBorder(BevelBorder.LOWERED,
      getBackground().brighter(), getBackground(),
      getBackground().darker(), getBackground());
      
    setLayout(new BorderLayout());
    setBorder(new EmptyBorder(5, 5, 5, 5));
    model = new ConfigureTreeModel();
    
    deck = new JPanel();
    deck.setLayout(layout);
    
    File templateFile = new File(
      filename.substring(0, filename.lastIndexOf('.')) +
      &quot;.template&quot;);
    if (templateFile.exists())
      readTemplate(templateFile.getName());
    readProperties(model, filename);
    
    tree = new ConfigureTree(model);
    tree.addTreeSelectionListener(this);
    add(&quot;West&quot;, new JScrollPane(tree));
    
    title = new TitleBar(&quot;Properties&quot;, icon);
    
    JPanel content = new JPanel();
    content.setLayout(new BorderLayout());
    content.add(&quot;Center&quot;, new JScrollPane(deck));
    
    JPanel panel = new JPanel();
    panel.setLayout(new BorderLayout());
    panel.setBorder(new EmptyBorder(0, 5, 0, 0));
    panel.add(&quot;North&quot;, title);
    panel.add(&quot;Center&quot;, content);
    add(&quot;Center&quot;, panel);
  }

  public void readTemplate(String filename)
    throws IOException
  {
    InputStream input = new FileInputStream(filename);
    template.load(input);
    input.close();
  }
  
  public void save() throws IOException
  {
    String prevfile = filename.substring(0,
      filename.lastIndexOf('.')) + &quot;.previous&quot;;
    File prev = new File(prevfile);
    if (prev.exists()) prev.delete();
    File file = new File(filename);
    if (file.exists()) file.renameTo(prev);
    writeProperties(model, filename);
  }
  
  public void save(String filename) throws IOException
  {
    writeProperties(model, filename);
  }
  
  private void writeProperties(ConfigureTreeModel tree,
    String filename) throws IOException
  {
    Vector paths = model.getPaths();
    int count = paths.size();
    
    BufferedWriter writer =
      new BufferedWriter(new FileWriter(filename));
    
    writer.write(&quot;# Saved by JConfigure on &quot; +
      DateFormat.getDateTimeInstance(
        DateFormat.SHORT, DateFormat.SHORT).
        format(new Date(System.currentTimeMillis())));
    writer.newLine();
    
    for (int i = 0; i &lt; count; i++)
    {
      String path = paths.elementAt(i).toString();
      if (decks.containsKey(path))
      {
        FieldPanel card = (FieldPanel)decks.get(path);
        String[] names = card.getFieldNames();
        String[] values = card.getFieldValues();
        for (int j = 0; j &lt; names.length; j++)
        {
          writer.write(path + &quot;.&quot; +
            names[j].substring(0,
              names[j].length() - 1)
             + &quot;=&quot; + values[j]);
          writer.newLine();   
        }
        
      }
    }
    writer.close();
  }

  private void readProperties(ConfigureTreeModel tree,
    String filename) throws IOException
  {
    BufferedReader reader =
      new BufferedReader(new FileReader(filename));
    String line;
    while ((line = reader.readLine()) != null)
    {
      processLine(tree, line);
    }
    reader.close();
  }

  private void processLine(ConfigureTreeModel tree, String line)
  {
    if (line.charAt(0) == '#') return;
    String prop = line.substring(0, line.indexOf('='));
    String path = prop.substring(0, prop.lastIndexOf('.'));
    String name = prop.substring(path.length() + 1);
    String value = line.substring(prop.length() + 1);
    tree.addPath(path);
    addField(path, name, value);
  }   

  public void addField(String path, String name, String value)
  {
    FieldPanel card;
    if (decks.containsKey(path))
    {
      card = (FieldPanel)decks.get(path);
    }
    else
    {
      card = new FieldPanel(path);
      decks.put(path, card);
      deck.add(path, card);
    }
    try
    {
      String className = &quot;PropertyTextField&quot;;
      String prop = path + &quot;.&quot; + name;
      if (template.containsKey(prop))
        className = (String)template.get(prop);
      PropertyField field = (PropertyField)
        Class.forName(className).newInstance();
      field.setValue(value);
      card.addEntry(name + &quot;:&quot;, (Component)field);
    }
    catch (InstantiationException e)
    {
      System.out.println(e);
    }
    catch (ClassNotFoundException e)
    {
      System.out.println(e);
    }
    catch (IllegalAccessException e)
    {
      System.out.println(e);
    }
  }

  public void valueChanged(TreeSelectionEvent event)
  {
    Object obj = tree.getLastSelectedPathComponent();
    if (obj == null) return;
    ConfigureTreeNode node = (ConfigureTreeNode)obj;
    if (node.isLeaf())
    {
      title.setText(node.getName() + &quot; properties&quot;);
      String path = treePath(tree.getSelectionPath());
      layout.show(deck, path);
      deck.repaint();
    }
  }
  
  public String treePath(TreePath treePath)
  {
    ConfigureTreeNode node;
    Object[] list = treePath.getPath();
    StringBuffer path = new StringBuffer();
    for (int i = 1; i &lt; list.length; i++)
    {
      node = (ConfigureTreeNode)list[i];
      if (i &gt; 1) path.append(&quot;.&quot;);
      path.append(node.getName());
    }
    return path.toString();
  }
}

Listing 12

public class JConfigureTest extends JPanel
  implements ActionListener
{
  protected JButton ok, cancel;
  protected JConfigure configure;

  public JConfigureTest() throws IOException
  {
    setLayout(new BorderLayout());
    configure = new JConfigure(&quot;JConfigure.properties&quot;);
    add(&quot;Center&quot;, configure);
    
    JPanel buttons = new JPanel();
    buttons.setLayout(new GridLayout(1, 2));
    buttons.add(buttonPanel(ok = new JButton(&quot;OK&quot;)));
    buttons.add(buttonPanel(cancel = new JButton(&quot;Cancel&quot;)));
    ok.addActionListener(this);
    cancel.addActionListener(this);
    
    JPanel south = new JPanel();
    south.setBorder(new EdgeBorder(EdgeBorder.NORTH));
    south.setLayout(new BorderLayout());
    south.add(&quot;East&quot;, buttons);
    add(&quot;South&quot;, south);
  }

  private JPanel buttonPanel(JButton button)
  {
    JPanel panel = new JPanel();
    panel.setBorder(new EmptyBorder(4, 4, 4, 4));
    panel.setLayout(new BorderLayout());
    panel.add(&quot;Center&quot;, button);
    return panel;
  }

  public void actionPerformed(ActionEvent event)
  {
    Object source = event.getSource();
    if (source == ok)
    {
      try
      {
        configure.save();
      }
      catch (IOException e) {}
      setVisible(false);
      System.exit(0);
    }
    if (source == cancel)
    {
      setVisible(false);
      System.exit(0);
    }
  }

  public static void main(String[] args)
    throws IOException
  {
    PLAF.setNativeLookAndFeel(true);
    JFrame frame = new JFrame(&quot;JConfigure Test&quot;);
    frame.setBounds(100, 100, 500, 360);
    frame.getContentPane().setLayout(new BorderLayout());
    frame.getContentPane().add(&quot;Center&quot;, new JConfigureTest());
    frame.show();
  }
}