Search code examples
javajavafxtreeview

Manually commit the edit in JavaFX TreeView


I want following behaviours:

  1. When I press SHIFT+Enter in editing TreeItem, new TreeItem is attached to the next.
  2. Commit the current edit.
  3. Move the edit focus to newly created TreeItem. (Changing into an editing state would be nice, but just focusing that item is also okay.)

I partially implemented first and third processes, but how can I manually commit current edit? (second behaviour) Without explicitly commiting, changes get lost when the focus is shifted.

Below is my source.

    private KeyCombination shiftEnter = new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHIFT_DOWN);

    @FXML
    public void typeHandle(KeyEvent e) {
        if (shiftEnter.match(e)) {
            TreeItem<Content> newItem = new TreeItem<>(null);
            List<TreeItem<Content>> siblings = treeView.getSelectionModel().getSelectedItem().getParent().getChildren();
            siblings.add(siblings.indexOf(treeView.getSelectionModel().getSelectedItem()) + 1, newItem);
            treeView.getSelectionModel().select(newItem);
        }
    }

Full source code of the controller if you wonder:

package jsh.hiercards;

import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTreeView;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.control.cell.TextFieldTreeCell;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.util.StringConverter;

import java.net.URL;
import java.util.List;
import java.util.ResourceBundle;

public class MainController implements Initializable {
    @FXML
    public JFXButton save;
    @FXML
    public JFXButton load;
    @FXML
    public Label pending;
    @FXML
    public Label completed;
    @FXML
    public Label incompleted;
    @FXML
    public JFXButton start;
    @FXML
    public JFXButton viewerMode;
    @FXML
    public JFXTreeView<Content> treeView;

    private KeyCombination shiftEnter = new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHIFT_DOWN);

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        treeView.setCellFactory(tv -> new TextFieldTreeCell<>(new StringConverter<Content>() {

            @Override
            public Content fromString(String text) {
                System.out.println(tv.getTreeItem(tv.getSelectionModel().getSelectedIndex()).getValue());
                TreeItem<Content> parentItem = tv.getTreeItem(tv.getSelectionModel().getSelectedIndex()).getParent();
                Concept parent = parentItem == null ? null : (Concept) parentItem.getValue();
                String[] tokens = text.split(":", 2);
                if (tokens.length < 2) {
                    return new Concept(tokens[0].trim(), parent);
                } else return new Property(tokens[0].trim(), parent, tokens[1].trim());
            }

            @Override
            public String toString(Content content) {
                if (content instanceof Property) {
                    return ((Property) content).name + " : " + ((Property) content).description;
                }
                return content == null ? "" : content.name;
            }
        }));

        TreeItem<Content> root = new TreeItem<>(new Concept("TESTROOT", null));

        root.getChildren().add(new TreeItem<>(new Concept("TESTCONCEPT", (Concept) root.getValue())));
        root.getChildren().add(new TreeItem<>(new Property("TESTPROPERTY", (Concept) root.getValue(), "DESCRIPTION")));
        treeView.setRoot(root);
    }

    @FXML
    public void startEdit(TreeView.EditEvent e) {
        System.out.println("sTART");
    }

    @FXML
    public void commitEdit(TreeView.EditEvent e) {
        System.out.println("COLMMIT");
    }

    @FXML
    public void cancelEdit(TreeView.EditEvent e) {
        System.out.println("edit    cancel");
    }

    @FXML
    public void typeHandle(KeyEvent e) {
        if (shiftEnter.match(e)) {
            TreeItem<Content> newItem = new TreeItem<>(null);
            List<TreeItem<Content>> siblings = treeView.getSelectionModel().getSelectedItem().getParent().getChildren();
            siblings.add(siblings.indexOf(treeView.getSelectionModel().getSelectedItem()) + 1, newItem);
            treeView.getSelectionModel().select(newItem);
        }
    }

    @FXML
    public void save() {

    }

    @FXML
    public void load() {
    }

    @FXML
    public void viewerMode() {

    }

    @FXML
    public void start() {

    }
}

Solution

  • Unfortunately, the editing api on the TreeView (actually, on all virtualized controls) is incomplete in not providing any way to commit an edit - the single method to control editing state

    tree.edit(item);
    
    • cancels an edit if item is null
    • starts an edit if item is not null (and implicitely cancels any ongoing edit if called while editing)

    The only collaborator that can commit is the cell - but we must not talk to the cell in application code, catch-22 :) Well not quite, we just need to look at the requirement list a bit differently, changing the order a bit:

    1. shift-enter in cell commits the current edit, signalling a "special" commit
    2. a handler on the tree notices the "special" and triggers further action
      1. add a new item
      2. select the new item
      3. (edits the new item)

    For 1 we need a custom cell, 2 can be done in a custom editCommit handler. Something like:

    KeyCombination shiftEnter = new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHIFT_DOWN);
    
    // custom cell 
    tree.setCellFactory(t -> {
        TextFieldTreeCell<MenuItem> cell = new TextFieldTreeCell<>(conv) {
    
            // we don't have access to super's field, keep an alias
            private TextField fieldAlias;
            
            @Override
            public void startEdit() {
                super.startEdit();
                // install a custom key handler 
                if (isEditing() && fieldAlias == null) {
                    fieldAlias = (TextField) lookup(".text-field");
                    fieldAlias.setOnKeyReleased(e -> {
                        if (shiftEnter.match(e)) {
                            shiftCommit(fieldAlias.getText());
                        }
                    });
                }
            }
    
            // signal "special" commit before calling commitEdit
            private void shiftCommit(String text) {
                MenuItem item = getConverter().fromString(text);
                getTreeView().getProperties().put(SHIFT_COMMIT, item);
                commitEdit(item);
                getTreeView().getProperties().remove(SHIFT_COMMIT);
            }
            
        };
        return cell;
    });
    
    // custom editCommit handler
    tree.setOnEditCommit(e -> {
        // normal edit, nothing to do
        // note: this is a bug in tree editing - the cell changes the value of the treeItem
        // even if there's a custom handler installed!
        if (tree.getProperties().get(SHIFT_COMMIT) == null) return; 
        // find the location of the edited item
        TreeItem<MenuItem> edited = e.getTreeItem();
        TreeItem<MenuItem> parent = edited.getParent();
        int index = -1;
        if (parent != null) {
            index = parent.getChildren().indexOf(edited);
        }
        if (index > 0) {
            // if found, insert a new item as next and select it
            TreeItem<MenuItem> added = new TreeItem<>(new MenuItem("added"));
            parent.getChildren().add(index + 1, added);
            tree.getSelectionModel().select(added);
            // start editing the new item 
            // must be delayed until all internal state changes are processed
            Platform.runLater(() -> {
                tree.edit(added);
            });
        }
    });