001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package icy.undo;
020
021import java.util.ArrayList;
022import java.util.List;
023
024import javax.swing.UIManager;
025import javax.swing.event.EventListenerList;
026import javax.swing.event.UndoableEditEvent;
027import javax.swing.event.UndoableEditListener;
028import javax.swing.undo.AbstractUndoableEdit;
029import javax.swing.undo.CannotRedoException;
030import javax.swing.undo.CannotUndoException;
031import javax.swing.undo.CompoundEdit;
032import javax.swing.undo.UndoableEdit;
033
034/**
035 * Custom UndoManager for Icy.
036 * 
037 * @author Stephane
038 */
039// public class IcyUndoManager extends UndoManager implements IcyUndoableEditListener
040public class IcyUndoManager extends AbstractUndoableEdit implements UndoableEditListener
041{
042    /**
043     * 
044     */
045    private static final long serialVersionUID = 3080107472163005941L;
046
047    private static final int INITIAL_LIMIT = 64;
048
049    /**
050     * owner of UndoManager
051     */
052    protected final Object owner;
053
054    /**
055     * The collection of <code>AbstractIcyUndoableEdit</code>s
056     * undone/redone "en masse" by this <code>CompoundEdit</code>.
057     */
058    protected List<AbstractIcyUndoableEdit> edits;
059
060    /**
061     * listeners
062     */
063    protected final EventListenerList listeners;
064
065    /**
066     * internals
067     */
068    protected int indexOfNextAdd;
069    protected int limit;
070
071    public IcyUndoManager(Object owner, int limit)
072    {
073        super();
074
075        edits = new ArrayList<AbstractIcyUndoableEdit>(INITIAL_LIMIT / 2);
076        this.owner = owner;
077        listeners = new EventListenerList();
078        indexOfNextAdd = 0;
079        this.limit = limit;
080    }
081
082    public IcyUndoManager(Object owner)
083    {
084        this(owner, INITIAL_LIMIT);
085    }
086
087    /**
088     * @return the owner
089     */
090    public Object getOwner()
091    {
092        return owner;
093    }
094
095    /**
096     * Returns the maximum number of edits this {@code UndoManager} holds. A value less than 0
097     * indicates the number of edits is not limited.
098     * 
099     * @return the maximum number of edits this {@code UndoManager} holds
100     * @see #addEdit
101     * @see #setLimit
102     */
103    public synchronized int getLimit()
104    {
105        return limit;
106    }
107
108    /**
109     * Empties the undo manager sending each edit a <code>die</code> message
110     * in the process.
111     * 
112     * @see AbstractUndoableEdit#die
113     */
114    public void discardAllEdits()
115    {
116        if (trimEdits(0, edits.size() - 1))
117            fireChangeEvent();
118    }
119
120    /**
121     * Remove future edits (the ones to redo) from the undo manager sending each edit a
122     * <code>die</code> message in the process.
123     * 
124     * @param keep
125     *        number of future edits to keep (1 means we keep only the next edit)
126     * @see AbstractUndoableEdit#die
127     */
128    public synchronized void discardFutureEdits(int keep)
129    {
130        if (trimEdits(indexOfNextAdd + keep, edits.size() - 1))
131            fireChangeEvent();
132    }
133
134    /**
135     * Remove future edits (the ones to redo) from the undo manager sending each edit a
136     * <code>die</code> message in the process.
137     * 
138     * @see AbstractUndoableEdit#die
139     */
140    public synchronized void discardFutureEdits()
141    {
142        discardFutureEdits(0);
143    }
144
145    /**
146     * Remove previous edits (the ones to undo) from the undo manager sending each edit a
147     * <code>die</code> message
148     * in the process.
149     * 
150     * @param keep
151     *        number of previous edits to keep (1 mean we keep only the last edit)
152     * @see AbstractUndoableEdit#die
153     */
154    public synchronized void discardOldEdits(int keep)
155    {
156        if (trimEdits(0, indexOfNextAdd - (1 + keep)))
157            fireChangeEvent();
158    }
159
160    /**
161     * Remove old edits (the ones to undo) from the undo manager sending each edit a
162     * <code>die</code> message in the process.
163     * 
164     * @see AbstractUndoableEdit#die
165     */
166    public synchronized void discardOldEdits()
167    {
168        discardOldEdits(0);
169    }
170
171    /**
172     * Reduces the number of queued edits to a range of size limit,
173     * centered on the index of the next edit.
174     */
175    protected boolean trimForLimit()
176    {
177        boolean result = false;
178
179        if (limit >= 0)
180        {
181            synchronized (edits)
182            {
183                final int size = edits.size();
184
185                if (size > limit)
186                {
187                    final int halfLimit = limit / 2;
188                    int keepFrom = indexOfNextAdd - 1 - halfLimit;
189                    int keepTo = indexOfNextAdd - 1 + halfLimit;
190
191                    // These are ints we're playing with, so dividing by two
192                    // rounds down for odd numbers, so make sure the limit was
193                    // honored properly. Note that the keep range is
194                    // inclusive.
195                    if (keepTo - keepFrom + 1 > limit)
196                        keepFrom++;
197
198                    // The keep range is centered on indexOfNextAdd,
199                    // but odds are good that the actual edits Vector
200                    // isn't. Move the keep range to keep it legal.
201                    if (keepFrom < 0)
202                    {
203                        keepTo -= keepFrom;
204                        keepFrom = 0;
205                    }
206                    if (keepTo >= size)
207                    {
208                        int delta = size - keepTo - 1;
209                        keepTo += delta;
210                        keepFrom += delta;
211                    }
212
213                    if (trimEdits(keepTo + 1, size - 1))
214                        result = true;
215                    if (trimEdits(0, keepFrom - 1))
216                        result = true;
217                }
218            }
219        }
220
221        return result;
222    }
223
224    /**
225     * Removes edits in the specified range.
226     * All edits in the given range (inclusive, and in reverse order)
227     * will have <code>die</code> invoked on them and are removed from
228     * the list of edits. This has no effect if <code>from</code> &gt; <code>to</code>.
229     * 
230     * @param from
231     *        the minimum index to remove
232     * @param to
233     *        the maximum index to remove
234     * @return <code>true</code> if some edits has been removed.
235     */
236    protected boolean trimEdits(int from, int to)
237    {
238        if (from <= to)
239        {
240            synchronized (edits)
241            {
242                for (int i = to; i >= from; i--)
243                {
244                    edits.get(i).die();
245                    edits.remove(i);
246                }
247            }
248
249            if (indexOfNextAdd > to)
250                indexOfNextAdd -= to - from + 1;
251            else if (indexOfNextAdd >= from)
252                indexOfNextAdd = from;
253
254            return true;
255        }
256
257        return false;
258    }
259
260    /**
261     * Sets the maximum number of edits this <code>UndoManager</code> holds. A value less than 0
262     * indicates the number of edits is not limited. If edits need to be discarded
263     * to shrink the limit, <code>die</code> will be invoked on them in the reverse
264     * order they were added. The default is 100.
265     * 
266     * @param l
267     *        the new limit
268     * @throws RuntimeException
269     *         if this {@code UndoManager} is not in progress
270     *         ({@code end} has been invoked)
271     * @see #addEdit
272     * @see #getLimit
273     */
274    public synchronized void setLimit(int l)
275    {
276        limit = l;
277
278        if (trimForLimit())
279            fireChangeEvent();
280    }
281
282    /**
283     * Returns the the next significant edit to be undone if <code>undo</code> is invoked. This
284     * returns <code>null</code> if there are no edits to be undone.
285     * 
286     * @return the next significant edit to be undone
287     */
288    protected AbstractIcyUndoableEdit editToBeUndone()
289    {
290        synchronized (edits)
291        {
292            int i = indexOfNextAdd;
293
294            while (i > 0)
295            {
296                final AbstractIcyUndoableEdit edit = edits.get(--i);
297
298                if (edit.isSignificant())
299                    return edit;
300            }
301        }
302        return null;
303    }
304
305    /**
306     * Returns the the next significant edit to be redone if <code>redo</code> is invoked. This
307     * returns <code>null</code> if there are no edits to be redone.
308     * 
309     * @return the next significant edit to be redone
310     */
311    protected AbstractIcyUndoableEdit editToBeRedone()
312    {
313        synchronized (edits)
314        {
315            final int count = edits.size();
316            int i = indexOfNextAdd;
317
318            while (i < count)
319            {
320                final AbstractIcyUndoableEdit edit = edits.get(i++);
321
322                if (edit.isSignificant())
323                    return edit;
324            }
325        }
326
327        return null;
328    }
329
330    /**
331     * Undoes all changes.
332     * 
333     * @throws CannotUndoException
334     *         if one of the edits throws <code>CannotUndoException</code>
335     */
336    public void undoAll() throws CannotUndoException
337    {
338        while (indexOfNextAdd > 0)
339        {
340            final AbstractIcyUndoableEdit next = edits.get(--indexOfNextAdd);
341            next.undo();
342        }
343
344        // automatically remove useless edits
345        if (!canRedo())
346            discardFutureEdits();
347
348        fireChangeEvent();
349    }
350
351    /**
352     * Undoes all changes from the index of the next edit to <code>edit</code>, updating the index
353     * of the next edit appropriately.
354     * 
355     * @throws CannotUndoException
356     *         if one of the edits throws <code>CannotUndoException</code>
357     */
358    protected void undoTo(AbstractIcyUndoableEdit edit) throws CannotUndoException
359    {
360        boolean done = false;
361
362        while (!done)
363        {
364            final AbstractIcyUndoableEdit next = edits.get(--indexOfNextAdd);
365            next.undo();
366            done = (next == edit);
367        }
368    }
369
370    /**
371     * Undoes the appropriate edits. This invokes <code>undo</code> on all edits between the
372     * index of the next edit and the last significant edit, updating
373     * the index of the next edit appropriately.
374     * 
375     * @throws CannotUndoException
376     *         if one of the edits throws <code>CannotUndoException</code> or there are no edits
377     *         to be undone
378     * @see #canUndo
379     * @see #editToBeUndone
380     */
381    @Override
382    public synchronized void undo() throws CannotUndoException
383    {
384        final AbstractIcyUndoableEdit edit = editToBeUndone();
385
386        if (edit == null)
387            throw new CannotUndoException();
388
389        undoTo(edit);
390
391        // automatically remove useless edits
392        if (!canRedo())
393            discardFutureEdits();
394
395        fireChangeEvent();
396    }
397
398    /**
399     * Returns true if edits may be undone. This returns true if there are any edits to be undone
400     * (<code>editToBeUndone</code> returns non-<code>null</code>).
401     * 
402     * @return true if there are edits to be undone
403     * @see #editToBeUndone
404     */
405    @Override
406    public synchronized boolean canUndo()
407    {
408        final AbstractIcyUndoableEdit edit = editToBeUndone();
409        return (edit != null) && edit.canUndo();
410    }
411
412    /**
413     * Redoes all changes from the index of the next edit to <code>edit</code>, updating the index
414     * of the next edit appropriately.
415     * 
416     * @throws CannotRedoException
417     *         if one of the edits throws <code>CannotRedoException</code>
418     */
419    protected void redoTo(AbstractIcyUndoableEdit edit) throws CannotRedoException
420    {
421        boolean done = false;
422
423        while (!done)
424        {
425            final AbstractIcyUndoableEdit next = edits.get(indexOfNextAdd++);
426            next.redo();
427            done = (next == edit);
428        }
429    }
430
431    /**
432     * Redo the appropriate edits. This invokes <code>redo</code> on
433     * all edits between the index of the next edit and the next
434     * significant edit, updating the index of the next edit appropriately.
435     * 
436     * @throws CannotRedoException
437     *         if one of the edits throws <code>CannotRedoException</code> or there are no edits
438     *         to be redone
439     * @see CompoundEdit#end
440     * @see #canRedo
441     * @see #editToBeRedone
442     */
443    @Override
444    public synchronized void redo() throws CannotRedoException
445    {
446        AbstractIcyUndoableEdit edit = editToBeRedone();
447
448        if (edit == null)
449            throw new CannotRedoException();
450
451        redoTo(edit);
452
453        // automatically remove useless edits
454        if (!canUndo())
455            discardOldEdits();
456
457        fireChangeEvent();
458    }
459
460    /**
461     * Returns true if edits may be redone. If <code>end</code> has
462     * been invoked, this returns the value from super. Otherwise,
463     * this returns true if there are any edits to be redone
464     * (<code>editToBeRedone</code> returns non-<code>null</code>).
465     * 
466     * @return true if there are edits to be redone
467     * @see CompoundEdit#canRedo
468     * @see #editToBeRedone
469     */
470    @Override
471    public synchronized boolean canRedo()
472    {
473        final AbstractIcyUndoableEdit edit = editToBeRedone();
474        return (edit != null) && edit.canRedo();
475    }
476
477    /**
478     * Undo or redo all changes until the specified edit.<br>
479     * The specified edit should be in "done" state after the operation.<br>
480     * That means redo operation is inclusive while undo is exclusive.<br>
481     * To undo all operations just use undoAll().
482     */
483    public synchronized void undoOrRedoTo(AbstractIcyUndoableEdit edit) throws CannotRedoException, CannotUndoException
484    {
485        final int index = getIndex(edit);
486
487        // can undo or redo ?
488        if (index != -1)
489        {
490            // we want indexOfNextAdd to change to (index + 1)
491            while ((indexOfNextAdd - 1) > index)
492            {
493                // process undo
494                final AbstractIcyUndoableEdit next = edits.get(--indexOfNextAdd);
495                next.undo();
496            }
497            while (indexOfNextAdd <= index)
498            {
499                // process undo
500                AbstractIcyUndoableEdit next = edits.get(indexOfNextAdd++);
501                next.redo();
502            }
503
504            // automatically remove useless edits
505            if (!canUndo())
506                discardOldEdits();
507            if (!canRedo())
508                discardFutureEdits();
509
510            // notify change
511            fireChangeEvent();
512        }
513    }
514
515    /**
516     * Returns the last <code>AbstractIcyUndoableEdit</code> in <code>edits</code>, or
517     * <code>null</code> if <code>edits</code> is empty.
518     */
519    protected AbstractIcyUndoableEdit lastEdit()
520    {
521        synchronized (edits)
522        {
523            int count = edits.size();
524
525            if (count > 0)
526                return edits.get(count - 1);
527        }
528
529        return null;
530    }
531
532    @Override
533    public boolean addEdit(UndoableEdit anEdit)
534    {
535        if (anEdit instanceof AbstractIcyUndoableEdit)
536            addEdit((AbstractIcyUndoableEdit) anEdit);
537
538        return super.addEdit(anEdit);
539    }
540
541    /**
542     * Adds an <code>AbstractIcyUndoableEdit</code> to this <code>UndoManager</code>, if it's
543     * possible. This
544     * removes all edits from the index of the next edit to the end of the edits
545     * list.
546     * 
547     * @param anEdit
548     *        the edit to be added
549     * @see CompoundEdit#addEdit
550     */
551    public synchronized void addEdit(AbstractIcyUndoableEdit anEdit)
552    {
553        synchronized (edits)
554        {
555            // Trim from the indexOfNextAdd to the end, as we'll
556            // never reach these edits once the new one is added.
557            trimEdits(indexOfNextAdd, edits.size() - 1);
558
559            final AbstractIcyUndoableEdit last = lastEdit();
560
561            // If this is the first edit received, just add it.
562            // Otherwise, give the last one a chance to absorb the new
563            // one. If it won't, give the new one a chance to absorb
564            // the last one.
565            if (last == null)
566                edits.add(anEdit);
567            else if (!last.addEdit(anEdit))
568            {
569                // try to replace current edit
570                if (anEdit.replaceEdit(last))
571                    edits.set(edits.size() - 1, anEdit);
572                else
573                    // simply add the new edit
574                    edits.add(anEdit);
575            }
576
577            // make sure the indexOfNextAdd is pointed at the right place
578            indexOfNextAdd = edits.size();
579
580            // enforce the limit
581            trimForLimit();
582        }
583
584        // notify change
585        fireChangeEvent();
586    }
587
588    /**
589     * Prevent the last edit inserted in the UndoManager to be merged with the next inserted edit
590     * (even if they are compatible)
591     */
592    public void noMergeForNextEdit()
593    {
594        // set last edit to un-mergeable
595        if (indexOfNextAdd > 0)
596            edits.get(indexOfNextAdd - 1).setMergeable(false);
597    }
598
599    /**
600     * Returns a description of the undoable form of this edit.
601     * If there are edits to be undone, this returns
602     * the value from the next significant edit that will be undone.
603     * If there are no edits to be undone this returns the value from
604     * the <code>UIManager</code> property "AbstractUndoableEdit.undoText".
605     * 
606     * @return a description of the undoable form of this edit
607     * @see #undo
608     * @see CompoundEdit#getUndoPresentationName
609     */
610    @Override
611    public synchronized String getUndoPresentationName()
612    {
613        if (canUndo())
614            return editToBeUndone().getUndoPresentationName();
615
616        return UIManager.getString("AbstractUndoableEdit.undoText");
617    }
618
619    /**
620     * Returns a description of the redoable form of this edit.
621     * If there are edits to be redone, this returns
622     * the value from the next significant edit that will be redone.
623     * If there are no edits to be redone this returns the value from
624     * the <code>UIManager</code> property "AbstractUndoableEdit.redoText".
625     * 
626     * @return a description of the redoable form of this edit
627     * @see #redo
628     * @see CompoundEdit#getRedoPresentationName
629     */
630    @Override
631    public synchronized String getRedoPresentationName()
632    {
633        if (canRedo())
634            return editToBeRedone().getRedoPresentationName();
635
636        return UIManager.getString("AbstractUndoableEdit.redoText");
637    }
638
639    /**
640     * Add the specified listener to listeners list
641     */
642    public void addListener(IcyUndoManagerListener listener)
643    {
644        listeners.add(IcyUndoManagerListener.class, listener);
645    }
646
647    /**
648     * Remove the specified listener from listeners list
649     */
650    public void removeListener(IcyUndoManagerListener listener)
651    {
652        listeners.remove(IcyUndoManagerListener.class, listener);
653    }
654
655    /**
656     * Get listeners list
657     */
658    public IcyUndoManagerListener[] getListeners()
659    {
660        return listeners.getListeners(IcyUndoManagerListener.class);
661    }
662
663    /**
664     * fire change event
665     */
666    private void fireChangeEvent()
667    {
668        for (IcyUndoManagerListener listener : listeners.getListeners(IcyUndoManagerListener.class))
669            listener.undoManagerChanged(this);
670    }
671
672    /**
673     * Retrieve all edits in the UndoManager
674     */
675    public List<AbstractIcyUndoableEdit> getAllEdits()
676    {
677        synchronized (edits)
678        {
679            return new ArrayList<AbstractIcyUndoableEdit>(edits);
680        }
681    }
682
683    /**
684     * Get number of edit in UndoManager
685     */
686    public int getEditsCount()
687    {
688        return edits.size();
689    }
690
691    /**
692     * Get the index in list of specified edit
693     */
694    public int getIndex(AbstractIcyUndoableEdit e)
695    {
696        return edits.indexOf(e);
697    }
698
699    /**
700     * Get the index in list of specified significant edit
701     */
702    public int getSignificantIndex(AbstractIcyUndoableEdit e)
703    {
704        int result = 0;
705
706        synchronized (edits)
707        {
708            for (AbstractIcyUndoableEdit edit : edits)
709            {
710                if (edit.isSignificant())
711                {
712                    if (edit == e)
713                        return result;
714                    result++;
715                }
716            }
717        }
718
719        return -1;
720    }
721
722    /**
723     * Get number of significant edit before the specified position in UndoManager
724     */
725    public int getSignificantEditsCountBefore(int index)
726    {
727        int result = 0;
728
729        synchronized (edits)
730        {
731            for (int i = 0; i < index; i++)
732                if (edits.get(i).isSignificant())
733                    result++;
734        }
735
736        return result;
737    }
738
739    /**
740     * Get number of significant edit in UndoManager
741     */
742    public int getSignificantEditsCount()
743    {
744        int result = 0;
745
746        synchronized (edits)
747        {
748            for (AbstractIcyUndoableEdit edit : edits)
749                if (edit.isSignificant())
750                    result++;
751        }
752
753        return result;
754    }
755
756    /**
757     * Get edit of specified index
758     */
759    public AbstractIcyUndoableEdit getEdit(int index)
760    {
761        synchronized (edits)
762        {
763            if (index < edits.size())
764                return edits.get(index);
765        }
766
767        return null;
768    }
769
770    /**
771     * Get significant edit of specified index
772     */
773    public AbstractIcyUndoableEdit getSignificantEdit(int index)
774    {
775        int i = 0;
776
777        synchronized (edits)
778        {
779            for (AbstractIcyUndoableEdit edit : edits)
780            {
781                if (edit.isSignificant())
782                {
783                    if (i == index)
784                        return edit;
785                    i++;
786                }
787            }
788        }
789
790        return null;
791    }
792
793    /**
794     * Return the next insert index.
795     */
796    public int getNextAddIndex()
797    {
798        return indexOfNextAdd;
799    }
800
801    /**
802     * Get index of first edit from specified source
803     */
804    public int getFirstEditIndex(Object source)
805    {
806        if (source == null)
807            return -1;
808
809        synchronized (edits)
810        {
811            for (int i = 0; i < edits.size(); i++)
812                if ((edits.get(i)).getSource() == source)
813                    return i;
814        }
815
816        return -1;
817    }
818
819    /**
820     * Get index of last edit from specified source
821     */
822    public int getLastEditIndex(Object source)
823    {
824        if (source == null)
825            return -1;
826
827        synchronized (edits)
828        {
829            for (int i = edits.size() - 1; i >= 0; i--)
830                if ((edits.get(i)).getSource() == source)
831                    return i;
832        }
833
834        return -1;
835    }
836
837    /**
838     * Discard edits from specified source by sending each edit a <code>die</code> message
839     * in the process.
840     */
841    public void discardEdits(Object source)
842    {
843        synchronized (edits)
844        {
845            final int lastIndex = getLastEditIndex(source);
846
847            if (lastIndex != -1)
848            {
849                final List<AbstractIcyUndoableEdit> validEdits = new ArrayList<AbstractIcyUndoableEdit>();
850
851                // keep valid edits
852                for (int i = lastIndex + 1; i < edits.size(); i++)
853                    validEdits.add(edits.get(i));
854
855                // remove all edits
856                edits.clear();
857                indexOfNextAdd = 0;
858
859                // add valid edits
860                for (AbstractIcyUndoableEdit edit : validEdits)
861                    edits.add(edit);
862
863                // make sure the indexOfNextAdd is pointed at the right place
864                indexOfNextAdd = edits.size();
865
866                // notify we removed some edits
867                fireChangeEvent();
868            }
869        }
870    }
871
872    @Override
873    public void undoableEditHappened(UndoableEditEvent e)
874    {
875        addEdit(e.getEdit());
876    }
877
878}