├── EventMarkerNotification.cs ├── EventMarkerNotification.cs.meta ├── EventMarkerReceiver.cs ├── EventMarkerReceiver.cs.meta ├── EventMarkerTrack.cs ├── EventMarkerTrack.cs.meta ├── LICENSE ├── LICENSE.meta ├── README.md ├── README.md.meta ├── Resources.meta ├── Resources ├── EventMarker.png ├── EventMarker.png.meta ├── EventMarker.svg ├── EventMarker.svg.meta ├── EventMarkerIcon.png ├── EventMarkerIcon.png.meta ├── EventMarkerIcon.svg ├── EventMarkerIcon.svg.meta ├── EventMarker_Collapsed.png ├── EventMarker_Collapsed.png.meta ├── EventMarker_Collapsed.svg ├── EventMarker_Collapsed.svg.meta ├── EventMarker_Selected.png ├── EventMarker_Selected.png.meta ├── EventMarker_Selected.svg └── EventMarker_Selected.svg.meta ├── Stylesheets.meta ├── Stylesheets ├── Extensions.meta └── Extensions │ ├── common.uss │ └── common.uss.meta ├── TimelineTools.cs └── TimelineTools.cs.meta /EventMarkerNotification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using UnityEngine; 4 | using UnityEngine.Playables; 5 | using UnityEngine.Timeline; 6 | using Object = UnityEngine.Object; 7 | 8 | namespace TimelineTools 9 | { 10 | namespace Events 11 | { 12 | public enum ParameterType 13 | { 14 | None, 15 | Bool, 16 | Int, 17 | Float, 18 | String, 19 | Object, 20 | Enum, 21 | Playable, 22 | EventMarkerNotification 23 | } 24 | 25 | [Serializable] 26 | public class Argument 27 | { 28 | // Argument type 29 | public ParameterType parameterType; 30 | // argument properties 31 | public bool Bool; 32 | public int Int; 33 | public string String; 34 | public float Float; 35 | public ExposedReference Object; 36 | } 37 | 38 | [Serializable] 39 | public class Callback 40 | { 41 | // Names 42 | public string assemblyName; 43 | public string methodName; 44 | public Argument[] arguments; 45 | } 46 | 47 | [CustomStyle("EventMarkerStyle")] 48 | [Serializable, DisplayName("Event Marker")] 49 | public class EventMarkerNotification : Marker, INotification, INotificationOptionProvider 50 | { 51 | public Callback[] callbacks; 52 | public bool retroactive = true; 53 | public bool emitOnce; 54 | public bool emitInEditor = true; 55 | public Color color = new(1.0f, 1.0f, 1.0f, 0.5f); 56 | public bool showLineOverlay = true; 57 | 58 | PropertyName INotification.id { get { return new PropertyName(); } } 59 | 60 | NotificationFlags INotificationOptionProvider.flags 61 | { 62 | get 63 | { 64 | return (retroactive ? NotificationFlags.Retroactive : default) | 65 | (emitOnce ? NotificationFlags.TriggerOnce : default) | 66 | (emitInEditor ? NotificationFlags.TriggerInEditMode : default); 67 | } 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /EventMarkerNotification.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 409864d1c6c7dde4c86616e24b54616a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /EventMarkerReceiver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using UnityEngine; 4 | using UnityEngine.Playables; 5 | 6 | namespace TimelineTools 7 | { 8 | namespace Events 9 | { 10 | public class EventMarkerReceiver : MonoBehaviour, INotificationReceiver 11 | { 12 | public void OnNotify(Playable origin, INotification notification, object context) 13 | { 14 | //An INotificationReceiver will receive all the triggered notifications. We need to 15 | //have a filter to use only the notifications that we can process. 16 | var message = notification as EventMarkerNotification; 17 | if (message == null || message.callbacks == null) return; 18 | 19 | foreach (var callback in message.callbacks) 20 | { 21 | // Setup arguments 22 | object[] arguments = new object[callback.arguments.Length]; 23 | Type[] types = new Type[callback.arguments.Length]; 24 | for (int i = 0; i < arguments.Length; i++) 25 | { 26 | var argument = callback.arguments[i]; 27 | if (argument.parameterType == ParameterType.Bool) 28 | arguments[i] = argument.Bool; 29 | else if (argument.parameterType == ParameterType.Int) 30 | arguments[i] = argument.Int; 31 | else if (argument.parameterType == ParameterType.Float) 32 | arguments[i] = argument.Float; 33 | else if (argument.parameterType == ParameterType.Object) 34 | arguments[i] = argument.Object.Resolve(origin.GetGraph().GetResolver()); 35 | else if (argument.parameterType == ParameterType.String) 36 | arguments[i] = argument.String; 37 | else if (argument.parameterType == ParameterType.Enum) 38 | arguments[i] = Enum.ToObject(Type.GetType(argument.String + ",Assembly-CSharp"), argument.Int); 39 | else if (argument.parameterType == ParameterType.Playable) 40 | arguments[i] = origin; 41 | else if (argument.parameterType == ParameterType.EventMarkerNotification) 42 | arguments[i] = message; 43 | 44 | types[i] = arguments[i].GetType(); 45 | } 46 | 47 | // Call method 48 | var component = gameObject.GetComponentInChildren(Type.GetType(callback.assemblyName)); 49 | const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly; 50 | MethodInfo methodInfo = component.GetType().GetMethod(callback.methodName, bindingFlags, null, types, null); 51 | methodInfo.Invoke(component, arguments); 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /EventMarkerReceiver.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 33b0cc6190d91164d94cd3681fff0ed7 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {fileID: 2800000, guid: 2e7da690a91425640b75c328929f5897, type: 3} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /EventMarkerTrack.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using UnityEngine; 3 | using UnityEngine.Timeline; 4 | 5 | #if UNITY_EDITOR 6 | using System; 7 | using System.Reflection; 8 | using UnityEngine.Playables; 9 | using UnityEditor; 10 | using UnityEditor.Timeline; 11 | using Component = UnityEngine.Component; 12 | using Object = UnityEngine.Object; 13 | #endif 14 | 15 | namespace TimelineTools 16 | { 17 | namespace Events 18 | { 19 | [TrackColor(1f, 0.89f, 0.8f)] 20 | [TrackBindingType(typeof(EventMarkerReceiver)), DisplayName("Event Marker Track")] 21 | public class EventMarkerTrack : TrackAsset 22 | { 23 | #if UNITY_EDITOR 24 | // Duplicate version of Unity's DrivenPropertyManager.bindings.cs:TryPropertyModification because it is an internal API 25 | private static void TryPropertyModification(Object driver, Object target, string propertyPath) 26 | { 27 | // Deviate from Unity's DrivenPropertyManager.bindings.cs:TryPropertyModification Internals because we need to retrieve private APIs: 28 | // TryRegisterPropertyPartial(driver, target, propertyPath); 29 | 30 | // Retrieve DrivenPropertyManager.TryRegisterProperty() API driver to directly call the silent version 31 | var drivenPropertyManager = Type.GetType("UnityEngine.DrivenPropertyManager,UnityEngine.CoreModule"); 32 | var tryRegisterProperty = drivenPropertyManager.GetMethod("TryRegisterProperty"); // Silent (ignores property not found) 33 | //var registerProperty = drivenPropertyManager.GetMethod("RegisterProperty"); // Vocal (complains if property not found) 34 | 35 | // Setup arguments for DrivenPropertyManager.TryRegisterProperty() 36 | object[] arguments = new object[3]; 37 | arguments[0] = driver; // always the same every call 38 | arguments[1] = target; 39 | arguments[2] = propertyPath; 40 | 41 | // Invoke the silent API 42 | tryRegisterProperty.Invoke(null, arguments); 43 | } 44 | 45 | // Duplicate version of Unity's PropertyCollector.cs:AddPropertyModification API which is silent instead of displaying errors for non-existent properties 46 | private static void AddPropertyModification(Component comp, string name) 47 | { 48 | if (comp == null) 49 | return; 50 | 51 | // var driver = WindowState.previewDriver; // Deviate from Unity's AddPropertyModification Internals: 52 | var windowState = typeof(TimelineEditor).GetProperty("state", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null); 53 | var previewDriver = (AnimationModeDriver)windowState.GetType().GetProperty("previewDriver", BindingFlags.Public | BindingFlags.Static).GetValue(null); 54 | 55 | var driver = previewDriver; 56 | if (driver == null || !AnimationMode.InAnimationMode(driver)) 57 | return; 58 | 59 | // Register Property will display an error if a property doesn't exist (wanted behaviour) 60 | // However, it also displays an error on Monobehaviour m_Script property, since it can't be driven. (not wanted behaviour) 61 | // case 967026 62 | if (name == "m_Script" && (comp as MonoBehaviour) != null) 63 | return; 64 | 65 | // Deviate from Unity's PropertyCollector.cs:AddPropertyModification API that doesn't display errors instead: 66 | // DrivenPropertyManager.RegisterProperty(driver, comp, name); 67 | TryPropertyModification(driver, comp, name); // silent registration 68 | } 69 | 70 | 71 | // Duplicate working version of Unity's PropertyCollector.cs:AddFromComponent API. 72 | public void AddFromComponent(GameObject obj, Component component) 73 | { 74 | if (Application.isPlaying) 75 | return; 76 | 77 | if (obj == null || component == null) 78 | return; 79 | 80 | var serializedObject = new SerializedObject(component); 81 | SerializedProperty property = serializedObject.GetIterator(); 82 | 83 | // Deviate from Unity's PropertyCollector.cs:AddFromComponent API because we want all children to be recorded: 84 | //while (property.NextVisible(true)) 85 | while (property.Next(true)) 86 | { 87 | // Deviate from Unity's PropertyCollector.cs:AddFromComponent API, because we want to register animatable types: 88 | //if (property.hasVisibleChildren || !AnimatedParameterUtility.IsTypeAnimatable(property.propertyType)) 89 | if (property.hasVisibleChildren) 90 | continue; 91 | 92 | AddPropertyModification(component, property.propertyPath); 93 | } 94 | } 95 | 96 | public override void GatherProperties(PlayableDirector director, IPropertyCollector driver) 97 | { 98 | // Grab game object from director 99 | var binding = director.GetGenericBinding(this); 100 | var gameObject = binding as GameObject; 101 | 102 | // Iterate each component of game object 103 | if (gameObject != null) 104 | foreach (var component in gameObject.GetComponentsInChildren()) 105 | AddFromComponent(gameObject, component); 106 | } 107 | #endif 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /EventMarkerTrack.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b39a5a40254442a42bcb3e3d450f4a07 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 27d2bb10d9f69a04c94ff7238722f1ac 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UnityTimelineTools 2 | Unity Plugin that adds the following features to timeline: 3 | 1. Frame Cutting and Insertion 4 | 2. Timeline and Animation View Synchronization 5 | 3. Multi-parameter Bound Object Method Calls 6 | 7 | ## Frame Cutting and Insertion 8 | Cut and insert frames to the overall timeline including infinite animation tracks. 9 | 10 | ![Cut_Insert](https://user-images.githubusercontent.com/5836001/201495143-d20c75a5-f624-423d-ad19-ea1ff815737f.png) 11 | 12 | ## Timeline and Animation View Synchronization 13 | Synchronize timeline and animation views to the same timescale and location. 14 | 15 | ![Sync](https://user-images.githubusercontent.com/5836001/201495192-92c6ec90-ceea-4286-b5b7-e44ece2deec1.png)\ 16 | ![Sync_Views](https://user-images.githubusercontent.com/5836001/201495269-548744c5-48a2-4cae-9329-48e8e9d57038.png) 17 | 18 | 19 | ## Bound Object Method Calls 20 | Call methods on timeline bound objects by adding an Event Marker Receiver to the given object and adding Event Markers to its timeline track. 21 | 22 | ### Features 23 | 1. *bool*, *int*, *float*, *string*, *enum*, and *Object* parameter types 24 | 2. Method overloading 25 | 3. Multiple arguments 26 | 4. Enumerations 27 | 28 | 29 | ### Usage Images 30 | * Adding the Event Marker Receiver script to a game object\ 31 | ![Adding the Event Marker Receiver script to a game object](https://user-images.githubusercontent.com/5836001/201498890-5e60b80e-2def-4b1e-9efc-6e0cc79db133.png) ![Add_Component2](https://user-images.githubusercontent.com/5836001/201498943-347b6eb5-f334-4438-a7ae-666169b9d06e.png) 32 | 33 | * Adding an Event Marker to a timeline\ 34 | ![Adding an Event Marker to a timeline](https://user-images.githubusercontent.com/5836001/201495690-400274e9-e06a-4404-a0a9-09f79a9c24c6.png) 35 | 36 | * Viewing the Event Marker inspector\ 37 | ![Viewing the Event Marker inspector](https://user-images.githubusercontent.com/5836001/201495423-e914b968-4838-4402-98a2-3b3858f3aa12.png) 38 | 39 | * Selecting an object method\ 40 | ![Selecting an object method](https://user-images.githubusercontent.com/5836001/202037104-1f874b59-aacf-4fac-afc9-30eac99aa009.png) 41 | 42 | * Using a multiple argument method\ 43 | ![Using a multiple argument method](https://user-images.githubusercontent.com/5836001/202037734-154b3e48-f6b8-4979-8e96-84b6b15d5072.png) 44 | 45 | * Viewing Overloaded methods\ 46 | ![Viewing Overloaded methods](https://user-images.githubusercontent.com/5836001/202051465-1775f82e-982a-453a-8cac-aeb7dd6c55f9.png) 47 | 48 | * Selecting an enumeration\ 49 | ![Selecting an enumeration](https://user-images.githubusercontent.com/5836001/202051157-afdfd86b-9123-49b7-b72d-ca0751cdcaa9.png) 50 | 51 | * Viewing an Event Marker tooltip\ 52 | ![Viewing an Event Marker Tooltip](https://user-images.githubusercontent.com/5836001/201495376-3a3cb844-2910-4215-afd8-ed3f3b1e3f79.png) 53 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f5da7ef3e4a38174e88bf50c033e5440 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Resources.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ba556c9a5ab75094da10300617f5b759 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Resources/EventMarker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaphat/UnityTimelineTools/9510e34609d193a7f80827d1890856c216c23135/Resources/EventMarker.png -------------------------------------------------------------------------------- /Resources/EventMarker.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ec630f19b3496d649bcc34fea26472e3 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 12 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 0 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | flipGreenChannel: 0 24 | isReadable: 0 25 | streamingMipmaps: 0 26 | streamingMipmapsPriority: 0 27 | vTOnly: 0 28 | ignoreMipmapLimit: 0 29 | grayScaleToAlpha: 0 30 | generateCubemap: 6 31 | cubemapConvolution: 0 32 | seamlessCubemap: 0 33 | textureFormat: 1 34 | maxTextureSize: 2048 35 | textureSettings: 36 | serializedVersion: 2 37 | filterMode: 1 38 | aniso: 1 39 | mipBias: 0 40 | wrapU: 1 41 | wrapV: 1 42 | wrapW: 1 43 | nPOTScale: 0 44 | lightmap: 0 45 | compressionQuality: 50 46 | spriteMode: 1 47 | spriteExtrude: 1 48 | spriteMeshType: 1 49 | alignment: 0 50 | spritePivot: {x: 0.5, y: 0.5} 51 | spritePixelsToUnits: 100 52 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 53 | spriteGenerateFallbackPhysicsShape: 1 54 | alphaUsage: 1 55 | alphaIsTransparency: 1 56 | spriteTessellationDetail: -1 57 | textureType: 8 58 | textureShape: 1 59 | singleChannelComponent: 0 60 | flipbookRows: 1 61 | flipbookColumns: 1 62 | maxTextureSizeSet: 0 63 | compressionQualitySet: 0 64 | textureFormatSet: 0 65 | ignorePngGamma: 0 66 | applyGammaDecoding: 0 67 | swizzle: 50462976 68 | cookieLightType: 0 69 | platformSettings: 70 | - serializedVersion: 3 71 | buildTarget: DefaultTexturePlatform 72 | maxTextureSize: 2048 73 | resizeAlgorithm: 0 74 | textureFormat: -1 75 | textureCompression: 1 76 | compressionQuality: 50 77 | crunchedCompression: 0 78 | allowsAlphaSplitting: 0 79 | overridden: 0 80 | androidETC2FallbackOverride: 0 81 | forceMaximumCompressionQuality_BC6H_BC7: 0 82 | - serializedVersion: 3 83 | buildTarget: Standalone 84 | maxTextureSize: 2048 85 | resizeAlgorithm: 0 86 | textureFormat: -1 87 | textureCompression: 1 88 | compressionQuality: 50 89 | crunchedCompression: 0 90 | allowsAlphaSplitting: 0 91 | overridden: 0 92 | androidETC2FallbackOverride: 0 93 | forceMaximumCompressionQuality_BC6H_BC7: 0 94 | - serializedVersion: 3 95 | buildTarget: Server 96 | maxTextureSize: 2048 97 | resizeAlgorithm: 0 98 | textureFormat: -1 99 | textureCompression: 1 100 | compressionQuality: 50 101 | crunchedCompression: 0 102 | allowsAlphaSplitting: 0 103 | overridden: 0 104 | androidETC2FallbackOverride: 0 105 | forceMaximumCompressionQuality_BC6H_BC7: 0 106 | spriteSheet: 107 | serializedVersion: 2 108 | sprites: [] 109 | outline: [] 110 | physicsShape: [] 111 | bones: [] 112 | spriteID: 5e97eb03825dee720800000000000000 113 | internalID: 0 114 | vertices: [] 115 | indices: 116 | edges: [] 117 | weights: [] 118 | secondaryTextures: [] 119 | nameFileIdTable: {} 120 | mipmapLimitGroupName: 121 | pSDRemoveMatte: 0 122 | userData: 123 | assetBundleName: 124 | assetBundleVariant: 125 | -------------------------------------------------------------------------------- /Resources/EventMarker.svg: -------------------------------------------------------------------------------- 1 | 2 | 135 | 138 | 141 | 144 | 147 | 150 | 153 | 156 | 159 | 162 | 165 | 168 | 171 | 174 | 177 | </> 189 | -------------------------------------------------------------------------------- /Resources/EventMarker.svg.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1ca95be14c2b90445829cb443cc71fab 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Resources/EventMarkerIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaphat/UnityTimelineTools/9510e34609d193a7f80827d1890856c216c23135/Resources/EventMarkerIcon.png -------------------------------------------------------------------------------- /Resources/EventMarkerIcon.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2e7da690a91425640b75c328929f5897 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 12 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 0 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | flipGreenChannel: 0 24 | isReadable: 0 25 | streamingMipmaps: 0 26 | streamingMipmapsPriority: 0 27 | vTOnly: 0 28 | ignoreMipmapLimit: 0 29 | grayScaleToAlpha: 0 30 | generateCubemap: 6 31 | cubemapConvolution: 0 32 | seamlessCubemap: 0 33 | textureFormat: 1 34 | maxTextureSize: 2048 35 | textureSettings: 36 | serializedVersion: 2 37 | filterMode: 1 38 | aniso: 1 39 | mipBias: 0 40 | wrapU: 1 41 | wrapV: 1 42 | wrapW: 1 43 | nPOTScale: 0 44 | lightmap: 0 45 | compressionQuality: 50 46 | spriteMode: 1 47 | spriteExtrude: 1 48 | spriteMeshType: 1 49 | alignment: 0 50 | spritePivot: {x: 0.5, y: 0.5} 51 | spritePixelsToUnits: 100 52 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 53 | spriteGenerateFallbackPhysicsShape: 1 54 | alphaUsage: 1 55 | alphaIsTransparency: 1 56 | spriteTessellationDetail: -1 57 | textureType: 8 58 | textureShape: 1 59 | singleChannelComponent: 0 60 | flipbookRows: 1 61 | flipbookColumns: 1 62 | maxTextureSizeSet: 0 63 | compressionQualitySet: 0 64 | textureFormatSet: 0 65 | ignorePngGamma: 0 66 | applyGammaDecoding: 0 67 | swizzle: 50462976 68 | cookieLightType: 0 69 | platformSettings: 70 | - serializedVersion: 3 71 | buildTarget: DefaultTexturePlatform 72 | maxTextureSize: 2048 73 | resizeAlgorithm: 0 74 | textureFormat: -1 75 | textureCompression: 1 76 | compressionQuality: 50 77 | crunchedCompression: 0 78 | allowsAlphaSplitting: 0 79 | overridden: 0 80 | androidETC2FallbackOverride: 0 81 | forceMaximumCompressionQuality_BC6H_BC7: 0 82 | - serializedVersion: 3 83 | buildTarget: Standalone 84 | maxTextureSize: 2048 85 | resizeAlgorithm: 0 86 | textureFormat: -1 87 | textureCompression: 1 88 | compressionQuality: 50 89 | crunchedCompression: 0 90 | allowsAlphaSplitting: 0 91 | overridden: 0 92 | androidETC2FallbackOverride: 0 93 | forceMaximumCompressionQuality_BC6H_BC7: 0 94 | - serializedVersion: 3 95 | buildTarget: Server 96 | maxTextureSize: 2048 97 | resizeAlgorithm: 0 98 | textureFormat: -1 99 | textureCompression: 1 100 | compressionQuality: 50 101 | crunchedCompression: 0 102 | allowsAlphaSplitting: 0 103 | overridden: 0 104 | androidETC2FallbackOverride: 0 105 | forceMaximumCompressionQuality_BC6H_BC7: 0 106 | spriteSheet: 107 | serializedVersion: 2 108 | sprites: [] 109 | outline: [] 110 | physicsShape: [] 111 | bones: [] 112 | spriteID: 5e97eb03825dee720800000000000000 113 | internalID: 0 114 | vertices: [] 115 | indices: 116 | edges: [] 117 | weights: [] 118 | secondaryTextures: [] 119 | nameFileIdTable: {} 120 | mipmapLimitGroupName: 121 | pSDRemoveMatte: 0 122 | userData: 123 | assetBundleName: 124 | assetBundleVariant: 125 | -------------------------------------------------------------------------------- /Resources/EventMarkerIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 187 | 190 | 193 | 196 | 199 | 202 | 205 | 208 | 211 | 214 | 217 | 220 | 223 | 226 | 229 | </> 241 | -------------------------------------------------------------------------------- /Resources/EventMarkerIcon.svg.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1e23e9d9b2d8c1e49bf6e6de6254e5ea 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Resources/EventMarker_Collapsed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaphat/UnityTimelineTools/9510e34609d193a7f80827d1890856c216c23135/Resources/EventMarker_Collapsed.png -------------------------------------------------------------------------------- /Resources/EventMarker_Collapsed.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f3af61869f91bec46a4ae5b877eeeb3a 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 12 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 0 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | flipGreenChannel: 0 24 | isReadable: 0 25 | streamingMipmaps: 0 26 | streamingMipmapsPriority: 0 27 | vTOnly: 0 28 | ignoreMipmapLimit: 0 29 | grayScaleToAlpha: 0 30 | generateCubemap: 6 31 | cubemapConvolution: 0 32 | seamlessCubemap: 0 33 | textureFormat: 1 34 | maxTextureSize: 2048 35 | textureSettings: 36 | serializedVersion: 2 37 | filterMode: 1 38 | aniso: 1 39 | mipBias: 0 40 | wrapU: 1 41 | wrapV: 1 42 | wrapW: 1 43 | nPOTScale: 0 44 | lightmap: 0 45 | compressionQuality: 50 46 | spriteMode: 1 47 | spriteExtrude: 1 48 | spriteMeshType: 1 49 | alignment: 0 50 | spritePivot: {x: 0.5, y: 0.5} 51 | spritePixelsToUnits: 100 52 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 53 | spriteGenerateFallbackPhysicsShape: 1 54 | alphaUsage: 1 55 | alphaIsTransparency: 1 56 | spriteTessellationDetail: -1 57 | textureType: 8 58 | textureShape: 1 59 | singleChannelComponent: 0 60 | flipbookRows: 1 61 | flipbookColumns: 1 62 | maxTextureSizeSet: 0 63 | compressionQualitySet: 0 64 | textureFormatSet: 0 65 | ignorePngGamma: 0 66 | applyGammaDecoding: 0 67 | swizzle: 50462976 68 | cookieLightType: 0 69 | platformSettings: 70 | - serializedVersion: 3 71 | buildTarget: DefaultTexturePlatform 72 | maxTextureSize: 2048 73 | resizeAlgorithm: 0 74 | textureFormat: -1 75 | textureCompression: 1 76 | compressionQuality: 50 77 | crunchedCompression: 0 78 | allowsAlphaSplitting: 0 79 | overridden: 0 80 | androidETC2FallbackOverride: 0 81 | forceMaximumCompressionQuality_BC6H_BC7: 0 82 | - serializedVersion: 3 83 | buildTarget: Standalone 84 | maxTextureSize: 2048 85 | resizeAlgorithm: 0 86 | textureFormat: -1 87 | textureCompression: 1 88 | compressionQuality: 50 89 | crunchedCompression: 0 90 | allowsAlphaSplitting: 0 91 | overridden: 0 92 | androidETC2FallbackOverride: 0 93 | forceMaximumCompressionQuality_BC6H_BC7: 0 94 | - serializedVersion: 3 95 | buildTarget: Server 96 | maxTextureSize: 2048 97 | resizeAlgorithm: 0 98 | textureFormat: -1 99 | textureCompression: 1 100 | compressionQuality: 50 101 | crunchedCompression: 0 102 | allowsAlphaSplitting: 0 103 | overridden: 0 104 | androidETC2FallbackOverride: 0 105 | forceMaximumCompressionQuality_BC6H_BC7: 0 106 | spriteSheet: 107 | serializedVersion: 2 108 | sprites: [] 109 | outline: [] 110 | physicsShape: [] 111 | bones: [] 112 | spriteID: 5e97eb03825dee720800000000000000 113 | internalID: 0 114 | vertices: [] 115 | indices: 116 | edges: [] 117 | weights: [] 118 | secondaryTextures: [] 119 | nameFileIdTable: {} 120 | mipmapLimitGroupName: 121 | pSDRemoveMatte: 0 122 | userData: 123 | assetBundleName: 124 | assetBundleVariant: 125 | -------------------------------------------------------------------------------- /Resources/EventMarker_Collapsed.svg: -------------------------------------------------------------------------------- 1 | 2 | 117 | 120 | 123 | 126 | 129 | 132 | 135 | 138 | 141 | 144 | 147 | 150 | 153 | 156 | 159 | 178 | -------------------------------------------------------------------------------- /Resources/EventMarker_Collapsed.svg.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 88c596e155df15745bc24090d5bd26c4 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Resources/EventMarker_Selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaphat/UnityTimelineTools/9510e34609d193a7f80827d1890856c216c23135/Resources/EventMarker_Selected.png -------------------------------------------------------------------------------- /Resources/EventMarker_Selected.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8382328e8e458aa46a9a915a8e7f1a0b 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 12 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 0 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | flipGreenChannel: 0 24 | isReadable: 0 25 | streamingMipmaps: 0 26 | streamingMipmapsPriority: 0 27 | vTOnly: 0 28 | ignoreMipmapLimit: 0 29 | grayScaleToAlpha: 0 30 | generateCubemap: 6 31 | cubemapConvolution: 0 32 | seamlessCubemap: 0 33 | textureFormat: 1 34 | maxTextureSize: 2048 35 | textureSettings: 36 | serializedVersion: 2 37 | filterMode: 1 38 | aniso: 1 39 | mipBias: 0 40 | wrapU: 1 41 | wrapV: 1 42 | wrapW: 1 43 | nPOTScale: 0 44 | lightmap: 0 45 | compressionQuality: 50 46 | spriteMode: 1 47 | spriteExtrude: 1 48 | spriteMeshType: 1 49 | alignment: 0 50 | spritePivot: {x: 0.5, y: 0.5} 51 | spritePixelsToUnits: 100 52 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 53 | spriteGenerateFallbackPhysicsShape: 1 54 | alphaUsage: 1 55 | alphaIsTransparency: 1 56 | spriteTessellationDetail: -1 57 | textureType: 8 58 | textureShape: 1 59 | singleChannelComponent: 0 60 | flipbookRows: 1 61 | flipbookColumns: 1 62 | maxTextureSizeSet: 0 63 | compressionQualitySet: 0 64 | textureFormatSet: 0 65 | ignorePngGamma: 0 66 | applyGammaDecoding: 0 67 | swizzle: 50462976 68 | cookieLightType: 0 69 | platformSettings: 70 | - serializedVersion: 3 71 | buildTarget: DefaultTexturePlatform 72 | maxTextureSize: 2048 73 | resizeAlgorithm: 0 74 | textureFormat: -1 75 | textureCompression: 1 76 | compressionQuality: 50 77 | crunchedCompression: 0 78 | allowsAlphaSplitting: 0 79 | overridden: 0 80 | androidETC2FallbackOverride: 0 81 | forceMaximumCompressionQuality_BC6H_BC7: 0 82 | - serializedVersion: 3 83 | buildTarget: Standalone 84 | maxTextureSize: 2048 85 | resizeAlgorithm: 0 86 | textureFormat: -1 87 | textureCompression: 1 88 | compressionQuality: 50 89 | crunchedCompression: 0 90 | allowsAlphaSplitting: 0 91 | overridden: 0 92 | androidETC2FallbackOverride: 0 93 | forceMaximumCompressionQuality_BC6H_BC7: 0 94 | - serializedVersion: 3 95 | buildTarget: Server 96 | maxTextureSize: 2048 97 | resizeAlgorithm: 0 98 | textureFormat: -1 99 | textureCompression: 1 100 | compressionQuality: 50 101 | crunchedCompression: 0 102 | allowsAlphaSplitting: 0 103 | overridden: 0 104 | androidETC2FallbackOverride: 0 105 | forceMaximumCompressionQuality_BC6H_BC7: 0 106 | spriteSheet: 107 | serializedVersion: 2 108 | sprites: [] 109 | outline: [] 110 | physicsShape: [] 111 | bones: [] 112 | spriteID: 5e97eb03825dee720800000000000000 113 | internalID: 0 114 | vertices: [] 115 | indices: 116 | edges: [] 117 | weights: [] 118 | secondaryTextures: [] 119 | nameFileIdTable: {} 120 | mipmapLimitGroupName: 121 | pSDRemoveMatte: 0 122 | userData: 123 | assetBundleName: 124 | assetBundleVariant: 125 | -------------------------------------------------------------------------------- /Resources/EventMarker_Selected.svg: -------------------------------------------------------------------------------- 1 | 2 | 124 | 127 | 130 | 133 | 136 | 139 | 142 | 145 | 148 | 151 | 154 | 157 | 160 | 163 | 166 | 167 | -------------------------------------------------------------------------------- /Resources/EventMarker_Selected.svg.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1a557c8b97b529848b44a2e325f485e7 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Stylesheets.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e13d4ae3eee9bc547aa3d1643bfbbd37 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Stylesheets/Extensions.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5f6b0878ac8f9ac42a3ef56a6173c9e7 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Stylesheets/Extensions/common.uss: -------------------------------------------------------------------------------- 1 | 2 | /* Custom USS stylesheet. */ 3 | 4 | .EventMarkerStyle 5 | { 6 | width:18px; 7 | height:18px; 8 | } 9 | 10 | /* A marker will use the default style when it is collapsed.*/ 11 | .EventMarkerStyle 12 | { 13 | background-image: resource("EventMarker_Collapsed"); 14 | } 15 | 16 | /* A marker will use the hover:focus:checked pseudo-state when it is selected.*/ 17 | .EventMarkerStyle:hover:checked 18 | { 19 | background-image: resource("EventMarker"); 20 | } 21 | 22 | /* A marker will use this style when it is not selected and not collapsed.*/ 23 | .EventMarkerStyle:checked 24 | { 25 | background-image: resource("EventMarker"); 26 | } 27 | -------------------------------------------------------------------------------- /Stylesheets/Extensions/common.uss.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c71b44fd3e8e7f94b943d6e2ffd0cfa5 3 | ScriptedImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 2 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} 11 | disableValidation: 0 12 | -------------------------------------------------------------------------------- /TimelineTools.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using UnityEngine; 7 | using UnityEngine.Playables; 8 | using UnityEngine.Timeline; 9 | using Object = UnityEngine.Object; 10 | 11 | #if UNITY_EDITOR 12 | using UnityEditor; 13 | using UnityEditor.Timeline; 14 | using UnityEditor.Timeline.Actions; 15 | using UnityEditorInternal; 16 | // Adds support for inserting and cutting frames in all tracks and infinite clips within the timeline. 17 | namespace TimelineTools 18 | { 19 | class InsertCutExtensions 20 | { 21 | // Menu entries 22 | const string MenuPath_Insert_1 = "Tools/Timeline/Insert Frames/1 Frame"; 23 | const string MenuPath_Insert_5 = "Tools/Timeline/Insert Frames/5 Frames"; 24 | const string MenuPath_Insert_10 = "Tools/Timeline/Insert Frames/10 Frames"; 25 | const string MenuPath_Insert_15 = "Tools/Timeline/Insert Frames/15 Frames"; 26 | const string MenuPath_Insert_20 = "Tools/Timeline/Insert Frames/20 Frames"; 27 | const string MenuPath_Insert_25 = "Tools/Timeline/Insert Frames/25 Frames"; 28 | const string MenuPath_Insert_30 = "Tools/Timeline/Insert Frames/30 Frames"; 29 | const string MenuPath_Insert_50 = "Tools/Timeline/Insert Frames/50 Frames"; 30 | const string MenuPath_Insert_60 = "Tools/Timeline/Insert Frames/60 Frames"; 31 | const string MenuPath_Insert_100 = "Tools/Timeline/Insert Frames/100 Frames"; 32 | const string MenuPath_Insert_120 = "Tools/Timeline/Insert Frames/120 Frames"; 33 | const string MenuPath_Insert_180 = "Tools/Timeline/Insert Frames/180 Frames"; 34 | const string MenuPath_Insert_200 = "Tools/Timeline/Insert Frames/200 Frames"; 35 | const string MenuPath_Insert_240 = "Tools/Timeline/Insert Frames/240 Frames"; 36 | const string MenuPath_Insert_300 = "Tools/Timeline/Insert Frames/300 Frames"; 37 | const string MenuPath_Insert_360 = "Tools/Timeline/Insert Frames/360 Frames"; 38 | const string MenuPath_Insert_400 = "Tools/Timeline/Insert Frames/400 Frames"; 39 | const string MenuPath_Insert_420 = "Tools/Timeline/Insert Frames/420 Frames"; 40 | const string MenuPath_Insert_480 = "Tools/Timeline/Insert Frames/480 Frames"; 41 | const string MenuPath_Insert_500 = "Tools/Timeline/Insert Frames/500 Frames"; 42 | const string MenuPath_Insert_1000 = "Tools/Timeline/Insert Frames/1000 Frames"; 43 | const string MenuPath_Cut_1 = "Tools/Timeline/Cut Frames/1 Frame"; 44 | const string MenuPath_Cut_5 = "Tools/Timeline/Cut Frames/5 Frames"; 45 | const string MenuPath_Cut_10 = "Tools/Timeline/Cut Frames/10 Frames"; 46 | const string MenuPath_Cut_15 = "Tools/Timeline/Cut Frames/15 Frames"; 47 | const string MenuPath_Cut_20 = "Tools/Timeline/Cut Frames/20 Frames"; 48 | const string MenuPath_Cut_25 = "Tools/Timeline/Cut Frames/25 Frames"; 49 | const string MenuPath_Cut_30 = "Tools/Timeline/Cut Frames/30 Frames"; 50 | const string MenuPath_Cut_50 = "Tools/Timeline/Cut Frames/50 Frames"; 51 | const string MenuPath_Cut_60 = "Tools/Timeline/Cut Frames/60 Frames"; 52 | const string MenuPath_Cut_100 = "Tools/Timeline/Cut Frames/100 Frames"; 53 | const string MenuPath_Cut_120 = "Tools/Timeline/Cut Frames/120 Frames"; 54 | const string MenuPath_Cut_180 = "Tools/Timeline/Cut Frames/180 Frames"; 55 | const string MenuPath_Cut_200 = "Tools/Timeline/Cut Frames/200 Frames"; 56 | const string MenuPath_Cut_240 = "Tools/Timeline/Cut Frames/240 Frames"; 57 | const string MenuPath_Cut_300 = "Tools/Timeline/Cut Frames/300 Frames"; 58 | const string MenuPath_Cut_360 = "Tools/Timeline/Cut Frames/360 Frames"; 59 | const string MenuPath_Cut_400 = "Tools/Timeline/Cut Frames/400 Frames"; 60 | const string MenuPath_Cut_420 = "Tools/Timeline/Cut Frames/420 Frames"; 61 | const string MenuPath_Cut_480 = "Tools/Timeline/Cut Frames/480 Frames"; 62 | const string MenuPath_Cut_500 = "Tools/Timeline/Cut Frames/500 Frames"; 63 | const string MenuPath_Cut_1000 = "Tools/Timeline/Cut Frames/1000 Frames"; 64 | 65 | // Class to specify the number of frames to insert/cut 66 | public class Frames : Attribute 67 | { 68 | public float frames; 69 | public Frames(float frames) { this.frames = frames; } 70 | } 71 | 72 | // Class to call insert/cut method 73 | abstract class Action : TimelineAction 74 | { 75 | public override ActionValidity Validate(ActionContext actionContext) { return ActionValidity.Valid; } 76 | public override bool Execute(ActionContext actionContext) 77 | { Insert(); return true; } 78 | public static void Insert() { InsertCutExtensions.Insert(((Frames)Attribute.GetCustomAttribute(typeof(T), typeof(Frames))).frames); } 79 | }; 80 | 81 | // Add insert menu items 82 | [Frames(1)][MenuEntry(MenuPath_Insert_1, 0)] class Insert_1 : Action { [MenuItem(MenuPath_Insert_1, priority = 0)] public static void F() { Insert(); } }; 83 | [Frames(5)][MenuEntry(MenuPath_Insert_5, 1)] class Insert_5 : Action { [MenuItem(MenuPath_Insert_5, priority = 1)] public static void F() { Insert(); } }; 84 | [Frames(10)][MenuEntry(MenuPath_Insert_10, 2)] class Insert_10 : Action { [MenuItem(MenuPath_Insert_10, priority = 2)] public static void F() { Insert(); } }; 85 | [Frames(15)][MenuEntry(MenuPath_Insert_15, 3)] class Insert_15 : Action { [MenuItem(MenuPath_Insert_15, priority = 3)] public static void F() { Insert(); } }; 86 | [Frames(20)][MenuEntry(MenuPath_Insert_20, 4)] class Insert_20 : Action { [MenuItem(MenuPath_Insert_20, priority = 4)] public static void F() { Insert(); } }; 87 | [Frames(25)][MenuEntry(MenuPath_Insert_25, 5)] class Insert_25 : Action { [MenuItem(MenuPath_Insert_25, priority = 5)] public static void F() { Insert(); } }; 88 | [Frames(30)][MenuEntry(MenuPath_Insert_30, 6)] class Insert_30 : Action { [MenuItem(MenuPath_Insert_30, priority = 6)] public static void F() { Insert(); } }; 89 | [Frames(50)][MenuEntry(MenuPath_Insert_50, 7)] class Insert_50 : Action { [MenuItem(MenuPath_Insert_50, priority = 7)] public static void F() { Insert(); } }; 90 | [Frames(60)][MenuEntry(MenuPath_Insert_60, 8)] class Insert_60 : Action { [MenuItem(MenuPath_Insert_60, priority = 8)] public static void F() { Insert(); } }; 91 | [Frames(100)][MenuEntry(MenuPath_Insert_100, 9)] class Insert_100 : Action { [MenuItem(MenuPath_Insert_100, priority = 9)] public static void F() { Insert(); } }; 92 | [Frames(120)][MenuEntry(MenuPath_Insert_120, 10)] class Insert_120 : Action { [MenuItem(MenuPath_Insert_120, priority = 10)] public static void F() { Insert(); } }; 93 | [Frames(180)][MenuEntry(MenuPath_Insert_180, 11)] class Insert_180 : Action { [MenuItem(MenuPath_Insert_180, priority = 11)] public static void F() { Insert(); } }; 94 | [Frames(200)][MenuEntry(MenuPath_Insert_200, 12)] class Insert_200 : Action { [MenuItem(MenuPath_Insert_200, priority = 12)] public static void F() { Insert(); } }; 95 | [Frames(240)][MenuEntry(MenuPath_Insert_240, 13)] class Insert_240 : Action { [MenuItem(MenuPath_Insert_240, priority = 13)] public static void F() { Insert(); } }; 96 | [Frames(300)][MenuEntry(MenuPath_Insert_300, 14)] class Insert_300 : Action { [MenuItem(MenuPath_Insert_300, priority = 14)] public static void F() { Insert(); } }; 97 | [Frames(360)][MenuEntry(MenuPath_Insert_360, 15)] class Insert_360 : Action { [MenuItem(MenuPath_Insert_360, priority = 15)] public static void F() { Insert(); } }; 98 | [Frames(400)][MenuEntry(MenuPath_Insert_400, 16)] class Insert_400 : Action { [MenuItem(MenuPath_Insert_400, priority = 16)] public static void F() { Insert(); } }; 99 | [Frames(420)][MenuEntry(MenuPath_Insert_420, 17)] class Insert_420 : Action { [MenuItem(MenuPath_Insert_420, priority = 17)] public static void F() { Insert(); } }; 100 | [Frames(480)][MenuEntry(MenuPath_Insert_480, 18)] class Insert_480 : Action { [MenuItem(MenuPath_Insert_480, priority = 18)] public static void F() { Insert(); } }; 101 | [Frames(500)][MenuEntry(MenuPath_Insert_500, 19)] class Insert_500 : Action { [MenuItem(MenuPath_Insert_500, priority = 19)] public static void F() { Insert(); } }; 102 | [Frames(1000)][MenuEntry(MenuPath_Insert_1000, 20)] class Insert_1000 : Action { [MenuItem(MenuPath_Insert_1000, priority = 20)] public static void F() { Insert(); } }; 103 | 104 | // Add cut menu items 105 | [Frames(-1)][MenuEntry(MenuPath_Cut_1, 0)] class Cut_1 : Action { [MenuItem(MenuPath_Cut_1, priority = 0)] public static void F() { Insert(); } }; 106 | [Frames(-5)][MenuEntry(MenuPath_Cut_5, 1)] class Cut_5 : Action { [MenuItem(MenuPath_Cut_5, priority = 1)] public static void F() { Insert(); } }; 107 | [Frames(-10)][MenuEntry(MenuPath_Cut_10, 2)] class Cut_10 : Action { [MenuItem(MenuPath_Cut_10, priority = 2)] public static void F() { Insert(); } }; 108 | [Frames(-15)][MenuEntry(MenuPath_Cut_15, 3)] class Cut_15 : Action { [MenuItem(MenuPath_Cut_15, priority = 3)] public static void F() { Insert(); } }; 109 | [Frames(-20)][MenuEntry(MenuPath_Cut_20, 4)] class Cut_20 : Action { [MenuItem(MenuPath_Cut_20, priority = 4)] public static void F() { Insert(); } }; 110 | [Frames(-25)][MenuEntry(MenuPath_Cut_25, 5)] class Cut_25 : Action { [MenuItem(MenuPath_Cut_25, priority = 5)] public static void F() { Insert(); } }; 111 | [Frames(-30)][MenuEntry(MenuPath_Cut_30, 6)] class Cut_30 : Action { [MenuItem(MenuPath_Cut_30, priority = 6)] public static void F() { Insert(); } }; 112 | [Frames(-50)][MenuEntry(MenuPath_Cut_50, 7)] class Cut_50 : Action { [MenuItem(MenuPath_Cut_50, priority = 7)] public static void F() { Insert(); } }; 113 | [Frames(-60)][MenuEntry(MenuPath_Cut_60, 8)] class Cut_60 : Action { [MenuItem(MenuPath_Cut_60, priority = 8)] public static void F() { Insert(); } }; 114 | [Frames(-100)][MenuEntry(MenuPath_Cut_100, 9)] class Cut_100 : Action { [MenuItem(MenuPath_Cut_100, priority = 9)] public static void F() { Insert(); } }; 115 | [Frames(-120)][MenuEntry(MenuPath_Cut_120, 10)] class Cut_120 : Action { [MenuItem(MenuPath_Cut_120, priority = 10)] public static void F() { Insert(); } }; 116 | [Frames(-180)][MenuEntry(MenuPath_Cut_180, 11)] class Cut_180 : Action { [MenuItem(MenuPath_Cut_180, priority = 11)] public static void F() { Insert(); } }; 117 | [Frames(-200)][MenuEntry(MenuPath_Cut_200, 12)] class Cut_200 : Action { [MenuItem(MenuPath_Cut_200, priority = 12)] public static void F() { Insert(); } }; 118 | [Frames(-240)][MenuEntry(MenuPath_Cut_240, 13)] class Cut_240 : Action { [MenuItem(MenuPath_Cut_240, priority = 13)] public static void F() { Insert(); } }; 119 | [Frames(-300)][MenuEntry(MenuPath_Cut_300, 14)] class Cut_300 : Action { [MenuItem(MenuPath_Cut_300, priority = 14)] public static void F() { Insert(); } }; 120 | [Frames(-360)][MenuEntry(MenuPath_Cut_360, 15)] class Cut_360 : Action { [MenuItem(MenuPath_Cut_360, priority = 15)] public static void F() { Insert(); } }; 121 | [Frames(-400)][MenuEntry(MenuPath_Cut_400, 16)] class Cut_400 : Action { [MenuItem(MenuPath_Cut_400, priority = 16)] public static void F() { Insert(); } }; 122 | [Frames(-420)][MenuEntry(MenuPath_Cut_420, 17)] class Cut_420 : Action { [MenuItem(MenuPath_Cut_420, priority = 17)] public static void F() { Insert(); } }; 123 | [Frames(-480)][MenuEntry(MenuPath_Cut_480, 18)] class Cut_480 : Action { [MenuItem(MenuPath_Cut_480, priority = 18)] public static void F() { Insert(); } }; 124 | [Frames(-500)][MenuEntry(MenuPath_Cut_500, 19)] class Cut_500 : Action { [MenuItem(MenuPath_Cut_500, priority = 19)] public static void F() { Insert(); } }; 125 | [Frames(-1000)][MenuEntry(MenuPath_Cut_1000, 20)] class Cut_1000 : Action { [MenuItem(MenuPath_Cut_1000, priority = 20)] public static void F() { Insert(); } }; 126 | 127 | // Undo group name 128 | static readonly string undoKey = "Insert Frames"; 129 | 130 | private static void Insert(float frames) 131 | { 132 | // Grab timeline asset 133 | var timelineAsset = TimelineEditor.inspectedAsset; 134 | if (timelineAsset == null) return; 135 | 136 | // Grab playable director 137 | var playableDirector = TimelineEditor.inspectedDirector; 138 | if (playableDirector == null) return; 139 | 140 | // Register undo for any changes directly to timeline asset 141 | UndoExtensions.RegisterCompleteTimeline(timelineAsset, undoKey); 142 | 143 | // Grab the current time of the playhead 144 | var currentTime = playableDirector.time; 145 | 146 | // filter only unlocked tracks 147 | var unlockedTracks = timelineAsset.GetOutputTracks().Where(e => !e.lockedInHierarchy); 148 | 149 | // Compute tolerance for determining whether to shift a track or not 150 | double kTimeEpsilon = 1e-14; // from com.unity.timeline/Runtime/Utilities/TimeUtility.cs 151 | var tolerance = Math.Max(Math.Abs(currentTime), 1) * timelineAsset.editorSettings.frameRate * kTimeEpsilon; 152 | 153 | // Handle infinite animation clips (really tracks) 154 | // filter infinite animation tracks 155 | var infiniteTracks = unlockedTracks.OfType().Where(e => !e.inClipMode && e.infiniteClip != null); 156 | foreach (var track in infiniteTracks) 157 | { 158 | // Grab the infinite clip in track 159 | var clip = track.infiniteClip; 160 | 161 | // Register undo for any changes in infinite clip 162 | Undo.RegisterCompleteObjectUndo(clip, undoKey); 163 | 164 | // Get amount of time to insert/cut in seconds using the clip framerate. 165 | var amount = frames / clip.frameRate; 166 | 167 | // Update events in clip: insert/cut time by amount. 168 | List updatedEvents = new(); 169 | foreach (var evnt in clip.events) 170 | { 171 | if (evnt.time > currentTime) evnt.time += amount; 172 | updatedEvents.Add(evnt); 173 | } 174 | AnimationUtility.SetAnimationEvents(clip, updatedEvents.ToArray()); 175 | 176 | // update float curves: insert/cut time by amount. 177 | var floatBindings = AnimationUtility.GetCurveBindings(clip); 178 | foreach (var bind in floatBindings) 179 | { 180 | var curve = AnimationUtility.GetEditorCurve(clip, bind); 181 | var keys = curve.keys; 182 | for (var i = 0; i < keys.Length; i++) 183 | if ((keys[i].time - currentTime) >= -tolerance) keys[i].time += amount; 184 | curve.keys = keys; 185 | AnimationUtility.SetEditorCurve(clip, bind, curve); 186 | } 187 | 188 | // update the PPtr curves: insert/cut time by amount. 189 | var objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(clip); 190 | foreach (var bind in objectBindings) 191 | { 192 | var curve = AnimationUtility.GetObjectReferenceCurve(clip, bind); 193 | for (var i = 0; i < curve.Length; i++) 194 | if ((curve[i].time - currentTime) >= -tolerance) curve[i].time += amount; 195 | AnimationUtility.SetObjectReferenceCurve(clip, bind, curve); 196 | } 197 | 198 | // Grab markers to modify in track based off of current time and tolerance 199 | foreach (var marker in track.GetMarkers()) 200 | { 201 | if ((marker.time - currentTime) >= -tolerance) marker.time += amount; 202 | } 203 | 204 | // Mark clip as dirty 205 | EditorUtility.SetDirty(clip); 206 | } 207 | 208 | // Handle other tracks (all unlocked non-infinite animation or non-animation clips) 209 | { 210 | // Get other tracks 211 | var otherTracks = unlockedTracks.Where(e => (e is AnimationTrack a && (a.inClipMode == true || a.infiniteClip == null)) || e is not AnimationTrack).ToList(); 212 | 213 | // Get amount of time to insert/cut in seconds for tracks using the timeline assets frame rate 214 | var amount = frames / timelineAsset.editorSettings.frameRate; 215 | 216 | // Grab clips to modify in track based off of current time and tolerance 217 | var clips = otherTracks.SelectMany(x => x.GetClips()).Where(x => (x.start - currentTime) >= -tolerance).ToList(); 218 | foreach (var clip in clips) 219 | { 220 | clip.start += amount; 221 | } 222 | 223 | // Grab markers to modify in track based off of current time and tolerance 224 | var markers = otherTracks.SelectMany(x => x.GetMarkers()).Where(x => (x.time - currentTime) >= -tolerance).ToList(); 225 | foreach (var marker in markers) 226 | { 227 | marker.time += amount; 228 | } 229 | } 230 | 231 | // Refresh editor 232 | TimelineEditor.Refresh(RefreshReason.ContentsModified); 233 | } 234 | } 235 | 236 | // Adds support for Synchronizing timeline with animation view 237 | public class AnimationViewSynchronizer 238 | { 239 | private static bool enabled = false; 240 | private const string menuPath = "Tools/Timeline/Sync Timeline && Animation Views"; 241 | 242 | [MenuItem(menuPath, priority = 0)] 243 | public static void Sync() 244 | { 245 | enabled = !enabled; 246 | Menu.SetChecked(menuPath, enabled); 247 | 248 | // Remove and add update callback 249 | EditorApplication.update -= OnUpdate; 250 | if (enabled) EditorApplication.update += OnUpdate; 251 | } 252 | 253 | private static void OnUpdate() 254 | { 255 | // Get timeline window ruler range 256 | var visibleTimeRange = (Vector2)typeof(TimelineEditor).GetProperty("visibleTimeRange", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null); 257 | if (visibleTimeRange == null) return; 258 | 259 | // Get Animation Window horizontal range setter 260 | var animationWindow = EditorWindow.GetWindow(false, null, false); 261 | if (animationWindow == null) return; 262 | var m_AnimEditor = animationWindow.GetType().GetField("m_AnimEditor", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(animationWindow); 263 | if (m_AnimEditor == null) return; 264 | var m_State = m_AnimEditor.GetType().GetField("m_State", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(m_AnimEditor); 265 | if (m_State == null) return; 266 | var m_TimeArea = m_State.GetType().GetField("m_TimeArea", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(m_State); 267 | if (m_TimeArea == null) return; 268 | var SetShownHRangeInsideMargins = m_TimeArea.GetType().GetMethod("SetShownHRangeInsideMargins"); 269 | if (SetShownHRangeInsideMargins == null) return; 270 | 271 | // Call Range Updater on Animation view 272 | var parametersArray = new object[] { visibleTimeRange.x, visibleTimeRange.y }; 273 | SetShownHRangeInsideMargins.Invoke(m_TimeArea, parametersArray); 274 | 275 | // Force repaint 276 | animationWindow.Repaint(); 277 | } 278 | } 279 | 280 | // Adds Editor support for Timeline Event Markers for calling GameObject methods 281 | namespace Events 282 | { 283 | [CustomTimelineEditor(typeof(EventMarkerTrack))] 284 | public class EventTrackEditor : TrackEditor 285 | { 286 | readonly Texture2D iconTexture; 287 | 288 | public EventTrackEditor() 289 | { 290 | iconTexture = Resources.Load("EventMarkerIcon"); 291 | } 292 | 293 | public override TrackDrawOptions GetTrackOptions(TrackAsset track, Object binding) 294 | { 295 | var options = base.GetTrackOptions(track, binding); 296 | options.icon = iconTexture; 297 | return options; 298 | } 299 | } 300 | 301 | // Add event handler for detecting timeline marker events during timeline preview scrubbing - fixes scrubbing not calling events 302 | public class TimelineEditorEventHandler 303 | { 304 | [InitializeOnLoadMethod] 305 | public static void OnLoad() 306 | { 307 | EditorApplication.update -= OnUpdate; 308 | EditorApplication.update += OnUpdate; 309 | } 310 | 311 | // Handle pushing marker event notifications for timeline preview scrubbing 312 | static double previousTime = 0; 313 | static readonly HashSet firedEvents = new(); // store fired events 314 | static bool inTimeline = false; 315 | public static void OnUpdate() 316 | { 317 | 318 | // Do nothing if no inspected director (not in timeline) 319 | if (TimelineEditor.inspectedDirector == null) 320 | { 321 | // Clear all events if we were previously in the timeline 322 | if (inTimeline) 323 | { 324 | inTimeline = false; 325 | firedEvents.Clear(); 326 | previousTime = 0; 327 | } 328 | return; 329 | } 330 | 331 | var director = TimelineEditor.inspectedDirector; 332 | 333 | // Check if scrubbing 334 | var graph = director.playableGraph; 335 | var isScrub = !Application.isPlaying && graph.IsValid() && !graph.IsPlaying() && previousTime != director.time; 336 | if (!isScrub) return; 337 | 338 | // Keep track of the fact that we entered the timeline to clear state info later 339 | inTimeline = true; 340 | 341 | // Clear all events if timeline moved in reverse (and refire them if marked as retroactive) 342 | if (previousTime > director.time) firedEvents.Clear(); 343 | 344 | // Loop each track 345 | for (int i = 0; i < graph.GetOutputCount(); i++) 346 | { 347 | SortedList sortedMarkers = new(); 348 | // Get track and continue if null 349 | var output = graph.GetOutput(i); 350 | var playable = output.GetSourcePlayable().GetInput(i); 351 | var track = output.GetReferenceObject() as TrackAsset; 352 | if (track == null) continue; 353 | 354 | // Loop each marker of type INotification and sort 355 | var markers = track.GetMarkers().OfType().OfType(); 356 | foreach (var marker in markers) sortedMarkers.Add(marker.time, marker); 357 | 358 | // Iterate the sorted marker list to check for whether events need to be processed 359 | foreach (var marker in sortedMarkers.Values) 360 | { 361 | if (marker.emitInEditor && !firedEvents.Contains(marker)) 362 | { 363 | // Push notification if current time matches notification time 364 | if (director.time == 0 && marker.time == 0 || Math.Abs(director.time - marker.time) < 1e-14) 365 | { 366 | // Add to list of fired events 367 | firedEvents.Add(marker); 368 | 369 | // Push event 370 | output.PushNotification(playable, marker); 371 | } 372 | // Push notification if time after notification time and notification hasn't been previously pushed 373 | else if (director.time >= marker.time) 374 | { 375 | // Add to list of fired events 376 | firedEvents.Add(marker); 377 | 378 | // Don't emit if timeline reversed and notification not marked as retroactive 379 | if (previousTime > director.time && !marker.retroactive) continue; 380 | 381 | // Push event 382 | output.PushNotification(playable, marker); 383 | } 384 | } 385 | } 386 | } 387 | 388 | // Record current time 389 | previousTime = director.time; 390 | } 391 | } 392 | 393 | // For storing parsed method information in the editor 394 | public class CallbackDescription 395 | { 396 | public string assemblyName; // Object type of the method 397 | public string methodName; // The short name of the method 398 | public string fullMethodName; // The name of the method with parameters: e.g.: Foo(arg_type) 399 | public string qualifiedMethodName; // The name of the class + method + parameters: e.g. Bar.Foo(arg_type) 400 | public List parameterTypes; // none, bool, int, float, string, Object, Enum 401 | public ArrayList defaultParameters; // default value for the given parameter 402 | } 403 | 404 | // For uniquely identifying Stored SerializedProperty methods with found CallbackDescription methods using assembly name, method name, and argument types 405 | public static class ListExtensions 406 | { 407 | public static int FindMethod(this IList callbacks, SerializedProperty assemblyName, SerializedProperty methodName, SerializedProperty arguments) 408 | { 409 | // Iterate each callback method in list 410 | for (int id = 0; id < callbacks.Count; id++) 411 | { 412 | var callback = callbacks[id]; 413 | 414 | // if num params, assembly name, or method name don't match continue to next callback 415 | if (arguments.arraySize != callback.parameterTypes.Count || assemblyName.stringValue.Split(",")[0] != callback.assemblyName.Split(",")[0] || methodName.stringValue != callback.methodName) 416 | continue; 417 | 418 | // Iterate each param type 419 | bool isMatch = true; 420 | int i; 421 | for (i = 0; i < callback.parameterTypes.Count; i++) 422 | { 423 | // Grab types 424 | var type = callback.parameterTypes[i]; 425 | var argumentProperty = arguments.GetArrayElementAtIndex(i); 426 | SerializedProperty m_ParameterType = argumentProperty.FindPropertyRelative("parameterType"); 427 | var type2 = m_ParameterType.enumValueIndex; 428 | 429 | // break early if no match 430 | if (type == typeof(bool) && type2 == (int)ParameterType.Bool) 431 | continue; 432 | else if (type == typeof(int) && type2 == (int)ParameterType.Int) 433 | continue; 434 | else if (type == typeof(float) && type2 == (int)ParameterType.Float) 435 | continue; 436 | else if (type == typeof(string) && type2 == (int)ParameterType.String) 437 | continue; 438 | else if (type == typeof(Playable) && type2 == (int)ParameterType.Playable) // handle before object 439 | continue; 440 | else if (type == typeof(EventMarkerNotification) && type2 == (int)ParameterType.EventMarkerNotification) // handle before object 441 | continue; 442 | else if ((type == typeof(object) || type.IsSubclassOf(typeof(Object))) && type2 == (int)ParameterType.Object) 443 | continue; 444 | else if (type.IsEnum && (type2 == (int)ParameterType.Enum || argumentProperty.FindPropertyRelative("String").stringValue.Split(",")[0] == type.FullName)) 445 | continue; 446 | isMatch = false; 447 | break; 448 | } 449 | 450 | // if count match then method matches so return id 451 | if (isMatch) return id; 452 | } 453 | return -1; 454 | } 455 | } 456 | 457 | // Custom Inspector for creating EventMarkers 458 | [CustomEditor(typeof(EventMarkerNotification)), CanEditMultipleObjects] 459 | public class EventMarkerInspector : Editor 460 | { 461 | // Cached data for speeding up editor 462 | class EditorCache 463 | { 464 | public int selectedMethodId; // selected id for a given reorderable list entry 465 | public string[] dropdown; // dropdown for for a given reorderable list entry 466 | } 467 | Dictionary editorCache; 468 | GameObject cachedGameObject; // bound game object 469 | List cachedSupportedMethods; // supported methods for the given game object 470 | ReorderableList cachedMethodList; // selected methods in a reorderable list 471 | 472 | // Properties 473 | SerializedProperty m_Time; 474 | SerializedProperty m_Callbacks; 475 | SerializedProperty m_Retroactive; 476 | SerializedProperty m_EmitOnce; 477 | SerializedProperty m_EmitInEditor; 478 | SerializedProperty m_Color; 479 | SerializedProperty m_ShowLineOverlay; 480 | 481 | // Get serialized object properties (for UI) 482 | public void OnEnable() 483 | { 484 | // Functional properties 485 | m_Time = serializedObject.FindProperty("m_Time"); 486 | m_Callbacks = serializedObject.FindProperty("callbacks"); 487 | m_Retroactive = serializedObject.FindProperty("retroactive"); 488 | m_EmitOnce = serializedObject.FindProperty("emitOnce"); 489 | m_EmitInEditor = serializedObject.FindProperty("emitInEditor"); 490 | 491 | // Style properties 492 | m_Color = serializedObject.FindProperty("color"); 493 | m_ShowLineOverlay = serializedObject.FindProperty("showLineOverlay"); 494 | } 495 | 496 | // Draw inspector GUI 497 | public override void OnInspectorGUI() 498 | { 499 | serializedObject.Update(); 500 | 501 | var marker = target as Marker; 502 | 503 | // Make sure there is an instance of all objects before attempting to show anything in inspector 504 | if (marker == null || marker.parent == null || TimelineEditor.inspectedDirector == null) return; 505 | 506 | // Get bound scene object 507 | var boundObj = TimelineEditor.inspectedDirector.GetGenericBinding(marker.parent); 508 | { 509 | using var changeScope = new EditorGUI.ChangeCheckScope(); 510 | EditorGUILayout.PropertyField(m_Time); 511 | EditorGUILayout.Space(); 512 | 513 | EditorGUILayout.LabelField("Event Properties"); 514 | EditorGUI.indentLevel++; 515 | EditorGUILayout.PropertyField(m_Retroactive); 516 | EditorGUILayout.PropertyField(m_EmitOnce); 517 | EditorGUILayout.PropertyField(m_EmitInEditor); 518 | 519 | EditorGUILayout.Space(); 520 | EditorGUI.indentLevel--; 521 | EditorGUILayout.LabelField("Marker Style"); 522 | EditorGUI.indentLevel++; 523 | EditorGUILayout.PropertyField(m_Color); 524 | EditorGUILayout.PropertyField(m_ShowLineOverlay); 525 | 526 | EditorGUILayout.Space(); 527 | 528 | GameObject curGameObject = null; 529 | if (boundObj as GameObject != null) curGameObject = (GameObject)boundObj; 530 | else if (boundObj as Component != null) curGameObject = ((Component)boundObj).gameObject; 531 | 532 | // Workaround Unity Timeline bug where active context is lost on save which breaks the inspector for 533 | // Object Fields with Exposed References 534 | if (Selection.activeContext == null) 535 | Selection.SetActiveObjectWithContext(target, TimelineEditor.inspectedDirector); // Re-set the context 536 | 537 | // Only rebuild list if something as changed (it isn't draggable otherwise) 538 | if (cachedMethodList == null || cachedGameObject != curGameObject) 539 | { 540 | // Warning -- event markers should only be used in event marker tracks for correct timeline preview behaviour 541 | if (marker.parent is not EventMarkerTrack) 542 | Debug.LogWarning("TimelineTools: Add Event Marker to an Event Marker Track"); 543 | 544 | cachedGameObject = curGameObject; 545 | cachedSupportedMethods = CollectSupportedMethods(cachedGameObject).ToList(); 546 | 547 | cachedMethodList = new ReorderableList(serializedObject, m_Callbacks, true, true, true, true) 548 | { 549 | elementHeightCallback = GetElementHeight, 550 | drawElementCallback = DrawMethodAndArguments, 551 | drawHeaderCallback = delegate (Rect rect) { EditorGUI.LabelField(rect, "GameObject Methods"); }, 552 | onChangedCallback = delegate (ReorderableList list) { cachedMethodList = null; } 553 | }; 554 | editorCache = new(); 555 | } 556 | 557 | // Layout reorderable list 558 | cachedMethodList.DoLayoutList(); 559 | 560 | // apply changes 561 | if (changeScope.changed) serializedObject.ApplyModifiedProperties(); 562 | } 563 | } 564 | 565 | // Height determiner for a given element 566 | float GetElementHeight(int index) 567 | { 568 | // Retrieve element (elements are added when + is clicked in reorderable list UI) 569 | SerializedProperty element = cachedMethodList.serializedProperty.GetArrayElementAtIndex(index); 570 | 571 | // Retrieve element properties 572 | SerializedProperty m_Arguments = element.FindPropertyRelative("arguments"); 573 | 574 | // Determine height 575 | if (m_Arguments.arraySize == 0) return EditorGUIUtility.singleLineHeight + 10; 576 | else return EditorGUIUtility.singleLineHeight * 2 + 10; 577 | } 578 | 579 | // Draw drawer entry for given element 580 | void DrawMethodAndArguments(Rect rect, int index, bool isActive, bool isFocused) 581 | { 582 | // Compute first field position 583 | Rect line = new(rect.x, rect.y + 4, rect.width, EditorGUIUtility.singleLineHeight); 584 | 585 | // Retrieve element (elements are added when + is clicked in reorderable list UI) 586 | SerializedProperty element = cachedMethodList.serializedProperty.GetArrayElementAtIndex(index); 587 | 588 | // Retrieve element properties 589 | SerializedProperty m_AssemblyName = element.FindPropertyRelative("assemblyName"); 590 | SerializedProperty m_MethodName = element.FindPropertyRelative("methodName"); 591 | SerializedProperty m_Arguments = element.FindPropertyRelative("arguments"); 592 | 593 | // Initialize cache for this index if not initialized 594 | if (!editorCache.ContainsKey(index)) editorCache.Add(index, new()); 595 | var cache = editorCache[index]; 596 | 597 | // Generate dropdown if the the cache is empty 598 | if (cache.dropdown == null) 599 | { 600 | // Get current method ID based off of stored name (index really) 601 | cache.selectedMethodId = cachedSupportedMethods.FindMethod(m_AssemblyName, m_MethodName, m_Arguments); 602 | 603 | // Create dropdown 604 | var qualifiedMethodNames = cachedSupportedMethods.Select(i => i.qualifiedMethodName); 605 | var dropdownList = new List() { "", "" }; // Add 2x blank line entries 606 | dropdownList.AddRange(qualifiedMethodNames); 607 | cache.dropdown = dropdownList.ToArray(); 608 | } 609 | 610 | // Draw popup (dropdown box) 611 | var previousMixedValue = EditorGUI.showMixedValue; 612 | { 613 | GUIStyle style = EditorStyles.popup; 614 | style.richText = true; 615 | 616 | // Create dropdownlist with 'pseudo entry' for the currently selected method at the top of the list 617 | CallbackDescription selectedMethod = cache.selectedMethodId > -1 ? cachedSupportedMethods[cache.selectedMethodId] : null; 618 | cache.dropdown[0] = cache.selectedMethodId > -1 ? selectedMethod.assemblyName.Split(",")[0] + "." + selectedMethod.fullMethodName : "No method"; 619 | 620 | // Store old selected method id case it isn't changed 621 | var oldSelectedMethodId = cache.selectedMethodId; 622 | cache.selectedMethodId = EditorGUI.Popup(line, 0, cache.dropdown, style); 623 | 624 | // Update field position 625 | line.y += EditorGUIUtility.singleLineHeight + 2; 626 | 627 | // Normalize selection 628 | if (cache.selectedMethodId == 0) 629 | cache.selectedMethodId = oldSelectedMethodId; // No selection so restore actual method id 630 | else 631 | cache.selectedMethodId -= 2; // normalize to get actual Id (-2 for the two 'pseudo entries' 632 | } 633 | 634 | EditorGUI.showMixedValue = previousMixedValue; 635 | 636 | // If selected method is valid then try to draw parameters 637 | if (cache.selectedMethodId > -1 && cache.selectedMethodId < cachedSupportedMethods.Count) 638 | { 639 | var callbackDescription = cachedSupportedMethods.ElementAt(cache.selectedMethodId); 640 | 641 | // Detect method change in order to initialize default parameters 642 | var methodChanged = m_MethodName.stringValue != callbackDescription.methodName; 643 | 644 | // Fillout assembly and method name properties using the selected id 645 | m_AssemblyName.stringValue = callbackDescription.assemblyName; 646 | m_MethodName.stringValue = callbackDescription.methodName; 647 | 648 | // Draw each argument 649 | DrawArguments(line, element, callbackDescription, methodChanged); 650 | } 651 | } 652 | 653 | // Create UI elements for the given parameter types of a methods arguments 654 | void DrawArguments(Rect rect, SerializedProperty element, CallbackDescription callbackDescription, bool initialize) 655 | { 656 | // Find the amount of user enterable arguments to compute UI entry box sizes 657 | int enterableArgCount = 0; 658 | foreach (var type in callbackDescription.parameterTypes) 659 | if (type != typeof(Playable) && type != typeof(EventMarkerNotification)) enterableArgCount++; 660 | 661 | // Compute the rect for the method parameters based off of the count 662 | var paramWidth = rect.width / enterableArgCount; 663 | rect.width = paramWidth - 5; 664 | 665 | // Grab the arguments property 666 | SerializedProperty m_Arguments = element.FindPropertyRelative("arguments"); 667 | 668 | // Resize the arguments array 669 | m_Arguments.arraySize = callbackDescription.parameterTypes.Count; 670 | 671 | // Iterate and display each argument by type 672 | for (var i = 0; i < m_Arguments.arraySize; i++) 673 | { 674 | var type = callbackDescription.parameterTypes[i]; 675 | var defaultValue = callbackDescription.defaultParameters[i]; 676 | var argumentProperty = m_Arguments.GetArrayElementAtIndex(i); 677 | SerializedProperty m_ParameterType = argumentProperty.FindPropertyRelative("parameterType"); 678 | 679 | // Assign Param type and generate field. The Field style is determined by the serialized property type 680 | if (type == typeof(bool)) 681 | { 682 | m_ParameterType.enumValueIndex = (int)ParameterType.Bool; 683 | var property = argumentProperty.FindPropertyRelative("Bool"); 684 | if (initialize && defaultValue.GetType() != typeof(DBNull)) property.boolValue = (bool)defaultValue; 685 | EditorGUI.PropertyField(rect, property, GUIContent.none); 686 | } 687 | else if (type == typeof(int)) 688 | { 689 | m_ParameterType.enumValueIndex = (int)ParameterType.Int; 690 | var property = argumentProperty.FindPropertyRelative("Int"); 691 | if (initialize && defaultValue.GetType() != typeof(DBNull)) property.intValue = (int)defaultValue; 692 | EditorGUI.PropertyField(rect, property, GUIContent.none); 693 | } 694 | else if (type == typeof(float)) 695 | { 696 | m_ParameterType.enumValueIndex = (int)ParameterType.Float; 697 | var property = argumentProperty.FindPropertyRelative("Float"); 698 | if (initialize && defaultValue.GetType() != typeof(DBNull)) property.floatValue = (float)defaultValue; 699 | EditorGUI.PropertyField(rect, property, GUIContent.none); 700 | } 701 | else if (type == typeof(string)) 702 | { 703 | m_ParameterType.enumValueIndex = (int)ParameterType.String; 704 | var property = argumentProperty.FindPropertyRelative("String"); 705 | if (initialize && defaultValue.GetType() != typeof(DBNull)) property.stringValue = (string)defaultValue; 706 | EditorGUI.PropertyField(rect, property, GUIContent.none); 707 | } 708 | else if (type == typeof(Playable)) // handle before object 709 | { 710 | m_ParameterType.enumValueIndex = (int)ParameterType.Playable; 711 | continue; 712 | } 713 | else if (type == typeof(EventMarkerNotification)) // handle before object 714 | { 715 | m_ParameterType.enumValueIndex = (int)ParameterType.EventMarkerNotification; 716 | continue; 717 | } 718 | else if (type == typeof(object) || type.IsSubclassOf(typeof(Object))) 719 | { 720 | var property = argumentProperty.FindPropertyRelative("Object"); 721 | var exposedName = property.FindPropertyRelative("exposedName"); 722 | var defaultExposedValue = property.FindPropertyRelative("defaultValue"); 723 | 724 | m_ParameterType.enumValueIndex = (int)ParameterType.Object; 725 | if (initialize && defaultValue.GetType() != typeof(DBNull)) property.exposedReferenceValue = (Object)defaultValue; 726 | var obj = EditorGUI.ObjectField(rect, property.exposedReferenceValue, type, true); 727 | if (property.exposedReferenceValue != obj) 728 | { 729 | TimelineEditor.inspectedDirector.ClearReferenceValue(exposedName.stringValue); 730 | exposedName.stringValue = ""; 731 | defaultExposedValue.objectReferenceValue = null; 732 | if (obj is GameObject x && x.scene.name != null) 733 | property.exposedReferenceValue = obj; // scene / exposed object 734 | else 735 | defaultExposedValue.objectReferenceValue = obj; // prefab-nonscene object 736 | } 737 | } 738 | else if (type.IsEnum) 739 | { 740 | m_ParameterType.enumValueIndex = (int)ParameterType.Enum; 741 | var intProperty = argumentProperty.FindPropertyRelative("Int"); 742 | var stringProperty = argumentProperty.FindPropertyRelative("String"); 743 | if (initialize && defaultValue.GetType() != typeof(DBNull)) intProperty.intValue = (int)defaultValue; 744 | intProperty.intValue = (int)(object)EditorGUI.EnumPopup(rect, (Enum)Enum.ToObject(type, intProperty.intValue)); // Parse as enum type 745 | stringProperty.stringValue = type.FullName; // store full type name in string 746 | } 747 | 748 | // Update field position 749 | rect.x += paramWidth; 750 | } 751 | } 752 | 753 | // Helper method for retrieving method signatures from an Object 754 | public static IEnumerable CollectSupportedMethods(Object obj) 755 | { 756 | // return if object is null 757 | if (obj == null) return Enumerable.Empty(); 758 | 759 | // Create a list to fill with supported methods 760 | List supportedMethods = new(); 761 | 762 | // Create a list of objects to search methods for (include base object) 763 | List objectList = new() { obj }; 764 | 765 | // Get components if object is a game object 766 | var components = (obj as GameObject)?.GetComponentsInChildren(); 767 | if (components != null) objectList.AddRange(components); 768 | 769 | // Iterate over base Object and all components 770 | foreach (var item in objectList) 771 | { 772 | // Get item type. If the type is a monoscript then get the class type directly 773 | var itemType = item is MonoScript monoScript ? monoScript.GetClass() : item.GetType(); 774 | 775 | // Loop over type for derived type up the entire inheritence hierarchy 776 | while (itemType != null) 777 | { 778 | // Get methods for class type. Include instance methods if the type is a game object or component 779 | var methods = itemType.GetMethods((item is GameObject || item is Component ? BindingFlags.Instance : 0) | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly); 780 | foreach (var method in methods) 781 | { 782 | // don't support adding built in method names 783 | if (method.Name == "Main" && method.Name == "Start" && method.Name == "Awake" && method.Name == "Update") continue; 784 | 785 | var parameters = method.GetParameters(); // get parameters 786 | List parameterTypes = new(); // create empty parameter list 787 | ArrayList defaultValues = new(); // create empty default arguments list 788 | string fullMethodName = method.Name + "("; // start full method name signature 789 | bool validMethod = true; // mark the method as valid until proven otherwise 790 | 791 | // Parse parameter types 792 | for (int i = 0; i < parameters.Length; i++) 793 | { 794 | if (i > 0) fullMethodName += ", "; 795 | var parameter = parameters[i]; 796 | var strType = ""; 797 | if (parameter.ParameterType == typeof(bool)) 798 | strType = "bool"; 799 | else if (parameter.ParameterType == typeof(int)) 800 | strType = "int"; 801 | else if (parameter.ParameterType == typeof(float)) 802 | strType = "float"; 803 | else if (parameter.ParameterType == typeof(string)) 804 | strType = "string"; 805 | else if (parameter.ParameterType == typeof(Playable)) // handle before object 806 | strType = "Playable"; 807 | else if (parameter.ParameterType == typeof(EventMarkerNotification)) // handle before object 808 | strType = "EventMarkerNotification"; 809 | else if (parameter.ParameterType == typeof(object) || parameter.ParameterType.IsSubclassOf(typeof(Object))) 810 | strType = "Object"; 811 | else if (parameter.ParameterType.IsEnum && Enum.GetUnderlyingType(parameter.ParameterType) == typeof(int)) 812 | strType = parameter.ParameterType.Name; // use underlying typename for fullanme string 813 | else 814 | validMethod = false; 815 | 816 | // Add parameter and update full method name with parameter type and name 817 | parameterTypes.Add(parameter.ParameterType); 818 | defaultValues.Add(parameter.DefaultValue); 819 | fullMethodName += strType + " " + parameter.Name; 820 | } 821 | 822 | // one or more argument types was not supported so don't add method. 823 | if (validMethod == false) continue; 824 | 825 | // Finish the full name signature 826 | fullMethodName += ")"; 827 | 828 | // Collect the first two pieces of the FQN 829 | var assemblyName = itemType.FullName + "," + itemType.Module.Assembly.GetName().Name; 830 | 831 | // Create method description object 832 | var supportedMethod = new CallbackDescription 833 | { 834 | methodName = method.Name, 835 | fullMethodName = fullMethodName, 836 | qualifiedMethodName = itemType + "/" + fullMethodName[0] + "/" + fullMethodName, 837 | parameterTypes = parameterTypes, 838 | defaultParameters = defaultValues, 839 | assemblyName = assemblyName 840 | }; 841 | supportedMethods.Add(supportedMethod); 842 | } 843 | 844 | // Get base type to check it for methods as well 845 | itemType = itemType.BaseType; 846 | } 847 | } 848 | 849 | return supportedMethods.OrderBy(x => x.fullMethodName, StringComparer.Ordinal).ToList(); 850 | } 851 | } 852 | 853 | // Editor used by the Timeline window to customize the appearance of a marker 854 | [CustomTimelineEditor(typeof(EventMarkerNotification))] 855 | public class EventMarkerOverlay : MarkerEditor 856 | { 857 | const float k_LineOverlayWidth = 6.0f; 858 | 859 | static readonly Texture2D iconTexture; 860 | static readonly Texture2D overlayTexture; 861 | static readonly Texture2D overlaySelectedTexture; 862 | static readonly Texture2D overlayCollapsedTexture; 863 | 864 | static EventMarkerOverlay() 865 | { 866 | iconTexture = Resources.Load("EventMarkerIcon"); 867 | overlayTexture = Resources.Load("EventMarker"); 868 | overlaySelectedTexture = Resources.Load("EventMarker_Selected"); 869 | overlayCollapsedTexture = Resources.Load("EventMarker_Collapsed"); 870 | } 871 | 872 | // Draws a vertical line on top of the Timeline window's contents. 873 | public override void DrawOverlay(IMarker marker, MarkerUIStates uiState, MarkerOverlayRegion region) 874 | { 875 | // The `marker argument needs to be cast as the appropriate type, usually the one specified in the `CustomTimelineEditor` attribute 876 | var annotation = marker as EventMarkerNotification; 877 | if (annotation == null) return; 878 | 879 | if (annotation.showLineOverlay) DrawLineOverlay(annotation.color, region); 880 | 881 | DrawColorOverlay(region, annotation.color, uiState); 882 | } 883 | 884 | // Sets the marker's tooltip based on its title. 885 | public override MarkerDrawOptions GetMarkerOptions(IMarker marker) 886 | { 887 | // The `marker argument needs to be cast as the appropriate type, usually the one specified in the `CustomTimelineEditor` attribute 888 | var eventMarker = marker as EventMarkerNotification; 889 | if (eventMarker == null) return base.GetMarkerOptions(marker); 890 | 891 | // Set marker icon 892 | EditorGUIUtility.SetIconForObject(eventMarker, iconTexture); 893 | 894 | // Tooltip format 895 | string richMethodFormat = EditorGUIUtility.isProSkin ? 896 | "{0}.{1}({2})\n" : "{0}.{1}({2})\n"; 897 | string richArgumentFormat = EditorGUIUtility.isProSkin ? 898 | "{0} ({1})" : "{0} ({1})"; 899 | 900 | // Create tooltip 901 | string tooltip = ""; 902 | 903 | if (eventMarker.callbacks != null) 904 | { 905 | foreach (var callback in eventMarker.callbacks) 906 | { 907 | // if no method name, give up 908 | if (callback.methodName.Length == 0) continue; 909 | 910 | string arg = "", type = "", color = "#000000", argumentText = ""; 911 | for (int i = 0; i < callback.arguments.Length; i++) 912 | { 913 | if (i > 0) argumentText += ", "; 914 | 915 | // Supports int, float, Object, string, enum, and none types. The Field style is determined by the serialized property type 916 | var argument = callback.arguments[i]; 917 | var objectValue = argument.Object.defaultValue.ToString().Split('(', ')'); 918 | if (argument.parameterType == ParameterType.Bool) 919 | (arg, type, color) = (argument.Bool.ToString(), "bool", argument.Bool ? "#009900" : "#ff2222"); 920 | else if (argument.parameterType == ParameterType.Int) 921 | (arg, type, color) = (argument.Int.ToString(), "int", EditorGUIUtility.isProSkin ? "#b5cea8" : "#098658"); 922 | else if (argument.parameterType == ParameterType.Float) 923 | (arg, type, color) = (argument.Float.ToString(), "float", EditorGUIUtility.isProSkin ? "#b5cea8" : "#098658"); 924 | else if (argument.parameterType == ParameterType.String) 925 | (arg, type, color) = ("\"" + argument.String + "\"", "string", EditorGUIUtility.isProSkin ? "#ce9178" : "#a31515"); 926 | else if (argument.parameterType == ParameterType.Playable) 927 | (arg, type, color) = ("playable", "Playable", EditorGUIUtility.isProSkin ? "#4ec9b0" : "#267f99"); 928 | else if (argument.parameterType == ParameterType.EventMarkerNotification) 929 | (arg, type, color) = ("notification", "EventMarkerNotification", EditorGUIUtility.isProSkin ? "#4ec9b0" : "#267f99"); 930 | else if (argument.parameterType == ParameterType.Object) 931 | (arg, type, color) = objectValue[0] == "null" ? ("None", "Object", "#ff0000") : 932 | objectValue[0].Length > 48 ? ("...", "Object", EditorGUIUtility.isProSkin ? "#4ec9b0" : "#267f99") : 933 | (objectValue[0], objectValue[1], EditorGUIUtility.isProSkin ? "#4ec9b0" : "#267f99"); 934 | else if (argument.parameterType == ParameterType.Enum) 935 | (arg, type, color) = (Enum.ToObject(Type.GetType(argument.String + ",Assembly-CSharp"), argument.Int).ToString(), "Enum", EditorGUIUtility.isProSkin ? "#4ec9b0" : "#267f99"); 936 | 937 | argumentText += string.Format(richArgumentFormat, arg, type, color); 938 | } 939 | 940 | // Format and trim string if no args 941 | tooltip += string.Format(richMethodFormat, callback.assemblyName.Split(",")[0], callback.methodName, argumentText); 942 | } 943 | } 944 | 945 | tooltip = tooltip.Length == 0 ? "No method" : tooltip.TrimEnd(); 946 | return new MarkerDrawOptions { tooltip = tooltip }; 947 | } 948 | 949 | static void DrawLineOverlay(Color color, MarkerOverlayRegion region) 950 | { 951 | // Calculate markerRegion's center on the x axis 952 | float markerRegionCenterX = region.markerRegion.xMin + (region.markerRegion.width - k_LineOverlayWidth) / 2.0f; 953 | 954 | // Calculate a rectangle that uses the full timeline region's height 955 | Rect overlayLineRect = new(markerRegionCenterX, region.timelineRegion.y, k_LineOverlayWidth, region.timelineRegion.height); 956 | 957 | Color overlayLineColor = new(color.r, color.g, color.b, color.a * 0.5f); 958 | EditorGUI.DrawRect(overlayLineRect, overlayLineColor); 959 | } 960 | 961 | static void DrawColorOverlay(MarkerOverlayRegion region, Color color, MarkerUIStates state) 962 | { 963 | // Save the Editor's overlay color before changing it 964 | Color oldColor = GUI.color; 965 | 966 | if (state.HasFlag(MarkerUIStates.Selected)) 967 | { 968 | GUI.color = color; 969 | GUI.DrawTexture(region.markerRegion, overlayTexture); 970 | GUI.color = new(1.0f, 1.0f, 1.0f, 1.0f); 971 | GUI.DrawTexture(region.markerRegion, overlaySelectedTexture); 972 | } 973 | else if (state.HasFlag(MarkerUIStates.Collapsed)) 974 | { 975 | GUI.color = color; 976 | GUI.DrawTexture(region.markerRegion, overlayCollapsedTexture); 977 | } 978 | else if (state.HasFlag(MarkerUIStates.None)) 979 | { 980 | GUI.color = color; 981 | GUI.DrawTexture(region.markerRegion, overlayTexture); 982 | } 983 | 984 | // Restore the previous Editor's overlay color 985 | GUI.color = oldColor; 986 | } 987 | } 988 | } 989 | } 990 | #endif 991 | -------------------------------------------------------------------------------- /TimelineTools.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 46866ae17f946424594d17f90406fd1b 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {fileID: 2800000, guid: 2e7da690a91425640b75c328929f5897, type: 3} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | --------------------------------------------------------------------------------