f26c838cf766337c672639cd4a1a5a79e845f75e
[mono.git] / mcs / class / referencesource / System.Activities.Presentation / System.Activities.Presentation / System / Activities / Presentation / UndoEngine.cs
1 //----------------------------------------------------------------
2 // Copyright (c) Microsoft Corporation.  All rights reserved.
3 //----------------------------------------------------------------
4
5 namespace System.Activities.Presentation
6 {
7     using System;
8     using System.Activities.Presentation.Model;
9     using System.Collections.Generic;
10     using System.Diagnostics.CodeAnalysis;
11     using System.Globalization;
12     using System.Linq;
13     using System.Runtime;
14
15     [Fx.Tag.XamlVisible(false)]
16     public class UndoEngine : IUndoEngineOperations
17     {
18         const int capacity = 100;
19         List<UndoUnit> undoBuffer;
20         List<UndoUnit> redoBuffer;
21         EditingContext context;
22         Bookmark bookmark;
23         //interface which contains actual implementation of AddUndoUnit, Undo and Redo operations
24         IUndoEngineOperations undoEngineImpl = null;
25
26         public event EventHandler<UndoUnitEventArgs> UndoUnitAdded;
27         public event EventHandler<UndoUnitEventArgs> UndoCompleted;
28         public event EventHandler<UndoUnitEventArgs> RedoCompleted;
29         public event EventHandler<UndoUnitEventArgs> UndoUnitCancelled;
30         public event EventHandler UndoUnitDiscarded;
31         public event EventHandler UndoRedoBufferChanged;
32
33         public UndoEngine(EditingContext context)
34         {
35             this.context = context;
36             undoBuffer = new List<UndoUnit>(capacity);
37             redoBuffer = new List<UndoUnit>(capacity);
38             this.undoEngineImpl = this;
39         }
40
41         internal bool IsBookmarkInPlace { get { return this.bookmark != null; } }
42
43         // CreateImmediateEditingScope - creates a new ImmediateEditingScope which gatters all edits in its  
44         // undo unit list. all changes in ImmediateEditingScope appear as a one change and can be 
45         // undoned or redoned as a one set.
46         internal ImmediateEditingScope CreateImmediateEditingScope(string bookmarkName, ModelTreeManager modelTreeManager)
47         {
48             Fx.Assert(modelTreeManager != null, "modelTreeManager should not be null.");
49
50             //only one bookmark is supported
51             Fx.Assert(this.bookmark == null, "Nested bookmarks are not supported.");
52
53             //create bookmark undo unit, and give it a description
54             BookmarkUndoUnit unit = new BookmarkUndoUnit(this.context, modelTreeManager)
55             {
56                 Description = bookmarkName ?? string.Empty,
57             };
58             //create bookmark, and pass bookmark undo unit to it.
59             this.bookmark = new Bookmark(this, unit);
60             //switch implementation of AddUndoUnit, Undo, Redo to be delegated through bookmark
61             this.undoEngineImpl = bookmark;
62             return new ImmediateEditingScope(modelTreeManager, this.bookmark);
63         }
64
65         public IEnumerable<string> GetUndoActions()
66         {
67             return this.undoBuffer.Select(p => p.Description);
68         }
69
70         public IEnumerable<string> GetRedoActions()
71         {
72             return this.redoBuffer.Select(p => p.Description);
73         }
74
75         public void AddUndoUnit(UndoUnit unit)
76         {
77             if (unit == null)
78             {
79                 throw FxTrace.Exception.ArgumentNull("unit");
80             }
81             this.undoEngineImpl.AddUndoUnitCore(unit);
82         }
83
84         public bool Undo()
85         {
86             this.IsUndoRedoInProgress = true;
87             bool succeeded = this.undoEngineImpl.UndoCore();
88             this.IsUndoRedoInProgress = false;
89             if (succeeded)
90             {
91                 this.NotifyUndoRedoBufferChanged();
92             }
93             return succeeded;
94         }
95
96         public bool Redo()
97         {
98             this.IsUndoRedoInProgress = true;
99             bool succeeded = this.undoEngineImpl.RedoCore();
100             this.IsUndoRedoInProgress = false;
101             if (succeeded)
102             {
103                 this.NotifyUndoRedoBufferChanged();
104             }
105             return succeeded;
106         }
107
108         public bool IsUndoRedoInProgress
109         {
110             get;
111             private set;
112         }
113
114         void IUndoEngineOperations.AddUndoUnitCore(UndoUnit unit)
115         {
116             undoBuffer.Add(unit);
117             this.NotifyUndoUnitAdded(unit);
118
119             if (undoBuffer.Count > capacity)
120             {
121                 undoBuffer.RemoveAt(0);
122                 NotifyUndoUnitDiscarded();
123             }
124
125             redoBuffer.Clear();
126             this.NotifyUndoRedoBufferChanged();
127         }
128
129         bool IUndoEngineOperations.UndoCore()
130         {
131             bool succeeded = false;
132             if (undoBuffer.Count > 0)
133             {
134                 UndoUnit unitToUndo = undoBuffer.Last();
135                 undoBuffer.RemoveAt(undoBuffer.Count - 1);
136                 unitToUndo.Undo();
137                 redoBuffer.Add(unitToUndo);
138                 NotifyUndoExecuted(unitToUndo);
139                 succeeded = true;
140             }
141             return succeeded;
142         }
143
144         bool IUndoEngineOperations.RedoCore()
145         {
146             bool succeeded = false;
147             if (redoBuffer.Count > 0)
148             {
149                 UndoUnit unitToRedo = redoBuffer.Last();
150                 redoBuffer.RemoveAt(redoBuffer.Count - 1);
151                 unitToRedo.Redo();
152                 undoBuffer.Add(unitToRedo);
153                 NotifyRedoExecuted(unitToRedo);
154                 succeeded = true;
155             }
156             return succeeded;
157         }
158
159         private void NotifyUndoUnitAdded(UndoUnit unit)
160         {
161             if (this.UndoUnitAdded != null)
162             {
163                 this.UndoUnitAdded(this, new UndoUnitEventArgs() { UndoUnit = unit });
164             }
165         }
166
167         private void NotifyUndoExecuted(UndoUnit unit)
168         {
169             if (this.UndoCompleted != null)
170             {
171                 this.UndoCompleted(this, new UndoUnitEventArgs() { UndoUnit = unit });
172             }
173         }
174
175         private void NotifyRedoExecuted(UndoUnit unit)
176         {
177             if (this.RedoCompleted != null)
178             {
179                 this.RedoCompleted(this, new UndoUnitEventArgs() { UndoUnit = unit });
180             }
181         }
182
183         private void NotifyUndoUnitCancelled(UndoUnit unit)
184         {
185             if (this.UndoUnitCancelled != null)
186             {
187                 this.UndoUnitCancelled(this, new UndoUnitEventArgs() { UndoUnit = unit });
188             }
189         }
190
191         private void NotifyUndoUnitDiscarded()
192         {
193             if (this.UndoUnitDiscarded != null)
194             {
195                 this.UndoUnitDiscarded(this, null);
196             }
197         }
198
199         private void NotifyUndoRedoBufferChanged()
200         {
201             if (null != this.UndoRedoBufferChanged)
202             {
203                 this.UndoRedoBufferChanged(this, EventArgs.Empty);
204             }
205         }
206
207         //Bookmark implementation - implements core UndoEngine operations + IDisposable - 
208         //default bookmark behavior is to Rollback changes, unless committed explicitly. 
209         //usage of IDisposable enables usage of pattern:
210         // using (Bookmark b = new Bookmark())....
211         internal sealed class Bookmark : IDisposable, IUndoEngineOperations
212         {
213             BookmarkUndoUnit containerUndoUnit;
214             UndoEngine undoEngine;
215             bool isCommitted = false;
216             bool isRolledBack = false;
217             bool isDisposed = false;
218
219             internal Bookmark(UndoEngine undoEngine, BookmarkUndoUnit undoUnit)
220             {
221                 this.undoEngine = undoEngine;
222                 this.containerUndoUnit = undoUnit;
223             }
224         
225             public void CommitBookmark()
226             {
227                 //cannot commit more than once...
228                 if (this.isDisposed)
229                 {
230                     throw FxTrace.Exception.AsError(new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, SR.UndoEngine_OperationNotAllowed, "CommitBookmark")));
231                 }
232
233                 this.isCommitted = true;
234                 //restore original undo engine implementation
235                 this.undoEngine.undoEngineImpl = this.undoEngine;
236                 //get rid of the bookmark
237                 this.undoEngine.bookmark = null;
238                 //check if bookmark has any changes
239                 if (this.containerUndoUnit.DoList.Count != 0 || this.containerUndoUnit.RedoList.Count != 0)
240                 {
241                     //add all changes in bookmark into a undo list as a one element
242                     this.undoEngine.AddUndoUnit(this.containerUndoUnit);
243                 }
244                 //dispose bookmark
245                 this.Dispose();
246             }
247
248             public void RollbackBookmark()
249             {
250                 //cannot rollback more than once
251                 if (this.isDisposed)
252                 {
253                     throw FxTrace.Exception.AsError(new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, SR.UndoEngine_OperationNotAllowed, "RollbackBookmark")));
254                 }
255                 this.isRolledBack = true;
256                 //get through the list of all accumulated changes and reverse each of them
257                 foreach (UndoUnit unit in this.containerUndoUnit.DoList.Reverse<UndoUnit>())
258                 {
259                     unit.Undo();
260                 }
261                 //clear the lists
262                 this.containerUndoUnit.DoList.Clear();
263                 this.containerUndoUnit.RedoList.Clear();
264
265                 //restore original undo engine implementation
266                 this.undoEngine.undoEngineImpl = this.undoEngine;
267                 //get rid of the bookmark
268                 this.undoEngine.bookmark = null;
269                 this.undoEngine.NotifyUndoUnitCancelled(this.containerUndoUnit);
270                 //dispose bookmark
271                 this.Dispose();
272             }
273
274             public void Dispose()
275             {
276                 if (!this.isDisposed)
277                 {
278                     GC.SuppressFinalize(this);
279                     DisposeInternal();
280                 }
281             }
282
283             void DisposeInternal()
284             {
285                 if (!this.isDisposed)
286                 {
287                     //if not committed or rolled back - rollback by default  
288                     if (!this.isCommitted && !this.isRolledBack)
289                     {
290                         this.RollbackBookmark();
291                     }
292                     this.isDisposed = true;
293                 }
294             }
295
296             void IUndoEngineOperations.AddUndoUnitCore(UndoUnit unit)
297             {
298                 //add element to Undo list
299                 this.containerUndoUnit.DoList.Add(unit);
300                 //clear redo list
301                 this.containerUndoUnit.RedoList.Clear();
302             }
303
304             bool IUndoEngineOperations.UndoCore()
305             {
306                 //if there is anything to undo
307                 bool succeeded = false;
308                 if (this.containerUndoUnit.DoList.Count > 0)
309                 {
310                     //get the last element done
311                     UndoUnit unitToUndo = this.containerUndoUnit.DoList.Last();
312                     //remove it
313                     this.containerUndoUnit.DoList.RemoveAt(this.containerUndoUnit.DoList.Count - 1);
314                     //undo it
315                     unitToUndo.Undo();
316                     //and insert to the head of redo list
317                     this.containerUndoUnit.RedoList.Insert(0, unitToUndo);
318                     succeeded = true;
319                 }
320                 return succeeded;
321             }
322
323             bool IUndoEngineOperations.RedoCore()
324             {
325                 //if there is anything to redo
326                 bool succeeded = false;
327                 if (this.containerUndoUnit.RedoList.Count > 0)
328                 {
329                     //get first element to redo
330                     UndoUnit unitToRedo = this.containerUndoUnit.RedoList.First();
331                     //remove it
332                     this.containerUndoUnit.RedoList.RemoveAt(0);
333                     //redo it
334                     unitToRedo.Redo();
335                     //add it to the end of undo list
336                     this.containerUndoUnit.DoList.Add(unitToRedo);
337                     succeeded = true;
338                 }
339                 return succeeded;
340             }
341         }
342     }
343 }