You're breathtaking!

This commit is contained in:
Wojtek Figat
2020-12-07 23:40:54 +01:00
commit 6fb9eee74c
5143 changed files with 1153594 additions and 0 deletions

View File

@@ -0,0 +1,182 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.Scripting;
using FlaxEngine;
using Object = FlaxEngine.Object;
namespace FlaxEditor.Actions
{
/// <summary>
/// Implementation of <see cref="IUndoAction"/> used to add/remove <see cref="Script"/> from the <see cref="Actor"/>.
/// </summary>
/// <seealso cref="IUndoAction" />
[Serializable]
sealed class AddRemoveScript : IUndoAction
{
[Serialize]
private bool _isAdd;
[Serialize]
private Guid _scriptId;
[Serialize]
private Guid _prefabId;
[Serialize]
private Guid _prefabObjectId;
[Serialize]
private string _scriptTypeName;
[Serialize]
private string _scriptData;
[Serialize]
private Guid _parentId;
[Serialize]
private int _orderInParent;
[Serialize]
private bool _enabled;
internal AddRemoveScript(bool isAdd, Script script)
{
_isAdd = isAdd;
_scriptId = script.ID;
_scriptTypeName = script.TypeName;
_prefabId = script.PrefabID;
_prefabObjectId = script.PrefabObjectID;
_scriptData = FlaxEngine.Json.JsonSerializer.Serialize(script);
_parentId = script.Actor.ID;
_orderInParent = script.OrderInParent;
_enabled = script.Enabled;
}
internal AddRemoveScript(bool isAdd, Actor parentActor, ScriptType scriptType)
{
_isAdd = isAdd;
_scriptId = Guid.NewGuid();
_scriptTypeName = scriptType.TypeName;
_scriptData = null;
_parentId = parentActor.ID;
_orderInParent = -1;
_enabled = true;
}
/// <summary>
/// Creates a new added script undo action.
/// </summary>
/// <param name="script">The new script.</param>
/// <returns>The action.</returns>
public static AddRemoveScript Added(Script script)
{
if (script == null)
throw new ArgumentNullException(nameof(script));
return new AddRemoveScript(true, script);
}
/// <summary>
/// Creates a new add script undo action.
/// </summary>
/// <param name="parentActor">The parent actor.</param>
/// <param name="scriptType">The script type.</param>
/// <returns>The action.</returns>
public static AddRemoveScript Add(Actor parentActor, ScriptType scriptType)
{
if (parentActor == null)
throw new ArgumentNullException(nameof(parentActor));
if (!scriptType)
throw new ArgumentNullException(nameof(scriptType));
return new AddRemoveScript(true, parentActor, scriptType);
}
/// <summary>
/// Creates a new remove script undo action.
/// </summary>
/// <param name="script">The script.</param>
/// <returns>The action.</returns>
public static AddRemoveScript Remove(Script script)
{
if (script == null)
throw new ArgumentNullException(nameof(script));
return new AddRemoveScript(false, script);
}
/// <inheritdoc />
public string ActionString => _isAdd ? "Add script" : "Remove script";
/// <inheritdoc />
public void Do()
{
if (_isAdd)
DoAdd();
else
DoRemove();
}
/// <inheritdoc />
public void Undo()
{
if (_isAdd)
DoRemove();
else
DoAdd();
}
/// <inheritdoc />
public void Dispose()
{
_scriptTypeName = null;
_scriptData = null;
}
private void DoRemove()
{
// Remove script (it could be removed by sth else, just check it)
var script = Object.Find<Script>(ref _scriptId);
if (!script)
{
Editor.LogWarning("Missing script.");
return;
}
if (script.Actor)
Editor.Instance.Scene.MarkSceneEdited(script.Scene);
Object.Destroy(ref script);
}
private void DoAdd()
{
// Restore script
var parentActor = Object.Find<Actor>(ref _parentId);
if (parentActor == null)
{
Editor.LogWarning("Missing parent actor.");
return;
}
var type = TypeUtils.GetType(_scriptTypeName);
if (!type)
{
Editor.LogWarning("Cannot find script type " + _scriptTypeName);
return;
}
var script = type.CreateInstance() as Script;
if (script == null)
{
Editor.LogWarning("Cannot create script of type " + _scriptTypeName);
return;
}
Object.Internal_ChangeID(Object.GetUnmanagedPtr(script), ref _scriptId);
if (_scriptData != null)
FlaxEngine.Json.JsonSerializer.Deserialize(script, _scriptData);
script.Enabled = _enabled;
script.Parent = parentActor;
if (_orderInParent != -1)
script.OrderInParent = _orderInParent;
if (_prefabObjectId != Guid.Empty)
SceneObject.Internal_LinkPrefab(Object.GetUnmanagedPtr(script), ref _prefabId, ref _prefabObjectId);
Editor.Instance.Scene.MarkSceneEdited(parentActor.Scene);
}
}
}

View File

@@ -0,0 +1,170 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEngine;
using Object = FlaxEngine.Object;
namespace FlaxEditor.Actions
{
/// <summary>
/// Implementation of <see cref="IUndoAction"/> used to break/restore <see cref="Prefab"/> connection for the collection of <see cref="Actor"/> and <see cref="Script"/> objects.
/// </summary>
/// <remarks>
/// This action assumes that all objects in the given actor hierarchy are using the same prefab asset.
/// </remarks>
/// <seealso cref="IUndoAction" />
[Serializable]
sealed class BreakPrefabLinkAction : IUndoAction
{
[Serialize]
private readonly bool _isBreak;
[Serialize]
private Guid _actorId;
[Serialize]
private Guid _prefabId;
[Serialize]
private Dictionary<Guid, Guid> _prefabObjectIds;
private BreakPrefabLinkAction(bool isBreak, Guid actorId, Guid prefabId)
{
_isBreak = isBreak;
_actorId = actorId;
_prefabId = prefabId;
}
private BreakPrefabLinkAction(bool isBreak, Actor actor)
{
_isBreak = isBreak;
_actorId = actor.ID;
_prefabId = actor.PrefabID;
_prefabObjectIds = new Dictionary<Guid, Guid>(1024);
CollectIds(actor);
}
/// <summary>
/// Creates a new undo action that in state for breaking prefab connection.
/// </summary>
/// <param name="actor">The target actor.</param>
/// <returns>The action.</returns>
public static BreakPrefabLinkAction Break(Actor actor)
{
if (actor == null)
throw new ArgumentNullException(nameof(actor));
return new BreakPrefabLinkAction(true, actor.ID, Guid.Empty);
}
/// <summary>
/// Creates a new undo action that in state for linked prefab connection. Action on perform will undo that.
/// </summary>
/// <param name="actor">The target actor.</param>
/// <returns>The action.</returns>
public static BreakPrefabLinkAction Linked(Actor actor)
{
if (actor == null)
throw new ArgumentNullException(nameof(actor));
if (!actor.HasPrefabLink)
throw new FlaxException("Cannot register missing prefab link.");
return new BreakPrefabLinkAction(false, actor);
}
/// <inheritdoc />
public string ActionString => _isBreak ? "Break prefab link" : "Link prefab";
/// <inheritdoc />
public void Do()
{
if (_isBreak)
DoBreak();
else
DoLink();
}
/// <inheritdoc />
public void Undo()
{
if (_isBreak)
DoLink();
else
DoBreak();
}
/// <inheritdoc />
public void Dispose()
{
_prefabObjectIds.Clear();
}
private void DoLink()
{
if (_prefabObjectIds == null)
throw new FlaxException("Cannot link prefab. Missing objects Ids mapping.");
var actor = Object.Find<Actor>(ref _actorId);
if (actor == null)
throw new FlaxException("Cannot link prefab. Missing actor.");
// Restore cached links
foreach (var e in _prefabObjectIds)
{
var objId = e.Key;
var prefabObjId = e.Value;
var obj = Object.Find<Object>(ref objId);
if (obj is Actor)
{
Actor.Internal_LinkPrefab(Object.GetUnmanagedPtr(obj), ref _prefabId, ref prefabObjId);
}
else if (obj is Script)
{
Script.Internal_LinkPrefab(Object.GetUnmanagedPtr(obj), ref _prefabId, ref prefabObjId);
}
}
Editor.Instance.Scene.MarkSceneEdited(actor.Scene);
Editor.Instance.Windows.PropertiesWin.Presenter.BuildLayout();
}
private void CollectIds(Actor actor)
{
_prefabObjectIds.Add(actor.ID, actor.PrefabObjectID);
for (int i = 0; i < actor.ChildrenCount; i++)
{
CollectIds(actor.GetChild(i));
}
for (int i = 0; i < actor.ScriptsCount; i++)
{
var script = actor.GetScript(i);
_prefabObjectIds.Add(script.ID, script.PrefabObjectID);
}
}
private void DoBreak()
{
var actor = Object.Find<Actor>(ref _actorId);
if (actor == null)
throw new FlaxException("Cannot break prefab link. Missing actor.");
if (!actor.HasPrefabLink)
throw new FlaxException("Cannot break missing prefab link.");
if (_prefabObjectIds == null)
_prefabObjectIds = new Dictionary<Guid, Guid>(1024);
else
_prefabObjectIds.Clear();
CollectIds(actor);
_prefabId = actor.PrefabID;
actor.BreakPrefabLink();
Editor.Instance.Scene.MarkSceneEdited(actor.Scene);
Editor.Instance.Windows.PropertiesWin.Presenter.BuildLayout();
}
}
}

View File

@@ -0,0 +1,99 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.Modules;
using FlaxEngine;
namespace FlaxEditor.Actions
{
/// <summary>
/// Change <see cref="Script"/> order or enable/disable undo action.
/// </summary>
/// <seealso cref="FlaxEditor.IUndoAction" />
/// <seealso cref="FlaxEditor.ISceneEditAction" />
[Serializable]
class ChangeScriptAction : IUndoAction, ISceneEditAction
{
[Serialize]
private Guid _scriptId;
[Serialize]
private bool _enableA;
[Serialize]
private int _orderA;
[Serialize]
private bool _enableB;
[Serialize]
private int _orderB;
private ChangeScriptAction(Script script, bool enable, int order)
{
_scriptId = script.ID;
_enableA = script.Enabled;
_orderA = script.OrderInParent;
_enableB = enable;
_orderB = order;
}
/// <summary>
/// Creates new undo action that changes script order in parent actor scripts collection.
/// </summary>
/// <param name="script">The script to reorder.</param>
/// <param name="newOrder">New index.</param>
/// <returns>The action (not performed yet).</returns>
public static ChangeScriptAction ChangeOrder(Script script, int newOrder)
{
return new ChangeScriptAction(script, script.Enabled, newOrder);
}
/// <summary>
/// Creates new undo action that enables/disables script.
/// </summary>
/// <param name="script">The script to enable or disable.</param>
/// <param name="newEnabled">New enable state.</param>
/// <returns>The action (not performed yet).</returns>
public static ChangeScriptAction ChangeEnabled(Script script, bool newEnabled)
{
return new ChangeScriptAction(script, newEnabled, script.OrderInParent);
}
/// <inheritdoc />
public string ActionString => "Edit script";
/// <inheritdoc />
public void Do()
{
var script = FlaxEngine.Object.Find<Script>(ref _scriptId);
if (script == null)
return;
script.Enabled = _enableB;
script.OrderInParent = _orderB;
}
/// <inheritdoc />
public void Undo()
{
var script = FlaxEngine.Object.Find<Script>(ref _scriptId);
if (script == null)
return;
script.Enabled = _enableA;
script.OrderInParent = _orderA;
}
/// <inheritdoc />
public void Dispose()
{
}
/// <inheritdoc />
public void MarkSceneEdited(SceneModule sceneModule)
{
var script = FlaxEngine.Object.Find<Script>(ref _scriptId);
if (script != null)
sceneModule.MarkSceneEdited(script.Scene);
}
}
}

View File

@@ -0,0 +1,154 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEditor.SceneGraph;
using FlaxEngine;
namespace FlaxEditor.Actions
{
/// <summary>
/// Implementation of <see cref="IUndoAction"/> used to delete a selection of <see cref="ActorNode"/>.
/// </summary>
/// <seealso cref="FlaxEditor.IUndoAction" />
[Serializable]
class DeleteActorsAction : IUndoAction
{
[Serialize]
private byte[] _data;
[Serialize]
private Guid[] _prefabIds;
[Serialize]
private Guid[] _prefabObjectIds;
[Serialize]
private bool _isInverted;
/// <summary>
/// The node parents.
/// </summary>
[Serialize]
protected List<ActorNode> _nodeParents;
/// <summary>
/// Initializes a new instance of the <see cref="DeleteActorsAction"/> class.
/// </summary>
/// <param name="objects">The objects.</param>
/// <param name="isInverted">If set to <c>true</c> action will be inverted - instead of delete it will be create actors.</param>
internal DeleteActorsAction(List<SceneGraphNode> objects, bool isInverted = false)
{
_isInverted = isInverted;
_nodeParents = new List<ActorNode>(objects.Count);
var actorNodes = new List<ActorNode>(objects.Count);
var actors = new List<Actor>(objects.Count);
for (int i = 0; i < objects.Count; i++)
{
if (objects[i] is ActorNode node)
{
actorNodes.Add(node);
actors.Add(node.Actor);
}
}
actorNodes.BuildNodesParents(_nodeParents);
_data = Actor.ToBytes(actors.ToArray());
_prefabIds = new Guid[actors.Count];
_prefabObjectIds = new Guid[actors.Count];
for (int i = 0; i < actors.Count; i++)
{
_prefabIds[i] = actors[i].PrefabID;
_prefabObjectIds[i] = actors[i].PrefabObjectID;
}
}
/// <inheritdoc />
public string ActionString => _isInverted ? "Create actors" : "Delete actors";
/// <inheritdoc />
public void Do()
{
if (_isInverted)
Create();
else
Delete();
}
/// <inheritdoc />
public void Undo()
{
if (_isInverted)
Delete();
else
Create();
}
/// <inheritdoc />
public void Dispose()
{
_data = null;
_prefabIds = null;
_prefabObjectIds = null;
}
/// <summary>
/// Deletes the objects.
/// </summary>
protected virtual void Delete()
{
// Remove objects
for (int i = 0; i < _nodeParents.Count; i++)
{
var node = _nodeParents[i];
Editor.Instance.Scene.MarkSceneEdited(node.ParentScene);
node.Delete();
}
_nodeParents.Clear();
}
/// <summary>
/// Gets the node.
/// </summary>
/// <param name="id">The actor id.</param>
/// <returns>The scene graph node.</returns>
protected virtual SceneGraphNode GetNode(Guid id)
{
return SceneGraphFactory.FindNode(id);
}
/// <summary>
/// Creates the removed objects (from data).
/// </summary>
protected virtual void Create()
{
// Restore objects
var actors = Actor.FromBytes(_data);
if (actors == null)
return;
for (int i = 0; i < actors.Length; i++)
{
Guid prefabId = _prefabIds[i];
if (prefabId != Guid.Empty)
{
Actor.Internal_LinkPrefab(FlaxEngine.Object.GetUnmanagedPtr(actors[i]), ref prefabId, ref _prefabObjectIds[i]);
}
}
var actorNodes = new List<ActorNode>(actors.Length);
for (int i = 0; i < actors.Length; i++)
{
var foundNode = GetNode(actors[i].ID);
if (foundNode is ActorNode node)
{
actorNodes.Add(node);
}
}
actorNodes.BuildNodesParents(_nodeParents);
for (int i = 0; i < _nodeParents.Count; i++)
{
Editor.Instance.Scene.MarkSceneEdited(_nodeParents[i].ParentScene);
}
}
}
}

View File

@@ -0,0 +1,198 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.SceneGraph;
using FlaxEngine;
namespace FlaxEditor.Actions
{
/// <summary>
/// Implementation of <see cref="IUndoAction"/> used to paste a set of <see cref="ActorNode"/>.
/// </summary>
/// <seealso cref="FlaxEditor.IUndoAction" />
[Serializable]
class PasteActorsAction : IUndoAction
{
[Serialize]
private Dictionary<Guid, Guid> _idsMapping;
[Serialize]
private byte[] _data;
[Serialize]
private Guid _pasteParent;
/// <summary>
/// The node parents.
/// </summary>
[Serialize]
protected List<Guid> _nodeParents;
/// <summary>
/// Initializes a new instance of the <see cref="PasteActorsAction"/> class.
/// </summary>
/// <param name="data">The data.</param>
/// <param name="objectIds">The object ids.</param>
/// <param name="pasteParent">The paste parent object id.</param>
/// <param name="name">The action name.</param>
protected PasteActorsAction(byte[] data, Guid[] objectIds, ref Guid pasteParent, string name)
{
ActionString = name;
_pasteParent = pasteParent;
_idsMapping = new Dictionary<Guid, Guid>(objectIds.Length * 4);
for (int i = 0; i < objectIds.Length; i++)
{
_idsMapping[objectIds[i]] = Guid.NewGuid();
}
_nodeParents = new List<Guid>(objectIds.Length);
_data = data;
}
internal static PasteActorsAction Paste(byte[] data, Guid pasteParent)
{
var objectIds = Actor.TryGetSerializedObjectsIds(data);
if (objectIds == null)
return null;
return new PasteActorsAction(data, objectIds, ref pasteParent, "Paste actors");
}
internal static PasteActorsAction Duplicate(byte[] data, Guid pasteParent)
{
var objectIds = Actor.TryGetSerializedObjectsIds(data);
if (objectIds == null)
return null;
return new PasteActorsAction(data, objectIds, ref pasteParent, "Duplicate actors");
}
/// <summary>
/// Links the broken parent reference (missing parent). By default links the actor to the first scene.
/// </summary>
/// <param name="actor">The actor.</param>
protected virtual void LinkBrokenParentReference(Actor actor)
{
// Link to the first scene root
if (Level.ScenesCount == 0)
throw new Exception("Failed to paste actor with a broken reference. No loaded scenes.");
actor.SetParent(Level.GetScene(0), false);
}
/// <inheritdoc />
public string ActionString { get; }
/// <summary>
/// Performs the paste/duplicate action and outputs created objects nodes.
/// </summary>
/// <param name="nodes">The nodes.</param>
/// <param name="nodeParents">The node parents.</param>
public virtual void Do(out List<ActorNode> nodes, out List<ActorNode> nodeParents)
{
// Restore objects
var actors = Actor.FromBytes(_data, _idsMapping);
if (actors == null)
{
nodes = null;
nodeParents = null;
return;
}
nodes = new List<ActorNode>(actors.Length);
for (int i = 0; i < actors.Length; i++)
{
var actor = actors[i];
// Check if has no parent linked (broken reference eg. old parent not existing)
if (actor.Parent == null)
{
LinkBrokenParentReference(actor);
}
var node = GetNode(actor.ID);
if (node is ActorNode actorNode)
{
nodes.Add(actorNode);
}
}
nodeParents = nodes.BuildNodesParents();
// Cache pasted nodes ids (parents only)
_nodeParents.Clear();
_nodeParents.Capacity = Mathf.Max(_nodeParents.Capacity, nodeParents.Count);
for (int i = 0; i < nodeParents.Count; i++)
{
_nodeParents.Add(nodeParents[i].ID);
}
var pasteParentNode = Editor.Instance.Scene.GetActorNode(_pasteParent);
if (pasteParentNode != null)
{
// Move pasted actors to the parent target (if specified and valid)
for (int i = 0; i < nodeParents.Count; i++)
{
nodeParents[i].Actor.SetParent(pasteParentNode.Actor, false);
}
}
for (int i = 0; i < nodeParents.Count; i++)
{
// Fix name collisions (only for parents)
var node = nodeParents[i];
var parent = node.Actor?.Parent;
if (parent != null)
{
string name = node.Name;
Actor[] children = parent.Children;
if (children.Any(x => x.Name == name))
{
// Generate new name
node.Actor.Name = StringUtils.IncrementNameNumber(name, x => children.All(y => y.Name != x));
}
}
Editor.Instance.Scene.MarkSceneEdited(node.ParentScene);
}
}
/// <summary>
/// Gets the node.
/// </summary>
/// <param name="id">The actor id.</param>
/// <returns>The scene graph node.</returns>
protected virtual SceneGraphNode GetNode(Guid id)
{
return SceneGraphFactory.FindNode(id);
}
/// <inheritdoc />
public void Do()
{
Do(out _, out _);
}
/// <inheritdoc />
public virtual void Undo()
{
// Remove objects
for (int i = 0; i < _nodeParents.Count; i++)
{
var node = GetNode(_nodeParents[i]);
Editor.Instance.Scene.MarkSceneEdited(node.ParentScene);
node.Delete();
}
_nodeParents.Clear();
}
/// <inheritdoc />
public virtual void Dispose()
{
_nodeParents?.Clear();
_idsMapping?.Clear();
_data = null;
}
}
}

View File

@@ -0,0 +1,67 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.SceneGraph;
namespace FlaxEditor
{
/// <summary>
/// Objects selection change action.
/// </summary>
/// <seealso cref="IUndoAction" />
[Serializable]
public class SelectionChangeAction : UndoActionBase<SelectionChangeAction.DataStorage>
{
/// <summary>
/// The undo data.
/// </summary>
[Serializable]
public struct DataStorage
{
/// <summary>
/// The 'before' selection.
/// </summary>
public SceneGraphNode[] Before;
/// <summary>
/// The 'after' selection.
/// </summary>
public SceneGraphNode[] After;
}
private Action<SceneGraphNode[]> _callback;
/// <summary>
/// User selection has changed
/// </summary>
/// <param name="before">Previously selected nodes</param>
/// <param name="after">Newly selected nodes</param>
/// <param name="callback">Selection change callback</param>
public SelectionChangeAction(SceneGraphNode[] before, SceneGraphNode[] after, Action<SceneGraphNode[]> callback)
{
Data = new DataStorage
{
Before = before,
After = after,
};
_callback = callback;
}
/// <inheritdoc />
public override string ActionString => "Selection change";
/// <inheritdoc />
public override void Do()
{
var data = Data;
_callback(data.After);
}
/// <inheritdoc />
public override void Undo()
{
var data = Data;
_callback(data.Before);
}
}
}

View File

@@ -0,0 +1,143 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEditor.Modules;
using FlaxEditor.SceneGraph;
using FlaxEngine;
namespace FlaxEditor
{
/// <summary>
/// Implementation of <see cref="IUndoAction"/> used to transform a selection of <see cref="SceneGraphNode"/>.
/// The same logic could be achieved using <see cref="UndoMultiBlock"/> but it would be slower.
/// Since we use this kind of action very ofter (for <see cref="FlaxEditor.Gizmo.TransformGizmo"/> operations) it's better to provide faster implementation.
/// </summary>
/// <seealso cref="FlaxEditor.IUndoAction" />
[Serializable]
public sealed class TransformObjectsAction : UndoActionBase<TransformObjectsAction.DataStorage>, ISceneEditAction
{
/// <summary>
/// The undo data.
/// </summary>
[Serializable]
public struct DataStorage
{
/// <summary>
/// The scene of the selected objects.
/// </summary>
public Scene Scene;
/// <summary>
/// The selection pool.
/// </summary>
public SceneGraphNode[] Selection;
/// <summary>
/// The 'before' state.
/// </summary>
public Transform[] Before;
/// <summary>
/// The 'after' state.
/// </summary>
public Transform[] After;
/// <summary>
/// The cached bounding box that contains all selected items in 'before' state.
/// </summary>
public BoundingBox BeforeBounds;
/// <summary>
/// The cached bounding box that contains all selected items in 'after' state.
/// </summary>
public BoundingBox AfterBounds;
/// <summary>
/// True if navigation system has been modified during editing the selected objects (navmesh auto-rebuild is required).
/// </summary>
public bool NavigationDirty;
}
internal TransformObjectsAction(List<SceneGraphNode> selection, List<Transform> before, ref BoundingBox boundsBefore, bool navigationDirty)
{
var after = Utilities.Utils.GetTransformsAndBounds(selection, out var afterBounds);
// TODO: support moving objects from more than one scene
var scene = selection[0].ParentScene?.Scene;
var data = new DataStorage
{
Scene = scene,
Selection = selection.ToArray(),
After = after,
Before = before.ToArray(),
BeforeBounds = boundsBefore,
AfterBounds = afterBounds,
NavigationDirty = navigationDirty,
};
Data = data;
InvalidateBounds(ref data);
}
/// <inheritdoc />
public override string ActionString => "Transform object(s)";
/// <inheritdoc />
public override void Do()
{
var data = Data;
for (int i = 0; i < data.Selection.Length; i++)
{
data.Selection[i].Transform = data.After[i];
}
InvalidateBounds(ref data);
}
/// <inheritdoc />
public override void Undo()
{
var data = Data;
for (int i = 0; i < data.Selection.Length; i++)
{
data.Selection[i].Transform = data.Before[i];
}
InvalidateBounds(ref data);
}
private void InvalidateBounds(ref DataStorage data)
{
if (!data.NavigationDirty)
return;
var editor = Editor.Instance;
bool isPlayMode = editor.StateMachine.IsPlayMode;
var options = editor.Options.Options;
// Auto NavMesh rebuild
if (!isPlayMode && options.General.AutoRebuildNavMesh && data.Scene != null)
{
// Handle simple case where objects were moved just a little and use one navmesh build request to improve performance
if (data.BeforeBounds.Intersects(ref data.AfterBounds))
{
Navigation.BuildNavMesh(data.Scene, BoundingBox.Merge(data.BeforeBounds, data.AfterBounds), options.General.AutoRebuildNavMeshTimeoutMs);
}
else
{
Navigation.BuildNavMesh(data.Scene, data.BeforeBounds, options.General.AutoRebuildNavMeshTimeoutMs);
Navigation.BuildNavMesh(data.Scene, data.AfterBounds, options.General.AutoRebuildNavMeshTimeoutMs);
}
}
}
void ISceneEditAction.MarkSceneEdited(SceneModule sceneModule)
{
var data = Data;
for (int i = 0; i < data.Selection.Length; i++)
{
sceneModule.MarkSceneEdited(data.Selection[i].ParentScene);
}
}
}
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.History;
using FlaxEditor.Options;
using FlaxEngine;
namespace FlaxEditor
{
/// <summary>
/// Implementation of <see cref="Undo"/> customized for the <see cref="Editor"/>.
/// </summary>
/// <seealso cref="FlaxEditor.Undo" />
public class EditorUndo : Undo
{
private readonly Editor _editor;
internal EditorUndo(Editor editor)
: base(500)
{
_editor = editor;
editor.Options.OptionsChanged += OnOptionsChanged;
}
private void OnOptionsChanged(EditorOptions options)
{
Capacity = options.General.UndoActionsCapacity;
}
/// <inheritdoc />
public override bool Enabled
{
get => _editor.StateMachine.CurrentState.CanUseUndoRedo;
set => throw new AccessViolationException("Cannot change enabled state of the editor main undo.");
}
/// <inheritdoc />
protected override void OnAction(IUndoAction action)
{
CheckSceneEdited(action);
base.OnAction(action);
}
/// <inheritdoc />
protected override void OnUndo(IUndoAction action)
{
CheckSceneEdited(action);
base.OnUndo(action);
}
/// <inheritdoc />
protected override void OnRedo(IUndoAction action)
{
CheckSceneEdited(action);
base.OnRedo(action);
}
/// <summary>
/// Checks if the any scene has been edited after performing the given action.
/// </summary>
/// <param name="action">The action.</param>
private void CheckSceneEdited(IUndoAction action)
{
// Note: this is automatic tracking system to check if undo action modifies scene objects
// Skip if all scenes are already modified
if (Editor.Instance.Scene.IsEverySceneEdited())
return;
// ReSharper disable once SuspiciousTypeConversion.Global
if (action is ISceneEditAction sceneEditAction)
{
sceneEditAction.MarkSceneEdited(Editor.Instance.Scene);
}
else if (action is UndoActionObject undoActionObject)
{
var data = undoActionObject.PrepareData();
if (data.TargetInstance is SceneGraph.SceneGraphNode node)
{
Editor.Instance.Scene.MarkSceneEdited(node.ParentScene);
}
else if (data.TargetInstance is Actor actor)
{
Editor.Instance.Scene.MarkSceneEdited(actor.Scene);
}
else if (data.TargetInstance is Script script && script.Actor != null)
{
Editor.Instance.Scene.MarkSceneEdited(script.Actor.Scene);
}
}
else if (action is MultiUndoAction multiUndoAction)
{
// Process child actions
for (int i = 0; i < multiUndoAction.Actions.Length; i++)
{
CheckSceneEdited(multiUndoAction.Actions[i]);
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.Modules;
namespace FlaxEditor
{
/// <summary>
/// Interface for undo action that can modify scene data (actors, scripts, etc.)
/// </summary>
public interface ISceneEditAction
{
/// <summary>
/// Marks the scenes edited.
/// </summary>
/// <param name="sceneModule">The scene module.</param>
void MarkSceneEdited(SceneModule sceneModule);
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.History;
namespace FlaxEditor
{
/// <summary>
/// Interface for <see cref="Undo"/> actions.
/// </summary>
/// <seealso cref="FlaxEditor.History.IHistoryAction" />
public interface IUndoAction : IHistoryAction
{
/// <summary>
/// Performs this action.
/// </summary>
void Do();
/// <summary>
/// Undoes this action.
/// </summary>
void Undo();
}
}

View File

@@ -0,0 +1,79 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEngine;
namespace FlaxEditor
{
/// <summary>
/// Implementation of <see cref="IUndoAction"/> that contains one or more child actions performed at once. Allows to merge different actions.
/// </summary>
/// <seealso cref="FlaxEditor.IUndoAction" />
[Serializable]
[HideInEditor]
public class MultiUndoAction : IUndoAction
{
/// <summary>
/// The child actions.
/// </summary>
[Serialize]
public readonly IUndoAction[] Actions;
/// <summary>
/// Initializes a new instance of the <see cref="MultiUndoAction"/> class.
/// </summary>
/// <param name="actions">The actions to include within this multi action.</param>
public MultiUndoAction(params IUndoAction[] actions)
{
Actions = actions?.ToArray() ?? throw new ArgumentNullException();
if (Actions.Length == 0)
throw new ArgumentException("Empty actions collection.");
ActionString = Actions[0].ActionString;
}
/// <summary>
/// Initializes a new instance of the <see cref="MultiUndoAction"/> class.
/// </summary>
/// <param name="actions">The actions to include within this multi action.</param>
/// <param name="actionString">The action string.</param>
public MultiUndoAction(IEnumerable<IUndoAction> actions, string actionString = null)
{
Actions = actions?.ToArray() ?? throw new ArgumentNullException();
if (Actions.Length == 0)
throw new ArgumentException("Empty actions collection.");
ActionString = actionString ?? Actions[0].ActionString;
}
/// <inheritdoc />
public string ActionString { get; }
/// <inheritdoc />
public void Do()
{
for (int i = 0; i < Actions.Length; i++)
{
Actions[i].Do();
}
}
/// <inheritdoc />
public void Undo()
{
for (int i = Actions.Length - 1; i >= 0; i--)
{
Actions[i].Undo();
}
}
/// <inheritdoc />
public void Dispose()
{
for (int i = 0; i < Actions.Length; i++)
{
Actions[i].Dispose();
}
}
}
}

422
Source/Editor/Undo/Undo.cs Normal file
View File

@@ -0,0 +1,422 @@
// Copyright (c) 2012-2020 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>
/// <value>
/// <c>true</c> if enabled; otherwise, <c>false</c>.
/// </value>
public virtual bool Enabled { get; set; } = true;
/// <summary>
/// Gets a value indicating whether can do undo on last performed action.
/// </summary>
/// <value>
/// <c>true</c> if can perform undo; otherwise, <c>false</c>.
/// </value>
public bool CanUndo => UndoOperationsStack.HistoryCount > 0;
/// <summary>
/// Gets a value indicating whether can do redo on last undone action.
/// </summary>
/// <value>
/// <c>true</c> if can perform redo; otherwise, <c>false</c>.
/// </value>
public bool CanRedo => UndoOperationsStack.ReverseCount > 0;
/// <summary>
/// Gets the first name of the undo action.
/// </summary>
/// <value>
/// The first name of the undo action.
/// </value>
public string FirstUndoName => UndoOperationsStack.PeekHistory().ActionString;
/// <summary>
/// Gets the first name of the redo action.
/// </summary>
/// <value>
/// The first name of the redo action.
/// </value>
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.");
var actions = new List<UndoActionObject>();
for (int i = 0; i < snapshotInstances.Length; i++)
{
var diff = Snapshot[i].Compare(snapshotInstances[i]);
if (diff.Count == 0)
continue;
actions.Add(new UndoActionObject(diff, ActionString, SnapshotInstances[i]));
}
if (actions.Count == 0)
return null;
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();
}
}
}

View File

@@ -0,0 +1,89 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.SceneGraph;
using FlaxEngine;
using Newtonsoft.Json;
using JsonSerializer = FlaxEngine.Json.JsonSerializer;
namespace FlaxEditor
{
internal class SceneTreeNodeConverter : JsonConverter
{
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
{
Guid id = Guid.Empty;
if (value is SceneGraphNode obj)
id = obj.ID;
writer.WriteValue(id.ToString("N"));
}
/// <inheritdoc />
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.String)
{
var id = Guid.Parse((string)reader.Value);
return SceneGraphFactory.FindNode(id);
}
return null;
}
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return typeof(SceneGraphNode).IsAssignableFrom(objectType);
}
}
/// <summary>
/// Base class for <see cref="IUndoAction"/> implementations. Stores undo data serialized and preserves references to the game objects.
/// </summary>
/// <typeparam name="TData">The type of the data. Must have <see cref="SerializableAttribute"/>.</typeparam>
/// <seealso cref="FlaxEditor.IUndoAction" />
[Serializable]
public abstract class UndoActionBase<TData> : IUndoAction where TData : struct
{
/// <summary>
/// The serialized data (Json text).
/// </summary>
[Serialize]
protected string _data;
/// <summary>
/// Gets or sets the serialized undo data.
/// </summary>
/// <value>
/// The data.
/// </value>
[NoSerialize]
public TData Data
{
get => JsonConvert.DeserializeObject<TData>(_data, JsonSerializer.Settings);
protected set => _data = JsonConvert.SerializeObject(value, Formatting.None, JsonSerializer.Settings);
/*protected set
{
_data = JsonConvert.SerializeObject(value, Formatting.Indented, JsonSerializer.Settings);
Debug.Info(_data);
}*/
}
/// <inheritdoc />
public abstract string ActionString { get; }
/// <inheritdoc />
public abstract void Do();
/// <inheritdoc />
public abstract void Undo();
/// <inheritdoc />
public virtual void Dispose()
{
_data = null;
}
}
}

View File

@@ -0,0 +1,49 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEngine;
namespace FlaxEditor
{
/// <summary>
/// Helper class to record undo operations in a block with <c>using</c> keyword.
/// <example>
/// using(new UndoBlock(undo, obj, "Rename"))
/// {
/// obj.Name = "super name";
/// }
/// </example>
/// </summary>
/// <seealso cref="System.IDisposable" />
[HideInEditor]
public class UndoBlock : IDisposable
{
private readonly Undo _undo;
private readonly object _snapshotUndoInternal;
private readonly IUndoAction _customActionBefore;
private readonly IUndoAction _customActionAfter;
/// <summary>
/// Creates new undo object for recording actions with using pattern.
/// </summary>
/// <param name="undo">The undo/redo object.</param>
/// <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="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 UndoBlock(Undo undo, object snapshotInstance, string actionString, IUndoAction customActionBefore = null, IUndoAction customActionAfter = null)
{
_snapshotUndoInternal = snapshotInstance;
_undo = undo;
_undo.RecordBegin(_snapshotUndoInternal, actionString);
_customActionBefore = customActionBefore;
_customActionAfter = customActionAfter;
}
/// <inheritdoc />
public void Dispose()
{
_undo.RecordEnd(_snapshotUndoInternal, _customActionBefore, _customActionAfter);
}
}
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEngine;
namespace FlaxEditor
{
/// <summary>
/// Helper class to record undo operations in a block with <c>using</c> keyword. Records changes for one or more objects.
/// </summary>
/// <example>
/// using(new UndoMultiBlock(undo, objs, "Rename objects"))
/// {
/// foreach(var e in objs)
/// e.Name = "super name";
/// }
/// </example>
/// <seealso cref="System.IDisposable" />
[HideInEditor]
public class UndoMultiBlock : IDisposable
{
private readonly object[] _snapshotUndoInternal;
private readonly Undo _undo;
private readonly IUndoAction _customActionBefore;
private readonly IUndoAction _customActionAfter;
/// <summary>
/// Creates new undo object for recording actions with using pattern.
/// </summary>
/// <param name="undo">The undo/redo object.</param>
/// <param name="snapshotInstances">Instances of objects to record.</param>
/// <param name="actionString">Name of action to be displayed in undo stack.</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 UndoMultiBlock(Undo undo, IEnumerable<object> snapshotInstances, string actionString, IUndoAction customActionBefore = null, IUndoAction customActionAfter = null)
{
_snapshotUndoInternal = snapshotInstances.ToArray();
_undo = undo;
_undo.RecordMultiBegin(_snapshotUndoInternal, actionString);
_customActionBefore = customActionBefore;
_customActionAfter = customActionAfter;
}
/// <inheritdoc />
public void Dispose()
{
_undo.RecordMultiEnd(_snapshotUndoInternal, _customActionBefore, _customActionAfter);
}
}
}