问题
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:
- Run the following code
- Click the
setText()
button - Press CTRL+Z to undo (you'll see an empty textarea!)
- 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);
}
}
回答1:
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;
}
}
}
回答2:
By default javax.swing.undo.UndoManager
retains each undoable edit, including the one that removes of the original text (your step three). Individual edits are inaccessible, but you can group edits using the approach cited here. Some additional notes on your example:
For better cross-platform results, use
getMenuShortcutKeyMask()
as suggested here.Use a layout; if necessary, invoke
setSize()
afterpack()
, as shown here.Swing GUI objects should be constructed and manipulated only on the event dispatch thread.
Code:
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import static javax.swing.JFrame.EXIT_ON_CLOSE;
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;
@SuppressWarnings("serial")
public class JTextComponentSetTextUndoEvent extends JFrame {
private static final int MASK
= Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
private JTextArea area = new JTextArea();
private UndoManager undoManager = new UndoManager();
public JTextComponentSetTextUndoEvent() {
setDefaultCloseOperation(EXIT_ON_CLOSE);
area.setText("Test");
add(area);
JButton btnSettext = new JButton("setText()");
btnSettext.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
area.setText("stackoverflow.com");
}
});
add(btnSettext, BorderLayout.PAGE_END);
Document doc = area.getDocument();
doc.addUndoableEditListener(new UndoableEditListener() {
@Override
public void undoableEditHappened(UndoableEditEvent e) {
undoManager.addEdit(e.getEdit());
System.out.println(e);
}
});
area.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);
}
}
});
area.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, MASK), "Undo");
area.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);
}
}
});
area.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Y,MASK), "Redo");
pack();
setSize(320, 240);
setLocationRelativeTo(null);
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
new JTextComponentSetTextUndoEvent().setVisible(true);
}
});
}
}
回答3:
A simple workaround is to use replaceRange:
area.replaceRange(newText, 0, area.getText().length());
This counts as a single edit, so is undone in one step.
回答4:
I needed a solution that combined grouping the remove/insert of replace with a single undo (aterai's answer) plus treating continuous insertion/deletions of single characters as a single undo (similar to http://java-sl.com/tip_merge_undo_edits.html).
The combined code is:
/*##################*/
/* TextCompoundEdit */
/*##################*/
class TextCompoundEdit extends CompoundEdit
{
private boolean isUnDone = false;
/*************/
/* getLength */
/*************/
public int getLength()
{
return edits.size();
}
/********/
/* undo */
/********/
public void undo() throws CannotUndoException
{
super.undo();
isUnDone = true;
}
/********/
/* redo */
/********/
public void redo() throws CannotUndoException
{
super.redo();
isUnDone = false;
}
/***********/
/* canUndo */
/***********/
public boolean canUndo()
{
return (edits.size() > 0) && (! isUnDone);
}
/***********/
/* canRedo */
/***********/
public boolean canRedo()
{
return (edits.size() > 0) && isUnDone;
}
}
/*#################*/
/* TextUndoManager */
/*#################*/
class TextUndoManager extends AbstractUndoableEdit
implements UndoableEditListener
{
private String lastEditName = null;
private int lastStart = 0;
private ArrayList<TextCompoundEdit> edits = new ArrayList<TextCompoundEdit>();
private TextCompoundEdit current;
private int pointer = -1;
private int groupIndex = 0;
private String groupName = null;
/************************/
/* undoableEditHappened */
/************************/
public void undoableEditHappened(
UndoableEditEvent e)
{
boolean isNeedStart = false;
UndoableEdit edit = e.getEdit();
if (! (edit instanceof AbstractDocument.DefaultDocumentEvent))
{ return; }
AbstractDocument.DefaultDocumentEvent event = (AbstractDocument.DefaultDocumentEvent) edit;
int start = event.getOffset();
String editName;
/*============================================*/
/* If an explicit group name is not present, */
/* use the INSERT/REMOVE name from the event. */
/*============================================*/
if (groupName != null)
{ editName = groupName; }
else
{ editName = event.getType().toString(); }
/*============================*/
/* Create a new compound edit */
/* for the very first edit. */
/*============================*/
if (current == null)
{ isNeedStart = true; }
/*============================*/
/* Create a new compound edit */
/* for a different operation. */
/*============================*/
else if ((lastEditName == null) ||
(! lastEditName.equals(editName)))
{ isNeedStart = true; }
/*================================================*/
/* Only group continuous single character inserts */
/* and deletes. Create a new edit if the user has */
/* moved the caret from its prior position. */
/*================================================*/
else if (groupName == null)
{
if ((event.getType() == DocumentEvent.EventType.INSERT) &&
(start != (lastStart + 1)))
{ isNeedStart = true; }
else if ((event.getType() == DocumentEvent.EventType.REMOVE) &&
(start != (lastStart - 1)))
{ isNeedStart = true; }
}
/*=========================================*/
/* Adding a new edit will clear all of the */
/* redos forward of the current position. */
/*=========================================*/
while (pointer < edits.size() - 1)
{
edits.remove(edits.size() - 1);
isNeedStart = true;
}
/*===================*/
/* Add the new edit. */
/*===================*/
if (isNeedStart)
{ createCompoundEdit(); }
current.addEdit(edit);
/*=====================================*/
/* Remember prior state for next edit. */
/*=====================================*/
lastEditName = editName;
lastStart = start;
}
/*********************/
/* startEditGrouping */
/*********************/
public void startEditGrouping()
{
groupName = "Group-" + groupIndex++;
}
/********************/
/* stopEditGrouping */
/********************/
public void stopEditGrouping()
{
groupName = null;
}
/**********************/
/* createCompoundEdit */
/**********************/
private void createCompoundEdit()
{
if (current == null)
{ current = new TextCompoundEdit(); }
else if (current.getLength() > 0)
{ current = new TextCompoundEdit(); }
edits.add(current);
pointer++;
}
/********/
/* undo */
/********/
public void undo() throws CannotUndoException
{
if (! canUndo())
{ throw new CannotUndoException(); }
TextCompoundEdit u = edits.get(pointer);
u.undo();
pointer--;
}
/********/
/* redo */
/********/
public void redo() throws CannotUndoException
{
if (! canRedo())
{ throw new CannotUndoException(); }
pointer++;
TextCompoundEdit u = edits.get(pointer);
u.redo();
}
/***********/
/* canUndo */
/***********/
public boolean canUndo()
{
return pointer >= 0;
}
/***********/
/* canRedo */
/***********/
public boolean canRedo()
{
return (edits.size() > 0) && (pointer < (edits.size() - 1));
}
}
/*#######################*/
/* TextUndoPlainDocument */
/*#######################*/
class TextUndoPlainDocument extends PlainDocument
{
private TextUndoManager undoManager;
/*************************/
/* TextUndoPlainDocument */
/*************************/
TextUndoPlainDocument(
TextUndoManager theManager)
{
super();
undoManager = theManager;
this.addUndoableEditListener(undoManager);
}
/***********/
/* replace */
/***********/
@Override
public void replace(
int offset,
int length,
String text,
AttributeSet attrs) throws BadLocationException
{
if (length == 0)
{ super.replace(offset,length,text,attrs); }
else
{
undoManager.startEditGrouping();
super.replace(offset,length,text,attrs);
undoManager.stopEditGrouping();
}
}
}
I invoke it in this way:
JTextArea textArea = new JTextArea();
TextUndoManager textAreaUndo = new TextUndoManager();
textArea.setDocument(new TextUndoPlainDocument(textAreaUndo));
来源:https://stackoverflow.com/questions/24433089/jtextarea-settext-undomanager