// (c) 2003 Allen I Holub. All rights reserved. package com.holub.ui; import javax.swing.*; import javax.swing.border.*; import java.awt.event.*; import java.awt.*; import java.util.Date; import java.util.Calendar; import java.util.Locale; import java.text.DateFormatSymbols; import java.net.URL; /*** * A calendar-dispaly/date-selection widget. * <p> * Here's what it looks like: * <blockquote> * <img src="../../../images/DateSelector.gif"> * </blockquote> * "Today" is highlighted. * Select a date by clicking on it. * The background is transparant by default — it's grey here because * the underlying window is grey. * <p> <img src="../../../images/NavigableDateSelector.gif"> * 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 * to advances the * calandar by one month (single arrow) or one year (double arrow) * forwards (right-pointing arrow) or backwards (left-pointing arrow). * "Today" is highlighted. * Navigation bars are specified using a Gang-of-Four "Decorator" * object that wraps the raw <code>DateSelectorPanel</code> * Both the wrapper and the underlying panel implement the * <code>DateSelectory</code> interface, so can be use * used interchangably. The following code creates the * date selector at right. * <pre> * DateSelector selector = new DateSelectorPanel(); * selector = new NavigableDateSelector( selector ); * </pre> * The same thing can be accomplished with a convenience constuctor that * creates the wrapped DateSelectorPanel for you: * <pre> * DateSelector selector = new NavigableDateSelector(); * </pre> * <p> * <img src="../../../images/TitledNavigableDateSelector.gif"> * The other augmentation of interest is a title that shows the * month name and year that's displayed. (there's an example at right). * Use the same decoration strategy as before to add the title: * <pre> * DateSelector selector = new DateSelectorPanel(); * selector = new NavigableDateSelector( selector ); * selector = new TitledDateSelector ( selector ); * </pre> * You can leave out the navigation bar by ommiting the * second line of the foregoing code. * Again, a convenience constructor is provided to create a * titled date selector (without the navigation bar) as follows: * <pre> * DateSelector selector = new TitledDateSelector(); * </pre> * <p> * <img src="../../../images/DateSelectorDialog.gif"> * The final variant is the lightweight popup dialog shown at right. * It can be dragged around by the title bar (though dragging can * be disabled) and closed by clicking on the "close" icon on the * upper right. As before, use a decorator to manufacture a dialog: * <pre> * DateSelector selector = new DateSelectorPanel(); * selector = new NavigableDateSelector( selector ); // add navigation * selector = new DateSelectorDialog ( selector ); * </pre> * Note that you don't need a title because one is supplied for you * in the dialog-box title bar. Also as before, a convenience * constructor to create a navigable dialog box like the one at * right: * <pre> * DateSelector = new DateSelectcorDialog(); * <pre> * All the earlier examples create a claendar for the current * month. Several methods are provided, below, to change the date * in your program. For the most part, they work like simliar * methods of the {@link Calendar} class. * <DL> * <DT><b>Revisions</b> * <DD> * 2003/6/9: Allen Holub added a column heading holding two-character * day-name abbreviations. * <br> * </DD> * * </DL> * <DL> * <DT><b>Known Problems</b> * <DD> * The month and day names are hard coded (in English). Future versions * will load these strings from a resource bundle. The week layout * (S M T W Th F Sa Su) is the default layout for the underlying * {@link Calendar}, which should change with Locale as appropriate. * This feature has not been tested, however. * </DD> * </DL> * * </DL> * <DL> * <DT><b>Revisions</b> * <DD> * <table cellspacing=0 cellpadding=3> * <tr> <td>2003-07-10</td> * <td>Added day-of-week labels to tops of columns. * </td> * </tr> * <tr> <td>2003-07-17</td> * <td>Modified to use Java's DateFormatSymbols class to get * month and weekday names for current default locale. * </td> * </tr> * </table> * </DD> * </DL> * * <!-- ====================== distribution terms ===================== --> * <p><blockquote * style="border-style: solid; border-width:thin; padding: 1em 1em 1em 1em;"> * <center> * Copyright © 2003, Allen I. Holub. All rights reserved. * </center> * <br> * <br> * This code is distributed under the terms of the * <a href="http://www.gnu.org/licenses/gpl.html" * >GNU Public License</a> (GPL) * with the following ammendment to section 2.c: * <p> * As a requirement for distributing this code, your splash screen, * about box, or equivalent must include an my name, copyright, * <em>and URL</em>. An acceptable message would be: * <center> * This program contains Allen Holub's <em>XXX</em> utility.<br> * (c) 2003 Allen I. Holub. All Rights Reserved.<br> * http://www.holub.com<br> * </center> * If your progam does not run interactively, then the foregoing * notice must appear in your documentation. * </blockquote> * <!-- =============================================================== --> * @author Allen I. Holub * * @see com.holub.ui.DateSelector * @see com.holub.ui.DateSelectorDialog * @see com.holub.ui.DateInput * @see com.holub.ui.InteractiveDate * @see com.holub.ui.NavigableDateSelector * @see com.holub.ui.TitledDateSelector */ public class DateSelectorPanel extends JPanel implements DateSelector { private static String[] weekdays; private static String[] months; static { // Initialize weekdays and months with Local-specific // strings. DateFormatSymbols symbols = new DateFormatSymbols( Locale.getDefault() ); months = symbols.getMonths(); weekdays = symbols.getShortWeekdays(); } private static final int DAYS_IN_WEEK = 7, // days in a week MAX_WEEKS = 6; // maximum weeks in any month 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(); // An ActionListener that fields all events coming in from the // calendar // private final ButtonHandler dayListener = new ButtonHandler(); // "days" is not a two-dimensional array. I drop buttons into // a gridLayout and let the layout manager worry about // what goes where. The first buttion is the first day of the // first week on the grid, the 8th button is the first day of the // second week of the grid, and so forth. private JButton[] days = new JButton[ DAYS_IN_WEEK * MAX_WEEKS ]; { for( int i = 0; i <days.length; i++ ) { JButton day = newDayButton("--"); day.addActionListener( dayListener ); day.setActionCommand("D"); days[i] = day; } } public static JButton newDayButton( String text ) { JButton day = new JButton(text); day.setBorder (new EmptyBorder(1,2,1,2)); day.setFocusPainted (false); day.setOpaque (false); return day; } public static JButton newDayButton( char c ) { return newDayButton( "" + c ); } private JButton[] dayNames = { newDayButton( weekdays[1].charAt(0) ), newDayButton( weekdays[2].charAt(0) ), newDayButton( weekdays[3].charAt(0) ), newDayButton( weekdays[4].charAt(0) ), newDayButton( weekdays[5].charAt(0) ), newDayButton( weekdays[6].charAt(0) ), newDayButton( weekdays[7].charAt(0) ) }; /** Create a DateSelector representing the current date. */ public DateSelectorPanel() //{=DateSelectorPanel.noarg} { JPanel calendarDisplay = new JPanel(); calendarDisplay.setOpaque(false); calendarDisplay.setBorder( BorderFactory.createEmptyBorder(5,3,0,1) ); // Need enough rows to hold the maximum number of weeks in a month // plus one more for the week names. calendarDisplay.setLayout(new GridLayout( MAX_WEEKS + 1 /*rows*/, DAYS_IN_WEEK /*columns*/ )); for( int i = 0; i < dayNames.length; ++i ) //{=DateSelectorPanel.init.grid} calendarDisplay.add(dayNames[i]); for( int i = 0; i < days.length; ++i ) calendarDisplay.add(days[i]); setOpaque( false ); setLayout( new BorderLayout() ); add(calendarDisplay, BorderLayout.CENTER); updateCalendarDisplay(); } /** Create a DateSelectorPanel for an arbitrary date. * @param initialDate Calendar will display this date. The specified * date is highlighted as "today". * @see #DateSelectorPanel(int,int,int) */ public DateSelectorPanel(Date initialDate) { this(); calendar.setTime( initialDate ); today. setTime( initialDate ); updateCalendarDisplay(); } /** Create a DateSelectorPanel for an arbitrary date. * @param year the full year (e.g. 2003) * @param month the month id (0=january, 1=feb, etc. [this is the * convention supported by the other date classes]) * @param day the day of the month. This day will be highlighted * as "today" on the displayed calendar. Use 0 to suppress * the highlighting. * @see #DateSelectorPanel(Date) */ public DateSelectorPanel( int year, int month, int day ) { this(); calendar.set(year,month,day); if( day != 0 ) today.set(year,month,day); updateCalendarDisplay(); } /************************************************************************ * List of observers. */ private ActionListener subscribers = null; /** Add a listener that's notified when the user scrolls the * selector or picks a date. * @see DateSelector */ public synchronized void addActionListener(ActionListener l) { subscribers = AWTEventMulticaster.add(subscribers, l); } /** Remove a listener. * @see DateSelector */ public synchronized void removeActionListener(ActionListener l) { subscribers = AWTEventMulticaster.remove(subscribers, l); } /** Notify the listeners of a scroll or select */ private void fire_ActionEvent( int id, String command ) { if (subscribers != null) subscribers.actionPerformed(new ActionEvent(this, id, command) ); } /*********************************************************************** * Handle clicks from the buttons that represent calendar days. */ private class ButtonHandler 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 choosen Integer.parseInt(text) // date. ); fire_ActionEvent( SELECT_ACTION, calendar.getTime().toString() ); } } } } //---------------------------------------------------------------------- private JButton highlighted = null; private void clearHighlight() { 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 ); } //---------------------------------------------------------------------- /** Redraw the buttons that comprise the calandar to display the current * month */ private void updateCalendarDisplay() { setVisible(false); // improves paint speed & reduces flicker clearHighlight(); // The buttons that comprise the calendar are in a single // dimentioned 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 firstDayOffset = calendar.get(Calendar.DAY_OF_WEEK); /* 1 */ // assert firstDayOffset < days.length; int i = 0; while( i < firstDayOffset-1 ) /* 2 */ days[i++].setText(""); int dayOfMonth = 1; for(; i < days.length; ++i ) /* 3 */ { // Can't get calendar.equals(today) to work, so do it manually 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(dayOfMonth) ); calendar.roll( Calendar.DATE, /*up=*/ true ); // forward one day dayOfMonth = calendar.get(Calendar.DATE); if( dayOfMonth == 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 becasue we want to start clearing at the cell // after that. while( ++i < days.length ) /* 4 */ days[i].setText(""); setVisible(true); } /** Create a naviagion button with an image appropriate to the caption. * The <code>caption</code> argument is used as the button's "action * command." This method is public only because it has to be. * (It overrides a public method.) Pretend it's not here. */ public void addNotify() { super.addNotify(); int month = calendar.get(Calendar.MONTH); int year = calendar.get(Calendar.YEAR); fire_ActionEvent( CHANGE_ACTION, months[month] + " " + year ); } /** Returns the {@link Date Date} selected by the user or null if * the window was closed without selecting a date. The returned * Date has hours, minutes, and seconds values of 0. Modifying * the returned Date has no effect on the displayed date. */ public Date getDateRepresentation() { return calendar.getTime(); } /** Returns a Calendar that represents the currently selected * date. This object is not hooked up to the widget in any * way. Modifying the returned Calendar * will not affect the displayed date, for example. * This calender represents the most recently selected date. */ public Calendar getCalendarRepresentation() { Calendar clone = Calendar.getInstance(); clone.setTime( calendar.getTime() ); return clone; } /** Display the specified date. * @param src display the date represented by this Calendar. * Modifying the Calendar after this call will * not affect the displayed date in any way. */ public void displayDate( Calendar src ) { calendar.setTime( src.getTime() ); updateCalendarDisplay(); } /** Display the specified date. * @param src display the date represented by this Date object. * Modifying the Calendar after this call will * not affect the displayed date in any way. */ public void displayDate( Date src ) { calendar.setTime( src ); updateCalendarDisplay(); } /** Works just like {@link Calendar#roll(int,boolean)}. */ public void roll(int field, boolean up) { calendar.roll(field,up); updateCalendarDisplay(); } /** Works just like {@link Calendar#roll(int,int)}. */ public void roll(int field, int amount) { calendar.roll(field,amount); updateCalendarDisplay(); } /** Works just like {@link Calendar#set(int,int,int)} * Sets "today" (which is higlighted) to the indicated day. */ public void set( int year, int month, int date ) { calendar.set(year,month,date); today.set(year,month,date); updateCalendarDisplay(); } /** Works just like {@link Calendar#get(int)} */ public int get( int field ) { return calendar.get(field); } /** Works just like {@link Calendar#setTime(Date)}, * Sets "today" (which is higlighted) to the indicated day. */ public void setTime( Date d ) { calendar.setTime(d); today.setTime(d); updateCalendarDisplay(); } /** Works just like {@link Calendar#getTime} */ public Date getTime( ) { return calendar.getTime(); } /** Return a Calendar object that represents the currently-displayed * month and year. Modifying this object will not affect the * current panel. * @return a Calendar representing the panel's state. */ public Calendar getCalendar() { Calendar c = Calendar.getInstance(); c.setTime( calendar.getTime() ); return c; } /** Change the display to match the indicated calendar. This Calendar * argument is used only to provide the new date/time information. * Modifying it after a call to the current method will not affect * the DateSelectorPanel at all. * Sets "today" (which is higlighted) to the indicated day. * @param calendar A calendar positioned t the date to display. */ public void setFromCalendar(Calendar calendar) { this.calendar.setTime( calendar.getTime() ); today.setTime( calendar.getTime() ); updateCalendarDisplay(); } //---------------------------------------------------------------------- private static class Test { public static void main( String[] args ) { JFrame frame = new JFrame(); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); frame.getContentPane().setLayout( new FlowLayout() ); DateSelector left = new TitledDateSelector(new NavigableDateSelector()); DateSelector center = new NavigableDateSelector(); DateSelector right = new DateSelectorPanel(1900,1,2); ((NavigableDateSelector)center).changeNavigationBarColor( Colors.TRANSPARENT ); ActionListener l = new ActionListener() { public void actionPerformed(ActionEvent e) { System.out.println( e.getActionCommand() ); } }; left.addActionListener (l); center.addActionListener(l); right.addActionListener (l); JPanel white = new JPanel(); // proove that it's transparent. white.setBackground(Color.WHITE); white.add( (JPanel)center ); // I hate these casts, but they're // mandated by the fact that // Component is not an interface. // frame.getContentPane().add( (JPanel)left ); frame.getContentPane().add( white ); frame.getContentPane().add( (JPanel)right ); frame.pack(); frame.show(); } } }