ORIGINAL DRAFT

This month’s visual component is fairly simple, though useful in a wide variety of circumstances. Software with tables, including spreadsheets, often support customizable borders. Cell borders are typically editable using a specialized component that provides end-user control over various parameters. Each side of the border can be turned on and off, and may be adjusted for thickness or fill, for example. JCellBorder does just that and supports the use of a standard implementation of the Swing Border interface, called CellBorder.

Aside from controlling the edges, thickness, and fill, you can also set the color of the CellBorder, though we won’t provide color support in our interface, since it’s so easy to implement using the Swing JColorChooser. Instead, we’ll focus on making it easy to change each side of the border and to toggle between line and fill modes by just clicking in the center panel. If you take a look at Figure 1, you’ll see the basic layout in action. JCellBorder is implemented as a JPanel extension, so you can present it either in a window or dialog box, depending on your needs.

Figure 1: JCellBorder with a simple border and
all four edges activated. The north and south edges are set to five pixels, while the west and east edges are 
set to one pixel.

Figure 1: JCellBorder with a simple border and all four edges activated. The north and south edges are set to five pixels, while the west and east edges are set to one pixel.

The first thing we want to do is develop a CellBorder class that implements the Swing Border interface. By following the Border interface, we guarantee that CellBorder is completely reusable, with any JComponent implementation. After a quick look at CellBorder, we’ll take a closer look at JCellBorder, which provides user-interactive control over CellBorder, for customization purposes.

Figure 2: The JCellBorder editor uses a 
CellBorderPanel to show the Cellborder and a couple of Icon implementations for the buttons.

Figure 2: The JCellBorder editor uses a CellBorderPanel to show the Cellborder and a couple of Icon implementations for the buttons.

Figure 2 shows the classes used in this project. Both the arrows and dotted square views are implemented as Swing Icon objects. Again, we’re maximizing reusability by doing this, enabling you to reuse the ArrowIcon and CellBorderIcon classes in any JLabel or JButton, for example. Both these classes are straight forward enough to be self-explanatory. The Icon interface looks like this:

public interface Icon
{
  public int getIconHeight();
  public int getIconWidth();
  public void paintIcon(Component c, Graphics g, int x, int y);
}

The getIconHeight and getIconWidth methods are pretty self-evident and the paintIcon method does the real work of drawing the icon. You’ll notice that the x, y origin is specified and you have access to the Graphics context to draw into. If necessary, you can also get information about the Component in which you’re drawing, but that’s rarely necessary. In our case, the ArrowIcon draws an arrow in the specified direction and the CellBorderIcon draws a dotted rectangle with a solid line on the specified side. In both these classes, I’ve provided constructors that let you specify the icon size and direction parameters.

Let’s take a look at the CellBorder class and then we’ll cover JCellBorder and a supporting class called CellBorderPanel. The ArrowIcon, CellBorderIcon and the rest of the classes in this project are available online at www.javapro.com.

Listing 1 shows the code for CellBorder. There are five constructors, the first four with default values for different permutations of the fifth. The last constructor accepts the three parameters you can change in the CellBorder class, the Insets (which is a standard AWT object for specifying the thickness of the border on each of the four sides), the Color of the border, and a boolean value specifying whether the border is drawn as an inner and outer line border or filled-in as a solid border.

The default value for each of these parameters are specified in the argument-free constructor, which creates a filled, black, single-pixel border. Of course, when the line is a single pixel on each side, it matters little whether your are in fill mode or not, but this is a suitable default value in most cases. The other three constructors let you specify the Insets only, the Insets and fill or the Insets and Color parameters, using the default values mentioned for the first constructor where a given parameter is not passed in as an argument.

Cellborder implements the Swing Border interface which has three methods and looks like this:

public interface Border
{
  public Insets getBorderInsets(Component c);
  public boolean isBorderOpaque(); 
  public  void paintBorder(Component c, Graphics g, int x, int y, int w, int h);
}

The getBorderInsets method returns our Insets object, the isBorderOpaque method returns the state of our fill parameter, and the paintBorder method actually draws the border. The paintBorder method signature includes a Graphics context and the rectangular coordinates of the outside of the border to be drawn. This is important because a Border may be nested with a CompoundBorder and may not be drawn on the outside edge of the component, so we have to avoid making any assumptions in this regard.

The border is drawn either as four filled-in rectangles, one on each side if we’re drawing a solid fill, or as two line rectangles, one on the inside and one on the outside if we’re not filling the border. You’ll notice that we make no assumptions about the background and don’t drawn outside the Insets area. A drawRect call places the bottom and right lines below and to the right of the width and height coordinates, so we have to make a few adjustments to make sure we’re painting in the right place.

The rest of the CellBorder class is primarily a set of accessors that enable us to reach existing values or set them to new values more dynamically. Most existing Border implementations are written as immutable objects and don’t tend to provide accessors beyond those specified in the interface, but we expect to change things on-the-fly, so both read and write accessors are provided for each of the arguments supported by CellBorder.

The getInsets method is provided for convenience, since it’s intuitively better than getBorderInsets, which expects a Component argument, not actually needed in this case. It seemed like a good idea to provide an accessor that doesn’t imply any alternate behavior, effectively avoiding the expectation that passing a null component value is a good idea.

To present the CellBorder in our editable interface, we use a JPanel extension called CellBorderPanel. Listing 2 shows the code for CellBorderPanel. The constructor sets up a default CellBorder and makes sure the panel is not opaque so that changes to the containing panel can be made without affecting the background differently. We also implement the MouseListener interface and register for mouse events in the constructor.

When the mousePressed event is received, we toggle the border fill based on the current setting and repaint the panel. We do this by calling the refresh method, which also calls revalidate before repainting. This is important since the border size impacts the drawing area for the panel and typically needs to be adjusted before repainting. We use the refresh method for convenience throughout the CellBorderPanel class after making changes to the border.

There are eight adjustment methods and two accessors for the Border itself. Since you may want to present the user with an editing view on an existing border, you need to be able to pass the border into the JCellBorder component easily, and retrieve it when editing is complete. The getBorder method is simple but you’ll notice that the setBorder method also sets up an number of instance variables that specify the insets for each side as well as whether they are selected as visible or not. By default, we set each edge to be visible.

The eight methods that follow provide the ability to toggle visibility and set the thickness for each of the four sides of the border. The ability to toggle visibility is useful in permitting users to flip the visibility of each side on or off without loosing the thickness associated with it. CellBorder itself doesn’t care about these notions, so it is the CellBorderPanel that manages these values.

The various toggle methods first set the internal select value and then change the Insets value of the border, depending on the setting. If the select value is false, the inset for the specified side is set to zero. Otherwise, the value depends on the last used insets value for that side. After making changes, we call refresh to update the view.

The set of adjust methods change the value of the insets for a given side. Since it’s possible that the side being edited is not visible, we check and return immediately if it’s not. Otherwise, we can apply the requested change. The increment value may be either positive or negative and, while I’ve not imposed any maximum value, the code tests to make sure we don’t drop below zero. If this was permitted, the user may be confused when pressing the increment button if the insets value was several increments below zero, since no visible change would occur.

JCellBorder is responsible for a number of elements aside from the CellBorderPanel. Listing 3 shows the code for JCellBorder, which first declares variables for each arrow and toggle button, along with an instance of the CellBorderPanel. The arrow buttons are named sideMore and sideLeft, where side is one of the four cardinal directions, indicating whether they increment or decrement the thickness. The toggle button variables are simply named for the four cardinal directions.

As is common with Swing components, most of the work done by the constructor is meant to set up the visual display. Each of the buttons are created and assigned to their respective instance variables, placed in panels with appropriate layout manages to arrange them the way we want. Each of the arrow button pairs are set in a NORTH/SOUTH or EAST/WEST BorderLayout position. We use a method called makeButtonPanel to construct the panel, based on the orientation provided as an argument.

There are two methods we use to actually build the buttons; makeToggleButton and makeArrowButton. In each case we are interested in controlling various parameters without having to subclass JToggleButton and JButton, respectively. The margin is removed, as is the ability to paint the focus and we set a preferred size explicitly to control the visual layout more effectively. Mostly, these are esthetic adjustments that keep the component arranged properly.

Most of the work is delegated to CellBorderPanel in the actionPerformed method. Each button registers this class as an ActionListener and we use the instance variables to recognize which button was activated. It’s worth mentioning that this is a much better approach than watching for String values in each button, though I have to say that I still see way too much code written that way even today. Aside from higher overhead, using strings is troublesome when you try to localize your code, not to mention prone to all kinds of spelling errors that typed variables protect your from.

JCellBorder is an interesting component for a couple of reasons. First is provides an interactive control for editing a highly reusable CellBorder class that can be applied to any Swing component. It also applies a couple of Swing interfaces that maximize reusability and flexibility, effectively extending Swing with some useful implementations. One of the simplest classes, ArrowIcon, is especially useful when you want to control the size and orientation of an arrow in a button or label. JCellBorder, as a whole, helps users customize table or other cells on-demand, enhancing your applications and the end-user experience.

Listing 1

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

public class CellBorder
  implements Border
{
  protected Insets insets;
  protected boolean fill;
  protected Color color;
  
  public CellBorder()
  {
    this(new Insets(1, 1, 1, 1),
      Color.black, true);
  }
  
  public CellBorder(Insets insets)
  {
    this(insets, Color.black, true);
  }
  
  public CellBorder(
    Insets insets, boolean fill)
  {
    this(insets, Color.black, fill);
  }
  
  public CellBorder(
    Insets insets, Color color)
  {
    this(insets, color, true);
  }
  
  public CellBorder(Insets insets,
    Color color, boolean fill)
  {
    this.insets = insets;
    setColor(color);
    setFill(fill);
  }
    
  public boolean getFill()
  {
    return fill;
  }
  
  public void setFill(boolean fill)
  {
    this.fill = fill;
  }
  
  public boolean isBorderOpaque()
  {
    return fill;
  }
  
  public Color getColor()
  {
    return color;
  }
  
  public void setColor(Color color)
  {
    this.color = color;
  }
  
  public Insets getInsets()
  {
    return insets;
  }
  
  public Insets getBorderInsets(Component c)
  {
    return insets;
  }
  
  public void paintBorder(Component c,
    Graphics g, int x, int y, int w, int h)
  {
    g.setColor(color);
    if (fill)
    {
      g.fillRect(x, y, w, insets.top);
      g.fillRect(x, y, insets.left, h);
      g.fillRect(x, y + h - insets.bottom,
        w, insets.bottom);
      g.fillRect(x + w - insets.right,
        y, insets.right, h);
    }
    else
    {
      g.drawRect(x, y, w - 1, h - 1);
      g.drawRect(
        x + insets.left - 1,
        y + insets.top - 1,
        w - insets.left - insets.right + 1,
        h - insets.top - insets.bottom + 1);
    }
  }
}

Listing 2

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

public class CellBorderPanel extends JPanel
  implements MouseListener
{
  protected CellBorder border;
  protected boolean northSelect, southSelect;
  protected boolean eastSelect, westSelect;
  protected int northInset, southInset;
  protected int eastInset, westInset;
  
  public CellBorderPanel()
  {
    setCellBorder(new CellBorder(
      new Insets(1, 1, 1, 1), Color.blue));
    addMouseListener(this);
    setOpaque(false);
  }
  
  public void mouseEntered(MouseEvent event) {}
  public void mouseExited(MouseEvent event) {}
  public void mouseClicked(MouseEvent event) {}
  public void mouseReleased(MouseEvent event) {}
  
  public void mousePressed(MouseEvent event)
  {
    border.setFill(!border.getFill());
    refresh();
  }
  
  public CellBorder getCellBorder()
  {
    return border;
  }
  
  public void setCellBorder(CellBorder border)
  {
    this.border = border;
    setBorder(border);
    Insets insets = getInsets();
    northInset = insets.top;
    southInset = insets.bottom;
    eastInset = insets.right;
    westInset = insets.left;
    northSelect = true;
    southSelect = true;
    eastSelect = true;
    westSelect = true;
  }
  
  protected void refresh()
  {
    revalidate();
    repaint();
  }
  
  public void toggleNorth(boolean selected)
  {
    northSelect = selected;
    border.getInsets().top =
      northSelect ? northInset : 0;
    refresh();
  }
  
  public void toggleSouth(boolean selected)
  {
    southSelect = selected;
    border.getInsets().bottom =
      southSelect ? southInset : 0;
    refresh();
  }
  
  public void toggleEast(boolean selected)
  {
    eastSelect = selected;
    border.getInsets().right =
      eastSelect ? eastInset : 0;
    refresh();
  }
  
  public void toggleWest(boolean selected)
  {
    westSelect = selected;
    border.getInsets().left =
      westSelect ? westInset : 0;
    refresh();
  }
  
  public void adjustNorth(int increment)
  {
    if (!northSelect) return;
    northInset += increment;
    northInset = Math.max(0, northInset);
    border.getInsets().top = northInset;
    refresh();
  }
  
  public void adjustSouth(int increment)
  {
    if (!southSelect) return;
    southInset += increment;
    southInset = Math.max(0, southInset);
    border.getInsets().bottom = southInset;
    refresh();
  }

  public void adjustWest(int increment)
  {
    if (!westSelect) return;
    westInset += increment;
    westInset = Math.max(0, westInset);
    border.getInsets().left = westInset;
    refresh();
  }

  public void adjustEast(int increment)
  {
    if (!eastSelect) return;
    eastInset += increment;
    eastInset = Math.max(0, eastInset);
    border.getInsets().right = eastInset;
    refresh();
  }
}

Listing 3

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

public class JCellBorder extends JPanel
  implements ActionListener, SwingConstants
{
  protected JButton northMore, southMore, eastMore, westMore;
  protected JButton northLess, southLess, eastLess, westLess;
  protected JToggleButton north, south, east, west;
  protected CellBorderPanel borderPanel;
    
  public JCellBorder()
  {
    setLayout(new BorderLayout(2, 2));
    setPreferredSize(new Dimension(200, 200));
    setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
      
    JPanel panel = new JPanel(new GridLayout());
    panel.setBorder(BorderFactory.createCompoundBorder(
      BorderFactory.createEtchedBorder(),
      BorderFactory.createEmptyBorder(10, 10, 10, 10)));
    panel.add(borderPanel = new CellBorderPanel());

    add(BorderLayout.CENTER, panel);
    
    JPanel northPanel = new JPanel(new BorderLayout());
    northPanel.add(
      BorderLayout.CENTER, makeButtonPanel(
      northLess = makeArrowButton(NORTH),
      northMore = makeArrowButton(SOUTH),
      true));
    northPanel.add(BorderLayout.WEST,
      west = makeToggleButton(WEST));
    northPanel.add(BorderLayout.EAST,
      north = makeToggleButton(NORTH));
    add(BorderLayout.NORTH, northPanel);
    
    JPanel southPanel = new JPanel(new BorderLayout());
    southPanel.add(
      BorderLayout.CENTER, makeButtonPanel(
      southMore = makeArrowButton(NORTH),
      southLess = makeArrowButton(SOUTH),
      true));
    southPanel.add(BorderLayout.WEST,
      south = makeToggleButton(SOUTH));
    southPanel.add(BorderLayout.EAST,
      east = makeToggleButton(EAST));
    add(BorderLayout.SOUTH, southPanel);
    
    add(BorderLayout.WEST, makeButtonPanel(
      westLess = makeArrowButton(WEST),
      westMore = makeArrowButton(EAST),
      false));
    add(BorderLayout.EAST, makeButtonPanel(
      eastMore = makeArrowButton(WEST),
      eastLess = makeArrowButton(EAST),
      false));
    
    north.addActionListener(this);
    south.addActionListener(this);
    east.addActionListener(this);
    west.addActionListener(this);
    
    northMore.addActionListener(this);
    northLess.addActionListener(this);
    southMore.addActionListener(this);
    southLess.addActionListener(this);
    westMore.addActionListener(this);
    westLess.addActionListener(this);
    eastMore.addActionListener(this);
    eastLess.addActionListener(this);
  }
  
  public CellBorder getCellBorder()
  {
    return borderPanel.getCellBorder();
  }
  
  public void setCellBorder(CellBorder border)
  {
    borderPanel.setCellBorder(border);
  }

  public JPanel makeButtonPanel(
    JButton one, JButton two, boolean vertical)
  {
    JPanel panel = new JPanel(new BorderLayout());
    if (vertical)
    {
      panel.add(BorderLayout.NORTH, one);
      panel.add(BorderLayout.SOUTH, two);
    }
    else
    {
      panel.add(BorderLayout.WEST, one);
      panel.add(BorderLayout.EAST, two);
    }
    return panel;
  }
  
  public JToggleButton makeToggleButton(int dir)
  {
    JToggleButton button = new JToggleButton(new CellBorderIcon(dir));
    button.setPreferredSize(new Dimension(24, 24));
    button.setMargin(new Insets(0, 0, 0, 0));
    //button.setBorderPainted(false);
    button.setFocusPainted(false);
    button.setSelected(true);
    return button;
  }
  
  public JButton makeArrowButton(int dir)
  {
    JButton button = new JButton(new ArrowIcon(dir));
    button.setPreferredSize(new Dimension(12, 12));
    button.setMargin(new Insets(0, 0, 0, 0));
    button.setBorderPainted(false);
    button.setFocusPainted(false);
    return button;
  }
  
  public void actionPerformed(ActionEvent event)
  {
    Object source = event.getSource();
    if (source == north)
    {
      borderPanel.toggleNorth(north.isSelected());
    }
    if (source == south)
    {
      borderPanel.toggleSouth(south.isSelected());
    }
    if (source == east)
    {
      borderPanel.toggleEast(east.isSelected());
    }
    if (source == west)
    {
      borderPanel.toggleWest(west.isSelected());
    }
    
    if (source == northMore)
    {
      borderPanel.adjustNorth(1);
    }
    if (source == southMore)
    {
      borderPanel.adjustSouth(1);
    }
    if (source == westMore)
    {
      borderPanel.adjustWest(1);
    }
    if (source == eastMore)
    {
      borderPanel.adjustEast(1);
    }
    
    if (source == northLess)
    {
      borderPanel.adjustNorth(-1);
    }
    if (source == southLess)
    {
      borderPanel.adjustSouth(-1);
    }
    if (source == westLess)
    {
      borderPanel.adjustWest(-1);
    }
    if (source == eastLess)
    {
      borderPanel.adjustEast(-1);
    }
  }
}