JTextArea setText()和UndoManager

我正在使用UndoManager捕获我的JTextArea更改。

然而,方法setText()会删除所有内容,然后粘贴文本。 当我撤消时,我首先看到一个空白区域然后它将显示它之前有哪些文本。

如何重现:

  1. 运行以下代码
  2. 单击setText()按钮
  3. CTRL + Z撤消(你会看到一个空的textarea!)
  4. CTRL + Z撤消(您将看到实际的先前文本)

我想跳过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); } } 

你可以尝试这样的事情:

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

默认情况下, javax.swing.undo.UndoManager保留每个可撤消的编辑,包括删除原始文本的编辑(第3步)。 单个编辑无法访问,但您可以使用此处引用的方法对编辑进行分组 。 关于您的示例的一些其他说明:

  • 要获得更好的跨平台结果,请按照此处的建议使用getMenuShortcutKeyMask()

  • 使用布局 ; 如有必要, pack() 之后调用setSize() pack() ,如下所示。

  • 应该在事件派发线程上构造和操作Swing GUI对象。

代码 :

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

一个简单的解决方法是使用replaceRange:

 area.replaceRange(newText, 0, area.getText().length()); 

这计为一次编辑,因此只需一步即可撤消。

我需要一个解决方案,将删除/插入替换与单个撤消(aterai的答案)相结合,并将单个字符的连续插入/删除视为单个撤消(类似于http://java-sl.com/tip_merge_undo_edits.html )。

合并后的代码是:

  /*##################*/ /* 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 edits = new ArrayList(); 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(); } } } 

我用这种方式调用它:

 JTextArea textArea = new JTextArea(); TextUndoManager textAreaUndo = new TextUndoManager(); textArea.setDocument(new TextUndoPlainDocument(textAreaUndo));