You're breathtaking!
This commit is contained in:
257
Source/Editor/History/HistoryStack.cs
Normal file
257
Source/Editor/History/HistoryStack.cs
Normal file
@@ -0,0 +1,257 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using FlaxEngine.Collections;
|
||||
|
||||
namespace FlaxEditor.History
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller for handling stack manipulations in history and reverse buffers.
|
||||
/// </summary>
|
||||
public sealed class HistoryStack
|
||||
{
|
||||
private int _historyActionsLimit;
|
||||
|
||||
private readonly CircularBuffer<IHistoryAction> _historyActions;
|
||||
private readonly CircularBuffer<IHistoryAction> _reverseActions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HistoryStack"/> class.
|
||||
/// </summary>
|
||||
/// <param name="historyActionsLimit">The history actions limit.</param>
|
||||
public HistoryStack(int historyActionsLimit = 1000)
|
||||
{
|
||||
if (historyActionsLimit < 1)
|
||||
throw new ArgumentOutOfRangeException();
|
||||
|
||||
_historyActionsLimit = historyActionsLimit;
|
||||
_historyActions = new CircularBuffer<IHistoryAction>(_historyActionsLimit);
|
||||
_reverseActions = new CircularBuffer<IHistoryAction>(_historyActionsLimit);
|
||||
|
||||
_historyActions.OnItemOverflown += OnItemOverflown;
|
||||
_reverseActions.OnItemOverflown += OnItemOverflown;
|
||||
}
|
||||
|
||||
private void OnItemOverflown(object sender, CircularBuffer<IHistoryAction>.ItemOverflownEventArgs e)
|
||||
{
|
||||
// Dispose item to prevent leaks
|
||||
e.Item.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the history actions limit.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The history actions limit.
|
||||
/// </value>
|
||||
public int HistoryActionsLimit
|
||||
{
|
||||
get => _historyActionsLimit;
|
||||
set
|
||||
{
|
||||
if (value < 1)
|
||||
throw new ArgumentOutOfRangeException();
|
||||
if (_historyActionsLimit == value)
|
||||
return;
|
||||
|
||||
// Cache actions
|
||||
var history = _historyActions.ToArray();
|
||||
var reverse = _reverseActions.ToArray();
|
||||
|
||||
// Resize buffers
|
||||
_historyActionsLimit = value;
|
||||
_historyActions.Clear(_historyActionsLimit);
|
||||
_reverseActions.Clear(_historyActionsLimit);
|
||||
|
||||
// Add actions back
|
||||
for (int i = 0; i < _historyActionsLimit && i < history.Length; i++)
|
||||
{
|
||||
_historyActions.PushBack(history[i]);
|
||||
}
|
||||
for (int i = 0; i < _historyActionsLimit && i < reverse.Length; i++)
|
||||
{
|
||||
_reverseActions.PushBack(reverse[i]);
|
||||
}
|
||||
|
||||
// Cleanup reaming actions
|
||||
for (int i = _historyActionsLimit; i < history.Length; i++)
|
||||
{
|
||||
history[i].Dispose();
|
||||
}
|
||||
for (int i = _historyActionsLimit; i < reverse.Length; i++)
|
||||
{
|
||||
reverse[i].Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the history count.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The history count.
|
||||
/// </value>
|
||||
public int HistoryCount => _historyActions.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reverse count.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The reverse count.
|
||||
/// </value>
|
||||
public int ReverseCount => _reverseActions.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Adds new history element at top of history stack, and drops reverse stack
|
||||
/// </summary>
|
||||
/// <param name="item">Item to add</param>
|
||||
public void Push(IHistoryAction item)
|
||||
{
|
||||
_historyActions.PushFront(item);
|
||||
ClearReverse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets top-most item in history stack
|
||||
/// </summary>
|
||||
/// <returns>Found element or null</returns>
|
||||
public IHistoryAction PeekHistory()
|
||||
{
|
||||
return _historyActions.Count == 0 ? null : _historyActions[_historyActions.Count - 1];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets top-most item in reverse stack
|
||||
/// </summary>
|
||||
/// <returns>Found element or null</returns>
|
||||
public IHistoryAction PeekReverse()
|
||||
{
|
||||
return _reverseActions.Count == 0 ? null : _reverseActions[_reverseActions.Count - 1];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets top-most item in history stack, and removes it from history stack. Adds forgot element in reverse stack.
|
||||
/// </summary>
|
||||
/// <returns>Found element or null</returns>
|
||||
public IHistoryAction PopHistory()
|
||||
{
|
||||
var item = PeekHistory();
|
||||
if (item == null)
|
||||
return null;
|
||||
_historyActions.PopFront();
|
||||
_reverseActions.PushFront(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets top-most item in reverse stack, and removes it from reverse stack. Adds forgot element in history stack.
|
||||
/// </summary>
|
||||
/// <returns>Found element or null</returns>
|
||||
public IHistoryAction PopReverse()
|
||||
{
|
||||
var item = PeekReverse();
|
||||
if (item == null)
|
||||
return null;
|
||||
_reverseActions.PopFront();
|
||||
_historyActions.PushFront(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets element at given index from top of history stack, and adds all skipped elements to reverse stack
|
||||
/// </summary>
|
||||
/// <remarks>If skipElements is bigger, then amount of elements in history, returns null, clears history and pushes all to reverse stack</remarks>
|
||||
/// <param name="skipElements">Amount of elements to skip from history stack</param>
|
||||
/// <returns>>Found element or null</returns>
|
||||
public IHistoryAction TravelBack(int skipElements)
|
||||
{
|
||||
if (skipElements <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(skipElements), "argument cannot be smaller or equal to 0");
|
||||
}
|
||||
|
||||
if (_historyActions.Count - skipElements <= 0)
|
||||
{
|
||||
foreach (var historyAction in _historyActions)
|
||||
{
|
||||
_reverseActions.PushFront(historyAction);
|
||||
}
|
||||
var result = _historyActions.Back();
|
||||
ClearHistory();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Iterate all but one elements to skip. Last element is handled exclusively
|
||||
for (int i = 0; i < skipElements - 1; i++)
|
||||
{
|
||||
PopHistory();
|
||||
}
|
||||
|
||||
return PopHistory();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets element at given index from top of reverse stack, and adds all skipped elements to history stack
|
||||
/// </summary>
|
||||
/// <remarks>If skipElements is bigger, then amount of elements in reverse, returns null, clears reverse and pushes all to history stack</remarks>
|
||||
/// <param name="skipElements">Amount of elements to skip from reverse stack</param>
|
||||
/// <returns>>Found element or null</returns>
|
||||
public IHistoryAction TravelReverse(int skipElements)
|
||||
{
|
||||
if (skipElements <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(skipElements), "skipElement cannot be smaller or equal to 0");
|
||||
}
|
||||
|
||||
if (_reverseActions.Count - skipElements <= 0)
|
||||
{
|
||||
foreach (var reverseAction in _reverseActions.Reverse())
|
||||
{
|
||||
_historyActions.PushFront(reverseAction);
|
||||
}
|
||||
ClearReverse();
|
||||
return PeekHistory();
|
||||
}
|
||||
|
||||
// iterate all but one elements to skip. Last element is handled exclusively
|
||||
for (int i = 0; i < skipElements - 1; i++)
|
||||
{
|
||||
PopReverse();
|
||||
}
|
||||
|
||||
return PopReverse();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears whole history (back and front).
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
ClearHistory();
|
||||
ClearReverse();
|
||||
}
|
||||
|
||||
private void ClearHistory()
|
||||
{
|
||||
if (_historyActions.Count > 0)
|
||||
{
|
||||
var actions = _historyActions.ToArray();
|
||||
for (int i = 0; i < actions.Length; i++)
|
||||
actions[i].Dispose();
|
||||
_historyActions.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearReverse()
|
||||
{
|
||||
if (_reverseActions.Count > 0)
|
||||
{
|
||||
var actions = _reverseActions.ToArray();
|
||||
for (int i = 0; i < actions.Length; i++)
|
||||
actions[i].Dispose();
|
||||
_reverseActions.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Source/Editor/History/IHistoryAction.cs
Normal file
20
Source/Editor/History/IHistoryAction.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
namespace FlaxEditor.History
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for <see cref="HistoryStack"/> actions.
|
||||
/// </summary>
|
||||
public interface IHistoryAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Name or key of performed action
|
||||
/// </summary>
|
||||
string ActionString { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
void Dispose();
|
||||
}
|
||||
}
|
||||
205
Source/Editor/History/UndoActionObject.cs
Normal file
205
Source/Editor/History/UndoActionObject.cs
Normal file
@@ -0,0 +1,205 @@
|
||||
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FlaxEditor.Utilities;
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.History
|
||||
{
|
||||
/// <summary>
|
||||
/// Undo action object.
|
||||
/// </summary>
|
||||
/// <seealso cref="IUndoAction" />
|
||||
[Serializable]
|
||||
public class UndoActionObject : UndoActionBase<UndoActionObject.DataStorage>
|
||||
{
|
||||
/// <summary>
|
||||
/// The data value storage to solve issue for flax objects and editor scene tree nodes which are serialized by ref id.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public struct DataValue
|
||||
{
|
||||
/// <summary>
|
||||
/// The generic value (anything).
|
||||
/// </summary>
|
||||
public object Generic;
|
||||
|
||||
/// <summary>
|
||||
/// The flax object.
|
||||
/// </summary>
|
||||
public FlaxEngine.Object FlaxObject;
|
||||
|
||||
/// <summary>
|
||||
/// The editor node object.
|
||||
/// </summary>
|
||||
public SceneGraph.SceneGraphNode EditorNode;
|
||||
|
||||
/// <summary>
|
||||
/// The id.
|
||||
/// </summary>
|
||||
public Guid? Id;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the proper value.
|
||||
/// </summary>
|
||||
[NoSerialize]
|
||||
public object Value => EditorNode ?? FlaxObject ?? Generic ?? Id;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataValue"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public DataValue(object value)
|
||||
{
|
||||
Generic = null;
|
||||
FlaxObject = null;
|
||||
EditorNode = null;
|
||||
Id = null;
|
||||
|
||||
if (value is SceneGraph.SceneGraphNode editorNode)
|
||||
{
|
||||
EditorNode = editorNode;
|
||||
}
|
||||
else if (value is FlaxEngine.Object flaxObject)
|
||||
{
|
||||
FlaxObject = flaxObject;
|
||||
}
|
||||
else if (value is Guid guid)
|
||||
{
|
||||
Id = guid;
|
||||
}
|
||||
else
|
||||
{
|
||||
Generic = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The data.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public struct DataStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// The values 1.
|
||||
/// </summary>
|
||||
public DataValue[] Values1;
|
||||
|
||||
/// <summary>
|
||||
/// The values 2.
|
||||
/// </summary>
|
||||
public DataValue[] Values2;
|
||||
|
||||
/// <summary>
|
||||
/// The instance.
|
||||
/// </summary>
|
||||
public DataValue Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepared undo data container object.
|
||||
/// </summary>
|
||||
public struct DataPrepared
|
||||
{
|
||||
/// <summary>
|
||||
/// The difference data.
|
||||
/// </summary>
|
||||
public MemberComparison[] Diff;
|
||||
|
||||
/// <summary>
|
||||
/// The target object instance.
|
||||
/// </summary>
|
||||
public object TargetInstance;
|
||||
}
|
||||
|
||||
// For objects that cannot be referenced in undo action like: FlaxEngine.Object or SceneGraphNode we store them in DataStorage,
|
||||
// otherwise here:
|
||||
private readonly object TargetInstance;
|
||||
|
||||
private readonly MemberInfoPath[] Members;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target.
|
||||
/// </summary>
|
||||
public object Target => TargetInstance;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UndoActionObject"/> class.
|
||||
/// </summary>
|
||||
/// <param name="diff">The difference.</param>
|
||||
/// <param name="actionString">The action string.</param>
|
||||
/// <param name="targetInstance">The target instance.</param>
|
||||
public UndoActionObject(List<MemberComparison> diff, string actionString, object targetInstance)
|
||||
{
|
||||
bool useDataStorageForInstance = targetInstance is FlaxEngine.Object || targetInstance is SceneGraph.SceneGraphNode;
|
||||
|
||||
ActionString = actionString;
|
||||
TargetInstance = useDataStorageForInstance ? null : targetInstance;
|
||||
|
||||
int count = diff.Count;
|
||||
var values1 = new DataValue[count];
|
||||
var values2 = new DataValue[count];
|
||||
Members = new MemberInfoPath[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
values1[i] = new DataValue(diff[i].Value1);
|
||||
values2[i] = new DataValue(diff[i].Value2);
|
||||
Members[i] = diff[i].MemberPath;
|
||||
}
|
||||
|
||||
Data = new DataStorage
|
||||
{
|
||||
Values1 = values1,
|
||||
Values2 = values2,
|
||||
Instance = new DataValue(useDataStorageForInstance ? targetInstance : null)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares the data for the undo.
|
||||
/// </summary>
|
||||
/// <returns>The prepared undo action data.</returns>
|
||||
public DataPrepared PrepareData()
|
||||
{
|
||||
var data = Data;
|
||||
var count = data.Values1.Length;
|
||||
var diff = new MemberComparison[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
diff[i] = new MemberComparison(Members[i], data.Values1[i].Value, data.Values2[i].Value);
|
||||
}
|
||||
return new DataPrepared
|
||||
{
|
||||
Diff = diff,
|
||||
TargetInstance = data.Instance.Value ?? TargetInstance,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ActionString { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Do()
|
||||
{
|
||||
var data = PrepareData();
|
||||
for (var i = 0; i < data.Diff.Length; i++)
|
||||
{
|
||||
var diff = data.Diff[i];
|
||||
diff.SetMemberValue(data.TargetInstance, diff.Value2);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Undo()
|
||||
{
|
||||
var data = PrepareData();
|
||||
for (var i = data.Diff.Length - 1; i >= 0; i--)
|
||||
{
|
||||
var diff = data.Diff[i];
|
||||
diff.SetMemberValue(data.TargetInstance, diff.Value1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user