Skip to content

Commit

Permalink
Merge pull request #84 from gldiazcardenas/issue-75-year-picker
Browse files Browse the repository at this point in the history
Issue 75 - Year Picker
  • Loading branch information
dlemmermann authored Sep 7, 2023
2 parents 04454e0 + c3776a9 commit e037562
Show file tree
Hide file tree
Showing 8 changed files with 593 additions and 0 deletions.
48 changes: 48 additions & 0 deletions gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/YearPickerApp.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.dlsc.gemsfx.demo;

import com.dlsc.gemsfx.YearPicker;
import fr.brouillard.oss.cssfx.CSSFX;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class YearPickerApp extends Application {

@Override
public void start(Stage stage) {
YearPicker yearPicker = new YearPicker();

Label valueLabel = new Label();
valueLabel.textProperty().bind(Bindings.convert(yearPicker.yearProperty()));

CheckBox editable = new CheckBox("Editable");
editable.selectedProperty().bindBidirectional(yearPicker.editableProperty());

CheckBox disable = new CheckBox("Disable");
disable.selectedProperty().bindBidirectional(yearPicker.disableProperty());

VBox vBox = new VBox(10, yearPicker, valueLabel, editable, disable);

vBox.setPadding(new Insets(20));
vBox.setAlignment(Pos.TOP_LEFT);

Scene scene = new Scene(vBox);
CSSFX.start();

stage.setTitle("YearPicker");
stage.setScene(scene);
stage.sizeToScene();
stage.centerOnScreen();
stage.show();
}

public static void main(String[] args) {
launch();
}
}
38 changes: 38 additions & 0 deletions gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/YearViewApp.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.dlsc.gemsfx.demo;

import com.dlsc.gemsfx.YearView;
import fr.brouillard.oss.cssfx.CSSFX;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class YearViewApp extends Application {

@Override
public void start(Stage stage) {
YearView view = new YearView();
view.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);

VBox vBox = new VBox(view);

vBox.setPadding(new Insets(20));
vBox.setAlignment(Pos.CENTER);

Scene scene = new Scene(vBox);
CSSFX.start();

stage.setTitle("YearView");
stage.setScene(scene);
stage.sizeToScene();
stage.centerOnScreen();
stage.show();
}

public static void main(String[] args) {
launch();
}
}
145 changes: 145 additions & 0 deletions gemsfx/src/main/java/com/dlsc/gemsfx/YearPicker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package com.dlsc.gemsfx;

import com.dlsc.gemsfx.skins.YearPickerSkin;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.css.PseudoClass;
import javafx.scene.control.ComboBoxBase;
import javafx.scene.control.Skin;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Region;
import javafx.util.converter.NumberStringConverter;
import org.apache.commons.lang3.StringUtils;

import java.text.DecimalFormat;
import java.text.ParsePosition;
import java.time.Year;
import java.util.function.UnaryOperator;

public class YearPicker extends ComboBoxBase<Year> {

private final TextField editor = new TextField();
private final NumberStringFilteredConverter converter = new NumberStringFilteredConverter();

public YearPicker() {
getStyleClass().setAll("year-picker", "text-input");

setFocusTraversable(false);

valueProperty().addListener((obs, oldV, newV) -> {
updateText(newV);
year.set(newV == null ? null : newV.getValue());
});

editor.setTextFormatter(new TextFormatter<>(converter, null, converter.getFilter()));
editor.editableProperty().bind(editableProperty());
editor.setOnAction(evt -> commit());
editor.focusedProperty().addListener(it -> {
if (!editor.isFocused()) {
commit();
}
pseudoClassStateChanged(PseudoClass.getPseudoClass("focused"), editor.isFocused());
});

editor.addEventHandler(KeyEvent.KEY_PRESSED, evt -> {
Year value = getValue();
if (value != null) {
if (evt.getCode().equals(KeyCode.DOWN)) {
setValue(value.plusYears(1));
} else if (evt.getCode().equals(KeyCode.UP)) {
setValue(value.minusYears(1));
}
}
});

setMaxWidth(Region.USE_PREF_SIZE);
updateText(null);
}

/**
* Returns the text field control used for manual input.
*
* @return the editor / text field
*/
public final TextField getEditor() {
return editor;
}

@Override
protected Skin<?> createDefaultSkin() {
return new YearPickerSkin(this);
}

@Override
public String getUserAgentStylesheet() {
return YearMonthView.class.getResource("year-picker.css").toExternalForm();
}

private final ReadOnlyObjectWrapper<Integer> year = new ReadOnlyObjectWrapper<>(this, "wrapper");

public final ReadOnlyObjectProperty<Integer> yearProperty() {
return year.getReadOnlyProperty();
}

public final Integer getYear() {
return year.get();
}

private void commit() {
String text = editor.getText();
if (StringUtils.isNotBlank(text)) {
Number value = converter.fromString(text);
if (value != null) {
setValue(Year.of(value.intValue()));
} else {
setValue(null);
}
}
}

private void updateText(Year value) {
if (value != null) {
editor.setText(String.valueOf(value.getValue()));
} else {
editor.setText("");
}
editor.positionCaret(editor.getText().length());
}

static class NumberStringFilteredConverter extends NumberStringConverter {

public NumberStringFilteredConverter() {
super(new DecimalFormat("####"));
}

UnaryOperator<TextFormatter.Change> getFilter() {
return change -> {
String newText = change.getControlNewText();

if (!newText.isEmpty()) {
// Convert to number
ParsePosition parsePosition = new ParsePosition(0);
Number value = getNumberFormat().parse(newText, parsePosition);
if (value == null || parsePosition.getIndex() < newText.length()) {
return null;
}

// Validate max length
if (newText.length() > 4) {
String head = change.getControlNewText().substring(0, 4);
change.setText(head);
int oldLength = change.getControlText().length();
change.setRange(0, oldLength);
}
}

return change;
};
}

}

}
41 changes: 41 additions & 0 deletions gemsfx/src/main/java/com/dlsc/gemsfx/YearView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.dlsc.gemsfx;

import com.dlsc.gemsfx.skins.YearViewSkin;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;

import java.time.Year;

public class YearView extends Control {

public YearView() {
getStyleClass().add("year-view");
}

@Override
protected Skin<?> createDefaultSkin() {
return new YearViewSkin(this);
}

@Override
public String getUserAgentStylesheet() {
return YearView.class.getResource("year-view.css").toExternalForm();
}

private final ObjectProperty<Year> value = new SimpleObjectProperty<>(this, "value");

public Year getValue() {
return value.get();
}

public ObjectProperty<Year> valueProperty() {
return value;
}

public void setValue(Year value) {
this.value.set(value);
}

}
54 changes: 54 additions & 0 deletions gemsfx/src/main/java/com/dlsc/gemsfx/skins/YearPickerSkin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.dlsc.gemsfx.skins;

import com.dlsc.gemsfx.YearPicker;
import com.dlsc.gemsfx.YearView;
import javafx.scene.Node;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.kordamp.ikonli.javafx.FontIcon;

import java.util.Objects;

public class YearPickerSkin extends CustomComboBoxSkinBase<YearPicker> {

private YearView yearView;

public YearPickerSkin(YearPicker picker) {
super(picker);

picker.setOnMouseClicked(evt -> picker.show());

FontIcon calendarIcon = new FontIcon();
calendarIcon.getStyleClass().add("edit-icon"); // using styles similar to combobox, for consistency

StackPane editButton = new StackPane(calendarIcon);
editButton.setFocusTraversable(false);
editButton.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
editButton.getStyleClass().add("edit-button"); // using styles similar to combobox, for consistency
editButton.setOnMouseClicked(evt -> picker.show());

HBox.setHgrow(picker.getEditor(), Priority.ALWAYS);

HBox box = new HBox(picker.getEditor(), editButton);
box.getStyleClass().add("box");

getChildren().add(box);
}

@Override
protected Node getPopupContent() {
if (yearView == null) {
yearView = new YearView();
yearView.valueProperty().bindBidirectional(getSkinnable().valueProperty());
yearView.valueProperty().addListener((obs, oldValue, newValue) -> {
if (!Objects.equals(oldValue, newValue)) {
getSkinnable().hide();
}
});
}
return yearView;
}

}
Loading

0 comments on commit e037562

Please sign in to comment.