package org.solrmarc.debug; import java.awt.event.*; import javax.swing.*; import javax.swing.text.*; import javax.swing.event.*; import javax.swing.undo.*; /* ** 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. */ public class CompoundUndoManager extends UndoManager implements UndoableEditListener, DocumentListener { /** * */ private static final long serialVersionUID = 4418890504436288446L; 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 CompoundUndoManager(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() { 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() { 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. // int offsetChange = textComponent.getCaretPosition() - lastOffset; // int lengthChange = textComponent.getDocument().getLength() - lastLength; 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; } // // Implement DocumentListener // /* * 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) {} class MyCompoundEdit extends CompoundEdit { /** * */ private static final long serialVersionUID = 4988240353630290406L; 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 */ class UndoAction extends AbstractAction { /** * */ private static final long serialVersionUID = 3755896176417375970L; 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("control Z") ); 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 */ class RedoAction extends AbstractAction { /** * */ private static final long serialVersionUID = -3275708654104430189L; 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() ); } } }