// 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();
}
}
}