using System; using System.Linq; using UnityEditor.Timeline.Actions; using UnityEngine; using UnityEngine.Timeline; namespace UnityEditor.Timeline { static class Gaps { static readonly string kInsertTime = "Insert Time"; public static void Insert(TimelineAsset asset, double at, double amount, double tolerance) { var tracks = asset.flattenedTracks.Where(x => x.lockedInHierarchy == false).ToList(); // gather all clips var clips = tracks.SelectMany(x => x.clips).Where(x => (x.start - at) >= -tolerance).ToList(); var markers = tracks.SelectMany(x => x.GetMarkers()).Where(x => (x.time - at) >= -tolerance).ToList(); // push undo on the tracks for the clips that are being modified UndoExtensions.RegisterClips(clips, kInsertTime); // push the clips foreach (var clip in clips) { clip.start += amount; } // push undos and move the markers UndoExtensions.RegisterMarkers(markers, kInsertTime); foreach (var marker in markers) { marker.time += amount; } TimelineEditor.Refresh(RefreshReason.ContentsModified); } } class PlayheadContextMenu : Manipulator { readonly TimeAreaItem m_TimeAreaItem; static readonly int[] kFrameInsertionValues = {5, 10, 25, 100}; static readonly GUIContent[] kFrameInsertionValuesGuiContents = { EditorGUIUtility.TrTextContent("Insert/Frame/5 Frames"), EditorGUIUtility.TrTextContent("Insert/Frame/10 Frames"), EditorGUIUtility.TrTextContent("Insert/Frame/25 Frames"), EditorGUIUtility.TrTextContent("Insert/Frame/100 Frames") }; static readonly GUIContent kSingleFrameGuiContents = EditorGUIUtility.TrTextContent("Insert/Frame/Single"); static readonly GUIContent kSelectedTimeGuiContents = EditorGUIUtility.TrTextContent("Insert/Selected Time"); public PlayheadContextMenu(TimeAreaItem timeAreaItem) { m_TimeAreaItem = timeAreaItem; } protected override bool ContextClick(Event evt, WindowState state) { if (!m_TimeAreaItem.bounds.Contains(evt.mousePosition)) return false; var tolerance = TimeUtility.GetEpsilon(state.editSequence.time, state.referenceSequence.frameRate); var menu = new GenericMenu(); if (!TimelineWindow.instance.state.editSequence.isReadOnly) { menu.AddItem(kSingleFrameGuiContents, false, () => Gaps.Insert(state.editSequence.asset, state.editSequence.time, 1.0 / state.referenceSequence.frameRate, tolerance) ); for (var i = 0; i != kFrameInsertionValues.Length; ++i) { double f = kFrameInsertionValues[i]; menu.AddItem( kFrameInsertionValuesGuiContents[i], false, () => Gaps.Insert(state.editSequence.asset, state.editSequence.time, f / state.referenceSequence.frameRate, tolerance) ); } var playRangeTime = state.playRange; if (playRangeTime.y > playRangeTime.x) { menu.AddItem(kSelectedTimeGuiContents, false, () => Gaps.Insert(state.editSequence.asset, playRangeTime.x, playRangeTime.y - playRangeTime.x, TimeUtility.GetEpsilon(playRangeTime.x, state.referenceSequence.frameRate)) ); } } menu.AddItem(EditorGUIUtility.TrTextContent("Select/Clips Ending Before"), false, () => SelectClipsEndingBefore(state)); menu.AddItem(EditorGUIUtility.TrTextContent("Select/Clips Starting Before"), false, () => SelectClipsStartingBefore(state)); menu.AddItem(EditorGUIUtility.TrTextContent("Select/Clips Ending After"), false, () => SelectClipsEndingAfter(state)); menu.AddItem(EditorGUIUtility.TrTextContent("Select/Clips Starting After"), false, () => SelectClipsStartingAfter(state)); menu.AddItem(EditorGUIUtility.TrTextContent("Select/Clips Intersecting"), false, () => SelectClipsIntersecting(state)); menu.AddItem(EditorGUIUtility.TrTextContent("Select/Blends Intersecting"), false, () => SelectBlendsIntersecting(state)); menu.ShowAsContext(); return true; } internal static void SelectClipsEndingBefore(WindowState state) { var tolerance = TimeUtility.GetEpsilon(state.editSequence.time, state.referenceSequence.frameRate); SelectMenuCallback(x => x.end < state.editSequence.time + tolerance, state); } internal static void SelectClipsStartingBefore(WindowState state) { var tolerance = TimeUtility.GetEpsilon(state.editSequence.time, state.referenceSequence.frameRate); SelectMenuCallback(x => x.start < state.editSequence.time + tolerance, state); } internal static void SelectClipsEndingAfter(WindowState state) { var tolerance = TimeUtility.GetEpsilon(state.editSequence.time, state.referenceSequence.frameRate); SelectMenuCallback(x => x.end - state.editSequence.time >= -tolerance, state); } internal static void SelectClipsStartingAfter(WindowState state) { var tolerance = TimeUtility.GetEpsilon(state.editSequence.time, state.referenceSequence.frameRate); SelectMenuCallback(x => x.start - state.editSequence.time >= -tolerance, state); } internal static void SelectClipsIntersecting(WindowState state) { SelectMenuCallback(x => x.start <= state.editSequence.time && state.editSequence.time <= x.end, state); } internal static void SelectBlendsIntersecting(WindowState state) { SelectMenuCallback(x => SelectBlendingIntersecting(x, state.editSequence.time), state); } static bool SelectBlendingIntersecting(TimelineClip clip, double time) { return clip.start <= time && time <= clip.end && ( (time <= clip.start + clip.blendInDuration) || (time >= clip.end - clip.blendOutDuration) ); } static void SelectMenuCallback(Func selector, WindowState state) { var allClips = state.GetWindow().treeView.allClipGuis; if (allClips == null) return; SelectionManager.Clear(); for (var i = 0; i != allClips.Count; ++i) { var c = allClips[i]; if (c != null && c.clip != null && c.clip.parentTrack.lockedInHierarchy == false && selector(c.clip)) { SelectionManager.Add(c.clip); } } } } class TimeAreaContextMenu : Manipulator { protected override bool ContextClick(Event evt, WindowState state) { if (state.timeAreaRect.Contains(Event.current.mousePosition)) { var menu = new GenericMenu(); AddTimeAreaMenuItems(menu, state); menu.ShowAsContext(); return true; } return false; } internal static void AddTimeAreaMenuItems(GenericMenu menu, WindowState state) { foreach (var value in Enum.GetValues(typeof(TimelineAsset.DurationMode))) { var mode = (TimelineAsset.DurationMode)value; var item = EditorGUIUtility.TextContent(string.Format(TimelineWindow.Styles.DurationModeText, L10n.Tr(ObjectNames.NicifyVariableName(mode.ToString())))); if (state.recording || state.IsEditingASubTimeline() || state.editSequence.asset == null || state.editSequence.isReadOnly) menu.AddDisabledItem(item); else menu.AddItem(item, state.editSequence.asset.durationMode == mode, () => SelectDurationCallback(state, mode)); menu.AddItem(DirectorStyles.showMarkersOnTimeline, state.showMarkerHeader, () => Invoker.InvokeWithSelected()); } } static void SelectDurationCallback(WindowState state, TimelineAsset.DurationMode mode) { if (mode == state.editSequence.asset.durationMode) return; UndoExtensions.RegisterTimeline(state.editSequence.asset, "Duration Mode"); // if we switched from Auto to Fixed, use the auto duration as the new fixed duration so the end marker stay in the same position. if (state.editSequence.asset.durationMode == TimelineAsset.DurationMode.BasedOnClips && mode == TimelineAsset.DurationMode.FixedLength) { state.editSequence.asset.UpdateFixedDurationWithItemsDuration(); } state.editSequence.asset.durationMode = mode; state.UpdateRootPlayableDuration(state.editSequence.duration); } } class Scrub : Manipulator { readonly Func m_OnMouseDown; readonly Action m_OnMouseDrag; readonly System.Action m_OnMouseUp; bool m_IsCaptured; public Scrub(Func onMouseDown, Action onMouseDrag, System.Action onMouseUp) { m_OnMouseDown = onMouseDown; m_OnMouseDrag = onMouseDrag; m_OnMouseUp = onMouseUp; } protected override bool MouseDown(Event evt, WindowState state) { if (evt.button != 0) return false; if (!m_OnMouseDown(evt, state)) return false; state.AddCaptured(this); m_IsCaptured = true; return true; } protected override bool MouseUp(Event evt, WindowState state) { if (!m_IsCaptured) return false; m_IsCaptured = false; state.RemoveCaptured(this); m_OnMouseUp(); return true; } protected override bool MouseDrag(Event evt, WindowState state) { if (!m_IsCaptured) return false; m_OnMouseDrag(state.GetSnappedTimeAtMousePosition(evt.mousePosition)); return true; } } class TimeAreaItem : Control { public Color headColor { get; set; } public Color lineColor { get; set; } public bool drawLine { get; set; } public bool drawHead { get; set; } public bool canMoveHead { get; set; } public string tooltip { get; set; } public Vector2 boundOffset { get; set; } readonly GUIContent m_HeaderContent = new GUIContent(); readonly GUIStyle m_Style; readonly Tooltip m_Tooltip; Rect m_BoundingRect; float widgetHeight { get { return m_Style.fixedHeight; } } float widgetWidth { get { return m_Style.fixedWidth; } } public Rect bounds { get { Rect r = m_BoundingRect; r.y = TimelineWindow.instance.state.timeAreaRect.yMax - widgetHeight; r.position += boundOffset; return r; } } public GUIStyle style { get { return m_Style; } } public bool showTooltip { get; set; } // is this the first frame the drag callback is being invoked public bool firstDrag { get; private set; } public TimeAreaItem(GUIStyle style, Action onDrag) { m_Style = style; headColor = Color.white; var scrub = new Scrub( (evt, state) => { firstDrag = true; return state.timeAreaRect.Contains(evt.mousePosition) && bounds.Contains(evt.mousePosition); }, (d) => { if (onDrag != null) onDrag(d); firstDrag = false; }, () => { showTooltip = false; firstDrag = false; } ); AddManipulator(scrub); lineColor = m_Style.normal.textColor; drawLine = true; drawHead = true; canMoveHead = false; tooltip = string.Empty; boundOffset = Vector2.zero; m_Tooltip = new Tooltip(DirectorStyles.Instance.displayBackground, DirectorStyles.Instance.tinyFont); } public void Draw(Rect rect, WindowState state, double time) { var clipRect = new Rect(0.0f, 0.0f, TimelineWindow.instance.position.width, TimelineWindow.instance.position.height); clipRect.xMin += state.sequencerHeaderWidth; using (new GUIViewportScope(clipRect)) { Vector2 windowCoordinate = rect.min; windowCoordinate.y += 4.0f; windowCoordinate.x = state.TimeToPixel(time); m_BoundingRect = new Rect((windowCoordinate.x - widgetWidth / 2.0f), windowCoordinate.y, widgetWidth, widgetHeight); // Do not paint if the time cursor goes outside the timeline bounds... if (Event.current.type == EventType.Repaint) { if (m_BoundingRect.xMax < state.timeAreaRect.xMin) return; if (m_BoundingRect.xMin > state.timeAreaRect.xMax) return; } var top = new Vector3(windowCoordinate.x, rect.y - DirectorStyles.kDurationGuiThickness); var bottom = new Vector3(windowCoordinate.x, rect.yMax); if (drawLine) { Rect lineRect = Rect.MinMaxRect(top.x - 0.5f, top.y, bottom.x + 0.5f, bottom.y); EditorGUI.DrawRect(lineRect, lineColor); } if (drawHead && Event.current.type == EventType.Repaint) { Color c = GUI.color; GUI.color = headColor; style.Draw(bounds, m_HeaderContent, false, false, false, false); GUI.color = c; if (canMoveHead) EditorGUIUtility.AddCursorRect(bounds, MouseCursor.MoveArrow); } if (showTooltip) { m_Tooltip.text = TimeReferenceUtility.ToTimeString(time); Vector2 position = bounds.position; position.y = state.timeAreaRect.y; position.y -= m_Tooltip.bounds.height; position.x -= Mathf.Abs(m_Tooltip.bounds.width - bounds.width) / 2.0f; Rect tooltipBounds = bounds; tooltipBounds.position = position; m_Tooltip.bounds = tooltipBounds; m_Tooltip.Draw(); } } } } }