0

In the following code, I create a jtable with a custom cell editor for the first column and then add undo capabilities to the table. When you run the program, the program allows you to change the values in the first column (test by appending a "d" and then an "e" to the "abc" already there). Now enter control-z (undo) and enter control-z again. It works as expected. But now enter control-z (undo) again. This time the "abc" is erased. It looks like the swing system is setting the initial value of the column and creating an undo event for that action which the user can then undo. My question - how do I write my code so that the user only can undo the actions the user makes?

import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.DefaultCellEditor;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JRootPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;
import javax.swing.undo.UndoableEdit;

public class UndoExample extends JFrame {

private static final long serialVersionUID = 1L;;   
static Boolean objEnableUndoRedoActions = true; 
UndoExample rootFrame;

public UndoExample() {
    // This procedure starts the whole thing off.

    //Create table
    final String[] tableColumns = {"Column 1", "Column 2"};
    JTable tabUndoExample = new JTable(
            new DefaultTableModel(null, tableColumns) {
                private static final long serialVersionUID = 1L;                
    });
    final DefaultTableModel tabUndoExampleModel = (DefaultTableModel) tabUndoExample
            .getModel();
    tabUndoExampleModel.addRow(new Object[]{"abc", true});
    tabUndoExampleModel.addRow(new Object[]{"zyw", false});

    // Create the undo/redo manager
    UndoManager objUndoManager = new UndoManager();

    // Create a cell editor
    JTextField tfTabField = new JTextField();
    TableCellEditor objEditor = new DefaultCellEditor(tfTabField);

    // Make the cell editor the default editor for this table's first column
    tabUndoExample.getColumnModel().getColumn(0)
        .setCellEditor(objEditor);

    // Create the undo action on the field's document for the column
    tfTabField.getDocument().addUndoableEditListener(
            new uelUndoRedoTableCellField(objUndoManager, tabUndoExample));

    // Allow undo and redo to be entered by the user
    UndoRedoSetKeys(this, "Example", objUndoManager);
    tabUndoExample.setInheritsPopupMenu(true);

     //Add the table to the frame and show the frame         
    this.add(tabUndoExample);
    this.pack();
    setLocationRelativeTo(null);
    }

public static void main(final String[] args) {
    // Launches the application. This is required syntax.

    SwingUtilities.invokeLater(new Runnable() {
        @Override
        public void run() {
            try {       
                final UndoExample rootFrame = new UndoExample();
                rootFrame.setVisible(true);             
            } catch (final Exception e) {

            }
        }
    });
}

@SuppressWarnings("serial")
static class aueUndoRedoTableCellField extends AbstractUndoableEdit {
    // Wrap the text edit action item as we need to add the table
    // row and column information.  This code is invoked when the
    // code sees an undo event created and then later when the 
    // user requests an undo/redo.

    JTable objTable = null;
    UndoableEdit objUndoableEdit;
    int objCol = -1;
    int objRow = -1;

    public aueUndoRedoTableCellField(UndoableEdit undoableEdit,
            JTable table, int row, int col) {
        super();
        objUndoableEdit = undoableEdit;
        objTable = table;
        objCol = col;
        objRow = row;
    }

    public void redo() throws CannotRedoException {
        // When the user enters redo (or undo), this code sets
        // that we are doing an redo (or undo), sets the cursor
        // to the right location, and then does the undo (or redo)
        // to the table cell.  
        UndoRedoManagerSetEnabled(false);
        super.redo();
        @SuppressWarnings("unused")
        boolean success = objTable.editCellAt(objRow, objCol);
        objTable.changeSelection(objRow, objCol, false, false);
        objUndoableEdit.redo();
        UndoRedoManagerSetEnabled(true);
    }

    public void undo() throws CannotUndoException {
        super.undo();
        UndoRedoManagerSetEnabled(false);
        @SuppressWarnings("unused")
        boolean success = objTable.editCellAt(objRow, objCol);
        objTable.changeSelection(objRow, objCol, false, false);
        objUndoableEdit.undo();
        UndoRedoManagerSetEnabled(true);
    }
}

static class aUndoRedo extends AbstractAction {
    // This code is bound to the undo/redo keystrokes and tells
    // Java what commands to run when the keys are later entered
    // by the user.

    private static final long serialVersionUID = 1L;
    Boolean objUndo = true;
    UndoManager objUndoManager = null;
    final String objLocation;

    public aUndoRedo(Boolean Undo, UndoManager undoManager, String location) {
        super();
        objUndo = Undo;
        objUndoManager = undoManager;
        objLocation = location;
    }

    @Override
    public void actionPerformed(ActionEvent ae) {
        try {
            // See if operation allowed
            if (!objUndoManager.canUndo() && objUndo
                    || !objUndoManager.canRedo() && !objUndo)
                return;
            UndoRedoManagerSetEnabled(false);
            if (objUndo) {
                objUndoManager.undo();
            } else {
                objUndoManager.redo();
            }
            UndoRedoManagerSetEnabled(true);
            // Catch errors and let user know
        } catch (Exception e) {

            UndoRedoManagerSetEnabled(true);
        }
    }
}

static class uelUndoRedoTableCellField implements UndoableEditListener {
    // This action is called when the user changes the table's
    // text cell.  It saves the change for later undo/redo.

    private UndoManager objUndoManager = null;
    private JTable objTable = null;

    public uelUndoRedoTableCellField(UndoManager undoManager,
            JTable table) {
        objUndoManager = undoManager;
        objTable = table;
    }

    @Override
    public void undoableEditHappened(UndoableEditEvent e) {
        // Remember the edit but only if the code isn't doing
        // an undo or redo currently.
        if (UndoRedoManagerIsEnabled()) {
            objUndoManager.addEdit(new aueUndoRedoTableCellField(e
                    .getEdit(), objTable, objTable.getSelectedRow(),
                    objTable.getSelectedColumn()));
        }
    }
}

static public Boolean UndoRedoManagerIsEnabled() {
    // See if we are currently doing an undo/redo.
    // Return true if so.
    return objEnableUndoRedoActions;
}

static public void UndoRedoManagerSetEnabled(Boolean state) {
    // Set the state of whether we are in undo/redo code.
    objEnableUndoRedoActions = state;
}


static void UndoRedoSetKeys(JFrame frame, final String location, UndoManager undoManager) {
    // Allow undo and redo to be called via these keystrokes for this dialog
    final String cntl_y = "CNTL_Y";
    final KeyStroke ksCntlY = KeyStroke.getKeyStroke("control Y");
    final String cntl_z = "CNTL_Z";
    final KeyStroke ksCntlZ = KeyStroke.getKeyStroke("control Z");

    JRootPane root = frame.getRootPane();
    root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
            .put(ksCntlZ, cntl_z);
    root.getActionMap().put(
            cntl_z,
            new aUndoRedo(true, undoManager, location));
    root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
            .put(ksCntlY, cntl_y);
    root.getActionMap().put(
            cntl_y,
            new aUndoRedo(false, undoManager, location));
}

}
mKorbel
  • 109,525
  • 20
  • 134
  • 319
tro
  • 1
  • 1
  • Hitting ctrl+z is sending the table into edit mode and sending it the ctrl+z key stroke – MadProgrammer Nov 16 '15 at 03:53
  • I don't believe this is the case. You'll see in the UndoRedoSetKeys procedure I map ctrl+z to implement an undo event. Also, if you run the program, the program does an undo. I also, using the debugger, proved that once the user enters ctrl+z, the code to do the undo is called. Are you saying that ctrl+z event is ALSO then being sent to the table? – tro Nov 16 '15 at 04:14
  • Yeah, it is, you can apply [this](http://stackoverflow.com/questions/23052777/can-jtable-cell-edit-consume-key-strokes) which will stop it, and stop the undo from work. The `JTextField` starts out with no text, it's then supplied the cell's value and put on the screen and the key stroke which started the editing mode is sent to the field (so if you typed `a`, it would be appended to the end of the text), but instead, it triggers the undo, sending the field back to it's initial, blank state :P – MadProgrammer Nov 16 '15 at 04:16
  • *"Are you saying that ctrl+z event is ALSO then being sent to the table?"* - not in so many words, yes, the `JTable` responds to the event, but it re-dispatches the key event to the editor AFTER the editor is established on the screen. Try typing any character on the keyboard, the character is appended to the text of the field ;) – MadProgrammer Nov 16 '15 at 04:18

1 Answers1

2

When you press a key, a series of things occur. The JTable, process the key stroke, it checks to see if the cell is editable (as the TableModel), it then asks the editor for the currently selected cell if the event should edit the cell (CellEditor#isCellEditable(EventObject)).

If this method returns true, the editor is prepared, the value from the TableModel is applied to the editor (ie setText is called), and the editor is added to the JTable, finally, the event which triggered the edit mode is re-dispatched to the editor, in your case the Ctrl+Z, which then triggers and undo event, returning the editor it's initial state (before setText was called).

You can try and use something like...

TableCellEditor objEditor = new DefaultCellEditor(tfTabField) {
    @Override
    public boolean isCellEditable(EventObject anEvent) {
        boolean isEditable = super.isCellEditable(anEvent); //To change body of generated methods, choose Tools | Templates.
        if (isEditable && anEvent instanceof KeyEvent) {
            KeyEvent ke = (KeyEvent) anEvent;
            if (ke.isControlDown() && ke.getKeyCode() == KeyEvent.VK_Z) {
                isEditable = false;
            }
        }
        return isEditable;
    }

};

to prevent the JTable from been placed into edit when a specific key stroke occurs

Updated

So based on Andrew's answer from JTextArea setText() & UndoManager, I devised a "configurable" UndoableEditListener which can be set to ignore undoable actions, for example...

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.Document;
import javax.swing.text.PlainDocument;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;

public class FixedField {

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

    public FixedField() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public static class UndoableEditHandler implements UndoableEditListener {

        private static final int MASK
                = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();

        private UndoManager undoManager = new UndoManager();

        private boolean canUndo = true;

        public UndoableEditHandler(JTextField field) {
            Document doc = field.getDocument();
            doc.addUndoableEditListener(this);
            field.getActionMap().put("Undo", new AbstractAction("Undo") {
                @Override
                public void actionPerformed(ActionEvent evt) {
                    try {
                        if (undoManager.canUndo()) {
                            undoManager.undo();
                        }
                    } catch (CannotUndoException e) {
                        System.out.println(e);
                    }
                }
            });
            field.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, MASK), "Undo");
            field.getActionMap().put("Redo", new AbstractAction("Redo") {
                @Override
                public void actionPerformed(ActionEvent evt) {
                    try {
                        if (undoManager.canRedo()) {
                            undoManager.redo();
                        }
                    } catch (CannotRedoException e) {
                        System.out.println(e);
                    }
                }
            });
            field.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Y, MASK), "Redo");
        }

        @Override
        public void undoableEditHappened(UndoableEditEvent e) {
            if (canUndo()) {
                undoManager.addEdit(e.getEdit());
            }
        }

        public void setCanUndo(boolean canUndo) {
            this.canUndo = canUndo;
        }

        public boolean canUndo() {
            return canUndo;
        }

    }

    public class TestPane extends JPanel {

        public TestPane() {
            JTextField field = new JTextField(10);
            UndoableEditHandler handler = new UndoableEditHandler(field);

            handler.setCanUndo(false);
            field.setText("Help");
            handler.setCanUndo(true);
            add(field);
        }

    }

}

Now, you're going to have to devices your own TableCellEditor to support this, for example...

public static class MyCellEditor extends AbstractCellEditor implements TableCellEditor {

    private JTextField editor;
    private UndoableEditHandler undoableEditHandler;

    public MyCellEditor(JTextField editor) {
        this.editor = editor;
        undoableEditHandler = new UndoableEditHandler(editor);
        editor.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                fireEditingStopped();
            }
        });
    }

    @Override
    public Object getCellEditorValue() {
        return editor.getText();
    }

    @Override
    public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
        undoableEditHandler.setCanUndo(false);
        editor.setText(value == null ? null : value.toString());
        undoableEditHandler.setCanUndo(true);
        return editor;
    }

}
Community
  • 1
  • 1
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • First - thanks for your time and help. I added the above code in the obvious place in the first code I provide (I can give you the whole code here if useful) and tested it. It still has the same behavior. The 3rd undo causes the value "abc" to be erased. Also, to make sure we are in synch. While I am editing the cell, the behavior I want is for the first undos to remove then "e" and then the "d" and the third undo to do nothing since there are no undo events to undo at that time. – tro Nov 16 '15 at 04:57
  • Yes, this is part of what I've been saying. When `DefaultCellEditor#getTableCellEditorComponent` is called, it calls `setText` of the `JTextField` passing the entire `String` value for the cell in ONE single, undoable, action. So, once the editor is edit mode, if you press ctrl+z, the WHOLE value will be undone. The best I can do is stop if from causing the editor coming up blank when you press ctrl+z before you're in edit mode – MadProgrammer Nov 16 '15 at 05:00
  • Consider what would happen if you pasted a block of text into a field. Should, when you press ctrl+z the whole block be removed or only a single character? – MadProgrammer Nov 16 '15 at 05:04
  • Again - thanks. But the solution isn't a good one for my users. Even in edit mode people should be able to undo - in fact - that's when undo is most useful! To me, it sounds like a Swing bug. The table should not be generating that first undo event. I don't get it when using standard cell editors. Unfortunately, I need the default cell editor in that I want cells to be JTextAreas so they can be multiple lines. The only other idea I have is to somehow eat that first undo programmatically. How to do that looks painful. – tro Nov 16 '15 at 05:09
  • It's not a bug, it's how it works. If you really, really, really want to screw with the expectations of your users, you could apply each character of the cell value to the editor individually – MadProgrammer Nov 16 '15 at 05:11
  • From the user's point of view, I think having the 3rd undo not happen is most intuitive. The setting of the table to blank is not a state the user ever saw or created and thus should not be "undoable" back to. Are you arguing that the user does want a way to remove the "abc" and it is most intuitive to them that these values do go away? Maybe I should mention that my program pre-fills the table with default values, that is, the user doesn't start with a blank table. – tro Nov 16 '15 at 05:32
  • It all depends on you context (sorry, I've been trying to make sense of you code but it's eluding me). The `JTextField` starts out blank and then is applied a value to it. Is it unreasonable to assume that the user might want to blank the field? If you don't want this behavior, you will need to "turn off" the undo support for the field before the primary value is set and back on after it. – MadProgrammer Nov 16 '15 at 05:34
  • Let me give you an example - suppose the table asks you for the world's best programmer's name, and the field is prepopulated with "MadProgrammer" when the application comes up because we all know of your talents. But you are a modest person, so you want to change the value to "tro" but then reconsider, so you hit undo a couple of times. But you hit it one time too many and the field goes blank. Would that make sense to you? I like this idea of turning off the undo support until the primary field is set. Is there a way to do this? – tro Nov 16 '15 at 05:43
  • It's probably not 100%, but it should get a step closer, take a look at the updates – MadProgrammer Nov 16 '15 at 06:02