Choosing a Period or Date Using ONE JavaFX 8 DatePicker

In the application I'm currently working on, I need to select one date or period from the same JavaFX 8 DatePicker.

The preferred way to do this would be as follows:

  • Selecting a single date is the same as the default behavior for DatePicker.

  • Period selection - select the start / end date by holding down the mouse button and dragging it to the desired end / start date. When the mouse button is released, you have determined your period. Acceptable is that you cannot select dates other than those displayed.

  • Editing should work both for one date (ex 24.12.2014) and for a period (for example: 12.24.2014 - 12.27.2014)

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

Rendering of selected period

If orange indicates the current date, blue indicates the selected period. The image is taken from a prototype that I made, but where the period is selected using 2 DatePickers, not one.

I looked at the source code for

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

which has

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

to find a detection method when the mouse chose the end date when the mouse was released (or, possibly, drag and drop detection).

However, I'm not quite sure how to do this. Any suggestions?

I am attaching a simple prototype code that I have made so far (which uses 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; } 
+5
source share
1 answer

I think that you are already on the right track ... DateCell and drag and drop may work because the popup does not close if a drag event is detected or when it ends. This gives you the ability to track cells selected by the user.

This is a quick hack, but it can help you with a range choice.

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

After the drag event is completed, the selected range is displayed 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 here is what it looks like:

Range selection on DatePicker

This is not updating the text box so far, as it is due to the use of custom formatting.

EDIT

I added a custom string converter to display the range in the text box after selection, and also to select the range if the correct 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

+5
source

Source: https://habr.com/ru/post/1208036/


All Articles