Free cookie consent management tool by TermsFeed Policy Generator

source: branches/RemoveBackwardsCompatibility/HeuristicLab.ExtLibs/HeuristicLab.AvalonEdit/5.0.1/AvalonEdit-5.0.1/Document/UndoStack.cs @ 13346

Last change on this file since 13346 was 11700, checked in by jkarder, 10 years ago

#2077: created branch and added first version

File size: 14.3 KB
Line 
1// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy of this
4// software and associated documentation files (the "Software"), to deal in the Software
5// without restriction, including without limitation the rights to use, copy, modify, merge,
6// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
7// to whom the Software is furnished to do so, subject to the following conditions:
8//
9// The above copyright notice and this permission notice shall be included in all copies or
10// substantial portions of the Software.
11//
12// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
15// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
17// DEALINGS IN THE SOFTWARE.
18
19using System;
20using System.Collections.Generic;
21using System.ComponentModel;
22using System.Diagnostics;
23using ICSharpCode.AvalonEdit.Utils;
24
25namespace ICSharpCode.AvalonEdit.Document
26{
27  /// <summary>
28  /// Undo stack implementation.
29  /// </summary>
30  [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")]
31  public sealed class UndoStack : INotifyPropertyChanged
32  {
33    /// undo stack is listening for changes
34    internal const int StateListen = 0;
35    /// undo stack is reverting/repeating a set of changes
36    internal const int StatePlayback = 1;
37    // undo stack is reverting/repeating a set of changes and modifies the document to do this
38    internal const int StatePlaybackModifyDocument = 2;
39    /// state is used for checking that noone but the UndoStack performs changes
40    /// during Undo events
41    internal int state = StateListen;
42   
43    Deque<IUndoableOperation> undostack = new Deque<IUndoableOperation>();
44    Deque<IUndoableOperation> redostack = new Deque<IUndoableOperation>();
45    int sizeLimit = int.MaxValue;
46   
47    int undoGroupDepth;
48    int actionCountInUndoGroup;
49    int optionalActionCount;
50    object lastGroupDescriptor;
51    bool allowContinue;
52   
53    #region IsOriginalFile implementation
54    // implements feature request SD2-784 - File still considered dirty after undoing all changes
55   
56    /// <summary>
57    /// Number of times undo must be executed until the original state is reached.
58    /// Negative: number of times redo must be executed until the original state is reached.
59    /// Special case: int.MinValue == original state is unreachable
60    /// </summary>
61    int elementsOnUndoUntilOriginalFile;
62   
63    bool isOriginalFile = true;
64   
65    /// <summary>
66    /// Gets whether the document is currently in its original state (no modifications).
67    /// </summary>
68    public bool IsOriginalFile {
69      get { return isOriginalFile; }
70    }
71   
72    void RecalcIsOriginalFile()
73    {
74      bool newIsOriginalFile = (elementsOnUndoUntilOriginalFile == 0);
75      if (newIsOriginalFile != isOriginalFile) {
76        isOriginalFile = newIsOriginalFile;
77        NotifyPropertyChanged("IsOriginalFile");
78      }
79    }
80   
81    /// <summary>
82    /// Marks the current state as original. Discards any previous "original" markers.
83    /// </summary>
84    public void MarkAsOriginalFile()
85    {
86      elementsOnUndoUntilOriginalFile = 0;
87      RecalcIsOriginalFile();
88    }
89   
90    /// <summary>
91    /// Discards the current "original" marker.
92    /// </summary>
93    public void DiscardOriginalFileMarker()
94    {
95      elementsOnUndoUntilOriginalFile = int.MinValue;
96      RecalcIsOriginalFile();
97    }
98   
99    void FileModified(int newElementsOnUndoStack)
100    {
101      if (elementsOnUndoUntilOriginalFile == int.MinValue)
102        return;
103     
104      elementsOnUndoUntilOriginalFile += newElementsOnUndoStack;
105      if (elementsOnUndoUntilOriginalFile > undostack.Count)
106        elementsOnUndoUntilOriginalFile = int.MinValue;
107     
108      // don't call RecalcIsOriginalFile(): wait until end of undo group
109    }
110    #endregion
111   
112    /// <summary>
113    /// Gets if the undo stack currently accepts changes.
114    /// Is false while an undo action is running.
115    /// </summary>
116    public bool AcceptChanges {
117      get { return state == StateListen; }
118    }
119   
120    /// <summary>
121    /// Gets if there are actions on the undo stack.
122    /// Use the PropertyChanged event to listen to changes of this property.
123    /// </summary>
124    public bool CanUndo {
125      get { return undostack.Count > 0; }
126    }
127   
128    /// <summary>
129    /// Gets if there are actions on the redo stack.
130    /// Use the PropertyChanged event to listen to changes of this property.
131    /// </summary>
132    public bool CanRedo {
133      get { return redostack.Count > 0; }
134    }
135   
136    /// <summary>
137    /// Gets/Sets the limit on the number of items on the undo stack.
138    /// </summary>
139    /// <remarks>The size limit is enforced only on the number of stored top-level undo groups.
140    /// Elements within undo groups do not count towards the size limit.</remarks>
141    public int SizeLimit {
142      get { return sizeLimit; }
143      set {
144        if (value < 0)
145          ThrowUtil.CheckNotNegative(value, "value");
146        if (sizeLimit != value) {
147          sizeLimit = value;
148          NotifyPropertyChanged("SizeLimit");
149          if (undoGroupDepth == 0)
150            EnforceSizeLimit();
151        }
152      }
153    }
154   
155    void EnforceSizeLimit()
156    {
157      Debug.Assert(undoGroupDepth == 0);
158      while (undostack.Count > sizeLimit)
159        undostack.PopFront();
160      while (redostack.Count > sizeLimit)
161        redostack.PopFront();
162    }
163   
164    /// <summary>
165    /// If an undo group is open, gets the group descriptor of the current top-level
166    /// undo group.
167    /// If no undo group is open, gets the group descriptor from the previous undo group.
168    /// </summary>
169    /// <remarks>The group descriptor can be used to join adjacent undo groups:
170    /// use a group descriptor to mark your changes, and on the second action,
171    /// compare LastGroupDescriptor and use <see cref="StartContinuedUndoGroup"/> if you
172    /// want to join the undo groups.</remarks>
173    public object LastGroupDescriptor {
174      get { return lastGroupDescriptor; }
175    }
176   
177    /// <summary>
178    /// Starts grouping changes.
179    /// Maintains a counter so that nested calls are possible.
180    /// </summary>
181    public void StartUndoGroup()
182    {
183      StartUndoGroup(null);
184    }
185   
186    /// <summary>
187    /// Starts grouping changes.
188    /// Maintains a counter so that nested calls are possible.
189    /// </summary>
190    /// <param name="groupDescriptor">An object that is stored with the undo group.
191    /// If this is not a top-level undo group, the parameter is ignored.</param>
192    public void StartUndoGroup(object groupDescriptor)
193    {
194      if (undoGroupDepth == 0) {
195        actionCountInUndoGroup = 0;
196        optionalActionCount = 0;
197        lastGroupDescriptor = groupDescriptor;
198      }
199      undoGroupDepth++;
200      //Util.LoggingService.Debug("Open undo group (new depth=" + undoGroupDepth + ")");
201    }
202   
203    /// <summary>
204    /// Starts grouping changes, continuing with the previously closed undo group if possible.
205    /// Maintains a counter so that nested calls are possible.
206    /// If the call to StartContinuedUndoGroup is a nested call, it behaves exactly
207    /// as <see cref="StartUndoGroup()"/>, only top-level calls can continue existing undo groups.
208    /// </summary>
209    /// <param name="groupDescriptor">An object that is stored with the undo group.
210    /// If this is not a top-level undo group, the parameter is ignored.</param>
211    public void StartContinuedUndoGroup(object groupDescriptor = null)
212    {
213      if (undoGroupDepth == 0) {
214        actionCountInUndoGroup = (allowContinue && undostack.Count > 0) ? 1 : 0;
215        optionalActionCount = 0;
216        lastGroupDescriptor = groupDescriptor;
217      }
218      undoGroupDepth++;
219      //Util.LoggingService.Debug("Continue undo group (new depth=" + undoGroupDepth + ")");
220    }
221   
222    /// <summary>
223    /// Stops grouping changes.
224    /// </summary>
225    public void EndUndoGroup()
226    {
227      if (undoGroupDepth == 0) throw new InvalidOperationException("There are no open undo groups");
228      undoGroupDepth--;
229      //Util.LoggingService.Debug("Close undo group (new depth=" + undoGroupDepth + ")");
230      if (undoGroupDepth == 0) {
231        Debug.Assert(state == StateListen || actionCountInUndoGroup == 0);
232        allowContinue = true;
233        if (actionCountInUndoGroup == optionalActionCount) {
234          // only optional actions: don't store them
235          for (int i = 0; i < optionalActionCount; i++) {
236            undostack.PopBack();
237          }
238          allowContinue = false;
239        } else if (actionCountInUndoGroup > 1) {
240          // combine all actions within the group into a single grouped action
241          undostack.PushBack(new UndoOperationGroup(undostack, actionCountInUndoGroup));
242          FileModified(-actionCountInUndoGroup + 1 + optionalActionCount);
243        }
244        //if (state == StateListen) {
245        EnforceSizeLimit();
246        RecalcIsOriginalFile(); // can raise event
247        //}
248      }
249    }
250   
251    /// <summary>
252    /// Throws an InvalidOperationException if an undo group is current open.
253    /// </summary>
254    void ThrowIfUndoGroupOpen()
255    {
256      if (undoGroupDepth != 0) {
257        undoGroupDepth = 0;
258        throw new InvalidOperationException("No undo group should be open at this point");
259      }
260      if (state != StateListen) {
261        throw new InvalidOperationException("This method cannot be called while an undo operation is being performed");
262      }
263    }
264   
265    List<TextDocument> affectedDocuments;
266   
267    internal void RegisterAffectedDocument(TextDocument document)
268    {
269      if (affectedDocuments == null)
270        affectedDocuments = new List<TextDocument>();
271      if (!affectedDocuments.Contains(document)) {
272        affectedDocuments.Add(document);
273        document.BeginUpdate();
274      }
275    }
276   
277    void CallEndUpdateOnAffectedDocuments()
278    {
279      if (affectedDocuments != null) {
280        foreach (TextDocument doc in affectedDocuments) {
281          doc.EndUpdate();
282        }
283        affectedDocuments = null;
284      }
285    }
286   
287    /// <summary>
288    /// Call this method to undo the last operation on the stack
289    /// </summary>
290    public void Undo()
291    {
292      ThrowIfUndoGroupOpen();
293      if (undostack.Count > 0) {
294        // disallow continuing undo groups after undo operation
295        lastGroupDescriptor = null; allowContinue = false;
296        // fetch operation to undo and move it to redo stack
297        IUndoableOperation uedit = undostack.PopBack();
298        redostack.PushBack(uedit);
299        state = StatePlayback;
300        try {
301          RunUndo(uedit);
302        } finally {
303          state = StateListen;
304          FileModified(-1);
305          CallEndUpdateOnAffectedDocuments();
306        }
307        RecalcIsOriginalFile();
308        if (undostack.Count == 0)
309          NotifyPropertyChanged("CanUndo");
310        if (redostack.Count == 1)
311          NotifyPropertyChanged("CanRedo");
312      }
313    }
314   
315    internal void RunUndo(IUndoableOperation op)
316    {
317      IUndoableOperationWithContext opWithCtx = op as IUndoableOperationWithContext;
318      if (opWithCtx != null)
319        opWithCtx.Undo(this);
320      else
321        op.Undo();
322    }
323   
324    /// <summary>
325    /// Call this method to redo the last undone operation
326    /// </summary>
327    public void Redo()
328    {
329      ThrowIfUndoGroupOpen();
330      if (redostack.Count > 0) {
331        lastGroupDescriptor = null; allowContinue = false;
332        IUndoableOperation uedit = redostack.PopBack();
333        undostack.PushBack(uedit);
334        state = StatePlayback;
335        try {
336          RunRedo(uedit);
337        } finally {
338          state = StateListen;
339          FileModified(1);
340          CallEndUpdateOnAffectedDocuments();
341        }
342        RecalcIsOriginalFile();
343        if (redostack.Count == 0)
344          NotifyPropertyChanged("CanRedo");
345        if (undostack.Count == 1)
346          NotifyPropertyChanged("CanUndo");
347      }
348    }
349   
350    internal void RunRedo(IUndoableOperation op)
351    {
352      IUndoableOperationWithContext opWithCtx = op as IUndoableOperationWithContext;
353      if (opWithCtx != null)
354        opWithCtx.Redo(this);
355      else
356        op.Redo();
357    }
358   
359    /// <summary>
360    /// Call this method to push an UndoableOperation on the undostack.
361    /// The redostack will be cleared if you use this method.
362    /// </summary>
363    public void Push(IUndoableOperation operation)
364    {
365      Push(operation, false);
366    }
367   
368    /// <summary>
369    /// Call this method to push an UndoableOperation on the undostack.
370    /// However, the operation will be only stored if the undo group contains a
371    /// non-optional operation.
372    /// Use this method to store the caret position/selection on the undo stack to
373    /// prevent having only actions that affect only the caret and not the document.
374    /// </summary>
375    public void PushOptional(IUndoableOperation operation)
376    {
377      if (undoGroupDepth == 0)
378        throw new InvalidOperationException("Cannot use PushOptional outside of undo group");
379      Push(operation, true);
380    }
381   
382    void Push(IUndoableOperation operation, bool isOptional)
383    {
384      if (operation == null) {
385        throw new ArgumentNullException("operation");
386      }
387     
388      if (state == StateListen && sizeLimit > 0) {
389        bool wasEmpty = undostack.Count == 0;
390       
391        bool needsUndoGroup = undoGroupDepth == 0;
392        if (needsUndoGroup) StartUndoGroup();
393        undostack.PushBack(operation);
394        actionCountInUndoGroup++;
395        if (isOptional)
396          optionalActionCount++;
397        else
398          FileModified(1);
399        if (needsUndoGroup) EndUndoGroup();
400        if (wasEmpty)
401          NotifyPropertyChanged("CanUndo");
402        ClearRedoStack();
403      }
404    }
405   
406    /// <summary>
407    /// Call this method, if you want to clear the redo stack
408    /// </summary>
409    public void ClearRedoStack()
410    {
411      if (redostack.Count != 0) {
412        redostack.Clear();
413        NotifyPropertyChanged("CanRedo");
414        // if the "original file" marker is on the redo stack: remove it
415        if (elementsOnUndoUntilOriginalFile < 0)
416          elementsOnUndoUntilOriginalFile = int.MinValue;
417      }
418    }
419   
420    /// <summary>
421    /// Clears both the undo and redo stack.
422    /// </summary>
423    public void ClearAll()
424    {
425      ThrowIfUndoGroupOpen();
426      actionCountInUndoGroup = 0;
427      optionalActionCount = 0;
428      if (undostack.Count != 0) {
429        lastGroupDescriptor = null;
430        allowContinue = false;
431        undostack.Clear();
432        NotifyPropertyChanged("CanUndo");
433      }
434      ClearRedoStack();
435    }
436   
437    internal void Push(TextDocument document, DocumentChangeEventArgs e)
438    {
439      if (state == StatePlayback)
440        throw new InvalidOperationException("Document changes during undo/redo operations are not allowed.");
441      if (state == StatePlaybackModifyDocument)
442        state = StatePlayback; // allow only 1 change per expected modification
443      else
444        Push(new DocumentChangeOperation(document, e));
445    }
446   
447    /// <summary>
448    /// Is raised when a property (CanUndo, CanRedo) changed.
449    /// </summary>
450    public event PropertyChangedEventHandler PropertyChanged;
451   
452    void NotifyPropertyChanged(string propertyName)
453    {
454      if (PropertyChanged != null)
455        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
456    }
457  }
458}
Note: See TracBrowser for help on using the repository browser.