package datecontrol;
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;
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;
/**
* @author Chain
*/
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() {
this(Locale.getDefault());
}
private Timer timer;
/**
* Initializes the date picker with the given locale.
*
* @param locale
* The locale.
*/
public DatePicker(Locale locale) {
calendarView = new CalendarView(locale);
textField = new TextField();
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 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);
}
}