BullyWiiPlaza BullyWiiPlaza - 6 months ago 37
Java Question

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);
}
}

Answer

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;
    }
  }
}