I want to have a JavaFX ListView of Person objects. I want the list to display only the name and allow the name to be edited. It should also preserve the other fields in each object after committing an edit to the name. How would you do this idiomatically in JavaFX?
I have the following code, which works, but it's kind of wonky because it has a StringConverter that converts one way from Person to a String of the person's name then doesn't do the reverse conversion and instead relies on the list cell commitEdit method to take a string of the name and set it on the appropriate person.
Here's the code:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.TextFieldListCell;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.StringConverter;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
stage.setTitle("My Custom List View");
ObservableList<Person> people = FXCollections.observableArrayList(
new Person("John Doe", "123 New York"),
new Person("Jane Doe", "456 San Francisco")
);
ListView<Person> listView = new ListView();
listView.setCellFactory(new CustomCellFactory());
listView.setEditable(true);
listView.setItems(people);
Scene scene = new Scene(listView,400,300);
stage.setScene(scene);
stage.show();
}
public static class CustomCellFactory implements Callback<ListView<Person>,ListCell<Person>> {
@Override
public ListCell<Person> call(ListView param) {
TextFieldListCell<Person> cell = new TextFieldListCell() {
@Override
public void updateItem(Object item, boolean empty) {
super.updateItem(item, empty);
if (!empty && item != null) {
System.out.println("updating item: "+item.toString());
setText(((Person) item).getName());
} else {
setText(null);
}
}
@Override
public void commitEdit(Object newName) {
((Person)getItem()).setName((String)newName);
super.commitEdit(getItem());
}
};
cell.setConverter(new StringConverter() {
@Override
public String toString(Object person) {
return ((Person)person).getName();
}
@Override
public Object fromString(String string) {
return string;
}
});
return cell;
}
}
public static class Person {
private String name;
private String address;
public Person(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String toString() {
return name+" at "+address;
}
}
}
TextFieldListCell
is just a convenience implementation of ListCell
that provides the most common form of editing for list cells (i.e. if the items in the list are String
s, or objects that have an easy conversion to and from strings). You'll often find that you need more specific editing (e.g. you'll often want to filter the text allowed in the editing text field using a TextFormatter
), and in that case you just implement the ListCell
yourself. I think this is a case where, on balance, it makes more sense to implement ListCell
from scratch.
It seems you can force the TextFieldListCell
to work for this use case, using:
listView.setCellFactory(lv -> {
TextFieldListCell<Person> cell = new TextFieldListCell<Person>();
cell.setConverter(new StringConverter<Person>() {
@Override
public String toString(Person person) {
return person.getName();
}
@Override
public Person fromString(String string) {
Person person = cell.getItem();
person.setName(string);
return person ;
}
});
return cell;
});
(Note that in your code, your updateItem()
method is equivalent to the one already implemented in TextFieldListCell
, so it's redundant, and the extra functionality in commitEdit(...)
is now in the (typesafe) StringConverter
, so there's no longer any need for a subclass.)
This just feels a little fragile, as it relies on a particular implementation of committing the new value from the text field and its interaction with the string converter, but it seems to work fine in tests.
My preference for this, however, would be to implement the ListCell
directly yourself, as it gives you full control over the interaction between the text field and the editing process. This is pretty straightforward:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage primaryStage) {
ListView<Person> listView = new ListView<>();
ObservableList<Person> people = FXCollections.observableArrayList(
new Person("John Doe", "123 New York"),
new Person("Jane Doe", "456 San Francisco")
);
listView.setEditable(true);
listView.setItems(people);
listView.setCellFactory(lv -> new ListCell<Person>() {
private TextField textField = new TextField() ;
{
textField.setOnAction(e -> {
commitEdit(getItem());
});
textField.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
if (e.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
});
}
@Override
protected void updateItem(Person person, boolean empty) {
super.updateItem(person, empty);
if (empty) {
setText(null);
setGraphic(null);
} else if (isEditing()) {
textField.setText(person.getName());
setText(null);
setGraphic(textField);
} else {
setText(person.getName());
setGraphic(null);
}
}
@Override
public void startEdit() {
super.startEdit();
textField.setText(getItem().getName());
setText(null);
setGraphic(textField);
textField.selectAll();
textField.requestFocus();
}
@Override
public void cancelEdit() {
super.cancelEdit();
setText(getItem().getName());
setGraphic(null);
}
@Override
public void commitEdit(Person person) {
super.commitEdit(person);
person.setName(textField.getText());
setText(textField.getText());
setGraphic(null);
}
});
// for debugging:
listView.setOnMouseClicked(e -> {
if (e.getClickCount() == 2) {
listView.getItems().forEach(p -> System.out.println(p.getName()));
}
});
Scene scene = new Scene(listView,400,300);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class Person {
private String name;
private String address;
public Person(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String toString() {
return name+" at "+address;
}
}
public static void main(String[] args) {
launch(args);
}
}
If you might need this kind of functionality frequently, you could easily create a reusable class:
import java.util.function.BiFunction;
import java.util.function.Function;
import javafx.scene.control.ListCell;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
public class EditingListCell<T> extends ListCell<T> {
private final TextField textField ;
private final Function<T, String> propertyAccessor ;
public EditingListCell(Function<T, String> propertyAccessor, BiFunction<String, T, T> updater) {
this.propertyAccessor = propertyAccessor ;
this.textField = new TextField();
textField.setOnAction(e -> {
T newItem = updater.apply(textField.getText(), getItem());
commitEdit(newItem);
});
textField.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
if (e.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
});
}
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else if (isEditing()) {
textField.setText(propertyAccessor.apply(item));
setText(null);
setGraphic(textField);
} else {
setText(propertyAccessor.apply(item));
setGraphic(null);
}
}
@Override
public void startEdit() {
super.startEdit();
textField.setText(propertyAccessor.apply(getItem()));
setText(null);
setGraphic(textField);
textField.selectAll();
textField.requestFocus();
}
@Override
public void cancelEdit() {
super.cancelEdit();
setText(propertyAccessor.apply(getItem()));
setGraphic(null);
}
@Override
public void commitEdit(T item) {
super.commitEdit(item);
getListView().getItems().set(getIndex(), item);
setText(propertyAccessor.apply(getItem()));
setGraphic(null);
}
}
and then you just need
listView.setCellFactory(lv -> new EditingListCell<>(
Person::getName,
(text, person) -> {
person.setName(text);
return person ;
})
);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With