UNDO/REDO 对于 JTextArea

UNDO/REDO for JTextArea

我正在写一个文本编辑器。而这里我运行陷入了困境。我需要为 JTextArea 实现 UNDO 和 Redo 功能。为此,我使用 UndoManager。但是,如果我取消或 return,则操作将被取消或一次 return 一个字符。如何使操作取消或 return不是按字符,而是按单词。

这是我的代码的样子:

 jta.getDocument().addUndoableEditListener( new UndoableEditListener() {
        public void undoableEditHappened(UndoableEditEvent e) {
             undoManager.addEdit(e.getEdit());      
        }
    });
 
         public static void undo() {
        if (undoManager.canUndo())
          try {
            undoManager.undo();
          } catch (CannotRedoException cre) {
            cre.printStackTrace();
          }  
      }
      
      public static void redo() {
        if (undoManager.canRedo())
          try {
            undoManager.redo();
          } catch (CannotRedoException cre) {
            cre.printStackTrace();
          }  
      }

很遗憾,此解决方案不起作用。我不明白如何使用它,因为我没有足够的编程经验: JTextPane undo and redo whole words

我进行了一些谷歌搜索,发现 JTextPane undo and redo whole words, which lead me to Merging UndoableEdits in one to be undone all together in JEditorPane.

对这个概念很感兴趣,我把它拆开并开始玩弄,这导致我遇到(至少一个)问题 - 如果你 select 所有文本并将其删除,它会导致 BadLocationException。所以我对它进行了更多尝试,我相信我已经为它实施了合适的修复。

我还添加了 ChangeListener 支持,因此您可以在状态更改时收到通知。

public class UndoManager extends AbstractUndoableEdit implements UndoableEditListener {
    private String lastEditName = null;
    private List<MergeComponentEdit> edits = new ArrayList<MergeComponentEdit>(32);
    private MergeComponentEdit current;
    private int pointer = -1;

    private List<ChangeListener> changeListeners = new ArrayList<>(8);

    public void addChangeListener(ChangeListener changeListener) {
        changeListeners.add(changeListener);
    }

    public void removeChangeListener(ChangeListener changeListener) {
        changeListeners.remove(changeListener);
    }

    public void undoableEditHappened(UndoableEditEvent e) {
        UndoableEdit edit = e.getEdit();
        if (edit instanceof AbstractDocument.DefaultDocumentEvent) {
            try {
                AbstractDocument.DefaultDocumentEvent event = (AbstractDocument.DefaultDocumentEvent) edit;
                int start = event.getOffset();
                int len = event.getLength();
                if (start + len > event.getDocument().getLength()) {
                    createCompoundEdit();
                    current.addEdit(edit);
                    lastEditName = edit.getPresentationName();
                } else {

                    String text = event.getDocument().getText(start, len);
                    boolean isNeedStart = false;
                    if (current == null) {
                        isNeedStart = true;
                    } else if (text.contains(" ")) {
                        isNeedStart = true;
                    } else if (lastEditName == null || !lastEditName.equals(edit.getPresentationName())) {
                        isNeedStart = true;
                    }

                    while (pointer < edits.size() - 1) {
                        edits.remove(edits.size() - 1);
                        isNeedStart = true;
                    }
                    if (isNeedStart) {
                        createCompoundEdit();
                    }

                    current.addEdit(edit);
                    lastEditName = edit.getPresentationName();
                }
                fireStateChanged();
            } catch (BadLocationException e1) {
                e1.printStackTrace();
            }
        }
    }

    public void createCompoundEdit() {
        if (current == null) {
            current = new MergeComponentEdit();
        } else if (current.getLength() > 0) {
            current = new MergeComponentEdit();
        }

        edits.add(current);
        pointer++;
    }

    public void undo() throws CannotUndoException {
        if (!canUndo()) {
            throw new CannotUndoException();
        }

        MergeComponentEdit u = edits.get(pointer);
        u.undo();
        pointer--;

        fireStateChanged();
    }

    public void redo() throws CannotUndoException {
        if (!canRedo()) {
            throw new CannotUndoException();
        }

        pointer++;
        MergeComponentEdit u = edits.get(pointer);
        u.redo();

        fireStateChanged();
    }

    public boolean canUndo() {
        return pointer >= 0;
    }

    public boolean canRedo() {
        return edits.size() > 0 && pointer < edits.size() - 1;
    }

    protected void fireStateChanged() {
        if (changeListeners.isEmpty()) {
            return;
        }
        ChangeEvent evt = new ChangeEvent(this);
        for (ChangeListener listener : changeListeners) {
            listener.stateChanged(evt);
        }
    }

    protected class MergeComponentEdit extends CompoundEdit {
        boolean isUnDone = false;

        public int getLength() {
            return edits.size();
        }

        public void undo() throws CannotUndoException {
            super.undo();
            isUnDone = true;
        }

        public void redo() throws CannotUndoException {
            super.redo();
            isUnDone = false;
        }

        public boolean canUndo() {
            return edits.size() > 0 && !isUnDone;
        }

        public boolean canRedo() {
            return edits.size() > 0 && isUnDone;
        }

    }
}

可运行示例

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JToolBar;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoableEdit;

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

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JButton btnUndo = new JButton("Undo");
                JButton btnRedo = new JButton("Redo");
                UndoManager undoManager = new UndoManager();
                undoManager.addChangeListener(new ChangeListener() {
                    @Override
                    public void stateChanged(ChangeEvent e) {
                        btnUndo.setEnabled(undoManager.canUndo());
                        btnRedo.setEnabled(undoManager.canRedo());
                    }
                });

                EditorPane editorPane = new EditorPane(undoManager);

                JToolBar tb = new JToolBar();
                btnUndo.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        undoManager.undo();
                    }
                });
                tb.add(btnUndo);
                btnRedo.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        undoManager.redo();
                    }
                });
                tb.add(btnRedo);

                JFrame frame = new JFrame();
                frame.add(tb, BorderLayout.NORTH);
                frame.add(new JScrollPane(editorPane));
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class EditorPane extends JPanel {

        private UndoManager undoManager;

        public EditorPane(UndoManager undoManager) {
            setLayout(new BorderLayout());
            this.undoManager = undoManager;
            JTextArea textArea = new JTextArea(20, 40);
            textArea.getDocument().addUndoableEditListener(undoManager);
            add(new JScrollPane(textArea));
        }

    }

    public class UndoManager extends AbstractUndoableEdit implements UndoableEditListener {
        private String lastEditName = null;
        private List<MergeComponentEdit> edits = new ArrayList<MergeComponentEdit>(32);
        private MergeComponentEdit current;
        private int pointer = -1;

        private List<ChangeListener> changeListeners = new ArrayList<>(8);

        public void addChangeListener(ChangeListener changeListener) {
            changeListeners.add(changeListener);
        }

        public void removeChangeListener(ChangeListener changeListener) {
            changeListeners.remove(changeListener);
        }

        public void undoableEditHappened(UndoableEditEvent e) {
            UndoableEdit edit = e.getEdit();
            if (edit instanceof AbstractDocument.DefaultDocumentEvent) {
                try {
                    AbstractDocument.DefaultDocumentEvent event = (AbstractDocument.DefaultDocumentEvent) edit;
                    int start = event.getOffset();
                    int len = event.getLength();
                    if (start + len > event.getDocument().getLength()) {
                        createCompoundEdit();
                        current.addEdit(edit);
                        lastEditName = edit.getPresentationName();
                    } else {

                        String text = event.getDocument().getText(start, len);
                        boolean isNeedStart = false;
                        if (current == null) {
                            isNeedStart = true;
                        } else if (text.contains(" ")) {
                            isNeedStart = true;
                        } else if (lastEditName == null || !lastEditName.equals(edit.getPresentationName())) {
                            isNeedStart = true;
                        }

                        while (pointer < edits.size() - 1) {
                            edits.remove(edits.size() - 1);
                            isNeedStart = true;
                        }
                        if (isNeedStart) {
                            createCompoundEdit();
                        }

                        current.addEdit(edit);
                        lastEditName = edit.getPresentationName();
                    }
                    fireStateChanged();
                } catch (BadLocationException e1) {
                    e1.printStackTrace();
                }
            }
        }

        public void createCompoundEdit() {
            if (current == null) {
                current = new MergeComponentEdit();
            } else if (current.getLength() > 0) {
                current = new MergeComponentEdit();
            }

            edits.add(current);
            pointer++;
        }

        public void undo() throws CannotUndoException {
            if (!canUndo()) {
                throw new CannotUndoException();
            }

            MergeComponentEdit u = edits.get(pointer);
            u.undo();
            pointer--;

            fireStateChanged();
        }

        public void redo() throws CannotUndoException {
            if (!canRedo()) {
                throw new CannotUndoException();
            }

            pointer++;
            MergeComponentEdit u = edits.get(pointer);
            u.redo();

            fireStateChanged();
        }

        public boolean canUndo() {
            return pointer >= 0;
        }

        public boolean canRedo() {
            return edits.size() > 0 && pointer < edits.size() - 1;
        }

        protected void fireStateChanged() {
            if (changeListeners.isEmpty()) {
                return;
            }
            ChangeEvent evt = new ChangeEvent(this);
            for (ChangeListener listener : changeListeners) {
                listener.stateChanged(evt);
            }
        }

        protected class MergeComponentEdit extends CompoundEdit {
            boolean isUnDone = false;

            public int getLength() {
                return edits.size();
            }

            public void undo() throws CannotUndoException {
                super.undo();
                isUnDone = true;
            }

            public void redo() throws CannotUndoException {
                super.redo();
                isUnDone = false;
            }

            public boolean canUndo() {
                return edits.size() > 0 && !isUnDone;
            }

            public boolean canRedo() {
                return edits.size() > 0 && isUnDone;
            }

        }
    }
}