using System; using System.Linq; using System.Collections.Generic; using UnityEngine; using UnityEngine.Timeline; namespace UnityEditor.Timeline { /// /// The user-defined options for drawing a track." /// public struct TrackDrawOptions { /// /// Text that indicates if the track should display an error. /// /// /// If the error text is not empty or null, then the track displays a warning. The error text is used as the tooltip. /// public string errorText { get; set; } /// /// The highlight color of the track. /// public Color trackColor { get; set; } /// /// The minimum height of the track. /// public float minimumHeight { get; set; } /// /// The icon displayed on the track header. /// /// /// If this value is null, then the default icon for the track is used. /// public Texture2D icon { get; set; } /// /// Indicates whether this instance and a specified object are equal. /// /// The object to compare with the current instance. /// Returns true if and this instance are the same type and represent the same value. public override bool Equals(object obj) { if (!(obj is TrackDrawOptions)) return false; return Equals((TrackDrawOptions)obj); } /// /// Compares this object with another TrackDrawOptions. /// /// The object to compare with. /// Returns true if this and are equal. public bool Equals(TrackDrawOptions other) { return errorText == other.errorText && trackColor == other.trackColor && minimumHeight == other.minimumHeight && icon == other.icon; } /// /// Returns the hash code for this instance. /// /// A 32-bit signed integer that is the hash code for this instance. public override int GetHashCode() { return HashUtility.CombineHash( errorText != null ? errorText.GetHashCode() : 0, trackColor.GetHashCode(), minimumHeight.GetHashCode(), icon != null ? icon.GetHashCode() : 0 ); } /// /// Compares two TrackDrawOptions objects. /// /// The first object. /// The second object. /// Returns true if they are equal. public static bool operator ==(TrackDrawOptions options1, TrackDrawOptions options2) { return options1.Equals(options2); } /// /// Compares two TrackDrawOptions objects. /// /// The first object. /// The second object. /// Returns true if they are not equal. public static bool operator !=(TrackDrawOptions options1, TrackDrawOptions options2) { return !options1.Equals(options2); } } /// /// The errors displayed for the track binding. /// public enum TrackBindingErrors { /// /// Select no errors. /// None = 0, /// /// The bound GameObject is disabled. /// BoundGameObjectDisabled = 1 << 0, /// /// The bound GameObject does not have a valid component. /// NoValidComponent = 1 << 1, /// /// The bound Object is a disabled Behaviour. /// BehaviourIsDisabled = 1 << 2, /// /// The bound Object is not of the correct type. /// InvalidBinding = 1 << 3, /// /// The bound Object is part of a prefab, and not an instance. /// PrefabBound = 1 << 4, /// /// Select all errors. /// All = Int32.MaxValue } /// /// Use this class to customize track types in the TimelineEditor. /// public class TrackEditor { static readonly string k_BoundGameObjectDisabled = L10n.Tr("The bound GameObject is disabled."); static readonly string k_NoValidComponent = L10n.Tr("Could not find appropriate component on this gameObject"); static readonly string k_RequiredComponentIsDisabled = L10n.Tr("The component is disabled"); static readonly string k_InvalidBinding = L10n.Tr("The bound object is not the correct type."); static readonly string k_PrefabBound = L10n.Tr("The bound object is a Prefab"); readonly Dictionary m_BindingCache = new Dictionary(); /// /// The default height of a track. /// public static readonly float DefaultTrackHeight = 30.0f; /// /// The minimum unscaled height of a track. /// public static readonly float MinimumTrackHeight = 10.0f; /// /// The maximum height of a track. /// public static readonly float MaximumTrackHeight = 256.0f; /// /// Implement this method to override the default options for drawing a track. /// /// The track from which track options are retrieved. /// The binding for the track. /// The options for drawing the track. public virtual TrackDrawOptions GetTrackOptions(TrackAsset track, UnityEngine.Object binding) { return new TrackDrawOptions() { errorText = GetErrorText(track, binding, TrackBindingErrors.All), minimumHeight = DefaultTrackHeight, trackColor = GetTrackColor(track), icon = null }; } /// /// Gets the error text for the specified track. /// /// The track to retrieve options for. /// The binding for the track. /// The errors to check for. /// An error to be displayed on the track, or string.Empty if there is no error. public string GetErrorText(TrackAsset track, UnityEngine.Object boundObject, TrackBindingErrors detectErrors) { if (track == null || boundObject == null) return string.Empty; var bindingType = GetBindingType(track); if (bindingType != null) { // bound to a prefab asset if (HasFlag(detectErrors, TrackBindingErrors.PrefabBound) && PrefabUtility.IsPartOfPrefabAsset(boundObject)) { return k_PrefabBound; } // If we are a component, allow for bound game objects (legacy) if (typeof(Component).IsAssignableFrom(bindingType)) { var gameObject = boundObject as GameObject; var component = boundObject as Component; if (component != null) gameObject = component.gameObject; // game object is bound with no component if (HasFlag(detectErrors, TrackBindingErrors.NoValidComponent) && gameObject != null && component == null) { component = gameObject.GetComponent(bindingType); if (component == null) { return k_NoValidComponent; } } // attached gameObject is disables (ignores Activation Track) if (HasFlag(detectErrors, TrackBindingErrors.BoundGameObjectDisabled) && gameObject != null && !gameObject.activeInHierarchy) { return k_BoundGameObjectDisabled; } // component is disabled var behaviour = component as Behaviour; if (HasFlag(detectErrors, TrackBindingErrors.BehaviourIsDisabled) && behaviour != null && !behaviour.enabled) { return k_RequiredComponentIsDisabled; } // mismatched binding if (HasFlag(detectErrors, TrackBindingErrors.InvalidBinding) && component != null && !bindingType.IsAssignableFrom(component.GetType())) { return k_InvalidBinding; } } // Mismatched binding (non-component) else if (HasFlag(detectErrors, TrackBindingErrors.InvalidBinding) && !bindingType.IsAssignableFrom(boundObject.GetType())) { return k_InvalidBinding; } } return string.Empty; } /// /// Gets the color information of a track. /// /// /// Returns the color for the specified track. public Color GetTrackColor(TrackAsset track) { return TrackResourceCache.GetTrackColor(track); } /// /// Gets the binding type for a track. /// /// The track to retrieve the binding type from. /// Returns the binding type for the specified track. Returns null if the track does not have binding. public System.Type GetBindingType(TrackAsset track) { if (track == null) return null; if (m_BindingCache.TryGetValue(track, out var result)) return result; result = track.outputs.Select(x => x.outputTargetType).FirstOrDefault(); m_BindingCache[track] = result; return result; } /// /// Callback for when a track is created. /// /// The track that is created. /// The source that the track is copied from. This can be set to null if the track is not a copy. public virtual void OnCreate(TrackAsset track, TrackAsset copiedFrom) { } /// /// Callback for when a track is changed. /// /// The track that is changed. public virtual void OnTrackChanged(TrackAsset track) { } static bool HasFlag(TrackBindingErrors errors, TrackBindingErrors flag) { return (errors & flag) != 0; } /// /// Override this method to validate if a binding for /// can be determined from . /// /// The default implementation of this method will return true if /// - is not null or, /// - is not part of a Prefab Asset or, /// - is a Component that can be bound to /// /// /// TBD /// True if a binding can be determined from . /// /// public virtual bool IsBindingAssignableFrom(UnityEngine.Object candidate, TrackAsset track) { var action = BindingUtility.GetBindingAction(GetBindingType(track), candidate); return action != BindingUtility.BindingAction.DoNotBind; } /// /// Override this method to determine which object to bind to . /// A binding object should be determined from . /// /// By default, the `TrackBindingType` attribute from will be used to determine the binding. /// /// The source object from which a track binding should be determined. /// The track to bind an object to. /// The object to bind to . /// /// public virtual UnityEngine.Object GetBindingFrom(UnityEngine.Object candidate, TrackAsset track) { Type bindingType = GetBindingType(track); BindingUtility.BindingAction action = BindingUtility.GetBindingAction(bindingType, candidate); return BindingUtility.GetBinding(action, candidate, bindingType); } } static class TrackEditorExtension { public static bool SupportsBindingAssign(this TrackEditor editor) { return TypeUtility.HasOverrideMethod(editor.GetType(), nameof(TrackEditor.GetBindingFrom)); } public static void OnCreate_Safe(this TrackEditor editor, TrackAsset track, TrackAsset copiedFrom) { try { editor.OnCreate(track, copiedFrom); } catch (Exception e) { Debug.LogException(e); } } public static TrackDrawOptions GetTrackOptions_Safe(this TrackEditor editor, TrackAsset track, UnityEngine.Object binding) { try { return editor.GetTrackOptions(track, binding); } catch (Exception e) { Debug.LogException(e); return CustomTimelineEditorCache.GetDefaultTrackEditor().GetTrackOptions(track, binding); } } public static UnityEngine.Object GetBindingFrom_Safe(this TrackEditor editor, UnityEngine.Object candidate, TrackAsset track) { try { return editor.GetBindingFrom(candidate, track); } catch (Exception e) { Debug.LogException(e); return candidate; } } public static bool IsBindingAssignableFrom_Safe(this TrackEditor editor, UnityEngine.Object candidate, TrackAsset track) { try { return editor.IsBindingAssignableFrom(candidate, track); } catch (Exception e) { Debug.LogException(e); return false; } } public static void OnTrackChanged_Safe(this TrackEditor editor, TrackAsset track) { try { editor.OnTrackChanged(track); } catch (Exception e) { Debug.LogException(e); } } } }