//********************************** Banshee Engine (www.banshee3d.com) **************************************************//
//**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************//
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using bs;
namespace bs.Editor
{
/** @addtogroup Utility-Editor
* @{
*/
///
/// Handles undo & redo operations for changes made on game objects. Game objects can be recorded just before a change
/// is made and the system will calculate the difference between that state and the state at the end of current frame.
/// This difference will then be recorded as a undo/redo operation. The undo/redo operation will also take care of
/// selecting the object & field it is acting upon.
///
internal class GameObjectUndo
{
///
/// Contains information about a component that needs its diff recorded.
///
private struct ComponentToRecord
{
private Component obj;
private string path;
private SerializedObject orgState;
///
/// Creates a new object instance, recording the current state of the component.
///
/// Component to record the state of.
///
/// Path to the field which should be focused when performing the undo/redo operation. This should be the path
/// as provided by .
///
internal ComponentToRecord(Component obj, string path)
{
this.obj = obj;
this.path = path;
orgState = SerializedObject.Create(obj);
}
///
/// Generates the diff from the previously recorded state and the current state. If there is a difference
/// an undo command is recorded.
///
internal void RecordCommand()
{
if (obj.IsDestroyed)
return;
SerializedObject newState = SerializedObject.Create(obj);
SerializedDiff oldToNew = SerializedDiff.Create(orgState, newState);
if (oldToNew == null || oldToNew.IsEmpty)
return;
SerializedDiff newToOld = SerializedDiff.Create(newState, orgState);
UndoRedo.Global.RegisterCommand(new RecordComponentUndo(obj, path, oldToNew, newToOld));
}
}
///
/// Contains information about scene objects that needs their diff recorded. Note this will not record the entire
/// scene object, but rather just its name, transform, active state and potentially other similar properties.
/// It's components as well as hierarchy state are ignored.
///
private struct SceneObjectHeaderToRecord
{
private SceneObject[] objs;
private string path;
private SceneObjectState[] orgStates;
///
/// Creates a new object instance, recording the current state of the scene object header.
///
/// Scene object to record the state of.
///
/// Path to the field which should be focused when performing the undo/redo operation.
///
internal SceneObjectHeaderToRecord(SceneObject obj, string path)
{
this.objs = new [] { obj };
this.path = path;
orgStates = new [] { SceneObjectState.Create(obj) };
}
///
/// Creates a new object instance, recording the current state of the scene object header for multiple scene
/// objects.
///
/// Scene objects to record the state of.
///
/// Path to the field which should be focused when performing the undo/redo operation.
///
internal SceneObjectHeaderToRecord(SceneObject[] objs, string path)
{
this.objs = objs;
this.path = path;
orgStates = new SceneObjectState[objs.Length];
for(int i = 0; i < orgStates.Length; i++)
orgStates[i] = SceneObjectState.Create(objs[i]);
}
///
/// Generates the diff from the previously recorded state and the current state. If there is a difference
/// an undo command is recorded.
///
internal void RecordCommand()
{
if (objs == null)
return;
List headers = new List();
for (int i = 0; i < objs.Length; i++)
{
SceneObject obj = objs[i];
SceneObjectState orgState = orgStates[i];
if (obj.IsDestroyed)
continue;
SceneObjectDiff oldToNew = SceneObjectDiff.Create(orgState, SceneObjectState.Create(obj));
if (oldToNew.flags == 0)
continue;
SceneObjectDiff newToOld = SceneObjectDiff.Create(SceneObjectState.Create(obj), orgState);
headers.Add(new SceneObjectHeaderUndo(obj, newToOld, oldToNew));
}
if (headers.Count > 0)
UndoRedo.Global.RegisterCommand(new RecordSceneObjectHeaderUndo(headers, path));
}
}
///
/// Contains information about a scene object that needs its diff recorded. Unlike
/// this will record the entire scene object,
/// including its components and optionally the child hierarchy.
///
private struct SceneObjectToRecord
{
private SceneObject obj;
private string description;
private SerializedSceneObject orgState;
///
/// Creates a new object instance, recording the current state of the scene object.
///
/// Scene object to record the state of.
/// If true, the child scene objects will be recorded as well.
///
/// Optional description that describes the change that is happening.
///
internal SceneObjectToRecord(SceneObject obj, bool hierarchy, string description)
{
this.obj = obj;
this.description = description;
orgState = new SerializedSceneObject(obj, hierarchy);
}
///
/// Generates the diff from the previously recorded state and the current state. If there is a difference
/// an undo command is recorded.
///
internal void RecordCommand()
{
if (obj.IsDestroyed)
return;
var newState = new SerializedSceneObject(obj);
UndoRedo.Global.RegisterCommand(new RecordSceneObjectUndo(obj, orgState, newState, description));
}
}
private static List components = new List();
private static List sceneObjectHeaders = new List();
private static List sceneObjects = new List();
///
/// Records the current state of the provided component, and generates a diff with the next state at the end of the
/// frame. If change is detected an undo operation will be recorded. Generally you want to call this just before
/// you are about to make a change to the component.
///
/// Component to record the state of.
///
/// Path to the field which should be focused when performing the undo/redo operation. This should be the path
/// as provided by .
///
public static void RecordComponent(Component obj, string fieldPath)
{
ComponentToRecord cmp = new ComponentToRecord(obj, fieldPath);
components.Add(cmp);
}
///
/// Records the current state of the provided scene object header, and generates a diff with the next state at the
/// end of the frame. If change is detected an undo operation will be recorded. Generally you want to call this
/// just before you are about to make a change to the scene object header.
///
/// Note this will not record the entire scene object, but rather just its name, transform, active state and
/// potentially other similar properties. It's components as well as hierarchy state are ignored.
///
/// Scene object to record the state of.
///
/// Name to the field which should be focused when performing the undo/redo operation.
///
public static void RecordSceneObjectHeader(SceneObject obj, string fieldName)
{
SceneObjectHeaderToRecord so = new SceneObjectHeaderToRecord(obj, fieldName);
sceneObjectHeaders.Add(so);
}
///
/// Records the current state of the provided scene object header, and generates a diff with the next state at the
/// end of the frame. If change is detected an undo operation will be recorded. Generally you want to call this
/// just before you are about to make a change to the scene object header.
///
/// Note this will not record the entire scene object, but rather just its name, transform, active state and
/// potentially other similar properties. It's components as well as hierarchy state are ignored.
///
/// Scene objects to record the state of.
public static void RecordSceneObjectHeader(SceneObject[] objs)
{
SceneObjectHeaderToRecord so = new SceneObjectHeaderToRecord(objs, null);
sceneObjectHeaders.Add(so);
}
///
/// Records the current state of the provided scene object header, and generates a diff with the next state at the
/// end of the frame. If change is detected an undo operation will be recorded. Generally you want to call this
/// just before you are about to make a change to the scene object.
///
/// Note this records the complete state of a scene object, including its header, its components and optionally its
/// child hierarchy.
///
/// Scene object to record.
///
/// If true the child objects will be recorded as well, otherwise just the provided object.
///
/// Optional description specifying the type of changes about to be made.
public static void RecordSceneObject(SceneObject obj, bool hierarchy, string description)
{
SceneObjectToRecord so = new SceneObjectToRecord(obj, hierarchy, description);
sceneObjects.Add(so);
}
///
/// Generates diffs for any objects that were previously recorded using any of the Record* methods. The diff is
/// generated by comparing the state at the time Record* was called, compared to the current object state.
///
public static void ResolveDiffs()
{
foreach (var entry in components)
entry.RecordCommand();
foreach (var entry in sceneObjectHeaders)
entry.RecordCommand();
foreach (var entry in sceneObjects)
entry.RecordCommand();
components.Clear();
sceneObjectHeaders.Clear();
sceneObjects.Clear();
}
}
///
/// Contains information about scene object state, excluding information about its components and hierarchy.
///
internal struct SceneObjectState
{
internal string name;
internal Vector3 position;
internal Quaternion rotation;
internal Vector3 scale;
internal bool active;
///
/// Initializes the state from a scene object.
///
/// Scene object to initialize the state from.
/// New state object.
internal static SceneObjectState Create(SceneObject so)
{
SceneObjectState state = new SceneObjectState();
state.name = so.Name;
state.position = so.LocalPosition;
state.rotation = so.LocalRotation;
state.scale = so.LocalScale;
state.active = so.Active;
return state;
}
}
///
/// Contains the difference between two objects and allows the changes to be applied to
/// a . The value of the different fields is stored as its own state, while the flags field
/// specified which of the properties is actually different.
///
internal struct SceneObjectDiff
{
internal SceneObjectState state;
internal SceneObjectDiffFlags flags;
///
/// Creates a diff object storing the difference between two objects.
///
/// State of the scene object to compare from.
/// State of the scene object to compare to.
/// Difference between the two scene object states.
internal static SceneObjectDiff Create(SceneObjectState oldState, SceneObjectState newState)
{
SceneObjectDiff diff = new SceneObjectDiff();
diff.state = new SceneObjectState();
if (oldState.name != newState.name)
{
diff.state.name = newState.name;
diff.flags |= SceneObjectDiffFlags.Name;
}
if (oldState.position != newState.position)
{
diff.state.position = newState.position;
diff.flags |= SceneObjectDiffFlags.Position;
}
if (oldState.rotation != newState.rotation)
{
diff.state.rotation = newState.rotation;
diff.flags |= SceneObjectDiffFlags.Rotation;
}
if (oldState.scale != newState.scale)
{
diff.state.scale = newState.scale;
diff.flags |= SceneObjectDiffFlags.Scale;
}
if (oldState.active != newState.active)
{
diff.state.active = newState.active;
diff.flags |= SceneObjectDiffFlags.Active;
}
return diff;
}
///
/// Applies the diff to an actual scene object.
///
/// Scene object to apply the diff to.
internal void Apply(SceneObject sceneObject)
{
if (flags.HasFlag(SceneObjectDiffFlags.Name))
sceneObject.Name = state.name;
if (flags.HasFlag(SceneObjectDiffFlags.Position))
sceneObject.LocalPosition = state.position;
if (flags.HasFlag(SceneObjectDiffFlags.Rotation))
sceneObject.LocalRotation = state.rotation;
if (flags.HasFlag(SceneObjectDiffFlags.Scale))
sceneObject.LocalScale = state.scale;
if (flags.HasFlag(SceneObjectDiffFlags.Active))
sceneObject.Active = state.active;
}
}
///
/// Contains information about two separate states of a scene object header.
///
internal struct SceneObjectHeaderUndo
{
internal SceneObject obj;
internal SceneObjectDiff newToOld;
internal SceneObjectDiff oldToNew;
internal SceneObjectHeaderUndo(SceneObject obj, SceneObjectDiff newToOld, SceneObjectDiff oldToNew)
{
this.obj = obj;
this.newToOld = newToOld;
this.oldToNew = oldToNew;
}
}
///
/// Stores the field changes in a as a difference between two states. Allows those changes to
/// be reverted and re-applied. Does not record changes to scene object components or hierarchy, but just the fields
/// considered its header (such as name, local transform and active state).
///
[SerializeObject]
internal class RecordSceneObjectHeaderUndo : UndoableCommand
{
private string fieldPath;
private List headers;
private RecordSceneObjectHeaderUndo() { }
///
/// Creates the new scene object header undo command.
///
/// Information about recorded states of scene object headers.
///
/// Optional path that controls which is the field being modified and should receive input focus when the command
/// is executed. Note that the diffs applied have no restriction on how many fields they can modify at once, but
/// only one field will receive focus.
public RecordSceneObjectHeaderUndo(List headers, string fieldPath)
{
this.headers = headers;
this.fieldPath = fieldPath;
}
///
protected override void Commit()
{
if (headers == null)
return;
foreach (var header in headers)
{
if (header.obj == null)
continue;
if (header.obj.IsDestroyed)
{
Debug.LogWarning("Attempting to commit state on a destroyed game-object.");
continue;
}
header.oldToNew.Apply(header.obj);
}
FocusOnField();
RefreshInspector();
}
///
protected override void Revert()
{
if (headers == null)
return;
foreach (var header in headers)
{
if (header.obj == null)
continue;
if (header.obj.IsDestroyed)
{
Debug.LogWarning("Attempting to revert state on a destroyed game-object.");
continue;
}
header.newToOld.Apply(header.obj);
}
FocusOnField();
RefreshInspector();
}
///
/// Selects the component's scene object and focuses on the specific field in the inspector, if the inspector
/// window is open.
///
private void FocusOnField()
{
if (headers != null && headers.Count > 0)
{
if (headers.Count == 1)
{
if (Selection.SceneObject != headers[0].obj)
Selection.SceneObject = headers[0].obj;
}
else
{
List objs = new List();
foreach (var header in headers)
{
if(header.obj != null)
objs.Add(header.obj);
}
Selection.SceneObjects = objs.ToArray();
}
if (!string.IsNullOrEmpty(fieldPath))
{
InspectorWindow inspectorWindow = EditorWindow.GetWindow();
inspectorWindow?.FocusOnField(headers[0].obj.UUID, fieldPath);
}
}
}
///
/// Updates the values of the fields displayed in the inspector window.
///
private void RefreshInspector()
{
InspectorWindow inspectorWindow = EditorWindow.GetWindow();
inspectorWindow?.RefreshSceneObjectFields(true);
}
}
///
/// Stores the field changes in a as a difference between two states. Allows those changes to
/// be reverted and re-applied. Unlike this command records the
/// full scene object state, including changes to its components and optionally any child objects.
///
[SerializeObject]
internal class RecordSceneObjectUndo : UndoableCommand
{
private SceneObject obj;
private SerializedSceneObject oldState;
private SerializedSceneObject newState;
private RecordSceneObjectUndo() { }
///
/// Creates the new scene object undo command.
///
/// Scene object on which to apply the performed changes.
/// Recorded original state of the scene object(s).
/// Recorded new state of the scene object(s).
///
/// Optional description of the changes made to the scene object between the two states.
///
public RecordSceneObjectUndo(SceneObject obj, SerializedSceneObject oldState, SerializedSceneObject newState,
string description)
{
this.obj = obj;
this.oldState = oldState;
this.newState = newState;
}
///
protected override void Commit()
{
newState?.Restore();
FocusOnObject();
RefreshInspector();
}
///
protected override void Revert()
{
oldState?.Restore();
FocusOnObject();
RefreshInspector();
}
///
/// Selects the scene object if not already selected.
///
private void FocusOnObject()
{
if (obj != null)
{
if (Selection.SceneObject != obj)
Selection.SceneObject = obj;
}
}
///
/// Updates the values of the fields displayed in the inspector window.
///
private void RefreshInspector()
{
InspectorWindow inspectorWindow = EditorWindow.GetWindow();
inspectorWindow?.RefreshSceneObjectFields(true);
}
}
///
/// Stores the field changes in a as a difference between two states. Allows those changes to
/// be reverted and re-applied.
///
[SerializeObject]
internal class RecordComponentUndo : UndoableCommand
{
private Component obj;
private string fieldPath;
private SerializedDiff newToOld;
private SerializedDiff oldToNew;
private RecordComponentUndo() { }
///
/// Creates the new component undo command.
///
/// Component on which to apply the performed changes.
///
/// Optional path that controls which is the field being modified and should receive input focus when the command
/// is executed. Note that the diffs applied have no restriction on how many fields they can modify at once, but
/// only one field will receive focus.
///
/// Difference that can be applied to the old object in order to get the new object state.
///
///
/// Difference that can be applied to the new object in order to get the old object state.
///
public RecordComponentUndo(Component obj, string fieldPath, SerializedDiff oldToNew, SerializedDiff newToOld)
{
this.obj = obj;
this.fieldPath = fieldPath;
this.oldToNew = oldToNew;
this.newToOld = newToOld;
}
///
protected override void Commit()
{
if (oldToNew == null || obj == null)
return;
if (obj.IsDestroyed)
{
Debug.LogWarning("Attempting to commit state on a destroyed game-object.");
return;
}
oldToNew.Apply(obj);
FocusOnField();
}
///
protected override void Revert()
{
if (newToOld == null || obj == null)
return;
if (obj.IsDestroyed)
{
Debug.LogWarning("Attempting to revert state on a destroyed game-object.");
return;
}
newToOld.Apply(obj);
FocusOnField();
}
///
/// Selects the component's scene object and focuses on the specific field in the inspector, if the inspector
/// window is open.
///
private void FocusOnField()
{
SceneObject so = obj.SceneObject;
if (so != null)
{
if (Selection.SceneObject != so)
Selection.SceneObject = so;
if (!string.IsNullOrEmpty(fieldPath))
{
InspectorWindow inspectorWindow = EditorWindow.GetWindow();
inspectorWindow?.FocusOnField(obj.UUID, fieldPath);
}
}
}
}
/** @} */
}