| CalendarComboBox |
/* ***************************************************************************
*
* File: CalendarWidget.java
* Package: ca.janeg.calendar
*
* Contains: ButtonActionListener
* CalendarModel
* CalendarSelectionListener
* InputListener
*
* References: 'Java Rules' by Douglas Dunn
* Addison-Wesley, 2002 (Chapter 5, section 13 - 19)
*
* 'Professional Java Custom UI Components'
* by Kenneth F. Krutsch, David S. Cargo, Virginia Howlett
* WROX Press, 2001 (Chapter 1-3)
*
* Date Author Changes
* ------------ ------------- ----------------------------------------------
* Oct 24, 2002 Jane Griscti Created
* Oct 27, 2002 jg Cleaned up calendar display
* Oct 30, 2002 jg added ctor CalendarComboBox( Calendar )
* Oct 31, 2002 jg Added listeners and Popup
* Nov 1, 2002 jg Cleaned up InputListener code to only accept
* valid dates
* Nov 2, 2002 jg modified getPopup() to handle display when
* component is positioned at the bottom of the screen
* Nov 3, 2002 jg changed some instance variables to class variables
* Mar 29, 2003 jg added setDate() contributed by James Waldrop
* *************************************************************************** */
package ca.janeg.calendar;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.text.DateFormat;
import java.text.DateFormatSymbols;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JFormattedTextField;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.Popup;
import javax.swing.PopupFactory;
import javax.swing.SwingConstants;
import javax.swing.border.LineBorder;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.plaf.basic.BasicArrowButton;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableColumn;
/**
* A custom component that mimics a combo box, displaying
* a perpetual calendar rather than a 'list'.
*
* @author Jane Griscti jane@janeg.ca
* @version 1.0 Oct 24, 2002
*/
public class CalendarComboBox extends JPanel {
// -- class fields
private static final DateFormatSymbols dfs = new DateFormatSymbols();
private static final String[] months = dfs.getMonths();
private static final String[] dayNames = new String[ 7 ];
private static final Toolkit toolkit = Toolkit.getDefaultToolkit();
private static final Dimension screenSize = toolkit.getScreenSize();
private static final PopupFactory factory =
PopupFactory.getSharedInstance();
// -- instance fields used with 'combo-box' panel
private final JPanel inputPanel = new JPanel();
private final JFormattedTextField input
= new JFormattedTextField( new Date() );
private final BasicArrowButton comboBtn
= new BasicArrowButton( SwingConstants.SOUTH );
// -- instance fields used with calendar panel
private final JPanel calPanel = new JPanel();
private final JTextField calLabel = new JTextField( 11 );
private final Calendar current = new GregorianCalendar();
private final CalendarModel display = new CalendarModel( 6, 6 );
private final JTable table = new JTable( display );
private final BasicArrowButton nextBtn =
new BasicArrowButton( SwingConstants.EAST );
private final BasicArrowButton prevBtn =
new BasicArrowButton( SwingConstants.WEST );
private final BasicArrowButton closeCalendarBtn =
new BasicArrowButton( SwingConstants.NORTH );
private Popup popup;
/**
* Create a new calendar combo-box object set with today's date.
*/
public CalendarComboBox(){
this( new GregorianCalendar() );
}
/**
* Create a new calendar combo-box object set with the given date.
*
* @param cal a calendar object
* @see java.util.GregorianCalendar
*/
public CalendarComboBox( final Calendar cal ){
super();
// set the calendar and input box date
Date date = cal.getTime();
current.setTime( date );
input.setValue( date );
// create the GUI elements and assign listeners
buildInputPanel();
buildCalendarDisplay();
registerListeners();
// intially, only display the input panel
add( inputPanel );
}
/*
* Creates a field and 'combo box' button above the calendar
* to allow user input.
*/
private void buildInputPanel(){
inputPanel.setLayout( new BoxLayout( inputPanel, BoxLayout.X_AXIS ) );
input.setColumns( 12 );
inputPanel.add( input );
comboBtn.setActionCommand( "combo" );
inputPanel.add( comboBtn );
}
/*
* Builds the calendar panel to be displayed in the popup
*/
private void buildCalendarDisplay(){
// Allow for individual cell selection and turn off
// grid lines.
table.setCellSelectionEnabled(true);
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setShowGrid( false );
// Calendar (table) column headers
// Set column headers to weekday names as given by
// the default Locale.
//
// Need to re-map the retreived names. If used as is,
// the table model ends up with an extra empty column as
// the returned names begin at index 1, not zero.
String[] names = dfs.getShortWeekdays();
for( int i=1; i<names.length; i++ ){
dayNames[ i - 1 ] = "" + names[ i ].charAt( 0 );
}
display.setColumnIdentifiers( dayNames );
table.setModel( display );
// Set the column widths. Need to turn
// auto resizing off to make this work.
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
int count = table.getColumnCount();
for( int i = 0; i < count; i ++ ){
TableColumn col = table.getColumnModel().getColumn( i );
col.setPreferredWidth( 20 );
}
// Column headers are only displayed automatically
// if the table is put in a JScrollPane. Don't want
// to use one here, so need to add the headers
// manually.
JTableHeader header = table.getTableHeader();
header.setFont( header.getFont().deriveFont( Font.BOLD ) );
JPanel panel = new JPanel();
panel.setLayout( new BoxLayout( panel, BoxLayout.Y_AXIS ) );
panel.add( header );
panel.add( table );
calPanel.setBorder( new LineBorder( Color.BLACK ) );
calPanel.setLayout( new BorderLayout() );
calPanel.add( buildCalendarNavigationPanel(), BorderLayout.NORTH );
calPanel.add( panel );
}
/*
* Creates a small panel above the month table to display the month and
* year along with the 'prevBtn', 'nextBtn' month selection buttons
* and a 'closeCalendarBtn'.
*/
private JPanel buildCalendarNavigationPanel(){
JPanel panel = new JPanel();
panel.setLayout( new BoxLayout( panel, BoxLayout.X_AXIS ) );
// Add a text display of the selected month and year.
// A JTextField is used for the label instead of a JLabel
// as it is easier to ensure a consistent size; JLabel
// expands and contracts with the text size
calLabel.setEditable( false );
int fontSize = calLabel.getFont().getSize();
calLabel.setFont( calLabel.getFont().deriveFont( Font.PLAIN, fontSize - 2 ) );
panel.add( calLabel );
// set button commands and add to panel
prevBtn.setActionCommand( "prevBtn" );
nextBtn.setActionCommand( "nextBtn" );
closeCalendarBtn.setActionCommand( "close" );
panel.add( prevBtn );
panel.add( nextBtn );
panel.add( closeCalendarBtn );
return panel;
}
/*
* Register all required listeners with appropriate
* components
*/
private void registerListeners(){
ButtonActionListener btnListener = new ButtonActionListener();
// 'Combo-box' listeners
input.addKeyListener( new InputListener() );
comboBtn.addActionListener( btnListener );
// Calendar (table) selection listener
// Must be added to both the table selection model
// and the column selection model; otherwise, new
// column selections on the same row are not recognized
CalendarSelectionListener listener = new CalendarSelectionListener();
table.getSelectionModel().addListSelectionListener( listener );
table.getColumnModel().getSelectionModel()
.addListSelectionListener( listener );
// Calendar navigation listeners
prevBtn.addActionListener( btnListener );
nextBtn.addActionListener( btnListener );
closeCalendarBtn.addActionListener( btnListener );
}
/*
* Fill the table model with the days in the selected month.
* Rows in the table correspond to 'weeks', columns to 'days'.
*
* Strategy:
* 1. get the first calendar day in the new month
* 2. find it's position in the first week of the month to
* determine the starting column for the day numbers
* 3. find the actual number of days in the month
* 4. fill the calendar with the day values, erasing any days
* left over from the old month
*/
private void updateTable( Calendar cal ){
Calendar dayOne = new GregorianCalendar(
cal.get( Calendar.YEAR ),
cal.get( Calendar.MONTH ),
1 );
// compute the number of days in the month and
// the start column for the first day in the first week
int actualDays = cal.getActualMaximum( Calendar.DATE );
int startIndex = dayOne.get( Calendar.DAY_OF_WEEK ) - 1;
// fill the calendar for the new month
int day = 1;
for( int row = 0; row < 6 ; row++ ){
for( int col = 0; col < 7; col++ ){
if( ( col < startIndex && row == 0 ) || day > actualDays ){
// overwrite any left over values from old month
display.setValueAt( "", row, col );
}else{
display.setValueAt( new Integer( day ), row, col );
day++;
}
}
}
// set the month, year label
calLabel.setText( months[ cal.get( Calendar.MONTH ) ] +
", " + cal.get( Calendar.YEAR ) );
// set the calendar selection
table.changeSelection( cal.get( Calendar.WEEK_OF_MONTH ) - 1,
cal.get( Calendar.DAY_OF_WEEK ) - 1,
false, false );
}
/*
* Gets a Popup to hold the calendar display and determines
* it's position on the screen.
*/
private Popup getPopup(){
Point p = input.getLocationOnScreen();
Dimension inputSize = input.getPreferredSize();
Dimension calendarSize = calPanel.getPreferredSize();
if( ( p.y + calendarSize.height ) < screenSize.height) {
// will fit below input panel
popup = factory.getPopup( input, calPanel,
p.x, p.y + (int)inputSize.height );
} else {
// need to fit it above input panel
popup = factory.getPopup( input, calPanel,
p.x, p.y - (int)calendarSize.height );
}
return popup;
}
/*
* Returns the currently selected date as a <code>Calendar</code> object.
*
* @return Calendar the currently selected calendar date
*/
public Calendar getDate(){
return current;
}
/**
* Sets the current date and updates the UI to reflect the new date.
* @param newDate the new date as a <code>Date</code> object.
* @see Date
* @author James Waldrop
*/
public void setDate(Date newDate) {
current.setTime(newDate);
input.setValue(current.getTime());
}
/*
* Creates a custom model to back the table.
*/
private class CalendarModel extends DefaultTableModel {
public CalendarModel( int row, int col ){
super( row, col );
}
/**
* Overrides the method to return an Integer class
* type for all columns. The numbers are automatically
* right-aligned by a default renderer that's supplied
* as part of JTable.
*/
public Class getColumnClass( int column ){
return Integer.class;
}
/**
* Overrides the method to disable cell editing.
* The default is editable.
*/
public boolean isCellEditable( int row, int col ){
return false;
}
}
/*
* Captures the 'prevBtn', 'nextBtn', 'comboBtn' and
* 'closeCalendarBtn' actions.
*
* The combo button is disabled when the popup is shown
* and enabled when the popup is hidden. Failure to do
* so results in the popup screen area not being cleared
* correctly if the user clicks the button while the popup
* is being displayed.
*/
private class ButtonActionListener implements ActionListener {
public void actionPerformed( ActionEvent e ){
String cmd = e.getActionCommand();
if( cmd.equals( "prevBtn" ) ){
current.add( Calendar.MONTH, -1 );
input.setValue( current.getTime() );
}else if( cmd.equals( "nextBtn" ) ){
current.add( Calendar.MONTH, 1 );
input.setValue( current.getTime() );
}else if( cmd.equals( "close" ) ){
popup.hide();
comboBtn.setEnabled( true );
}else{
comboBtn.setEnabled( false );
popup = getPopup();
popup.show();
}
updateTable( current );
}
}
/*
* Captures a user selection in the calendar display and
* changes the value in the 'combo box' to match the selected date.
*
*/
private class CalendarSelectionListener implements ListSelectionListener {
public void valueChanged(ListSelectionEvent e){
if ( !e.getValueIsAdjusting() ) {
int row = table.getSelectedRow();
int col = table.getSelectedColumn();
Object value = null;
try{
value = display.getValueAt(row, col);
}catch( ArrayIndexOutOfBoundsException ex ){
// ignore, happens when the calendar is
// displayed for the first time
}
if( value instanceof Integer ){
int day = ( (Integer)value ).intValue();
current.set( Calendar.DATE, day );
input.setValue( current.getTime() );
}
}
}
}
/*
* Captures user input in the 'combo box'
* If the input is a valid date and the user pressed
* ENTER or TAB, the calendar selection is updated
*/
private class InputListener extends KeyAdapter {
public void keyTyped(KeyEvent e) {
DateFormat df = DateFormat.getDateInstance();
Date date = null;
try{
date = df.parse( input.getText() );
}catch( ParseException ex ){
// ignore invalid dates
}
// change the calendar selection if the date is valid
// and the user hit ENTER or TAB
char c = e.getKeyChar();
if( date != null &&
( c == KeyEvent.VK_ENTER || c == KeyEvent.VK_TAB ) ) {
current.setTime( date );
updateTable( current );
}
}
}
}
| CalendarComboBox |