Skip to content

Commit

Permalink
Fix #2806: date editor also accepts incomplete dates (#2816)
Browse files Browse the repository at this point in the history
* Fix #2806: date editor also accepts incomplete dates

* Remove empty line
  • Loading branch information
tobiasdiez authored May 4, 2017
1 parent a16e612 commit ede4dc8
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 29 deletions.
4 changes: 2 additions & 2 deletions src/main/java/org/jabref/gui/fieldeditors/DateEditor.fxml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.DatePicker?>
<?import javafx.scene.layout.HBox?>
<?import org.jabref.gui.util.component.TemporalAccessorPicker?>
<fx:root xmlns:fx="http://javafx.com/fxml/1" type="HBox" xmlns="http://javafx.com/javafx/8.0.112">
<DatePicker fx:id="datePicker" prefHeight="0" HBox.hgrow="ALWAYS"/>
<TemporalAccessorPicker fx:id="datePicker" prefHeight="0" HBox.hgrow="ALWAYS"/>
</fx:root>
6 changes: 3 additions & 3 deletions src/main/java/org/jabref/gui/fieldeditors/DateEditor.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,25 @@

import javafx.fxml.FXML;
import javafx.scene.Parent;
import javafx.scene.control.DatePicker;
import javafx.scene.layout.HBox;

import org.jabref.gui.util.ControlHelper;
import org.jabref.gui.util.component.TemporalAccessorPicker;
import org.jabref.model.entry.BibEntry;

public class DateEditor extends HBox implements FieldEditorFX {

private final String fieldName;
@FXML private DateEditorViewModel viewModel;
@FXML private DatePicker datePicker;
@FXML private TemporalAccessorPicker datePicker;

public DateEditor(String fieldName, DateTimeFormatter dateFormatter) {
this.fieldName = fieldName;
this.viewModel = new DateEditorViewModel(dateFormatter);

ControlHelper.loadFXMLForControl(this);

datePicker.setConverter(viewModel.getDateToStringConverter());
datePicker.setStringConverter(viewModel.getDateToStringConverter());
datePicker.getEditor().textProperty().bindBidirectional(viewModel.textProperty());
}

Expand Down
20 changes: 13 additions & 7 deletions src/main/java/org/jabref/gui/fieldeditors/DateEditorViewModel.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package org.jabref.gui.fieldeditors;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;

import javafx.util.StringConverter;

Expand All @@ -15,11 +16,11 @@ public DateEditorViewModel(DateTimeFormatter dateFormatter) {
this.dateFormatter = dateFormatter;
}

public StringConverter<LocalDate> getDateToStringConverter() {
return new StringConverter<LocalDate>() {
public StringConverter<TemporalAccessor> getDateToStringConverter() {
return new StringConverter<TemporalAccessor>() {

@Override
public String toString(LocalDate date) {
public String toString(TemporalAccessor date) {
if (date != null) {
return dateFormatter.format(date);
} else {
Expand All @@ -28,10 +29,15 @@ public String toString(LocalDate date) {
}

@Override
public LocalDate fromString(String string) {
public TemporalAccessor fromString(String string) {
if (StringUtil.isNotBlank(string)) {
// We accept all kinds of dates (not just in the format specified)
return Date.parse(string).map(Date::toLocalDate).orElse(null);

try {
return dateFormatter.parse(string);
} catch (DateTimeParseException exception) {
// We accept all kinds of dates (not just in the format specified)
return Date.parse(string).map(Date::toTemporalAccessor).orElse(null);
}
} else {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static FieldEditorFX getForField(String fieldName, TaskExecutor taskExecu
// TODO: Implement all of them
if (Globals.prefs.get(JabRefPreferences.TIME_STAMP_FIELD).equals(fieldName) || fieldExtras.contains(FieldProperty.DATE)) {
if (fieldExtras.contains(FieldProperty.ISO_DATE)) {
return new DateEditor(fieldName, DateTimeFormatter.ISO_DATE);
return new DateEditor(fieldName, DateTimeFormatter.ofPattern("[uuuu][-MM][-dd]"));
} else {
return new DateEditor(fieldName, DateTimeFormatter.ofPattern(Globals.prefs.get(JabRefPreferences.TIME_STAMP_FORMAT)));
}
Expand Down
58 changes: 44 additions & 14 deletions src/main/java/org/jabref/gui/util/BindingsHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,34 @@ public String getName() {
pseudoClassState.bind(condition);
}

/**
* Binds propertA bidirectional to propertyB using the provided map functions to convert between them.
*/
public static <A, B> void bindBidirectional(Property<A> propertyA, Property<B> propertyB, Function<A, B> mapAtoB, Function<B, A> mapBtoA) {
Consumer<B> updateA = newValueB -> propertyA.setValue(mapBtoA.apply(newValueB));
Consumer<A> updateB = newValueA -> propertyB.setValue(mapAtoB.apply(newValueA));
bindBidirectional(propertyA, propertyB, updateA, updateB);
}

/**
* Binds propertA bidirectional to propertyB while using updateB to update propertyB when propertyA changed.
*/
public static <A> void bindBidirectional(Property<A> propertyA, ObservableValue<A> propertyB, Consumer<A> updateB) {
final BidirectionalBinding<A> binding = new BidirectionalBinding<>(propertyA, propertyB, updateB);
bindBidirectional(propertyA, propertyB, propertyA::setValue, updateB);
}

/**
* Binds propertA bidirectional to propertyB using updateB to update propertyB when propertyA changed and similar
* for updateA.
*/
public static <A, B> void bindBidirectional(ObservableValue<A> propertyA, ObservableValue<B> propertyB, Consumer<B> updateA, Consumer<A> updateB) {
final BidirectionalBinding<A, B> binding = new BidirectionalBinding<>(propertyA, propertyB, updateA, updateB);

// use updateB as initial source
propertyA.setValue(propertyB.getValue());
updateA.accept(propertyB.getValue());

propertyA.addListener(binding);
propertyB.addListener(binding);
propertyA.addListener(binding.getChangeListenerA());
propertyB.addListener(binding.getChangeListenerB());
}

public static <A, B> void bindContentBidirectional(ListProperty<A> listProperty, Property<B> property, Function<List<A>, B> mapToB, Function<B, List<A>> mapToList) {
Expand All @@ -79,27 +96,40 @@ public static <A, B> void bindContentBidirectional(ListProperty<A> listProperty,
property.addListener(binding);
}

private static class BidirectionalBinding<A> implements ChangeListener<A> {
private static class BidirectionalBinding<A, B> {

private final Property<A> propertyA;
private final ObservableValue<A> propertyA;
private final Consumer<B> updateA;
private final Consumer<A> updateB;
private boolean updating = false;

public BidirectionalBinding(Property<A> propertyA, ObservableValue<A> propertyB, Consumer<A> updateB) {
public BidirectionalBinding(ObservableValue<A> propertyA, ObservableValue<B> propertyB, Consumer<B> updateA, Consumer<A> updateB) {
this.propertyA = propertyA;
this.updateA = updateA;
this.updateB = updateB;
}

@Override
public void changed(ObservableValue<? extends A> observable, A oldValue, A newValue) {
public ChangeListener<? super A> getChangeListenerA() {
return this::changedA;
}

public ChangeListener<? super B> getChangeListenerB() {
return this::changedB;
}

public void changedA(ObservableValue<? extends A> observable, A oldValue, A newValue) {
updateLocked(updateB, oldValue, newValue);
}

public void changedB(ObservableValue<? extends B> observable, B oldValue, B newValue) {
updateLocked(updateA, oldValue, newValue);
}

private <T> void updateLocked(Consumer<T> update, T oldValue, T newValue) {
if (!updating) {
try {
updating = true;
if (observable == propertyA) {
updateB.accept(newValue);
} else {
propertyA.setValue(newValue);
}
update.accept(newValue);
} finally {
updating = false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
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);
}
}
}
4 changes: 2 additions & 2 deletions src/main/java/org/jabref/model/entry/Date.java
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ public Optional<Integer> getDay() {
return get(ChronoField.DAY_OF_MONTH);
}

public LocalDate toLocalDate() {
return LocalDate.from(date);
public TemporalAccessor toTemporalAccessor() {
return date;
}

@Override
Expand Down

0 comments on commit ede4dc8

Please sign in to comment.