001package arcommon.ui.other;
002
003import java.awt.event.ActionEvent;
004import java.util.ArrayList;
005
006import javax.swing.AbstractAction;
007import javax.swing.KeyStroke;
008import javax.swing.event.UndoableEditEvent;
009import javax.swing.event.UndoableEditListener;
010import javax.swing.text.Document;
011import javax.swing.text.JTextComponent;
012import javax.swing.undo.CannotRedoException;
013import javax.swing.undo.CannotUndoException;
014import javax.swing.undo.UndoManager;
015import javax.swing.undo.UndoableEdit;
016
017import arcommon.DateUtil;
018
019/**
020 * An UndoManager that can handle the double undo and redo that is created by a {@link javax.swing.text.JTextComponent#setText(String)} call.
021 * <br>
022 * The double undo is a remove (all) and a insert (all). These are combined by this UndoManager.<br>
023 * NOTE: The {@link #nextUndoIsDouble()} method MUST be MANUALLY called before the call to {@link javax.swing.text.JTextComponent#setText(String)}!<br>
024 * Or the two undos must be executed fast (&lt; 1msec) after each other (and {@link #FAST_EDITS_ARE_THE_SAME_EDIT} = true).<br>
025 * Fast like selecting text and then start writing. This is with this UndoManager also only one undo / redo.
026 */
027public class MyUndoManager extends UndoManager {
028    private static final long serialVersionUID = 4127345062834612017L;
029    
030    public static final boolean FAST_EDITS_ARE_THE_SAME_EDIT = true;
031
032    public static MyUndoManager addUndoToTextComponent(JTextComponent textComponent) {
033        // Code copied from http://www.exampledepot.com/egs/javax.swing.undo/UndoText.html
034        // and modified
035        final MyUndoManager myUndoManager = new MyUndoManager();
036        Document document = textComponent.getDocument();
037        
038        // Listen for undo and redo events
039        document.addUndoableEditListener(new UndoableEditListener() {
040            public void undoableEditHappened(UndoableEditEvent event) {
041                myUndoManager.addEdit(event.getEdit());
042            }
043        });
044
045        // Create an undo action and add it to the text component
046        textComponent.getActionMap().put("Undo", new AbstractAction("Undo") {
047            public void actionPerformed(ActionEvent event) {
048                try {
049                    if (myUndoManager.canUndo()) {
050                        myUndoManager.undo();
051                    }
052                } catch (CannotUndoException e) {
053                    ;
054                }
055            }
056            
057            private static final long serialVersionUID = 7732797549184931993L;
058        });
059
060        // Bind the undo action to control Z
061        textComponent.getInputMap().put(KeyStroke.getKeyStroke("control Z"), "Undo");
062
063        // Create a redo action and add it to the text component
064        textComponent.getActionMap().put("Redo", new AbstractAction("Redo") {
065            public void actionPerformed(ActionEvent event) {
066                try {
067                    if (myUndoManager.canRedo()) {
068                        myUndoManager.redo();
069                    }
070                } catch (CannotRedoException e) {
071                    ;
072                }
073            }
074            
075            private static final long serialVersionUID = 6838362662435784818L;
076        });
077        
078        // Bind the redo action to control shift Z
079        textComponent.getInputMap().put(KeyStroke.getKeyStroke("control shift Z"), "Redo");
080        
081        return myUndoManager;
082    }
083
084    private ArrayList<Boolean> doubleUndo;
085    private int index;
086    private boolean nextUndoIsDouble;
087    private long lastChangedTimeNano;
088    
089    public MyUndoManager() {
090        reset();
091    }
092    
093    private void reset() {
094        doubleUndo = new ArrayList<Boolean>();
095        index = -1;
096        lastChangedTimeNano = -1;
097    }
098
099    public void nextUndoIsDouble() {
100        nextUndoIsDouble = true;
101    }
102    
103    @Override
104    public synchronized boolean addEdit(UndoableEdit anEdit) {
105        if (index + 1 != doubleUndo.size()) {
106            doubleUndo = new ArrayList<Boolean>(doubleUndo.subList(0, index + 1));
107        }
108        doubleUndo.add(nextUndoIsDouble);
109        index++;
110        nextUndoIsDouble = false;
111        
112        long currentTime = System.nanoTime();
113        long diff = currentTime - lastChangedTimeNano;
114        if (diff < DateUtil.MILLISECOND_IN_NANOSECONDS && FAST_EDITS_ARE_THE_SAME_EDIT) {
115            doubleUndo.set(index - 1, Boolean.TRUE);
116        }
117        lastChangedTimeNano = currentTime;
118        
119        return super.addEdit(anEdit);
120    }
121    
122    @Override
123    public void undo() throws CannotUndoException {
124        if (index > 0 && doubleUndo.get(index - 1)) {
125            index--;
126            super.undo();
127        }
128        index--;
129        super.undo();
130    }
131    
132    @Override
133    public void redo() throws CannotRedoException {
134        if (index + 1 < doubleUndo.size() && doubleUndo.get(index + 1)) {
135            index++;
136            super.redo();
137        }
138        index++;
139        super.redo();
140    }
141    
142    @Override
143    public synchronized void discardAllEdits() {
144        reset();
145        super.discardAllEdits();
146    }
147    
148    @Override
149    public String toString() {
150        return "index: " + index + ", doubleUndo: " + doubleUndo + ", nextUndoIsDouble: " + nextUndoIsDouble;
151    }
152}