Unable to use DatePicker choose box when using TextFormatter - javafx

I want to create three(3) sets of Combobox (Year, Month, Day).
The Combobox Day should only be enabled until the Combobox Month and Year were filed correctly, and values should be synchronized based on the given month and year. (This means that it should check for leap years).
Here is what I have so far, I have a hint that I should use bindings and/or listeners to do this but struggle to do so.
public class Testing extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
final JFXComboBox<Month> cbMonths = new JFXComboBox<>();
final JFXComboBox<Integer> cbYears = new JFXComboBox<>();
final JFXComboBox<Integer> cbDays = new JFXComboBox<>();
// Month Values
cbMonths.getItems().setAll(Month.values());
// Year Values
Calendar calendar = Calendar.getInstance();
for (int i = calendar.get(Calendar.YEAR) ;
i >= (calendar.get(Calendar.YEAR) -35) ; i--)
{
cbYears.getItems().add(i);
}
// NOTE: will cause NPE
// I want to insert this code only when cbMonth and cbYears has a value
YearMonth numberOfDays = YearMonth.of(cbYear.getValue(), cbMonth.getValue());
for (int i = 1 ; i >= numberOfDays.lengthOfMonth() ; i ++) {
cbDays.getItems().add(i);
}
final HBox root = new HBox(cbMonth, cbYear, cbDays);
root.setAlignment(Pos.CENTER);
root.setSpacing(10.0);
root.setPadding(new Insets(10, 10, 10, 10));
Scene scene = new Scene(root, 300, 200);
primaryStage.setScene(scene);
primaryStage.show();
}
EDIT
Having a lack of time, I tried other options.
OPTION 1:
As #Zephyr points out, I switch to a date picker and set it to editable. I tried to override some of its default settings to come up with my desire output. But I notice that whenever I use TextFormatter I was unable to pick dates on the DatePicker choice box. Here is the sample code
public class DatePickerFinal extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
final String DATE_REGEX = "(0[1-9]|1[012])\\s(0[1-9]|[12][0-9]|3[01])\\s((19|2[0-9])[0-9]{2})";
final DateTimeFormatter SHOW_DATE = DateTimeFormatter.ofPattern("MMMM dd, yyyy", Locale.getDefault());
final DateTimeFormatter ENTER_DATE = DateTimeFormatter.ofPattern("MM dd yyyy", Locale.getDefault());
final LocalDate TODAY = LocalDate.now();
final JFXDatePicker DATE_PICKER = new JFXDatePicker();
// Disable some dates
DATE_PICKER.setDayCellFactory(new Callback<DatePicker, DateCell>() {
#Override
public DateCell call(DatePicker datePicker) {
return new DateCell() {
#Override
public void updateItem(LocalDate localDate, boolean b) {
super.updateItem(localDate, b);
setDisable(b || localDate.compareTo(TODAY) > 0 || localDate.compareTo(TODAY.minusYears(45)) < 0);
}
};
}
});
// Add StringConverter to make it more readable,
// and also rejecting disable dates inputted by the user
DATE_PICKER.setConverter(new StringConverter<LocalDate>() {
#Override
public String toString(LocalDate localDate) {
if (localDate == null) {
return "";
} else if (localDate.isAfter(TODAY) || localDate.isBefore(TODAY.minusYears(45))) {
return "";
} else {
return SHOW_DATE.format(localDate);
}
}
#Override
public LocalDate fromString(String s) {
return (s == null || s.isEmpty()) ? null : LocalDate.parse(s, ENTER_DATE);
}
});
// Then I want to manage user input so that they can only enter digits to the date picker
// then format it accordingly.
DATE_PICKER.getEditor().setTextFormatter(new TextFormatter<Object>(change -> {
String enteredText = change.getText();
if((enteredText.matches("[\\d]+")) || change.isDeleted()) {
final int oldTextLength = change.getControlText().length();
int newTextLength = change.getControlNewText().length();
if (newTextLength < oldTextLength) return change;
switch (newTextLength) {
case 2 :
case 5 :
StringBuilder stringBuilder = new StringBuilder(enteredText);
stringBuilder.append(" ");
change.setText(stringBuilder.toString());
newTextLength++;
break;
case 11 :
return null;
}
change.setCaretPosition(newTextLength);
change.setAnchor(newTextLength);
return change;
}
return null;
}));
// Add some validators where if the user input was valid or not. The below code was still in progress though.
RequiredFieldValidator requiredFieldValidator = new RequiredFieldValidator();
requiredFieldValidator.setMessage("Field Should Not Be Empty");
RegexValidator regexValidator = new RegexValidator("MM DD YYYY");
regexValidator.setRegexPattern(DATE_REGEX);
DATE_PICKER.setValidators(regexValidator);
DATE_PICKER.focusedProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observableValue, Boolean aBoolean, Boolean t1) {
if (t1) {
DATE_PICKER.validate();
}
}
});
DATE_PICKER.getEditor().textProperty().addListener(new ChangeListener<String>() {
#Override
public void changed(ObservableValue<? extends String> observableValue, String s, String t1) {
if (!DATE_PICKER.getEditor().getText().matches(DATE_REGEX)) {
DATE_PICKER.validate();
}
}
});
VBox root = new VBox(20, DATE_PICKER, new JFXButton("Button"));
root.setAlignment(Pos.CENTER);
Scene scene = new Scene(root, 300, 120);
primaryStage.setScene(scene);
primaryStage.show();
}
}
Aside from being editable, I also want the user to be able to just click and/or pick dates from the choice box. I hope someone could point me in the right direction :)

With #kleopatra's help. My solution is to create a class responsible for parsing the date selected by the user on the DatePickers default choice box. Furthermore, the date picker is set to editable so that the user can also edit it manually. However, there is a restriction where a user can ONLY insert numerical value when editing manually, also I wanted to make sure that the user should input only valid dates.
MCVE
public class DatePickerFinal extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
final String DATE_OF_BIRTH_REGEX
= "(0[1-9]|1[012])\\s(0[1-9]|[12][0-9]|3[01])\\s((19|2[0-9])[0-9]{2})";
final DateTimeFormatter showingDateFormat = DateTimeFormatter.ofPattern("MMMM dd, yyyy", Locale.getDefault());
final DateTimeFormatter inputtedDateFormat = DateTimeFormatter.ofPattern("MM dd yyyy", Locale.getDefault());
final LocalDate dateToday = LocalDate.now();
final JFXDatePicker datePicker = new JFXDatePicker();
// Disable some dates
datePicker.setDayCellFactory(new Callback<DatePicker, DateCell>() {
#Override
public DateCell call(DatePicker datePicker) {
return new DateCell() {
#Override
public void updateItem(LocalDate localDate, boolean b) {
super.updateItem(localDate, b);
setDisable(b || localDate.compareTo(dateToday) > 0 || localDate.compareTo(dateToday.minusYears(45)) < 0);
}
};
}
});
// Add StringConverter to make it more readable,
// and also rejecting disable dates inputted by the user
datePicker.setConverter(new StringConverter<LocalDate>() {
#Override
public String toString(LocalDate localDate) {
if (localDate == null) {
return "";
} else if (localDate.isAfter(dateToday) || localDate.isBefore(dateToday.minusYears(45))) {
return "";
} else {
return showingDateFormat.format(localDate);
}
}
#Override
public LocalDate fromString(String s) {
return (s == null || s.isEmpty()) ? null : LocalDate.parse(s, inputtedDateFormat);
}
});
// Add a validator
RequiredFieldValidator requiredFieldValidator = new RequiredFieldValidator();
requiredFieldValidator.setMessage("Enter with the format\nMM DD YYYY");
datePicker.setValidators(requiredFieldValidator);
// Format the user's input field
datePicker.getEditor().setTextFormatter(new TextFormatter<>(change -> {
String textEntered = change.getText();
DateValidator validator;
if (change.isContentChange()) {
validator = new DateValidator(change.getControlNewText(), showingDateFormat);
if (!validator.isValid()) {
datePicker.validate();
} else {
datePicker.resetValidation();
return change;
}
if (textEntered.matches("\\D+")) {
return null;
} else {
final int oldLength = change.getControlText().length();
int newLength = change.getControlNewText().length();
if (newLength < oldLength) return change;
if (newLength == 2 || newLength == 5) {
change.setText(textEntered + " ");
newLength++;
} else if (newLength == 11) {
validator = new DateValidator(change.getControlNewText(), inputtedDateFormat);
if (!validator.isValid()) {
return null;
} else {
datePicker.resetValidation();
}
}
change.setCaretPosition(newLength);
change.setAnchor(newLength);
}
}
return change;
}));
datePicker.focusedProperty().addListener((observableValue, wasFocused, isFocused) -> {
if (isFocused) {
Platform.runLater(()-> {
datePicker.validate();
datePicker.getEditor().selectAll();
});
} else {
datePicker.resetValidation();
}
});
datePicker.getEditor().textProperty().addListener(new ChangeListener<String>() {
#Override
public void changed(ObservableValue<? extends String> observableValue, String s, String t1) {
if (t1.matches(DATE_OF_BIRTH_REGEX)) {
datePicker.resetValidation();
}
}
});
// Show picker choice box on MouseEvent
datePicker.addEventFilter(MouseEvent.MOUSE_CLICKED, mouseEvent -> {
datePicker.show();
});
VBox root = new VBox(50, datePicker, new JFXButton("Button"));
root.setAlignment(Pos.CENTER);
Scene scene = new Scene(root, 300, 120);
primaryStage.setScene(scene);
primaryStage.show();
}
private static class DateValidator {
DateTimeFormatter formatter;
String date;
DateValidator (String date, DateTimeFormatter formatter) {
this.date = date;
this.formatter = formatter;
}
public boolean isValid() {
try {
LocalDate.parse(this.date, this.formatter);
} catch (Exception e) {
return false;
}
return true;
}
}
}

Related

Cannot save selected checkbox to variable

I am a problem with javafx. I have GridPane with about 40 CheckBoxes. I need make user can select only one CheckBox and selected CheckBox is saved to a variable.
This is method for work with checkboxes:
public static class createBet {
public static CheckBox bet;
public static CheckBox isBet(CheckBox[] group, int finalI, AnchorPane resultBlock) {
for (CheckBox j : group) {
if (j.equals(bet)) {
j.setSelected(false);
}
}
ObservableList resultLabels = resultBlock.getChildren();
Label label_num = (Label)resultLabels.get(0);
Label label_win = (Label)resultLabels.get(1);
if (group[finalI].isSelected()) {
bet = group[finalI];
resultBlock.setStyle("-fx-border-color: black;");
label_num.setStyle("-fx-text-fill: black");
label_win.setStyle("-fx-text-fill: black");
}
else {
bet = null;
resultBlock.setStyle("-fx-border-color: gray");
label_num.setStyle("-fx-text-fill: gray");
label_win.setStyle("-fx-text-fill: gray");
}
return bet;
}
}
This is a class for final variables:
public class bets {
public CheckBox numberBet = null;
public CheckBox colorBet = null;
public CheckBox evenBet = null;
}
There I use this classes:
//(block with variables and links to FXML)
createBet bet = new createBet();
bets bets = new bets();
for (int i = 0; i < numbersGroup.length; i++) {
int finalI = i;
numbersGroup[i].selectedProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
bets.numberBet = createBet.isBet(numbersGroup, finalI, resultNum);
}
});
}
}
}
Checkboxes are selected correctly, but variables for selected CheckBox (class bets) always equal null.
UPD. I comment class createBet and add code from it to the public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue). It not change the situation - variable bets.numberBet is changing in the listener only, outside it this variable allways equals null.
I want use numberBet in the whole code, it's main problem.
My solution for this problem. I DON'T set varbable numberBet with method isBet. isBetwork for change CheckBoxes and activation for block resultNum only.
Variables i set in inner action, where thei are used.
new version of class:
public class Bet {
private CheckBox bet;
public void forBet(CheckBox[] group, int finalI, AnchorPane resultBlock) {
for (CheckBox j : group) {
if (j.equals(bet)) {
j.setSelected(false);
}
}
ObservableList resultLabels = resultBlock.getChildren();
Label label_num = (Label)resultLabels.get(0);
Label label_win = (Label)resultLabels.get(1);
if (group[finalI].isSelected()) {
bet = group[finalI];
resultBlock.setStyle("-fx-border-color: black;");
label_num.setStyle("-fx-text-fill: black");
label_win.setStyle("-fx-text-fill: black");
}
else {
bet = null;
resultBlock.setStyle("-fx-border-color: gray");
label_num.setStyle("-fx-text-fill: gray");
label_win.setStyle("-fx-text-fill: gray");
}
}
public CheckBox startBet(CheckBox[] group) {
for (CheckBox i : group){
if (i.isSelected()) {
bet = i;
break;
}
}
return bet;
}
}
variables:
public CheckBox numberBet = null;
public CheckBox colorBet = null;
public CheckBox evenBet = null;
Using for methods:
Bet Bet = new Bet();
for (int i = 0; i < numbersGroup.length; i++) {
int finalI = i;
numbersGroup[i].selectedProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
Bet.forBet(numbersGroup, finalI,resultNum);
}
});
}
btn_start.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
numberBet = Bet.startBet(numbersGroup);
System.out.println(numberBet);
}
});
I think, it's resolve my issue.

Formatted TextField

I'm trying to create a TextField whose content is validated with a template. To do this, I create a TextFormatter to which I pass a StringConverter.
However, I do notice a weird thing about using StringConverter<String>. When I enter invalid data and the field loses focus, it does not clear its content (it only clears it after subsequent focusing). For comparison, when I use StringConverter<LocalTime> this problem is not noticed.
If I catch the change of focus and validate the data, the problem is solved, but I wonder why there is a discrepancy in the validation in both cases.
public class Sample extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
TextField fieldA = new TextField();
fieldA.setPromptText("00000");
fieldA.setTextFormatter(new TextFormatter<>(new StringConverter<String>() {
#Override
public String toString(String object) {
if(object == null) return "";
return object.matches("[0-9]{5}") ? object : "";
}
#Override
public String fromString(String string) {
if(string == null) return null;
return string.matches("[0-9]{5}") ? string : null;
}
}));
// fieldA.focusedProperty().addListener((observable, oldValue, newValue) -> {
// if(!fieldA.textProperty().getValueSafe().matches("[0-9]{5}")) {
// fieldA.setText(null);
// }
// });
TextField fieldB = new TextField();
fieldB.setPromptText("HH:MM:SS");
fieldB.setTextFormatter(new TextFormatter<>(new StringConverter<LocalTime>() {
#Override
public String toString(LocalTime object) {
if(object == null) return "";
return object.format(DateTimeFormatter.ofPattern("HH:mm:ss"));
}
#Override
public LocalTime fromString(String string) {
if(string == null) return null;
return LocalTime.parse(string, DateTimeFormatter.ofPattern("HH:mm:ss"));
}
}));
VBox vBox = new VBox(fieldA, fieldB);
vBox.setSpacing(5);
primaryStage.setScene(new Scene(vBox));
primaryStage.show();
}
}
ps: note that the purpose is not to create a TextField that can only accept 5 numbers. This is just an example.
I found the reason for the discrepancy in behavior. The main problem is that updating controls is done by binding valueProperty (in TextFormatter) with textProperty (in TextField). Because notifications of change to all Property objects are only saturated when the value of the wrapper is changed, sequential null submission causes a one-time notification.
The different behavior when using StringConverter<LocalTime> is because LocalTime::parse() throws a DateTimeParseException exception in invalid formatting. This in turn leads to a new valueProperty value being set, and to a previous valid control value.
This is the specific snippet of TextFormatter that is responsible for this behavior.
void updateValue(String text) {
if (!value.isBound()) {
try {
V v = valueConverter.fromString(text);
setValue(v);
} catch (Exception e) {
updateText(); // Set the text with the latest value
}
}
}
And the solution to the problem is that implementing StringConverter::fromString with an invalid value, instead of returning null, should throw unchecked exceptions.
public class Sample extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
TextField fieldA = new TextField();
fieldA.setPromptText("00000");
fieldA.setTextFormatter(new TextFormatter<>(new StringConverter<String>() {
#Override
public String toString(String object) {
if(object == null) return "";
return object.matches("[0-9]{5}") ? object : "";
}
#Override
public String fromString(String string) {
if(string == null)
throw new RuntimeException("Value is null");
if(string.matches("[0-9]{5}")) {
return string;
}
throw new RuntimeException("Value not match");
}
}));
TextField fieldB = new TextField();
fieldB.setPromptText("HH:MM:SS");
fieldB.setTextFormatter(new TextFormatter<>(new StringConverter<LocalTime>() {
#Override
public String toString(LocalTime object) {
if(object == null) return "";
return object.format(DateTimeFormatter.ofPattern("HH:mm:ss"));
}
#Override
public LocalTime fromString(String string) {
if(string == null) return null;
return LocalTime.parse(string, DateTimeFormatter.ofPattern("HH:mm:ss"));
}
}));
VBox vBox = new VBox(fieldA, fieldB);
vBox.setSpacing(5);
primaryStage.setScene(new Scene(vBox));
primaryStage.show();
}
}

JavaFX : TextField max and min value by listener

I need to limit interval of the text property of a text field
int maxLength = 64;
int minLength = 0;
txtSeuil.textProperty().addListener((v, oldValue, newValue) -> {
if (!newValue.matches("\\d*")) {
txtSeuil.setText(newValue.replaceAll("[^\\d*{1,2}]", ""));
if (txtSeuil.getText().length() > maxLength || txtSeuil.getText().length() < minLength) {
String s = txtSeuil.getText().substring(0, maxLength);
txtSeuil.setText(s);
}
}
});
the field does accept only numbers but any number, not just the interval values
If I have understood correctly, you're trying to implement a number field that only allows values within the interval [0, 64]. According to this answer, TextFormatter is the recommended way to accomplish such a functionality. Have a look at this MWE which should solve your problem:
public class RestrictedNumberFieldDemo extends Application {
public static void main(String[] args) {
launch();
}
#Override
public void start(Stage primaryStage) {
TextField numField = new TextField();
numField.setTextFormatter(new TextFormatter<Integer>(change -> {
// Deletion should always be possible.
if (change.isDeleted()) {
return change;
}
// How would the text look like after the change?
String txt = change.getControlNewText();
// There shouldn't be leading zeros.
if (txt.matches("0\\d+")) {
return null;
}
// Try parsing and check if the result is in [0, 64].
try {
int n = Integer.parseInt(txt);
return 0 <= n && n <= 64 ? change : null;
} catch (NumberFormatException e) {
return null;
}
}));
primaryStage.setScene(new Scene(numField));
primaryStage.show();
}
}
To solve your problem you can implement custom Filter for TextFormatter. It will allow to enter numbers only and restrict length of string. Here is snippet which show how it can works:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.stage.Stage;
public class Main5 extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
TextField textField = new TextField();
textField.setTextFormatter(new TextFormatter<Integer>(change -> {
if (!change.getText().isEmpty()) {
return change.getText().matches("\\d+") && change.getControlNewText().length() <= 5 ? change : null;
}
return change;
}));
primaryStage.setScene(new Scene(textField));
primaryStage.show();
}
}
Your problem is:
The length check is not done, if the regex matches. But the text can be a sequence of digits and be to long.
You need to do those checks independent of each other.
Also you're setting a new value in some cases which triggers the same checks again. Of course they will result in the String being evaluates as a valid input, but you could prevent checking again by introducing a field in the ChangeListener that stores, whether the listener is currently being executed:
txtSeuil.textProperty().addListener(new ChangeListener<String>() {
private boolean validating = false;
#Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
if (!validating) {
validating = true;
String newText = newValue;
boolean modify = false;
if (!newValue.matches("\\d*")) {
newText = newValue.replaceAll("[^\\d*{1,2}]", "");
modify = true;
}
if (newText.length() > maxLength || newText.length() < minLength) {
newText = newText.substring(0, maxLength);
modify = true;
}
if (modify) {
txtSeuil.setText(newText);
}
validating = false;
}
}
});

TreeView - Certain TreeItems are not allowed to be selected

I have created a Treeview (javafx), it looks like:
I want now, that only the "Tour"-TreeItems be selectable.
But I don't know how.
I have tried it with a ChangeListener, but I can only with it refresh the content of a Tab (TabPane)...the refresh works fine...but the "Delivery"-TreeItems can be selected :(
code:
public void showTours(List<Tour> pTours) {
treeViewPane.getSelectionModel().selectedItemProperty().addListener(treeItemChangeListener);
TreeItem tTreeRoot = new TreeItem<>("Root", new ImageView(Icons.getIcon24("truck_blue.png")));
tTreeRoot.setExpanded(true);
treeViewPane.setRoot(tTreeRoot);
for (Tour tTour : pTours) {
TreeItem<Object> tTourItem = new TreeItem<>(tTour);
tTreeRoot.getChildren().add(tTourItem);
if (tTour.getDeliveries() != null) {
for (Delivery tDelivery : tTour.getDeliveries()) {
TreeItem<Object> tDeliveryItem = new TreeItem<>(tDelivery);
tTourItem.getChildren().add(tDeliveryItem);
}
}
}
}
private final ChangeListener<TreeItem> treeItemChangeListener = (observable, oldValue, newValue) -> {
if (newValue != null && newValue.getValue() instanceof Tour){
Tour selectedTour = (Tour) newValue.getValue();
reloadTabContent(selectedTour);
}
};
I hope you can help me.
If you can show me example code, I will be really happy :)
Thank you
Modifying the selection behavior in any controls in JavaFX seems to be a bit of a pain; but the "proper" way to do this is to define a custom selection model for the tree. The easiest way to do this is to wrap the default selection model, and delegate the method calls to it, vetoing selection if the selection index is for an item which shouldn't be selected.
It's a good idea to select something whenever possible when a select method is called, as otherwise keyboard navigation will break.
Here is an implementation:
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.MultipleSelectionModel;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class CustomTreeSelectionModelExample extends Application {
#Override
public void start(Stage primaryStage) {
TreeItem<Object> root = new TreeItem<>("Root");
for (int i = 1 ; i <= 5 ; i++) {
TreeItem<Object> item = new TreeItem<>(new Tour("Tour "+i));
for (int j = 1 ; j <= 5; j++) {
Delivery delivery = new Delivery("Delivery "+j);
item.getChildren().add(new TreeItem<>(delivery));
}
root.getChildren().add(item);
}
TreeView<Object> tree = new TreeView<>();
tree.setSelectionModel(new TourSelectionModel(tree.getSelectionModel(), tree));
tree.setRoot(root);
primaryStage.setScene(new Scene(new BorderPane(tree), 400, 400));
primaryStage.show();
}
public static class TourSelectionModel extends MultipleSelectionModel<TreeItem<Object>> {
private final MultipleSelectionModel<TreeItem<Object>> selectionModel ;
private final TreeView<Object> tree ;
public TourSelectionModel(MultipleSelectionModel<TreeItem<Object>> selectionModel, TreeView<Object> tree) {
this.selectionModel = selectionModel ;
this.tree = tree ;
selectionModeProperty().bindBidirectional(selectionModel.selectionModeProperty());
}
#Override
public ObservableList<Integer> getSelectedIndices() {
return selectionModel.getSelectedIndices() ;
}
#Override
public ObservableList<TreeItem<Object>> getSelectedItems() {
return selectionModel.getSelectedItems() ;
}
#Override
public void selectIndices(int index, int... indices) {
List<Integer> indicesToSelect = Stream.concat(Stream.of(index), IntStream.of(indices).boxed())
.filter(i -> tree.getTreeItem(i).getValue() instanceof Tour)
.collect(Collectors.toList());
if (indicesToSelect.isEmpty()) {
return ;
}
selectionModel.selectIndices(indicesToSelect.get(0),
indicesToSelect.stream().skip(1).mapToInt(Integer::intValue).toArray());
}
#Override
public void selectAll() {
List<Integer> indicesToSelect = IntStream.range(0, tree.getExpandedItemCount())
.filter(i -> tree.getTreeItem(i).getValue() instanceof Tour)
.boxed()
.collect(Collectors.toList());
if (indicesToSelect.isEmpty()) {
return ;
}
selectionModel.selectIndices(0,
indicesToSelect.stream().skip(1).mapToInt(Integer::intValue).toArray());
}
#Override
public void selectFirst() {
IntStream.range(0, tree.getExpandedItemCount())
.filter(i -> tree.getTreeItem(i).getValue() instanceof Tour)
.findFirst()
.ifPresent(selectionModel::select);
}
#Override
public void selectLast() {
IntStream.iterate(tree.getExpandedItemCount() - 1, i -> i - 1)
.limit(tree.getExpandedItemCount())
.filter(i -> tree.getTreeItem(i).getValue() instanceof Tour)
.findFirst()
.ifPresent(selectionModel::select);
}
#Override
public void clearAndSelect(int index) {
int toSelect = index ;
int direction = selectionModel.getSelectedIndex() < index ? 1 : -1 ;
while (toSelect >= 0 && toSelect < tree.getExpandedItemCount() && ! (tree.getTreeItem(toSelect).getValue() instanceof Tour)) {
toSelect = toSelect + direction ;
}
if (toSelect >= 0 && toSelect < tree.getExpandedItemCount()) {
selectionModel.clearAndSelect(toSelect);
}
}
#Override
public void select(int index) {
int toSelect = index ;
int direction = selectionModel.getSelectedIndex() < index ? 1 : -1 ;
while (toSelect >= 0 && toSelect < tree.getExpandedItemCount() && ! (tree.getTreeItem(toSelect).getValue() instanceof Tour)) {
toSelect = toSelect + direction ;
}
if (toSelect >= 0 && toSelect < tree.getExpandedItemCount()) {
selectionModel.select(toSelect);
}
}
#Override
public void select(TreeItem<Object> obj) {
if (obj.getValue() instanceof Tour) {
selectionModel.select(obj);
}
}
#Override
public void clearSelection(int index) {
selectionModel.clearSelection(index);
}
#Override
public void clearSelection() {
selectionModel.clearSelection();
}
#Override
public boolean isSelected(int index) {
return selectionModel.isSelected(index);
}
#Override
public boolean isEmpty() {
return selectionModel.isEmpty();
}
#Override
public void selectPrevious() {
int current = selectionModel.getSelectedIndex() ;
if (current > 0) {
IntStream.iterate(current - 1, i -> i - 1).limit(current)
.filter(i -> tree.getTreeItem(i).getValue() instanceof Tour)
.findFirst()
.ifPresent(selectionModel::select);
}
}
#Override
public void selectNext() {
int current = selectionModel.getSelectedIndex() ;
if (current < tree.getExpandedItemCount() - 1) {
IntStream.range(current + 1, tree.getExpandedItemCount())
.filter(i -> tree.getTreeItem(i).getValue() instanceof Tour)
.findFirst()
.ifPresent(selectionModel::select);
}
}
}
public static class Tour {
private final String name ;
public Tour(String name) {
this.name = name ;
}
public String getName() {
return name ;
}
#Override
public String toString() {
return getName();
}
}
public static class Delivery {
private final String name;
public Delivery(String name) {
this.name = name;
}
public String getName() {
return name;
}
#Override
public String toString() {
return getName();
}
}
public static void main(String[] args) {
launch(args);
}
}
I modified the selection-model that James_D posted by making it a bit more generic so that you can specify a custom filter. The implementation is:
public class FilteredTreeViewSelectionModel<S> extends MultipleSelectionModel<TreeItem<S>> {
private final TreeView<S> treeView;
private final MultipleSelectionModel<TreeItem<S>> selectionModel;
private final TreeItemSelectionFilter<S> filter;
public FilteredTreeViewSelectionModel(
TreeView<S> treeView,
MultipleSelectionModel<TreeItem<S>> selectionModel,
TreeItemSelectionFilter<S> filter) {
this.treeView = treeView;
this.selectionModel = selectionModel;
this.filter = filter;
selectionModeProperty().bindBidirectional(selectionModel.selectionModeProperty());
}
#Override
public ObservableList<Integer> getSelectedIndices() {
return this.selectionModel.getSelectedIndices();
}
#Override
public ObservableList<TreeItem<S>> getSelectedItems() {
return this.selectionModel.getSelectedItems();
}
private int getRowCount() {
return this.treeView.getExpandedItemCount();
}
#Override
public boolean isSelected(int index) {
return this.selectionModel.isSelected(index);
}
#Override
public boolean isEmpty() {
return this.selectionModel.isEmpty();
}
#Override
public void select(int index) {
// If the row is -1, we need to clear the selection.
if (index == -1) {
this.selectionModel.clearSelection();
} else if (index >= 0 && index < getRowCount()) {
// If the tree-item at the specified row-index is selectable, we
// forward select call to the internal selection-model.
TreeItem<S> treeItem = this.treeView.getTreeItem(index);
if (this.filter.isSelectable(treeItem)) {
this.selectionModel.select(index);
}
}
}
#Override
public void select(TreeItem<S> treeItem) {
if (treeItem == null) {
// If the provided tree-item is null, and we are in single-selection
// mode we need to clear the selection.
if (getSelectionMode() == SelectionMode.SINGLE) {
this.selectionModel.clearSelection();
}
// Else, we just forward to the internal selection-model so that
// the selected-index can be set to -1, and the selected-item
// can be set to null.
else {
this.selectionModel.select(null);
}
} else if (this.filter.isSelectable(treeItem)) {
this.selectionModel.select(treeItem);
}
}
#Override
public void selectIndices(int index, int... indices) {
// If we have no trailing rows, we forward to normal row-selection.
if (indices == null || indices.length == 0) {
select(index);
return;
}
// Filter indices so that we only end up with those indices whose
// corresponding tree-items are selectable.
int[] filteredIndices = IntStream.concat(IntStream.of(index), Arrays.stream(indices)).filter(indexToCheck -> {
TreeItem<S> treeItem = treeView.getTreeItem(indexToCheck);
return (treeItem != null) && filter.isSelectable(treeItem);
}).toArray();
// If we have indices left, we proceed to forward to internal selection-model.
if (filteredIndices.length > 0) {
int newIndex = filteredIndices[0];
int[] newIndices = Arrays.copyOfRange(filteredIndices, 1, filteredIndices.length);
this.selectionModel.selectIndices(newIndex, newIndices);
}
}
#Override
public void clearAndSelect(int index) {
// If the index is out-of-bounds we just clear and return.
if (index < 0 || index >= getRowCount()) {
clearSelection();
return;
}
// Get tree-item at index.
TreeItem<S> treeItem = this.treeView.getTreeItem(index);
// If the tree-item at the specified row-index is selectable, we forward
// clear-and-select call to the internal selection-model.
if (this.filter.isSelectable(treeItem)) {
this.selectionModel.clearAndSelect(index);
}
// Else, we just do a normal clear-selection call.
else {
this.selectionModel.clearSelection();
}
}
#Override
public void selectAll() {
int rowCount = getRowCount();
// If we are in single-selection mode, we exit prematurely as
// we cannot select all rows.
if (getSelectionMode() == SelectionMode.SINGLE) {
return;
}
// If we only have a single index to select, we forward to the
// single-index select-method.
if (rowCount == 1) {
select(0);
}
// Else, if we have more than one index available, we construct an array
// of all the indices and forward to the selectIndices-method.
else if (rowCount > 1) {
int index = 0;
int[] indices = IntStream.range(1, rowCount).toArray();
selectIndices(index, indices);
}
}
#Override
public void clearSelection(int index) {
this.selectionModel.clearSelection(index);
}
#Override
public void clearSelection() {
this.selectionModel.clearSelection();
}
#Override
public void selectFirst() {
Optional<TreeItem<S>> firstItem = IntStream.range(0, getRowCount()).
mapToObj(this.treeView::getTreeItem).
filter(this.filter::isSelectable).
findFirst();
firstItem.ifPresent(this.selectionModel::select);
}
#Override
public void selectLast() {
int rowCount = getRowCount();
Optional<TreeItem<S>> lastItem = IntStream.iterate(rowCount - 1, i -> i - 1).
limit(rowCount).
mapToObj(this.treeView::getTreeItem).
filter(this.filter::isSelectable).
findFirst();
lastItem.ifPresent(this.selectionModel::select);
}
private int getFocusedIndex() {
FocusModel<TreeItem<S>> focusModel = this.treeView.getFocusModel();
return (focusModel == null) ? -1 : focusModel.getFocusedIndex();
}
#Override
public void selectPrevious() {
int focusIndex = getFocusedIndex();
// If we have nothing selected, wrap around to the last index.
int startIndex = (focusIndex == -1) ? getRowCount() : focusIndex;
if (startIndex > 0) {
Optional<TreeItem<S>> previousItem = IntStream.iterate(startIndex - 1, i -> i - 1).
limit(startIndex).
mapToObj(this.treeView::getTreeItem).
filter(this.filter::isSelectable).
findFirst();
previousItem.ifPresent(this.selectionModel::select);
}
}
#Override
public void selectNext() {
// If we have nothing selected, starting at -1 will work out correctly
// because we'll search from 0 onwards.
int startIndex = getFocusedIndex();
if (startIndex < getRowCount() - 1) {
Optional<TreeItem<S>> nextItem = IntStream.range(startIndex + 1, getRowCount()).
mapToObj(this.treeView::getTreeItem).
filter(this.filter::isSelectable).
findFirst();
nextItem.ifPresent(this.selectionModel::select);
}
}
}
I changed the selectIndex(int) method as this method should just forward the index-based selection to its internal selection-model if the filter permits. I disagree with the while loop logic as you explicitly pass the index to be selected to this method in the hopes that it can select it. The expected behaviour should be that it should ignore the select if the filter doesn't allow it. I also fleshed out the method by adding a catch for the index == -1 case as we need to clear selection when this happens.
The select(TreeItem) method was also changed quite a bit by checking for a null parameter and handling this separately so that if we are in single-selection mode we need to clear the selection, otherwise we call select(null) so that the internal selection-model handles it correctly. If we do have a tree-item we just check against filter and pass through to the internal selection-model.
The selectIndices(int, int[]) method is also different in that it should handle the case where the indices-array could be null or of length 0. If this is the case the select(index) method should be called.
I implemented the clearAndSelect(int) method a bit differently compared to the other approach. I do the boundary checks at the beginning to see if we need to call clearSelection() immediately. Else, I check if the TreeItem at the index is selectable via the filter. If it is we forward to the internal selection-model, else we just clear. I also disagree with the while-loop approach here that was done in the other implementation.
There is actually a bug with the selectPrevious() and selectNext() methods of James_D's implementation. If nothing is selected you need to snap to the last index when calling selectPrevious(). The opposite is true for selectFirst() where you need to snap to the first index if nothing is selected. You then work from these new indices to find the first item that is permitted by the filter. You also need to work with the focus-index and not the selected-index. You can see this behaviour if you look at the MultipleSelectionModelBase class for reference.
The TreeItemSelectionFilter is specified as:
public interface TreeItemSelectionFilter<S> {
public boolean isSelectable(TreeItem<S> treeItem);
}
For your particular case you can then wire it all together as:
....
MultipleSelectionModel<TreeItem<Object>> selectionModel = tree.getSelectionModel();
TreeItemSelectionFilter<Object> filter = treeItem -> treeItem.getValue() instanceof Tour;
FilteredTreeViewSelectionModel<Object> filteredSelectionModel = new FilteredTreeViewSelectionModel<>(tree, selectionModel, filter);
tree.setSelectionModel(filteredSelectionModel);
....
I've uploaded the source-code of an example application here so that you can easily test the behavior of the FilteredTreeViewSelectionModel for yourself. Compare it with the default selection-model and see if you are satisfied with the behavior.

JavaFx Create Table Cell Accepts numbers only?

I have TableView with column inside it that must only accept numbers.
and I added onMouseClickListener to enter edit mode on the mouse click instead of double click on the cell
I want a way to not allowing the user to enter any character except numbers. My code is:
Callback<TableColumn<DailyDetails, String>, TableCell<DailyDetails, String>> defaultCellFactory
= TextFieldTableCell.<DailyDetails>forTableColumn();
dailyCredit.setCellFactory(column -> {
TableCell<DailyDetails, String> cell = defaultCellFactory.call(column);
cell.setOnMouseClicked(e -> {
if (!cell.isEditing() && !cell.isEmpty()) {
cell.getTableView().edit(cell.getIndex(), column);
}
});
return cell;
});
I implemented Table cell from the scratch:
class NumberCell extends TableCell<DailyDetails, String> {
private TextField textField;
public NumberCell() {
}
#Override
public void startEdit() {
super.startEdit();
if (textField == null) {
createTextField();
}
setGraphic(textField);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
textField.selectAll();
}
#Override
public void cancelEdit() {
super.cancelEdit();
setText(String.valueOf(getItem()));
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setGraphic(textField);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
} else {
setText(getString());
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}
}
private void createTextField() {
textField = new TextField(getString());
//textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
textField.lengthProperty().addListener(new ChangeListener<Number>(){
#Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
if (newValue.intValue() > oldValue.intValue()) {
char ch = textField.getText().charAt(oldValue.intValue());
// Check if the new character is the number or other's
if (!(ch >= '0' && ch <= '9' )) {
// if it's not number then just setText to previous one
textField.setText(textField.getText().substring(0,textField.getText().length()-1));
}
}
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
}
Callback<TableColumn<DailyDetails, String>,
TableCell<DailyDetails, String>> cellFactory
= (TableColumn<DailyDetails, String> p) -> new NumberCell();
dailyDebit.setCellFactory(cellFactory);
the problem is i lost the on mouse listener cell.setOnMouseClicked!!!
how do i get the cell again to assign the listener ???
Just for driving the new api into everybody's brain: a full example with a slightly different TextFormatter (than in the other answer) that is Locale-aware and (dirtily!) hooked into core TextFieldTableCell, can be used in any custom editing TableCell as well:
/**
* Example of how-to use a TextFormatter in a editing TableCell.
*/
public class CellFormatting extends Application {
private Parent getContent() {
ObservableList<IntData> data = FXCollections.observableArrayList(
new IntData(1), new IntData(2), new IntData(3)
);
TableView<IntData> table = new TableView<>(data);
table.setEditable(true);
TableColumn<IntData, Integer> column = new TableColumn<>("Data");
column.setCellValueFactory(new PropertyValueFactory("data"));
// core default: will throw exception on illegal values
// column.setCellFactory(TextFieldTableCell.forTableColumn(new IntegerStringConverter()));
NumberFormat format = NumberFormat.getIntegerInstance();
UnaryOperator<TextFormatter.Change> filter = c -> {
if (c.isContentChange()) {
ParsePosition parsePosition = new ParsePosition(0);
// NumberFormat evaluates the beginning of the text
format.parse(c.getControlNewText(), parsePosition);
if (parsePosition.getIndex() == 0 ||
parsePosition.getIndex() < c.getControlNewText().length()) {
// reject parsing the complete text failed
return null;
}
}
return c;
};
column.setCellFactory(c -> new ValidatingTextFieldTableCell<>(
// note: each cell needs its own formatter
// see comment by #SurprisedCoconut
new TextFormatter<Integer>(
// note: should use local-aware converter instead of core!
new IntegerStringConverter(), 0,
filter)));
table.getColumns().add(column);
VBox box = new VBox(table);
return box;
}
/**
* TextFieldTableCell that validates input with a TextFormatter.
* <p>
* Extends TextFieldTableCell, accesses super's private field reflectively.
*
*/
public static class ValidatingTextFieldTableCell<S, T> extends TextFieldTableCell<S, T> {
private TextFormatter<T> formatter;
private TextField textAlias;
public ValidatingTextFieldTableCell() {
this((StringConverter<T>)null);
}
public ValidatingTextFieldTableCell(StringConverter<T> converter) {
super(converter);
}
public ValidatingTextFieldTableCell(TextFormatter<T> formatter) {
super(formatter.getValueConverter());
this.formatter = formatter;
}
/**
* Overridden to install the formatter. <p>
*
* Beware: implementation detail! super creates and configures
* the textField lazy on first access, so have to install after
* calling super.
*/
#Override
public void startEdit() {
super.startEdit();
installFormatter();
}
private void installFormatter() {
if (formatter != null && isEditing() && textAlias == null) {
textAlias = invokeTextField();
textAlias.setTextFormatter(formatter);
}
}
private TextField invokeTextField() {
Class<?> clazz = TextFieldTableCell.class;
try {
Field field = clazz.getDeclaredField("textField");
field.setAccessible(true);
return (TextField) field.get(this);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}
public static class IntData {
IntegerProperty data = new SimpleIntegerProperty(this, "data");
public IntData(int value) {
setData(value);
}
public void setData(int value) {
data.set(value);
}
public int getData() {
return data.get();
}
public IntegerProperty dataProperty() {
return data;
}
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene(new Scene(getContent()));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
BTW, the formatter is re-used from another question where the task at hand was to restrict input into a Spinner.
Use a TextFormatter on the TextField like this:
TextFormatter<String> formatter = new TextFormatter<String>( change -> {
change.setText(change.getText().replaceAll("[^0-9.,]", ""));
return change;
});
textField.setTextFormatter(formatter);
Works with Java8u40 upwards. Use e. g. the TableView example from the Oracle site as base.

Resources