/*
* -----------------------------------------------------------------------
* Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/>
* -----------------------------------------------------------------------
* This file (CalendarPicker.java) is part of project Time4J.
*
* Time4J is free software: You can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 2.1 of the License, or
* (at your option) any later version.
*
* Time4J is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Time4J. If not, see <http://www.gnu.org/licenses/>.
* -----------------------------------------------------------------------
*/
package net.time4j.ui.javafx;
import javafx.animation.Animation;
import javafx.animation.FadeTransition;
import javafx.animation.Interpolator;
import javafx.animation.ScaleTransition;
import javafx.beans.Observable;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.stage.Popup;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
import javafx.util.Duration;
import net.time4j.PlainDate;
import net.time4j.SystemClock;
import net.time4j.ZonalClock;
import net.time4j.calendar.HijriCalendar;
import net.time4j.calendar.MinguoCalendar;
import net.time4j.calendar.PersianCalendar;
import net.time4j.calendar.ThaiSolarCalendar;
import net.time4j.engine.CalendarDate;
import net.time4j.engine.CalendarFamily;
import net.time4j.engine.CalendarVariant;
import net.time4j.engine.Calendrical;
import net.time4j.engine.Chronology;
import net.time4j.engine.StartOfDay;
import net.time4j.engine.TimeAxis;
import net.time4j.engine.VariantSource;
import net.time4j.format.Attributes;
import net.time4j.format.DisplayMode;
import net.time4j.format.Leniency;
import net.time4j.format.expert.ChronoFormatter;
import net.time4j.format.expert.ParseLog;
import net.time4j.format.expert.PatternType;
import java.util.Locale;
import java.util.function.Supplier;
/**
* <p>Represents a combination of a text editor and a button which can open a calendar view for picking
* any arbitrary calendar date. </p>
*
* @param <T> denotes the calendar system to be used
* @author Meno Hochschild
* @since 4.20
* @doctags.concurrency {mutable}
*/
/*[deutsch]
* <p>Repräsentiert eine Kombination aus einem Texteditor und einer Schaltfläche, die
* einen grafischen Kalender zur Auswahl eines beliebigen Datums öffnen kann. </p>
*
* @param <T> denotes the calendar system to be used
* @author Meno Hochschild
* @since 4.20
* @doctags.concurrency {mutable}
*/
public class CalendarPicker<T extends CalendarDate>
extends HBox {
//~ Statische Felder/Initialisierungen --------------------------------
private static final Duration STD_ANIMATION_TIME = Duration.seconds(0.5);
private static final String CSS_CALENDAR_EDITOR_ERROR = "calendar-editor-error";
private static final String CSS_CALENDAR_DIALOG_START = "calendar-dialog-start";
//~ Instanzvariablen --------------------------------------------------
private TextField textField;
private Button popupButton;
private Popup popupDialog;
private CalendarControl<T> control;
private FXCalendarSystem<T> calsys;
private boolean committingText = false;
private boolean textInChange = false;
private boolean valueInChange = false;
private boolean selectionInChange = false;
private ObjectProperty<T> valuePropertyInt = new SimpleObjectProperty<>(this, "VALUE-INTERNAL");
private ObjectProperty<T> valuePropertyExt = new SimpleObjectProperty<>(this, "VALUE");
private StringProperty errorProperty = new SimpleStringProperty(this, "ERROR");
private ObjectProperty<ChronoFormatter<T>> formatProperty = new SimpleObjectProperty<>(this, "DATE-FORMAT");
private StringProperty promptProperty = new SimpleStringProperty(this, "PROMPT-TEXT");
//~ Konstruktoren -----------------------------------------------------
private CalendarPicker(
FXCalendarSystem<T> calsys,
Locale locale,
Supplier<T> todaySupplier,
Chronology<T> chronology,
T minDate,
T maxDate
) {
super();
this.control = new CalendarControl<>(locale, todaySupplier, chronology, minDate, maxDate);
this.calsys = calsys;
// initialization of properties
this.control.selectedDateProperty().setValue(null);
this.control.pageDateProperty().setValue(todaySupplier.get());
this.valuePropertyInt.setValue(null);
this.errorProperty.setValue(null);
this.formatProperty.setValue(null);
this.promptProperty.setValue(null);
this.setShowWeeks(true);
this.setLengthOfAnimations(STD_ANIMATION_TIME);
// our components
this.getStylesheets().add("/net/time4j/ui/javafx/calendar.css");
this.textField = new TextField();
this.popupButton = this.createPopupButton();
this.popupDialog = null;
this.getChildren().add(this.textField);
this.getChildren().add(this.popupButton);
HBox.setHgrow(this.textField, Priority.ALWAYS);
// listeners and bindings
this.control.selectedDateProperty().addListener(
(observable, oldValue, newValue) -> {
control.pageDateProperty().setValue((newValue == null) ? control.today() : newValue);
if (!valueInChange) {
selectionInChange = true;
valuePropertyInt.setValue(newValue);
selectionInChange = false;
if (!committingText) {
hidePopup();
}
}
}
);
this.valuePropertyInt.addListener(
(observable, oldValue, newValue) -> {
valueInChange = true;
if (!committingText) {
errorProperty.setValue(null);
if (!selectionInChange) {
control.selectedDateProperty().setValue(newValue);
}
updateTextField();
}
valuePropertyExt.setValue(newValue);
valueInChange = false;
}
);
this.valuePropertyExt.addListener(
(observable, oldValue, newValue) -> {
if (
(newValue != null)
&& (newValue.isBefore(control.minDateProperty().get())
|| newValue.isAfter(control.maxDateProperty().get()))
) {
throw new IllegalArgumentException("Out of range: " + newValue);
} else if (!valueInChange) {
valuePropertyInt.setValue(newValue);
}
}
);
this.errorProperty.addListener(
observable -> {
if (errorProperty.getValue() == null) {
textField.getStyleClass().remove(CSS_CALENDAR_EDITOR_ERROR);
textField.setTooltip(null);
} else {
if (!textField.getStyleClass().contains(CSS_CALENDAR_EDITOR_ERROR)) {
textField.getStyleClass().add(CSS_CALENDAR_EDITOR_ERROR);
}
textField.setTooltip(new Tooltip(errorProperty.getValue()));
}
}
);
this.textField.promptTextProperty().bind(new PromptBinding());
this.textField.minHeightProperty().bind(this.minHeightProperty());
this.textField.maxHeightProperty().bind(this.maxHeightProperty());
this.textField.textProperty().addListener( // CLEAR
(observable, oldValue, newValue) -> {
if (!textInChange) {
if ((newValue == null) || newValue.isEmpty()) {
committingText = true;
control.selectedDateProperty().setValue(null);
errorProperty.setValue(null);
committingText = false;
} else {
textField.setTooltip(null);
}
}
}
);
this.textField.focusedProperty().addListener( // FOCUS-LOST
observable -> {
if (!textField.isFocused()) {
commitTextInput();
}
}
);
this.textField.setOnAction( // ENTER
event -> {
hidePopup();
this.popupButton.requestFocus();
if (this.textField.isFocused()) {
commitTextInput();
}
}
);
this.control.localeProperty().addListener(
observable -> {
updateTextField();
}
);
}
//~ Methoden ----------------------------------------------------------
/**
* <p>Creates a new {@code CalendarPicker} for the gregorian calendar system using system defaults
* for the locale and the current local time. </p>
*
* @return CalendarPicker
* @see Locale#getDefault(Locale.Category) Locale.getDefault(Locale.Category.FORMAT)
* @see SystemClock#inLocalView()
* @see ZonalClock#today()
*/
/*[deutsch]
* <p>Erzeugt einen neuen {@code CalendarPicker} für das gregorianische Kalendersystem
* unter Benutzung von aus dem System abgeleiteten Standardwerten für die Sprach- und
* Lädereinstellung und die aktuelle Zonenzeit. </p>
*
* @return CalendarPicker
* @see Locale#getDefault(Locale.Category) Locale.getDefault(Locale.Category.FORMAT)
* @see SystemClock#inLocalView()
* @see ZonalClock#today()
*/
public static CalendarPicker<PlainDate> gregorianWithSystemDefaults() {
return CalendarPicker.gregorian(
Locale.getDefault(Locale.Category.FORMAT),
() -> SystemClock.inLocalView().today()
);
}
/**
* <p>Creates a new {@code CalendarPicker} for the gregorian calendar system. </p>
*
* @param locale the language and country configuration
* @param todaySupplier determines the current calendar date
* @return CalendarPicker
*/
/*[deutsch]
* <p>Erzeugt einen neuen {@code CalendarPicker} für das gregorianische Kalendersystem. </p>
*
* @param locale the language and country configuration
* @param todaySupplier determines the current calendar date
* @return CalendarPicker
*/
public static CalendarPicker<PlainDate> gregorian(
Locale locale,
Supplier<PlainDate> todaySupplier
) {
return CalendarPicker.create(
PlainDate.axis(),
new FXCalendarSystemIso8601(),
locale,
todaySupplier
);
}
/**
* <p>Creates a new {@code CalendarPicker} for the islamic calendar using system defaults
* for the locale and the current local time. </p>
*
* <p>Following code selects the Umalqura variant of Saudi-Arabia: </p>
*
* <pre>
* CalendarPicker<HijriCalendar> picker =
* CalendarPicker.hijriWithSystemDefaults(() -> HijriCalendar.VARIANT_UMALQURA);
* </pre>
*
* @param variantSource the variant of the underlying islamic calendar
* @return CalendarPicker
* @see Locale#getDefault(Locale.Category) Locale.getDefault(Locale.Category.FORMAT)
* @see SystemClock#inLocalView()
* @see ZonalClock#now(CalendarFamily, VariantSource, StartOfDay)
* @see HijriCalendar#VARIANT_UMALQURA
* @see net.time4j.calendar.HijriAlgorithm
*/
/*[deutsch]
* <p>Erzeugt einen neuen {@code CalendarPicker} für den islamischen Kalender
* unter Benutzung von aus dem System abgeleiteten Standardwerten für die Sprach- und
* Lädereinstellung und die aktuelle Zonenzeit. </p>
*
* <p>Folgender Code wählt die Umalqura-Variante von Saudi-Arabien: </p>
*
* <pre>
* CalendarPicker<HijriCalendar> picker =
* CalendarPicker.hijriWithSystemDefaults(() -> HijriCalendar.VARIANT_UMALQURA);
* </pre>
*
* @param variantSource the variant of the underlying islamic calendar
* @return CalendarPicker
* @see Locale#getDefault(Locale.Category) Locale.getDefault(Locale.Category.FORMAT)
* @see SystemClock#inLocalView()
* @see ZonalClock#now(CalendarFamily, VariantSource, StartOfDay)
* @see HijriCalendar#VARIANT_UMALQURA
* @see net.time4j.calendar.HijriAlgorithm
*/
public static CalendarPicker<HijriCalendar> hijriWithSystemDefaults(VariantSource variantSource) {
return CalendarPicker.hijri(
variantSource,
Locale.getDefault(Locale.Category.FORMAT),
() -> SystemClock.inLocalView().now(HijriCalendar.family(), variantSource, StartOfDay.EVENING).toDate()
);
}
/**
* <p>Creates a new {@code CalendarPicker} for the islamic calendar. </p>
*
* @param variantSource the variant of the underlying islamic calendar
* @param locale the language and country configuration
* @param todaySupplier determines the current calendar date
* @return CalendarPicker
*/
/*[deutsch]
* <p>Erzeugt einen neuen {@code CalendarPicker} für den islamischen Kalender. </p>
*
* @param variantSource the variant of the underlying islamic calendar
* @param locale the language and country configuration
* @param todaySupplier determines the current calendar date
* @return CalendarPicker
* @return CalendarPicker
*/
public static CalendarPicker<HijriCalendar> hijri(
VariantSource variantSource,
Locale locale,
Supplier<HijriCalendar> todaySupplier
) {
return CalendarPicker.create(
HijriCalendar.family(),
new FXCalendarSystemHijri(variantSource.getVariant()),
locale,
todaySupplier
);
}
/**
* <p>Creates a new {@code CalendarPicker} for the persian calendar system using system defaults
* for the locale and the current local time. </p>
*
* @return CalendarPicker
* @see Locale#getDefault(Locale.Category) Locale.getDefault(Locale.Category.FORMAT)
* @see SystemClock#inLocalView()
* @see ZonalClock#now(Chronology)
*/
/*[deutsch]
* <p>Erzeugt einen neuen {@code CalendarPicker} für das persische Kalendersystem
* unter Benutzung von aus dem System abgeleiteten Standardwerten für die Sprach- und
* Lädereinstellung und die aktuelle Zonenzeit. </p>
*
* @return CalendarPicker
* @see Locale#getDefault(Locale.Category) Locale.getDefault(Locale.Category.FORMAT)
* @see SystemClock#inLocalView()
* @see ZonalClock#now(Chronology)
*/
public static CalendarPicker<PersianCalendar> persianWithSystemDefaults() {
return CalendarPicker.persian(
Locale.getDefault(Locale.Category.FORMAT),
() -> SystemClock.inLocalView().now(PersianCalendar.axis())
);
}
/**
* <p>Creates a new {@code CalendarPicker} for the persian calendar system (jalali). </p>
*
* @param locale the language and country configuration
* @param todaySupplier determines the current calendar date
* @return CalendarPicker
*/
/*[deutsch]
* <p>Erzeugt einen neuen {@code CalendarPicker} für das persische Kalendersystem (jalali). </p>
*
* @param locale the language and country configuration
* @param todaySupplier determines the current calendar date
* @return CalendarPicker
*/
public static CalendarPicker<PersianCalendar> persian(
Locale locale,
Supplier<PersianCalendar> todaySupplier
) {
return CalendarPicker.create(
PersianCalendar.axis(),
new FXCalendarSystemPersian(),
locale,
todaySupplier
);
}
/**
* <p>Creates a new {@code CalendarPicker} for the calendar system of Taiwan using system defaults
* for the locale and the current local time. </p>
*
* @return CalendarPicker
* @see Locale#getDefault(Locale.Category) Locale.getDefault(Locale.Category.FORMAT)
* @see SystemClock#inLocalView()
* @see ZonalClock#now(Chronology)
*/
/*[deutsch]
* <p>Erzeugt einen neuen {@code CalendarPicker} für den Kalender auf Taiwan
* unter Benutzung von aus dem System abgeleiteten Standardwerten für die Sprach- und
* Lädereinstellung und die aktuelle Zonenzeit. </p>
*
* @return CalendarPicker
* @see Locale#getDefault(Locale.Category) Locale.getDefault(Locale.Category.FORMAT)
* @see SystemClock#inLocalView()
* @see ZonalClock#now(Chronology)
*/
public static CalendarPicker<MinguoCalendar> minguoWithSystemDefaults() {
return CalendarPicker.minguo(
Locale.getDefault(Locale.Category.FORMAT),
() -> SystemClock.inLocalView().now(MinguoCalendar.axis())
);
}
/**
* <p>Creates a new {@code CalendarPicker} for the calendar system used in Taiwan. </p>
*
* @param locale the language and country configuration
* @param todaySupplier determines the current calendar date
* @return CalendarPicker
*/
/*[deutsch]
* <p>Erzeugt einen neuen {@code CalendarPicker} für den Kalender auf Taiwan. </p>
*
* @param locale the language and country configuration
* @param todaySupplier determines the current calendar date
* @return CalendarPicker
*/
public static CalendarPicker<MinguoCalendar> minguo(
Locale locale,
Supplier<MinguoCalendar> todaySupplier
) {
return CalendarPicker.create(
MinguoCalendar.axis(),
new FXCalendarSystemMinguo(),
locale,
todaySupplier
);
}
/**
* <p>Creates a new {@code CalendarPicker} for the buddhist calendar system in Thailand using system defaults
* for the locale and the current local time. </p>
*
* @return CalendarPicker
* @see Locale#getDefault(Locale.Category) Locale.getDefault(Locale.Category.FORMAT)
* @see SystemClock#inLocalView()
* @see ZonalClock#now(Chronology)
*/
/*[deutsch]
* <p>Erzeugt einen neuen {@code CalendarPicker} für das Thai-Kalendersystem
* unter Benutzung von aus dem System abgeleiteten Standardwerten für die Sprach- und
* Lädereinstellung und die aktuelle Zonenzeit. </p>
*
* @return CalendarPicker
* @see Locale#getDefault(Locale.Category) Locale.getDefault(Locale.Category.FORMAT)
* @see SystemClock#inLocalView()
* @see ZonalClock#now(Chronology)
*/
public static CalendarPicker<ThaiSolarCalendar> thaiWithSystemDefaults() {
return CalendarPicker.thai(
Locale.getDefault(Locale.Category.FORMAT),
() -> SystemClock.inLocalView().now(ThaiSolarCalendar.axis())
);
}
/**
* <p>Creates a new {@code CalendarPicker} for the buddhist calendar system used in Thailand. </p>
*
* @param locale the language and country configuration
* @param todaySupplier determines the current calendar date
* @return CalendarPicker
*/
/*[deutsch]
* <p>Erzeugt einen neuen {@code CalendarPicker} für das Thai-Kalendersystem. </p>
*
* @param locale the language and country configuration
* @param todaySupplier determines the current calendar date
* @return CalendarPicker
*/
public static CalendarPicker<ThaiSolarCalendar> thai(
Locale locale,
Supplier<ThaiSolarCalendar> todaySupplier
) {
return CalendarPicker.create(
ThaiSolarCalendar.axis(),
new FXCalendarSystemThai(),
locale,
todaySupplier
);
}
/**
* The current format locale.
*
* Note: If the locale is set to {@code null} (not recommended) then the locale in effect is {@code Locale.ROOT}.
*
* @return read-write-property
* @see #setLocale(Locale)
*/
/*[deutsch]
* Die assoziierte Sprach- und Ländereinstellung fü Formatierungszwecke.
*
* Hinweis: Wenn die Sprache auf {@code null} gesetzt wird (nicht empfohlen), dann
* wird effektiv {@code Locale.ROOT} angenommen.
*
* @return read-write-property
* @see #setLocale(Locale)
*/
public ObjectProperty<Locale> localeProperty() {
return this.control.localeProperty();
}
/**
* The current calendar date value associated with this component.
*
* @return read-write-property
* @see #setValue(CalendarDate) setValue(T)
*/
/*[deutsch]
* Der assoziierte Datumswert.
*
* @return read-write-property
* @see #setValue(CalendarDate) setValue(T)
*/
public ObjectProperty<T> valueProperty() {
return this.valuePropertyExt;
}
/**
* The current error information associated with this component.
*
* @return read-only-property
*/
/*[deutsch]
* Die assoziierte Fehlerinformation.
*
* @return read-only-property
*/
public ReadOnlyStringProperty errorProperty() {
return this.errorProperty;
}
/**
* The customized editor date format.
*
* @return read-property
* @see #setDateFormat(ChronoFormatter)
*/
/*[deutsch]
* Das mit dem Texteditor assoziierte Datumsformat.
*
* @return read-property
* @see #setDateFormat(ChronoFormatter)
*/
public ReadOnlyObjectProperty<ChronoFormatter<T>> dateFormatProperty() {
return this.formatProperty;
}
/**
* The customized editor prompt text.
*
* @return read-property
* @see #setPromptText(String)
*/
/*[deutsch]
* Der mit dem Texteditor assoziierte Aufforderungstext.
*
* @return read-property
* @see #setPromptText(String)
*/
public ReadOnlyStringProperty promptTextProperty() {
return this.promptProperty;
}
/**
* The minimum value which can be selected.
*
* @return read-property
* @see #setMinDate(CalendarDate) setMinDate(T)
*/
/*[deutsch]
* Der minimal auswählbare Wert.
*
* @return read-property
* @see #setMinDate(CalendarDate) setMinDate(T)
*/
public ReadOnlyObjectProperty<T> minDateProperty() {
return this.control.minDateProperty();
}
/**
* The maximum value which can be selected.
*
* @return read-property
* @see #setMaxDate(CalendarDate) setMaxDate(T)
*/
/*[deutsch]
* Der maximal auswählbare Wert.
*
* @return read-property
* @see #setMaxDate(CalendarDate) setMaxDate(T)
*/
public ReadOnlyObjectProperty<T> maxDateProperty() {
return this.control.maxDateProperty();
}
/**
* Determines if the calendar shows week numbers.
*
* @return boolean property
* @see #setShowWeeks(boolean)
*/
/*[deutsch]
* Legt fest, ob die Nummern von Kalenderwochen im Kalender angezeigt werden.
*
* @return boolean property
* @see #setShowWeeks(boolean)
*/
public BooleanProperty showWeeksProperty() {
return this.control.showWeeksProperty();
}
/**
* Determines if the header shows the chosen calendar range.
*
* @return boolean property
* @see #setShowInfoLabel(boolean)
*/
/*[deutsch]
* Legt fest, ob der Kopf den gewählten Tabellenbereich als ISO-Datumsintervall anzeigt.
*
* @return boolean property
* @see #setShowInfoLabel(boolean)
*/
public BooleanProperty showInfoLabelProperty() {
return this.control.showInfoLabelProperty();
}
/**
* Determines if and how long any animations will happen.
*
* If the duration is negative or zero then animations are switched off.
*
* @return read-property
* @see #setLengthOfAnimations(Duration)
*/
/*[deutsch]
* Legt fest, ob und wie lange Animationen dauern.
*
* Ist die Dauer negativ oder gleich {@code ZERO}, dann sind Animationen abgeschaltet.
*
* @return read-property
* @see #setLengthOfAnimations(Duration)
*/
public ReadOnlyObjectProperty<Duration> lengthOfAnimationsProperty() {
return this.control.lengthOfAnimationsProperty();
}
/**
* <p>Allows user-defined customizations of date cells in the month view. </p>
*
* @return read-write property (nullable)
* @see #setCellCustomizer(CellCustomizer)
*/
/*[deutsch]
* <p>Erlaubt benutzerdefinierte Anpassungen von Datumszellen in der Monatssicht. </p>
*
* @return read-write property (nullable)
* @see #setCellCustomizer(CellCustomizer)
*/
public ObjectProperty<CellCustomizer<T>> cellCustomizerProperty() {
return this.control.cellCustomizerProperty();
}
public void setLocale(Locale locale){
this.localeProperty().setValue(locale);
}
public void setValue(T value){
this.valuePropertyExt.setValue(value);
}
public void setDateFormat(ChronoFormatter<T> dateFormat) {
if (dateFormat == null) {
throw new NullPointerException("Missing date format.");
}
this.formatProperty.setValue(dateFormat);
}
public void setPromptText(String promptText) {
if (promptText == null) {
throw new NullPointerException("Missing prompt text.");
}
this.promptProperty.setValue(promptText);
}
public void setMinDate(T minimum) {
if (minimum == null) {
throw new NullPointerException("Missing minimum date.");
}
this.control.minDateProperty().setValue(minimum);
}
public void setMaxDate(T maximum) {
if (maximum == null) {
throw new NullPointerException("Missing maximum date.");
}
this.control.maxDateProperty().setValue(maximum);
}
public void setShowWeeks(boolean showWeeks) {
this.showWeeksProperty().set(showWeeks);
}
public void setShowInfoLabel(boolean showInfoLabel) {
this.showInfoLabelProperty().set(showInfoLabel);
}
public void setLengthOfAnimations(Duration duration) {
this.control.lengthOfAnimationsProperty().set((duration == null) ? Duration.ZERO : duration);
}
public void setCellCustomizer(CellCustomizer<T> customizer) {
this.cellCustomizerProperty().set(customizer);
}
private static <D extends CalendarVariant<D>> CalendarPicker<D> create(
CalendarFamily<D> family,
FXCalendarSystem<D> calsys,
Locale locale,
Supplier<D> todaySupplier
) {
CalendarPicker<D> picker =
new CalendarPicker<>(
calsys,
locale,
todaySupplier,
family,
calsys.getChronologicalMinimum(),
calsys.getChronologicalMaximum());
picker.setShowInfoLabel(true);
return picker;
}
private static <U, T extends Calendrical<U, T>> CalendarPicker<T> create(
TimeAxis<U, T> axis,
FXCalendarSystem<T> calsys,
Locale locale,
Supplier<T> todaySupplier
) {
CalendarPicker<T> picker =
new CalendarPicker<>(calsys, locale, todaySupplier, axis, axis.getMinimum(), axis.getMaximum());
picker.setShowInfoLabel(axis != PlainDate.axis());
return picker;
}
private void commitTextInput() {
try {
this.committingText = true;
ParseLog plog = new ParseLog();
String input = this.textField.getText();
if ((input == null) || input.trim().isEmpty()) {
this.control.selectedDateProperty().setValue(null);
this.errorProperty.setValue(null);
} else {
T date = this.getFormat().parse(input, plog);
if (plog.isError()) {
this.control.selectedDateProperty().setValue(null);
this.errorProperty.setValue(
"[error-position=" + plog.getErrorIndex() + "] " + plog.getErrorMessage());
} else if (
date.isBefore(this.control.minDateProperty().get())
|| date.isAfter(this.control.maxDateProperty().get())
) {
this.control.selectedDateProperty().setValue(null);
this.errorProperty.setValue("[error] Out of range: " + date);
} else {
this.control.selectedDateProperty().setValue(date);
this.errorProperty.setValue(null);
}
}
} catch (RuntimeException ex) {
this.control.selectedDateProperty().setValue(null);
this.errorProperty.setValue("[error] " + ex.getMessage());
} finally {
this.updateTextField();
this.committingText = false;
}
}
private void updateTextField() {
if (this.errorProperty.getValue() == null) {
this.textInChange = true;
T value = this.valuePropertyInt.getValue();
if (value == null) {
this.textField.setText("");
} else {
String s = this.getFormat().format(value);
if (!this.textField.getText().equals(s)) {
this.textField.setText(s);
}
}
this.textInChange = false;
}
}
private ChronoFormatter<T> getFormat() {
// we always use strict parsing in order to avoid problems with the print/parse-roundtrip of 2-digit-years
ChronoFormatter<T> f;
if (this.formatProperty.getValue() == null) {
Locale locale = this.control.localeProperty().get();
if (locale == null) {
locale = Locale.ROOT;
}
String pattern = this.getStdFormatPattern(locale);
f = ChronoFormatter.ofPattern(
pattern,
PatternType.CLDR,
locale,
this.control.chronology()
).with(Leniency.STRICT);
if (this.calsys.getVariantSource().isPresent()) {
f = f.withCalendarVariant(this.calsys.getVariantSource().get());
}
} else {
f = this.formatProperty.getValue();
if (!f.getAttributes().get(Attributes.LENIENCY, Leniency.SMART).isStrict()) {
f = f.with(Leniency.STRICT);
}
}
return f;
}
private String getStdFormatPattern(Locale locale) {
String pattern = this.control.chronology().getFormatPattern(DisplayMode.SHORT, locale);
if (pattern.contains("yy") && !pattern.contains("yyy")) {
pattern = pattern.replace("yy", "yyyy"); // avoid two-digit-years if possible anyway
}
return pattern;
}
private void showPopup() {
if (this.popupDialog == null) {
Popup p = new AnimatedPopup();
p.setAutoHide(true);
p.setAutoFix(true);
p.setHideOnEscape(true);
CalendarContent<T> cc = new CalendarContent<>(this.control, this.calsys);
cc.getStylesheets().setAll(this.getStylesheets());
this.getStylesheets().addListener(
(Observable observable) -> {
cc.getStylesheets().setAll(getStylesheets());
}
);
p.getContent().add(cc);
this.popupDialog = p;
}
Bounds cBounds = this.popupDialog.getContent().get(0).getBoundsInLocal();
Bounds pBounds = this.localToScene(this.getBoundsInLocal());
Scene scene = this.getScene();
Window window = scene.getWindow();
double x = cBounds.getMinX() + pBounds.getMinX() + scene.getX() + window.getX();
double y = cBounds.getMinY() + pBounds.getHeight() + pBounds.getMinY() + scene.getY() + window.getY();
this.popupDialog.show(this, x, y);
// fix for issue reported by Dimitris Michaelides:
// see https://bitbucket.org/controlsfx/controlsfx/issues/185/nullpointerexception-when-using-popover
window.setOnCloseRequest(AnimatedPopup.class.cast(this.popupDialog).getClosingHandler());
}
private void hidePopup() {
if (this.popupDialog != null) {
this.popupDialog.hide();
}
}
private Button createPopupButton() {
Button button = new Button();
button.getStyleClass().add(CSS_CALENDAR_DIALOG_START);
ImageView image = new ImageView("/net/time4j/ui/javafx/calendar32.png");
image.setFitHeight(16);
image.setPreserveRatio(true);
button.setGraphic(image);
button.setOnAction(event -> showPopup());
return button;
}
//~ Innere Klassen ----------------------------------------------------
private class PromptBinding
extends StringBinding {
//~ Konstruktoren -------------------------------------------------
PromptBinding() {
super();
this.bind(control.localeProperty(), formatProperty, promptProperty);
}
//~ Methoden ------------------------------------------------------
@Override
protected String computeValue() {
if (promptProperty.getValue() != null) {
return promptProperty.getValue();
} else if (formatProperty.getValue() == null) {
Locale locale = control.localeProperty().get();
if (locale == null) {
locale = Locale.ROOT;
}
return getStdFormatPattern(locale);
}
return "";
}
}
private class AnimatedPopup extends Popup {
//~ Instanzvariablen ----------------------------------------------
private final FadeTransition hideFadeTransition;
private final ScaleTransition hideScaleTransition;
private final FadeTransition showFadeTransition;
private final ScaleTransition showScaleTransition;
private final EventHandler<WindowEvent> closingHandler;
//~ Konstruktoren -------------------------------------------------
AnimatedPopup() {
super();
Interpolator interpolator = new PopupInterpolator();
showFadeTransition = new FadeTransition(Duration.seconds(0.2), getScene().getRoot());
showFadeTransition.setFromValue(0);
showFadeTransition.setToValue(1);
showFadeTransition.setInterpolator(interpolator);
showScaleTransition = new ScaleTransition(Duration.seconds(0.2), getScene().getRoot());
showScaleTransition.setFromX(0.8);
showScaleTransition.setFromY(0.8);
showScaleTransition.setToY(1);
showScaleTransition.setToX(1);
showScaleTransition.setInterpolator(interpolator);
hideFadeTransition = new FadeTransition(Duration.seconds(.3), getScene().getRoot());
hideFadeTransition.setFromValue(1);
hideFadeTransition.setToValue(0);
hideFadeTransition.setInterpolator(interpolator);
hideScaleTransition = new ScaleTransition(Duration.seconds(.3), getScene().getRoot());
hideScaleTransition.setFromX(1);
hideScaleTransition.setFromY(1);
hideScaleTransition.setToY(0.8);
hideScaleTransition.setToX(0.8);
hideScaleTransition.setInterpolator(interpolator);
hideScaleTransition.setOnFinished(
actionEvent -> {
if (AnimatedPopup.super.isShowing()) {
AnimatedPopup.super.hide();
}
}
);
this.closingHandler = (
event -> {
final Popup p = popupDialog;
if (p != null) {
p.getOwnerWindow().removeEventFilter(
WindowEvent.WINDOW_CLOSE_REQUEST,
getClosingHandler());
if (p.isShowing()) {
// first closing request will only close the popup dialog but not the window
p.hide();
event.consume();
}
popupDialog = null;
}
}
);
}
//~ Methoden ------------------------------------------------------
@Override
public void show() {
super.show();
if (showFadeTransition.getStatus() != Animation.Status.RUNNING) {
showFadeTransition.playFromStart();
showScaleTransition.playFromStart();
}
}
@Override
public void hide() {
if (isShowing()) {
if (!getOwnerWindow().isShowing()) {
hideFadeTransition.stop();
hideScaleTransition.stop();
} else if (hideFadeTransition.getStatus() != Animation.Status.RUNNING) {
hideFadeTransition.playFromStart();
hideScaleTransition.playFromStart();
}
}
}
private EventHandler<WindowEvent> getClosingHandler() {
return this.closingHandler;
}
}
private static class PopupInterpolator
extends Interpolator {
//~ Methoden ------------------------------------------------------
@Override
protected double curve(double t) {
double s = 1.70158;
double v = 1 - t;
return 1 - (v * v * ((s + 1) * v - s));
}
}
}