407 lines
15 KiB
C#
407 lines
15 KiB
C#
|
using System;
|
||
|
using System.Linq;
|
||
|
using UnityEditor.Timeline.Actions;
|
||
|
using UnityEngine;
|
||
|
using UnityEngine.Timeline;
|
||
|
|
||
|
namespace UnityEditor.Timeline
|
||
|
{
|
||
|
static class Gaps
|
||
|
{
|
||
|
static readonly string kInsertTime = L10n.Tr("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 =
|
||
|
{
|
||
|
L10n.TextContent("Insert/Frame/5 Frames"),
|
||
|
L10n.TextContent("Insert/Frame/10 Frames"),
|
||
|
L10n.TextContent("Insert/Frame/25 Frames"),
|
||
|
L10n.TextContent("Insert/Frame/100 Frames")
|
||
|
};
|
||
|
|
||
|
static readonly GUIContent kSingleFrameGuiContents = L10n.TextContent("Insert/Frame/Single");
|
||
|
static readonly GUIContent kSelectedTimeGuiContents = L10n.TextContent("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(L10n.TextContent("Select/Clips Ending Before"), false, () => SelectClipsEndingBefore(state));
|
||
|
menu.AddItem(L10n.TextContent("Select/Clips Starting Before"), false, () => SelectClipsStartingBefore(state));
|
||
|
menu.AddItem(L10n.TextContent("Select/Clips Ending After"), false, () => SelectClipsEndingAfter(state));
|
||
|
menu.AddItem(L10n.TextContent("Select/Clips Starting After"), false, () => SelectClipsStartingAfter(state));
|
||
|
menu.AddItem(L10n.TextContent("Select/Clips Intersecting"), false, () => SelectClipsIntersecting(state));
|
||
|
menu.AddItem(L10n.TextContent("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<TimelineClip, bool> 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.GetParentTrack().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, () => state.showMarkerHeader = !state.showMarkerHeader);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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<Event, WindowState, bool> m_OnMouseDown;
|
||
|
readonly Action<double> m_OnMouseDrag;
|
||
|
readonly System.Action m_OnMouseUp;
|
||
|
|
||
|
bool m_IsCaptured;
|
||
|
|
||
|
public Scrub(Func<Event, WindowState, bool> onMouseDown, Action<double> 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<double> 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();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|