0

I have a table, and I create the columns in the following way:

@FXML
TableView<Row> tableView = new TableView<>();

private void createColumns(int numberOfColumns, Row firstRow, boolean errorsDisplayed) {
        for (int i = 0; i < numberOfColumns; i++) {
            int colNum = i;
            TableColumn<Row, String> column = new TableColumn<>(firstRow.getCell(i).toString());
            column.setCellValueFactory(param -> {
                int index = param.getTableView().getColumns().indexOf(param.getTableColumn());
                return new SimpleStringProperty(param.getValue().getLastCellNum() > index
                        ? param.getValue().getCell(index).toString() : null);
            });
            if (!errorsDisplayed || (colNum != numberOfColumns - 1 && colNum != numberOfColumns - 2)) {
                column.setCellFactory(TextFieldTableCell.forTableColumn());
                column.setOnEditCommit(param -> param.getTableView().getItems().get(param.getTablePosition()
                        .getRow()).getCell(colNum).setCellValue(param.getNewValue()));
            }
            tableView.getColumns().add(column);
        }
    }

Is there a way I could store different states of the table in order to restore them when the undo button being pressed?

JRobi17X
  • 67
  • 8
  • 1
    storing state should be part of your model (data), not the view - you'll need some logic to keep track of that state related to the item, make it observable and let a custom cell be aware of it. For concrete help, please provide a [mcve] – kleopatra Jan 11 '23 at 13:33
  • 1
    Don't conflate the View (in this case the TableView) with the data. Set up the TableView to display the data the way that you want, and to allow the user to interact with it the way that you want, and then handle the data as it's own thing. If you want to track changes to the data against a baseline version, then consider using the DirtyFX library. – DaveB Jan 11 '23 at 16:54
  • 1
    See if the ideas from [here](https://stackoverflow.com/questions/74319355/implement-undo-redo-button-in-javafx/74322170#74322170) can help. – SedJ601 Jan 11 '23 at 19:47

1 Answers1

1

I create an app that can hopefully help. It is probably full of traps and pitfalls so don't attempt to use the code as it. I got the Undo/Redo ideas from here. Two Stack are used to implement the ideas. I got the code for the TableView from here. Changes are committed on focus lost.

Main

/*
    Altered code from the following!
    1. https://docs.oracle.com/javafx/2/ui_controls/table-view.htm
    2. https://www.geeksforgeeks.org/implement-undo-and-redo-features-of-a-text-editor/
*/

import com.mycompany.javafxsimpletest.MyAction.MyActionType;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.util.Callback;
 
public class App extends Application { 
    final private UndoRedo undoRedo = new UndoRedo();   
    final private TableView<Person> table = new TableView();
    final private HBox hb = new HBox();
    
    private ObservableList<Person> data;    
 
    public static void main(String[] args) {
        launch(args);
    }
 
    @Override
    public void start(Stage stage) {
        data = FXCollections.observableArrayList(
                new Person(GenerateUniqueId.getUniqueId(), "Jacob", "Smith", "jacob.smith@example.com"),
                new Person(GenerateUniqueId.getUniqueId(), "Isabella", "Johnson", "isabella.johnson@example.com"),
                new Person(GenerateUniqueId.getUniqueId(), "Ethan", "Williams", "ethan.williams@example.com"),
                new Person(GenerateUniqueId.getUniqueId(), "Emma", "Jones", "emma.jones@example.com"),
                new Person(GenerateUniqueId.getUniqueId(), "Michael", "Brown", "michael.brown@example.com"));
        
        //Since I added these Persons via code and not manually, I added that action here!
        data.forEach((newPerson) -> {
            undoRedo.addAction(new MyAction(MyActionType.ADD, null, newPerson.copy()));
            System.out.println("Add Undo Action - \n\tActionType: " + MyActionType.ADD + "\n\tOld Person: " + null + "\n\tnew Person: " + newPerson);
        });
        
        Scene scene = new Scene(new Group());
        stage.setTitle("Table View Sample");
        stage.setWidth(450);
        stage.setHeight(550);
 
        final Label label = new Label("Address Book");
        label.setFont(new Font("Arial", 20));
 
        table.setEditable(true);
        Callback<TableColumn, TableCell> cellFactory = (TableColumn p) -> new EditingCell();
 
        TableColumn firstNameCol = new TableColumn("First Name");
        firstNameCol.setMinWidth(100);
        firstNameCol.setCellValueFactory(
            new PropertyValueFactory<Person, String>("firstName"));
        firstNameCol.setCellFactory(cellFactory);
        firstNameCol.setOnEditCommit(
            new EventHandler<CellEditEvent<Person, String>>() {
                @Override
                public void handle(CellEditEvent<Person, String> t) {
                    Person oldPerson = t.getTableView().getItems().get(t.getTablePosition().getRow());
                    Person newPerson = oldPerson.copy();
                    newPerson.setFirstName(t.getNewValue());
                                      
                    undoRedo.addAction(new MyAction(MyActionType.EDIT, oldPerson, newPerson));
                    System.out.println("Add Undo Action - \n\tActionType: " + MyActionType.EDIT + "\n\tOld Person: " + oldPerson + "\n\tnew Person: " + newPerson);
                }
             }
        );
 
 
        TableColumn lastNameCol = new TableColumn("Last Name");
        lastNameCol.setMinWidth(100);
        lastNameCol.setCellValueFactory(
            new PropertyValueFactory<Person, String>("lastName"));
        lastNameCol.setCellFactory(cellFactory);
        lastNameCol.setOnEditCommit(
            new EventHandler<CellEditEvent<Person, String>>() {
                @Override
                public void handle(CellEditEvent<Person, String> t) {
                    Person oldPerson = t.getTableView().getItems().get(t.getTablePosition().getRow());
                    Person newPerson = oldPerson.copy();
                    newPerson.setLastName(t.getNewValue());
                                      
                    undoRedo.addAction(new MyAction(MyActionType.EDIT, oldPerson, newPerson));
                    System.out.println("Add Undo Action - \n\tActionType: " + MyActionType.EDIT + "\n\tOld Person: " + oldPerson + "\n\tnew Person: " + newPerson);
                }
            }
        );
 
        TableColumn emailCol = new TableColumn("Email");
        emailCol.setMinWidth(200);
        emailCol.setCellValueFactory(
            new PropertyValueFactory<Person, String>("email"));
        emailCol.setCellFactory(cellFactory);
        emailCol.setOnEditCommit(
            new EventHandler<CellEditEvent<Person, String>>() {
                @Override
                public void handle(CellEditEvent<Person, String> t) {
                    Person oldPerson = t.getTableView().getItems().get(t.getTablePosition().getRow());
                    Person newPerson = oldPerson.copy();
                    newPerson.setEmail(t.getNewValue());
                                      
                    undoRedo.addAction(new MyAction(MyActionType.EDIT, oldPerson, newPerson));
                    System.out.println("Add Undo Action - \n\tActionType: " + MyActionType.EDIT + "\n\tOld Person: " + oldPerson + "\n\tnew Person: " + newPerson);
                }
            }
        );
 
        table.setItems(data);
        table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);
 
        final TextField addFirstName = new TextField();
        addFirstName.setPromptText("First Name");
        addFirstName.setMaxWidth(firstNameCol.getPrefWidth());
        final TextField addLastName = new TextField();
        addLastName.setMaxWidth(lastNameCol.getPrefWidth());
        addLastName.setPromptText("Last Name");
        final TextField addEmail = new TextField();
        addEmail.setMaxWidth(emailCol.getPrefWidth());
        addEmail.setPromptText("Email");
 
        final Button addButton = new Button("Add");
        addButton.setOnAction((ActionEvent e) -> {
            Person newPerson = new Person(GenerateUniqueId.getUniqueId(),addFirstName.getText(), addLastName.getText(), addEmail.getText());
            data.add(newPerson);
            addFirstName.clear();
            addLastName.clear();
            addEmail.clear();
            undoRedo.addAction(new MyAction(MyActionType.ADD, null, newPerson.copy()));
            System.out.println("Add Undo Action - ActionType: " + MyActionType.ADD + "\tPerson: " + newPerson);
        });
         
        Button btnUndo = new Button("<");
        btnUndo.setOnAction((t) -> {
            if(!undoRedo.isUndoEmpty())
            {
                MyAction myUndoAction = undoRedo.getUndo();
                Person oldPerson = (Person)myUndoAction.getOldAction();
                Person newPerson = (Person)myUndoAction.getNewAction();
                System.out.println("Add Undo Action - \n\tActionType: " + myUndoAction.getActionType() + "\n\tOld Person: " + oldPerson + "\n\tnew Person: " + newPerson);
                if(myUndoAction.getActionType() == MyActionType.ADD)
                {
                    data.remove(data.indexOf(data.stream().filter((z) -> z.getId() == newPerson.getId()).findFirst().get()));
                }
                else if(myUndoAction.getActionType() == MyActionType.EDIT)
                {
                    data.set(data.indexOf(data.stream().filter((z) -> z.getId() == oldPerson.getId()).findFirst().get()), oldPerson);
                }           
            }
        });
        
        Button btnRedo = new Button(">");
        btnRedo.setOnAction((t) -> {
            if(!undoRedo.isRedoEmpty())
            {
                MyAction myRedoAction = undoRedo.getRedo();
                Person oldPerson = (Person)myRedoAction.getOldAction();
                Person newPerson = (Person)myRedoAction.getNewAction();
                System.out.println("Add Undo Action - \n\tActionType: " +  myRedoAction.getActionType() + "\n\tOld Person: " + oldPerson + "\n\tnew Person: " + newPerson);
                
                if(myRedoAction.getActionType() == MyActionType.ADD)
                {
                    data.add(newPerson.getId(), newPerson);
                }
                else if(myRedoAction.getActionType() == MyActionType.EDIT)
                {
                    data.set(data.indexOf(data.stream().filter((z) -> z.getId() == newPerson.getId()).findFirst().get()), newPerson);
                }   
            }
        });
                
        hb.getChildren().addAll(addFirstName, addLastName, addEmail, addButton, btnUndo, btnRedo);
        hb.setSpacing(3);
 
        final VBox vbox = new VBox();
        vbox.setSpacing(5);
        vbox.setPadding(new Insets(10, 0, 0, 10));
        vbox.getChildren().addAll(label, table, hb);
 
        ((Group) scene.getRoot()).getChildren().addAll(vbox);
 
        stage.setScene(scene);
        stage.show();
    }
  
    class EditingCell extends TableCell<Person, String> {
 
        private TextField textField;
 
        public EditingCell() {
        }
 
        @Override
        public void startEdit() {
            if (!isEmpty()) {
                super.startEdit();
                createTextField();
                setText(null);
                setGraphic(textField);
                textField.selectAll();
            }
        }
 
        @Override
        public void cancelEdit() {
            super.cancelEdit();
 
            setText((String) getItem());
            setGraphic(null);
        }
 
        @Override
        public void updateItem(String item, boolean empty) {
            super.updateItem(item, empty);
 
            if (empty) {
                setText(null);
                setGraphic(null);
            } else {
                if (isEditing()) {
                    if (textField != null) {
                        textField.setText(getString());
                    }
                    setText(null);
                    setGraphic(textField);
                } else {
                    setText(getString());
                    setGraphic(null);
                }
            }
        }
 
        private void createTextField() {
            textField = new TextField(getString());
            textField.setMinWidth(this.getWidth() - this.getGraphicTextGap()* 2);
            textField.focusedProperty().addListener(new ChangeListener<Boolean>(){
                @Override
                public void changed(ObservableValue<? extends Boolean> arg0, 
                    Boolean arg1, Boolean arg2) {
                        if (!arg2) {
                            commitEdit(textField.getText());
                        }
                }
            });
        }
 
        private String getString() {
            return getItem() == null ? "" : getItem().toString();
        }
    }
}

Person

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

public class Person {        
    private final IntegerProperty id;
    private final StringProperty firstName;
    private final StringProperty lastName;
    private final StringProperty email;

    public Person(int id, String fName, String lName, String email) {
        this.id = new SimpleIntegerProperty(id);
        this.firstName = new SimpleStringProperty(fName);
        this.lastName = new SimpleStringProperty(lName);
        this.email = new SimpleStringProperty(email);
    }

    public String getFirstName() {
        return firstName.get();
    }

    public void setFirstName(String fName) {
        firstName.set(fName);
    }

    public String getLastName() {
        return lastName.get();
    }

    public void setLastName(String fName) {
        lastName.set(fName);
    }

    public String getEmail() {
        return email.get();
    }

    public void setEmail(String fName) {
        email.set(fName);
    }

    public int getId()
    {
        return id.get();
    }

    @Override public String toString()
    {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("id: ").append(this.id.get())
                .append("\tName:").append(this.firstName.get()).append(" ").append(this.lastName.get())
                .append("\temail: ").append(this.email.get());

        return stringBuilder.toString();
    }

    public Person copy()
    {
        Person copyPerson = new Person(this.id.get(), this.firstName.get(), this.lastName.get(), this.email.get());

        return copyPerson;
    }       
}

MyAction

/**
 *
 * @author blj0011(sedj601)
 * @param <T>
 */
public class MyAction<T>
{
    public enum MyActionType {
        ADD,
        DELETE,
        EDIT
    }
    private final T oldAction;
    private final T newAction;
    private final MyActionType myActionType;
    
    public MyAction(MyActionType actionType, T oldAction, T newAction) {   
        this.oldAction = oldAction;
        this.newAction = newAction;
        this.myActionType = actionType;
    }
    
    public T getOldAction()
    {
        return this.oldAction;
    }
    
    public T getNewAction()
    {
        return this.newAction;
    }
    
    public MyActionType getActionType()
    {
        return this.myActionType;
    }
}

UndoRedo

import java.util.Stack;

/**
 *
 * author 2. https://www.geeksforgeeks.org/implement-undo-and-redo-features-of-a-text-editor/
 * 
 */

public class UndoRedo {
    private final Stack<MyAction> undo = new Stack();
    private final Stack<MyAction> redo = new Stack();
       
    public void addAction(MyAction myAction)
    {
        undo.push(myAction);
    }
    
    // Function to perform
    // "UNDO" operation
    public MyAction getUndo()
    {
        MyAction myAction = undo.peek();
        undo.pop();        
        redo .push(myAction);
        
        return myAction;
    }
  
    // Function to perform
    // "REDO" operation
    public MyAction getRedo()
    {
        MyAction myAction = redo.peek();
        redo.pop();
        undo.push(myAction);
        
        return myAction;
    }
    
    //Check if stack is empty before attempting to do use getUndo()!
    public boolean isUndoEmpty()
    {
        return undo.empty();
    }
    
     //Check if stack is empty before attempting to do use getRedo()!
    public boolean isRedoEmpty()
    {
        return redo.empty();
    }
}

**GenerateUniqueId

/**
 *
 * @author blj0011(sedj601)
 */
public class GenerateUniqueId {
    static AtomicInteger uniqueIdGenerator = new AtomicInteger();
    
    public static int getUniqueId()
    {
        return uniqueIdGenerator.getAndIncrement();
    }
}

enter image description here

SedJ601
  • 12,173
  • 3
  • 41
  • 59