7

On the application I am currently working, it is necessary to select a single date or a period from the same JavaFX 8 DatePicker.

The preferred way of doing this would be as follows:

  1. Selecting a single date - same as default behaviour of the DatePicker.

  2. Selecting a period - select start/end date by holding down the mouse button and drag to the desired end/start date. When the mouse button is released you have defined your period. The fact that you cannot select dates other than those displayed is acceptable.

  3. Editing should work for both single date (ex 24.12.2014) and period ( ex: 24.12.2014 - 27.12.2014)

A possible rendering of the selected period (minus the content of the text editor) above would look like this:

Rendering of selected period

Where orange indicates current date, blue indicates selected period. The picture is from a prototype I made, but where the period is selected by using 2 DatePickers rather than one.

I had a look at the sourcecode for

com.sun.javafx.scene.control.skin.DatePickerContent

which has a

protected List<DateCell> dayCells = new ArrayList<DateCell>();

in order to find a way of detecting when the mouse selected a date end when the mouse was released (or maybe detecting a drag).

However I am not quite sure how to go about it. Any suggestions?

I am attaching the simple prototype code I have made so far (that makes use of 2 rather than the desired 1 datepicker).

Prototype so far

import java.time.LocalDate;

import javafx.beans.property.SimpleObjectProperty;

public interface PeriodController {

    /**
     * @return Today.
     */
    LocalDate currentDate();

    /**
     * @return Selected from date.
     */
    SimpleObjectProperty<LocalDate> fromDateProperty();

    /**
     * @return Selected to date.
     */
    SimpleObjectProperty<LocalDate> toDateProperty();
}


import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

import javafx.util.StringConverter;

public class DateConverter extends StringConverter<LocalDate> {

    private DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy"); // TODO i18n

    @Override
    public String toString(LocalDate date) {
        if (date != null) {
            return dateFormatter.format(date);
        } else {
            return "";
        }
    }

    @Override
    public LocalDate fromString(String string) {
        if (string != null && !string.isEmpty()) {
            return LocalDate.parse(string, dateFormatter);
        } else {
            return null;
        }
    }


}







import static java.lang.System.out;

import java.time.LocalDate;
import java.util.Locale;

import javafx.application.Application;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.HPos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class PeriodMain extends Application {

    private Stage stage;

    public static void main(String[] args) {
        Locale.setDefault(new Locale("no", "NO"));
        launch(args);
    }

    @Override
    public void start(Stage stage) {
        this.stage = stage;
        stage.setTitle("Period prototype ");
        initUI();
        stage.getScene().getStylesheets().add(getClass().getResource("/period-picker.css").toExternalForm());
        stage.show();
    }

    private void initUI() {
        VBox vbox = new VBox(20);
        vbox.setStyle("-fx-padding: 10;");
        Scene scene = new Scene(vbox, 400, 200);


        stage.setScene(scene);
        final PeriodPickerPrototype periodPickerPrototype = new PeriodPickerPrototype(new PeriodController() {

            SimpleObjectProperty<LocalDate> fromDate = new SimpleObjectProperty<>();
            SimpleObjectProperty<LocalDate> toDate = new SimpleObjectProperty<>();

            {
                final ChangeListener<LocalDate> dateListener = (observable, oldValue, newValue) -> {
                    if (fromDate.getValue() != null && toDate.getValue() != null) {
                        out.println("Selected period " + fromDate.getValue() + " - " + toDate.getValue());
                    }
                };
                fromDate.addListener(dateListener);
                toDate.addListener(dateListener);

            }


            @Override public LocalDate currentDate() {
                return LocalDate.now();
            }

            @Override public SimpleObjectProperty<LocalDate> fromDateProperty() {
                return fromDate;
            }

            @Override public SimpleObjectProperty<LocalDate> toDateProperty() {
                return toDate;
            }


        });

        GridPane gridPane = new GridPane();
        gridPane.setHgap(10);
        gridPane.setVgap(10);
        Label checkInlabel = new Label("Check-In Date:");
        GridPane.setHalignment(checkInlabel, HPos.LEFT);
        gridPane.add(periodPickerPrototype, 0, 1);
        vbox.getChildren().add(gridPane);
    }
}







import java.time.LocalDate;

import javafx.beans.value.ChangeListener;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.DateCell;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.util.Callback;
import javafx.util.StringConverter;


/**
 * Selecting a single date or a period - only a prototype.
 * As long as you have made an active choice on the {@code toDate}, the {@code fromDate} and {@code toDate} will have the same date.
 */
public class PeriodPickerPrototype extends GridPane {

    private static final String CSS_CALENDAR_BEFORE = "calendar-before";
    private static final String CSS_CALENDAR_BETWEEN = "calendar-between";
    private static final String CSS_CALENDAR_TODAY = "calendar-today";
    private static final boolean DISPLAY_WEEK_NUMBER = true;

    private Label fromLabel;
    private Label toLabel;

    private DatePicker fromDate;
    private DatePicker toDate;
    private StringConverter<LocalDate> converter;
    private PeriodController controller;
    private ChangeListener<LocalDate> fromDateListener;
    private ChangeListener<LocalDate> toDateListener;
    private Callback<DatePicker, DateCell> toDateCellFactory;
    private Callback<DatePicker, DateCell> fromDateCellFactory;
    private Tooltip todayTooltip;
    private boolean toDateIsActivlyChosenbyUser;

    public PeriodPickerPrototype(final PeriodController periodController)

    {
        this.controller = periodController;
        createComponents();
        makeLayout();
        createHandlers();
        bindAndRegisterHandlers();
        i18n();
        initComponent();
    }

    public void createComponents() {
        fromLabel = new Label();
        toLabel = new Label();
        fromDate = new DatePicker();
        toDate = new DatePicker();
        todayTooltip = new Tooltip();
    }

    public void createHandlers() {
        fromDate.setOnAction(event -> {
            if ((!toDateIsActivlyChosenbyUser) || fromDate.getValue().isAfter(toDate.getValue())) {
                setDateWithoutFiringEvent(fromDate.getValue(), toDate);
                toDateIsActivlyChosenbyUser = false;
            }

        });

        toDate.setOnAction(event -> toDateIsActivlyChosenbyUser = true);

        fromDateCellFactory = 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);
                        getStyleClass().removeAll(CSS_CALENDAR_TODAY, CSS_CALENDAR_BEFORE, CSS_CALENDAR_BETWEEN);

                        if ((item.isBefore(toDate.getValue()) || item.isEqual(toDate.getValue())) && item.isAfter(fromDate.getValue())) {
                            getStyleClass().add(CSS_CALENDAR_BETWEEN);
                        }

                        if (item.isEqual(controller.currentDate())) {
                            getStyleClass().add(CSS_CALENDAR_TODAY);
                            setTooltip(todayTooltip);
                        } else {
                            setTooltip(null);
                        }
                    }
                };
            }
        };

        toDateCellFactory =
                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);
                                setDisable(item.isBefore(fromDate.getValue()));
                                getStyleClass().removeAll(CSS_CALENDAR_TODAY, CSS_CALENDAR_BEFORE, CSS_CALENDAR_BETWEEN);


                                if (item.isBefore(fromDate.getValue())) {
                                    getStyleClass().add(CSS_CALENDAR_BEFORE);
                                } else if (item.isBefore(toDate.getValue()) || item.isEqual(toDate.getValue())) {
                                    getStyleClass().add(CSS_CALENDAR_BETWEEN);
                                }
                                if (item.isEqual(controller.currentDate())) {
                                    getStyleClass().add(CSS_CALENDAR_TODAY);
                                    setTooltip(todayTooltip);
                                } else {
                                    setTooltip(null);
                                }
                            }
                        };
                    }
                };
        converter = new DateConverter();
        fromDateListener = (observableValue, oldValue, newValue) -> {
            if (newValue == null) {
                // Restting old value and cancel..
                setDateWithoutFiringEvent(oldValue, fromDate);
                return;
            }
            controller.fromDateProperty().set(newValue);
        };
        toDateListener = (observableValue, oldValue, newValue) -> {
            if (newValue == null) {
                // Restting old value and cancel..
                setDateWithoutFiringEvent(oldValue, toDate);
                return;
            }
            controller.toDateProperty().set(newValue);
        };

    }

    /**
     * Changes the date on {@code datePicker} without fire {@code onAction} event.
     */
    private void setDateWithoutFiringEvent(LocalDate newDate, DatePicker datePicker) {
        final EventHandler<ActionEvent> onAction = datePicker.getOnAction();
        datePicker.setOnAction(null);
        datePicker.setValue(newDate);
        datePicker.setOnAction(onAction);
    }

    public void bindAndRegisterHandlers() {
        toDate.setDayCellFactory(toDateCellFactory);
        fromDate.setDayCellFactory(fromDateCellFactory);
        fromDate.valueProperty().addListener(fromDateListener);
        fromDate.setConverter(converter);
        toDate.valueProperty().addListener(toDateListener);
        toDate.setConverter(converter);

    }

    public void makeLayout() {
        setHgap(6);
        add(fromLabel, 0, 0);
        add(fromDate, 1, 0);
        add(toLabel, 2, 0);
        add(toDate, 3, 0);

        fromDate.setPrefWidth(120);
        toDate.setPrefWidth(120);
        fromLabel.setId("calendar-label");
        toLabel.setId("calendar-label");
    }

    public void i18n() {
        // i18n code replaced with
        fromDate.setPromptText("dd.mm.yyyy");
        toDate.setPromptText("dd.mm.yyyy");
        fromLabel.setText("From");
        toLabel.setText("To");
        todayTooltip.setText("Today");
    }

    public void initComponent() {
        fromDate.setTooltip(null);   // Ønsker ikke tooltip
        setDateWithoutFiringEvent(controller.currentDate(), fromDate);
        fromDate.setShowWeekNumbers(DISPLAY_WEEK_NUMBER);

        toDate.setTooltip(null);   // Ønsker ikke tooltip
        setDateWithoutFiringEvent(controller.currentDate(), toDate);
        toDate.setShowWeekNumbers(DISPLAY_WEEK_NUMBER);
    }


}

/** period-picker.css goes udner resources (using maven) **/ 

.date-picker {
    /*    -fx-font-size: 11pt;*/
}

.calendar-before {
}

.calendar-between {
    -fx-background-color: #bce9ff;
}

.calendar-between:hover {
    -fx-background-color: rgb(0, 150, 201);
}

.calendar-between:focused {
    -fx-background-color: rgb(0, 150, 201);
}

.calendar-today {
    -fx-background-color: rgb(255, 218, 111);
}

.calendar-today:hover {
    -fx-background-color: rgb(0, 150, 201);
}

.calendar-today:focused {
    -fx-background-color: rgb(0, 150, 201);
}

#calendar-label {
    -fx-font-style: italic;
    -fx-fill: rgb(75, 75, 75);
    -fx-font-size: 11;
}
Skjalg
  • 763
  • 5
  • 13

2 Answers2

7

I think you are already in the right track... DateCell and drag could work, since the popup is not closed if a dragging event is detected or when it ends. That gives you the opportunity to track the cells selected by the user.

This is a quick hack, but it may help you with the range selection.

First it will get the content and a list of all the cells within the displayed month, adding a listener to drag events, marking as the first cell that where the drag starts, and selecting all the cells within this first cell and the cell under the actual mouse position, deselecting the rest.

After the drag event finished, the selected range is shown on the console. And you can start all over again, until the popup is closed.

private DateCell iniCell=null;
private DateCell endCell=null;

@Override
public void start(Stage primaryStage) {
    DatePicker datePicker=new DatePicker();
    datePicker.setValue(LocalDate.now());

    Scene scene = new Scene(new AnchorPane(datePicker), 300, 250);

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

    datePicker.showingProperty().addListener((obs,b,b1)->{
        if(b1){
            DatePickerContent content = (DatePickerContent)((DatePickerSkin)datePicker.getSkin()).getPopupContent();

            List<DateCell> cells = content.lookupAll(".day-cell").stream()
                    .filter(ce->!ce.getStyleClass().contains("next-month"))
                    .map(n->(DateCell)n)
                    .collect(Collectors.toList());

            content.setOnMouseDragged(e->{
                Node n=e.getPickResult().getIntersectedNode();
                DateCell c=null;
                if(n instanceof DateCell){
                    c=(DateCell)n;
                } else if(n instanceof Text){
                    c=(DateCell)(n.getParent());
                }
                if(c!=null && c.getStyleClass().contains("day-cell") &&
                        !c.getStyleClass().contains("next-month")){
                    if(iniCell==null){
                        iniCell=c;
                    }
                    endCell=c;
                }
                if(iniCell!=null && endCell!=null){
                    int ini=(int)Math.min(Integer.parseInt(iniCell.getText()), 
                            Integer.parseInt(endCell.getText()));
                    int end=(int)Math.max(Integer.parseInt(iniCell.getText()), 
                            Integer.parseInt(endCell.getText()));
                    cells.stream()
                        .forEach(ce->ce.getStyleClass().remove("selected"));
                    cells.stream()
                        .filter(ce->Integer.parseInt(ce.getText())>=ini)
                        .filter(ce->Integer.parseInt(ce.getText())<=end)
                        .forEach(ce->ce.getStyleClass().add("selected"));
                }
            });
            content.setOnMouseReleased(e->{
                if(iniCell!=null && endCell!=null){
                    System.out.println("Selection from "+iniCell.getText()+" to "+endCell.getText());
                }
                endCell=null;
                iniCell=null;                    
            });
        }
    });
}

And this is how it looks like:

Range selection on DatePicker

For now this doesn't update the textfield, as this involves using a custom formatter.

EDIT

I've added a custom string converter to show the range on the textfield, after a selection is done, and also to select a range if a valid one is entered.

This is not bullet proof, but it works as a proof of concept.

private DateCell iniCell=null;
private DateCell endCell=null;

private LocalDate iniDate;
private LocalDate endDate;
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d.MM.uuuu", Locale.ENGLISH);    

@Override
public void start(Stage primaryStage) {
    DatePicker datePicker=new DatePicker();
    datePicker.setValue(LocalDate.now());
    datePicker.setConverter(new StringConverter<LocalDate>() {

        @Override
        public String toString(LocalDate object) {
            if(iniDate!=null && endDate!=null){
                return iniDate.format(formatter)+" - "+endDate.format(formatter);
            }
            return object.format(formatter);
        }

        @Override
        public LocalDate fromString(String string) {
            if(string.contains("-")){
                try{
                    iniDate=LocalDate.parse(string.split("-")[0].trim(), formatter);
                    endDate=LocalDate.parse(string.split("-")[1].trim(), formatter);
                } catch(DateTimeParseException dte){
                    return LocalDate.parse(string, formatter);
                }
                return iniDate;
            }
            return LocalDate.parse(string, formatter);
        }
    });
    Scene scene = new Scene(new AnchorPane(datePicker), 300, 250);

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

    datePicker.showingProperty().addListener((obs,b,b1)->{
        if(b1){
            DatePickerContent content = (DatePickerContent)((DatePickerSkin)datePicker.getSkin()).getPopupContent();

            List<DateCell> cells = content.lookupAll(".day-cell").stream()
                    .filter(ce->!ce.getStyleClass().contains("next-month"))
                    .map(n->(DateCell)n)
                    .collect(Collectors.toList());

            // select initial range
            if(iniDate!=null && endDate!=null){
                int ini=iniDate.getDayOfMonth();
                int end=endDate.getDayOfMonth();
                cells.stream()
                    .forEach(ce->ce.getStyleClass().remove("selected"));
                cells.stream()
                    .filter(ce->Integer.parseInt(ce.getText())>=ini)
                    .filter(ce->Integer.parseInt(ce.getText())<=end)
                    .forEach(ce->ce.getStyleClass().add("selected"));
            }
            iniCell=null; 
            endCell=null;
            content.setOnMouseDragged(e->{
                Node n=e.getPickResult().getIntersectedNode();
                DateCell c=null;
                if(n instanceof DateCell){
                    c=(DateCell)n;
                } else if(n instanceof Text){
                    c=(DateCell)(n.getParent());
                }
                if(c!=null && c.getStyleClass().contains("day-cell") &&
                        !c.getStyleClass().contains("next-month")){
                    if(iniCell==null){
                        iniCell=c;
                    }
                    endCell=c;
                }
                if(iniCell!=null && endCell!=null){
                    int ini=(int)Math.min(Integer.parseInt(iniCell.getText()), 
                            Integer.parseInt(endCell.getText()));
                    int end=(int)Math.max(Integer.parseInt(iniCell.getText()), 
                            Integer.parseInt(endCell.getText()));
                    cells.stream()
                        .forEach(ce->ce.getStyleClass().remove("selected"));
                    cells.stream()
                        .filter(ce->Integer.parseInt(ce.getText())>=ini)
                        .filter(ce->Integer.parseInt(ce.getText())<=end)
                        .forEach(ce->ce.getStyleClass().add("selected"));
                }
            });
            content.setOnMouseReleased(e->{
                if(iniCell!=null && endCell!=null){
                    iniDate=LocalDate.of(datePicker.getValue().getYear(), 
                                         datePicker.getValue().getMonth(),
                                         Integer.parseInt(iniCell.getText()));
                    endDate=LocalDate.of(datePicker.getValue().getYear(),
                                         datePicker.getValue().getMonth(),
                                         Integer.parseInt(endCell.getText()));
                    System.out.println("Selection from "+iniDate+" to "+endDate);

                    datePicker.setValue(iniDate);
                    int ini=iniDate.getDayOfMonth();
                    int end=endDate.getDayOfMonth();
                    cells.stream()
                        .forEach(ce->ce.getStyleClass().remove("selected"));
                    cells.stream()
                        .filter(ce->Integer.parseInt(ce.getText())>=ini)
                        .filter(ce->Integer.parseInt(ce.getText())<=end)
                        .forEach(ce->ce.getStyleClass().add("selected"));
                }
                endCell=null;
                iniCell=null;                   
            });
        }
    });
}

Range selection and edition

José Pereda
  • 44,311
  • 7
  • 104
  • 132
  • This was in line with what I was looking for. I will post back a more robust solution later. I need to be able to pick days into the next month, so instead of using 'getText' to derive the date, I will use reflection to access 'dayCellDate' on 'DatePickerContent' to correctly identify the 'LocalDate'. Also, when the selection is made, it should close, so I will add a 'datePicker.hide();'. It should work both backwards and forwards so adding a first/last method will allow you to select 25 first and 11 last (in the example). I appreciate your proof of concept. – Skjalg Dec 02 '14 at 13:27
  • Thanks. By `next month`do you mean selecting the days already shown in the same grid, right? I just removed them from the cells selection for simplicity. They have a `next-month` style so you won't need reflection, just set for these `datePicker.getValue().getMonth()+1` – José Pereda Dec 02 '14 at 13:39
  • Yes, I mean selecting the days allready shown. In the picture above you could choose for instance 31. december to 3rd og January. I realized the 'next-month' was there for simplicity. I realize many people are against the use of reflection in OO, but in my opionion it makes more sense here to use an existing method rather than to calculate it, however simpe. – Skjalg Dec 02 '14 at 13:50
  • I don't have anything against reflection. I just think for this case is really simple get the `LocalDate` from the cells by looking at the CSS styles `previous-month` or `next-month`, as we're already doing for selecting them with `selected`. – José Pereda Dec 02 '14 at 13:58
  • That is a fair point for consistency. I just find using the text of a button as starting point for finding the date is a sign of the API for the `DatePickerContent` being too restrictive. And what I would like is for the method to be public, I really do not understand why it is not. Further, it will require more testing for border cases, such as years etc, so why not use something that is already tested. – Skjalg Dec 02 '14 at 14:23
  • Yes, there are many controls that for a *extended* use require accesing private API. The good thing is we still can look into the source code for the proper way to do it. Private doesn't mean hidden. I guess it will eventually become public, but being this way gives them the chance to keep improving the API. – José Pereda Dec 02 '14 at 14:31
  • I just realized that if you move the code `datePicker.showingProperty().addListener` before `primaryStage.show();` the selected period will not be rendered correctly when you reopen the calendar. Ex. make a selection of 3 days, say, then reopen the calendar. Any idea why? – Skjalg Dec 03 '14 at 19:44
  • 1
    Yes, the reason is the lookups. They have to be called after the stage has been shown or they won't work. If you create the listener before, you need to call `applyCss()` and `layout()`, like in this [answer](http://stackoverflow.com/a/27105817/3956070). – José Pereda Dec 03 '14 at 20:27
  • That works, but makes it less useful as a reusable component, where the component may first be visible after a long time. I have to remember to call a mehod similair to this after the component is visible. `public void lateBinding() { applyCss(); layout(); showingProperty().addListener(showListener); }` – Skjalg Dec 03 '14 at 21:33
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/66163/discussion-between-jose-pereda-and-skjalg). – José Pereda Dec 03 '14 at 21:47
  • Problem is this is using private api's and no good with Java 9+ – trilogy Mar 11 '20 at 16:46
2

By using this answer here: https://stackoverflow.com/a/60618476/9278333

I was able to create this date range selector without the use of a private api:

enter image description here

Usage:

MultiDatePicker multiDatePicker = new MultiDatePicker().withRangeSelectionMode();

DatePicker rangePicker = multiDatePicker.getDatePicker();

import javafx.collections.FXCollections;
import javafx.scene.control.*;
import javafx.util.StringConverter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import static java.time.temporal.ChronoUnit.DAYS;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import javafx.collections.ObservableSet;
import javafx.event.EventHandler;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;

public class MultiDatePicker
{

    private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private final ObservableSet<LocalDate> selectedDates;
    private final DatePicker datePicker;

    public MultiDatePicker()
    {
        this.selectedDates = FXCollections.observableSet(new TreeSet<>());
        this.datePicker = new DatePicker();
        setUpDatePicker();
    }

    public MultiDatePicker withRangeSelectionMode()
    {

        EventHandler<MouseEvent> mouseClickedEventHandler = (MouseEvent clickEvent) ->
        {
            if (clickEvent.getButton() == MouseButton.PRIMARY)
            {
                if (!this.selectedDates.contains(this.datePicker.getValue()))
                {
                    this.selectedDates.add(datePicker.getValue());

                    this.selectedDates.addAll(getRangeGaps((LocalDate) this.selectedDates.toArray()[0], (LocalDate) this.selectedDates.toArray()[this.selectedDates.size() - 1]));

                } else
                {
                    this.selectedDates.remove(this.datePicker.getValue());
                    this.selectedDates.removeAll(getTailEndDatesToRemove(this.selectedDates, this.datePicker.getValue()));

                    this.datePicker.setValue(getClosestDateInTree(new TreeSet<>(this.selectedDates), this.datePicker.getValue()));

                }

            }
            this.datePicker.show();
            clickEvent.consume();
        };

        this.datePicker.setDayCellFactory((DatePicker param) -> new DateCell()
        {

            @Override
            public void updateItem(LocalDate item, boolean empty)
            {
                super.updateItem(item, empty);

                //...
                if (item != null && !empty)
                {
                    //...
                    addEventHandler(MouseEvent.MOUSE_CLICKED, mouseClickedEventHandler);
                } else
                {
                    //...
                    removeEventHandler(MouseEvent.MOUSE_CLICKED, mouseClickedEventHandler);
                }

                if (!selectedDates.isEmpty() && selectedDates.contains(item))
                {
                    if (Objects.equals(item, selectedDates.toArray()[0]) || Objects.equals(item, selectedDates.toArray()[selectedDates.size() - 1]))
                    {
                        setStyle("-fx-background-color: rgba(3, 169, 1, 0.7);");
                    } else
                    {
                        setStyle("-fx-background-color: rgba(3, 169, 244, 0.7);");
                    }
                } else
                {
                    setStyle(null);
                }

            }
        });
        return this;
    }

    public ObservableSet<LocalDate> getSelectedDates()
    {
        return this.selectedDates;
    }

    public DatePicker getDatePicker()
    {
        return this.datePicker;
    }

    private void setUpDatePicker()
    {
        this.datePicker.setConverter(new StringConverter<LocalDate>()
        {
            @Override
            public String toString(LocalDate date)
            {
                return (date == null) ? "" : DATE_FORMAT.format(date);
            }

            @Override
            public LocalDate fromString(String string)
            {
                return ((string == null) || string.isEmpty()) ? null : LocalDate.parse(string, DATE_FORMAT);
            }
        });

        EventHandler<MouseEvent> mouseClickedEventHandler = (MouseEvent clickEvent) ->
        {
            if (clickEvent.getButton() == MouseButton.PRIMARY)
            {
                if (!this.selectedDates.contains(this.datePicker.getValue()))
                {
                    this.selectedDates.add(datePicker.getValue());

                } else
                {
                    this.selectedDates.remove(this.datePicker.getValue());

                    this.datePicker.setValue(getClosestDateInTree(new TreeSet<>(this.selectedDates), this.datePicker.getValue()));

                }

            }
            this.datePicker.show();
            clickEvent.consume();
        };

        this.datePicker.setDayCellFactory((DatePicker param) -> new DateCell()
        {
            @Override
            public void updateItem(LocalDate item, boolean empty)
            {
                super.updateItem(item, empty);

                //...
                if (item != null && !empty)
                {
                    //...
                    addEventHandler(MouseEvent.MOUSE_CLICKED, mouseClickedEventHandler);
                } else
                {
                    //...
                    removeEventHandler(MouseEvent.MOUSE_CLICKED, mouseClickedEventHandler);
                }

                if (selectedDates.contains(item))
                {

                    setStyle("-fx-background-color: rgba(3, 169, 244, 0.7);");

                } else
                {
                    setStyle(null);

                }
            }
        });

    }

    private static Set<LocalDate> getTailEndDatesToRemove(Set<LocalDate> dates, LocalDate date)
    {

        TreeSet<LocalDate> tempTree = new TreeSet<>(dates);

        tempTree.add(date);

        int higher = tempTree.tailSet(date).size();
        int lower = tempTree.headSet(date).size();

        if (lower <= higher)
        {
            return tempTree.headSet(date);
        } else if (lower > higher)
        {
            return tempTree.tailSet(date);
        } else
        {
            return new TreeSet<>();
        }

    }

    private static LocalDate getClosestDateInTree(TreeSet<LocalDate> dates, LocalDate date)
    {
        Long lower = null;
        Long higher = null;

        if (dates.isEmpty())
        {
            return null;
        }

        if (dates.size() == 1)
        {
            return dates.first();
        }

        if (dates.lower(date) != null)
        {
            lower = Math.abs(DAYS.between(date, dates.lower(date)));
        }
        if (dates.higher(date) != null)
        {
            higher = Math.abs(DAYS.between(date, dates.higher(date)));
        }

        if (lower == null)
        {
            return dates.higher(date);
        } else if (higher == null)
        {
            return dates.lower(date);
        } else if (lower <= higher)
        {
            return dates.lower(date);
        } else if (lower > higher)
        {
            return dates.higher(date);
        } else
        {
            return null;
        }
    }

    private static Set<LocalDate> getRangeGaps(LocalDate min, LocalDate max)
    {
        Set<LocalDate> rangeGaps = new LinkedHashSet<>();

        if (min == null || max == null)
        {
            return rangeGaps;
        }

        LocalDate lastDate = min.plusDays(1);
        while (lastDate.isAfter(min) && lastDate.isBefore(max))
        {
            rangeGaps.add(lastDate);
            lastDate = lastDate.plusDays(1);

        }
        return rangeGaps;
    }
}
trilogy
  • 1,738
  • 15
  • 31