Create FUndoManager to encapsulate making undo/redo logic smarter and lumping typing changes together

This commit is contained in:
drdev
2013-11-17 21:38:27 +00:00
parent 9242f3439c
commit 2abc9b3f7c
4 changed files with 257 additions and 82 deletions

View File

@@ -1,24 +1,17 @@
package forge.gui.toolbox;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.event.DocumentListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;
@SuppressWarnings("serial")
public class FTextEditor extends JScrollPane {
private final JTextArea tarEditor;
private final FUndoManager undoManager;
public FTextEditor() {
tarEditor = new JTextArea();
@@ -28,8 +21,9 @@ public class FTextEditor extends JScrollPane {
skin.setBackground(FSkin.getColor(FSkin.Colors.CLR_THEME2));
skin.setCaretColor(FSkin.getColor(FSkin.Colors.CLR_TEXT));
undoManager = new FUndoManager(tarEditor);
tarEditor.setMargin(new Insets(3, 3, 3, 3));
tarEditor.getDocument().addUndoableEditListener(new MyUndoableEditListener());
tarEditor.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
@@ -37,15 +31,15 @@ public class FTextEditor extends JScrollPane {
switch (e.getKeyCode()) {
case KeyEvent.VK_Z:
if (e.isShiftDown()) {
redoAction.actionPerformed(null);
undoManager.redo();
}
else {
undoAction.actionPerformed(null);
undoManager.undo();
}
break;
case KeyEvent.VK_Y:
if (!e.isShiftDown()) {
redoAction.actionPerformed(null);
undoManager.redo();
}
break;
}
@@ -89,73 +83,4 @@ public class FTextEditor extends JScrollPane {
public void addDocumentListener(DocumentListener listener) {
tarEditor.getDocument().addDocumentListener(listener);
}
//Undo/Redo
private UndoAction undoAction = new UndoAction();
private RedoAction redoAction = new RedoAction();
private UndoManager undoManager = new UndoManager();
private class MyUndoableEditListener implements UndoableEditListener {
public void undoableEditHappened(UndoableEditEvent e) {
undoManager.addEdit(e.getEdit());
undoAction.updateUndoState();
redoAction.updateRedoState();
}
}
private class UndoAction extends AbstractAction {
public UndoAction() {
super("Undo");
setEnabled(false);
}
public void actionPerformed(ActionEvent e) {
try {
undoManager.undo();
} catch (CannotUndoException ex) {
System.out.println("Unable to undo: " + ex);
ex.printStackTrace();
}
updateUndoState();
redoAction.updateRedoState();
}
protected void updateUndoState() {
if (undoManager.canUndo()) {
setEnabled(true);
putValue(Action.NAME, undoManager.getUndoPresentationName());
} else {
setEnabled(false);
putValue(Action.NAME, "Undo");
}
}
}
private class RedoAction extends AbstractAction {
public RedoAction() {
super("Redo");
setEnabled(false);
}
public void actionPerformed(ActionEvent e) {
try {
undoManager.redo();
} catch (CannotRedoException ex) {
System.out.println("Unable to redo: " + ex);
ex.printStackTrace();
}
updateRedoState();
undoAction.updateUndoState();
}
protected void updateRedoState() {
if (undoManager.canRedo()) {
setEnabled(true);
putValue(Action.NAME, undoManager.getRedoPresentationName());
} else {
setEnabled(false);
putValue(Action.NAME, "Redo");
}
}
}
}

View File

@@ -0,0 +1,250 @@
package forge.gui.toolbox;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.AbstractDocument;
import javax.swing.text.JTextComponent;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoManager;
import javax.swing.undo.UndoableEdit;
/*
** This class will merge individual edits into a single larger edit.
** That is, characters entered sequentially will be grouped together and
** undone as a group. Any attribute changes will be considered as part
** of the group and will therefore be undone when the group is undone.
*/
@SuppressWarnings("serial")
public class FUndoManager extends UndoManager
implements UndoableEditListener, DocumentListener
{
private UndoManager undoManager;
private CompoundEdit compoundEdit;
private JTextComponent textComponent;
private UndoAction undoAction;
private RedoAction redoAction;
// These fields are used to help determine whether the edit is an
// incremental edit. The offset and length should increase by 1 for
// each character added or decrease by 1 for each character removed.
private int lastOffset;
private int lastLength;
public FUndoManager(JTextComponent textComponent) {
this.textComponent = textComponent;
undoManager = this;
undoAction = new UndoAction();
redoAction = new RedoAction();
textComponent.getDocument().addUndoableEditListener(this);
}
/*
** Add a DocumentLister before the undo is done so we can position
** the Caret correctly as each edit is undone.
*/
public void undo() {
if (canUndo()) {
textComponent.getDocument().addDocumentListener(this);
super.undo();
textComponent.getDocument().removeDocumentListener(this);
}
}
/*
** Add a DocumentLister before the redo is done so we can position
** the Caret correctly as each edit is redone.
*/
public void redo() {
if (canRedo()) {
textComponent.getDocument().addDocumentListener(this);
super.redo();
textComponent.getDocument().removeDocumentListener(this);
}
}
/*
** Whenever an UndoableEdit happens the edit will either be absorbed
** by the current compound edit or a new compound edit will be started
*/
public void undoableEditHappened(UndoableEditEvent e) {
// Start a new compound edit
if (compoundEdit == null) {
compoundEdit = startCompoundEdit(e.getEdit());
return;
}
int offsetChange = textComponent.getCaretPosition() - lastOffset;
int lengthChange = textComponent.getDocument().getLength() - lastLength;
// Check for an attribute change
AbstractDocument.DefaultDocumentEvent event = (AbstractDocument.DefaultDocumentEvent)e.getEdit();
if (event.getType().equals(DocumentEvent.EventType.CHANGE)) {
if (offsetChange == 0) {
compoundEdit.addEdit(e.getEdit());
return;
}
}
// Check for an incremental edit or backspace.
// The Change in Caret position and Document length should both be
// either 1 or -1.
if (offsetChange == lengthChange && Math.abs(offsetChange) == 1) {
compoundEdit.addEdit(e.getEdit());
lastOffset = textComponent.getCaretPosition();
lastLength = textComponent.getDocument().getLength();
return;
}
// Not incremental edit, end previous edit and start a new one
compoundEdit.end();
compoundEdit = startCompoundEdit(e.getEdit());
}
/*
** Each CompoundEdit will store a group of related incremental edits
** (ie. each character typed or backspaced is an incremental edit)
*/
private CompoundEdit startCompoundEdit(UndoableEdit anEdit) {
// Track Caret and Document information of this compound edit
lastOffset = textComponent.getCaretPosition();
lastLength = textComponent.getDocument().getLength();
// The compound edit is used to store incremental edits
compoundEdit = new MyCompoundEdit();
compoundEdit.addEdit(anEdit);
// The compound edit is added to the UndoManager. All incremental
// edits stored in the compound edit will be undone/redone at once
addEdit(compoundEdit);
undoAction.updateUndoState();
redoAction.updateRedoState();
return compoundEdit;
}
/*
* The Action to Undo changes to the Document.
* The state of the Action is managed by the CompoundUndoManager
*/
public Action getUndoAction() {
return undoAction;
}
/*
* The Action to Redo changes to the Document.
* The state of the Action is managed by the CompoundUndoManager
*/
public Action getRedoAction() {
return redoAction;
}
/*
* Updates to the Document as a result of Undo/Redo will cause the
* Caret to be repositioned
*/
public void insertUpdate(final DocumentEvent e) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
int offset = e.getOffset() + e.getLength();
offset = Math.min(offset, textComponent.getDocument().getLength());
textComponent.setCaretPosition(offset);
}
});
}
public void removeUpdate(DocumentEvent e) {
textComponent.setCaretPosition(e.getOffset());
}
public void changedUpdate(DocumentEvent e) {}
private class MyCompoundEdit extends CompoundEdit {
public boolean isInProgress() {
// in order for the canUndo() and canRedo() methods to work
// assume that the compound edit is never in progress
return false;
}
public void undo() throws CannotUndoException {
// End the edit so future edits don't get absorbed by this edit
if (compoundEdit != null)
compoundEdit.end();
super.undo();
// Always start a new compound edit after an undo
compoundEdit = null;
}
}
/*
* Perform the Undo and update the state of the undo/redo Actions
*/
private class UndoAction extends AbstractAction {
public UndoAction() {
putValue(Action.NAME, "Undo");
putValue(Action.SHORT_DESCRIPTION, getValue(Action.NAME));
putValue(Action.MNEMONIC_KEY, new Integer(KeyEvent.VK_U));
putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_MASK));
setEnabled(false);
}
public void actionPerformed(ActionEvent e) {
try {
undoManager.undo();
textComponent.requestFocusInWindow();
}
catch (CannotUndoException ex) {}
updateUndoState();
redoAction.updateRedoState();
}
private void updateUndoState() {
setEnabled(undoManager.canUndo());
}
}
/*
* Perform the Redo and update the state of the undo/redo Actions
*/
private class RedoAction extends AbstractAction {
public RedoAction() {
putValue(Action.NAME, "Redo");
putValue(Action.SHORT_DESCRIPTION, getValue(Action.NAME));
putValue(Action.MNEMONIC_KEY, new Integer(KeyEvent.VK_R));
putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_MASK));
setEnabled(false);
}
public void actionPerformed(ActionEvent e) {
try {
undoManager.redo();
textComponent.requestFocusInWindow();
}
catch (CannotRedoException ex) {}
updateRedoState();
undoAction.updateUndoState();
}
protected void updateRedoState() {
setEnabled(undoManager.canRedo());
}
}
}

View File

@@ -4,7 +4,6 @@ import java.io.File;
import java.io.PrintWriter;
import javax.swing.JOptionPane;
import javax.swing.JTextArea;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;