// // // // // $Revision$ // using System; using System.Collections.Generic; using System.ComponentModel; namespace ICSharpCode.AvalonEdit.Document { /// /// Undo stack implementation. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")] public sealed class UndoStack : INotifyPropertyChanged { Stack undostack = new Stack(); Stack redostack = new Stack(); bool acceptChanges = true; /// /// Gets if the undo stack currently accepts changes. /// Is false while an undo action is running. /// public bool AcceptChanges { get { return acceptChanges; } } /// /// Gets if there are actions on the undo stack. /// Use the PropertyChanged event to listen to changes of this property. /// public bool CanUndo { get { return undostack.Count > 0; } } /// /// Gets if there are actions on the redo stack. /// Use the PropertyChanged event to listen to changes of this property. /// public bool CanRedo { get { return redostack.Count > 0; } } int undoGroupDepth; int actionCountInUndoGroup; int optionalActionCount; object lastGroupDescriptor; /// /// If an undo group is open, gets the group descriptor of the current top-level /// undo group. /// If no undo group is open, gets the group descriptor from the previous undo group. /// /// The group descriptor can be used to join adjacent undo groups: /// use a group descriptor to mark your changes, and on the second action, /// compare LastGroupDescriptor and use if you /// want to join the undo groups. public object LastGroupDescriptor { get { return lastGroupDescriptor; } } /// /// Starts grouping changes. /// Maintains a counter so that nested calls are possible. /// public void StartUndoGroup() { StartUndoGroup(null); } /// /// Starts grouping changes. /// Maintains a counter so that nested calls are possible. /// /// An object that is stored with the undo group. /// If this is not a top-level undo group, the parameter is ignored. public void StartUndoGroup(object groupDescriptor) { if (undoGroupDepth == 0) { actionCountInUndoGroup = 0; optionalActionCount = 0; lastGroupDescriptor = groupDescriptor; } undoGroupDepth++; //Util.LoggingService.Debug("Open undo group (new depth=" + undoGroupDepth + ")"); } /// /// Starts grouping changes, continuing with the previously closed undo group. /// Maintains a counter so that nested calls are possible. /// If the call to StartContinuedUndoGroup is a nested call, it behaves exactly /// as , only top-level calls can continue existing undo groups. /// /// An object that is stored with the undo group. /// If this is not a top-level undo group, the parameter is ignored. public void StartContinuedUndoGroup(object groupDescriptor) { if (undoGroupDepth == 0) { actionCountInUndoGroup = undostack.Count > 0 ? 1 : 0; optionalActionCount = 0; lastGroupDescriptor = groupDescriptor; } undoGroupDepth++; //Util.LoggingService.Debug("Continue undo group (new depth=" + undoGroupDepth + ")"); } /// /// Stops grouping changes. /// public void EndUndoGroup() { if (undoGroupDepth == 0) throw new InvalidOperationException("There are no open undo groups"); undoGroupDepth--; //Util.LoggingService.Debug("Close undo group (new depth=" + undoGroupDepth + ")"); if (undoGroupDepth == 0) { if (actionCountInUndoGroup == optionalActionCount) { // only optional actions: don't store them for (int i = 0; i < optionalActionCount; i++) { undostack.Pop(); } } else if (actionCountInUndoGroup > 1) { undostack.Push(new UndoOperationGroup(undostack, actionCountInUndoGroup)); } } } /// /// Throws an InvalidOperationException if an undo group is current open. /// void VerifyNoUndoGroupOpen() { if (undoGroupDepth != 0) { undoGroupDepth = 0; throw new InvalidOperationException("No undo group should be open at this point"); } } /// /// Call this method to undo the last operation on the stack /// public void Undo() { VerifyNoUndoGroupOpen(); if (undostack.Count > 0) { lastGroupDescriptor = null; acceptChanges = false; IUndoableOperation uedit = undostack.Pop(); redostack.Push(uedit); uedit.Undo(); acceptChanges = true; if (undostack.Count == 0) NotifyPropertyChanged("CanUndo"); if (redostack.Count == 1) NotifyPropertyChanged("CanRedo"); } } /// /// Call this method to redo the last undone operation /// public void Redo() { VerifyNoUndoGroupOpen(); if (redostack.Count > 0) { lastGroupDescriptor = null; acceptChanges = false; IUndoableOperation uedit = redostack.Pop(); undostack.Push(uedit); uedit.Redo(); acceptChanges = true; if (redostack.Count == 0) NotifyPropertyChanged("CanRedo"); if (undostack.Count == 1) NotifyPropertyChanged("CanUndo"); } } /// /// Call this method to push an UndoableOperation on the undostack. /// The redostack will be cleared if you use this method. /// public void Push(IUndoableOperation operation) { Push(operation, false); } /// /// Call this method to push an UndoableOperation on the undostack. /// However, the operation will be only stored if the undo group contains a /// non-optional operation. /// Use this method to store the caret position/selection on the undo stack to /// prevent having only actions that affect only the caret and not the document. /// public void PushOptional(IUndoableOperation operation) { if (undoGroupDepth == 0) throw new InvalidOperationException("Cannot use PushOptional outside of undo group"); Push(operation, true); } void Push(IUndoableOperation operation, bool isOptional) { if (operation == null) { throw new ArgumentNullException("operation"); } if (acceptChanges) { bool wasEmpty = undostack.Count == 0; StartUndoGroup(); undostack.Push(operation); actionCountInUndoGroup++; if (isOptional) optionalActionCount++; EndUndoGroup(); if (wasEmpty) NotifyPropertyChanged("CanUndo"); ClearRedoStack(); } } /// /// Call this method, if you want to clear the redo stack /// public void ClearRedoStack() { if (redostack.Count != 0) { redostack.Clear(); NotifyPropertyChanged("CanRedo"); } } /// /// Clears both the undo and redo stack. /// public void ClearAll() { VerifyNoUndoGroupOpen(); if (undostack.Count != 0) { lastGroupDescriptor = null; undostack.Clear(); NotifyPropertyChanged("CanUndo"); } ClearRedoStack(); actionCountInUndoGroup = 0; optionalActionCount = 0; } internal void AttachToDocument(TextDocument document) { document.UpdateStarted += document_UpdateStarted; document.UpdateFinished += document_UpdateFinished; document.Changing += document_Changing; } void document_UpdateStarted(object sender, EventArgs e) { StartUndoGroup(); } void document_UpdateFinished(object sender, EventArgs e) { EndUndoGroup(); } void document_Changing(object sender, DocumentChangeEventArgs e) { TextDocument document = (TextDocument)sender; Push(new DocumentChangeOperation( document, e.Offset, document.GetText(e.Offset, e.RemovalLength), e.InsertedText, e.OffsetChangeMapOrNull)); } /// /// Is raised when a property (CanUndo, CanRedo) changed. /// public event PropertyChangedEventHandler PropertyChanged; void NotifyPropertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } }