//********************************** 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); } } /** @} */ }