//********************************** Banshee Engine (www.banshee3d.com) **************************************************//
//**************** Copyright (c) 2018 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 const float DRAG_SCALE = 3.0f;
private const float ZOOM_SCALE = 0.1f/120.0f; // One scroll step is usually 120 units, we want 1/10 of that
private GUILayout gui;
private GUILayout mainPanel;
private GUIPanel drawingPanel;
private GUIPanel eventsPanel;
private GUITexture timelineBackground;
private GUITexture eventsBackground;
private GUIGraphTime guiTimeline;
private GUIAnimEvents guiEvents;
private GUICurves guiCurveDrawing;
private GUIGraphValues guiSidebar;
private int scrollBarWidth;
private int scrollBarHeight;
private GUIResizeableScrollBarH horzScrollBar;
private GUIResizeableScrollBarV vertScrollBar;
private ContextMenu blankContextMenu;
private ContextMenu keyframeContextMenu;
private ContextMenu blankEventContextMenu;
private ContextMenu eventContextMenu;
private Vector2I contextClickPosition;
private EdCurveDrawInfo[] curveInfos = new EdCurveDrawInfo[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 bool showEvents = true;
private List events = new List();
private SceneObject eventsSO;
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);
guiCurveDrawing.SetRange(xRange, yRange * 2.0f);
guiSidebar.SetRange(offset.y - yRange, offset.y + yRange);
if(showEvents)
guiEvents.SetRange(xRange);
Redraw();
}
}
///
/// Determines how much to offset the displayed curve values.
///
public Vector2 Offset
{
get { return offset; }
set
{
offset = value;
guiTimeline.SetOffset(offset.x);
guiCurveDrawing.SetOffset(offset);
guiSidebar.SetRange(offset.y - yRange, offset.y + yRange);
if(showEvents)
guiEvents.SetOffset(offset.x);
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.
///
/// GUI layout into which to place the GUI element.
/// Width in pixels.
/// Height in pixels.
/// If true show events on the graph and allow their editing.
/// Options that control which elements to display when drawing curves.
public GUICurveEditor(GUILayout gui, int width, int height, bool showEvents,
CurveDrawOptions drawOptions = CurveDrawOptions.DrawMarkers | CurveDrawOptions.DrawKeyframes)
{
this.gui = gui;
this.showEvents = showEvents;
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);
horzScrollBar = new GUIResizeableScrollBarH();
horzScrollBar.OnScrollOrResize += OnHorzScrollOrResize;
vertScrollBar = new GUIResizeableScrollBarV();
vertScrollBar.OnScrollOrResize += OnVertScrollOrResize;
GUILayout mainLayout = gui.AddLayoutY();
GUILayout curveLayoutHorz = mainLayout.AddLayoutX();
GUILayout horzScrollBarLayout = mainLayout.AddLayoutX();
horzScrollBarLayout.AddElement(horzScrollBar);
horzScrollBarLayout.AddFlexibleSpace();
mainPanel = curveLayoutHorz.AddPanel();
curveLayoutHorz.AddElement(vertScrollBar);
curveLayoutHorz.AddFlexibleSpace();
scrollBarHeight = horzScrollBar.Bounds.height;
scrollBarWidth = vertScrollBar.Bounds.width;
this.width = Math.Max(0, width - scrollBarWidth);
this.height = Math.Max(0, height - scrollBarHeight);
GUIPanel timelinePanel = mainPanel.AddPanel();
guiTimeline = new GUIGraphTime(timelinePanel, this.width, TIMELINE_HEIGHT);
GUIPanel timelineBgPanel = mainPanel.AddPanel(1);
timelineBackground = new GUITexture(null, EditorStyles.Header);
timelineBackground.Bounds = new Rect2I(0, 0, this.width, TIMELINE_HEIGHT + VERT_PADDING);
timelineBgPanel.AddElement(timelineBackground);
int eventsHeaderHeight = 0;
if (showEvents)
{
eventsPanel = mainPanel.AddPanel();
eventsPanel.SetPosition(0, TIMELINE_HEIGHT + VERT_PADDING);
guiEvents = new GUIAnimEvents(eventsPanel, this.width, EVENTS_HEIGHT);
GUIPanel eventsBgPanel = eventsPanel.AddPanel(1);
eventsBackground = new GUITexture(null, EditorStyles.Header);
eventsBackground.Bounds = new Rect2I(0, 0, this.width, EVENTS_HEIGHT + VERT_PADDING);
eventsBgPanel.AddElement(eventsBackground);
eventsHeaderHeight = EVENTS_HEIGHT;
}
drawingPanel = mainPanel.AddPanel();
drawingPanel.SetPosition(0, TIMELINE_HEIGHT + eventsHeaderHeight + VERT_PADDING);
guiCurveDrawing = new GUICurves(drawOptions);
guiCurveDrawing.SetWidth(this.width);
guiCurveDrawing.SetHeight(this.height - TIMELINE_HEIGHT - eventsHeaderHeight - VERT_PADDING * 2);
guiCurveDrawing.SetRange(60.0f, 20.0f);
guiCurveDrawing.Curves = GetPlainCurveDrawInfos();
drawingPanel.AddElement(guiCurveDrawing);
GUIPanel sidebarPanel = mainPanel.AddPanel(-10);
sidebarPanel.SetPosition(0, TIMELINE_HEIGHT + eventsHeaderHeight + VERT_PADDING);
guiSidebar = new GUIGraphValues(sidebarPanel, SIDEBAR_WIDTH, this.height - TIMELINE_HEIGHT -
eventsHeaderHeight - VERT_PADDING * 2);
guiSidebar.SetRange(-10.0f, 10.0f);
horzScrollBar.SetWidth(this.width);
vertScrollBar.SetHeight(this.height);
UpdateScrollBarSize();
}
///
/// Change the set of curves to display.
///
/// New set of curves to draw on the GUI element.
public void SetCurves(EdCurveDrawInfo[] curveInfos)
{
this.curveInfos = curveInfos;
guiCurveDrawing.Curves = GetPlainCurveDrawInfos();
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 = Math.Max(0, width - scrollBarWidth);
this.height = Math.Max(0, height - scrollBarHeight);
int eventsHeaderHeight = 0;
if (showEvents)
{
eventsHeaderHeight = EVENTS_HEIGHT;
guiEvents.SetSize(this.width, EVENTS_HEIGHT);
eventsBackground.Bounds = new Rect2I(0, 0, this.width, EVENTS_HEIGHT + VERT_PADDING);
}
guiTimeline.SetSize(this.width, TIMELINE_HEIGHT);
guiCurveDrawing.SetWidth(this.width);
guiCurveDrawing.SetHeight(this.height - TIMELINE_HEIGHT - eventsHeaderHeight);
guiSidebar.SetSize(SIDEBAR_WIDTH, this.height - TIMELINE_HEIGHT - eventsHeaderHeight);
timelineBackground.Bounds = new Rect2I(0, 0, this.width, TIMELINE_HEIGHT + VERT_PADDING);
horzScrollBar.SetWidth(this.width);
vertScrollBar.SetHeight(this.height);
UpdateScrollBarSize();
UpdateScrollBarPosition();
}
///
/// Number of frames per second, used for frame selection and marking.
///
/// Number of prames per second.
public void SetFPS(int fps)
{
guiTimeline.SetFPS(fps);
guiCurveDrawing.FPS = (uint)fps;
if(showEvents)
guiEvents.SetFPS(fps);
Redraw();
}
///
/// Sets a scene object that will be used for enumerating components/methods used for adding events.
///
/// Scene object containing the animation component.
public void SetEventSceneObject(SceneObject so)
{
eventsSO = so;
}
///
/// 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);
guiCurveDrawing.MarkedFrame = (uint)frameIdx;
if(showEvents)
guiEvents.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.AddOrUpdateKeyframe(t, value);
curveInfo.curve.Apply();
}
}
else
ShowReadOnlyMessage();
OnCurveModified?.Invoke();
RefreshCurveDrawing();
UpdateEventsGUI();
}
///
/// Adds a new event at the currently selected event.
///
public void AddEventAtMarker()
{
ClearSelection();
if (!showEvents)
return;
float eventTime = guiEvents.GetTimeForFrame(markedFrameIdx);
EventInfo eventInfo = new EventInfo();
eventInfo.animEvent = new AnimationEvent("", eventTime);
events.Add(eventInfo);
OnEventAdded?.Invoke();
UpdateEventsGUI();
StartEventEdit(events.Count - 1);
}
///
/// Rebuilds GUI displaying the animation events list.
///
private void UpdateEventsGUI()
{
if (!showEvents)
return;
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()
{
guiTimeline.Rebuild();
guiSidebar.Rebuild();
if(showEvents)
guiEvents.Rebuild();
}
///
/// Updates the curve drawing. Should be called after animation curves change.
///
private void RefreshCurveDrawing()
{
guiCurveDrawing.Curves = GetPlainCurveDrawInfos();
}
///
/// 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);
// Refresh tangent display
guiCurveDrawing.SelectKeyframe(new KeyframeRef(selectedEntry.curveIdx, keyframeIdx), mode, true);
}
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);
// Refresh tangent display
guiCurveDrawing.SelectKeyframe(new KeyframeRef(selectedEntry.curveIdx, keyframeIdx), newMode, true);
}
}
curve.Apply();
}
OnCurveModified?.Invoke();
RefreshCurveDrawing();
}
///
/// Adds a new keyframe at the specified time on the provided curve.
///
/// Index of the curve to add the keyframe to.
/// Time at which to add the keyframe.
private void AddKeyframe(int curveIdx, float time)
{
ClearSelection();
if (!disableCurveEdit)
{
if (curveIdx < curveInfos.Length)
{
EdAnimationCurve curve = curveInfos[curveIdx].curve;
float value = curve.Evaluate(time, false);
curve.AddOrUpdateKeyframe(time, value);
curve.Apply();
}
}
else
ShowReadOnlyMessage();
OnCurveModified?.Invoke();
RefreshCurveDrawing();
UpdateEventsGUI();
}
///
/// 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.AddOrUpdateKeyframe(t, value);
curveInfo.curve.Apply();
}
}
else
ShowReadOnlyMessage();
OnCurveModified?.Invoke();
RefreshCurveDrawing();
UpdateEventsGUI();
}
}
///
/// Adds a new event at the position the context menu was opened at.
///
private void AddEventAtPosition()
{
if (!showEvents)
return;
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();
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();
ClearSelection();
OnCurveModified?.Invoke();
RefreshCurveDrawing();
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;
OnEventDeleted?.Invoke();
ClearSelection();
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)
{
TangentMode tangentMode = TangentMode.Auto;
if (keyframeRef.curveIdx >= 0 && keyframeRef.curveIdx < curveInfos.Length)
{
EdAnimationCurve curve = curveInfos[keyframeRef.curveIdx].curve;
if (keyframeRef.keyIdx >= 0 && keyframeRef.keyIdx < curve.TangentModes.Length)
tangentMode = curve.TangentModes[keyframeRef.keyIdx];
}
guiCurveDrawing.SelectKeyframe(keyframeRef, tangentMode, 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 = drawingPanel.Bounds;
position.x = MathEx.Clamp(position.x, 0, drawingBounds.width);
position.y = MathEx.Clamp(position.y, 0, drawingBounds.height);
KeyframeEditWindow editWindow = DropDownWindow.Open(drawingPanel, position);
editWindow.Initialize(keyFrame, x =>
{
curve.UpdateKeyframe(keyIndex, x.time, x.value);
curve.Apply();
RefreshCurveDrawing();
},
x =>
{
if (x)
OnCurveModified?.Invoke();
});
}
///
/// Opens the edit window for the currently selected event.
///
private void EditSelectedEvent()
{
if (!showEvents)
return;
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)
{
if (!showEvents || eventsSO == null)
return;
AnimationEvent animEvent = events[eventIdx].animEvent;
Vector2I position = new Vector2I();
position.x = guiEvents.GetOffset(animEvent.time);
position.y = EVENTS_HEIGHT/2;
Component[] components = eventsSO.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(eventsPanel, position);
editWindow.Initialize(animEvent, componentNames, () =>
{
UpdateEventsGUI();
},
x =>
{
if(x)
OnEventModified?.Invoke();
});
}
///
/// 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);
}
#region Input
///
/// 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;
Rect2I elementBounds = mainPanel.ScreenBounds;
Vector2I pointerPos = ev.ScreenPos - 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 = new Rect2I();
if(showEvents)
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;
}
UpdateEventsGUI();
}
else
{
int frameIdx = guiTimeline.GetFrame(pointerPos);
if (frameIdx != -1)
SetMarkedFrame(frameIdx);
else
{
int eventIdx;
if (showEvents && guiEvents.FindEvent(eventPos, out eventIdx))
{
if (!Input.IsButtonHeld(ButtonCode.LeftShift) && !Input.IsButtonHeld(ButtonCode.RightShift))
ClearSelection();
events[eventIdx].selected = true;
UpdateEventsGUI();
}
else
{
ClearSelection();
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, mainPanel);
UpdateEventsGUI();
}
else
{
// If clicked outside of current selection, just select the one keyframe
if (!IsSelected(keyframeRef))
{
ClearSelection();
SelectKeyframe(keyframeRef);
UpdateEventsGUI();
}
keyframeContextMenu.Open(pointerPos, mainPanel);
}
}
else if (showEvents && guiEvents.GetFrame(eventPos) != -1) // Clicked over events bar
{
contextClickPosition = eventPos;
int eventIdx;
if (!guiEvents.FindEvent(eventPos, out eventIdx))
{
ClearSelection();
blankEventContextMenu.Open(pointerPos, mainPanel);
UpdateEventsGUI();
}
else
{
// If clicked outside of current selection, just select the one event
if (!events[eventIdx].selected)
{
ClearSelection();
events[eventIdx].selected = true;
UpdateEventsGUI();
}
eventContextMenu.Open(pointerPos, mainPanel);
}
}
}
else if (ev.button == PointerButton.Middle)
{
Vector2 curvePos;
if (WindowToCurveSpace(pointerPos, out curvePos))
{
dragStartPos = pointerPos;
isMiddlePointerHeld = true;
}
}
}
///
/// Handles input. Should be called by the owning window whenever a pointer is double-clicked.
///
/// Object containing pointer press event information.
internal void OnPointerDoubleClicked(PointerEvent ev)
{
if (ev.IsUsed)
return;
Rect2I elementBounds = mainPanel.ScreenBounds;
Vector2I pointerPos = ev.ScreenPos - new Vector2I(elementBounds.x, elementBounds.y);
bool isOverEditor = pointerPos.x >= 0 && pointerPos.x < width && pointerPos.y >= 0 && pointerPos.y < height;
if (!isOverEditor)
return;
Rect2I drawingBounds = drawingPanel.Bounds;
Vector2I drawingPos = pointerPos - new Vector2I(drawingBounds.x, drawingBounds.y);
if (guiCurveDrawing.PixelToCurveSpace(drawingPos, out var curveCoord, true))
{
int curveIdx = (int)guiCurveDrawing.FindCurve(drawingPos);
if (curveIdx == -1)
return;
AddKeyframe(curveIdx, curveCoord.x);
}
}
///
/// 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)
{
Rect2I elementBounds = mainPanel.ScreenBounds;
Vector2I pointerPos = ev.ScreenPos - 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;
RefreshCurveDrawing();
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;
else
tangent = -tangent;
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;
RefreshCurveDrawing();
}
}
}
}
else // Move frame marker
{
int frameIdx = guiTimeline.GetFrame(pointerPos);
if (frameIdx != -1)
SetMarkedFrame(frameIdx);
OnFrameSelected?.Invoke(frameIdx);
}
}
if (isMiddlePointerHeld)
{
Rect2I elementBounds = mainPanel.ScreenBounds;
Vector2I pointerPos = ev.ScreenPos - new Vector2I(elementBounds.x, elementBounds.y);
int distance = Vector2I.Distance(dragStartPos, pointerPos);
if (distance >= DRAG_START_DISTANCE)
{
isZoomDragInProgress = true;
Cursor.Hide();
Rect2I clipRect;
clipRect.x = ev.ScreenPos.x - 2;
clipRect.y = ev.ScreenPos.y - 2;
clipRect.width = 4;
clipRect.height = 4;
Cursor.ClipToRect(clipRect);
}
}
}
///
/// 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 (isZoomDragInProgress)
{
Cursor.Show();
Cursor.ClipDisable();
}
if (isModifiedDuringDrag)
OnCurveModified?.Invoke();
isMiddlePointerHeld = false;
isZoomDragInProgress = false;
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();
}
#endregion
#region Scroll, drag, zoom
private Vector2I dragStartPos;
private bool isMiddlePointerHeld;
private bool isZoomDragInProgress;
private float zoomAmount;
///
/// Handles mouse scroll wheel and dragging events in order to zoom or drag the displayed curve editor contents.
/// Should be called every frame.
///
internal void HandleDragAndZoomInput()
{
// Handle middle mouse dragging
if (isZoomDragInProgress)
{
float lengthPerPixel = Range.x / Width;
float heightPerPixel = Range.y / Height;
float dragX = Input.GetAxisValue(InputAxis.MouseX) * DRAG_SCALE * lengthPerPixel;
float dragY = Input.GetAxisValue(InputAxis.MouseY) * DRAG_SCALE * heightPerPixel;
Vector2 offset = Offset;
offset.x = Math.Max(0.0f, offset.x + dragX);
offset.y -= dragY;
Offset = offset;
UpdateScrollBarSize();
UpdateScrollBarPosition();
}
// Handle zoom in/out
float scroll = Input.GetAxisValue(InputAxis.MouseZ);
if (scroll != 0.0f)
{
Rect2I elementBounds = mainPanel.ScreenBounds;
Debug.Log(elementBounds + " " + Input.PointerPosition);
Vector2I pointerPos = Input.PointerPosition - new Vector2I(elementBounds.x, elementBounds.y);
Vector2 curvePos;
if (WindowToCurveSpace(pointerPos, out curvePos))
{
float zoom = scroll * ZOOM_SCALE;
Zoom(curvePos, zoom);
}
}
}
///
/// Moves or resizes the vertical scroll bar under the curve editor.
///
/// New position of the scrollbar, in range [0, 1].
/// New size of the scrollbar handle, in range [0, 1].
private void SetVertScrollbarProperties(float position, float size)
{
Vector2 visibleRange = Range;
Vector2 totalRange = GetTotalRange();
visibleRange.y = totalRange.y*size;
Range = visibleRange;
float scrollableRange = totalRange.y - visibleRange.y;
Vector2 offset = Offset;
offset.y = -scrollableRange * (position * 2.0f - 1.0f);
Offset = offset;
}
///
/// Moves or resizes the horizontal scroll bar under the curve editor.
///
/// New position of the scrollbar, in range [0, 1].
/// New size of the scrollbar handle, in range [0, 1].
private void SetHorzScrollbarProperties(float position, float size)
{
Vector2 visibleRange = Range;
Vector2 totalRange = GetTotalRange();
visibleRange.x = totalRange.x * size;
Range = visibleRange;
float scrollableRange = totalRange.x - visibleRange.x;
Vector2 offset = Offset;
offset.x = scrollableRange * position;
Offset = offset;
}
///
/// Updates the size of both scrollbars depending on the currently visible curve area vs. the total curve area.
///
private void UpdateScrollBarSize()
{
Vector2 visibleRange = Range;
Vector2 totalRange = GetTotalRange();
horzScrollBar.HandleSize = visibleRange.x / totalRange.x;
vertScrollBar.HandleSize = visibleRange.y / totalRange.y;
}
///
/// Updates the position of both scrollbars depending on the offset currently applied to the visible curve area.
///
private void UpdateScrollBarPosition()
{
Vector2 visibleRange = Range;
Vector2 totalRange = GetTotalRange();
Vector2 scrollableRange = totalRange - visibleRange;
Vector2 offset = Offset;
if (scrollableRange.x > 0.0f)
horzScrollBar.Position = offset.x / scrollableRange.x;
else
horzScrollBar.Position = 0.0f;
if (scrollableRange.y > 0.0f)
{
float pos = offset.y/scrollableRange.y;
float sign = MathEx.Sign(pos);
pos = sign*MathEx.Clamp01(MathEx.Abs(pos));
pos = (1.0f - pos) /2.0f;
vertScrollBar.Position = pos;
}
else
vertScrollBar.Position = 0.0f;
}
///
/// Calculates the width/height of the curve area depending on the current zoom level.
///
/// Width/height of the curve area, in curve space (value, time).
private Vector2 GetZoomedRange()
{
float zoomLevel = MathEx.Pow(2, zoomAmount);
GetOptimalRangeAndOffset(out _, out var optimalRange);
return optimalRange / zoomLevel;
}
///
/// Returns the total width/height of the contents of the curve area.
///
/// Width/height of the curve area, in curve space (value, time).
private Vector2 GetTotalRange()
{
// Return optimal range (that covers the visible curve)
GetOptimalRangeAndOffset(out _, out var range);
// Increase range in case user zoomed out
Vector2 zoomedRange = GetZoomedRange();
return Vector2.Max(range, zoomedRange);
}
///
/// Zooms in or out at the provided position in the curve display.
///
/// Position to zoom towards, relative to the curve display area, in curve space
/// (value, time)
/// Amount to zoom in (positive), or out (negative).
private void Zoom(Vector2 curvePos, float amount)
{
// Increase or decrease the visible range depending on zoom level
Vector2 oldZoomedRange = GetZoomedRange();
zoomAmount = MathEx.Clamp(zoomAmount + amount, -10.0f, 10.0f);
Vector2 zoomedRange = GetZoomedRange();
Vector2 zoomedDiff = zoomedRange - oldZoomedRange;
Vector2 currentRange = Range;
Vector2 newRange = currentRange + zoomedDiff;
Range = newRange;
// When zooming, make sure to focus on the point provided, so adjust the offset
Vector2 rangeScale = newRange;
rangeScale.x /= currentRange.x;
rangeScale.y /= currentRange.y;
Vector2 relativeCurvePos = curvePos - Offset;
Vector2 newCurvePos = relativeCurvePos * rangeScale;
Vector2 diff = newCurvePos - relativeCurvePos;
Offset -= diff;
UpdateScrollBarSize();
UpdateScrollBarPosition();
}
///
/// Updates the offset and range of the curve display to fully fit the currently selected set of curves.
///
/// If true the time offset/range will be recalculated, otherwise current time offset will
/// be kept as is.
internal void CenterAndResize(bool resetTime)
{
GetOptimalRangeAndOffset(out var offset, out var range);
if (!resetTime)
{
offset.x = Offset.x;
range.x = Range.x;
}
Range = range;
Offset = offset;
UpdateScrollBarPosition();
UpdateScrollBarSize();
}
///
/// Triggered when the user moves or resizes the horizontal scrollbar.
///
/// New position of the scrollbar, in range [0, 1].
/// New size of the scrollbar, in range [0, 1].
private void OnHorzScrollOrResize(float position, float size)
{
SetHorzScrollbarProperties(position, size);
}
///
/// Triggered when the user moves or resizes the vertical scrollbar.
///
/// New position of the scrollbar, in range [0, 1].
/// New size of the scrollbar, in range [0, 1].
private void OnVertScrollOrResize(float position, float size)
{
SetVertScrollbarProperties(position, size);
}
#endregion
#region Helpers
///
/// Returns width/height required to show the entire contents of the currently displayed curves.
///
/// Offset used for centering the curves.
/// Range representing the width/height in curve space (time, value).
private void GetOptimalRangeAndOffset(out Vector2 offset, out Vector2 range)
{
AnimationCurve[] curves = new AnimationCurve[curveInfos.Length];
for(int i = 0; i < curveInfos.Length; i++)
curves[i] = curveInfos[i].curve.Normal;
float xMin, xMax;
float yMin, yMax;
AnimationUtility.CalculateRange(curves, out xMin, out xMax, out yMin, out yMax);
float xRange = xMax - xMin;
float yRange = (yMax - yMin) * 0.5f;
float yOffset = yMin + yRange;
// Add padding to y range
yRange *= 1.05f;
// Don't allow zero range
if (xRange == 0.0f)
xRange = 60.0f;
if (yRange == 0.0f)
yRange = 10.0f;
offset = new Vector2(xMin, yOffset);
range = new Vector2(xRange, yRange);
}
///
/// Returns a set of current curve draw infos with non-editor curves.
///
/// Curve draw infos.
private CurveDrawInfo[] GetPlainCurveDrawInfos()
{
CurveDrawInfo[] output = new CurveDrawInfo[curveInfos.Length];
for(int i = 0; i < curveInfos.Length; i++)
output[i] = new CurveDrawInfo(curveInfos[i].curve.Normal, curveInfos[i].color);
return output;
}
///
/// 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 this GUI element, 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 pointerPos, out Vector2 curveCoord)
{
Rect2I drawingBounds = drawingPanel.Bounds;
Vector2I drawingPos = pointerPos - new Vector2I(drawingBounds.x, drawingBounds.y);
Debug.Log(pointerPos + " " + drawingPos);
return guiCurveDrawing.PixelToCurveSpace(drawingPos, out curveCoord);
}
#endregion
}
///
/// Information necessary to drawing an editor curve.
///
public struct EdCurveDrawInfo
{
public EdCurveDrawInfo(EdAnimationCurve curve, Color color)
{
this.curve = curve;
this.color = color;
}
public EdAnimationCurve curve;
public Color color;
}
///
/// 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);
}
}
/** @} */
}