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}