ORIGINAL DRAFT

It seems ironic that the JCalendar widget was one of the first that came to mind when this column was being conceived. At the time, is seemed likely that Sun would include a calendar component with Swing. There were hints in the beta releases, preview directories that contained minor evidence that this was one of the areas to be developed. Swing 1.0 was released, later Swing 1.1, and finally JDK 1.2 hit the streets and, still, there was no calendar component. So here you have it, JCalendar, with all the bells and whistles.

Overall Design

As always, our design places considerable importance on flexibility. In keeping with the spirit of Swing, we support custom renderers, use the list selection model, and provide a JComboBox-style popup implementation. Figure 1 shows a calendar view using the default renderer. The JCalendar widget lets you specify the number of horizontal and vertical months to display. The arrow buttons let you move to the previous or next month, as do the page up and page down keys. You can select multiple days using either the mouse or the keyboard, and move around with the arrow keys. The home and end keys also let you jump to the first or last day.

Figure 1: CalendarView Display.

Figure 1: CalendarView Display.

To build JCalendar, we’ll need several supporting classes. Each day in this grid display is handled by the CalendarMonth class. The month title is provided by a CalendarTitle class, contained in a CalendarHeader with optional ArrowButton instances. Together, these are managed by a CalendarView class. Because JCalendar needs to handle multiple views in a single framework, we use an additional CalendarGroup class which is aware of each of the other views and consolidates many of the common operations. Finally, we’ll provide a JCalendarField class that works like a JComboBox.

Rendering Days

The CalendarMonth class is responsible for much of the logic required by JCalendar. It handles keyboard events, renders each of the days, interfaces with the list selection model, and more. In this section, we’ll walk through the listings that related directly to the CalendarMonth class. When you get the source code, run the CalendarMonthTest harness and you’ll see something similar to Figure 2, displaying each of the days for the current month.

Figure 2: CalendarMonth Display.

Figure 2: CalendarMonth Display.

Listing 1 shows the code for our CalendarRenderer interface. We declare two methods to support both the rendering and the background color selection for the unused cells. The getBackdrop method returns a Color object. The method that does the actual rendering is called getCalendarRendererComponent. It requires we provide a reference to the calling component, the value of the day being rendered, which is a string in this case, and two flags to indicate whether the rendering cell is selected and/or has the focus.

Listing 2 is the DefaultCalendarRenderer implementation. Like most of the Swing default renderers, we extend the JLabel component. Our view will use a raised and lowered border to indicate selection, with a blue background to indicate focus. Our implementation is used to render both the days and headers in the CalendarView. Nothing forces you to use the the same renderer for these, but they implement the same CalendarRender interface. We make the distinction between headers and cell rendering in the constructor and save the value for later use.

The getBackdrop method simply returns lightGray for the background. Our getCalendarRendererComponent method does the real work. It sets the label text, based on the value in the second argument, then sets the border, foreground and background colors depending on whether the cell is selected and/or has the focus. Listing 3 shows an alternate CalendarRenderer called a SimpleCalendarRenderer which provides a simplified, flattened view. The background is normally white and no borders are used. Selected cells are highlighted with a blue background. The focus and header underline are drawn by overriding the drawComponent method.

Listing 4 has the logic for a the CalendarMonth itself. You’ll notice that this class also requires a CalendarGroup instance. CalendarMonth is responsible for rendering, provides selection and notification, along with keyboard and mouse event handling. It also exposes a few accessors and navigational methods. Dates are stored using Java Calendar objects, providing us with information about each year, month and day. Some of the logic in CalendarMonth could have been handled by new Calendar methods from JDK 1.2 but they were left in just in case you need to get this running under Java 1.1.

The accessor methods are self-explanatory. The setDay method uses the setSelectionInterval from the ListSelectionModel interface to keep the model in sync. We use the Swing DefaultListSelectionModel in all our examples. Some of the support methods include nextMonth, prevMonth, setFirstDay and setLastDay. We also use a setActive method to set the active flag, which lets us determine if the current month has the virtual focus.

The rendering is handled in the paintComponent method and delegated to the drawCell method, which actually calls our registered renderer and uses the Swing CellRendererPane to paint the component. The CellRendererPane is critical to this equasion. It gets added to the CalendarMonth panel in the constructor as the Center component in a BorderLayout, so it expands to fill the viewing space. The CellRendererPane has an extended paintComponent method that takes the graphics context, the render component and the requested x, y, width and height.

The CalendarMonth paintComponent method draws the background based on the renderer getBackdrop color and then walks through a 6 by 7 grid checking each cell for validity. This is handled by the isValidDay method. In addition, we provide an isSelected method to determine if a cell is currently selected in the ListSelectionModel. The isSelected method always returns false if the CalendarMonth is not considered active. This is important, since we may be sharing the same ListSelectionModel between multiple CalendarMonth objects. Because the renderer is not actually a child component, we also need to provide our own calculations for the getPreferredSize and getMinimumSize methods.

To handle mouse selection, we intercept the mousePressed and mouseDragged events, and so must implement all the methods in both the MouseListener and MouseMotionListener interfaces. We register these in our constructor. The mousePressed event gets the focus and activates the clicked-on month before calculating the selected day from the mouse position. If the shift or control modifier keys are pressed, we extend the selection, otherwise we select the pointed-to day. In either case, we fire off an ActionEvent to any registered listener. The implementations for addActionListener, removeActionListener and fireActionEvent are listed at the end of the code. The mouseDragged event merely calculates the pointed-to day and extends the selection.

Monthly Functions

Navigating between months can be driven either by the page up and page down keys, by moving beyond the first or last day of the month with the arrow keys, or with the arrow buttons provided in the CalendarView. This class is largely a wrapper for several elements show in Figure 3. The ArrowButton objects are optionally placed in the upper left and right corners, with the CalendarTitle between them. Lets take a look at the individual elements and wrap this section up with the CalendarView class itself.

Figure 3: CalendarView Layout.

Figure 3: CalendarView Layout.

The ArrowButton in Listing 5 is a simple extension of the BasicArrowButton. Our implementation extends BasicArrowButton and overrides a copy of the paint method to make the border thinner, to stay consistent with the rest of our elements, which use the ThinBorder class from Listing 6.

Listing 7 shows the CalendarTitle class, which extends JLabel to show the current month and year, a convenient way to set the border and alignment values. The CalendarHeader class in Listing 8 implements a few of the same methods used by CalendarMonth to handle rendering. The CalendarHeader class shows the first letter of each day of the week above the CalendarMonth display, and uses the CalendarRenderer interface to do the drawing. As such, it needs to use the CellRenderPane and implements the getPreferredSize and getMinimumSize methods explicitly. We also implement the isFocusTraversable method to always return false, since this object is not selectable.

We tie all this together in the CalendarView, presented in Listing 9. Most of the work in our constructor sets up the layout and child components. The west and east arrows are created based on a pair of boolean arguments passed in by JCalendar. After creating the CalendarMonth, we register as an action listener to respond to basic selection events. When the month changes, we need to update the CalendarTitle text, formatted by the formatDate method.

JCalendar Widget

The high-level view and widget control is pulled together in the JCalendar class. Figure 4 shows the JCalendar in action, with a 2 by 3 grid specified. As you can see, the arrow buttons are only present in the top left and right CalendarView. The behavior is complicated by our desire to allow the user to flow from month to month with the arrow keys and to automatically update each month as a sequence whenever a month is changed. Furthermore, we want to avoid flicker and provide seamless repaint events, so we need to implement a CalendarGroup to manage these effectively.

Figure 4: JCalendar with a 2x3 Layout.

Figure 4: JCalendar with a 2x3 Layout.

Listing 10 shows the CalendarGroup class. We keep track of the parent object, so we can set paint events at the highest level after setting up the children, this avoids unnecessary paint events and keeps the elements from repainting sequentially. The group elements are held in a Vector array and we use an active variable to save the current month index. While there are several methods in this class, they are generally unremarkable, setting and getting the current month, adding new members and testing or setting positions. The nextMonth and prevMonth methods walk the list of group members, calling their own respective nextMonth and prevMonth methods.

The main JCalendar class is presented in Listing 11. We provide a number of constructor variants with default selection model and renderers. Each of these calls the main constructor, which sets up a GridLayout and populates the grid with CalendarView objects. This is where we decide which arrows are to be activated. Most of the following methods set and get properties for the date, selection model, header and cell renderers. We listen for CalendarMonth events and fire our own action events, implementing the addActionListener, removeActionListerner and fireActionEvent methods to support this.

JCalendarField

One of the best ways to put our calendar to work is in popup menus and ComboBox-style fields. To demonstrate this, we use the SimpleCalendarRenderer and produce a JCalendarField widget to add to your collection. Figure 5 shows how it looks when the user clicks on the arrow button. There are a number of minor tricks at work in this class. The class extends JPopupMenu and uses its ability to handle arbitrary components.

Figure 5: JCalendarField Popup.

Figure 5: JCalendarField Popup.

There doesn’t seem to be a way to extend JComboBox to use a new list popup. Since we need to use a component that needs to receive mouse events, we are forced to manage our own drop down positioning as well. As with many of the previously mentioned problems, the solution actually involves only a few lines of code. The hard part is typically finding the solution. Keep this in mind, especially for this widget. If you implement one from scratch, its likely you’ll be spending much of your time in this part of the code, unless you remember how its done.

Listing 12 shows the JCalendarField class. We provide two constructors, one of which uses the current date, the other expects you to provide one. Our main constructor, sets up a BorderLayout with a JTextField in the center and a normal BasicArrowButton on the right, pointing down. To push the button into the field, we get the field border, and set it as the border for the whole component, setting the JTextField border to null. This keeps things consistent with the currently selected look-and-feel. We also create a JPopupMenu and add a JCalendar instance as its only child.

If the popup is not visible when the button is pressed, we position it under the button and set the date to reflect the field value. Notice that we use getPreferredSize rather than getSize because the actual size in not actually established until the popup menu is first displayed, causing problems with positioning if we don’t use this approach. We also register to receive action events from the JCalendar object, closing the popup and retrieving the selected date when the mouse is clicked.

Summary

Both the JCalendar and JCalendarField, with their respective renderers and other features, provide a great deal of power. The calendar metaphor is ideal for navigating temporal regions. This is useful in browsers, for setting date ranges or simply for determining what day of the week a particular date falls on. For the user, this is the intuitive choice, consistent with their material experience and easy to understand. You can make your programs more accessible and customize this calendar widget to your heart’s content. Make it work for you.

Listing 1

public interface CalendarRenderer
{
  public Component getCalendarRendererComponent(
    JComponent parent, Object value,
    boolean isSelected, boolean hasFocus);
  public Color getBackdrop();
}

Listing 2

public class DefaultCalendarRenderer extends JLabel
  implements CalendarRenderer
{
  private boolean isHeader = false;
  
  private static final Border raised =
    new ThinBorder(ThinBorder.RAISED);
  private static final Border lowered =
    new ThinBorder(ThinBorder.LOWERED);

  public DefaultCalendarRenderer(boolean isHeader)
  {
    setOpaque(true);
    setBorder(raised);
    setVerticalAlignment(JLabel.TOP);
    setHorizontalAlignment(
      isHeader ? JLabel.CENTER : JLabel.LEFT);
    setPreferredSize(new Dimension(19, 18));
    setMinimumSize(new Dimension(19, 18));
    this.isHeader = isHeader;
  }
  
  public Color getBackdrop()
  {
    return Color.lightGray;
  }
  
  public Component getCalendarRendererComponent(
    JComponent parent, Object value,
    boolean isSelected, boolean hasFocus)
  {
    setText(value.toString());
    if (isSelected)
    {
      setBorder(lowered);
      setBackground(
        hasFocus ? Color.blue : Color.lightGray);
      setForeground(
        hasFocus ? Color.white : Color.black);
    }
    else
    {
      setBorder(raised);
      setBackground(isHeader ?
        Color.gray : Color.lightGray);
      setForeground(Color.black);
    }
    return this;
  }
}

Listing 3

public class SimpleCalendarRenderer extends JLabel
  implements CalendarRenderer
{
  private boolean hasFocus = false;
  private boolean isSelected = false;
  private boolean isHeader = false;

  public SimpleCalendarRenderer(boolean isHeader)
  {
    setOpaque(true);
    setBorder(null);
    setVerticalAlignment(JLabel.TOP);
    setHorizontalAlignment(JLabel.CENTER);
    setPreferredSize(new Dimension(19, 18));
    setMinimumSize(new Dimension(19, 18));
    this.isHeader = isHeader;
  }
  
  public Color getBackdrop()
  {
    return Color.white;
  }
  
  public Component getCalendarRendererComponent(
    JComponent parent, Object value,
    boolean isSelected, boolean hasFocus)
  {
    this.hasFocus = hasFocus;
    this.isSelected = isSelected;
    setText(value.toString());
    if (isSelected)
    {
      setBackground(Color.blue);
      setForeground(Color.white);
    }
    else
    {
      setBackground(Color.white);
      setForeground(Color.black);
    }
    return this;
  }
  
  public void paintComponent(Graphics g)
  {
    super.paintComponent(g);
    int w = getSize().width;
    int h = getSize().height;
    if (!isHeader && hasFocus & isSelected)
    {
      g.setColor(Color.white);
      BasicGraphicsUtils.drawDashedRect(
        g, 0, 0, w, h);
    }
    if (isHeader) g.drawLine(0, h - 1, w, h - 1);
  }
}

Listing 4

public class CalendarMonth extends JPanel
  implements MouseListener, MouseMotionListener,
    KeyListener, FocusListener
{
  public static final String ACTION_ENTER = "Enter";
  public static final String ACTION_CLICK = "Click";
  public static final String ACTION_NEXT = "Next";
  public static final String ACTION_PREV = "Prev";
  
  protected Vector listeners = new Vector();
  protected boolean hasFocus = false;
  protected CellRendererPane renderPane =
    new CellRendererPane();
  
  protected CalendarRenderer renderer;
  protected ListSelectionModel selector;
  protected CalendarGroup group;
    
  private static final int daysInMonth[] =
    {31,28,31,30,31,30,31,31,30,31,30,31};
    
  protected Calendar date = Calendar.getInstance();
  protected double xunit = 0, yunit = 0;
  protected int first, days;
  protected boolean active = true;

  public CalendarMonth(Calendar date,
    ListSelectionModel selector,
    CalendarRenderer renderer,
    CalendarGroup group)
  {
    this.group = group;
    this.selector = selector;
    this.renderer = renderer;
    group.add(this);
    setLayout(new BorderLayout());
    add(BorderLayout.CENTER, renderPane);
    addMouseMotionListener(this);
    addMouseListener(this);
    addFocusListener(this);
    addKeyListener(this);
    setDate(date);
  }

  public void setDay(int day)
  {
    selector.setSelectionInterval(day, day);
    date.set(Calendar.DAY_OF_MONTH, day);
  }

  public void setMonth(int month)
  {
    date.set(Calendar.MONTH, month);
    setDate(date);
  }

  public void setYear(int year)
  {
    date.set(Calendar.YEAR, year);
    setDate(date);
  }

  public void setDate(Calendar date)
  {
    this.date = date;
    Calendar temp = (Calendar)date.clone();
    temp.set(Calendar.DAY_OF_MONTH, 1);
    first = temp.get(Calendar.DAY_OF_WEEK) - 1;
    int current = date.get(Calendar.DAY_OF_MONTH);
    selector.setSelectionInterval(current, current);
    days = daysInMonth[date.get(Calendar.MONTH)];
    if (isLeapYear(date.get(Calendar.YEAR)) &&
      date.get(Calendar.MONTH) == 1) days = 28;
  }

  public Calendar getDate()
  {
    return date;
  }
  
  public void nextMonth()
  {
    date.add(Calendar.MONTH, 1);
    setDate(date);
    fireActionEvent(ACTION_NEXT);
  }

  public void prevMonth()
  {
    date.add(Calendar.MONTH, -1);
    setDate(date);  
    fireActionEvent(ACTION_PREV);
  }

  public void setFirstDay()
  {
    setDay(1);
  }
  
  public void setLastDay()
  {
    setDay(days);
  }
  
  public void setActive(boolean active)
  {
    this.active = active;
  }

  public void paintComponent(Graphics g)
  {
    xunit = getSize().width / 7;
    yunit = getSize().height / 6;
    g.setColor(renderer.getBackdrop());
    g.fillRect(0, 0, getSize().width, getSize().height);
    
    int day = 1;
    for (int y = 0; y < 6; y++)
    {
      for (int x = 0; x < 7; x ++)
      {
        if (isValidDay(x, y))
        {
          drawCell(g,
            (int)(x * xunit), (int)(y * yunit),
            (int)xunit, (int)yunit,
            "" + day, isSelected(day));
          day++;
        }
      }
    }
  }

  protected boolean isSelected(int day)
  {
    if (!active) return false;
    return selector.isSelectedIndex(day);
  }

  protected boolean isValidDay(int x, int y)
  {
    int day = (x + y * 7) - first;
    return (y == 0 && x >= first) || (y > 0 && day < days);
  }

  protected void drawCell(Graphics g, int x, int y,
    int w, int h, String text, boolean isSelected)
  {
    Component render =
      renderer.getCalendarRendererComponent(
        this, text, isSelected, hasFocus);
    renderPane.paintComponent(g, render, this, x, y, w, h);
  }

  protected boolean isLeapYear(int year)
  {
    return ((year % 4 == 0) &&
      ((year % 100 != 0) || (year % 400 == 0)));
  }

  public Dimension getPreferredSize()
  {
    Dimension dimension = 
      ((Component)renderer).getPreferredSize();
    int width = dimension.width * 7;
    int height = dimension.height * 6;
    return new Dimension(width, height);
  }

  public Dimension getMinimumSize()
  {
    Dimension dimension = 
      ((Component)renderer).getMinimumSize();
    int width = dimension.width * 7;
    int height = dimension.height * 6;
    return new Dimension(width, height);
  }
  
  public void mouseClicked(MouseEvent event) {}
  public void mouseReleased(MouseEvent event) {}
  public void mouseEntered(MouseEvent event) {}
  public void mouseExited(MouseEvent event) {}
  public void mousePressed(MouseEvent event)
  {
    if (!hasFocus())
    {
      requestFocus();
      group.setActiveMonth(this);
    }
    int x = (int)(event.getX() / xunit);
    int y = (int)(event.getY() / yunit);
    if (!isValidDay(x, y)) return;
    int day = x + y * 7 - first + 1;
    if (event.isShiftDown() || event.isControlDown())
    {
      selector.setLeadSelectionIndex(day);
      repaint();
    }
    else
    {
      setDay(day);
      repaint();
    }
    fireActionEvent(ACTION_CLICK);
  }
  
  public void mouseMoved(MouseEvent event) {}
  public void mouseDragged(MouseEvent event)
  {
    int x = (int)(event.getX() / xunit);
    int y = (int)(event.getY() / yunit);
    if (!isValidDay(x, y)) return;
    int day = x + y * 7 - first + 1;
    selector.setLeadSelectionIndex(day);
    repaint();
  }
  
  public void keyTyped(KeyEvent event) {}
  public void keyReleased(KeyEvent event) {}
  public void keyPressed(KeyEvent event)
  {
    if (!active) return;
    int key = event.getKeyCode();
    if (key == KeyEvent.VK_ENTER)
    {
      fireActionEvent(ACTION_ENTER);
    }
    if (key == KeyEvent.VK_HOME)
    {
      setFirstDay();
      repaint();
    }
    if (key == KeyEvent.VK_END)
    {
      setLastDay();
      repaint();
    }
    if (key == KeyEvent.VK_PAGE_DOWN)
    {
      group.prevMonth(true);
    }
    if (key == KeyEvent.VK_PAGE_UP)
    {
      group.nextMonth(true);
    }
    int anchor = selector.getAnchorSelectionIndex();
    int lead = selector.getLeadSelectionIndex();
    if (key == KeyEvent.VK_RIGHT)
    {
      if (event.isShiftDown() || event.isControlDown())
      {
        if (lead < days)
        {
          selector.setLeadSelectionIndex(lead + 1);
          repaint();
        }
      }
      else if (anchor < days)
      {
        setDay(anchor + 1);
        repaint();
      }
      else if (anchor == days)
      {
        if (group.isLastCalendarMonth(this))
        {
          group.nextMonth(true);
          setLastDay();
          repaint();
        }
        else
        {
          CalendarMonth next =
            group.nextCalendarMonth();
          next.setFirstDay();
          next.repaint();
          repaint();
        }
      }
    }
    if (key == KeyEvent.VK_LEFT)
    {
      if (event.isShiftDown() || event.isControlDown())
      {
        if (lead > 1)
        {
          selector.setLeadSelectionIndex(lead - 1);
          repaint();
        }
      }
      else if (anchor > 1)
      {
        setDay(anchor - 1);
        repaint();
      }
      else if (anchor == 1)
      {
        if (group.isFirstCalendarMonth(this))
        {
          group.prevMonth(true);
          setFirstDay();
          repaint();
        }
        else
        {
          CalendarMonth prev =
            group.prevCalendarMonth();
          prev.setLastDay();
          prev.repaint();
          repaint();
        }
      }
    }
    if (key == KeyEvent.VK_UP)
    {
      if (event.isShiftDown() || event.isControlDown())
      {
        if (lead > 7)
        {
          selector.setLeadSelectionIndex(lead - 7);
          repaint();
        }
      }
      else if (anchor > 7)
      {
        setDay(anchor - 7);
        repaint();
      }
    }
    if (key == KeyEvent.VK_DOWN)
    {
      if (event.isShiftDown() || event.isControlDown())
      {
        if (lead <= (days - 7))
        {
          selector.setLeadSelectionIndex(lead + 7);
          repaint();
        }
      }
      else if (anchor <= (days - 7))
      {
        setDay(anchor + 7);
        repaint();
      }
    }
  }

  public void focusGained(FocusEvent event)
  {
    hasFocus = true;
    repaint();
  }
  
  public void focusLost(FocusEvent event)
  {
    hasFocus = false;
    repaint();
  }

  public boolean isFocusTraversable()
  {
    return active;
  }
  
  public void addActionListener(ActionListener listener)
  {
    listeners.addElement(listener);
  }
    
  public void removeActionListener(ActionListener listener)
  {
    listeners.removeElement(listener);
  }

  public void fireActionEvent(String command)
  {
    ActionListener listener;
    Vector list = (Vector)listeners.clone();
    ActionEvent event = new ActionEvent(this,
    ActionEvent.ACTION_PERFORMED, command);
    for (int i = 0; i < list.size(); i++)
    {
      listener = ((ActionListener)list.elementAt(i));
      listener.actionPerformed(event);
    }
  }
}

Listing 5

public class ArrowButton extends BasicArrowButton
{
  public ArrowButton(int direction)
  {
    super(direction);
  }

  public void paint(Graphics g)
  {
    Color origColor;
    boolean isPressed, isEnabled;
    int w, h, size;

    w = getSize().width;
    h = getSize().height;
    origColor = g.getColor();
    isPressed = getModel().isPressed();
    isEnabled = isEnabled();

    g.setColor(getBackground());
    g.fillRect(1, 1, w-2, h-2);

    /// Draw the proper Border
    if (isPressed)
    {
      g.setColor(UIManager.getColor("controlShadow"));
      g.drawRect(0, 0, w-1, h-1);
    }
    else
    {
      g.setColor(UIManager.getColor("controlLtHighlight"));
      g.drawLine(0, 0, 0, h-1);
      g.drawLine(1, 0, w-2, 0);

      g.setColor(UIManager.getColor("controlShadow"));
      g.drawLine(0, h-1, w-1, h-1);
      g.drawLine(w-1, h-1, w-1, 0);
    }

    // If there's no room to draw arrow, bail
    if(h < 5 || w < 5)
    {
      g.setColor(origColor);
      return;
    }

    if (isPressed)
    {
      g.translate(1, 1);
    }

    // Draw the arrow
    size = Math.min((h - 4) / 3, (w - 4) / 3);
    size = Math.max(size, 2);
    paintTriangle(g, (w - size) / 2, (h - size) / 2,
      size, direction, isEnabled);

    // Reset the Graphics back to it's original settings
    if (isPressed)
    {
      g.translate(-1, -1);
    }
    g.setColor(origColor);
  }
}

Listing 6

public class ThinBorder implements Border
{
  public static final int RAISED  = 0;
  public static final int LOWERED = 1;
  public static final int thickness = 1;
  
  protected int type = RAISED;
  protected Color highlight;
  protected Color shadow;

  public ThinBorder()
  {
    this(LOWERED, null, null);
  }

  public ThinBorder(int type)
  {
    this(type, null, null);
  }

  public ThinBorder(int type,
    Color highlight, Color shadow)
  {
    this.type = type;
    this.highlight = highlight;
    this.shadow = shadow;
    }

  public boolean isBorderOpaque()
  {
    return true;
  }

  public Insets getBorderInsets(Component component)
  {
    return new Insets(thickness, thickness, thickness, thickness);
  }
  
  public Color getHightlightColor(Component c)
  {
    if (highlight == null)
      highlight = c.getBackground().brighter();
    return highlight;
  }

  public Color getShadowColor(Component c)
  {
    if (shadow == null)
      shadow = c.getBackground().darker();
    return shadow;
  }

  public void paintBorder(Component c, Graphics g,
    int x, int y, int w, int h)
  {
    Color hi = (type == RAISED ?
      getHightlightColor(c) : getShadowColor(c));
    Color lo = (type == RAISED ?
      getShadowColor(c) : getHightlightColor(c));
    
    for (int i = thickness - 1; i >= 0; i--)
    {
      g.setColor(hi);
      g.drawLine(x + i, y + i, x + w - i - 1, y + i);
      g.drawLine(x + i, y + i, x + i, x + h - i - 1);

      g.setColor(lo);
      g.drawLine(x + w - i - 1, y + i, x + w - i - 1, y + h - i - 1);
      g.drawLine(x + i, y + h - i - 1, x + w - i - 1, y + h - i - 1);
    }
  }

}

Listing 7

public class CalendarTitle extends JLabel
{
  public CalendarTitle(String text)
  {
    super(text);
    setHorizontalAlignment(JLabel.CENTER);
    setBorder(new ThinBorder(ThinBorder.RAISED));
  }
}

Listing 8

public class CalendarHeader extends JPanel
{
  protected CalendarRenderer renderer;
  protected CellRendererPane renderPane =
    new CellRendererPane();
    
  private static final String header[] =
    {"S", "M", "T", "W", "T", "F", "S"};
    
  protected double xunit = 0, yunit = 0;
  protected int first, days, current = 1;

  public CalendarHeader(CalendarRenderer renderer)
  {
    this.renderer = renderer;
    setLayout(new BorderLayout());
    add(BorderLayout.CENTER, renderPane);
  }

  public void paintComponent(Graphics g)
  {
    xunit = getSize().width / 7;
    yunit = getSize().height;
    for (int x = 0; x < 7; x ++)
    {
      drawCell(g, (int)(x * xunit), 0,
        (int)xunit, (int)yunit, header[x], false);
    }
  }

  private void drawCell(Graphics g, int x, int y,
    int w, int h, String text, boolean selected)
  {
    Component render = renderer.
      getCalendarRendererComponent(
        this, text, selected, false);
    renderPane.paintComponent(g, render, this, x, y, w, h);
  }

  public boolean isFocusTraversable()
  {
    return false;
  }

  public Dimension getPreferredSize()
  {
    Dimension dimension = 
      ((Component)renderer).getPreferredSize();
    int width = dimension.width * 7;
    int height = dimension.height;
    return new Dimension(width, height);
  }

  public Dimension getMinimumSize()
  {
    Dimension dimension = 
      ((Component)renderer).getMinimumSize();
    int width = dimension.width * 7;
    int height = dimension.height;
    return new Dimension(width, height);
  }
}

Listing 9

public class CalendarView extends JPanel
  implements ActionListener
{
  protected BasicArrowButton westArrow, eastArrow;
  protected CalendarTitle title;
  protected CalendarMonth month;
  protected CalendarGroup group;
  
  public CalendarView(Calendar date,
    boolean west, boolean east,
    ListSelectionModel selector,
    CalendarRenderer headRenderer,
    CalendarRenderer cellRenderer,
    CalendarGroup group)
  {
    this.group = group;
    setLayout(new BorderLayout());
    setBorder(new ThinBorder(ThinBorder.LOWERED));

    JPanel nav = new JPanel();
    nav.setLayout(new BorderLayout());
    nav.add(BorderLayout.CENTER, 
      title = new CalendarTitle(formatLabel(date)));
    if (west)
    {
      nav.add(BorderLayout.WEST, westArrow =
        new ArrowButton(BasicArrowButton. WEST));
      westArrow.addActionListener(this);
    }
    if (east)
    {
      nav.add(BorderLayout.EAST, eastArrow =
        new ArrowButton(BasicArrowButton. EAST));
      eastArrow.addActionListener(this);
    }

    JPanel header = new JPanel();
    header.setLayout(new BorderLayout());
    header.add(BorderLayout.NORTH, nav);
    header.add(BorderLayout.SOUTH,
      new CalendarHeader(headRenderer));
    
    add(BorderLayout.NORTH, header);
    add(BorderLayout.CENTER, month = new CalendarMonth(
      date, selector, cellRenderer, group));
    month.addActionListener(this);
  }

  private String formatLabel(Calendar calendar)
  {
    String year = " " + calendar.get(Calendar.YEAR);
    switch (calendar.get(Calendar.MONTH))
    {
      case 0: return "January" + year;
      case 1: return "February" + year;
      case 2: return "March" + year;
      case 3: return "April" + year;
      case 4: return "May" + year;
      case 5: return "June" + year;
      case 6: return "July" + year;
      case 7: return "August" + year;
      case 8: return "September" + year;
      case 9: return "October" + year;
      case 10: return "November" + year;
      default: return "December" + year;
    }
  }

  public BasicArrowButton getWestArrow()
  {
    return westArrow;
  }

  public BasicArrowButton getEastArrow()
  {
    return eastArrow;
  }

  public void nextMonth()
  {
    group.nextMonth(false);
    Calendar date = month.getDate();
    title.setText(formatLabel(date));
    repaint();
  }
  
  public void prevMonth()
  {
    group.prevMonth(false);
    Calendar date = month.getDate();
    title.setText(formatLabel(date));
    repaint();
  }

  public void actionPerformed(ActionEvent event)
  {
    Object source = event.getSource();
    if (source == month)
    {
      Calendar date = month.getDate();
      title.setText(formatLabel(date));
      title.repaint();
    }
    if (source == westArrow)
    {
      group.prevMonth(true);
    }
    if (source == eastArrow)
    {
      group.nextMonth(true);
    }
  }
}

Listing 10

public class CalendarGroup
{
  protected Component parent;
  protected Vector group;
  protected int active;
  
  public CalendarGroup()
  {
    this(null);
  }

  public CalendarGroup(Component parent)
  {
    this.parent = parent;
    group = new Vector();
    active = 0;
  }

  public void setParent(Component parent)
  {
    this.parent = parent;
  }
  
  public void setActiveMonth(int index)
  {
    if (index > group.size())
      throw new IndexOutOfBoundsException(
        "Out of range CalendarGroup index");
    active = index;
    for (int i = 0; i < group.size(); i++)
    {
      getCalendarMonth(i).setActive(i == active);
      if (i == active)
        getCalendarMonth(i).requestFocus();
    }
  }
  
  public void setActiveMonth(CalendarMonth month)
  {
    for (int i = 0; i < group.size(); i++)
    {
      if (getCalendarMonth(i) == month)
      {
        setActiveMonth(i);
        parent.repaint();
        break;
      }
    }
  }
  
  public void add(CalendarMonth month)
  {
    group.addElement(month);
    active = group.size() - 1;
  }
  
  public CalendarMonth getActiveMonth()
  {
    return getCalendarMonth(active);
  }
  
  public CalendarMonth getCalendarMonth(int index)
  {
    return (CalendarMonth)group.elementAt(index);
  }
  
  public CalendarMonth nextCalendarMonth()
  {
    active++;
    if (active >= group.size()) active = 0;
    setActiveMonth(active);
    return getCalendarMonth(active);
  }

  public CalendarMonth prevCalendarMonth()
  {
    active--;
    if (active < 0) active = group.size() - 1;
    setActiveMonth(active);
    return getCalendarMonth(active);
  }
  
  public boolean isFirstCalendarMonth(CalendarMonth month)
  {
    return month == getCalendarMonth(0);
  }
  
  public boolean isLastCalendarMonth(CalendarMonth month)
  {
    return month == getCalendarMonth(group.size() - 1);
  }
  
  public void nextMonth(boolean repaint)
  {
    for (int i = 0; i < group.size(); i++)
    {
      getCalendarMonth(i).nextMonth();
      getCalendarMonth(i).invalidate();
      if (repaint && parent != null)
        parent.repaint();
    }
  }

  public void prevMonth(boolean repaint)
  {
    for (int i = 0; i < group.size(); i++)
    {
      getCalendarMonth(i).prevMonth();
      getCalendarMonth(i).invalidate();
      if (repaint && parent != null)
        parent.repaint();
    }
  }
}

Listing 11

public class JCalendarField extends JPanel
  implements ActionListener, KeyListener
{
  protected DateFormat formatter =
    DateFormat.getDateInstance(DateFormat.LONG);
  
  protected BasicArrowButton calendarButton;
  protected JCalendar calendar;
  protected JPopupMenu popup;
  protected JTextField field;
  protected Calendar date;
  
  public JCalendarField()
  {
    this(Calendar.getInstance());
  }

  public JCalendarField(Calendar date)
  {
    setLayout(new BorderLayout());
    add(BorderLayout.CENTER, field = new JTextField());
    field.setEditable(false);
    setBorder(field.getBorder());
    field.setBorder(null);
    
    calendarButton =
      new BasicArrowButton(BasicArrowButton.SOUTH);
    calendarButton.addActionListener(this);
    add(BorderLayout.EAST, calendarButton);

    calendar = new JCalendar(date, 1, 1,
      new DefaultListSelectionModel(),
      new SimpleCalendarRenderer(true),
      new SimpleCalendarRenderer(false));
    calendar.addActionListener(this);
    popup = new JPopupMenu();
    popup.add(calendar);
    setField(date);
    
    CalendarMonth month =
      calendar.group.getActiveMonth();
    field.addKeyListener(month);
    field.addKeyListener(this);
  }

  public void actionPerformed(ActionEvent event)
  {
    if (event.getSource() == calendarButton)
    {
      field.requestFocus();
      getRootPane().setDefaultButton(null);
      if (!popup.isVisible())
      {
        Dimension dim = calendarButton.getSize();
        calendar.setDate(date);
        popup.show(calendarButton, dim.width -
          popup.getPreferredSize().width,
          dim.height);
      }
      else
      {
        popup.setVisible(false);
        field.requestFocus();
      }
    }
    if (event.getSource() == calendar)
    {
      setField(calendar.getDate());
      popup.setVisible(false);
      field.requestFocus();
    }
  }

  protected void setField(Calendar date)
  {
    this.date = date;
    field.setText(formatter.format(date.getTime()));
  }

  public void setDate(Calendar date)
  {
    calendar.setDate(date);
  }
  
  public Calendar getDate()
  {
    return calendar.getDate();
  }

  public void setCalendar(JCalendar calendar)
  {
    this.calendar = calendar;
  }
  
  public JCalendar getCalendar()
  {
    return calendar;
  }
  
  public void setDateFormat(DateFormat formatter)
  {
    this.formatter = formatter;
  }
  
  public void keyTyped(KeyEvent event) {}
  public void keyReleased(KeyEvent event) {}
  public void keyPressed(KeyEvent event)
  {
    if (event.getKeyCode() == KeyEvent.VK_ESCAPE)
      popup.setVisible(false);
  }
}

Listing 12

public class JCalendarField extends JPanel
  implements ActionListener
{
  protected DateFormat formatter =
    DateFormat.getDateInstance(DateFormat.LONG);
  
  protected BasicArrowButton calendarButton;
  protected JCalendar calendar;
  protected JPopupMenu popup;
  protected JTextField field;
  protected Calendar date;
  
  public JCalendarField()
  {
    this(Calendar.getInstance());
  }

  public JCalendarField(Calendar date)
  {
    setLayout(new BorderLayout());
    add(BorderLayout.CENTER, field = new JTextField());
    field.setEditable(false);
    setBorder(field.getBorder());
    field.setBorder(null);
    
    calendarButton =
      new BasicArrowButton(BasicArrowButton.SOUTH);
    calendarButton.addActionListener(this);
    add(BorderLayout.EAST, calendarButton);

    calendar = new JCalendar(date, 1, 1,
      new DefaultListSelectionModel(),
      new SimpleCalendarRenderer(true),
      new SimpleCalendarRenderer(false),
      new CalendarGroup());
    calendar.addActionListener(this);
    popup = new JPopupMenu();
    popup.add(calendar);
    setField(date);
  }

  public void actionPerformed(ActionEvent event)
  {
    if (event.getSource() == calendarButton)
    {
      field.requestFocus();
      getRootPane().setDefaultButton(null);
      if (!popup.isVisible())
      {
        Dimension dim = calendarButton.getSize();
        calendar.setDate(date);
        popup.show(calendarButton, dim.width -
          popup.getPreferredSize().width,
          dim.height);
      }
      else
      {
        popup.setVisible(false);
        field.requestFocus();
      }
    }
    if (event.getSource() == calendar)
    {
      setField(calendar.getDate());
      popup.setVisible(false);
      field.requestFocus();
    }
  }

  protected void setField(Calendar date)
  {
    this.date = date;
    field.setText(formatter.format(date.getTime()));
  }

  public void setDate(Calendar date)
  {
    calendar.setDate(date);
  }
  
  public Calendar getDate()
  {
    return calendar.getDate();
  }

  public void setCalendar(JCalendar calendar)
  {
    this.calendar = calendar;
  }
  
  public JCalendar getCalendar()
  {
    return calendar;
  }
  
  public void setDateFormat(DateFormat formatter)
  {
    this.formatter = formatter;
  }
 }
}