// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using FlaxEditor.History; using FlaxEditor.Utilities; using FlaxEngine.Collections; namespace FlaxEditor { /// /// The undo/redo actions recording object. /// public class Undo : IDisposable { /// /// Undo system event. /// /// The action. public delegate void UndoEventDelegate(IUndoAction action); internal interface IUndoInternal { /// /// Creates the undo action object on recording end. /// /// The snapshot object. /// The undo action. May be null if no changes found. IUndoAction End(object snapshotInstance); } /// /// Stack of undo actions for future disposal. /// private readonly OrderedDictionary _snapshots = new OrderedDictionary(); /// /// Gets the undo operations stack. /// /// /// The undo operations stack. /// public HistoryStack UndoOperationsStack { get; } /// /// Occurs when undo operation is done. /// public event UndoEventDelegate UndoDone; /// /// Occurs when redo operation is done. /// public event UndoEventDelegate RedoDone; /// /// Occurs when action is done and appended to the . /// public event UndoEventDelegate ActionDone; /// /// Gets or sets a value indicating whether this is enabled. /// public virtual bool Enabled { get; set; } = true; /// /// Gets a value indicating whether can do undo on last performed action. /// public bool CanUndo => UndoOperationsStack.HistoryCount > 0; /// /// Gets a value indicating whether can do redo on last undone action. /// public bool CanRedo => UndoOperationsStack.ReverseCount > 0; /// /// Gets the first name of the undo action. /// public string FirstUndoName => UndoOperationsStack.PeekHistory().ActionString; /// /// Gets the first name of the redo action. /// public string FirstRedoName => UndoOperationsStack.PeekReverse().ActionString; /// /// Gets or sets the capacity of the undo history buffers. /// public int Capacity { get => UndoOperationsStack.HistoryActionsLimit; set => UndoOperationsStack.HistoryActionsLimit = value; } /// /// Internal class for keeping reference of undo action. /// internal class UndoInternal : IUndoInternal { public string ActionString; public object SnapshotInstance; public ObjectSnapshot Snapshot; public UndoInternal(object snapshotInstance, string actionString) { ActionString = actionString; SnapshotInstance = snapshotInstance; Snapshot = ObjectSnapshot.CaptureSnapshot(snapshotInstance); } /// public IUndoAction End(object snapshotInstance) { var diff = Snapshot.Compare(snapshotInstance); if (diff.Count == 0) return null; return new UndoActionObject(diff, ActionString, SnapshotInstance); } } /// /// Initializes a new instance of the class. /// /// The history actions limit. public Undo(int historyActionsLimit = 1000) { UndoOperationsStack = new HistoryStack(historyActionsLimit); } /// /// Begins recording for undo action. /// /// Instance of an object to record. /// Name of action to be displayed in undo stack. public void RecordBegin(object snapshotInstance, string actionString) { if (!Enabled) return; _snapshots.Add(snapshotInstance, new UndoInternal(snapshotInstance, actionString)); } /// /// Ends recording for undo action. /// /// Instance of an object to finish recording, if null take last provided. /// Custom action to append to the undo block action before recorded modifications apply. /// Custom action to append to the undo block action after recorded modifications apply. public void RecordEnd(object snapshotInstance = null, IUndoAction customActionBefore = null, IUndoAction customActionAfter = null) { if (!Enabled) return; if (snapshotInstance == null) { snapshotInstance = _snapshots.Last().Key; } var action = _snapshots[snapshotInstance].End(snapshotInstance); _snapshots.Remove(snapshotInstance); // It may be null if no changes has been found during recording if (action != null) { // Batch with a custom action if provided if (customActionBefore != null && customActionAfter != null) { action = new MultiUndoAction(new[] { customActionBefore, action, customActionAfter }); } else if (customActionBefore != null) { action = new MultiUndoAction(new[] { customActionBefore, action }); } else if (customActionAfter != null) { action = new MultiUndoAction(new[] { action, customActionAfter }); } UndoOperationsStack.Push(action); OnAction(action); } } /// /// Internal class for keeping reference of undo action that modifies collection of objects. /// internal class UndoMultiInternal : IUndoInternal { public string ActionString; public object[] SnapshotInstances; public ObjectSnapshot[] Snapshot; public UndoMultiInternal(object[] snapshotInstances, string actionString) { ActionString = actionString; SnapshotInstances = snapshotInstances; Snapshot = new ObjectSnapshot[snapshotInstances.Length]; for (var i = 0; i < snapshotInstances.Length; i++) { Snapshot[i] = ObjectSnapshot.CaptureSnapshot(snapshotInstances[i]); } } /// public IUndoAction End(object snapshotInstance) { var snapshotInstances = (object[])snapshotInstance; if (snapshotInstances == null || snapshotInstances.Length != SnapshotInstances.Length) throw new ArgumentException("Invalid multi undo action objects."); List actions = null; for (int i = 0; i < snapshotInstances.Length; i++) { var diff = Snapshot[i].Compare(snapshotInstances[i]); if (diff.Count == 0) continue; if (actions == null) actions = new List(); actions.Add(new UndoActionObject(diff, ActionString, SnapshotInstances[i])); } if (actions == null) return null; if (actions.Count == 1) return actions[0]; return new MultiUndoAction(actions); } } /// /// Begins recording for undo action. /// /// Instances of objects to record. /// Name of action to be displayed in undo stack. public void RecordMultiBegin(object[] snapshotInstances, string actionString) { if (!Enabled) return; _snapshots.Add(snapshotInstances, new UndoMultiInternal(snapshotInstances, actionString)); } /// /// Ends recording for undo action. /// /// Instance of an object to finish recording, if null take last provided. /// Custom action to append to the undo block action before recorded modifications apply. /// Custom action to append to the undo block action after recorded modifications apply. public void RecordMultiEnd(object[] snapshotInstance = null, IUndoAction customActionBefore = null, IUndoAction customActionAfter = null) { if (!Enabled) return; if (snapshotInstance == null) { snapshotInstance = (object[])_snapshots.Last().Key; } var action = _snapshots[snapshotInstance].End(snapshotInstance); _snapshots.Remove(snapshotInstance); // It may be null if no changes has been found during recording if (action != null) { // Batch with a custom action if provided if (customActionBefore != null && customActionAfter != null) { action = new MultiUndoAction(new[] { customActionBefore, action, customActionAfter }); } else if (customActionBefore != null) { action = new MultiUndoAction(new[] { customActionBefore, action }); } else if (customActionAfter != null) { action = new MultiUndoAction(new[] { action, customActionAfter }); } UndoOperationsStack.Push(action); OnAction(action); } } /// /// Creates new undo action for provided instance of object. /// /// Instance of an object to record /// Name of action to be displayed in undo stack. /// Action in after witch recording will be finished. public void RecordAction(object snapshotInstance, string actionString, Action actionsToSave) { RecordBegin(snapshotInstance, actionString); actionsToSave?.Invoke(); RecordEnd(snapshotInstance); } /// /// Creates new undo action for provided instance of object. /// /// Instance of an object to record /// Name of action to be displayed in undo stack. /// Action in after witch recording will be finished. public void RecordAction(T snapshotInstance, string actionString, Action actionsToSave) where T : new() { RecordBegin(snapshotInstance, actionString); actionsToSave?.Invoke(snapshotInstance); RecordEnd(snapshotInstance); } /// /// Creates new undo action for provided instance of object. /// /// Instance of an object to record /// Name of action to be displayed in undo stack. /// Action in after witch recording will be finished. public void RecordAction(object snapshotInstance, string actionString, Action actionsToSave) { RecordBegin(snapshotInstance, actionString); actionsToSave?.Invoke(snapshotInstance); RecordEnd(snapshotInstance); } /// /// Adds the action to the history. /// /// The action. public void AddAction(IUndoAction action) { if (action == null) throw new ArgumentNullException(); if (!Enabled) return; UndoOperationsStack.Push(action); OnAction(action); } /// /// Undo last recorded action /// public void PerformUndo() { if (!Enabled || !CanUndo) return; var action = (IUndoAction)UndoOperationsStack.PopHistory(); action.Undo(); OnUndo(action); } /// /// Redo last undone action /// public void PerformRedo() { if (!Enabled || !CanRedo) return; var action = (IUndoAction)UndoOperationsStack.PopReverse(); action.Do(); OnRedo(action); } /// /// Called when performs action. /// /// The action. protected virtual void OnAction(IUndoAction action) { ActionDone?.Invoke(action); } /// /// Called when performs undo action. /// /// The action. protected virtual void OnUndo(IUndoAction action) { UndoDone?.Invoke(action); } /// /// Called when performs redo action. /// /// The action. protected virtual void OnRedo(IUndoAction action) { RedoDone?.Invoke(action); } /// /// Clears the history. /// public void Clear() { _snapshots.Clear(); UndoOperationsStack.Clear(); } /// public void Dispose() { UndoDone = null; RedoDone = null; ActionDone = null; Clear(); } } }