Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFX ListView edit TextFieldListCell

I am working on a client-server solution and I have a ListView with my connected clients. The user should be able to remotely rename the clients just by editing the name in the ListView. I have read a lot about editing ListView cells, but I haven't found any good example where I can just change a member attribute of my class. Most of the examples are with a list of strings, and in my opinion this is not modern software development, if the items inside of the ListView are more than just strings.

What I want to do is to change the attribute name inside of my client.

class Client {
    private String name;
    
    public String getName(){
        return name;
    }
    
    public String setName(String val){
        name = val;
    }
}

I don't care, if I have to implement the member name as a JavaFX property (in fact I already have done that, but left it out for readability and simplicity).

Failed attempt to use the TextFieldListCell Factory

In case you are interested, I have tried to use the TextFieldListCell cell factory:

this.listViewClients.setCellFactory(TextFieldListCell
 .forListView(new NetworkClientStringConverter(this.clientController)));

But there are some tricky things i found out:

  • Every time the name changes, I can't just access the object. Apparently what I am supposed to do is to create a new client and return it (fromString).
    • I solved this, by passing my client controller to the StringConverter. (This is not nice, I would prefer to just have access to the client I am changing.)
  • Still, the name does not change.
like image 514
El Mac Avatar asked Apr 05 '16 20:04

El Mac


1 Answers

This is a bit tricky using the standard TextFieldListCell, but the following seems to work.

Assume throughout this question there's a Client model class:

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Client {

    private final StringProperty name = new SimpleStringProperty();

    public final StringProperty nameProperty() {
        return this.name;
    }


    public final String getName() {
        return this.nameProperty().get();
    }


    public final void setName(final String name) {
        this.nameProperty().set(name);
    }

}

(fwiw I think everything still applies here if you use standard Java Bean properties instead of JavaFX Properties).

Create a converter class that references the cell:

public class ClientConverter extends StringConverter<Client> {
    private final ListCell<Client> cell;
    public ClientConverter(ListCell<Client> cell) {
        this.cell = cell ;
    }
    @Override
    public String toString(Client client) {
        return client.getName();
    }

    @Override
    public Client fromString(String string) {
        Client client = cell.getItem();
        client.setName(string);
        return client ;
    }

}

and then you can do

listViewClients.setCellFactory(lv -> {
    TextFieldListCell<Client> cell = new TextFieldListCell<>();
    cell.setConverter(new ClientConverter(cell));
    return cell ;
});

SSCCE:

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.TextFieldListCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.StringConverter;

public class EditableListView extends Application {

    @Override
    public void start(Stage primaryStage) {
        ListView<Client> listViewClients = new ListView<>();
        for (int i= 1 ; i <= 20; i++) {
            Client c = new Client();
            c.setName("Client "+i);
            listViewClients.getItems().add(c);
        }

        listViewClients.setEditable(true);

        listViewClients.setCellFactory(lv -> {
            TextFieldListCell<Client> cell = new TextFieldListCell<>();
            cell.setConverter(new ClientConverter(cell));
            return cell ;
        });

        // debug:

        Button debug = new Button("Show clients");
        debug.setOnAction(e -> listViewClients.getItems().stream().map(Client::getName).forEach(System.out::println));

        BorderPane root = new BorderPane(listViewClients, null, null, debug, null);
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }



    public static class ClientConverter extends StringConverter<Client> {
        private final ListCell<Client> cell;
        public ClientConverter(ListCell<Client> cell) {
            this.cell = cell ;
        }
        @Override
        public String toString(Client client) {
            return client.getName();
        }

        @Override
        public Client fromString(String string) {
            Client client = cell.getItem();
            client.setName(string);
            return client ;
        }

    }

    public static void main(String[] args) {
        launch(args);
    }
}

This feels like a bit of a hack though, because it's not really the job of the StringConverter to update the data in the model (Client) class. I would probably favor just creating the cell implementation from scratch here.

It's a little more code, but this feels safer:

public class ClientListCell extends ListCell<Client> {
    private final TextField textField = new TextField();

    public ClientListCell() {
        textField.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
            if (e.getCode() == KeyCode.ESCAPE) {
                cancelEdit();
            }
        });
        textField.setOnAction(e -> {
            getItem().setName(textField.getText());
            setText(textField.getText());
            setContentDisplay(ContentDisplay.TEXT_ONLY);
        });
        setGraphic(textField);
    }

    @Override
    protected void updateItem(Client client, boolean empty) {
        super.updateItem(client, empty);
        if (isEditing()) {
            textField.setText(client.getName());
            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
        } else {
            setContentDisplay(ContentDisplay.TEXT_ONLY);
            if (empty) {
                setText(null);
            } else {
                setText(client.getName());
            }
        }
    }

    @Override
    public void startEdit() {
        super.startEdit();
        textField.setText(getItem().getName());
        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
        textField.requestFocus();
        textField.selectAll();
    }

    @Override
    public void cancelEdit() {
        super.cancelEdit();
        setText(getItem().getName());
        setContentDisplay(ContentDisplay.TEXT_ONLY);
    }
}

and the SSCCE using this cell implementation is

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
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.scene.layout.BorderPane;
import javafx.stage.Stage;

public class EditableListView extends Application {

    @Override
    public void start(Stage primaryStage) {
        ListView<Client> listViewClients = new ListView<>();
        for (int i= 1 ; i <= 20; i++) {
            Client c = new Client();
            c.setName("Client "+i);
            listViewClients.getItems().add(c);
        }

        listViewClients.setEditable(true);

        listViewClients.setCellFactory(lv -> new ClientListCell());

        // debug:

        Button debug = new Button("Show clients");
        debug.setOnAction(e -> listViewClients.getItems().stream().map(Client::getName).forEach(System.out::println));

        BorderPane root = new BorderPane(listViewClients, null, null, debug, null);
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
like image 146
James_D Avatar answered Oct 31 '22 11:10

James_D