File: UndoRedoSupport.java

package info (click to toggle)
libglazedlists-java 1.9.0%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 3,024 kB
  • ctags: 4,252
  • sloc: java: 22,561; xml: 818; sh: 51; makefile: 5
file content (373 lines) | stat: -rw-r--r-- 14,479 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
/* Glazed Lists                                                 (c) 2003-2006 */
/* http://publicobject.com/glazedlists/                      publicobject.com,*/
/*                                                     O'Dell Engineering Ltd.*/
package ca.odell.glazedlists;

import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;

import javax.swing.event.EventListenerList;
import java.util.*;

/**
 * UndoRedoSupport, as the name suggests, will provide generic support for
 * undoing and redoing groups of changes to an {@link EventList}. The
 * granularity of each undoable edit is determined by the ListEvent from which
 * it was generated.
 *
 * <p>Not every change described in a ListEvent results in an undoable edit.
 * Specifically, a <strong>mutation</strong> of a list element IN PLACE does
 * not produce an undoable edit. For example, an {@link ObservableElementList}
 * which observes a change of an element, or a call to {@link List#set} with
 * the same object at that index produce a ListEvent that does not have a
 * corresponding {@link Edit} object. These ListEvents are ignored because they
 * lack sufficient information to undo or redo the change.
 *
 * <p>In general UndoRedoSupport only makes sense for use with a
 * {@link BasicEventList} or a trivial wrapper around a BasicEventList which
 * does not affect the order or type of the elements, such as an
 * {@link ObservableElementList}. Advanced transformations, such as
 * {@link SortedList} or {@link FilterList} will not work as expected with this
 * UndoRedoSupport class since their contents are controlled by information
 * outside of themselves ({@link Comparator}s and
 * {@link ca.odell.glazedlists.matchers.Matcher}s).
 *
 * <p>This class is agnostic to any particular GUI toolkit. As such it may be
 * used in a headless environment or can also be bound to a specific toolkit.
 *
 * @author James Lemieux
 */
public final class UndoRedoSupport<E> {

    /** A wrapper around the true source EventList provides control over the granularity of ListEvents it produces. */
    private TransactionList<E> txSource;

    /** A ListEventListener that watches the {@link #txSource} and in turn broadcasts an {@link Edit} object to all {@link Listener}s */
    private ListEventListener<E> txSourceListener = new TXSourceListener();

    /** A data structure storing all registered {@link Listener}s. */
    private final EventListenerList listenerList = new EventListenerList();

    /**
     * A count which, when greater than 0, indicates a ListEvent must be
     * ignored by this UndoRedoSupport because it was caused by an undo or
     * redo. An int is used rather than a boolean flag so that nested
     * undos/redos are properly handled.
     */
    private int ignoreListEvent = 0;

    /**
     * A list of the elements in the <code>source</code> prior to the most
     * recent change. It is necessary to track the prior elements since
     * list removals broadcast ListEvents which do not include the removed
     * element as part of the ListEvent. We use this list to locate
     * removed elements when constructing objects that can undo the change.
     * todo remove this list when ListEvent can reliably furnish us with a deleted value
     */
    private List<E> priorElements;

    private UndoRedoSupport(EventList<E> source) {
        // build a TransactionList that does NOT support rollback - we don't
        // need it and it relies on UndoRedoSupport, so we would have 
        this.txSource = new TransactionList<E>(source, false);
        this.txSource.addListEventListener(txSourceListener);

        this.priorElements = new ArrayList<E>(source);
    }

    /**
     * Add a {@link Listener} which will receive a callback when an undoable
     * edit occurs on the given source {@link EventList}.
     */
    public void addUndoSupportListener(Listener l) {
	    listenerList.add(Listener.class, l);
    }

    /**
     * Remove a {@link Listener} from receiving a callback when an undoable
     * edit occurs on the given source {@link EventList}.
     */
    public void removeUndoSupportListener(Listener l) {
	    listenerList.remove(Listener.class, l);
    }

    /**
     * Notifies all registered {@link Listener}s of the given <code>edit</code>.
     */
    private void fireUndoableEditHappened(Edit edit) {
        Object[] listeners = listenerList.getListenerList();

        for (int i = listeners.length-2; i>=0; i-=2) {
            if (listeners[i]== Listener.class)
                ((Listener) listeners[i+1]).undoableEditHappened(edit);
        }
    }

    /**
     * Installs support for undoing and redoing changes to the given
     * <code>source</code>. To be notified of undoable changes, a
     * {@link Listener} must be registered on the object that is returned by
     * this method. That Listener object will typically add the {@link Edit}
     * it is given over to whatever data structure is managing all undo/redo
     * functions for the entire application.
     *
     * @param source the EventList on which to provide undo/redo capabilities
     * @return an instance of UndoRedoSupport through which the undo/redo behaviour
     *      can be customized
     */
    public static <E> UndoRedoSupport install(EventList<E> source) {
        return new UndoRedoSupport<E>(source);
    }

    /**
     * This method removes undo/redo support from the {@link EventList} it was
     * installed on. This method is useful when the {@link EventList} must
     * outlive the UndoRedoSupport object itself. The UndoRedoSupport object will become
     * available for garbage collection independently of the {@link EventList}
     * it decorated with behaviour.
     */
    public void uninstall() {
        txSource.dispose();
        txSource.removeListEventListener(txSourceListener);

        txSource = null;
        priorElements = null;
    }

    /**
     * This Listener watches the TransactionList for changes and responds by
     * created an {@link Edit} and broadcasting that object to all registered
     * {@link Listener}s.
     */
    private class TXSourceListener implements ListEventListener<E> {
        public void listChanged(ListEvent<E> listChanges) {
            // if an undo or redo caused this ListEvent, it is not an undoable edit
            if (ignoreListEvent > 0)
                return;

            // build a CompositeEdit that describes the ListEvent and provides methods for undoing and redoing it
            final CompositeEdit edit = new CompositeEdit();

            while (listChanges.next()) {
                final int changeIndex = listChanges.getIndex();
                final int changeType = listChanges.getType();

                // provide an AddEdit to the CompositeEdit
                if (changeType == ListEvent.INSERT) {
                    final E inserted = txSource.get(changeIndex);
                    priorElements.add(changeIndex, inserted);
                    edit.add(new AddEdit<E>(txSource, changeIndex, inserted));

                // provide a RemoveEdit to the CompositeEdit
                } else if (changeType == ListEvent.DELETE) {
                    // try to get the previous value through the ListEvent
                    E deleted = listChanges.getOldValue();
                    E deletedElementFromPrivateCopy = priorElements.remove(changeIndex);

                    // if the ListEvent could not give us the previous value, use the value from priorElements
                    if (deleted == ListEvent.UNKNOWN_VALUE)
                        deleted = deletedElementFromPrivateCopy;

                    edit.add(new RemoveEdit<E>(txSource, changeIndex, deleted));

                // provide an UpdateEdit to the CompositeEdit
                } else if (changeType == ListEvent.UPDATE) {
                    E previousValue = listChanges.getOldValue();

                    // if the ListEvent could not give us the previous value, use the value from priorElements
                    if (previousValue == ListEvent.UNKNOWN_VALUE)
                        previousValue = priorElements.get(changeIndex);

                    final E newValue = txSource.get(changeIndex);

                    // if a different object is present at the index
                    if (newValue != previousValue) {
                        priorElements.set(changeIndex, newValue);
                        edit.add(new UpdateEdit<E>(txSource, changeIndex, newValue, previousValue));
                    }
                }
            }

            // if the edit has real contents, broadcast it
            if (!edit.isEmpty())
                fireUndoableEditHappened(edit.getSimplestEdit());
        }
    }

    /**
     * Implementations of this Listener interface should be registered with an
     * UndoRedoSupport object via {@link UndoRedoSupport#addUndoSupportListener}. They
     * will be notified of each undoable edit that occurs to the given EventList.
     */
    public interface Listener extends EventListener {
        /**
         * Notified of each undoable edit applied to the given EventList.
         */
        public void undoableEditHappened(Edit edit);
    }

    /**
     * Provides an easy interface to undo/redo a ListEvent in its entirety.
     * At any point in time it is only possible to do one, and only one, of
     * {@link #undo} and {@link #redo}. To determine which one is allowed, use
     * {@link #canUndo()} and {@link #canRedo()}.
     */
    public interface Edit {
        /** Undo the edit. */
        public void undo();

        /** Returns true if this edit may be undone. */
        public boolean canUndo();

        /** Re-applies the edit. */
        public void redo();

        /** Returns true if this edit may be redone. */
        public boolean canRedo();
    }

    private abstract class AbstractEdit implements Edit {
        /** Initially the Edit can be undone but not redone. */
        protected boolean canUndo = true;

        public void undo() {
            // validate that we can proceed with the undo
            if (!canUndo())
                throw new IllegalStateException("The Edit is in an incorrect state for undoing");

            ignoreListEvent++;
            try {
                undoImpl();
            } finally {
                ignoreListEvent--;
            }

            canUndo = false;
        }

        public void redo() {
            // validate that we can proceed with the redo
            if (!canRedo())
                throw new IllegalStateException("The Edit is in an incorrect state for redoing");

            ignoreListEvent++;
            try {
                redoImpl();
            } finally {
                ignoreListEvent--;
            }

            canUndo = true;
        }

        protected abstract void undoImpl();
        protected abstract void redoImpl();

        public final boolean canUndo() { return canUndo; }
        public final boolean canRedo() { return !canUndo; }
    }

    /**
     * An Edit which acts as a container for finer-grained Edit objects.
     */
    final class CompositeEdit extends AbstractEdit {

        /** The edits in the order they were made. */
        private final List<Edit> edits = new ArrayList<Edit>();

        /** Adds a single Edit to this container of Edits. */
        void add(Edit edit) { edits.add(edit); }

        /** Returns <tt>true</tt> if this container of Edits is empty; <tt>false</tt> otherwise. */
        private boolean isEmpty() { return edits.isEmpty(); }

        /** Returns the single Edit contained within this composite, if only one exists, otherwise it returns this entire CompositeEdit. */
        private Edit getSimplestEdit() { return edits.size() == 1 ? edits.get(0) : this; }

        @Override
        public void undoImpl() {
            txSource.beginEvent();
            try {
                // undo the Edits in reverse order they were applied
                for (ListIterator<Edit> i = edits.listIterator(edits.size()); i.hasPrevious();)
                    i.previous().undo();
            } finally {
                txSource.commitEvent();
            }
        }

        @Override
        public void redoImpl() {
            txSource.beginEvent();
            try {
                // re-apply each edit in their original order
                for (Edit edit : edits)
                    edit.redo();
            } finally {
                txSource.commitEvent();
            }
        }
    }

    /**
     * A base class implementing common logic and storage for the specific kind
     * of Edits which can occur to a single index in the EventList.
     */
    private abstract class AbstractSimpleEdit<E> extends AbstractEdit {

        protected final EventList<E> source;
        protected final int index;
        protected final E value;

        protected AbstractSimpleEdit(EventList<E> source, int index, E value) {
            this.source = source;
            this.index = index;
            this.value = value;
        }
    }

    /**
     * A class describing an undoable Add to an EventList.
     */
    private final class AddEdit<E> extends AbstractSimpleEdit<E> {
        public AddEdit(EventList<E> source, int index, E value) {
            super(source, index, value);
        }

        @Override
        public void undoImpl() { source.remove(index); }
        @Override
        public void redoImpl() { source.add(index, value); }
    }

    /**
     * A class describing an undoable Remove to an EventList.
     */
    private final class RemoveEdit<E> extends AbstractSimpleEdit<E> {
        public RemoveEdit(EventList<E> source, int index, E value) {
            super(source, index, value);
        }

        @Override
        public void undoImpl() { source.add(index, value); }
        @Override
        public void redoImpl() { source.remove(index); }
    }

    /**
     * A class describing an undoable Update to an EventList.
     */
    private final class UpdateEdit<E> extends AbstractSimpleEdit<E> {
        private final E oldValue;

        public UpdateEdit(EventList<E> source, int index, E value, E oldValue) {
            super(source, index, value);
            this.oldValue = oldValue;
        }

        @Override
        public void undoImpl() { source.set(index, oldValue); }
        @Override
        public void redoImpl() { source.set(index, value); }
    }
}