Files
FlaxEngine/Source/Editor/Undo/Undo.cs
2023-01-10 15:29:37 +01:00

412 lines
15 KiB
C#

// 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
{
/// <summary>
/// The undo/redo actions recording object.
/// </summary>
public class Undo : IDisposable
{
/// <summary>
/// Undo system event.
/// </summary>
/// <param name="action">The action.</param>
public delegate void UndoEventDelegate(IUndoAction action);
internal interface IUndoInternal
{
/// <summary>
/// Creates the undo action object on recording end.
/// </summary>
/// <param name="snapshotInstance">The snapshot object.</param>
/// <returns>The undo action. May be null if no changes found.</returns>
IUndoAction End(object snapshotInstance);
}
/// <summary>
/// Stack of undo actions for future disposal.
/// </summary>
private readonly OrderedDictionary<object, IUndoInternal> _snapshots = new OrderedDictionary<object, IUndoInternal>();
/// <summary>
/// Gets the undo operations stack.
/// </summary>
/// <value>
/// The undo operations stack.
/// </value>
public HistoryStack UndoOperationsStack { get; }
/// <summary>
/// Occurs when undo operation is done.
/// </summary>
public event UndoEventDelegate UndoDone;
/// <summary>
/// Occurs when redo operation is done.
/// </summary>
public event UndoEventDelegate RedoDone;
/// <summary>
/// Occurs when action is done and appended to the <see cref="Undo"/>.
/// </summary>
public event UndoEventDelegate ActionDone;
/// <summary>
/// Gets or sets a value indicating whether this <see cref="Undo"/> is enabled.
/// </summary>
public virtual bool Enabled { get; set; } = true;
/// <summary>
/// Gets a value indicating whether can do undo on last performed action.
/// </summary>
public bool CanUndo => UndoOperationsStack.HistoryCount > 0;
/// <summary>
/// Gets a value indicating whether can do redo on last undone action.
/// </summary>
public bool CanRedo => UndoOperationsStack.ReverseCount > 0;
/// <summary>
/// Gets the first name of the undo action.
/// </summary>
public string FirstUndoName => UndoOperationsStack.PeekHistory().ActionString;
/// <summary>
/// Gets the first name of the redo action.
/// </summary>
public string FirstRedoName => UndoOperationsStack.PeekReverse().ActionString;
/// <summary>
/// Gets or sets the capacity of the undo history buffers.
/// </summary>
public int Capacity
{
get => UndoOperationsStack.HistoryActionsLimit;
set => UndoOperationsStack.HistoryActionsLimit = value;
}
/// <summary>
/// Internal class for keeping reference of undo action.
/// </summary>
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);
}
/// <inheritdoc />
public IUndoAction End(object snapshotInstance)
{
var diff = Snapshot.Compare(snapshotInstance);
if (diff.Count == 0)
return null;
return new UndoActionObject(diff, ActionString, SnapshotInstance);
}
}
/// <summary>
/// Initializes a new instance of the <see cref="Undo"/> class.
/// </summary>
/// <param name="historyActionsLimit">The history actions limit.</param>
public Undo(int historyActionsLimit = 1000)
{
UndoOperationsStack = new HistoryStack(historyActionsLimit);
}
/// <summary>
/// Begins recording for undo action.
/// </summary>
/// <param name="snapshotInstance">Instance of an object to record.</param>
/// <param name="actionString">Name of action to be displayed in undo stack.</param>
public void RecordBegin(object snapshotInstance, string actionString)
{
if (!Enabled)
return;
_snapshots.Add(snapshotInstance, new UndoInternal(snapshotInstance, actionString));
}
/// <summary>
/// Ends recording for undo action.
/// </summary>
/// <param name="snapshotInstance">Instance of an object to finish recording, if null take last provided.</param>
/// <param name="customActionBefore">Custom action to append to the undo block action before recorded modifications apply.</param>
/// <param name="customActionAfter">Custom action to append to the undo block action after recorded modifications apply.</param>
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);
}
}
/// <summary>
/// Internal class for keeping reference of undo action that modifies collection of objects.
/// </summary>
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]);
}
}
/// <inheritdoc />
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<UndoActionObject> 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<UndoActionObject>();
actions.Add(new UndoActionObject(diff, ActionString, SnapshotInstances[i]));
}
if (actions == null)
return null;
if (actions.Count == 1)
return actions[0];
return new MultiUndoAction(actions);
}
}
/// <summary>
/// Begins recording for undo action.
/// </summary>
/// <param name="snapshotInstances">Instances of objects to record.</param>
/// <param name="actionString">Name of action to be displayed in undo stack.</param>
public void RecordMultiBegin(object[] snapshotInstances, string actionString)
{
if (!Enabled)
return;
_snapshots.Add(snapshotInstances, new UndoMultiInternal(snapshotInstances, actionString));
}
/// <summary>
/// Ends recording for undo action.
/// </summary>
/// <param name="snapshotInstance">Instance of an object to finish recording, if null take last provided.</param>
/// <param name="customActionBefore">Custom action to append to the undo block action before recorded modifications apply.</param>
/// <param name="customActionAfter">Custom action to append to the undo block action after recorded modifications apply.</param>
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);
}
}
/// <summary>
/// Creates new undo action for provided instance of object.
/// </summary>
/// <param name="snapshotInstance">Instance of an object to record</param>
/// <param name="actionString">Name of action to be displayed in undo stack.</param>
/// <param name="actionsToSave">Action in after witch recording will be finished.</param>
public void RecordAction(object snapshotInstance, string actionString, Action actionsToSave)
{
RecordBegin(snapshotInstance, actionString);
actionsToSave?.Invoke();
RecordEnd(snapshotInstance);
}
/// <summary>
/// Creates new undo action for provided instance of object.
/// </summary>
/// <param name="snapshotInstance">Instance of an object to record</param>
/// <param name="actionString">Name of action to be displayed in undo stack.</param>
/// <param name="actionsToSave">Action in after witch recording will be finished.</param>
public void RecordAction<T>(T snapshotInstance, string actionString, Action<T> actionsToSave)
where T : new()
{
RecordBegin(snapshotInstance, actionString);
actionsToSave?.Invoke(snapshotInstance);
RecordEnd(snapshotInstance);
}
/// <summary>
/// Creates new undo action for provided instance of object.
/// </summary>
/// <param name="snapshotInstance">Instance of an object to record</param>
/// <param name="actionString">Name of action to be displayed in undo stack.</param>
/// <param name="actionsToSave">Action in after witch recording will be finished.</param>
public void RecordAction(object snapshotInstance, string actionString, Action<object> actionsToSave)
{
RecordBegin(snapshotInstance, actionString);
actionsToSave?.Invoke(snapshotInstance);
RecordEnd(snapshotInstance);
}
/// <summary>
/// Adds the action to the history.
/// </summary>
/// <param name="action">The action.</param>
public void AddAction(IUndoAction action)
{
if (action == null)
throw new ArgumentNullException();
if (!Enabled)
return;
UndoOperationsStack.Push(action);
OnAction(action);
}
/// <summary>
/// Undo last recorded action
/// </summary>
public void PerformUndo()
{
if (!Enabled || !CanUndo)
return;
var action = (IUndoAction)UndoOperationsStack.PopHistory();
action.Undo();
OnUndo(action);
}
/// <summary>
/// Redo last undone action
/// </summary>
public void PerformRedo()
{
if (!Enabled || !CanRedo)
return;
var action = (IUndoAction)UndoOperationsStack.PopReverse();
action.Do();
OnRedo(action);
}
/// <summary>
/// Called when <see cref="Undo"/> performs action.
/// </summary>
/// <param name="action">The action.</param>
protected virtual void OnAction(IUndoAction action)
{
ActionDone?.Invoke(action);
}
/// <summary>
/// Called when <see cref="Undo"/> performs undo action.
/// </summary>
/// <param name="action">The action.</param>
protected virtual void OnUndo(IUndoAction action)
{
UndoDone?.Invoke(action);
}
/// <summary>
/// Called when <see cref="Undo"/> performs redo action.
/// </summary>
/// <param name="action">The action.</param>
protected virtual void OnRedo(IUndoAction action)
{
RedoDone?.Invoke(action);
}
/// <summary>
/// Clears the history.
/// </summary>
public void Clear()
{
_snapshots.Clear();
UndoOperationsStack.Clear();
}
/// <inheritdoc />
public void Dispose()
{
UndoDone = null;
RedoDone = null;
ActionDone = null;
Clear();
}
}
}