Search code examples
listviewjavafx

Selecting inner control of a custom listcell in a JavaFX ListView


I have:

public class TaskCell extends ListCell<Activity>...
..
    private TextField textField = new TextField();
    private HBox content = new HBox(..textField..);

    @Override
    public void updateItem(final Activity activity, boolean empty) {
        super.updateItem(activity, empty);
        ...
        textField.setText(activity.getTitle());
        setGraphic(content);
    }

Content is a HBox with the TextField and other controls.

Selecting a ListItem by clicking or by:

listView.getSelectionModel().select(1);

works and selects the the whole item. Now I want to use Key.Tab to select the inner TextField for editing. How is that possible? Question is not about KeyEvents but how to get requestFocus on the TextField (any control) on the selected ListItem.

Best and only partly working solution:

TaskCell selectedTaskCell = (TaskCell) listView.lookup(".cell:selected");
HBox hBox = (HBox) selectedTaskCell.getGraphic();
TextField textField = (TextField) hBox.getChildren().get(1);
textField.requestFocus();
textField.setVisible(false);

Which does get the right selected Cell proofed by debugging.. But requestFocus/setVisible always is done on the first List-Item. Somehow logically as ListCell is reused.. how to get focus on the selected item? Thx.


Solution

  • Firstly providing a full context of the problem will help others to get the exact issue what you are encountering.

    Few things I am not sure from your code. Where are you implementing the partly working solution? in KeyEvent handler/filter? on which KeyEvent type? Because if you don't use the correct one your results will change. And secondly, I am really not sure why you are turning off the visibility of the TextField after focus is requested.

    Anyway, from the code you provided, I believe you want to focus on selected cell TextField when a Tab key is pressed.

    Firstly, what I noticed is, the lookup call of ".cell:selected" is always returning the first cell. Infact if you lookup for ".cell:fake" will also return the first cell only irrespective of psuedo class ;). So to fix this I have an idea of setting an id to the cell that is selected and we can lookup it by the id.

    In the constructor of the cell:

    public TaskCell() {
        selectedProperty().addListener((obs, old, val) -> setId(val ? "mycell" : null));
    }
    

    Secondly, the default tabbing behaviour is implemented in bubbling stage (handler) of the KEY_PRESSED on ListView. So you need to ensure that you consume the event after you request focus on TextField. Otherwise the event will further propagate and will implement the default Tabbing behavior and will focus on first text field.

    listView.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
        if (e.getCode() == KeyCode.TAB) {
            Node tf = listView.lookup("#mycell .text-field");
            if (tf != null) {
                tf.requestFocus();
                e.consume(); // This is a required.
            }
        }
    });
    

    Note: If you don't want to consume the event, you can do it in the KEY_RELEASED handler. But you will see a quick jump of focus shift: first on the first cell text field and then on the selected cell text field. (which will look very odd)

    So combining the above two changes, below is complete working demo:

    enter image description here

    import javafx.application.Application;
    import javafx.beans.property.IntegerProperty;
    import javafx.beans.property.SimpleIntegerProperty;
    import javafx.beans.property.SimpleStringProperty;
    import javafx.beans.property.StringProperty;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.geometry.Insets;
    import javafx.scene.Node;
    import javafx.scene.Scene;
    import javafx.scene.control.*;
    import javafx.scene.input.KeyCode;
    import javafx.scene.input.KeyEvent;
    import javafx.scene.layout.HBox;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    
    import java.util.stream.IntStream;
    
    public class ListCellTextFieldDemo extends Application {
        @Override
        public void start(final Stage stage) throws Exception {
            ObservableList<Activity> activities = FXCollections.observableArrayList();
            IntStream.range(1, 30).forEach(i -> activities.add(new Activity(i, "Title " + i)));
    
            ListView<Activity> listView = new ListView<>();
            listView.setItems(activities);
            listView.setCellFactory(activityListView -> new TaskCell());
            listView.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
                if (e.getCode() == KeyCode.TAB) {
                    Node tf = listView.lookup("#mycell .text-field");
                    if (tf != null) {
                        tf.requestFocus();
                        e.consume(); // This is a required.
                    }
                }
            });
            VBox root = new VBox(listView);
            root.setPadding(new Insets(20));
            Scene scene = new Scene(root, 300, 450);
            stage.setScene(scene);
            stage.setTitle("ListCell Demo");
            stage.show();
        }
    
        class TaskCell extends ListCell<Activity> {
            private Label tokenLbl = new Label();
    
            private TextField field = new TextField();
    
            private HBox content = new HBox(15, tokenLbl, field);
    
            public TaskCell() {
                selectedProperty().addListener((obs, old, val) -> setId(val ? "mycell" : null));
            }
    
            @Override
            protected void updateItem(final Activity activity, final boolean b) {
                super.updateItem(activity, b);
                if (activity != null) {
                    tokenLbl.setText(activity.getToken() + "");
                    field.setText(activity.getTitle());
                    setGraphic(content);
                } else {
                    tokenLbl.setText("");
                    field.setText("");
                    setGraphic(null);
                }
            }
        }
    
        class Activity {
            private StringProperty title = new SimpleStringProperty();
            private IntegerProperty token = new SimpleIntegerProperty();
    
            public Activity(int t, String n) {
                title.set(n);
                token.set(t);
            }
    
            public String getTitle() {
                return title.get();
            }
    
            public int getToken() {
                return token.get();
            }
        }
    }