//********************************** Banshee Engine (www.banshee3d.com) **************************************************//
//**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************//
using System;
using System.Collections.Generic;
using BansheeEngine;
namespace BansheeEditor
{
/** @addtogroup AnimationEditor
* @{
*/
///
/// Displays a set of animation curves and events. Allows manipulation of both by adding, removing and modifying
/// curve keyframes, and animation events.
///
internal class GUICurveEditor
{
///
/// Information about currently selected set of keyframes for a specific curve.
///
class SelectedKeyframes
{
public int curveIdx;
public List keyIndices = new List();
}
///
/// Information about a keyframe that is currently being dragged.
///
struct DraggedKeyframe
{
public DraggedKeyframe(int index, KeyFrame original)
{
this.index = index;
this.original = original;
}
public int index;
public KeyFrame original;
}
///
/// Information about all keyframes of a specific curve that are currently being dragged.
///
class DraggedKeyframes
{
public int curveIdx;
public List keys = new List();
}
///
/// Data about an animation event.
///
class EventInfo
{
public AnimationEvent animEvent;
public bool selected;
}
private const int TIMELINE_HEIGHT = 20;
private const int VERT_PADDING = 2;
private const int EVENTS_HEIGHT = 15;
private const int SIDEBAR_WIDTH = 30;
private const int DRAG_START_DISTANCE = 3;
private AnimationWindow window;
private GUILayout gui;
private GUIPanel drawingPanel;
private GUIPanel eventsPanel;
private GUIGraphTime guiTimeline;
private GUIAnimEvents guiEvents;
private GUICurveDrawing guiCurveDrawing;
private GUIGraphValues guiSidebar;
private ContextMenu blankContextMenu;
private ContextMenu keyframeContextMenu;
private ContextMenu blankEventContextMenu;
private ContextMenu eventContextMenu;
private Vector2I contextClickPosition;
private CurveDrawInfo[] curveInfos = new CurveDrawInfo[0];
private bool disableCurveEdit = false;
private float xRange = 60.0f;
private float yRange = 10.0f;
private Vector2 offset;
private int width;
private int height;
private int markedFrameIdx;
private List selectedKeyframes = new List();
private List events = new List();
private bool isPointerHeld;
private bool isMousePressedOverKey;
private bool isMousePressedOverTangent;
private bool isDragInProgress;
private bool isModifiedDuringDrag;
private List draggedKeyframes = new List();
private TangentRef draggedTangent;
private Vector2I dragStart;
///
/// Triggers whenever user selects a new frame. Reports the index of the selected frame.
///
public Action OnFrameSelected;
///
/// Triggered whenever a new animation event is added.
///
public Action OnEventAdded;
///
/// Triggered whenever values in an animation event change.
///
public Action OnEventModified;
///
/// Triggered whenever an animation event is deleted.
///
public Action OnEventDeleted;
///
/// Triggered whenever keyframe in a curve is modified (added, removed or edited).
///
public Action OnCurveModified;
///
/// Triggered when the user clicks anywhere on the curve editor area.
///
public Action OnClicked;
///
/// The displayed range of the curve, where:
/// .x - Range of the horizontal area. Displayed area ranges from [0, x].
/// .y - Range of the vertical area. Displayed area ranges from [-y, y].
///
public Vector2 Range
{
get { return new Vector2(xRange, yRange); }
set
{
xRange = value.x;
yRange = value.y;
guiTimeline.SetRange(xRange);
guiEvents.SetRange(xRange);
guiCurveDrawing.SetRange(xRange, yRange * 2.0f);
guiSidebar.SetRange(offset.y - yRange, offset.y + yRange);
Redraw();
}
}
///
/// Determines how much to offset the displayed curve values.
///
public Vector2 Offset
{
get { return offset; }
set
{
offset = value;
guiTimeline.SetOffset(offset.x);
guiEvents.SetOffset(offset.x);
guiCurveDrawing.SetOffset(offset);
guiSidebar.SetRange(offset.y - yRange, offset.y + yRange);
Redraw();
}
}
///
/// Returns the width of the curve editor, in pixels.
///
public int Width
{
get { return width; }
}
///
/// Returns the height of the curve editor, in pixels.
///
public int Height
{
get { return height; }
}
///
/// Set to true if curves are not allowed to be edited.
///
public bool DisableCurveEdit
{
set { disableCurveEdit = value; }
}
///
/// Animation events displayed on the curve editor.
///
public AnimationEvent[] Events
{
get
{
AnimationEvent[] animEvents = new AnimationEvent[events.Count];
// Note: Hidden dependency. Returned events must point to the same event class this object is using, so
// that any modifications made in this class will be visible in the returned values.
for (int i = 0; i < events.Count; i++)
animEvents[i] = events[i].animEvent;
return animEvents;
}
set
{
events.Clear();
for (int i = 0; i < value.Length; i++)
{
EventInfo eventInfo = new EventInfo();
eventInfo.animEvent = value[i];
events.Add(eventInfo);
}
UpdateEventsGUI();
}
}
///
/// Creates a new curve editor GUI elements.
///
/// Parent window of the GUI element.
/// GUI layout into which to place the GUI element.
/// Width in pixels.
/// Height in pixels.
public GUICurveEditor(AnimationWindow window, GUILayout gui, int width, int height)
{
this.window = window;
this.gui = gui;
this.width = width;
this.height = height;
blankContextMenu = new ContextMenu();
blankContextMenu.AddItem("Add keyframe", AddKeyframeAtPosition);
blankEventContextMenu = new ContextMenu();
blankEventContextMenu.AddItem("Add event", AddEventAtPosition);
keyframeContextMenu = new ContextMenu();
keyframeContextMenu.AddItem("Delete", DeleteSelectedKeyframes);
keyframeContextMenu.AddItem("Edit", EditSelectedKeyframe);
keyframeContextMenu.AddItem("Tangents/Auto", () => { ChangeSelectionTangentMode(TangentMode.Auto); });
keyframeContextMenu.AddItem("Tangents/Free", () => { ChangeSelectionTangentMode(TangentMode.Free); });
keyframeContextMenu.AddItem("Tangents/In/Auto", () => { ChangeSelectionTangentMode(TangentMode.InAuto); });
keyframeContextMenu.AddItem("Tangents/In/Free", () => { ChangeSelectionTangentMode(TangentMode.InFree); });
keyframeContextMenu.AddItem("Tangents/In/Linear", () => { ChangeSelectionTangentMode(TangentMode.InLinear); });
keyframeContextMenu.AddItem("Tangents/In/Step", () => { ChangeSelectionTangentMode(TangentMode.InStep); });
keyframeContextMenu.AddItem("Tangents/Out/Auto", () => { ChangeSelectionTangentMode(TangentMode.OutAuto); });
keyframeContextMenu.AddItem("Tangents/Out/Free", () => { ChangeSelectionTangentMode(TangentMode.OutFree); });
keyframeContextMenu.AddItem("Tangents/Out/Linear", () => { ChangeSelectionTangentMode(TangentMode.OutLinear); });
keyframeContextMenu.AddItem("Tangents/Out/Step", () => { ChangeSelectionTangentMode(TangentMode.OutStep); });
eventContextMenu = new ContextMenu();
eventContextMenu.AddItem("Delete", DeleteSelectedEvents);
eventContextMenu.AddItem("Edit", EditSelectedEvent);
GUIPanel timelinePanel = gui.AddPanel();
guiTimeline = new GUIGraphTime(timelinePanel, width, TIMELINE_HEIGHT);
GUIPanel timelineBgPanel = gui.AddPanel(1);
GUITexture timelineBackground = new GUITexture(null, EditorStyles.Header);
timelineBackground.Bounds = new Rect2I(0, 0, width, TIMELINE_HEIGHT + VERT_PADDING);
timelineBgPanel.AddElement(timelineBackground);
eventsPanel = gui.AddPanel();
eventsPanel.SetPosition(0, TIMELINE_HEIGHT + VERT_PADDING);
guiEvents = new GUIAnimEvents(eventsPanel, width, EVENTS_HEIGHT);
GUIPanel eventsBgPanel = eventsPanel.AddPanel(1);
GUITexture eventsBackground = new GUITexture(null, EditorStyles.Header);
eventsBackground.Bounds = new Rect2I(0, 0, width, EVENTS_HEIGHT + VERT_PADDING);
eventsBgPanel.AddElement(eventsBackground);
drawingPanel = gui.AddPanel();
drawingPanel.SetPosition(0, TIMELINE_HEIGHT + EVENTS_HEIGHT + VERT_PADDING);
guiCurveDrawing = new GUICurveDrawing(drawingPanel, width, height - TIMELINE_HEIGHT - EVENTS_HEIGHT - VERT_PADDING * 2, curveInfos);
guiCurveDrawing.SetRange(60.0f, 20.0f);
GUIPanel sidebarPanel = gui.AddPanel(-10);
sidebarPanel.SetPosition(0, TIMELINE_HEIGHT + EVENTS_HEIGHT + VERT_PADDING);
guiSidebar = new GUIGraphValues(sidebarPanel, SIDEBAR_WIDTH, height - TIMELINE_HEIGHT - EVENTS_HEIGHT - VERT_PADDING * 2);
guiSidebar.SetRange(-10.0f, 10.0f);
}
///
/// Converts coordinate in curve space (time, value) into pixel coordinates relative to the curve drawing area
/// origin.
///
/// Time and value of the location to convert.
/// Coordinates relative to curve drawing area's origin, in pixels.
public Vector2I CurveToPixelSpace(Vector2 curveCoords)
{
return guiCurveDrawing.CurveToPixelSpace(curveCoords);
}
///
/// Converts coordinates in window space (relative to the parent window origin) into coordinates in curve space.
///
/// Coordinates relative to parent editor window, in pixels.
/// Curve coordinates within the range as specified by . Only
/// valid when function returns true.
/// True if the coordinates are within the curve area, false otherwise.
public bool WindowToCurveSpace(Vector2I windowPos, out Vector2 curveCoord)
{
Rect2I elementBounds = GUIUtility.CalculateBounds(gui, window.GUI);
Vector2I pointerPos = windowPos - new Vector2I(elementBounds.x, elementBounds.y);
Rect2I drawingBounds = drawingPanel.Bounds;
Vector2I drawingPos = pointerPos - new Vector2I(drawingBounds.x, drawingBounds.y);
return guiCurveDrawing.PixelToCurveSpace(drawingPos, out curveCoord);
}
///
/// Handles input. Should be called by the owning window whenever a pointer is pressed.
///
/// Object containing pointer press event information.
internal void OnPointerPressed(PointerEvent ev)
{
if (ev.IsUsed)
return;
Vector2I windowPos = window.ScreenToWindowPos(ev.ScreenPos);
Rect2I elementBounds = GUIUtility.CalculateBounds(gui, window.GUI);
Vector2I pointerPos = windowPos - new Vector2I(elementBounds.x, elementBounds.y);
bool isOverEditor = pointerPos.x >= 0 && pointerPos.x < width && pointerPos.y >= 0 && pointerPos.y < height;
if (!isOverEditor)
return;
else
OnClicked?.Invoke();
Rect2I drawingBounds = drawingPanel.Bounds;
Vector2I drawingPos = pointerPos - new Vector2I(drawingBounds.x, drawingBounds.y);
Rect2I eventBounds = eventsPanel.Bounds;
Vector2I eventPos = pointerPos - new Vector2I(eventBounds.x, eventBounds.y);
if (ev.Button == PointerButton.Left)
{
Vector2 curveCoord;
if (guiCurveDrawing.PixelToCurveSpace(drawingPos, out curveCoord, true))
{
KeyframeRef keyframeRef;
if (!guiCurveDrawing.FindKeyFrame(drawingPos, out keyframeRef))
{
TangentRef tangentRef;
if (guiCurveDrawing.FindTangent(drawingPos, out tangentRef))
{
isMousePressedOverTangent = true;
dragStart = drawingPos;
draggedTangent = tangentRef;
}
else
ClearSelection();
}
else
{
if (!IsSelected(keyframeRef))
{
if (!Input.IsButtonHeld(ButtonCode.LeftShift) && !Input.IsButtonHeld(ButtonCode.RightShift))
ClearSelection();
SelectKeyframe(keyframeRef);
}
isMousePressedOverKey = true;
dragStart = drawingPos;
}
guiCurveDrawing.Rebuild();
UpdateEventsGUI();
}
else
{
int frameIdx = guiTimeline.GetFrame(pointerPos);
if (frameIdx != -1)
SetMarkedFrame(frameIdx);
else
{
int eventIdx;
if (guiEvents.FindEvent(eventPos, out eventIdx))
{
if (!Input.IsButtonHeld(ButtonCode.LeftShift) && !Input.IsButtonHeld(ButtonCode.RightShift))
ClearSelection();
events[eventIdx].selected = true;
UpdateEventsGUI();
}
else
{
ClearSelection();
guiCurveDrawing.Rebuild();
UpdateEventsGUI();
}
}
OnFrameSelected?.Invoke(frameIdx);
}
isPointerHeld = true;
}
else if (ev.Button == PointerButton.Right)
{
Vector2 curveCoord;
if (guiCurveDrawing.PixelToCurveSpace(drawingPos, out curveCoord, true))
{
contextClickPosition = drawingPos;
KeyframeRef keyframeRef;
if (!guiCurveDrawing.FindKeyFrame(drawingPos, out keyframeRef))
{
ClearSelection();
blankContextMenu.Open(pointerPos, gui);
guiCurveDrawing.Rebuild();
UpdateEventsGUI();
}
else
{
// If clicked outside of current selection, just select the one keyframe
if (!IsSelected(keyframeRef))
{
ClearSelection();
SelectKeyframe(keyframeRef);
guiCurveDrawing.Rebuild();
UpdateEventsGUI();
}
keyframeContextMenu.Open(pointerPos, gui);
}
}
else if (guiEvents.GetFrame(eventPos) != -1) // Clicked over events bar
{
contextClickPosition = eventPos;
int eventIdx;
if (!guiEvents.FindEvent(eventPos, out eventIdx))
{
ClearSelection();
blankEventContextMenu.Open(pointerPos, gui);
guiCurveDrawing.Rebuild();
UpdateEventsGUI();
}
else
{
// If clicked outside of current selection, just select the one event
if (!events[eventIdx].selected)
{
ClearSelection();
events[eventIdx].selected = true;
guiCurveDrawing.Rebuild();
UpdateEventsGUI();
}
eventContextMenu.Open(pointerPos, gui);
}
}
}
}
///
/// Handles input. Should be called by the owning window whenever a pointer is moved.
///
/// Object containing pointer move event information.
internal void OnPointerMoved(PointerEvent ev)
{
if (ev.Button != PointerButton.Left)
return;
if (isPointerHeld)
{
Vector2I windowPos = window.ScreenToWindowPos(ev.ScreenPos);
Rect2I elementBounds = GUIUtility.CalculateBounds(gui, window.GUI);
Vector2I pointerPos = windowPos - new Vector2I(elementBounds.x, elementBounds.y);
if (isMousePressedOverKey || isMousePressedOverTangent)
{
Rect2I drawingBounds = drawingPanel.Bounds;
Vector2I drawingPos = pointerPos - new Vector2I(drawingBounds.x, drawingBounds.y);
if (!isDragInProgress)
{
int distance = Vector2I.Distance(drawingPos, dragStart);
if (distance >= DRAG_START_DISTANCE)
{
if (isMousePressedOverKey && !disableCurveEdit)
{
draggedKeyframes.Clear();
foreach (var selectedEntry in selectedKeyframes)
{
EdAnimationCurve curve = curveInfos[selectedEntry.curveIdx].curve;
KeyFrame[] keyFrames = curve.KeyFrames;
DraggedKeyframes newEntry = new DraggedKeyframes();
newEntry.curveIdx = selectedEntry.curveIdx;
draggedKeyframes.Add(newEntry);
foreach (var keyframeIdx in selectedEntry.keyIndices)
newEntry.keys.Add(new DraggedKeyframe(keyframeIdx, keyFrames[keyframeIdx]));
}
}
isDragInProgress = true;
}
}
if (isDragInProgress)
{
if (isMousePressedOverKey && !disableCurveEdit)
{
Vector2 diff = Vector2.Zero;
Vector2 dragStartCurve;
if (guiCurveDrawing.PixelToCurveSpace(dragStart, out dragStartCurve, true))
{
Vector2 currentPosCurve;
if (guiCurveDrawing.PixelToCurveSpace(drawingPos, out currentPosCurve, true))
diff = currentPosCurve - dragStartCurve;
}
foreach (var draggedEntry in draggedKeyframes)
{
EdAnimationCurve curve = curveInfos[draggedEntry.curveIdx].curve;
for (int i = 0; i < draggedEntry.keys.Count; i++)
{
DraggedKeyframe draggedKey = draggedEntry.keys[i];
float newTime = Math.Max(0.0f, draggedKey.original.time + diff.x);
float newValue = draggedKey.original.value + diff.y;
int newIndex = curve.UpdateKeyframe(draggedKey.index, newTime, newValue);
// It's possible key changed position due to time change, but since we're moving all
// keys at once they cannot change position relative to one another, otherwise we would
// need to update indices for other keys as well.
draggedKey.index = newIndex;
draggedEntry.keys[i] = draggedKey;
}
curve.Apply();
}
// Rebuild selected keys from dragged keys (after potential sorting)
ClearSelection();
foreach (var draggedEntry in draggedKeyframes)
{
foreach (var keyframe in draggedEntry.keys)
SelectKeyframe(new KeyframeRef(draggedEntry.curveIdx, keyframe.index));
}
isModifiedDuringDrag = true;
window.RecordClipState();
guiCurveDrawing.Rebuild();
UpdateEventsGUI();
}
else if (isMousePressedOverTangent && !disableCurveEdit)
{
EdAnimationCurve curve = curveInfos[draggedTangent.keyframeRef.curveIdx].curve;
KeyFrame keyframe = curve.KeyFrames[draggedTangent.keyframeRef.keyIdx];
Vector2 keyframeCurveCoords = new Vector2(keyframe.time, keyframe.value);
Vector2 currentPosCurve;
if (guiCurveDrawing.PixelToCurveSpace(drawingPos, out currentPosCurve, true))
{
Vector2 normal = currentPosCurve - keyframeCurveCoords;
normal = normal.Normalized;
float tangent = EdAnimationCurve.NormalToTangent(normal);
if (draggedTangent.type == TangentType.In)
{
if (normal.x > 0.0f)
tangent = float.PositiveInfinity;
keyframe.inTangent = -tangent;
if(curve.TangentModes[draggedTangent.keyframeRef.keyIdx] == TangentMode.Free)
keyframe.outTangent = -tangent;
}
else
{
if (normal.x < 0.0f)
tangent = float.PositiveInfinity;
keyframe.outTangent = tangent;
if (curve.TangentModes[draggedTangent.keyframeRef.keyIdx] == TangentMode.Free)
keyframe.inTangent = tangent;
}
curve.KeyFrames[draggedTangent.keyframeRef.keyIdx] = keyframe;
curve.Apply();
isModifiedDuringDrag = true;
window.RecordClipState();
guiCurveDrawing.Rebuild();
}
}
}
}
else // Move frame marker
{
int frameIdx = guiTimeline.GetFrame(pointerPos);
if (frameIdx != -1)
SetMarkedFrame(frameIdx);
OnFrameSelected?.Invoke(frameIdx);
}
}
}
///
/// Handles input. Should be called by the owning window whenever a pointer is released.
///
/// Object containing pointer release event information.
internal void OnPointerReleased(PointerEvent ev)
{
if (isModifiedDuringDrag)
OnCurveModified?.Invoke();
isPointerHeld = false;
isDragInProgress = false;
isMousePressedOverKey = false;
isMousePressedOverTangent = false;
isModifiedDuringDrag = false;
}
///
/// Handles input. Should be called by the owning window whenever a button is released.
///
/// Object containing button release event information.
internal void OnButtonUp(ButtonEvent ev)
{
if(ev.Button == ButtonCode.Delete)
DeleteSelectedKeyframes();
}
///
/// Change the set of curves to display.
///
/// New set of curves to draw on the GUI element.
public void SetCurves(CurveDrawInfo[] curveInfos)
{
this.curveInfos = curveInfos;
guiCurveDrawing.SetCurves(curveInfos);
Redraw();
}
///
/// Change the physical size of the GUI element.
///
/// Width of the element in pixels.
/// Height of the element in pixels.
public void SetSize(int width, int height)
{
this.width = width;
this.height = height;
guiTimeline.SetSize(width, TIMELINE_HEIGHT);
guiEvents.SetSize(height, EVENTS_HEIGHT);
guiCurveDrawing.SetSize(width, height - TIMELINE_HEIGHT - EVENTS_HEIGHT);
guiSidebar.SetSize(SIDEBAR_WIDTH, height - TIMELINE_HEIGHT - EVENTS_HEIGHT);
Redraw();
}
///
/// Number of frames per second, used for frame selection and marking.
///
/// Number of prames per second.
public void SetFPS(int fps)
{
guiTimeline.SetFPS(fps);
guiEvents.SetFPS(fps);
guiCurveDrawing.SetFPS(fps);
Redraw();
}
///
/// Returns time for a frame with the specified index. Depends on set range and FPS.
///
/// Index of the frame (not a key-frame) to get the time for.
/// Time of the frame with the provided index.
public float GetTimeForFrame(int frameIdx)
{
return guiCurveDrawing.GetTimeForFrame(frameIdx);
}
///
/// Sets the frame at which to display the frame marker.
///
/// Index of the frame to display the marker on, or -1 to clear the marker.
public void SetMarkedFrame(int frameIdx)
{
markedFrameIdx = frameIdx;
guiTimeline.SetMarkedFrame(frameIdx);
guiEvents.SetMarkedFrame(frameIdx);
guiCurveDrawing.SetMarkedFrame(frameIdx);
Redraw();
}
///
/// Adds a new keyframe at the currently selected frame.
///
public void AddKeyFrameAtMarker()
{
ClearSelection();
if (!disableCurveEdit)
{
foreach (var curveInfo in curveInfos)
{
float t = guiCurveDrawing.GetTimeForFrame(markedFrameIdx);
float value = curveInfo.curve.Evaluate(t);
curveInfo.curve.AddKeyframe(t, value);
curveInfo.curve.Apply();
}
}
else
ShowReadOnlyMessage();
window.RecordClipState();
OnCurveModified?.Invoke();
guiCurveDrawing.Rebuild();
UpdateEventsGUI();
}
///
/// Adds a new event at the currently selected event.
///
public void AddEventAtMarker()
{
ClearSelection();
float eventTime = guiEvents.GetTimeForFrame(markedFrameIdx);
EventInfo eventInfo = new EventInfo();
eventInfo.animEvent = new AnimationEvent("", eventTime);
events.Add(eventInfo);
OnEventAdded?.Invoke();
UpdateEventsGUI();
guiCurveDrawing.Rebuild();
StartEventEdit(events.Count - 1);
}
///
/// Rebuilds GUI displaying the animation events list.
///
private void UpdateEventsGUI()
{
AnimationEvent[] animEvents = new AnimationEvent[events.Count];
bool[] selected = new bool[events.Count];
for (int i = 0; i < events.Count; i++)
{
animEvents[i] = events[i].animEvent;
selected[i] = events[i].selected;
}
guiEvents.SetEvents(animEvents, selected);
guiEvents.Rebuild();
}
///
/// Rebuilds the entire curve editor GUI.
///
public void Redraw()
{
guiCurveDrawing.Rebuild();
guiTimeline.Rebuild();
guiEvents.Rebuild();
guiSidebar.Rebuild();
}
///
/// Changes the tangent mode for all currently selected keyframes.
///
/// Tangent mode to set. If only in or out tangent mode is provided, the mode for the opposite
/// tangent will be kept as is.
private void ChangeSelectionTangentMode(TangentMode mode)
{
if (disableCurveEdit)
{
ShowReadOnlyMessage();
return;
}
foreach (var selectedEntry in selectedKeyframes)
{
EdAnimationCurve curve = curveInfos[selectedEntry.curveIdx].curve;
foreach (var keyframeIdx in selectedEntry.keyIndices)
{
if (mode == TangentMode.Auto || mode == TangentMode.Free)
curve.SetTangentMode(keyframeIdx, mode);
else
{
TangentMode newMode = curve.TangentModes[keyframeIdx];
if (mode.HasFlag((TangentMode) TangentType.In))
{
// Replace only the in tangent mode, keeping the out tangent as is
TangentMode inFlags = (TangentMode.InAuto | TangentMode.InFree | TangentMode.InLinear |
TangentMode.InStep);
newMode &= ~inFlags;
newMode |= (mode & inFlags);
}
else
{
// Replace only the out tangent mode, keeping the in tangent as is
TangentMode outFlags = (TangentMode.OutAuto | TangentMode.OutFree | TangentMode.OutLinear |
TangentMode.OutStep);
newMode &= ~outFlags;
newMode |= (mode & outFlags);
}
curve.SetTangentMode(keyframeIdx, newMode);
}
}
curve.Apply();
}
window.RecordClipState();
OnCurveModified?.Invoke();
guiCurveDrawing.Rebuild();
}
///
/// Adds a new keyframe at the position the context menu was opened at.
///
private void AddKeyframeAtPosition()
{
Vector2 curveCoord;
if (guiCurveDrawing.PixelToCurveSpace(contextClickPosition, out curveCoord))
{
ClearSelection();
if (!disableCurveEdit)
{
foreach (var curveInfo in curveInfos)
{
float t = curveCoord.x;
float value = curveCoord.y;
curveInfo.curve.AddKeyframe(t, value);
curveInfo.curve.Apply();
}
}
else
ShowReadOnlyMessage();
window.RecordClipState();
OnCurveModified?.Invoke();
guiCurveDrawing.Rebuild();
UpdateEventsGUI();
}
}
///
/// Adds a new event at the position the context menu was opened at.
///
private void AddEventAtPosition()
{
int frame = guiEvents.GetFrame(contextClickPosition);
if (frame != -1)
{
ClearSelection();
float time = guiEvents.GetTime(contextClickPosition.x);
EventInfo eventInfo = new EventInfo();
eventInfo.animEvent = new AnimationEvent("", time);
events.Add(eventInfo);
OnEventAdded?.Invoke();
UpdateEventsGUI();
guiCurveDrawing.Rebuild();
StartEventEdit(events.Count - 1);
}
}
///
/// Removes all currently selected keyframes from the curves.
///
private void DeleteSelectedKeyframes()
{
if (!disableCurveEdit)
{
foreach (var selectedEntry in selectedKeyframes)
{
EdAnimationCurve curve = curveInfos[selectedEntry.curveIdx].curve;
// Sort keys from highest to lowest so the indices don't change
selectedEntry.keyIndices.Sort((x, y) =>
{
return y.CompareTo(x);
});
foreach (var keyframeIdx in selectedEntry.keyIndices)
curve.RemoveKeyframe(keyframeIdx);
curve.Apply();
}
}
else
ShowReadOnlyMessage();
window.RecordClipState();
ClearSelection();
OnCurveModified?.Invoke();
guiCurveDrawing.Rebuild();
UpdateEventsGUI();
}
///
/// Deletes all currently selected events.
///
private void DeleteSelectedEvents()
{
List newEvents = new List();
foreach (var entry in events)
{
if(!entry.selected)
newEvents.Add(entry);
}
events = newEvents;
window.RecordClipState();
OnEventDeleted?.Invoke();
ClearSelection();
guiCurveDrawing.Rebuild();
UpdateEventsGUI();
}
///
/// Unselects any selected keyframes and events.
///
private void ClearSelection()
{
guiCurveDrawing.ClearSelectedKeyframes();
selectedKeyframes.Clear();
foreach (var entry in events)
entry.selected = false;
}
///
/// Adds the provided keyframe to the selection list (doesn't clear existing ones).
///
/// Keyframe to select.
private void SelectKeyframe(KeyframeRef keyframeRef)
{
guiCurveDrawing.SelectKeyframe(keyframeRef, true);
if (!IsSelected(keyframeRef))
{
int curveIdx = selectedKeyframes.FindIndex(x =>
{
return x.curveIdx == keyframeRef.curveIdx;
});
if (curveIdx == -1)
{
curveIdx = selectedKeyframes.Count;
SelectedKeyframes newKeyframes = new SelectedKeyframes();
newKeyframes.curveIdx = keyframeRef.curveIdx;
selectedKeyframes.Add(newKeyframes);
}
selectedKeyframes[curveIdx].keyIndices.Add(keyframeRef.keyIdx);
}
}
///
/// Checks is the provided keyframe currently selected.
///
/// Keyframe to check.
/// True if selected, false otherwise.
private bool IsSelected(KeyframeRef keyframeRef)
{
int curveIdx = selectedKeyframes.FindIndex(x =>
{
return x.curveIdx == keyframeRef.curveIdx;
});
if (curveIdx == -1)
return false;
int keyIdx = selectedKeyframes[curveIdx].keyIndices.FindIndex(x =>
{
return x == keyframeRef.keyIdx;
});
return keyIdx != -1;
}
///
/// Opens the edit window for the currently selected keyframe.
///
private void EditSelectedKeyframe()
{
if (disableCurveEdit)
{
ShowReadOnlyMessage();
return;
}
if (selectedKeyframes.Count == 0)
return;
EdAnimationCurve curve = curveInfos[selectedKeyframes[0].curveIdx].curve;
KeyFrame[] keyFrames = curve.KeyFrames;
int keyIndex = selectedKeyframes[0].keyIndices[0];
KeyFrame keyFrame = keyFrames[keyIndex];
Vector2I position = guiCurveDrawing.CurveToPixelSpace(new Vector2(keyFrame.time, keyFrame.value));
Rect2I drawingBounds = GUIUtility.CalculateBounds(drawingPanel, window.GUI);
position.x = MathEx.Clamp(position.x, 0, drawingBounds.width);
position.y = MathEx.Clamp(position.y, 0, drawingBounds.height);
Vector2I windowPos = position + new Vector2I(drawingBounds.x, drawingBounds.y);
KeyframeEditWindow editWindow = DropDownWindow.Open(window, windowPos);
editWindow.Initialize(keyFrame, x =>
{
curve.UpdateKeyframe(keyIndex, x.time, x.value);
curve.Apply();
guiCurveDrawing.Rebuild();
OnCurveModified?.Invoke();
},
x =>
{
if (x)
window.RecordClipState();
});
}
///
/// Opens the edit window for the currently selected event.
///
private void EditSelectedEvent()
{
for (int i = 0; i < events.Count; i++)
{
if (events[i].selected)
{
StartEventEdit(i);
break;
}
}
}
///
/// Opens the event edit window for the specified event.
///
/// Event index to open the edit window for.
private void StartEventEdit(int eventIdx)
{
AnimationEvent animEvent = events[eventIdx].animEvent;
Vector2I position = new Vector2I();
position.x = guiEvents.GetOffset(animEvent.time);
position.y = EVENTS_HEIGHT/2;
Rect2I eventBounds = GUIUtility.CalculateBounds(eventsPanel, window.GUI);
Vector2I windowPos = position + new Vector2I(eventBounds.x, eventBounds.y);
SceneObject so = window.SelectedSO;
Component[] components = so.GetComponents();
string[] componentNames = new string[components.Length];
for (int i = 0; i < components.Length; i++)
componentNames[i] = components[i].GetType().Name;
EventEditWindow editWindow = DropDownWindow.Open(window, windowPos);
editWindow.Initialize(animEvent, componentNames, () =>
{
UpdateEventsGUI();
OnEventModified?.Invoke();
},
x =>
{
if(x)
window.RecordClipState();
});
}
///
/// Shows a dialog box that notifies the user that the animation clip is read only.
///
private void ShowReadOnlyMessage()
{
LocEdString title = new LocEdString("Warning");
LocEdString message =
new LocEdString("You cannot edit keyframes on animation clips that" +
" are imported from an external file.");
DialogBox.Open(title, message, DialogBox.Type.OK);
}
}
///
/// Drop down window that displays input boxes used for editing a keyframe.
///
[DefaultSize(120, 80)]
internal class KeyframeEditWindow : DropDownWindow
{
private Action closeCallback;
private bool changesMade;
///
/// Initializes the drop down window by creating the necessary GUI. Must be called after construction and before
/// use.
///
/// Keyframe whose properties to edit.
/// Callback triggered when event values change.
/// Callback triggered just before the window closes.
internal void Initialize(KeyFrame keyFrame, Action updateCallback, Action closeCallback)
{
GUIFloatField timeField = new GUIFloatField(new LocEdString("Time"), 40, "");
timeField.Value = keyFrame.time;
timeField.OnChanged += x => { keyFrame.time = x; changesMade = true; updateCallback(keyFrame); };
GUIFloatField valueField = new GUIFloatField(new LocEdString("Value"), 40, "");
valueField.Value = keyFrame.value;
valueField.OnChanged += x => { keyFrame.value = x; changesMade = true; updateCallback(keyFrame); };
GUILayoutY vertLayout = GUI.AddLayoutY();
vertLayout.AddFlexibleSpace();
GUILayoutX horzLayout = vertLayout.AddLayoutX();
horzLayout.AddFlexibleSpace();
GUILayout contentLayout = horzLayout.AddLayoutY();
GUILayout timeLayout = contentLayout.AddLayoutX();
timeLayout.AddSpace(5);
timeLayout.AddElement(timeField);
timeLayout.AddFlexibleSpace();
GUILayout componentLayout = contentLayout.AddLayoutX();
componentLayout.AddSpace(5);
componentLayout.AddElement(valueField);
componentLayout.AddFlexibleSpace();
horzLayout.AddFlexibleSpace();
vertLayout.AddFlexibleSpace();
this.closeCallback = closeCallback;
}
private void OnDestroy()
{
closeCallback?.Invoke(changesMade);
}
}
///
/// Drop down window that displays input boxes used for editing an event.
///
[DefaultSize(200, 80)]
internal class EventEditWindow : DropDownWindow
{
private Action closeCallback;
private bool changesMade;
///
/// Initializes the drop down window by creating the necessary GUI. Must be called after construction and before
/// use.
///
/// Event whose properties to edit.
/// List of component names that the user can select from.
/// Callback triggered when event values change.
/// Callback triggered just before the window closes.
internal void Initialize(AnimationEvent animEvent, string[] componentNames, Action updateCallback,
Action closeCallback)
{
int selectedIndex = -1;
string methodName = "";
if (!string.IsNullOrEmpty(animEvent.name))
{
string[] nameEntries = animEvent.name.Split('/');
if (nameEntries.Length > 1)
{
string typeName = nameEntries[0];
for (int i = 0; i < componentNames.Length; i++)
{
if (componentNames[i] == typeName)
{
selectedIndex = i;
break;
}
}
methodName = nameEntries[nameEntries.Length - 1];
}
}
GUIFloatField timeField = new GUIFloatField(new LocEdString("Time"), 40, "");
timeField.Value = animEvent.time;
timeField.OnChanged += x => { animEvent.time = x; changesMade = true; updateCallback(); };
GUIListBoxField componentField = new GUIListBoxField(componentNames, new LocEdString("Component"), 40);
if (selectedIndex != -1)
componentField.Index = selectedIndex;
componentField.OnSelectionChanged += x =>
{
string compName = "";
if (x != -1)
compName = componentNames[x] + "/";
animEvent.name = compName + x;
changesMade = true;
updateCallback();
};
GUITextField methodField = new GUITextField(new LocEdString("Method"), 40, false, "", GUIOption.FixedWidth(190));
methodField.Value = methodName;
methodField.OnChanged += x =>
{
string compName = "";
if(componentField.Index != -1)
compName = componentNames[componentField.Index] + "/";
animEvent.name = compName + x;
changesMade = true;
updateCallback();
};
GUILayoutY vertLayout = GUI.AddLayoutY();
vertLayout.AddFlexibleSpace();
GUILayoutX horzLayout = vertLayout.AddLayoutX();
horzLayout.AddFlexibleSpace();
GUILayout contentLayout = horzLayout.AddLayoutY();
GUILayout timeLayout = contentLayout.AddLayoutX();
timeLayout.AddSpace(5);
timeLayout.AddElement(timeField);
timeLayout.AddFlexibleSpace();
GUILayout componentLayout = contentLayout.AddLayoutX();
componentLayout.AddSpace(5);
componentLayout.AddElement(componentField);
componentLayout.AddFlexibleSpace();
GUILayout methodLayout = contentLayout.AddLayoutX();
methodLayout.AddSpace(5);
methodLayout.AddElement(methodField);
methodLayout.AddFlexibleSpace();
horzLayout.AddFlexibleSpace();
vertLayout.AddFlexibleSpace();
this.closeCallback = closeCallback;
}
private void OnDestroy()
{
closeCallback?.Invoke(changesMade);
}
}
/** @} */
}