ORIGINAL DRAFT
The closer the metaphor of a user interface to reality, the easier it is to convey expectations to the end user. Seldom encountered but very reflective is the book metaphor, which allows users to navigate through several pages of information. This approach has be used effectively in Personal Information Management (PIM) software. This month’s development effort centers on a component called JBinder which emulates the behavior of a physical binder, allowing you to drop any JComponent into the framework, automatically providing page numbering and navigation buttons in an esthetically pleasant display.
Among other things, we’ll be implementing a BinderLayout manager, which works much like a CardLayout divided into a left and right panels.You can see JBinder demonstrated in Figure 1. The JBinderTest class generates a collection of randomized art pages and lets you navigate through them using the JBinder component. The small, folded corners allow you to move forward and backward through the pages. The visual effect is achieved by using an OverlayLayout and setting a few rings atop the book pages. We’ll use different components to represent the book, book pages, rings and other elements to make these elements as reusable as possible.
Figure 2 shows the classes we’ll be developing in this article. The AbstractLayout is the basis for all my layout managers. It merely implements the methods required by the LayoutManager2 interface, leaving the preferredLayoutSize, minimumLayoutSize and layoutContainer methods to be implemented by the subclass. I’ve used this technique for close to three years for implementing layout managers and I can declare with confidence that I’ve never had to do any surgery on this class, suggesting that it’s about as reusable as they get.
We’ll cover the BinderLayout implementation but leave AbstractLayout for another time. You’ll find it online with the rest of the code at www.java-pro.com. The CurvedBorder and ShadowBorder were implemented in a previous Visual Components: JBorder column, so I won’t cover those in this installment either. Since print space is limited, most of our focus will center on BinderLayout, BinderBook, BinderPage and JBinder itself.
BinderButton is a trivial class which fires an ActionEvent to listeners whenever it’s clicked. It draws a little triangle in the upper left or right, depending on the page it’s on, acting predominantly like a page-turning button. BinderButton is used to move to the next or previous page, depending on which button is pressed. If the upper right button is pressed, we move to the next page. Otherwise, we move back a page. The visual effect of changing pages is strong enough that additional feedback to show mouse clicks seemed unnecessary in this design.
Listing 1 shows the code for BinderLayout. The preferredLayoutSize and minimumLayoutSize methods calculate the maximum width and height of a single component and multiply the width by two, adding insets and the horizontal gap to account for the largest possible preferred and minimum sizes, respectively.
Most of the work is accomplished by the layoutContainer method, which walks through the list of contained components, setting whether they are visible and enabled, using calls to the setVisible and setEnabled methods. Accounting for insets and the horizontal gap setting, one or two pages are resized to cover the left and right half of the display area. If this is the first component, it gets displayed in the right half, emulating the first page of an open book. Additional pages are displayed in pairs, unless the last page is an even number, in which case it ends up on the left side, leaving an empty right side, like the end of a book.
The BinderBook class in Listing 2 is responsible for displaying the book view and uses the BinderLayout to arrange it’s children. We use the CurvedBorder implementation to round the outside edges, with an empty border inset to leave some additional space around the outside, achieving the desired look of a book cover in the background. The paintComponent method draws a silvery, rounded rectangle to emulate the rib in a binder, as well as a couple of edges to highlight the folds you would find in a real book.
To make it easier to increment or decrement the visible current page, we implement two aptly named methods called increment and decrement, which make sure it’s appropriate to move forward or backward before executing the change and calling revalidate to redo the layout. Since it’s common to need to determine what page a given component is on, we also implement a getPageNumber method which expects a reference to the component being sought, returning -1 if the component was not found.
Listing 3 shows the code for BinderPage, which wraps any component you add to the JBinder class with a JPanel extension that has appropriate margins. The header needs to contains a BinderButton for navigation but there are conditions under which this button may need to be on the right or left, or even unused, so we create both a west and east copy and override the setVisible method to watch for changes that affect the view.
When a page is made visible, we determine which component it is, on which side the button should be drawn, if at all, and enable it as necessary. We also set the page number in the footer at the same time. By using this approach, rather than predetermining the page position, it becomes possible to insert or remove pages dynamically, without any negative repercussions.
Notice that we use a compound ShadowBorder and LineBorder combination to get the visual effect of a page. The margins of the page are the result of using white panels with a minimum height and width of 16 pixels. The BinderButton implementation uses the same 16 by 16 dimensions and the JLabel used for a footer tends to take a similar amount of space, assuming a default font size.
The constructor for BinderPage expects a reference to the JBinder it is associated with, as well as the component to be contained in the center part of the BorderLayout. We use the JBinder reference to increment or decrement the page number when we receive an ActionEvent. As such, BinderPage implements the ActionListener interface and chooses which direction we need to go, based on whether the west or east BinderButton is visible.
JBinder ties everything together. Listing 4 shows how it is implemented. We use an OverlayLayout to put a binder rings above the BinderBook we create. The binder rings are merely a set of JLabel/ImageIcon elements with a largely transparent BinderRing.gif file that show a couple of ring holes and the ring effect itself. The increment and decrement methods make it possible to move back and forth through the pages and automatically repaint the display as the view changes.
We expose a getBook method to make sure the book reference is accessible from the BinderPage implementation without tight coupling. This is more desirable than asking for the book instance variable directly, just in case things have to change at some later time.
Finally, we have an addPage method which will create a BinderPage instance for us automatically when we add a page. This allows us to use arbitrary components for each page without having to worry about consistency or navigation, since BinderPage basically takes care of all that for us. To make sure you can call the standard Container remove method to remove pages, the addPage method returns the wrapping BinderPage reference we used. You’ll need to use that reference to drop pages from the book if you need to.
JBinder is a simple yet effective component that lets you use a book metaphor to communicate with your user. Not all applications lend themselves to this paradigm, so you need to exercise caution before deciding to use it. For personal information managers, this is a good reflection of the way people already use paper versions to manage business cards, appointments, calendars, and so on, but word processors or spreadsheets would obviously suffer from a lack of real estate. Whatever your choice, these techniques are instructive, and you may even find that BinderLayout itself can be useful under alternate circumstances. Enjoy.
Listing 1
import java.awt.*;
public class BinderLayout extends AbstractLayout
{
protected int page = 0;
public BinderLayout(int hgap)
{
super(hgap, 0);
}
public Dimension preferredLayoutSize(Container parent)
{
int count = parent.getComponentCount();
int width = 0;
int height = 0;
for (int i = 0; i < count; i++)
{
Component child = parent.getComponent(i);
Dimension size = child.getPreferredSize();
if (size.width > width)
width = size.width;
if (size.height > height)
height = size.height;
}
Insets insets = parent.getInsets();
int x = insets.left + insets.right;
int y = insets.top + insets.bottom;
return new Dimension(width * 2 + x, height + y);
}
public Dimension minimumLayoutSize(Container parent)
{
int count = parent.getComponentCount();
int width = 0;
int height = 0;
for (int i = 0; i < count; i++)
{
Component child = parent.getComponent(i);
Dimension size = child.getMinimumSize();
if (size.width > width)
width = size.width;
if (size.height > height)
height = size.height;
}
Insets insets = parent.getInsets();
int x = insets.left + insets.right;
int y = insets.top + insets.bottom;
return new Dimension(width * 2 + x, height + y);
}
public void layoutContainer(Container parent)
{
int count = parent.getComponentCount();
Insets insets = parent.getInsets();
int x = insets.left;
int y = insets.top;
int w = parent.getSize().width -
(insets.left + insets.right) - hgap;
int h = parent.getSize().height -
(insets.top + insets.bottom);
for (int i = 0; i < count; i++)
{
Component child = parent.getComponent(i);
boolean active = (i == page - 1 || i == page);
child.setVisible(active);
child.setEnabled(active);
if (i == page - 1)
{
child.setBounds(x, y, w / 2, h);
}
if (i == page)
{
child.setBounds(x + w / 2 + hgap, y, w / 2, h);
}
}
}
public int getPage()
{
return page;
}
public void setPage(int page)
{
this.page = page;
}
}
Listing 2
import java.awt.*;
import java.awt.geom.*;
import javax.swing.*;
public class BinderBook extends JPanel
{
protected BinderLayout layout;
public BinderBook()
{
setOpaque(false);
setLayout(layout = new BinderLayout(10));
setBorder(BorderFactory.createCompoundBorder(
new CurvedBorder(7),
BorderFactory.createEmptyBorder(8, 8, 8, 8)));
}
public void paintComponent(Graphics gc)
{
Graphics2D g = (Graphics2D)gc;
int w = getSize().width;
int h = getSize().height;
int m = w / 2;
int x = 33;
g.setColor(Color.black);
g.drawLine(m - x - 1, 0, m - x - 1, h);
g.drawLine(m + x - 1, 0, m + x - 1, h);
g.setColor(Color.white);
g.drawLine(m - x + 1, 0, m - x + 1, h);
g.drawLine(m + x + 1, 0, m + x + 1, h);
int z = 27;
RoundRectangle2D shape = new
RoundRectangle2D.Float(m - z, 20, z * 2, h - 40, 15, 15);
GradientPaint gradient = new GradientPaint(
m - z, 0, Color.white, m + z, 0, Color.lightGray);
g.setPaint(gradient);
g.fill(shape);
g.setColor(Color.black);
g.draw(shape);
}
public void increment()
{
int page = layout.getPage();
int count = getComponentCount();
if (page + 2 <= count)
{
layout.setPage(page + 2);
revalidate();
}
}
public void decrement()
{
int page = layout.getPage();
int count = getComponentCount();
if (page - 2 >= 0)
{
layout.setPage(page - 2);
revalidate();
}
}
public int getPageNumber(JComponent child)
{
int count = getComponentCount();
for (int i = 0; i < count; i++)
{
if (child == getComponent(i))
return i;
}
return -1;
}
}
Listing 3
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class BinderPage extends JPanel
implements ActionListener
{
protected JBinder binder;
protected PageButton westButton, eastButton;
protected JLabel footer;
public BinderPage(JBinder binder, Component paper)
{
this.binder = binder;
setLayout(new BorderLayout());
JPanel header = new JPanel(new BorderLayout());
header.add(BorderLayout.EAST, eastButton =
new PageButton(PageButton.NORTH_EAST));
header.add(BorderLayout.WEST, westButton =
new PageButton(PageButton.NORTH_WEST));
westButton.addActionListener(this);
eastButton.addActionListener(this);
header.setBackground(Color.white);
header.setPreferredSize(new Dimension(16, 16));
header.setOpaque(true);
footer = new JLabel("", JLabel.CENTER);
footer.setBackground(Color.white);
footer.setOpaque(true);
JPanel west = new JPanel();
west.setPreferredSize(new Dimension(16, 16));
west.setBackground(Color.white);
west.setOpaque(true);
JPanel east = new JPanel();
east.setPreferredSize(new Dimension(16, 16));
east.setBackground(Color.white);
east.setOpaque(true);
add(BorderLayout.NORTH, header);
add(BorderLayout.SOUTH, footer);
add(BorderLayout.WEST, west);
add(BorderLayout.EAST, east);
add(BorderLayout.CENTER, paper);
setBorder(BorderFactory.createCompoundBorder(
new ShadowBorder(3),
BorderFactory.createLineBorder(Color.black)));
}
public void setVisible(boolean visible)
{
super.setVisible(visible);
if (visible)
{
BinderBook book = binder.getBook();
int page = book.getPageNumber(this);
int last = book.getComponentCount() - 1;
eastButton.setVisible(page % 2 == 0 && page < last);
westButton.setVisible(page % 2 != 0);
footer.setText("Page " + (page + 1));
}
}
public void actionPerformed(ActionEvent event)
{
if (westButton.isVisible())
binder.decrement();
if (eastButton.isVisible())
binder.increment();
}
}
Listing 4
import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;
public class JBinder extends JPanel
{
protected BinderBook book;
public JBinder(int ringCount)
{
setLayout(new OverlayLayout(this));
add(new BinderRings(ringCount));
add(book = new BinderBook());
}
public BinderPage addPage(JComponent child)
{
BinderPage page = new BinderPage(this, child);
book.add(page);
return page;
}
public BinderBook getBook()
{
return book;
}
public void increment()
{
book.increment();
repaint();
}
public void decrement()
{
book.decrement();
repaint();
}
}