ORIGINAL DRAFT

This month we’ll develop a component that’s useful when representing objects with a set of inbound and outbound links. Web pages have this property, with outbound links embedded in the page itself, and inbound links found in other pages that point to this page. Figure 1 shows an illustrative view of a center page with both inbound and outbound links.

Figure 1: The page views are drawn by a PageRenderer
and connection lines by a ConnectionRenderer, so you can customize JLink as much as you like.

Figure 1: The page views are drawn by a PageRenderer and connection lines by a ConnectionRenderer, so you can customize JLink as much as you like.

To maximize flexibility, we’ll use a PageModel to represent a page, with a collection of inbound and outbound pages. The page view is drawn by a PageRenderer and connecting lines are drawn by a ConnectionRenderer, so that you can easily change the way this looks by developing alternate renderers to suit your needs. We’ll provide a default model and two default renderers.

Figure 2: JLink classes include interfaces for a model
and two renderers, along with default implementations for each.

Figure 2: JLink classes include interfaces for a model and two renderers, along with default implementations for each.

Key to customization is the definition of simple interfaces for the elements we plan to model and view. The PageRenderer interface is responsible for drawing the pages you can see in the display. The getPageRendererComponent method takes a reference to the JLink that contains it, a Page object that describes the page to draw, along with the x and y position, defined as a center point.

public interface PageRenderer
{
  public JComponent
    getPageRendererComponent(
      JLink link, Page page, int x, int y);
}

The ConnectionRenderer is responsible for drawing connecting lines. The getConnectionRendererComponent method expects a reference to the containing JLink component, along with the two end points to draw a line between.

public interface ConnectionRenderer
{
  public JComponent
    getConnnectionRendererComponent(
      JLink link, int x1, int y1,
      int x2, int y2);
}

Finally, the PageModel is intended to represent a page and it’s links. A Page object is accessible through the getThisPage method and we can iterate through the list of inbound or outbound links by getting the count and asking for each entry by its index value. We also support notification by allowing change listeners to be added to the model.

public interface PageModel
{
  public Page getThisPage();
	
  public int getInboundLinkCount();
  public Page getInboundLink(int index);
	
  public int getOutboundLinkCount();
  public Page getOutboundLink(int index);
	
  public void addChangeListener(ChangeListener listener);
  public void removeChangeListener(ChangeListener listener);
}

The DefaultPageModel implements this interface and uses an instance variable to store "this page" and a List to store each of the inbound and outbound links. The default constructor creates a few entries to illustrate the example you see in Figure 1. This class supports the ability to set the current page and to add inbound or outbound pages. You can also add or remove change listeners. There’s not too much to learn from this class, so let’s move on to something more interesting.

We’ll skip over the Page object. It merely stores a pair of strings that give us the name and file name for a page. The constructor lets you provide both the name and file strings and we provide getName and getFile accessor methods. The DefaultPageRenderer makes use of the Page object, but you can extend it to store anything you like if you implement your own PageRenderer.

Listing 1 shows the code for the DefaultPageRenderer class. There are a few constants to define the page size, as well as the foreground and background colors for the text label. Both renderers in this implementation expect to have full run of the display area, so the drawing needs to be constrained by the renderer itself rather than the container.

The constructor sets the preferred size using the Dimension declared in the SIZE constant. The getPageRendererComponent method stores the values we’re interested in in instance variables and then returns a reference to the component by referencing ‘this’.

The real work takes place in the paintComponent method. We use a Graphics2D so that we can draw gradients. The first thing we do is set a few variables for the width, height, radius and shadow. These are all single character variables. The radius is a reference to the inset for the page fold at the upper right of the page drawing.

Having set the variables we create a Polygon object with the page outline, including the cut off top right corner. We also create a GradientPaint object that places a light color at the upper left and a dark color at the bottom right, to reflect the standard lighting convention used in user interfaces. With all this setup done, we can draw the components of the page.

The shadow gets drawn first with the outline of the page offset to the bottom right by the s variable setting. We use the translate method to move the drawing area. Next we draw the page gradient, moving the translation back to the original position first. Finally, we draw the outline of the page in black and draw two lines at the top right to show the folded-in page corner.

The rest of the paintComponent method draws the label. We use a TextLayout object, which necessitates passing in the FontRenderContext from the current Graphics object. We also get the bounds to draw the background and adjust the rectangle to add a little room around the label. Finally, we center the label and find the top left drawing position before drawing the background and then the text over the background rectangle.

The DefaultConnectionRenderer is much simpler. Listing 2 shows how we store the values passed through the getConnnectionRendererComponent method as instance variables and then draw the line in the paintComponent method. You can use a drawLine method to accomplish the same result but I chose to use a Graphics2D context and a Line2D object to make it easier to influence the thickness of the line. To make it thicker, all you have to do is add a setStroke method call, passing a BasicStroke instance with a thicker setting.

Listing 3 shows the code for JLink itself. JLink makes heavy use of renderers and therefore keeps an instance of a CellRendererPane handy. We also keep an instance of a PageModel, PageRenderer and ConnectionRenderer, all of which are the default implementations I mentioned earlier. The constructor sets the foreground color to black and background color to white.

The paintComponent method does most of the work. We clear the background with the background color and set the foreground color, which may be referenced by renderers through the JLink instance we pass through both interfaces. The left and right values are used to draw the inbound and outbound objects. We calculate those up front. We set the x1 and y1 values to the center of the display area. That’s where the center page will be drawn.

The next thing we do is draw the inbound connections and pages. We do this in two passes so that the pages draw over top of the connection lines. We get the count from the model and use the unit variable to calculate a set of equal divisions. In both cases, we fetch each model Page and pass it to the renderer interface before drawing the component with the CellRendererPane.

Notice that the drawing area specified in the CellRendererPage is the whole drawing area of the JLink component and not a limited area constrained to the renderers. This means you can draw anywhere in the display area with these renderers. It also means you have to be careful about drawing over everything else, so a renderer is expected to be primarily transparent and well behaved.

The next pair of loops handle drawing the connections and pages for the outbound links and we complete our processing by drawing the final, center page. The center page is drawn last, after all the connections meet in the middle. This makes it easier to ignore meeting edges with the ConnectionRenderer. In some cases, such as when you want to draw arrow points with the ConnectionRenderer, you’ll have to use the preferred size Dimension from the PageRenderer to put the arrow point in the right place.

The JLink class supports getting and setting the PageModel. When the model is set, we first make sure any previously registered listening is dicsonnected by calling the removeChangeListener method on the JList instance. After setting the instance variable to the model referenced in the method argument, we register as a listener and repaint the display whenever the model changes. This allows you to make dynamic changes to the model and have the view automatically reflect those changes.

JLink is a fairly simple component, useful in cases where you want to display both inbound and outbound connections for a given object. This is especially relevant with hypertext, such as a web page, but it might be used to represent synapses and axons in a neural model or even connections to an integrated circuit. Since you can develop renderers to draw whatever you need, the infrastructure is ready to customize.

Listing 1

import java.awt.*;
import java.awt.font.*;
import java.awt.geom.*;
import javax.swing.*;

public class DefaultPageRenderer
  extends JPanel
  implements PageRenderer
{
  public static Dimension SIZE =
    new Dimension(24, 32);
  public static Color FOREGROUND = Color.black;
  public static Color BACKGROUND = Color.yellow;

  protected Page page;
  protected int x, y;
  
  public DefaultPageRenderer()
  {
    setPreferredSize(SIZE);
  }
  
  public JComponent getPageRendererComponent(
    JLink link, Page page, int x, int y)
  {
    this.page = page;
    this.x = x;
    this.y = y;
    return this;
  }
  
  public void paintComponent(Graphics gc)
  {
    Graphics2D g = (Graphics2D)gc;
    
    int r = 6;
    int s = 2;
    int w = getPreferredSize().width - 1;
    int h = getPreferredSize().height - 1;
    g.translate(x - w / 2, y - h / 2);
    
    Polygon poly = new Polygon();
    poly.addPoint(0, 0);
    poly.addPoint(w - r - s, 0);
    poly.addPoint(w - s, r);
    poly.addPoint(w - s, h - s);
    poly.addPoint(0, h - s);
    
    GradientPaint gradient = new GradientPaint(
      0, 0, Color.white, w, h, Color.lightGray);
    
    // Shadow
    g.setPaint(Color.gray);
    g.translate(s, s);
    g.fillPolygon(poly);
    // Gradient
    g.setPaint(gradient);
    g.translate(-s, -s);
    g.fillPolygon(poly);
    // Outline
    g.setPaint(Color.black);
    g.drawPolygon(poly);
    g.drawLine(w - r - s, 0, w - r - s, r);
    g.drawLine(w - r - s, r, w - s, r);
    
    // Name
    FontRenderContext context = g.getFontRenderContext();
    TextLayout layout = new TextLayout(
      page.getName(), getFont(), context);
    Rectangle2D bounds = layout.getBounds();
    bounds = new Rectangle2D.Double(
      bounds.getX() - 2,
      bounds.getY() - 2,
      bounds.getWidth() + 4,
      bounds.getHeight() + 4);
    
    int ww = (int)bounds.getWidth();
    int hh = (int)bounds.getHeight();
    int xx = (w - ww) / 2;
    int yy = h + hh;
    g.translate(xx, yy);
    g.setColor(BACKGROUND);
    g.fill(bounds);
    g.setColor(FOREGROUND);
    g.draw(bounds);
    layout.draw(g, 0, 0);
  }
}

Listing 2

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

public class DefaultConnectionRenderer
  extends JPanel
  implements ConnectionRenderer
{
  protected JLink link;
  protected int x1, y1, x2, y2;

  public JComponent getConnnectionRendererComponent(
    JLink link, int x1, int y1, int x2, int y2)
  {
    this.link = link;
    this.x1 = x1;
    this.y1 = y1;
    this.x2 = x2;
    this.y2 = y2;
    return this;
  }
  
  public void paintComponent(Graphics gc)
  {
    Graphics2D g = (Graphics2D)gc;
    Shape shape = new Line2D.Double(x1, y1, x2, y2);
    g.setColor(link.getForeground());
    g.draw(shape);
  }
}

Listing 3

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

public class JLink extends JPanel
  implements ChangeListener
{
  protected CellRendererPane rendererPane =
    new CellRendererPane();
  protected ConnectionRenderer connectionRenderer =
    new DefaultConnectionRenderer();
  protected PageRenderer pageRenderer =
    new DefaultPageRenderer();
  protected PageModel model =
    new DefaultPageModel();

  public JLink()
  {
    setForeground(Color.black);
    setBackground(Color.white);
  }
  
  public PageModel getModel()
  {
    return model;
  }
  
  public void setModel(PageModel model)
  {
    if (this.model != null)
    {
      this.model.removeChangeListener(this);
    }
    this.model = model;
    model.addChangeListener(this);
  }
  
  public void stateChanged(ChangeEvent event)
  {
    repaint();
  }
  
  public void paintComponent(Graphics g)
  {
    int w = getSize().width;
    int h = getSize().height;
    g.setColor(getBackground());
    g.fillRect(0, 0, w, h);
    g.setColor(getForeground());
    int left = w / 6;
    int right = w - left;
    
    int unit = 0;
    int x1 = (w / 2);
    int y1 = (h / 2);
    
    // Inbound pages
    int in = model.getInboundLinkCount();
    unit = h / (in + 1);
    for (int i = 0; i < in; i++)
    {
      int x2 = left;
      int y2 = (i * unit) + unit;
      JComponent connection = connectionRenderer.
        getConnnectionRendererComponent(
          this, x1, y1, x2, y2);
      rendererPane.paintComponent(g,
        connection, this, 0, 0, w, h);
    }
    for (int i = 0; i < in; i++)
    {
      int x2 = left;
      int y2 = (i * unit) + unit;
      JComponent page = pageRenderer.
        getPageRendererComponent(this,
          model.getInboundLink(i), x2, y2);
      rendererPane.paintComponent(g,
        page, this, 0, 0, w, h);
    }
    
    // Outbound pages
    int out = model.getOutboundLinkCount();
    unit = h / (out + 1);
    for (int i = 0; i < out; i++)
    {
      int x2 = right;
      int y2 = (i * unit) + unit;
      JComponent connection = connectionRenderer.
        getConnnectionRendererComponent(
          this, x1, y1, x2, y2);
      rendererPane.paintComponent(g,
        connection, this, 0, 0, w, h);
    }
    for (int i = 0; i < out; i++)
    {
      int x2 = right;
      int y2 = (i * unit) + unit;
      JComponent page = pageRenderer.
        getPageRendererComponent(this, 
          model.getOutboundLink(i), x2, y2);
      rendererPane.paintComponent(g,
        page, this, 0, 0, w, h);
    }
    
    // Center page
    JComponent center = pageRenderer.
      getPageRendererComponent(
        this, model.getThisPage(), x1, y1);
    rendererPane.paintComponent(g,
      center, this, 0, 0, w, h);
  }
}