using System; using System.Linq; using UnityEngine; using UnityEngine.Timeline; namespace UnityEditor.Timeline { [Flags] enum InputEvent { None = 0, DragEnter = 1, DragExit = 2, Drag = 4, KeyboardInput = 8 } static class InputEventMethods { public static bool InputHasBegun(this InputEvent evt) { return evt == InputEvent.DragEnter || evt == InputEvent.KeyboardInput; } } static class TimelineInspectorUtility { internal static class Styles { public static readonly GUIContent SecondsPrefix = L10n.TextContent("s", "Seconds"); public static readonly GUIContent FramesPrefix = L10n.TextContent("f", "Frames"); } public static void TimeField(SerializedProperty property, GUIContent label, bool readOnly, double frameRate, double minValue, double maxValue, ref InputEvent inputEvent) { var rect = EditorGUILayout.GetControlRect(); TimeField(rect, property, label, readOnly, frameRate, minValue, maxValue, ref inputEvent); } // Display Time related properties in frames and seconds public static void TimeField(Rect rect, SerializedProperty property, GUIContent label, bool readOnly, double frameRate, double minValue, double maxValue, ref InputEvent inputEvent) { using (var propertyScope = new PropertyScope(rect, label, property)) { GUIContent title = propertyScope.content; rect = EditorGUI.PrefixLabel(rect, title); using (new IndentLevelScope(0)) using (new LabelWidthScope((int)EditorGUI.kMiniLabelW)) using (new GUIMixedValueScope(property.hasMultipleDifferentValues)) { var secondsRect = new Rect(rect.xMin, rect.yMin, rect.width / 2 - EditorGUI.kSpacingSubLabel, rect.height); var framesRect = new Rect(rect.xMin + rect.width / 2, rect.yMin, rect.width / 2, rect.height); if (readOnly) { EditorGUI.FloatField(secondsRect, Styles.SecondsPrefix, (float)property.doubleValue, EditorStyles.label); } else { EditorGUI.BeginChangeCheck(); DelayedAndDraggableDoubleField(secondsRect, Styles.SecondsPrefix, property, ref inputEvent); if (EditorGUI.EndChangeCheck()) { property.doubleValue = Clamp(property.doubleValue, minValue, maxValue); } } if (frameRate > TimeUtility.kTimeEpsilon) { EditorGUI.BeginChangeCheck(); double time = property.doubleValue; int frames = TimeUtility.ToFrames(time, frameRate); double exactFrames = TimeUtility.ToExactFrames(time, frameRate); bool useIntField = TimeUtility.OnFrameBoundary(time, frameRate); if (readOnly) { if (useIntField) EditorGUI.IntField(framesRect, Styles.FramesPrefix, frames, EditorStyles.label); else EditorGUI.DoubleField(framesRect, Styles.FramesPrefix, exactFrames, EditorStyles.label); } else { if (useIntField) { int newFrames = DelayedAndDraggableIntField(framesRect, Styles.FramesPrefix, frames, ref inputEvent); time = Math.Max(0, TimeUtility.FromFrames(newFrames, frameRate)); } else { double newExactFrames = DelayedAndDraggableDoubleField(framesRect, Styles.FramesPrefix, exactFrames, ref inputEvent); time = Math.Max(0, TimeUtility.FromFrames((int)Math.Floor(newExactFrames), frameRate)); } } if (EditorGUI.EndChangeCheck()) { property.doubleValue = Clamp(time, minValue, maxValue); } } } } } public static double TimeFieldUsingTimeReference( GUIContent label, double time, bool readOnly, bool showMixed, double frameRate, double minValue, double maxValue, ref InputEvent inputEvent) { var state = TimelineWindow.instance.state; var needsTimeConversion = state != null && state.timeReferenceMode == TimeReferenceMode.Global; if (needsTimeConversion) time = state.editSequence.ToGlobalTime(time); var t = TimeField(label, time, readOnly, showMixed, frameRate, minValue, maxValue, ref inputEvent); if (needsTimeConversion) t = state.editSequence.ToLocalTime(t); return t; } public static double DurationFieldUsingTimeReference( GUIContent label, double start, double end, bool readOnly, bool showMixed, double frameRate, double minValue, double maxValue, ref InputEvent inputEvent) { var state = TimelineWindow.instance.state; var needsTimeConversion = state != null && state.timeReferenceMode == TimeReferenceMode.Global; if (needsTimeConversion) { start = state.editSequence.ToGlobalTime(start); end = state.editSequence.ToGlobalTime(end); } var duration = end - start; var t = TimeField(label, duration, readOnly, showMixed, frameRate, minValue, maxValue, ref inputEvent); end = start + t; if (needsTimeConversion) { start = state.editSequence.ToLocalTime(start); end = state.editSequence.ToLocalTime(end); } return end - start; } public static double TimeField(Rect rect, GUIContent label, double time, bool readOnly, bool showMixed, double frameRate, double minValue, double maxValue, ref InputEvent inputEvent) { using (new HorizontalScope(label, GUIStyle.none)) { rect = EditorGUI.PrefixLabel(rect, label); using (new IndentLevelScope(0)) using (new LabelWidthScope((int)EditorGUI.kMiniLabelW)) using (new GUIMixedValueScope(showMixed)) { var secondsRect = new Rect(rect.xMin, rect.yMin, rect.width / 2 - EditorGUI.kSpacingSubLabel, rect.height); var framesRect = new Rect(rect.xMin + rect.width / 2, rect.yMin, rect.width / 2, rect.height); if (readOnly) { EditorGUI.FloatField(secondsRect, Styles.SecondsPrefix, (float)time, EditorStyles.label); } else { time = DelayedAndDraggableDoubleField(secondsRect, Styles.SecondsPrefix, time, ref inputEvent); } if (frameRate > TimeUtility.kTimeEpsilon) { int frames = TimeUtility.ToFrames(time, frameRate); double exactFrames = TimeUtility.ToExactFrames(time, frameRate); bool useIntField = TimeUtility.OnFrameBoundary(time, frameRate); if (readOnly) { if (useIntField) EditorGUI.IntField(framesRect, Styles.FramesPrefix, frames, EditorStyles.label); else EditorGUI.FloatField(framesRect, Styles.FramesPrefix, (float)exactFrames, EditorStyles.label); } else { double newTime; EditorGUI.BeginChangeCheck(); if (useIntField) { int newFrames = DelayedAndDraggableIntField(framesRect, Styles.FramesPrefix, frames, ref inputEvent); newTime = Math.Max(0, TimeUtility.FromFrames(newFrames, frameRate)); } else { double newExactFrames = DelayedAndDraggableDoubleField(framesRect, Styles.FramesPrefix, exactFrames, ref inputEvent); newTime = Math.Max(0, TimeUtility.FromFrames((int)Math.Floor(newExactFrames), frameRate)); } if (EditorGUI.EndChangeCheck()) { time = newTime; } } } } } return Clamp(time, minValue, maxValue); } public static double TimeField(GUIContent label, double time, bool readOnly, bool showMixed, double frameRate, double minValue, double maxValue, ref InputEvent inputEvent) { var rect = EditorGUILayout.GetControlRect(); return TimeField(rect, label, time, readOnly, showMixed, frameRate, minValue, maxValue, ref inputEvent); } static InputEvent InputEventType(Rect rect, int id) { var evt = Event.current; switch (evt.GetTypeForControl(id)) { case EventType.MouseDown: if (rect.Contains(evt.mousePosition) && evt.button == 0) { return InputEvent.DragEnter; } break; case EventType.MouseUp: if (GUIUtility.hotControl == id) { return InputEvent.DragExit; } break; case EventType.MouseDrag: if (GUIUtility.hotControl == id) { return InputEvent.Drag; } break; case EventType.KeyDown: if (GUIUtility.hotControl == id && evt.keyCode == KeyCode.Escape) { return InputEvent.DragExit; } break; } return InputEvent.None; } static double DelayedAndDraggableDoubleField(Rect rect, GUIContent label, double value, ref InputEvent inputEvent, double dragSensitivity) { var id = GUIUtility.GetControlID(FocusType.Keyboard); var fieldRect = EditorGUI.PrefixLabel(rect, id, label); rect.xMax = fieldRect.x; double refValue = value; long dummy = 0; inputEvent |= InputEventType(rect, id); EditorGUI.DragNumberValue(rect, id, true, ref refValue, ref dummy, dragSensitivity); EditorGUI.BeginChangeCheck(); var result = EditorGUI.DelayedDoubleFieldInternal(fieldRect, GUIContent.none, refValue, EditorStyles.numberField); if (EditorGUI.EndChangeCheck()) inputEvent |= InputEvent.KeyboardInput; return result; } static int DelayedAndDraggableIntField(Rect rect, GUIContent label, int value, ref InputEvent inputEvent, long dragSensitivity) { var id = GUIUtility.GetControlID(FocusType.Keyboard); var fieldRect = EditorGUI.PrefixLabel(rect, id, label); rect.xMax = fieldRect.x; double dummy = 0.0; long refValue = value; inputEvent |= InputEventType(rect, id); EditorGUI.DragNumberValue(rect, id, false, ref dummy, ref refValue, dragSensitivity); EditorGUI.BeginChangeCheck(); var result = EditorGUI.DelayedIntFieldInternal(fieldRect, GUIContent.none, (int)refValue, EditorStyles.numberField); if (EditorGUI.EndChangeCheck()) inputEvent |= InputEvent.KeyboardInput; return result; } internal static double DelayedAndDraggableDoubleField(GUIContent label, double value, ref InputEvent action, double dragSensitivity) { var r = EditorGUILayout.s_LastRect = EditorGUILayout.GetControlRect(false, EditorGUI.kSingleLineHeight); return DelayedAndDraggableDoubleField(r, label, value, ref action, dragSensitivity); } static void DelayedAndDraggableDoubleField(Rect rect, GUIContent label, SerializedProperty property, ref InputEvent inputEvent) { EditorGUI.BeginChangeCheck(); var newValue = DelayedAndDraggableDoubleField(rect, label, property.doubleValue, ref inputEvent); if (EditorGUI.EndChangeCheck()) property.doubleValue = newValue; } static double DelayedAndDraggableDoubleField(Rect rect, GUIContent label, double value, ref InputEvent inputEvent) { var dragSensitivity = NumericFieldDraggerUtility.CalculateFloatDragSensitivity(value); return DelayedAndDraggableDoubleField(rect, label, value, ref inputEvent, dragSensitivity); } static int DelayedAndDraggableIntField(Rect rect, GUIContent label, int value, ref InputEvent inputEvent) { var dragSensitivity = NumericFieldDraggerUtility.CalculateIntDragSensitivity(value); return DelayedAndDraggableIntField(rect, label, value, ref inputEvent, dragSensitivity); } internal static T Clamp(this T val, T min, T max) where T : IComparable { if (val.CompareTo(min) < 0) return min; if (val.CompareTo(max) > 0) return max; return val; } public static Editor GetInspectorForObjects(UnityEngine.Object[] objects, Editor previousEditor) { // create cached editor throws on assembly reload... try { if (objects.Any(x => x != null)) { var director = TimelineWindow.instance.state.editSequence.director; Editor.CreateCachedEditorWithContext(objects, director, null, ref previousEditor); return previousEditor; } } catch (Exception) { } return null; } public static SerializedProperty FindExposedReferenceTableFrom(UnityEngine.Object context) { const string exposedReferencesPropertyPath = "m_ExposedReferences"; if (context as IExposedPropertyTable == null) return null; var so = new SerializedObject(context); return so.FindProperty(exposedReferencesPropertyPath); } } }