Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a ListView of complex objects and allow editing a field on the object?

Tags:

javafx

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;
        }
    }
}
like image 598
Jason Avatar asked Mar 12 '16 22:03

Jason


1 Answers

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 Strings, 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 ;
        })
);
like image 129
James_D Avatar answered Nov 06 '22 09:11

James_D