001/* An attribute that holds the undo/redo information about a model.
002
003 Copyright (c) 2003-2014 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023 PT_COPYRIGHT_VERSION_2
024 COPYRIGHTENDKEY
025
026 */
027package ptolemy.kernel.undo;
028
029import java.util.List;
030import java.util.Stack;
031
032import ptolemy.kernel.util.IllegalActionException;
033import ptolemy.kernel.util.InternalErrorException;
034import ptolemy.kernel.util.KernelException;
035import ptolemy.kernel.util.NameDuplicationException;
036import ptolemy.kernel.util.NamedObj;
037import ptolemy.kernel.util.SingletonAttribute;
038
039///////////////////////////////////////////////////////////////////
040//// UndoStackAttribute
041
042/**
043 This attribute holds the undo/redo information for a model.
044 This attribute is not persistent, so undo/redo information disappears
045 when the model is closed. It is also a singleton, meaning that it will
046 replace any previous attribute that has the same name
047 and is an instance of the same base class, SingletonAttribute.
048 <p>
049 Two stacks of information are maintained - one for undo information and
050 one for redo information. Normally, a push onto this stack puts the
051 undo information in the undo stack. However, if the push occurs during
052 the execution of an undo, then the information is put on the redo stack.
053 The entries on the stack implement the UndoAction interface.
054 <p>
055 NOTE: the information in the redo stack is emptied when a new undo action is
056 pushed onto the undo stack that was not the result of a redo being
057 requested. This situation arises when a user requests a series of undo
058 and redo operations, and then performs some normal undoable action. At this
059 point the information in the redo stack is not relevant to the state of
060 the model and so must be cleared.
061
062 @see UndoAction
063 @author Neil Smyth and Edward A. Lee
064 @version $Id$
065 @since Ptolemy II 3.1
066 @Pt.ProposedRating Yellow (eal)
067 @Pt.AcceptedRating Red (cxh)
068 */
069public class UndoStackAttribute extends SingletonAttribute {
070    /** Construct an attribute with the given name contained by the
071     *  specified container. The container argument must not be null,
072     *  or a NullPointerException will be thrown. This attribute will
073     *  use the workspace of the container for synchronization and
074     *  version counts. If the name argument is null, then the name is
075     *  set to the empty string. The object is added to the directory
076     *  of the workspace if the container is null. Increment the
077     *  version of the workspace.
078     *  @param container The container.
079     *  @param name The name of this attribute.
080     *  @exception IllegalActionException If the attribute is not of an
081     *   acceptable class for the container, or if the name contains a
082     *   period.
083     *  @exception NameDuplicationException If the name coincides with an
084     *   attribute already in the container.
085     */
086    public UndoStackAttribute(NamedObj container, String name)
087            throws IllegalActionException, NameDuplicationException {
088        super(container, name);
089        setPersistent(false);
090    }
091
092    ///////////////////////////////////////////////////////////////////
093    ////                         public methods                    ////
094
095    /** Get the UndoStackAttribute associated with the given object.
096     *  This is done by searching up the containment hierarchy until
097     *  such an attribute is found. If no such attribute is found,
098     *  then create and attach a new one to the top level.
099     *  This method gets read access on the workspace associated
100     *  with the specified object.
101     *  @param object The model for which an undo stack is required
102     *   (must not be null or a NullPointerException will the thrown).
103     *  @return The current undo stack attribute if there is one, or a new one.
104     */
105    public static UndoStackAttribute getUndoInfo(final NamedObj object) {
106        // Note, the parameter is final so we do not assign to it,
107        // so we are sure that we call getReadAccess on the same object.
108        try {
109            object.workspace().getReadAccess();
110
111            NamedObj topLevel = object.toplevel();
112            NamedObj container = object;
113
114            while (container != null) {
115                List attrList = container
116                        .attributeList(UndoStackAttribute.class);
117
118                if (attrList.size() > 0) {
119                    return (UndoStackAttribute) attrList.get(0);
120                }
121
122                container = container.getContainer();
123            }
124
125            // If we get here, there is no such attribute.
126            // Create and attach a new instance.
127            try {
128                return new UndoStackAttribute(topLevel, "_undoInfo");
129            } catch (KernelException e) {
130                throw new InternalErrorException(e);
131            }
132        } finally {
133            object.workspace().doneReading();
134        }
135    }
136
137    /** Merge the top two undo entries into a single action, unless
138     *  we are in either a redo or an undo, in which case the merge
139     *  happens automatically and need not be explicitly requested
140     *  by the client. If there
141     *  are fewer than two entries on the stack, do nothing. Note
142     *  that when two entries are merged, the one on the top of
143     *  the stack becomes the first one executed and the one
144     *  below that on the stack becomes the second one executed.
145     *  This method gets write access on the workspace.
146     */
147    public void mergeTopTwo() {
148        try {
149            workspace().getWriteAccess();
150
151            if (_inUndo == 0 && _inRedo == 0) {
152                if (_undoEntries.size() > 1) {
153                    UndoAction lastUndo = (UndoAction) _undoEntries.pop();
154                    UndoAction firstUndo = (UndoAction) _undoEntries.pop();
155                    UndoAction mergedAction = new MergeUndoActions(lastUndo,
156                            firstUndo);
157                    _undoEntries.push(mergedAction);
158
159                    if (_debugging) {
160                        _debug("=======> Merging top two on undo stack:\n"
161                                + mergedAction);
162                    }
163                }
164            }
165        } finally {
166            workspace().doneWriting();
167        }
168    }
169
170    /** Push an action to the undo stack, or if we are executing an undo,
171     *  onto the redo stack. This method gets write access on the workspace.
172     *  @param action The undo action.
173     */
174    public void push(UndoAction action) {
175        try {
176            workspace().getWriteAccess();
177
178            if (_inUndo > 1) {
179                UndoAction previousRedo = (UndoAction) _redoEntries.pop();
180                UndoAction mergedAction = new MergeUndoActions(action,
181                        previousRedo);
182                _redoEntries.push(mergedAction);
183
184                if (_debugging) {
185                    _debug("=======> Merging action onto redo stack to get:\n"
186                            + mergedAction);
187                }
188
189                _inUndo++;
190            } else if (_inUndo == 1) {
191                if (_debugging) {
192                    _debug("=======> Pushing action onto redo stack:\n"
193                            + action);
194                }
195
196                _redoEntries.push(action);
197                _inUndo++;
198            } else if (_inRedo > 1) {
199                UndoAction previousUndo = (UndoAction) _undoEntries.pop();
200                UndoAction mergedAction = new MergeUndoActions(action,
201                        previousUndo);
202
203                if (_debugging) {
204                    _debug("=======> Merging redo action onto undo stack to get:\n"
205                            + mergedAction);
206                }
207
208                _undoEntries.push(mergedAction);
209                _inRedo++;
210            } else if (_inRedo == 1) {
211                if (_debugging) {
212                    _debug("=======> Pushing redo action onto undo stack:\n"
213                            + action);
214                }
215
216                _undoEntries.push(action);
217                _inRedo++;
218            } else {
219                if (_debugging) {
220                    _debug("=======> Pushing action onto undo stack:\n"
221                            + action);
222                }
223
224                _undoEntries.push(action);
225
226                if (_debugging) {
227                    _debug("======= Clearing redo stack.\n");
228                }
229
230                _redoEntries.clear();
231            }
232        } finally {
233            // Do not increment the workspace version just
234            // because we pushed an action onto the stack.
235            workspace().doneTemporaryWriting();
236        }
237    }
238
239    /** Remove the top redo action and execute it.
240     *  If there are no redo entries, do nothing.
241     *  This method gets write access on the workspace.
242     *  @exception Exception If something goes wrong.
243     */
244    public void redo() throws Exception {
245        if (_redoEntries.size() > 0) {
246            try {
247                workspace().getWriteAccess();
248
249                UndoAction action = (UndoAction) _redoEntries.pop();
250
251                if (_debugging) {
252                    _debug("<====== Executing redo action:\n" + action);
253                }
254
255                try {
256                    _inRedo = 1;
257
258                    // NOTE: We assume that if in executing this
259                    // action any change request is made, that the
260                    // change request is honored before execute()
261                    // returns. Otherwise, _inRedo will erroneously
262                    // be back at 0 when that change is finally
263                    // executed.
264                    action.execute();
265                } finally {
266                    _inRedo = 0;
267                }
268            } finally {
269                workspace().doneWriting();
270            }
271        }
272    }
273
274    /** Remove the top undo action and execute it.
275     *  If there are no undo entries, do nothing.
276     *  This method gets write access on the workspace.
277     *  @exception Exception If something goes wrong.
278     */
279    public void undo() throws Exception {
280        try {
281            workspace().getWriteAccess();
282
283            if (_undoEntries.size() > 0) {
284                UndoAction action = (UndoAction) _undoEntries.pop();
285
286                if (_debugging) {
287                    _debug("<====== Executing undo action:\n" + action);
288                }
289
290                try {
291                    _inUndo = 1;
292                    action.execute();
293                } finally {
294                    _inUndo = 0;
295                }
296            }
297        } finally {
298            workspace().doneWriting();
299        }
300    }
301
302    ///////////////////////////////////////////////////////////////////
303    ////                         private named static classes      ////
304
305    /** An undo or redo action on the stack. */
306    private static class MergeUndoActions implements UndoAction {
307
308        // FindBugs suggested refactoring an inner class into this
309        // named static class, which we did.  This makes the class
310        // smaller and avoids leaks.
311
312        /** Create an undo action from two actions. */
313        public MergeUndoActions(UndoAction firstAction,
314                UndoAction secondAction) {
315            _firstAction = firstAction;
316            _secondAction = secondAction;
317        }
318
319        /** Execute the action. */
320        @Override
321        public void execute() throws Exception {
322            _firstAction.execute();
323            _secondAction.execute();
324        }
325
326        @Override
327        public String toString() {
328            return "Merged action.\nFirst part:\n" + _firstAction
329                    + "\n\nSecond part:\n" + _secondAction;
330        }
331
332        private UndoAction _firstAction;
333        private UndoAction _secondAction;
334    }
335
336    ///////////////////////////////////////////////////////////////////
337    ////                         private variables                 ////
338    // Counter used to count pushes during a redo.
339    private int _inRedo = 0;
340
341    // Counter used to count pushes during an undo.
342    private int _inUndo = 0;
343
344    // The stack of available redo entries
345    private Stack _redoEntries = new Stack();
346
347    // The stack of available undo entries
348    private Stack _undoEntries = new Stack();
349}