Improve date picker

- Support adding top/bottom node to embed custom user widgets
- Move prev/next arrows to the right side
- Add clock example
- Add show/hide header example
- Fix unreadable next month today fg color
This commit is contained in:
mkpaz 2022-09-29 16:21:24 +04:00
parent 54ae621bed
commit a295fd799c
5 changed files with 186 additions and 50 deletions

@ -35,7 +35,11 @@ import javafx.css.Styleable;
import javafx.css.StyleableBooleanProperty; import javafx.css.StyleableBooleanProperty;
import javafx.css.StyleableProperty; import javafx.css.StyleableProperty;
import javafx.css.converter.BooleanConverter; import javafx.css.converter.BooleanConverter;
import javafx.scene.control.*; import javafx.scene.Node;
import javafx.scene.control.Cell;
import javafx.scene.control.Control;
import javafx.scene.control.DateCell;
import javafx.scene.control.Skin;
import javafx.util.Callback; import javafx.util.Callback;
import java.time.DateTimeException; import java.time.DateTimeException;
@ -227,6 +231,34 @@ public class InlineDatePicker extends Control {
return showWeekNumbersProperty().getValue(); return showWeekNumbersProperty().getValue();
} }
private final ObjectProperty<Node> topNode = new SimpleObjectProperty<>(this, "topNode", null);
public final void setTopNode(Node value) {
topNode.setValue(value);
}
public final Node getTopNode() {
return topNode.getValue();
}
public ObjectProperty<Node> topNodeProperty() {
return topNode;
}
private final ObjectProperty<Node> bottomNode = new SimpleObjectProperty<>(this, "bottomNode", null);
public final void setBottomNode(Node value) {
bottomNode.setValue(value);
}
public final Node getBottomNode() {
return bottomNode.getValue();
}
public ObjectProperty<Node> bottomNodeProperty() {
return bottomNode;
}
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Stylesheet Handling // // Stylesheet Handling //
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////

@ -114,6 +114,30 @@ public class InlineDatePickerSkin extends BehaviorSkinBase<InlineDatePicker, Inl
updateGrid(); updateGrid();
updateWeekNumberCells(); updateWeekNumberCells();
}); });
registerChangeListener(datePicker.topNodeProperty(), e -> {
Node node = datePicker.getTopNode();
if (node == null) {
rootPane.getChildren().removeIf(c -> c.getStyleClass().contains("top-node"));
} else {
if (!node.getStyleClass().contains("top-node")) {
node.getStyleClass().add("top-node");
}
rootPane.getChildren().add(0, node);
}
});
registerChangeListener(datePicker.bottomNodeProperty(), e -> {
Node node = datePicker.getBottomNode();
if (node == null) {
rootPane.getChildren().removeIf(c -> c.getStyleClass().contains("bottom-node"));
} else {
if (!node.getStyleClass().contains("bottom-node")) {
node.getStyleClass().add("bottom-node");
}
rootPane.getChildren().add(node);
}
});
} }
@Override @Override
@ -153,6 +177,11 @@ public class InlineDatePickerSkin extends BehaviorSkinBase<InlineDatePicker, Inl
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
protected void createUI() { protected void createUI() {
if (getControl().getTopNode() != null) {
getControl().getTopNode().getStyleClass().add("top-node");
rootPane.getChildren().add(getControl().getTopNode());
}
// YearMonth // // YearMonth //
LocalDate value = getControl().getValue(); LocalDate value = getControl().getValue();
@ -191,6 +220,11 @@ public class InlineDatePickerSkin extends BehaviorSkinBase<InlineDatePicker, Inl
rootPane.getStyleClass().addAll("date-picker-popup", "inline-date-picker"); rootPane.getStyleClass().addAll("date-picker-popup", "inline-date-picker");
rootPane.getChildren().add(calendarGrid); rootPane.getChildren().add(calendarGrid);
if (getControl().getBottomNode() != null) {
getControl().getBottomNode().getStyleClass().add("bottom-node");
rootPane.getChildren().add(getControl().getBottomNode());
}
getChildren().add(rootPane); getChildren().add(rootPane);
getControl().setOnKeyPressed(e -> behavior.onKeyPressed(e)); getControl().setOnKeyPressed(e -> behavior.onKeyPressed(e));
@ -211,8 +245,8 @@ public class InlineDatePickerSkin extends BehaviorSkinBase<InlineDatePicker, Inl
leftArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE); leftArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
backButton.setGraphic(leftArrow); backButton.setGraphic(leftArrow);
Region leftSpacer = new Region(); Region spacer = new Region();
HBox.setHgrow(leftSpacer, Priority.ALWAYS); HBox.setHgrow(spacer, Priority.ALWAYS);
monthLabel = new Label(); monthLabel = new Label();
monthLabel.getStyleClass().add("month-label"); monthLabel.getStyleClass().add("month-label");
@ -220,9 +254,6 @@ public class InlineDatePickerSkin extends BehaviorSkinBase<InlineDatePicker, Inl
yearLabel = new Label(); yearLabel = new Label();
yearLabel.getStyleClass().add("year-label"); yearLabel.getStyleClass().add("year-label");
Region rightSpacer = new Region();
HBox.setHgrow(rightSpacer, Priority.ALWAYS);
forwardButton = new Button(); forwardButton = new Button();
forwardButton.getStyleClass().addAll("forward-button"); forwardButton.getStyleClass().addAll("forward-button");
forwardButton.setOnMouseClicked(e -> behavior.moveForward(e)); forwardButton.setOnMouseClicked(e -> behavior.moveForward(e));
@ -232,7 +263,7 @@ public class InlineDatePickerSkin extends BehaviorSkinBase<InlineDatePicker, Inl
rightArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE); rightArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
forwardButton.setGraphic(rightArrow); forwardButton.setGraphic(rightArrow);
monthYearPane.getChildren().addAll(backButton, leftSpacer, monthLabel, yearLabel, rightSpacer, forwardButton); monthYearPane.getChildren().addAll(monthLabel, yearLabel, spacer, backButton, forwardButton);
return monthYearPane; return monthYearPane;
} }

@ -7,26 +7,36 @@ import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.SampleBlock;
import atlantafx.sampler.theme.CSSFragment;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.DateCell; import javafx.scene.control.DateCell;
import javafx.scene.control.DatePicker; import javafx.scene.control.DatePicker;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.util.Callback; import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL; import org.kordamp.ikonli.material2.Material2AL;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalTime;
import java.time.chrono.HijrahChronology; import java.time.chrono.HijrahChronology;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Objects;
import static atlantafx.base.theme.Styles.BUTTON_ICON; import static atlantafx.base.theme.Styles.BUTTON_ICON;
import static atlantafx.base.theme.Styles.FLAT; import static atlantafx.base.theme.Styles.FLAT;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import static javafx.scene.layout.GridPane.REMAINING; import static javafx.scene.layout.GridPane.REMAINING;
public class DatePickerPage extends AbstractPage { public class DatePickerPage extends AbstractPage {
@ -34,17 +44,32 @@ public class DatePickerPage extends AbstractPage {
public static final String NAME = "DatePicker"; public static final String NAME = "DatePicker";
private static final LocalDate TODAY = LocalDate.now(); private static final LocalDate TODAY = LocalDate.now();
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_DATE; private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_DATE;
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
private static final String DATE_FORMATTER_PROMPT = "yyyy-MM-dd"; private static final String DATE_FORMATTER_PROMPT = "yyyy-MM-dd";
private static final int INLINE_DATE_PICKER_COL = 0; private static final int INLINE_DATE_PICKER_COL = 0;
private static final int INLINE_DATE_PICKER_ROW = 4; private static final int INLINE_DATE_PICKER_ROW = 4;
private static final CSSFragment NO_YEAR_MONTH_STYLE = new CSSFragment("""
.date-picker-popup >.month-year-pane {
visibility: hidden;
-fx-min-width: 0;
-fx-pref-width: 0;
-fx-max-width: 0;
-fx-min-height: 0;
-fx-pref-height: 0;
-fx-max-height: 0;
}
""");
@Override @Override
public String getName() { return NAME; } public String getName() { return NAME; }
private final BooleanProperty weekNumProperty = new SimpleBooleanProperty(); private final BooleanProperty weekNumProperty = new SimpleBooleanProperty();
private final BooleanProperty showClockProperty = new SimpleBooleanProperty();
private final BooleanProperty editableProperty = new SimpleBooleanProperty(); private final BooleanProperty editableProperty = new SimpleBooleanProperty();
private final BooleanProperty offPastDatesProperty = new SimpleBooleanProperty(); private final BooleanProperty offPastDatesProperty = new SimpleBooleanProperty();
private final BooleanProperty disableProperty = new SimpleBooleanProperty(); private final BooleanProperty disableProperty = new SimpleBooleanProperty();
private final Clock clock = new Clock();
private DatePickerColorSelector colorSelector;
public DatePickerPage() { public DatePickerPage() {
super(); super();
@ -56,15 +81,12 @@ public class DatePickerPage extends AbstractPage {
private GridPane createPlayground() { private GridPane createPlayground() {
var grid = new GridPane(); var grid = new GridPane();
grid.setHgap(40); grid.setHgap(40);
grid.setVgap(SampleBlock.BLOCK_VGAP); grid.setVgap(BLOCK_VGAP);
final var popupDatePicker = createPopupDatePicker(); final var popupDatePicker = createPopupDatePicker();
final var inlineDatePicker = createInlineDatePicker(null);
var inlineValue = new Label(); colorSelector = new DatePickerColorSelector(grid);
inlineValue.setAlignment(Pos.CENTER); final var inlineDatePicker = createInlineDatePicker(null);
inlineValue.setMaxWidth(Double.MAX_VALUE);
inlineValue.textProperty().bind(inlineDatePicker.valueProperty().asString());
// == CONTROLS == // == CONTROLS ==
@ -72,6 +94,20 @@ public class DatePickerPage extends AbstractPage {
weekNumProperty.bind(weekNumToggle.selectedProperty()); weekNumProperty.bind(weekNumToggle.selectedProperty());
weekNumToggle.setSelected(true); weekNumToggle.setSelected(true);
var showClockToggle = new ToggleSwitch("Show clock");
showClockProperty.bind(showClockToggle.selectedProperty());
showClockToggle.setSelected(true);
var showYearMonthToggle = new ToggleSwitch("Show header");
showYearMonthToggle.setSelected(true);
showYearMonthToggle.selectedProperty().addListener((obs, old, val) -> {
if (!val) {
NO_YEAR_MONTH_STYLE.addTo(grid);
} else {
NO_YEAR_MONTH_STYLE.removeFrom(grid);
}
});
var chronologyToggle = new ToggleSwitch("Second chronology"); var chronologyToggle = new ToggleSwitch("Second chronology");
chronologyToggle.selectedProperty().addListener( chronologyToggle.selectedProperty().addListener(
(obs, old, val) -> popupDatePicker.setChronology(val ? HijrahChronology.INSTANCE : null) (obs, old, val) -> popupDatePicker.setChronology(val ? HijrahChronology.INSTANCE : null)
@ -94,16 +130,16 @@ public class DatePickerPage extends AbstractPage {
var datePicker = createInlineDatePicker(val ? dp -> new FutureDateCell() : null); var datePicker = createInlineDatePicker(val ? dp -> new FutureDateCell() : null);
grid.getChildren().removeIf(n -> n instanceof InlineDatePicker); grid.getChildren().removeIf(n -> n instanceof InlineDatePicker);
grid.add(datePicker, INLINE_DATE_PICKER_COL, INLINE_DATE_PICKER_ROW); grid.add(datePicker, INLINE_DATE_PICKER_COL, INLINE_DATE_PICKER_ROW);
inlineValue.textProperty().unbind();
inlineValue.textProperty().bind(datePicker.valueProperty().asString());
}); });
var disablePickerToggle = new ToggleSwitch("Disable"); var disablePickerToggle = new ToggleSwitch("Disable");
disableProperty.bind(disablePickerToggle.selectedProperty()); disableProperty.bind(disablePickerToggle.selectedProperty());
var controls = new VBox( var controls = new VBox(
SampleBlock.BLOCK_VGAP, BLOCK_VGAP,
weekNumToggle, weekNumToggle,
showClockToggle,
showYearMonthToggle,
chronologyToggle, chronologyToggle,
editableToggle, editableToggle,
offPastDatesToggle, offPastDatesToggle,
@ -111,8 +147,6 @@ public class DatePickerPage extends AbstractPage {
); );
controls.setAlignment(Pos.CENTER_RIGHT); controls.setAlignment(Pos.CENTER_RIGHT);
var colorSelector = new DatePickerColorSelector(grid);
// == GRID == // == GRID ==
var defaultLabel = new Label("Default"); var defaultLabel = new Label("Default");
@ -126,8 +160,6 @@ public class DatePickerPage extends AbstractPage {
grid.add(new Spacer(20), 0, 2); grid.add(new Spacer(20), 0, 2);
grid.add(inlineLabel, 0, 3); grid.add(inlineLabel, 0, 3);
grid.add(inlineDatePicker, INLINE_DATE_PICKER_COL, INLINE_DATE_PICKER_ROW); grid.add(inlineDatePicker, INLINE_DATE_PICKER_COL, INLINE_DATE_PICKER_ROW);
grid.add(inlineValue, 0, 5);
grid.add(colorSelector, 0, 6);
grid.add(controls, 1, 0, 1, REMAINING); grid.add(controls, 1, 0, 1, REMAINING);
return grid; return grid;
@ -152,6 +184,14 @@ public class DatePickerPage extends AbstractPage {
datePicker.setValue(TODAY); datePicker.setValue(TODAY);
datePicker.showWeekNumbersProperty().bind(weekNumProperty); datePicker.showWeekNumbersProperty().bind(weekNumProperty);
datePicker.disableProperty().bind(disableProperty); datePicker.disableProperty().bind(disableProperty);
datePicker.setTopNode(clock);
datePicker.setBottomNode(colorSelector);
datePicker.topNodeProperty().bind(Bindings.createObjectBinding(
() -> showClockProperty.get() ? clock : null, showClockProperty)
);
return datePicker; return datePicker;
} }
@ -184,21 +224,51 @@ public class DatePickerPage extends AbstractPage {
} }
} }
private static class Clock extends VBox {
public Clock() {
var clockLabel = new Label(TIME_FORMATTER.format(LocalTime.now()));
clockLabel.getStyleClass().add(Styles.TITLE_2);
var dateLabel = new Label(DateTimeFormatter.ofPattern("EEEE, LLLL dd, yyyy").format(LocalDate.now()));
setStyle("""
-fx-border-width: 0 0 0.5 0;
-fx-border-color: -color-border-default;"""
);
setSpacing(BLOCK_VGAP);
getChildren().setAll(clockLabel, dateLabel);
var t = new Timeline(new KeyFrame(
Duration.seconds(1),
e -> clockLabel.setText(TIME_FORMATTER.format(LocalTime.now()))
));
t.setCycleCount(Animation.INDEFINITE);
t.playFromStart();
}
}
// This class shares stylesheet with the AccentColorSelector // This class shares stylesheet with the AccentColorSelector
private static class DatePickerColorSelector extends HBox { private static class DatePickerColorSelector extends HBox {
private final Pane parent; private final Pane parent;
private final ObjectProperty<CSSFragment> style = new SimpleObjectProperty<>();
public DatePickerColorSelector(Pane parent) { public DatePickerColorSelector(Pane parent) {
super(); super();
this.parent = parent; this.parent = Objects.requireNonNull(parent);
createView(); createView();
} }
private void createView() { private void createView() {
var resetBtn = new Button("", new FontIcon(Material2AL.CLEAR)); var resetBtn = new Button("", new FontIcon(Material2AL.CLEAR));
resetBtn.getStyleClass().addAll(BUTTON_ICON, FLAT); resetBtn.getStyleClass().addAll(BUTTON_ICON, FLAT);
resetBtn.setOnAction(e -> resetColorVariables()); resetBtn.setOnAction(e -> style.set(null));
style.addListener((obs, old, val) -> {
if (old != null) { old.removeFrom(parent); }
if (val != null) { val.addTo(parent); }
});
setAlignment(Pos.CENTER); setAlignment(Pos.CENTER);
getChildren().setAll( getChildren().setAll(
@ -217,33 +287,27 @@ public class DatePickerPage extends AbstractPage {
var btn = new Button("", icon); var btn = new Button("", icon);
btn.getStyleClass().addAll(BUTTON_ICON, FLAT, "color-button"); btn.getStyleClass().addAll(BUTTON_ICON, FLAT, "color-button");
btn.setOnAction(e -> setColorVariables(bgColorName, fgColorName)); btn.setOnAction(e -> updateStyle(bgColorName, fgColorName));
return btn; return btn;
} }
private void setColorVariables(String bgColorName, String fgColorName) { private void updateStyle(String bgColorName, String fgColorName) {
for (Node node : parent.getChildren()) { style.set(new CSSFragment(String.format("""
var style = String.format("-color-date-border:%s;-color-date-month-year-bg:%s;-color-date-month-year-fg:%s;", .date-picker-popup {
bgColorName, -color-date-border: %s;
bgColorName, -color-date-month-year-bg: %s;
fgColorName -color-date-month-year-fg: %s;
); }
.date-picker-popup >.top-node {
if (node instanceof InlineDatePicker dp) { -fx-background-color: %s;
var popup = dp.lookup(".date-picker-popup"); -color-fg-default: %s;
if (popup != null) { popup.setStyle(style); } -color-border-default: %s;
} -fx-border-color: %s;
} }""",
} bgColorName, bgColorName, fgColorName,
bgColorName, fgColorName, fgColorName, fgColorName
private void resetColorVariables() { )));
for (Node node : parent.getChildren()) {
if (node instanceof InlineDatePicker dp) {
var popup = dp.lookup(".date-picker-popup");
if (popup != null) { popup.setStyle(null); }
}
}
} }
} }
} }

@ -24,12 +24,12 @@ public final class CSSFragment {
public void removeFrom(Region region) { public void removeFrom(Region region) {
Objects.requireNonNull(region); Objects.requireNonNull(region);
region.getStyleClass().remove(toDataURI()); region.getStylesheets().remove(toDataURI());
} }
public boolean existsIn(Region region) { public boolean existsIn(Region region) {
Objects.requireNonNull(region); Objects.requireNonNull(region);
return region.getStyleClass().contains(toDataURI()); return region.getStylesheets().contains(toDataURI());
} }
@Override @Override

@ -173,9 +173,15 @@ $chrono-cell-padding: 0.083333em $cell-padding-x 0.083333em 0.333333em !default;
.inline-date-picker { .inline-date-picker {
-fx-effect: none; -fx-effect: none;
>.top-node,
>.bottom-node {
-fx-padding: $content-padding-y calc($content-padding-x * 2) $content-padding-y calc($content-padding-x * 2);
}
>.month-year-pane { >.month-year-pane {
-fx-alignment: CENTER; -fx-padding: $content-padding-y calc($content-padding-x * 2) $content-padding-y calc($content-padding-x * 2);
-fx-spacing: 10px; -fx-alignment: CENTER_LEFT;
-fx-spacing: 6px;
>.button { >.button {
-fx-background-color: transparent; -fx-background-color: transparent;
@ -185,6 +191,7 @@ $chrono-cell-padding: 0.083333em $cell-padding-x 0.083333em 0.333333em !default;
} }
>.back-button { >.back-button {
-fx-padding: 0 1em 0 0;
>.left-arrow { >.left-arrow {
@include icons.get("chevron-left", false); @include icons.get("chevron-left", false);
-fx-background-color: -color-date-month-year-fg; -fx-background-color: -color-date-month-year-fg;
@ -221,6 +228,8 @@ $chrono-cell-padding: 0.083333em $cell-padding-x 0.083333em 0.333333em !default;
.date-picker-popup>.calendar-grid>.selected, .date-picker-popup>.calendar-grid>.selected,
.date-picker-popup>.calendar-grid>.selected>.secondary-text, .date-picker-popup>.calendar-grid>.selected>.secondary-text,
.date-picker-popup>.calendar-grid>.previous-month.selected, .date-picker-popup>.calendar-grid>.previous-month.selected,
.date-picker-popup>.calendar-grid>.previous-month.today.selected,
.date-picker-popup>.calendar-grid>.next-month.today.selected,
.date-picker-popup>.calendar-grid>.next-month.selected { .date-picker-popup>.calendar-grid>.next-month.selected {
-fx-background-color: -color-date-day-bg-selected; -fx-background-color: -color-date-day-bg-selected;
-fx-text-fill: -color-date-day-fg-selected; -fx-text-fill: -color-date-day-fg-selected;