How do I modify the Month and Year Controls on JavaFX 8's DatePicker?


Question

For an application I'm developing, a lot of older clients with larger fingers and poor eyesight need the selection arrows and Date/Year labels to be larger on the DatePicker Pop-Up.

In the documentation of the DatePicker class, there are methods described for modifying the DateCells within the calendar itself, but nothing describes or elaborates on the construction of the popup any further. (At least, the code is not self-documented plainly enough for my entry-level knowledge of Java development to grasp any hints)

My implementation ends up looking like this

Sometimes when I open and close the calendar, the size of the font and buttons in the Month/Year controls fluctuate, but I have no idea how or why. I assume some CSS may be tampering with it, but I have no idea why it would sometimes work and sometimes not. CSS' true nature has always escaped me.

I know it [DatePicker] is based on a ComboBox, but I have absolutely zero idea how it is constructed and fully implemented, much less how to modify very specific elements in it.

Is there a method I can call/Override, or a parameter I can modify in-line to style these properly? Also, could there be a combination of classes and elements I can call in CSS to style these to avoid having to outright modify DatePicker itself as described above?

Here is my implementation:

@FXML private DatePicker dPicker;

final Callback<DatePicker, DateCell> dayCellFactory =
            new Callback<DatePicker, DateCell>() {
                @Override
                public DateCell call(final DatePicker datePicker) {
                    return new DateCell() {
                        @Override
                        public void updateItem(LocalDate item, boolean empty) {
                            super.updateItem(item, empty);

                            setMinSize(100,100);
                            setStyle("-fx-font-size:50px;-fx-font-weight:bold;");
                        }
                    };
                }
            };

dPicker.setDayCellFactory(dayCellFactory);

...and here is DataPicker.java:

/*
 * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package javafx.scene.control;

// editor and converter code in sync with ComboBox 4858:e60e9a5396e6

import java.time.LocalDate;
import java.time.DateTimeException;
import java.time.chrono.Chronology;
import java.time.chrono.ChronoLocalDate;
import java.time.chrono.IsoChronology;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DecimalStyle;
import java.time.format.FormatStyle;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;

import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableBooleanProperty;
import javafx.css.StyleableProperty;
import javafx.util.Callback;
import javafx.util.StringConverter;

import com.sun.javafx.css.converters.BooleanConverter;
import com.sun.javafx.scene.control.skin.DatePickerSkin;
import com.sun.javafx.scene.control.skin.resources.ControlResources;


/**
 * The DatePicker control allows the user to enter a date as text or
 * to select a date from a calendar popup. The calendar is based on
 * either the standard ISO-8601 chronology or any of the other
 * chronology classes defined in the java.time.chrono package.
 *
 * <p>The {@link #valueProperty() value} property represents the
 * currently selected {@link java.time.LocalDate}.  An initial date can
 * be set via the {@link #DatePicker(java.time.LocalDate) constructor}
 * or by calling {@link #setValue(java.time.LocalDate) setValue()}.  The
 * default value is null.
 *
 * <pre><code>
 * final DatePicker datePicker = new DatePicker();
 * datePicker.setOnAction(new EventHandler() {
 *     public void handle(Event t) {
 *         LocalDate date = datePicker.getValue();
 *         System.err.println("Selected date: " + date);
 *     }
 * });
 * </code></pre>
 *
 * The {@link #chronologyProperty() chronology} property specifies a
 * calendar system to be used for parsing, displaying, and choosing
 * dates.
 * The {@link #valueProperty() value} property is always defined in
 * the ISO calendar system, however, so applications based on a
 * different chronology may use the conversion methods provided in the
 * {@link java.time.chrono.Chronology} API to get or set the
 * corresponding {@link java.time.chrono.ChronoLocalDate} value. For
 * example:
 *
 * <pre><code>
 * LocalDate isoDate = datePicker.getValue();
 * ChronoLocalDate chronoDate =
 *     ((isoDate != null) ? datePicker.getChronology().date(isoDate) : null);
 * System.err.println("Selected date: " + chronoDate);
 * </code></pre>
 *
 *
 * @since JavaFX 8.0
 */
public class DatePicker extends ComboBoxBase<LocalDate> {

private LocalDate lastValidDate = null;
private Chronology lastValidChronology = IsoChronology.INSTANCE;

/**
 * Creates a default DatePicker instance with a <code>null</code> date value set.
 */
public DatePicker() {
    this(null);

    valueProperty().addListener(new InvalidationListener() {
        @Override public void invalidated(Observable observable) {
            LocalDate date = getValue();
            Chronology chrono = getChronology();

            if (validateDate(chrono, date)) {
                lastValidDate = date;
            } else {
                System.err.println("Restoring value to " +
                            ((lastValidDate == null) ? "null" : getConverter().toString(lastValidDate)));
                setValue(lastValidDate);
            }
        }
    });

    chronologyProperty().addListener(new InvalidationListener() {
        @Override public void invalidated(Observable observable) {
            LocalDate date = getValue();
            Chronology chrono = getChronology();

            if (validateDate(chrono, date)) {
                lastValidChronology = chrono;
            } else {
                System.err.println("Restoring value to " + lastValidChronology);
                setChronology(lastValidChronology);
            }
        }
    });
}

private boolean validateDate(Chronology chrono, LocalDate date) {
    try {
        if (date != null) {
            chrono.date(date);
        }
        return true;
    } catch (DateTimeException ex) {
        System.err.println(ex);
        return false;
    }
}

/**
 * Creates a DatePicker instance and sets the
 * {@link #valueProperty() value} to the given date.
 *
 * @param localDate to be set as the currently selected date in the DatePicker. Can be null.
 */
public DatePicker(LocalDate localDate) {
    setValue(localDate);
    getStyleClass().add(DEFAULT_STYLE_CLASS);
    setEditable(true);
}


/***************************************************************************
 *                                                                         *
 * Properties                                                                 *
 *                                                                         *
 **************************************************************************/


/**
 * A custom cell factory can be provided to customize individual
 * day cells in the DatePicker popup. Refer to {@link DateCell}
 * and {@link Cell} for more information on cell factories.
 * Example:
 *
 * <pre><code>
 * final Callback&lt;DatePicker, DateCell&gt; dayCellFactory = new Callback&lt;DatePicker, DateCell&gt;() {
 *     public DateCell call(final DatePicker datePicker) {
 *         return new DateCell() {
 *             &#064;Override public void updateItem(LocalDate item, boolean empty) {
 *                 super.updateItem(item, empty);
 *
 *                 if (MonthDay.from(item).equals(MonthDay.of(9, 25))) {
 *                     setTooltip(new Tooltip("Happy Birthday!"));
 *                     setStyle("-fx-background-color: #ff4444;");
 *                 }
 *                 if (item.equals(LocalDate.now().plusDays(1))) {
 *                     // Tomorrow is too soon.
 *                     setDisable(true);
 *                 }
 *             }
 *         };
 *     }
 * };
 * datePicker.setDayCellFactory(dayCellFactory);
 * </code></pre>
 *
 * @defaultValue null
 */
private ObjectProperty<Callback<DatePicker, DateCell>> dayCellFactory;
public final void setDayCellFactory(Callback<DatePicker, DateCell> value) {
    dayCellFactoryProperty().set(value);
}
public final Callback<DatePicker, DateCell> getDayCellFactory() {
    return (dayCellFactory != null) ? dayCellFactory.get() : null;
}
public final ObjectProperty<Callback<DatePicker, DateCell>> dayCellFactoryProperty() {
    if (dayCellFactory == null) {
        dayCellFactory = new SimpleObjectProperty<Callback<DatePicker, DateCell>>(this, "dayCellFactory");
    }
    return dayCellFactory;
}



/**
 * The calendar system used for parsing, displaying, and choosing
 * dates in the DatePicker control.
 *
 * <p>The default value is returned from a call to
 * {@code Chronology.ofLocale(Locale.getDefault(Locale.Category.FORMAT))}.
 * The default is usually {@link java.time.chrono.IsoChronology} unless
 * provided explicitly in the {@link java.util.Locale} by use of a
 * Locale calendar extension.
 *
 * Setting the value to <code>null</code> will restore the default
 * chronology.
 */
public final ObjectProperty<Chronology> chronologyProperty() {
    return chronology;
}
private ObjectProperty<Chronology> chronology =
    new SimpleObjectProperty<Chronology>(this, "chronology", null);
public final Chronology getChronology() {
    Chronology chrono = chronology.get();
    if (chrono == null) {
        try {
            chrono = Chronology.ofLocale(Locale.getDefault(Locale.Category.FORMAT));
        } catch (Exception ex) {
            System.err.println(ex);
        }
        if (chrono == null) {
            chrono = IsoChronology.INSTANCE;
        }
        //System.err.println(chrono);
    }
    return chrono;
}
public final void setChronology(Chronology value) {
    chronology.setValue(value);
}


/**
 * Whether the DatePicker popup should display a column showing
 * week numbers.
 *
 * <p>The default value is specified in a resource bundle, and
 * depends on the country of the current locale.
 */
public final BooleanProperty showWeekNumbersProperty() {
    if (showWeekNumbers == null) {
        String country = Locale.getDefault(Locale.Category.FORMAT).getCountry();
        boolean localizedDefault =
            (!country.isEmpty() &&
             ControlResources.getNonTranslatableString("DatePicker.showWeekNumbers").contains(country));
        showWeekNumbers = new StyleableBooleanProperty(localizedDefault) {
            @Override public CssMetaData<DatePicker,Boolean> getCssMetaData() {
                return StyleableProperties.SHOW_WEEK_NUMBERS;
            }

            @Override public Object getBean() {
                return DatePicker.this;
            }

            @Override public String getName() {
                return "showWeekNumbers";
            }
        };
    }
    return showWeekNumbers;
}
private BooleanProperty showWeekNumbers;
public final void setShowWeekNumbers(boolean value) {
    showWeekNumbersProperty().setValue(value);
}
public final boolean isShowWeekNumbers() {
    return showWeekNumbersProperty().getValue();
}


// --- string converter
/**
 * Converts the input text to an object of type LocalDate and vice
 * versa.
 *
 * <p>If not set by the application, the DatePicker skin class will
 * set a converter based on a {@link java.time.DateTimeFormatter}
 * for the current {@link java.util.Locale} and
 * {@link #chronologyProperty() chronology}. This formatter is
 * then used to parse and display the current date value.
 *
 * Setting the value to <code>null</code> will restore the default
 * converter.
 *
 * <p>Example using an explicit formatter:
 * <pre><code>
 * datePicker.setConverter(new StringConverter&lt;LocalDate&gt;() {
 *     String pattern = "yyyy-MM-dd";
 *     DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(pattern);
 *
 *     {
 *         datePicker.setPromptText(pattern.toLowerCase());
 *     }
 *
 *     &#064;Override public String toString(LocalDate date) {
 *         if (date != null) {
 *             return dateFormatter.format(date);
 *         } else {
 *             return "";
 *         }
 *     }
 *
 *     &#064;Override public LocalDate fromString(String string) {
 *         if (string != null && !string.isEmpty()) {
 *             return LocalDate.parse(string, dateFormatter);
 *         } else {
 *             return null;
 *         }
 *     }
 * });
 * </code></pre>
 * <p>Example that wraps the default formatter and catches parse exceptions:
 * <pre><code>
 *   final StringConverter&lt;LocalDate&gt; defaultConverter = datePicker.getConverter();
 *   datePicker.setConverter(new StringConverter&lt;LocalDate&gt;() {
 *       &#064;Override public String toString(LocalDate value) {
 *           return defaultConverter.toString(value);
 *       }
 *
 *       &#064;Override public LocalDate fromString(String text) {
 *           try {
 *               return defaultConverter.fromString(text);
 *           } catch (DateTimeParseException ex) {
 *               System.err.println("HelloDatePicker: "+ex.getMessage());
 *               throw ex;
 *           }
 *       }
 *   });
 * </code></pre>
 *
 * @see javafx.scene.control.ComboBox#converterProperty
 */
public final ObjectProperty<StringConverter<LocalDate>> converterProperty() { return converter; }
private ObjectProperty<StringConverter<LocalDate>> converter =
        new SimpleObjectProperty<StringConverter<LocalDate>>(this, "converter", null);
public final void setConverter(StringConverter<LocalDate> value) { converterProperty().set(value); }
public final StringConverter<LocalDate> getConverter() {
    StringConverter<LocalDate> converter = converterProperty().get();
    if (converter != null) {
        return converter;
    } else {
        return defaultConverter;
    }
}

private StringConverter<LocalDate> defaultConverter = new StringConverter<LocalDate>() {
    @Override public String toString(LocalDate value) {
        if (value != null) {
            Locale locale = Locale.getDefault(Locale.Category.FORMAT);
            Chronology chrono = getChronology();
            ChronoLocalDate cDate;
            try {
                cDate = chrono.date(value);
            } catch (DateTimeException ex) {
                System.err.println(ex);
                chrono = IsoChronology.INSTANCE;
                cDate = value;
            }
            DateTimeFormatter dateFormatter =
                DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
                                 .withLocale(locale)
                                 .withChronology(chrono)
                                 .withDecimalStyle(DecimalStyle.of(locale));

            String pattern =
                DateTimeFormatterBuilder.getLocalizedDateTimePattern(FormatStyle.SHORT,
                                                                     null, chrono, locale);

            if (pattern.contains("yy") && !pattern.contains("yyy")) {
                // Modify pattern to show four-digit year, including leading zeros.
                String newPattern = pattern.replace("yy", "yyyy");
                //System.err.println("Fixing pattern ("+forParsing+"): "+pattern+" -> "+newPattern);
                dateFormatter = DateTimeFormatter.ofPattern(newPattern)
                                                 .withDecimalStyle(DecimalStyle.of(locale));
            }

            return dateFormatter.format(cDate);
        } else {
            return "";
        }
    }

    @Override public LocalDate fromString(String text) {
        if (text != null && !text.isEmpty()) {
            Locale locale = Locale.getDefault(Locale.Category.FORMAT);
            Chronology chrono = getChronology();

            String pattern =
                DateTimeFormatterBuilder.getLocalizedDateTimePattern(FormatStyle.SHORT,
                                                                     null, chrono, locale);
            DateTimeFormatter df =
                new DateTimeFormatterBuilder().parseLenient()
                                              .appendPattern(pattern)
                                              .toFormatter()
                                              .withChronology(chrono)
                                              .withDecimalStyle(DecimalStyle.of(locale));
            TemporalAccessor temporal = df.parse(text);
            ChronoLocalDate cDate = chrono.date(temporal);
            return LocalDate.from(cDate);
        }
        return null;
    }
};


// --- Editor
/**
 * The editor for the DatePicker.
 *
 * @see javafx.scene.control.ComboBox#editorProperty
 */
private ReadOnlyObjectWrapper<TextField> editor;
public final TextField getEditor() {
    return editorProperty().get();
}
public final ReadOnlyObjectProperty<TextField> editorProperty() {
    if (editor == null) {
        editor = new ReadOnlyObjectWrapper<TextField>(this, "editor");
        editor.set(new ComboBoxListViewSkin.FakeFocusTextField());
    }
    return editor.getReadOnlyProperty();
}

/** {@inheritDoc} */
@Override protected Skin<?> createDefaultSkin() {
    return new DatePickerSkin(this);
}


/***************************************************************************
 *                                                                         *
 * Stylesheet Handling                                                     *
 *                                                                         *
 **************************************************************************/

private static final String DEFAULT_STYLE_CLASS = "date-picker";

 /**
  * @treatAsPrivate implementation detail
  */
private static class StyleableProperties {
    private static final String country =
        Locale.getDefault(Locale.Category.FORMAT).getCountry();
    private static final CssMetaData<DatePicker, Boolean> SHOW_WEEK_NUMBERS =
          new CssMetaData<DatePicker, Boolean>("-fx-show-week-numbers",
               BooleanConverter.getInstance(),
               (!country.isEmpty() &&
                    ControlResources.getNonTranslatableString("DatePicker.showWeekNumbers").contains(country))) {
        @Override public boolean isSettable(DatePicker n) {
            return n.showWeekNumbers == null || !n.showWeekNumbers.isBound();
        }

        @Override public StyleableProperty<Boolean> getStyleableProperty(DatePicker n) {
            return (StyleableProperty)n.showWeekNumbersProperty();
        }
    };

    private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;

    static {
        final List<CssMetaData<? extends Styleable, ?>> styleables =
            new ArrayList<CssMetaData<? extends Styleable, ?>>(Control.getClassCssMetaData());
        Collections.addAll(styleables,
            SHOW_WEEK_NUMBERS
        );
        STYLEABLES = Collections.unmodifiableList(styleables);
    }
}

/**
 * @return The CssMetaData associated with this class, which may include the
 * CssMetaData of its super classes.
 */
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
    return StyleableProperties.STYLEABLES;
}

/**
 * {@inheritDoc}
 */
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
    return getClassCssMetaData();
}
}

...and thanks in advance for being patient and helping me out.

1
0
10/13/2014 1:39:53 PM

The best way to change appearance like this is to use an external CSS. You can refer to the source code for the default stylesheet to see how the defaults are defined: the interesting parts for the DatePicker are near the bottom (lines 2932 onwards at the time of writing).

You can change the font size for the entire control just by doing

.date-picker {
    -fx-font-size: 18pt ;
}

That ends up looking a little cluttered, so adding a bit of padding around the day names helps. The arrows will automatically scale to fit, so they will increase size with the increased font size on the month and year labels.

Here's a complete example:

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.DatePicker;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;


public class DatePickerExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        DatePicker datePicker = new DatePicker();
        VBox root = new VBox(datePicker);

        Scene scene = new Scene(root, 250, 150);

        scene.getStylesheets().add("date-picker-readable.css");

        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

date-picker-readable.css:

.date-picker {
    -fx-font-size: 18pt; 
    -fx-font-weight: bold ;
}
.date-picker .day-name-cell {
    -fx-padding: 10px ;
}
0
10/13/2014 3:07:02 PM

Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon