javaswingjtextareaundo-redo

JTextArea setText() & UndoManager


I'm using an UndoManager to capture changes in my JTextArea.

The method setText() however deletes everything and then pastes the text. When I undo I firstly see an empty area and then it would show which text it had before.

How to reproduce:

  1. Run the following code
  2. Click the setText() button
  3. Press CTRL+Z to undo (you'll see an empty textarea!)
  4. Press CTRL+Z to undo (you'll see the actual previous text)

I want to skip 3).

import javax.swing.AbstractAction;
import javax.swing.JFrame;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.Document;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;

import java.awt.event.ActionEvent;
import javax.swing.JButton;
import java.awt.event.ActionListener;

@SuppressWarnings("serial")
public class JTextComponentSetTextUndoEvent extends JFrame
{
    JTextArea area = new JTextArea();

    public JTextComponentSetTextUndoEvent()
    {
        setSize(300, 300);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        getContentPane().setLayout(null);

        area.setText("Test");
        area.setBounds(0, 96, 146, 165);
        getContentPane().add(area);

        JButton btnSettext = new JButton("setText()");
        btnSettext.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent arg0)
            {
                area.setText("stackoverflow.com");
            }
        });
        btnSettext.setBounds(0, 28, 200, 50);
        getContentPane().add(btnSettext);

        final UndoManager undoManager = new UndoManager();
        Document doc = area.getDocument();

        doc.addUndoableEditListener(new UndoableEditListener()
        {
            public void undoableEditHappened(UndoableEditEvent evt)
            {
                undoManager.addEdit(evt.getEdit());
            }
        });

        area.getActionMap().put("Undo", new AbstractAction("Undo")
        {
            public void actionPerformed(ActionEvent evt)
            {
                try
                {
                    if (undoManager.canUndo())
                    {
                        undoManager.undo();
                    }
                } catch (CannotUndoException e)
                {
                }
            }
        });

        area.getInputMap().put(KeyStroke.getKeyStroke("control Z"), "Undo");

        area.getActionMap().put("Redo", new AbstractAction("Redo")
        {
            public void actionPerformed(ActionEvent evt)
            {
                try
                {
                    if (undoManager.canRedo())
                    {
                        undoManager.redo();
                    }
                } catch (CannotRedoException e)
                {
                }
            }
        });

        area.getInputMap().put(KeyStroke.getKeyStroke("control Y"), "Redo");
    }

    public static void main(String[] args)
    {
        new JTextComponentSetTextUndoEvent().setVisible(true);
    }
}

Solution

  • You can try something like this:

    //Works fine for me on Windows 7 x64 using JDK 1.7.0_60:
    import java.awt.*;
    import java.awt.event.*;
    import java.util.*;
    import javax.swing.*;
    import javax.swing.event.*;
    import javax.swing.text.*;
    import javax.swing.undo.*;
    
    public final class UndoManagerTest {
      private final JTextField textField0 = new JTextField("default");
      private final JTextField textField1 = new JTextField();
      private final UndoManager undoManager0 = new UndoManager();
      private final UndoManager undoManager1 = new UndoManager();
    
      public JComponent makeUI() {
        textField1.setDocument(new CustomUndoPlainDocument());
        textField1.setText("aaaaaaaaaaaaaaaaaaaaa");
    
        textField0.getDocument().addUndoableEditListener(undoManager0);
        textField1.getDocument().addUndoableEditListener(undoManager1);
    
        JPanel p = new JPanel();
        p.add(new JButton(new AbstractAction("undo") {
          @Override public void actionPerformed(ActionEvent e) {
            if (undoManager0.canUndo()) {
              undoManager0.undo();
            }
            if (undoManager1.canUndo()) {
              undoManager1.undo();
            }
          }
        }));
        p.add(new JButton(new AbstractAction("redo") {
          @Override public void actionPerformed(ActionEvent e) {
            if (undoManager0.canRedo()) {
              undoManager0.redo();
            }
            if (undoManager1.canRedo()) {
              undoManager1.redo();
            }
          }
        }));
        p.add(new JButton(new AbstractAction("setText(new Date())") {
          @Override public void actionPerformed(ActionEvent e) {
            String str = new Date().toString();
            textField0.setText(str);
            textField1.setText(str);
          }
        }));
    
        Box box = Box.createVerticalBox();
        box.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
        box.add(makePanel("Default", textField0));
        box.add(Box.createVerticalStrut(5));
        box.add(makePanel("replace ignoring undo", textField1));
    
        JPanel pp = new JPanel(new BorderLayout());
        pp.add(box, BorderLayout.NORTH);
        pp.add(p, BorderLayout.SOUTH);
        return pp;
      }
      private static JPanel makePanel(String title, JComponent c) {
        JPanel p = new JPanel(new BorderLayout());
        p.setBorder(BorderFactory.createTitledBorder(title));
        p.add(c);
        return p;
      }
      public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
          @Override public void run() {
            createAndShowGUI();
          }
        });
      }
      public static void createAndShowGUI() {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        f.getContentPane().add(new UndoManagerTest().makeUI());
        f.setSize(320, 240);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
      }
    }
    
    class CustomUndoPlainDocument extends PlainDocument {
      private CompoundEdit compoundEdit;
      @Override protected void fireUndoableEditUpdate(UndoableEditEvent e) {
        if (compoundEdit == null) {
          super.fireUndoableEditUpdate(e);
        } else {
          compoundEdit.addEdit(e.getEdit());
        }
      }
      @Override public void replace(
          int offset, int length,
          String text, AttributeSet attrs) throws BadLocationException {
        if (length == 0) {
          System.out.println("insert");
          super.replace(offset, length, text, attrs);
        } else {
          System.out.println("replace");
          compoundEdit = new CompoundEdit();
          super.fireUndoableEditUpdate(new UndoableEditEvent(this, compoundEdit));
          super.replace(offset, length, text, attrs);
          compoundEdit.end();
          compoundEdit = null;
        }
      }
    }