package org.jabref.gui.util.component; import java.time.DateTimeException; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.Year; import java.time.YearMonth; import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalQueries; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.scene.control.DatePicker; import javafx.util.StringConverter; import org.jabref.gui.util.BindingsHelper; /** * A date picker with configurable datetime format where both date and time can be changed via the text field and the * date can additionally be changed via the JavaFX default date picker. Also supports incomplete dates. * * First recall how the date picker normally works: - The user selects a date in the popup, which sets {@link * #valueProperty()} to the selected date. - The converter ({@link #converterProperty()}) is used to transform the date * to a string representation and display it in the text field. * * The idea is now to intercept the process and add an additional step: - The user selects a date in the popup, which * sets {@link #valueProperty()} to the selected date. - The date is converted to a {@link TemporalAccessor} (i.e, * enriched by a time component) using {@link #addCurrentTime(LocalDate)} - The string converter ({@link * #stringConverterProperty()}) is used to transform the temporal accessor to a string representation and display it in * the text field. * * Inspiration taken from https://github.com/edvin/tornadofx-controls/blob/master/src/main/java/tornadofx/control/DateTimePicker.java */ public class TemporalAccessorPicker extends DatePicker { private ObjectProperty<TemporalAccessor> temporalAccessorValue = new SimpleObjectProperty<>(LocalDateTime.now()); private DateTimeFormatter defaultFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); private ObjectProperty<StringConverter<TemporalAccessor>> converter = new SimpleObjectProperty<StringConverter<TemporalAccessor>>(null); public TemporalAccessorPicker() { setConverter(new InternalConverter()); // Synchronize changes of the underlying date value with the temporalAccessorValue BindingsHelper.bindBidirectional(valueProperty(), temporalAccessorValue, TemporalAccessorPicker::addCurrentTime, TemporalAccessorPicker::getDate); } private static TemporalAccessor addCurrentTime(LocalDate date) { if (date == null) { return null; } return LocalDateTime.of(date, LocalTime.now()); } private static LocalDate getDate(TemporalAccessor temporalAccessor) { if (temporalAccessor == null) { return null; } return getLocalDate(temporalAccessor); } private static LocalDate getLocalDate(TemporalAccessor dateTime) { // Try to get as much information from the temporal accessor LocalDate date = dateTime.query(TemporalQueries.localDate()); if (date != null) { return date; } try { return YearMonth.from(dateTime).atDay(1); } catch (DateTimeException exception) { return Year.from(dateTime).atDay(1); } } public final ObjectProperty<StringConverter<TemporalAccessor>> stringConverterProperty() { return converter; } public final StringConverter<TemporalAccessor> getStringConverter() { StringConverter<TemporalAccessor> converter = stringConverterProperty().get(); if (converter != null) { return converter; } else { return new StringConverter<TemporalAccessor>() { @Override public String toString(TemporalAccessor value) { return defaultFormatter.format(value); } @Override public TemporalAccessor fromString(String value) { return LocalDateTime.parse(value, defaultFormatter); } }; } } public final void setStringConverter(StringConverter<TemporalAccessor> value) { stringConverterProperty().set(value); } public TemporalAccessor getTemporalAccessorValue() { return temporalAccessorValue.get(); } public void setTemporalAccessorValue(TemporalAccessor temporalAccessorValue) { this.temporalAccessorValue.set(temporalAccessorValue); } public ObjectProperty<TemporalAccessor> temporalAccessorValueProperty() { return temporalAccessorValue; } private class InternalConverter extends StringConverter<LocalDate> { public String toString(LocalDate object) { TemporalAccessor value = getTemporalAccessorValue(); return (value != null) ? getStringConverter().toString(value) : ""; } public LocalDate fromString(String value) { if (value == null || value.isEmpty()) { temporalAccessorValue.set(null); return null; } TemporalAccessor dateTime = getStringConverter().fromString(value); temporalAccessorValue.set(dateTime); return getLocalDate(dateTime); } } }