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 ); } }