Solve the date-selection problem once and for all

Implement a date-selection widget using the Decorator pattern

Date selection is a problem that pops up every few months when you build client-side graphical user interfaces (GUIs). Unfortunately, Java doesn't provide anything like a DateChooser class, and the date-selection widgets I found in a recent Web search were too heavyweight for my purposes. I wanted something clean, unobtrusive, and flexible. Figure 1 shows a few variants of this article's class, so you can see what I mean by "clean" at the display level.

Figure 1. Various ways to present a Date_selector object

The free off-the-Web solutions were great examples of the you-get-what-you-pay-for principle: amateurish code that must be completely rewritten to be useful. This article's example, in fact, started out as an off-the-Web control, but by the time I finished fixing it, not a single line of the original code remained, and the architecture had completely changed (or, I should say, I added an architecture to an amorphous Java mass). So much for "free."

My widget also provides a good Decorator design pattern demonstration and a good look at Java's Calendar class, and it shows how to implement your own title bars and frames on a Swing JDialog.

Decorators

As is the case with any complex problem, the solution is greatly simplified by breaking the big problem up into smaller problems, each of which can be easily implemented (and tested). The Decorator pattern is great for this approach.

You've seen decorators if you've used Java's input/output (I/O) classes. The complex problem is efficiently reading a compressed stream of bytes, for example. You can do this with one massive class, but it's easier to break the problem up into three distinct subproblems:

  1. Read bytes
  2. Make reads more efficient with buffering
  3. Decompress a stream of bytes

Java solves the first problem with a FileInputStream, instantiated like this:

    try
    {   InputStream in = new FileInputStream( "file.name" );
    }
    catch( IOException e )
    {   System.err.println( "Couldn't open file.name" );
        e.printStackTrace();
    }

You then add buffering with a decoration (or "wrapping") strategy. You wrap the InputStream object with another InputStream implementor that buffers bytes. You ask the wrapper for a byte; it asks the wrapped stream for many bytes and returns the first one. The decorator wrapping goes like this:

    try
    {   InputStream in = new FileInputStream( "file.name" );
        in = new BufferedInputStream( in );
    }
    catch( IOException e )
    {   System.err.println( "Couldn't open file.name" );
        e.printStackTrace();
    }

Add decompression with another decorator:

    try
    {   InputStream in = new FileInputStream( "file.name" );
        in = new BufferedInputStream( in );
        in = new GZipInputStream( in );
    }
    catch( IOException e )
    {   System.err.println( "Couldn't open file.name" );
        e.printStackTrace();
    }

You can increase filtering by adding more decorators.

This solution is very flexible. You can mix and max the decorators to get the feature mix you need. More importantly, each decorator is relatively simple because it solves only one problem. Consequently, the decorators are easy to write, debug, and modify without affecting the rest of the system. I can change the buffering algorithm by rewriting BufferedInputStream, for example, and not touch any other decorators (or any code that uses them). I can also add new filter functionality simply by implementing a new decorator. (Classes like CipherInputStream were added to Java this way.)

Date selection from the center out

I apply the Decorator principle to the date-selection problem. The core problem is displaying a calendar for a single month, so I start by implementing a class that does only that. Figure 2 shows the Date_selector_panel user interface. It's just a calendar, but it's easy to create because I don't have to clutter the code with unnecessary details. "Today" is highlighted. Select a date by clicking on it. (ActionListener objects registered with the widget are notified when you select a date.) The background is transparent by default—it appears gray here because the underlying window is gray.

Figure 2. A raw Date_selector_panel, created with the no-arg constructor

This raw date selector can be "decorated" in several ways to make it more useful. First, you can add a navigation bar to the bottom (shown in Figure 3) to move the calendar one month (single arrow) or one year (double arrow) forward (right-pointing arrow) or backward (left-pointing arrow).

Figure 3. Add navigability

You can add navigation bars with a decorator object that wraps the raw Date_selector_panel. Both the wrapper and the underlying panel implement the Date_selector interface, so you can use them interchangeably. The following code creates the decorated date selector in Figure 3:

Date_selector selector = new Date_selector_panel();
selector = new Navigable_date_selector( selector );

The same effect is accomplished with a convenience constructor that creates the wrapped Date_selector_panel for you:

Date_selector selector = new Navigable_date_selector();

The other augmentation I've supported is a title that shows the month name and year that displays on the calendar (see Figure 4).

Figure 4. Add a title

I use the same decoration strategy as before to add the title:

Date_selector selector = new Date_selector_panel();
selector = new Navigable_date_selector( selector );
selector = new Titled_date_selector   ( selector );

Leave out the navigation bar by omitting the second line in the above code. It doesn't matter which order you mix in the title and navigation bars if you need both. Again, a convenience constructor creates a titled date selector (without the navigation bar) as follows:

Date_selector selector = new Titled_date_selector();

The final variant is Figure 5's lightweight pop-up dialog. It can be dragged by the title bar (though dragging can be disabled) and closed by clicking on the close icon in the upper right corner.

Figure 5. A date-selection dialog

As before, use a decorator to manufacture a dialog:

Date_selector selector = new Date_selector_panel();
selector = new Navigable_date_selector( selector ); // Add navigation
Date_selector_dialog popup = new Date_selector_dialog( selector );
//...
Date = popup.select();  // Returns the user-selected date, or null if the
                        // dialog was closed without selecting anything.
// Or you can do this:
popup.setVisible( true );           // Pop it up. Block until dismissal.
Date = popup.get_selected_date();   // Get the selected date.

Note that you don't need to mix in a title here because you use the dialog-box title bar for that purpose. As before, the Date_selector_dialog defines a convenience constructor to create a navigable dialog box like the one in Figure 5:

Date_selector = new Date_selector_dialog();

All the earlier examples create a calendar for the current month. Several methods can programmatically change the date. For the most part, they work like the java.util.Calendar class's similar methods.

Implement Date_selector

I implement the core Date_selector_panel as an interface to make the Decorator pattern viable. (In the I/O example, all the decorators are InputStream objects, so they can be treated interchangeably. The constructor arguments to all the decorators are all InputStream references. You can wrap any decorator with another decorator without the latter knowing what it's wrapping.)

The core interface looks like this (I left out the Javadoc and other extraneous code in the interest of clarity. See Resources for the complete code):

public interface Date_selector
{
    public static final int CHANGE_ACTION = 0;
    public static final int SELECT_ACTION = 1;
    public void addActionListener(ActionListener l);
    public void removeActionListener(ActionListener l);
    public Date get_selected_date();
    public Date get_current_date();
    /** Must work just like {@link Calendar#roll(int,boolean)} */
    public void roll(int flag, boolean up);
    /** Must work just like {@link Calendar#get(int)} */
    public int get(int flag);
}

The interface defines two constant values, CHANGE_ACTION and SELECT_ACTION, used in event processing (more in a moment). The interface provides ways to add and remove ActionListener objects that are notified when a date is selected. Finally, it provides an interface that mimics the java.util.Calendar() methods to advance the calendar by some increment (a month, typically) and get an attribute's value (such as the month or year).

Let's return to the action listeners: add listeners to your date selector in the same way you add a listener to a button:

Date_selector selector = new Date_selector_panel();
selector.addActionListener
(   new ActionListener()
    {   public void actionPerformed( ActionEvent e )
        {   // Do whatever you'd do to process an event
        }
    }
);

The listener strategy is a realization of the Observer design pattern.

Listeners are notified (actionPerformed(...) is called) in two situations, which can be distinguished from one another by sending the ActionEvent object passed to actionPerformed a getID() message. The table below lists the details.

Action events

getID()returnsDescription
CHANGE_ACTIONThis event is sent when the calendar panel changes the displayed month or year (typically because some sort of navigator bar asked it to). Call event.getActionCommand getActionCommand() to get a string holding the current (after the scroll) month and year. You can also call get_current_date() to get the date the user selected.
SELECT_ACTIONSent every time the user clicks on a date. Call event.getActionCommand getActionCommand() to get a string representing the selected date. (This string takes the same form as the one returned by toString.) You can also call get_selected_date() to get the date the user selected.

The following code demonstrates the two event types. It sets up any Date_selector to update a label every time the user navigates to a new month and also prints the selected date to the console when the user clicks on it:

Date_selector selector = new Date_selector_panel();
JLabel moth_display = new JLabel();
s.addActionListener
(   new ActionListener()
    {   public void actionPerformed( ActionEvent e )
        {   if( e.getID() == Date_selector.CHANGE_ACTION )
                month_display.setText( e.getActionCommand() );
            else
                System.out.println( e.getActionCommand() );
        }
    }
);

All classes that implement Date_selector also extend JPanel (or at least some Container derivative). I want to mandate this requirement in the code so that a Date_selector can also be used as a JPanel without a cast, but I can't do that here because java.awt.Container is not an interface. For a method to return something that can be treated as a Date_selector or Container, it would have to implement both classes, which is illegal. Also, an interface (Date_selector) can't extend a class (Container).

This deficiency is a good example of how it's sometimes difficult to retrofit Gang of Four patterns on existing code that doesn't appropriately use interfaces. If this code was my own, I would refactor it to make Container an interface and then implement both Container and Date_selector in my decorators. (I obviously can't change a class in a java.* package, however). This class-to-interface refactoring is difficult because all the code that instantiated the class-now-interface using new would break.

One of the few reasons to introduce C++-style operator overloading into Java is to make this sort of refactoring easy by overloading operator new. That way, you can new an interface and have the overload provide an instance of a default implementation class. The Java solution is to introduce a Gang of Four factory, but it's a lot easier to do things right from the beginning.

Implement the basic selector

The core date-selection class is Date_selector_panel. Looking at it in pieces, I start with a few variable declarations, shown below. Don't be thrown by the "instance initializer" syntax. (Think of the code in parentheses that doesn't seem to be attached to anything as a static initializer block without the static.) Instance initializers are concatenated in declaration order and then append to the top of every constructor that doesn't chain to another constructor using the this(...) construct. They provide a way to initialize a field with a guarantee that the initialization code will effectively appear in every constructor. It's also convenient to put all initialization code for a given field in one place at the point of initialization. That way, no uninitialized (or partially initialized) variables lurk around causing runtime errors. Moving the code to the constructor makes maintenance harder because you must check two places when you make a change.

The code in parentheses following the calendar declaration, for example, is an instance initializer:

public class Date_selector_panel extends JPanel implements Date_selector
{
    // TODO: These strings should be in a resource bundle so they
    // can be internationalized
    //
    private String[] months =
    {   "Jan","Feb", "Mar","Apr", "May","June",
        "July","Aug","Sept","Oct","Nov","Dec"
    };
    private static final int DAYS_IN_WEEK = 7,  // Days in a week
                             MAX_WEEKS    = 6;  // Maximum weeks in any month
    // The date the user selects
    private Date selected = null;
    private Calendar calendar = Calendar.getInstance();
    {   calendar.set( Calendar.HOUR,    0 );
        calendar.set( Calendar.MINUTE,  0 );
        calendar.set( Calendar.SECOND,  0 );
    }
    // The calendar that's displayed on the screen
    private final Calendar today = Calendar.getInstance();

The next order of business is to handle clicks on individual days. I take the easy way out and make each day on the screen a JButton whose label is the date (compared to doing manual hit testing on a large window). I define a single listener object and register it with every button. The listener fetches the button label, which holds the date, and sets the Calendar field (calendar, defined above) to that value. It then extracts a Date object from the Calendar and uses that Date to make a String to send to the listeners (by means of the fire_ActionEvent() call).

// An ActionListener that fields all events coming in from the
// calendar.
//
private final Button_handler day_listener  = new Button_handler();
private class Button_handler implements ActionListener
{   public void actionPerformed(ActionEvent e)
    {
        if (e.getActionCommand().equals("D"))
        {   String text = ((JButton) e.getSource()).getText();
            if(text.length() > 0)  //  <=0 means click on blank square. Ignore.
            {   calendar.set
                (   calendar.get(Calendar.YEAR),    // Reset the calendar
                    calendar.get(Calendar.MONTH),   // to be the chosen
                    Integer.parseInt(text)          // date.
                );
                selected = calendar.getTime();
                fire_ActionEvent( SELECT_ACTION, selected.toString() );
            }
        }
    }
}

Java's AWTEventMulticaster class implements action listeners. The implementation is right out of the documentation, to which I refer you (An article on multicasters is also listed in Resources.):

private ActionListener subscribers = null;
public synchronized void addActionListener(ActionListener l)
{   subscribers = AWTEventMulticaster.add(subscribers, l);
}
public synchronized void removeActionListener(ActionListener l)
{   subscribers = AWTEventMulticaster.remove(subscribers, l);
}
private void fire_ActionEvent( int id, String command )
{   if (subscribers != null)
         subscribers.actionPerformed(new ActionEvent(this, id, command) );
}
// addNotify() is called just before the panel displays. It
// actually creates the underlying graphical objects, so you
// must call super.addNotify() to bring the panel into existence.
// I use it as a hook here to send an initialization
// ActionEvent to anybody who's supporting a title.
public void addNotify()
{
    super.addNotify();
    int month = calendar.get(Calendar.MONTH);
    int year  = calendar.get(Calendar.YEAR);
    fire_ActionEvent( CHANGE_ACTION, months[month] + " " + year );
}

I then create and initialize the array of buttons that represents the days. Interestingly, the calendar is not represented by a two-dimensional array; rather, I drop buttons into a GridLayout and let the layout manager worry about what goes where. The first button in the linear array is the first day of the first week on the grid; the eighth button is the first day of the second week on the grid; and so forth. Here's the code:

private JButton[] days = new JButton[ DAYS_IN_WEEK * MAX_WEEKS ];
{   for( int i = 0; i < days.length; i++ )
    {
        JButton day = new JButton("--");
        days[i] = day;
        day.setBorder           (BorderFactory.createEmptyBorder(1,2,1,2)); // Clear small border
        day.setFocusPainted     (false);                    // Cannot get focus
        day.setActionCommand    ("D");
        day.addActionListener   (day_listener);             // Our single listener
        day.setOpaque           (false);                    // Transparent background
    }
}

Next come the constructors. The main work is in the no-arg constructor, which creates a JPanel to hold the calendar, installs a grid layout, and then drops the buttons into it:

public Date_selector_panel()
{
    JPanel calendar_display = new JPanel();
    calendar_display.setOpaque(false);
    calendar_display.setBorder( BorderFactory.createEmptyBorder(5,3,0,1) );
    calendar_display.setLayout(new GridLayout(MAX_WEEKS /*rows*/, DAYS_IN_WEEK /*columns*/ ));
    for( int i = 0; i <  days.length; ++i )
        calendar_display.add(days[i]);
    setOpaque( false );
    setLayout( new BorderLayout() );
    add(calendar_display, BorderLayout.CENTER);
    update_calendar_display();
}

The full code listing has a few additional convenience constructors that initialize the calendar to something other than the current time, which I omitted here.

The meat of the Date_selector_panel class is the update_calendar_display() method, which updates the displayed calendar when the underlying Calendar changes. I use the java.util.Calendar() methods to determine the offset from Sunday to the first day of the month, setting the labels on those buttons in the days to empty strings. I then continue adding labels to a linear array of buttons, changing them to the appropriate date. Finally, I fill out the labels on the remaining "day" buttons (which follow the last day of the month) with empty strings. This way, every button you see on the screen is modified, even if it doesn't contain a valid day for the current month. The code doesn't do any layout—the GridLayout already laid out the buttons when they were put into their container:

private void update_calendar_display()
{
    setVisible(false);  // Improves paint speed and reduces flicker.
    clear_highlight();
    // The buttons that comprise the calendar are in a single
    // dimensioned array that was added to a 6x7 grid layout in
    // order. Because of the linear structure, it's easy to
    // lay out the calendar just by changing the labels on
    // the buttons. Here's the algorithm used below:
    //
    //  1) Find out the offset to the first day of the month.
    //  2) Clear everything up to that offset.
    //  3) Add the days of the month.
    //  4) Clear everything else.
    int month = calendar.get(Calendar.MONTH);
    int year  = calendar.get(Calendar.YEAR);
    fire_ActionEvent( CHANGE_ACTION, months[month] + " " + year );
    calendar.set( year, month, 1 ); // First day of the current month.
    int first_day_offset = calendar.get(Calendar.DAY_OF_WEEK);      /* 1 */
    assert Calendar.SUNDAY == 0;
    assert first_day_offset < days.length;
    int i = 0;
    while( i < first_day_offset-1 )                              /* 2 */
        days[i++].setText("");
    int day_of_month = 1;
    for(; i < days.length; ++i )                                 /* 3 */
    {
        if( calendar.get(Calendar.MONTH)==today.get(Calendar.MONTH)
        &&  calendar.get(Calendar.YEAR )==today.get(Calendar.YEAR )
        &&  calendar.get(Calendar.DATE )==today.get(Calendar.DATE ) )
        {   highlight( days[i] );
        }
        days[i].setText( String.valueOf(day_of_month) );
        calendar.roll( Calendar.DATE, /*up=*/ true );   // Forward one day.
        day_of_month = calendar.get(Calendar.DATE);
        if( day_of_month == 1 )
            break;
    }
    // Note that we break out of the previous loop with i positioned
    // at the last day we added, thus the following ++ *must* be a
    // preincrement because we want to start clearing at the cell
    // after that.
    while( ++i < days.length )                                 /* 4 */
        days[i].setText("");
    setVisible(true);
}
private JButton highlighted = null;
private void clear_highlight()
{
    if( highlighted != null )
    {   highlighted.setBackground( Color.WHITE );
        highlighted.setForeground( Color.BLACK );
        highlighted.setOpaque(false);
        highlighted = null;
    }
}
private void highlight( JButton cell )
{
    highlighted = cell;
    cell.setBackground( com.holub.ui.Colors.DARK_RED );
    cell.setForeground( Color.WHITE );
    cell.setOpaque( true );
}

The remainder of the class just defines various methods to access the calendar's value that represents the selected or delayed dates. These are pretty trivial and aren't shown.

Titles

The Date_selector_panel is the hard part. Now let's look at the decorators. The Titled_date_selector does only one thing: adds a title to an unadorned calendar. It's a JPanel that implements Date_selector and is passed the Date_selector to wrap as a constructor argument. It just displays the existing Date_selector along with a JLabel that shows the date.

The implementation, shown below, is trivial. The only code not strictly user interface-related is the action listener set up about 10 lines from the top. The listener catches the notification that the date changed (either via user navigation or a method call), and appropriately updates the label. The remaining code just sets up a panel and drops the wrapped Date_selector and JLabel title into it.

The point is that this code is simple—easy to write, easy to manage. If it was mixed into the Date_selector_panel itself, it would considerably increase that class's complexity with no obvious benefit. (The code has to go somewhere, so we might as well organize for clarity rather than lump it all in one place.) If I want to make the title fancier, I just modify this single class and don't worry about the rest of the system at all:

public class Titled_date_selector extends JPanel implements Date_selector
{   private       Date_selector selector;
    private final JLabel title = new JLabel("XXXX");
    /** Wrap an existing Date_selector to add a title bar showing
     *  the displayed month and year. The title changes as the
     *  user navigates.
     */
    public Titled_date_selector( Date_selector selector )
    {   this.selector = selector;
        title.setHorizontalAlignment(SwingConstants.CENTER);
        title.setOpaque     ( true                                      );
        title.setBackground ( com.holub.ui.Colors.LIGHT_YELLOW          );
        title.setFont       ( title.getFont().deriveFont( Font.BOLD )   );
        selector.addActionListener
        (   new ActionListener()
            {   public void actionPerformed( ActionEvent e )
                {   if( e.getID() == Date_selector_panel.CHANGE_ACTION )
                        title.setText( e.getActionCommand() );
                    else
                        my_subscribers.actionPerformed(e);
                }
            }
        );
        setOpaque(false);
        setLayout( new BorderLayout() );
        add( title,  BorderLayout.NORTH );
        add( (JPanel)selector, BorderLayout.CENTER );
    }
    /** This constructor lets you specify the background color of the
     *  title strip that holds the month name and year (the default
     *  is light yellow).
     *
     *  @param label_background_color the color of the title bar, or
     *      null to make it transparent.
     */
    public Titled_date_selector( Date_selector selector, Color label_background_color )
    {   this(selector);
        if( label_background_color == null )
            title.setOpaque( false );
        else
            title.setBackground( label_background_color );
    }
    private ActionListener my_subscribers = null;
    public synchronized void addActionListener(ActionListener l)
    {   my_subscribers = AWTEventMulticaster.add(my_subscribers, l);
    }
    public synchronized void removeActionListener(ActionListener l)
    {   my_subscribers = AWTEventMulticaster.remove(my_subscribers, l);
    }
    public Date get_selected_date()     { return selector.get_selected_date();  }
    public Date get_current_date()      { return selector.get_current_date();   }
    public void roll(int f, boolean up) {        selector.roll(f,up);           }
    public int  get(int f)              { return selector.get(f);               }
}

Navigation

The code for the navigation bar, shown below, is a bit longer, though equally simple. The code starts by defining four files that hold arrow images. (I plan to eventually modify the code to programmatically render the arrows, but for now, they're just image files.) Images are fetched as "resources," using the following code:

ClassLoader loader = getClass().getClassLoader();
loader.getResource( "IMAGE_FILE_NAME" );

The classloader looks for the images in the same places it looks for classes: If the program runs from the filesystem, for example, it looks for the images in directories on the CLASSPATH. If the program runs from a jar file, it looks in the jar file. By not using absolute paths, the code is much easier to package into a jar file to ship to your users, since both the code and the resources it uses can all remain in the jar file, and files don't have to be installed all over the filesystem.

The navigation bar is a panel containing four buttons that use the images as labels. The button's ActionListener just rolls the wrapped calendar object using the Date_selector's roll(...) method, and the act of rolling the calendar to the next month causes the title bar to be notified. It's important to notice that the navigation bar doesn't know or care about the title. When it rolls the wrapped selector, the selector notifies its listeners. The title wrapper is a listener, so it updates the title. The navigation bar doesn't even know the title wrapper is there:

public class Navigable_date_selector extends JPanel implements Date_selector
{   private       Date_selector selector;
    // Names of image files used for the navigator bar
    private static final String
        NEXT_YEAR       = "images/10px.red.arrow.right.double.gif",
        NEXT_MONTH      = "images/10px.red.arrow.right.gif",
        PREVIOUS_YEAR   = "images/10px.red.arrow.left.double.gif",
        PREVIOUS_MONTH  = "images/10px.red.arrow.left.gif";
    // These constants are used to identify the button and
    // as the button caption in the event that the appropriate
    // image file can't be located
    private static final String FORWARD_MONTH   = ">"    ,
                                FORWARD_YEAR    = ">>"    ,
                                BACK_MONTH      = "<"    ,
                                BACK_YEAR       = "<<"    ;
    private JPanel navigation = new JPanel();
    public Navigable_date_selector( Date_selector selector )
    {   this.selector = selector;
        setBorder( null  );
        setOpaque( false );
        setLayout( new BorderLayout() );
        add( (JPanel)selector, BorderLayout.CENTER );
        navigation.setLayout(new FlowLayout());
        navigation.setBorder( null );
        navigation.setBackground( com.holub.ui.Colors.LIGHT_YELLOW );
        navigation.add( make_navigation_button(BACK_YEAR    ) );
        navigation.add( make_navigation_button(BACK_MONTH   ) );
        navigation.add( make_navigation_button(FORWARD_MONTH) );
        navigation.add( make_navigation_button(FORWARD_YEAR ) );
        add(navigation, BorderLayout.SOUTH);
    }
    // ...
    // I left out a few constructors and utility methods that go here
    // ...
    private final Navigation_handler navigation_listener
                                        = new Navigation_handler();
    /** Handle clicks from the navigation bar buttons */
    private class Navigation_handler implements ActionListener
    {   public void actionPerformed(ActionEvent e)
        {   String direction = e.getActionCommand();
            if     (direction==FORWARD_YEAR )selector.roll(Calendar.YEAR,true);
            else if(direction==BACK_YEAR    )selector.roll(Calendar.YEAR,false);
            else if(direction==FORWARD_MONTH)
            {
                selector.roll(Calendar.MONTH,true);
                if( selector.get(Calendar.MONTH) == Calendar.JANUARY )
                    selector.roll(Calendar.YEAR,true);
            }
            else if (direction==BACK_MONTH  )
            {
                selector.roll(Calendar.MONTH,false);
                if( selector.get(Calendar.MONTH) == Calendar.DECEMBER )
                    selector.roll(Calendar.YEAR,false);
            }
            else
            {   assert false:  "Unexpected direction";
            }
        }
    }
    private JButton make_navigation_button(String caption)
    {
        ClassLoader loader = getClass().getClassLoader();
        URL image =
            (caption==FORWARD_YEAR  )? loader.getResource(NEXT_YEAR):
            (caption==BACK_YEAR     )? loader.getResource(PREVIOUS_YEAR):
            (caption==FORWARD_MONTH )? loader.getResource(NEXT_MONTH):
                                       loader.getResource(PREVIOUS_MONTH) ;
        JButton b = (image!=null) ? new JButton( new ImageIcon(image) )
                                  : new JButton(caption)
                                  ;
        b.setBorder(new EmptyBorder(0,4,0,4));
        b.setFocusPainted(false);
        b.setActionCommand(caption);
        b.addActionListener( navigation_listener );
        b.setOpaque( false );
        return b;
    }
    public synchronized void addActionListener(ActionListener l)
    {   selector.addActionListener(l);
    }
    public synchronized void removeActionListener(ActionListener l)
    {   selector.removeActionListener(l);
    }
    public Date get_selected_date()     { return selector.get_selected_date();  }
    public Date get_current_date()      { return selector.get_current_date();   }
    public void roll(int f, boolean up) {        selector.roll(f,up);           }
    public int  get(int f)              { return selector.get(f);               }
}

Pop-up dialogs

The dialog wrapper involves two classes. The first one, Popup_dialog, extends JDialog to implement the small, lightly framed dialog shown earlier. The main problem with turning off the normal dialog frame is that all the decoration (the title bar as well as the frame) disappears, so you need to build your own title bar if you want it, which most of the following code does.

The title bar is a panel containing a label (for the title) and a button (for the close icon, which loads as a resource in the same way the navigation arrows load).

Here's the code that does this:

public class Popup_dialog extends JDialog
{
    private Color TITLE_BAR_COLOR = com.holub.ui.Colors.LIGHT_YELLOW;
    private Color CLOSE_BOX_COLOR = com.holub.ui.Colors.DARK_RED;
    private JLabel title = new JLabel("xxxxxxxxxxxxxx");
    {   title.setHorizontalAlignment(SwingConstants.CENTER);
        title.setOpaque( false );
        title.setFont( title.getFont().deriveFont(Font.BOLD) );
    }
    private JPanel header = new JPanel();
    {   header.setBackground( TITLE_BAR_COLOR );
        header.setLayout( new BorderLayout() );
        header.setBorder( BorderFactory.createEmptyBorder(2,2,2,2) );
        header.add( title                   , BorderLayout.CENTER );
        header.add( create_close_button()   , BorderLayout.EAST   );
    }
    private JPanel content_pane = new JPanel();
    {   content_pane.setLayout( new BorderLayout() );
    }
    public Popup_dialog( Frame owner ){ super(owner); setModal(true); }
    public Popup_dialog( Dialog owner){ super(owner); setModal(true); }
    /* Code common to all constructors. */
    {
        init_dragable();
        setUndecorated( true );
        JPanel contents = new JPanel();
        contents.setBorder( BorderFactory.createLineBorder(Color.BLACK,1) );
        contents.setLayout(new BorderLayout());
        contents.add(header,       BorderLayout.NORTH);
        contents.add(content_pane, BorderLayout.CENTER);
        contents.setBackground( Color.WHITE );
        setContentPane( contents ); // , BorderLayout.CENTER );
        setLocation(100,100);
    }
    private JButton create_close_button()
    {
        URL image = getClass().getClassLoader().getResource(
                                                "images/8px.red.X.gif");
        JButton b = (image!=null) ? new JButton( new ImageIcon(image) )
                                  : new JButton( "  X  " )
                                  ;
        Border outer = BorderFactory.createLineBorder(CLOSE_BOX_COLOR,1);
        Border inner = BorderFactory.createEmptyBorder(2,2,2,2);
        b.setBorder( BorderFactory.createCompoundBorder(outer,inner) );
        b.setOpaque( false );
        b.addActionListener
        (   new ActionListener()
            {   public void actionPerformed(ActionEvent e)
                {   Popup_dialog.this.setVisible(false);
                    Popup_dialog.this.dispose();
                }
            }
        );
        b.setFocusable( false );
        return b;
    }
    /** Set the dialog title to the indicated text. */
    public void setTitle( String text ){ title.setText( text ); }
    /** Add your widgets to the window returned by this method, in
     *  a manner similar to a JFrame. Do not modify the Popup_dialog
     *  itself. The returned container is a {@link JPanel JPanel}
     *  with a preinstalled {@link BorderLayout}.
     *  By default, it's colored dialog-box gray.
     *  @return the content pane.
     */
    public Container getContentPane(){ return content_pane; }

The interesting code is the drag support. The basic idea is to register two listeners. A mouse listener registers that the button has been pressed and stores the mouse click's parent-window-relative location within the title label in the instance variable reference_position.

When the mouse drags, a mouse-motion listener is notified of movement. The problem is that the event associated with the drag notification is screen relative, not parent-window relative. The drag handler then moves the dialog box to place the original reference_position under the current mouse (screen) position. That is, it pulls the window over to where the mouse is now.

The code follows:

private Point reference_position = new Point(0,0);
private MouseMotionListener movement_handler;
private MouseListener click_handler;
private void init_dragable()
{
    movement_handler =
        new MouseMotionAdapter()
        {   public void mouseDragged( MouseEvent e )
            {   // The reference position is the (window-relative)
                // cursor position when the click occurred. The
                // current_mouse_position is mouse position
                // now, and the deltas represent the distance
                // moved.
                Point current_mouse_position  = e.getPoint();
                Point current_window_location = getLocation();
                int delta_x=current_mouse_position.x - reference_position.x;
                int delta_y=current_mouse_position.y - reference_position.y;
                // Move the window over by the computed delta. This move
                // effectively shifts the window-relative, current mouse
                // position back to the original reference position.
                current_window_location.translate(delta_x, delta_y);
                setLocation(current_window_location);
            }
        };
    click_handler =
        new MouseAdapter()
        {   public void mousePressed( MouseEvent e )
            {   reference_position = e.getPoint();  // Start of the drag
            }
        };
    setDragable(true);
}
/** Turn dragability on or off.
 */
public void setDragable( boolean on )
{   if( on )
    {   title.addMouseMotionListener ( movement_handler );
        title.addMouseListener       ( click_handler    );
    }
    else
    {   title.removeMouseMotionListener ( movement_handler );
        title.removeMouseListener       ( click_handler );
    }
}

Date dialogs

All that's left is another trivial wrapper. The Date_selector_dialog decorator just puts the wrapped Date_selector inside a Popup_dialog. It duplicates a little code from the Titled_date_selector since it uses the dialog's title bar to display the current date and month. It doesn't worry about navigation at all. The Date_selector passed to it can be wrapped in with a navigator (or not), as so:

public class Date_selector_dialog extends Popup_dialog implements Date_selector
{
    private Date_selector selector = new Date_selector_panel();
    /** Creates a dialog box with the indicated parent that holds
     *  a standard {@link Date_selector_panel Date_selector_panel}
     *  (as created using the no-arg constructor).
     */
    public Date_selector_dialog( Frame parent )
    {   super(parent);
        selector = new Navigable_date_selector( new Date_selector_panel() );
        init();
    }
    /* Like {@link #Date_selector_dialog(Frame),
     * but for a {@link Dialog} parent.
     */
    public Date_selector_dialog( Dialog parent )
    {   super(parent);
        selector = new Navigable_date_selector( new Date_selector_panel() );
        init();
    }
    /** Creates a dialog box with the indicated parent that holds
     *  the indicated Date_selector.
     *  Note that the current month and year display in the
     *  dialog-box title bar, so there's no need to display them in
     *  the selector too.
     */
    public Date_selector_dialog( Frame parent, Date_selector to_wrap )
    {   super(parent);
        selector = to_wrap;
        init();
    }
    /* Like {@link #Date_selector_dialog(Frame,Date_selector),
     * but for a {@link Dialog} parent.
     */
    public Date_selector_dialog( Dialog parent, Date_selector to_wrap )
    {   super(parent);
        selector = to_wrap;
        init();
    }
    /** Code common to all constructors.
     */
    private void init()
    {   getContentPane().add( (Container)selector, BorderLayout.CENTER );
        selector.addActionListener
        (   new ActionListener()
            {   public void actionPerformed( ActionEvent event )
                {   if( event.getID() == Date_selector.CHANGE_ACTION )
                    {   setTitle( event.getActionCommand() );
                    }
                    else
                    {   setVisible(false);
                        dispose();
                    }
                }
            }
        );
        ((Container)selector).setVisible(true);
        pack();
    }
    /** For use when you pop up a dialog using
     * setVisible(true) rather than {@link #select}.
     * @return the selected date or null if the dialog was closed
     *          without selecting anything.
     */
    public Date get_selected_date()
    {   return selector.get_selected_date();
    }
    /** Get the current date. The dialog stays in existence
     *  until the user closes it or selects a date, so this
     *  method can be used to see what month the user has
     *  scrolled to.
     *  @return the date currently displayed on the calendar.
     */
    public Date get_current_date()
    {   return selector.get_current_date();
    }
    /** Add an action listener for both.
     *  {@link Date_selector#CHANGE_ACTION} and
     *  {@link Date_selector#SELECT_ACTION} action events.
     */
    public void addActionListener(ActionListener l)
    {   selector.addActionListener(l);
    }
    /** Remove a previously added listener. */
    public void removeActionListener(ActionListener l)
    {   selector.removeActionListener(l);
    }
    /** Pops up the chooser and blocks until the user selects
     *  a date.
     * @return the selected date or null if the dialog was closed
     *          without selecting anything.
     */
    public Date select()
    {
        setVisible(true);
        return selector.get_selected_date();
    }
    public void roll(int f, boolean up) {        selector.roll(f,up);       }
    public int  get(int f)              { return selector.get(f);           }
}

Time to decorate

The main thing to take away from this article is not necessarily the calendar-selector widget though it's useful but rather the use of Decorator to partition a naturally layered set of capabilities. The Decorator pattern serves to simplify the code considerably by breaking it into manageable chunks that are easy to write and debug. Each decorator is highly focused and uncluttered as a consequence. If you need to change something, you know exactly where to make the change, and if you need to add another decoration, you just implement a simple interface. The partitioning also reduces the coupling relationships—the undesirable dependencies—between classes. Making a change or adding a feature in a monolithic implementation would have a much higher pucker factor. So, go forth and decorate.

Allen Holub has worked in the computer industry since 1979. He currently works as a consultant, helping companies not squander money on software by providing advice to executives, training, and design-and-programming services. He's authored eight books, including Taming Java Threads (Apress, 2000) and Compiler Design in C (Pearson Higher Education, 1990), and teaches regularly for the University of California Berkeley Extension. Find more information on his Website (http://www.holub.com).

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more