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.StyleableProperty;
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 java.time.DateTimeException;
@ -227,6 +231,34 @@ public class InlineDatePicker extends Control {
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 //
///////////////////////////////////////////////////////////////////////////

@ -114,6 +114,30 @@ public class InlineDatePickerSkin extends BehaviorSkinBase<InlineDatePicker, Inl
updateGrid();
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
@ -153,6 +177,11 @@ public class InlineDatePickerSkin extends BehaviorSkinBase<InlineDatePicker, Inl
///////////////////////////////////////////////////////////////////////////
protected void createUI() {
if (getControl().getTopNode() != null) {
getControl().getTopNode().getStyleClass().add("top-node");
rootPane.getChildren().add(getControl().getTopNode());
}
// YearMonth //
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.getChildren().add(calendarGrid);
if (getControl().getBottomNode() != null) {
getControl().getBottomNode().getStyleClass().add("bottom-node");
rootPane.getChildren().add(getControl().getBottomNode());
}
getChildren().add(rootPane);
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);
backButton.setGraphic(leftArrow);
Region leftSpacer = new Region();
HBox.setHgrow(leftSpacer, Priority.ALWAYS);
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
monthLabel = new Label();
monthLabel.getStyleClass().add("month-label");
@ -220,9 +254,6 @@ public class InlineDatePickerSkin extends BehaviorSkinBase<InlineDatePicker, Inl
yearLabel = new Label();
yearLabel.getStyleClass().add("year-label");
Region rightSpacer = new Region();
HBox.setHgrow(rightSpacer, Priority.ALWAYS);
forwardButton = new Button();
forwardButton.getStyleClass().addAll("forward-button");
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);
forwardButton.setGraphic(rightArrow);
monthYearPane.getChildren().addAll(backButton, leftSpacer, monthLabel, yearLabel, rightSpacer, forwardButton);
monthYearPane.getChildren().addAll(monthLabel, yearLabel, spacer, backButton, forwardButton);
return monthYearPane;
}

@ -7,26 +7,36 @@ import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.AbstractPage;
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.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.DateCell;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.chrono.HijrahChronology;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import static atlantafx.base.theme.Styles.BUTTON_ICON;
import static atlantafx.base.theme.Styles.FLAT;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import static javafx.scene.layout.GridPane.REMAINING;
public class DatePickerPage extends AbstractPage {
@ -34,17 +44,32 @@ public class DatePickerPage extends AbstractPage {
public static final String NAME = "DatePicker";
private static final LocalDate TODAY = LocalDate.now();
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 int INLINE_DATE_PICKER_COL = 0;
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
public String getName() { return NAME; }
private final BooleanProperty weekNumProperty = new SimpleBooleanProperty();
private final BooleanProperty showClockProperty = new SimpleBooleanProperty();
private final BooleanProperty editableProperty = new SimpleBooleanProperty();
private final BooleanProperty offPastDatesProperty = new SimpleBooleanProperty();
private final BooleanProperty disableProperty = new SimpleBooleanProperty();
private final Clock clock = new Clock();
private DatePickerColorSelector colorSelector;
public DatePickerPage() {
super();
@ -56,15 +81,12 @@ public class DatePickerPage extends AbstractPage {
private GridPane createPlayground() {
var grid = new GridPane();
grid.setHgap(40);
grid.setVgap(SampleBlock.BLOCK_VGAP);
grid.setVgap(BLOCK_VGAP);
final var popupDatePicker = createPopupDatePicker();
final var inlineDatePicker = createInlineDatePicker(null);
var inlineValue = new Label();
inlineValue.setAlignment(Pos.CENTER);
inlineValue.setMaxWidth(Double.MAX_VALUE);
inlineValue.textProperty().bind(inlineDatePicker.valueProperty().asString());
colorSelector = new DatePickerColorSelector(grid);
final var inlineDatePicker = createInlineDatePicker(null);
// == CONTROLS ==
@ -72,6 +94,20 @@ public class DatePickerPage extends AbstractPage {
weekNumProperty.bind(weekNumToggle.selectedProperty());
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");
chronologyToggle.selectedProperty().addListener(
(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);
grid.getChildren().removeIf(n -> n instanceof InlineDatePicker);
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");
disableProperty.bind(disablePickerToggle.selectedProperty());
var controls = new VBox(
SampleBlock.BLOCK_VGAP,
BLOCK_VGAP,
weekNumToggle,
showClockToggle,
showYearMonthToggle,
chronologyToggle,
editableToggle,
offPastDatesToggle,
@ -111,8 +147,6 @@ public class DatePickerPage extends AbstractPage {
);
controls.setAlignment(Pos.CENTER_RIGHT);
var colorSelector = new DatePickerColorSelector(grid);
// == GRID ==
var defaultLabel = new Label("Default");
@ -126,8 +160,6 @@ public class DatePickerPage extends AbstractPage {
grid.add(new Spacer(20), 0, 2);
grid.add(inlineLabel, 0, 3);
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);
return grid;
@ -152,6 +184,14 @@ public class DatePickerPage extends AbstractPage {
datePicker.setValue(TODAY);
datePicker.showWeekNumbersProperty().bind(weekNumProperty);
datePicker.disableProperty().bind(disableProperty);
datePicker.setTopNode(clock);
datePicker.setBottomNode(colorSelector);
datePicker.topNodeProperty().bind(Bindings.createObjectBinding(
() -> showClockProperty.get() ? clock : null, showClockProperty)
);
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
private static class DatePickerColorSelector extends HBox {
private final Pane parent;
private final ObjectProperty<CSSFragment> style = new SimpleObjectProperty<>();
public DatePickerColorSelector(Pane parent) {
super();
this.parent = parent;
this.parent = Objects.requireNonNull(parent);
createView();
}
private void createView() {
var resetBtn = new Button("", new FontIcon(Material2AL.CLEAR));
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);
getChildren().setAll(
@ -217,33 +287,27 @@ public class DatePickerPage extends AbstractPage {
var btn = new Button("", icon);
btn.getStyleClass().addAll(BUTTON_ICON, FLAT, "color-button");
btn.setOnAction(e -> setColorVariables(bgColorName, fgColorName));
btn.setOnAction(e -> updateStyle(bgColorName, fgColorName));
return btn;
}
private void setColorVariables(String bgColorName, String fgColorName) {
for (Node node : parent.getChildren()) {
var style = String.format("-color-date-border:%s;-color-date-month-year-bg:%s;-color-date-month-year-fg:%s;",
bgColorName,
bgColorName,
fgColorName
);
if (node instanceof InlineDatePicker dp) {
var popup = dp.lookup(".date-picker-popup");
if (popup != null) { popup.setStyle(style); }
}
}
}
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); }
}
}
private void updateStyle(String bgColorName, String fgColorName) {
style.set(new CSSFragment(String.format("""
.date-picker-popup {
-color-date-border: %s;
-color-date-month-year-bg: %s;
-color-date-month-year-fg: %s;
}
.date-picker-popup >.top-node {
-fx-background-color: %s;
-color-fg-default: %s;
-color-border-default: %s;
-fx-border-color: %s;
}""",
bgColorName, bgColorName, fgColorName,
bgColorName, fgColorName, fgColorName, fgColorName
)));
}
}
}

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

@ -173,9 +173,15 @@ $chrono-cell-padding: 0.083333em $cell-padding-x 0.083333em 0.333333em !default;
.inline-date-picker {
-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 {
-fx-alignment: CENTER;
-fx-spacing: 10px;
-fx-padding: $content-padding-y calc($content-padding-x * 2) $content-padding-y calc($content-padding-x * 2);
-fx-alignment: CENTER_LEFT;
-fx-spacing: 6px;
>.button {
-fx-background-color: transparent;
@ -185,6 +191,7 @@ $chrono-cell-padding: 0.083333em $cell-padding-x 0.083333em 0.333333em !default;
}
>.back-button {
-fx-padding: 0 1em 0 0;
>.left-arrow {
@include icons.get("chevron-left", false);
-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>.secondary-text,
.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 {
-fx-background-color: -color-date-day-bg-selected;
-fx-text-fill: -color-date-day-fg-selected;