package edu.uva.softwarecons.ui.widget;
import eu.schudt.javafx.controls.calendar.CalendarView;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.effect.DropShadow;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.stage.Popup;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
/**
* Falconlabs
*
* @author Santiago Carrillo
* Date: 3/16/14
*/
public class DatePicker
extends HBox
{
private static final String CSS_DATE_PICKER_VALID = "datepicker-valid";
private static final String CSS_DATE_PICKER_INVALID = "datepicker-invalid";
/**
* Initializes the date picker with the default locale.
*/
public DatePicker( EventHandler<ActionEvent> eventHandler, String id )
{
this( Locale.getDefault(), eventHandler, id );
}
private Timer timer;
private final EventHandler<ActionEvent> eventHandler;
/**
* Initializes the date picker with the given locale.
*
* @param locale The locale.
*/
public DatePicker( Locale locale, EventHandler<ActionEvent> eventHandler, String id )
{
calendarView = new CalendarView( locale );
textField = new TextField();
textField.setId( id );
this.eventHandler = eventHandler;
textField.setEditable( false );
this.locale.set( locale );
calendarView.setEffect( new DropShadow() );
// Use the same locale.
calendarView.localeProperty().bind( localeProperty() );
// Bind the current date of the calendar view with the selected date, so that the calendar shows up with the same month as in the text field.
calendarView.currentDateProperty().bind( selectedDateProperty() );
// When the user selects a date in the calendar view, hide it.
calendarView.selectedDateProperty().addListener( new InvalidationListener()
{
@Override
public void invalidated( Observable observable )
{
selectedDate.set( calendarView.selectedDateProperty().get() );
hidePopup();
}
} );
// Let the prompt text property listen to locale or date format changes.
textField.promptTextProperty().bind( new StringBinding()
{
{
super.bind( localeProperty(), promptTextProperty(), dateFormatProperty() );
}
@Override
protected String computeValue()
{
// First check, if there is a custom prompt text.
if ( promptTextProperty().get() != null )
{
return promptTextProperty().get();
}
// If not, use the the date format's pattern.
DateFormat dateFormat = getActualDateFormat();
if ( dateFormat instanceof SimpleDateFormat )
{
return ( (SimpleDateFormat) dateFormat ).toPattern();
}
return "";
}
} );
// Change the CSS styles, when this control becomes invalid.
invalid.addListener( new InvalidationListener()
{
@Override
public void invalidated( Observable observable )
{
if ( invalid.get() )
{
textField.getStyleClass().add( CSS_DATE_PICKER_INVALID );
textField.getStyleClass().remove( CSS_DATE_PICKER_VALID );
}
else
{
textField.getStyleClass().remove( CSS_DATE_PICKER_INVALID );
textField.getStyleClass().add( CSS_DATE_PICKER_VALID );
}
}
} );
// When the text field no longer has the focus, try to parse the date.
textField.addEventHandler( MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>()
{
@Override
public void handle( MouseEvent event )
{
if ( !textField.focusedProperty().get() )
{
if ( !textField.getText().equals( "" ) )
{
tryParse( true );
}
}
else
{
showPopup();
}
}
} );
// Listen to user input.
textField.textProperty().addListener( new ChangeListener<String>()
{
@Override
public void changed( ObservableValue<? extends String> observableValue, String s, String s1 )
{
// Only evaluate the input, it it wasn't set programmatically.
if ( textSetProgrammatically )
{
return;
}
if ( timer != null )
{
timer.cancel();
}
// If the user clears the text field, set the date to null and the field to valid.
if ( s1.equals( "" ) )
{
selectedDate.set( null );
invalid.set( false );
}
else
{
// Start a timer, so that the user input is not evaluated immediately, but after a second.
// This way, input like 01/01/1 is not immediately parsed as 01/01/01.
// The user gets one second time, to complete his date, maybe his intention was to enter 01/01/12.
timer = new Timer();
timer.schedule( new TimerTask()
{
@Override
public void run()
{
Platform.runLater( new Runnable()
{
@Override
public void run()
{
tryParse( false );
}
} );
}
}, 1000 );
}
}
} );
selectedDateProperty().addListener( new InvalidationListener()
{
@Override
public void invalidated( Observable observable )
{
updateTextField();
invalid.set( false );
}
} );
localeProperty().addListener( new InvalidationListener()
{
@Override
public void invalidated( Observable observable )
{
updateTextField();
}
} );
textField.addEventHandler( KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>()
{
@Override
public void handle( KeyEvent keyEvent )
{
if ( keyEvent.getCode() == KeyCode.DOWN )
{
showPopup();
}
}
} );
Button button = new Button( ">" );
button.setFocusTraversable( false );
button.setOnAction( new EventHandler<ActionEvent>()
{
@Override
public void handle( ActionEvent actionEvent )
{
showPopup();
}
} );
getChildren().add( textField );
//getChildren().add(button);
////////////////////////////////////////////////////////////
// Lines added by Marco Jakob
////////////////////////////////////////////////////////////
HBox.setHgrow( textField, Priority.ALWAYS );
// Pass style sheet changes to underlying CalendarView
getStylesheets().addListener( new InvalidationListener()
{
@Override
public void invalidated( Observable observable )
{
calendarView.getStylesheets().setAll( getStylesheets() );
}
} );
}
private void hidePopup()
{
if ( popup != null )
{
popup.hide();
}
}
/**
* Tries to parse the text field for a valid date.
*
* @param setDateToNullOnException True, if the date should be set to null, when a {@link java.text.ParseException} occurs.
* This is the case, when the text field loses focus.
*/
private void tryParse( boolean setDateToNullOnException )
{
if ( timer != null )
{
timer.cancel();
}
try
{
// Double parse the date here, since e.g. 01.01.1 is parsed as year 1, and then formatted as 01.01.01 and then parsed as year 2001.
// This might lead to an undesired date.
DateFormat dateFormat = getActualDateFormat();
Date parsedDate = dateFormat.parse( textField.getText() );
parsedDate = dateFormat.parse( dateFormat.format( parsedDate ) );
if ( selectedDate.get() == null
|| selectedDate.get() != null && parsedDate.getTime() != selectedDate.get().getTime() )
{
selectedDate.set( parsedDate );
}
invalid.set( false );
updateTextField();
}
catch ( ParseException e )
{
invalid.set( true );
if ( setDateToNullOnException )
{
selectedDate.set( null );
}
}
}
private boolean textSetProgrammatically;
/**
* Updates the text field.
*/
private void updateTextField()
{
// Mark the we update the text field (and not the user), so that it can be ignored, by textField.textProperty()
textSetProgrammatically = true;
if ( selectedDateProperty().get() != null )
{
String date = getActualDateFormat().format( selectedDateProperty().get() );
if ( !textField.getText().equals( date ) )
{
textField.setText( date );
eventHandler.handle( new ActionEvent( textField, null ) );
}
}
else
{
textField.setText( "" );
}
textSetProgrammatically = false;
}
/**
* Gets the actual date format. If {@link #dateFormatProperty()} is set, take it, otherwise get a default format for the current locale.
*
* @return The date format.
*/
private DateFormat getActualDateFormat()
{
if ( dateFormat.get() != null )
{
return dateFormat.get();
}
DateFormat format = DateFormat.getDateInstance( DateFormat.SHORT, locale.get() );
format.setCalendar( calendarView.getCalendar() );
format.setLenient( false );
return format;
}
private CalendarView calendarView;
/**
* Use this to set further properties of the calendar.
*
* @return The calendar view.
*/
public CalendarView getCalendarView()
{
return calendarView;
}
private TextField textField;
private BooleanProperty invalid = new SimpleBooleanProperty();
/**
* States whether the user input is invalid (is no valid date).
*
* @return The property.
*/
public ReadOnlyBooleanProperty invalidProperty()
{
return invalid;
}
/**
* The locale.
*
* @return The property.
*/
public ObjectProperty<Locale> localeProperty()
{
return locale;
}
private ObjectProperty<Locale> locale = new SimpleObjectProperty<Locale>();
public void setLocale( Locale locale )
{
this.locale.set( locale );
}
public Locale getLocale()
{
return locale.get();
}
/**
* The selected date.
*
* @return The property.
*/
public ObjectProperty<Date> selectedDateProperty()
{
return selectedDate;
}
private ObjectProperty<Date> selectedDate = new SimpleObjectProperty<Date>();
public void setSelectedDate( Date date )
{
this.selectedDate.set( date );
}
public Date getSelectedDate()
{
return selectedDate.get();
}
/**
* Gets the date format.
*
* @return The date format.
*/
public ObjectProperty<DateFormat> dateFormatProperty()
{
return dateFormat;
}
private ObjectProperty<DateFormat> dateFormat = new SimpleObjectProperty<DateFormat>();
public void setDateFormat( DateFormat dateFormat )
{
this.dateFormat.set( dateFormat );
}
public DateFormat getDateFormat()
{
return dateFormat.get();
}
private StringProperty promptText = new SimpleStringProperty();
/**
* The prompt text for the text field.
* By default, the prompt text is taken from the date format pattern.
*
* @return The property.
*/
public StringProperty promptTextProperty()
{
return promptText;
}
public void setPromptText( String promptText )
{
this.promptText.set( promptText );
}
public String getPromptText()
{
return promptText.get();
}
private Popup popup;
/**
* Shows the pop up.
*/
private void showPopup()
{
if ( popup == null )
{
popup = new Popup();
popup.setAutoHide( true );
popup.setHideOnEscape( true );
popup.setAutoFix( true );
popup.getContent().add( calendarView );
}
Bounds calendarBounds = calendarView.getBoundsInLocal();
Bounds bounds = localToScene( getBoundsInLocal() );
double posX = calendarBounds.getMinX() + bounds.getMinX() + getScene().getX() + getScene().getWindow().getX();
double posY = calendarBounds.getMinY() + bounds.getHeight() + bounds.getMinY() + getScene().getY()
+ getScene().getWindow().getY();
popup.show( this, posX, posY );
}
}