// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Text; using FlaxEditor.GUI.Timeline.Undo; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; namespace FlaxEditor.GUI.Timeline.Tracks { /// /// The timeline track for invoking events on a certain points in the time. /// /// public class EventTrack : MemberTrack, IKeyframesEditorContext { /// /// Gets the archetype. /// /// The archetype. public static TrackArchetype GetArchetype() { return new TrackArchetype { TypeId = 15, Name = "Event", DisableSpawnViaGUI = true, Create = options => new EventTrack(ref options), Load = LoadTrack, Save = SaveTrack, }; } private static void LoadTrack(int version, Track track, BinaryReader stream) { var e = (EventTrack)track; var paramsCount = stream.ReadInt32(); e.EventParamsSizes = new int[paramsCount]; e.EventParamsTypes = new Type[paramsCount]; int eventsCount = stream.ReadInt32(); e.MemberName = LoadName(stream); bool isInvalid = false; for (int i = 0; i < paramsCount; i++) { e.EventParamsSizes[i] = stream.ReadInt32(); var paramTypeName = LoadName(stream); e.EventParamsTypes[i] = TypeUtils.GetManagedType(paramTypeName); if (e.EventParamsTypes[i] == null) isInvalid = true; } if (isInvalid) { e.Events.ResetKeyframes(); stream.ReadBytes(eventsCount * (sizeof(float) + e.EventParamsSizes.Sum())); Editor.LogError("Cannot load track " + e.MemberName + " of type " + e.MemberTypeName + ". Failed to find the value type information."); return; } e.OnEventParamsChanged(); var events = new KeyframesEditor.Keyframe[eventsCount]; var dataBuffer = paramsCount != 0 ? new byte[e.EventParamsSizes.Max()] : null; GCHandle handle = paramsCount != 0 ? GCHandle.Alloc(dataBuffer, GCHandleType.Pinned) : new GCHandle(); for (int i = 0; i < eventsCount; i++) { var time = stream.ReadSingle(); var key = new EventKey { Parameters = new object[paramsCount] }; for (int j = 0; j < paramsCount; j++) { stream.Read(dataBuffer, 0, e.EventParamsSizes[j]); key.Parameters[j] = Marshal.PtrToStructure(handle.AddrOfPinnedObject(), e.EventParamsTypes[j]); } events[i] = new KeyframesEditor.Keyframe { Time = time, Value = key, }; } if (handle.IsAllocated) handle.Free(); e.Events.SetKeyframes(events); } private static void SaveTrack(Track track, BinaryWriter stream) { var e = (EventTrack)track; var events = e.Events.Keyframes; var paramsCount = e.EventParamsTypes.Length; stream.Write(paramsCount); stream.Write(events.Count); SaveName(stream, e.MemberName); for (int i = 0; i < e.EventParamsTypes.Length; i++) { stream.Write(e.EventParamsSizes[i]); SaveName(stream, e.EventParamsTypes[i]?.FullName); } var dataBuffer = paramsCount != 0 ? new byte[e.EventParamsSizes.Max()] : null; IntPtr ptr = paramsCount != 0 ? Marshal.AllocHGlobal(dataBuffer.Length) : IntPtr.Zero; for (int i = 0; i < events.Count; i++) { var keyframe = events[i]; var key = (EventKey)keyframe.Value; stream.Write(keyframe.Time); for (int j = 0; j < paramsCount; j++) { Marshal.StructureToPtr(key.Parameters[j], ptr, true); Marshal.Copy(ptr, dataBuffer, 0, e.EventParamsSizes[j]); stream.Write(dataBuffer, 0, e.EventParamsSizes[j]); } } Marshal.FreeHGlobal(ptr); } private byte[] _eventsEditingStartData; /// /// The event keyframes editor. /// public KeyframesEditor Events; /// /// The event parameters data sizes collection. /// public int[] EventParamsSizes = Utils.GetEmptyArray(); /// /// The event parameters types collection. /// public Type[] EventParamsTypes = Utils.GetEmptyArray(); /// /// The event key data. /// public struct EventKey { /// /// The parameters values. /// [EditorDisplay("Parameters", EditorDisplayAttribute.InlineStyle), ExpandGroups] [Collection(CanReorderItems = false, CanResize = true)] public object[] Parameters; /// public override string ToString() { if (Parameters == null || Parameters.Length == 0) return "()"; var sb = new StringBuilder(); sb.Append('('); for (int i = 0; i < Parameters.Length; i++) { if (i != 0) sb.Append(", "); sb.Append(Parameters[i] ?? ""); } sb.Append(')'); return sb.ToString(); } } /// public EventTrack(ref TrackCreateOptions options) : base(ref options, true, false) { Height = 20.0f; // Events editor Events = new KeyframesEditor { EnableZoom = false, EnablePanning = false, ScrollBars = ScrollBars.None, }; Events.Edited += OnEventsEdited; Events.EditingStart += OnEventsEditingStart; Events.EditingEnd += OnEventsEditingEnd; Events.UnlockChildrenRecursive(); _addKey.Clicked += OnAddKeyClicked; _leftKey.Clicked += OnLeftKeyClicked; _rightKey.Clicked += OnRightKeyClicked; } private void OnRightKeyClicked(Image image, MouseButton button) { if (button == MouseButton.Left && GetNextKeyframeFrame(Timeline.CurrentTime, out var frame)) { Timeline.OnSeek(frame); } } /// public override bool GetNextKeyframeFrame(float time, out int result) { for (int i = 0; i < Events.Keyframes.Count; i++) { var k = Events.Keyframes[i]; if (k.Time > time) { result = Mathf.FloorToInt(k.Time * Timeline.FramesPerSecond); return true; } } return base.GetNextKeyframeFrame(time, out result); } private void OnAddKeyClicked(Image image, MouseButton button) { if (button == MouseButton.Left) { // Evaluate a value var time = Timeline.CurrentTime; if (!TryGetValue(out var value)) value = Events.Evaluate(time); // Find event at the current location for (int i = Events.Keyframes.Count - 1; i >= 0; i--) { var k = Events.Keyframes[i]; var frame = Mathf.FloorToInt(k.Time * Timeline.FramesPerSecond); if (frame == Timeline.CurrentFrame) { // Skip if value is the same if (k.Value == value) return; // Update existing event value Events.SetKeyframe(i, value); return; } } // Add a new key using (new TrackUndoBlock(this)) Events.AddKeyframe(new KeyframesEditor.Keyframe(time, value)); } } private void OnLeftKeyClicked(Image image, MouseButton button) { if (button == MouseButton.Left && GetPreviousKeyframeFrame(Timeline.CurrentTime, out var frame)) { Timeline.OnSeek(frame); } } /// public override bool GetPreviousKeyframeFrame(float time, out int result) { for (int i = Events.Keyframes.Count - 1; i >= 0; i--) { var k = Events.Keyframes[i]; if (k.Time < time) { result = Mathf.FloorToInt(k.Time * Timeline.FramesPerSecond); return true; } } return base.GetPreviousKeyframeFrame(time, out result); } private void UpdateEvents() { if (Events == null || Timeline == null) return; bool wasVisible = Events.Visible; Events.Visible = Visible; if (!Visible) { if (wasVisible) Events.ClearSelection(); return; } Events.KeyframesEditorContext = Timeline; Events.CustomViewPanning = Timeline.OnKeyframesViewPanning; Events.Bounds = new Rectangle(Timeline.StartOffset, Y + 1.0f, Timeline.Duration * Timeline.UnitsPerSecond * Timeline.Zoom, Height - 2.0f); Events.ViewScale = new Float2(Timeline.Zoom, 1.0f); Events.Visible = Visible; Events.UpdateKeyframes(); } private void OnEventsEdited() { Timeline.MarkAsEdited(); } private void OnEventsEditingStart() { _eventsEditingStartData = EditTrackAction.CaptureData(this); } private void OnEventsEditingEnd() { var after = EditTrackAction.CaptureData(this); if (!Utils.ArraysEqual(_eventsEditingStartData, after)) Timeline.AddBatchedUndoAction(new EditTrackAction(Timeline, this, _eventsEditingStartData, after)); _eventsEditingStartData = null; } private void OnEventParamsChanged() { // Calculate data size EventParamsSizes = new int[EventParamsTypes.Length]; for (int i = 0; i < EventParamsTypes.Length; i++) { var type = EventParamsTypes[i]; EventParamsSizes[i] = Marshal.SizeOf(type.IsEnum ? Enum.GetUnderlyingType(type) : type); } // Build default value var defaultValue = new EventKey { Parameters = new object[EventParamsTypes.Length] }; for (int i = 0; i < EventParamsTypes.Length; i++) defaultValue.Parameters[i] = Activator.CreateInstance(EventParamsTypes[i]); Events.DefaultValue = defaultValue; } /// protected override MemberTypes MemberTypes => MemberTypes.Method; /// protected override void OnMemberChanged(MemberInfo value, Type type) { base.OnMemberChanged(value, type); Events.ResetKeyframes(); if (value is MethodInfo m) { EventParamsTypes = m.GetParameters().Select(x => x.ParameterType).ToArray(); } else { EventParamsTypes = Utils.GetEmptyArray(); } OnEventParamsChanged(); } /// protected override void OnVisibleChanged() { base.OnVisibleChanged(); Events.Visible = Visible; } /// public override void OnTimelineChanged(Timeline timeline) { base.OnTimelineChanged(timeline); Events.Parent = timeline?.MediaPanel; Events.FPS = timeline?.FramesPerSecond; UpdateEvents(); } /// public override void OnTimelineZoomChanged() { base.OnTimelineZoomChanged(); UpdateEvents(); } /// public override void OnTimelineArrange() { base.OnTimelineArrange(); UpdateEvents(); } /// public override void OnTimelineFpsChanged(float before, float after) { base.OnTimelineFpsChanged(before, after); Events.FPS = after; } /// public override void OnDestroy() { if (Events != null) { Events.Dispose(); Events = null; } base.OnDestroy(); } /// public new void OnKeyframesDeselect(IKeyframesEditor editor) { if (Events != null && Events.Visible) Events.OnKeyframesDeselect(editor); } /// public new void OnKeyframesSelection(IKeyframesEditor editor, ContainerControl control, Rectangle selection) { if (Events != null && Events.Visible) Events.OnKeyframesSelection(editor, control, selection); } /// public new int OnKeyframesSelectionCount() { return Events != null && Events.Visible ? Events.OnKeyframesSelectionCount() : 0; } /// public new void OnKeyframesDelete(IKeyframesEditor editor) { if (Events != null && Events.Visible) Events.OnKeyframesDelete(editor); } /// public new void OnKeyframesMove(IKeyframesEditor editor, ContainerControl control, Float2 location, bool start, bool end) { if (Events != null && Events.Visible) Events.OnKeyframesMove(editor, control, location, start, end); } /// public new void OnKeyframesCopy(IKeyframesEditor editor, float? timeOffset, StringBuilder data) { if (Events != null && Events.Visible) Events.OnKeyframesCopy(editor, timeOffset, data); } /// public new void OnKeyframesPaste(IKeyframesEditor editor, float? timeOffset, string[] datas, ref int index) { if (Events != null && Events.Visible) Events.OnKeyframesPaste(editor, timeOffset, datas, ref index); } /// public new void OnKeyframesGet(Action get) { Events?.OnKeyframesGet(Name, get); } /// public new void OnKeyframesSet(List> keyframes) { Events?.OnKeyframesSet(keyframes); } } }