/*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* Copyright (c) 2001 - 2013 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved.
*/
package org.pentaho.reporting.libraries.designtime.swing.date;
import org.pentaho.reporting.libraries.base.util.ObjectUtilities;
import org.pentaho.reporting.libraries.designtime.swing.Messages;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.DateFormatSymbols;
import java.util.Calendar;
import java.util.Date;
/**
* A panel that allows the user to select a date.
*
* @author David Gilbert
* @noinspection UnnecessaryBoxing
*/
public class DateChooserPanel extends JPanel {
private class MonthSelectionAction implements ActionListener {
private MonthSelectionAction() {
}
/**
* Invoked when an action occurs.
*/
public void actionPerformed( final ActionEvent e ) {
final JComboBox c = (JComboBox) e.getSource();
if ( !refreshing ) {
// In most cases, changing the month will not change the selected
// day. But if the selected day is 29, 30 or 31 and the newly
// selected month doesn't have that many days, we revert to the
// last day of the newly selected month...
final int dayOfMonth = dateView.get( Calendar.DAY_OF_MONTH );
dateView.set( Calendar.DAY_OF_MONTH, 1 );
dateView.set( Calendar.MONTH, c.getSelectedIndex() );
final int maxDayOfMonth = dateView.getActualMaximum( Calendar.DAY_OF_MONTH );
dateView.set( Calendar.DAY_OF_MONTH, Math.min( dayOfMonth, maxDayOfMonth ) );
setDateSelected( false );
if ( yearOrMonthChangeSelectsDate ) {
setDate( dateView.getTime() );
} else {
selectedDate = dateView.getTime();
refreshButtons();
}
}
}
}
private class YearSelectionAction implements ActionListener {
private YearSelectionAction() {
}
/**
* Invoked when an action occurs.
*/
public void actionPerformed( final ActionEvent e ) {
if ( !refreshing ) {
final JComboBox c = (JComboBox) e.getSource();
final String yT = (String) c.getSelectedItem();
if ( yT == null ) {
return;
}
try {
final int y = Integer.parseInt( yT );
// in most cases, changing the year will not change the
// selected day. But if the selected day is Feb 29, and the
// newly selected year is not a leap year, we revert to
// Feb 28...
final int dayOfMonth = dateView.get( Calendar.DAY_OF_MONTH );
dateView.set( Calendar.DAY_OF_MONTH, 1 );
dateView.set( Calendar.YEAR, y );
final int maxDayOfMonth = dateView.getActualMaximum( Calendar.DAY_OF_MONTH );
dateView.set( Calendar.DAY_OF_MONTH, Math.min( dayOfMonth, maxDayOfMonth ) );
setDateSelected( false );
if ( yearOrMonthChangeSelectsDate ) {
setDate( dateView.getTime() );
refreshYearSelector();
} else {
selectedDate = dateView.getTime();
refreshYearSelector();
refreshButtons();
}
} catch ( NumberFormatException nf ) {
// ignore user input ..
}
}
}
}
private class SelectTodayAction extends AbstractAction {
/**
* Defines an <code>Action</code> object with a default description string and default icon.
*/
private SelectTodayAction() {
putValue( Action.NAME, Messages.getInstance().getString( "DateChooserPanel.Today" ) );
}
/**
* Invoked when an action occurs.
*/
public void actionPerformed( final ActionEvent e ) {
setDateSelected( true );
setDate( new Date() );
}
}
private class SelectDayAction extends AbstractAction {
private Date number;
/**
* Defines an <code>Action</code> object with a default description string and default icon.
*/
private SelectDayAction( final Date selectedDate, final String number ) {
this.number = selectedDate;
putValue( Action.NAME, number );
}
/**
* Invoked when an action occurs.
*/
public void actionPerformed( final ActionEvent e ) {
setDateSelected( true );
setDate( number );
}
}
public final static String PROPERTY_DATE = "date";
/**
* The date used to display the current selection in the panel. This date cannot be null, while the selected date can
* be null.
*/
private Calendar dateView;
private Date selectedDate;
/**
* The color for the selected date.
*/
private Color chosenDateButtonColor;
/**
* The color for dates in the current month.
*/
private Color chosenMonthButtonColor;
/**
* The color for dates that are visible, but not in the current month.
*/
private Color chosenOtherButtonColor;
/**
* The first day-of-the-week.
*/
private int firstDayOfWeek;
/**
* The range used for selecting years.
*/
private int yearSelectionRange = 20;
/**
* The font used to display the date.
*/
private Font dateFont = new Font( "SansSerif", Font.PLAIN, 10 ); //$NON-NLS-1$
/**
* A combo for selecting the month.
*/
private JComboBox monthSelector;
/**
* A combo for selecting the year.
*/
private JComboBox yearSelector;
/**
* An array of buttons used to display the days-of-the-month.
*/
private JButton[] buttons;
/**
* A flag that indicates whether or not we are currently refreshing the buttons.
*/
private boolean refreshing = false;
/**
* The ordered set of all seven days of a week, beginning with the 'firstDayOfWeek'.
*/
private int[] WEEK_DAYS;
private boolean dateSelected;
private boolean yearOrMonthChangeSelectsDate;
/**
* Constructs a new date chooser panel, using today's date as the initial selection.
*/
public DateChooserPanel() {
this( Calendar.getInstance(), false );
}
/**
* Constructs a new date chooser panel.
*
* @param calendar the calendar controlling the date.
* @param controlPanel a flag that indicates whether or not the 'today' button should appear on the panel.
*/
public DateChooserPanel( final Calendar calendar,
final boolean controlPanel ) {
super( new BorderLayout() );
this.chosenDateButtonColor = SystemColor.textHighlight; //$NON-NLS-1$
this.chosenMonthButtonColor = SystemColor.control; //$NON-NLS-1$
this.chosenOtherButtonColor = SystemColor.controlShadow; //$NON-NLS-1$
// the default date is today...
this.dateView = calendar;
this.firstDayOfWeek = calendar.getFirstDayOfWeek();
this.WEEK_DAYS = new int[ 7 ];
for ( int i = 0; i < 7; i++ ) {
this.WEEK_DAYS[ i ] = ( ( this.firstDayOfWeek + i - 1 ) % 7 ) + 1;
}
add( constructSelectionPanel(), BorderLayout.NORTH );
add( getCalendarPanel(), BorderLayout.CENTER );
if ( controlPanel ) {
add( constructControlPanel(), BorderLayout.SOUTH );
}
setDateSelected( false );
setDate( calendar.getTime() );
}
public boolean isDateSelected() {
return dateSelected;
}
public void setDateSelected( final boolean dateSeleccted ) {
this.dateSelected = dateSeleccted;
}
/**
* Sets the date chosen in the panel.
*
* @param theDate the new date.
*/
public void setDate( final Date theDate, boolean firePC ) {
final Date oldDate = this.selectedDate;
this.selectedDate = theDate;
if ( theDate != null ) {
this.dateView.setTime( theDate );
refreshing = true;
this.monthSelector.setSelectedIndex( this.dateView.get( Calendar.MONTH ) );
refreshing = false;
refreshYearSelector();
refreshButtons();
}
if ( firePC ) {
if ( ObjectUtilities.equal( oldDate, theDate ) ) {
firePropertyChange( PROPERTY_DATE, null, theDate );
} else {
firePropertyChange( PROPERTY_DATE, oldDate, theDate );
}
}
}
public void setDate( final Date theDate ) {
setDate( theDate, true );
}
/**
* Returns the date selected in the panel.
*
* @return the selected date.
*/
public Date getDate() {
return selectedDate;
}
/**
* Returns a panel of buttons, each button representing a day in the month. This is a sub-component of the DatePanel.
*
* @return the panel.
*/
private JPanel getCalendarPanel() {
final JPanel p = new JPanel( new GridLayout( 7, 7 ) );
final DateFormatSymbols dateFormatSymbols = new DateFormatSymbols();
final String[] weekDays = dateFormatSymbols.getShortWeekdays();
for ( int i = 0; i < this.WEEK_DAYS.length; i++ ) {
p.add( new JLabel( weekDays[ this.WEEK_DAYS[ i ] ], SwingConstants.CENTER ) );
}
this.buttons = new JButton[ 42 ];
for ( int i = 0; i < 42; i++ ) {
final JButton b = new JButton( "" );
b.setMargin( new Insets( 1, 1, 1, 1 ) );
b.setName( Integer.toString( i ) );
b.setFont( this.dateFont );
b.setFocusPainted( false );
b.putClientProperty( "JButton.buttonType", "square" ); //$NON-NLS-1$ $NON-NLS-2$
this.buttons[ i ] = b;
p.add( b );
}
return p;
}
/**
* Returns the button color according to the specified date.
*
* @param theDate the date.
* @return the color.
*/
private Color getButtonBackgroundColor( final Calendar theDate ) {
if ( equalDates( theDate, this.dateView ) ) {
return this.chosenDateButtonColor;
} else if ( theDate.get( Calendar.MONTH ) == this.dateView.get( Calendar.MONTH ) ) {
return this.chosenMonthButtonColor;
} else {
return this.chosenOtherButtonColor;
}
}
/**
* Returns true if the two dates are equal (time of day is ignored).
*
* @param c1 the first date.
* @param c2 the second date.
* @return boolean.
*/
private boolean equalDates( final Calendar c1, final Calendar c2 ) {
if ( ( c1.get( Calendar.DATE ) == c2.get( Calendar.DATE ) )
&& ( c1.get( Calendar.MONTH ) == c2.get( Calendar.MONTH ) )
&& ( c1.get( Calendar.YEAR ) == c2.get( Calendar.YEAR ) ) ) {
return true;
} else {
return false;
}
}
/**
* Returns the first date that is visible in the grid. This should always be in the month preceding the month of the
* selected date.
*
* @return the date.
*/
private Calendar getFirstVisibleDate() {
final Calendar c = Calendar.getInstance();
c.set( this.dateView.get( Calendar.YEAR ), this.dateView.get(
Calendar.MONTH ), 1 );
c.add( Calendar.DATE, -1 );
while ( c.get( Calendar.DAY_OF_WEEK ) != getFirstDayOfWeek() ) {
c.add( Calendar.DATE, -1 );
}
c.set( Calendar.HOUR_OF_DAY, 0 );
c.set( Calendar.MINUTE, 0 );
c.set( Calendar.SECOND, 0 );
c.set( Calendar.MILLISECOND, 0 );
c.set( Calendar.ZONE_OFFSET, 3 );
return c;
}
/**
* Returns the first day of the week (controls the labels in the date panel).
*
* @return the first day of the week.
*/
private int getFirstDayOfWeek() {
return this.firstDayOfWeek;
}
/**
* Update the button labels and colors to reflect date selection.
*/
private void refreshButtons() {
final Calendar c = getFirstVisibleDate();
for ( int i = 0; i < 42; i++ ) {
final JButton b = this.buttons[ i ];
b.setAction( new SelectDayAction( c.getTime(), String.valueOf( c.get( Calendar.DATE ) ) ) );
b.setBackground( getButtonBackgroundColor( c ) );
b.setFont( getButtonFontStyle( c ) );
c.add( Calendar.DATE, 1 );
}
}
private Font getButtonFontStyle( final Calendar theDate ) {
if ( equalDates( theDate, this.dateView ) ) {
return this.dateFont.deriveFont( Font.BOLD );
} else if ( theDate.get( Calendar.MONTH ) == this.dateView.get( Calendar.MONTH ) ) {
return this.dateFont;
} else {
return this.dateFont.deriveFont( Font.ITALIC );
}
}
/**
* Changes the contents of the year selection JComboBox to reflect the chosen date and the year range.
*/
private void refreshYearSelector() {
if ( !this.refreshing ) {
this.refreshing = true;
this.yearSelector.removeAllItems();
final String[] years = getYears( this.dateView.get( Calendar.YEAR ) );
for ( int i = 0; i < years.length; i++ ) {
this.yearSelector.addItem( years[ i ] );
}
this.yearSelector.setSelectedItem( String.valueOf( this.dateView.get( Calendar.YEAR ) ) );
this.refreshing = false;
}
}
/**
* Returns a vector of years preceding and following the specified year. The number of years preceding and following
* is determined by the yearSelectionRange attribute.
*
* @param chosenYear the selected year.
* @return a vector of years.
*/
private String[] getYears( final int chosenYear ) {
final int size = this.yearSelectionRange * 2 + 1;
final int start = chosenYear - this.yearSelectionRange;
final String[] years = new String[ size ];
for ( int i = 0; i < size; i++ ) {
years[ i ] = String.valueOf( i + start );
}
return years;
}
/**
* Constructs a panel containing two JComboBoxes (for the month and year) and a button (to reset the date to TODAY).
*
* @return the panel.
*/
private JPanel constructSelectionPanel() {
final JPanel p = new JPanel();
final int minMonth = this.dateView.getMinimum( Calendar.MONTH );
final int maxMonth = this.dateView.getMaximum( Calendar.MONTH );
final String[] months = new String[ maxMonth - minMonth + 1 ];
System.arraycopy( new DateFormatSymbols().getMonths(), minMonth, months, 0, months.length );
this.monthSelector = new JComboBox( months );
this.monthSelector.addActionListener( new MonthSelectionAction() );
p.add( this.monthSelector );
this.yearSelector = new JComboBox( getYears( 0 ) );
this.yearSelector.setEditable( true );
this.yearSelector.addActionListener( new YearSelectionAction() );
p.add( this.yearSelector );
return p;
}
/**
* Returns a panel that appears at the bottom of the calendar panel - contains a button for selecting today's date.
*
* @return the panel.
*/
private JPanel constructControlPanel() {
final JPanel p = new JPanel();
p.setBorder( BorderFactory.createEmptyBorder( 2, 5, 2, 5 ) );
p.add( new JButton( new SelectTodayAction() ) );
return p;
}
/**
* Returns the color for the currently selected date.
*
* @return a color.
*/
public Color getChosenDateButtonColor() {
return this.chosenDateButtonColor;
}
/**
* Redefines the color for the currently selected date.
*
* @param chosenDateButtonColor the new color
*/
public void setChosenDateButtonColor( final Color chosenDateButtonColor ) {
if ( chosenDateButtonColor == null ) {
throw new NullPointerException( "UIColor must not be null." );
}
final Color oldValue = this.chosenDateButtonColor;
this.chosenDateButtonColor = chosenDateButtonColor;
refreshButtons();
firePropertyChange( "chosenDateButtonColor", oldValue, chosenDateButtonColor ); //$NON-NLS-1$
}
/**
* Returns the color for the buttons representing the current month.
*
* @return the color for the current month.
*/
public Color getChosenMonthButtonColor() {
return this.chosenMonthButtonColor;
}
/**
* Defines the color for the buttons representing the current month.
*
* @param chosenMonthButtonColor the color for the current month.
*/
public void setChosenMonthButtonColor( final Color chosenMonthButtonColor ) {
if ( chosenMonthButtonColor == null ) {
throw new NullPointerException( "UIColor must not be null." );
}
final Color oldValue = this.chosenMonthButtonColor;
this.chosenMonthButtonColor = chosenMonthButtonColor;
refreshButtons();
firePropertyChange( "chosenMonthButtonColor", oldValue, chosenMonthButtonColor ); //$NON-NLS-1$
}
/**
* Returns the color for the buttons representing the other months.
*
* @return a color.
*/
public Color getChosenOtherButtonColor() {
return this.chosenOtherButtonColor;
}
/**
* Redefines the color for the buttons representing the other months.
*
* @param chosenOtherButtonColor a color.
*/
public void setChosenOtherButtonColor( final Color chosenOtherButtonColor ) {
if ( chosenOtherButtonColor == null ) {
throw new NullPointerException( "UIColor must not be null." );
}
final Color oldValue = this.chosenOtherButtonColor;
this.chosenOtherButtonColor = chosenOtherButtonColor;
refreshButtons();
firePropertyChange( "chosenOtherButtonColor", oldValue, chosenOtherButtonColor ); //$NON-NLS-1$
}
/**
* Returns the range of years available for selection (defaults to 20).
*
* @return The range.
*/
public int getYearSelectionRange() {
return this.yearSelectionRange;
}
/**
* Sets the range of years available for selection.
*
* @param yearSelectionRange the range.
*/
public void setYearSelectionRange( final int yearSelectionRange ) {
final int oldYearSelectionRange = this.yearSelectionRange;
this.yearSelectionRange = yearSelectionRange;
refreshYearSelector();
firePropertyChange( "yearSelectionRange", oldYearSelectionRange, yearSelectionRange ); //$NON-NLS-1$
}
}