This commit is contained in:
2021-06-13 10:28:03 +02:00
parent eb70603c85
commit df2d24cbd3
7487 changed files with 943244 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Action context to be used by actions.
/// </summary>
/// <seealso cref="Invoker"/>
/// <seealso cref="TimelineAction"/>
public struct ActionContext
{
IEnumerable<TrackAsset> m_Tracks;
IEnumerable<TimelineClip> m_Clips;
IEnumerable<IMarker> m_Markers;
/// <summary>
/// The Timeline asset that is currently opened in the Timeline window.
/// </summary>
public TimelineAsset timeline;
/// <summary>
/// The PlayableDirector that is used to play the current Timeline asset.
/// </summary>
public PlayableDirector director;
/// <summary>
/// Time based on the position of the cursor on the timeline (in seconds).
/// null if the time is not available (in case of a shortcut for example).
/// </summary>
public double? invocationTime;
/// <summary>
/// Tracks that will be used by the actions.
/// </summary>
public IEnumerable<TrackAsset> tracks
{
get => m_Tracks ?? Enumerable.Empty<TrackAsset>();
set => m_Tracks = value;
}
/// <summary>
/// Clips that will be used by the actions.
/// </summary>
public IEnumerable<TimelineClip> clips
{
get => m_Clips ?? Enumerable.Empty<TimelineClip>();
set => m_Clips = value;
}
/// <summary>
/// Markers that will be used by the actions.
/// </summary>
public IEnumerable<IMarker> markers
{
get => m_Markers ?? Enumerable.Empty<IMarker>();
set => m_Markers = value;
}
}
}

View File

@@ -0,0 +1,306 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline.Actions
{
static class ActionManager
{
static bool s_ShowActionTriggeredByShortcut = false;
public static readonly IReadOnlyList<TimelineAction> TimelineActions = InstantiateClassesOfType<TimelineAction>();
public static readonly IReadOnlyList<ClipAction> ClipActions = InstantiateClassesOfType<ClipAction>();
public static readonly IReadOnlyList<TrackAction> TrackActions = InstantiateClassesOfType<TrackAction>();
public static readonly IReadOnlyList<MarkerAction> MarkerActions = InstantiateClassesOfType<MarkerAction>();
public static readonly IReadOnlyList<TimelineAction> TimelineActionsWithShortcuts = ActionsWithShortCuts(TimelineActions);
public static readonly IReadOnlyList<ClipAction> ClipActionsWithShortcuts = ActionsWithShortCuts(ClipActions);
public static readonly IReadOnlyList<TrackAction> TrackActionsWithShortcuts = ActionsWithShortCuts(TrackActions);
public static readonly IReadOnlyList<MarkerAction> MarkerActionsWithShortcuts = ActionsWithShortCuts(MarkerActions);
public static readonly HashSet<Type> ActionsWithAutoUndo = TypesWithAttribute<ApplyDefaultUndoAttribute>();
public static TU GetCachedAction<T, TU>(this IReadOnlyList<TU> list) where T : TU
{
return list.FirstOrDefault(x => x.GetType() == typeof(T));
}
public static void GetMenuEntries(IReadOnlyList<TimelineAction> actions, Vector2? mousePos, List<MenuActionItem> menuItems)
{
var globalContext = TimelineEditor.CurrentContext(mousePos);
foreach (var action in actions)
{
try
{
BuildMenu(action, globalContext, menuItems);
}
catch (Exception e)
{
Debug.LogException(e);
}
}
}
public static void GetMenuEntries(IReadOnlyList<TrackAction> actions, List<MenuActionItem> menuItems)
{
var tracks = SelectionManager.SelectedTracks();
if (!tracks.Any())
return;
foreach (var action in actions)
{
try
{
BuildMenu(action, tracks, menuItems);
}
catch (Exception e)
{
Debug.LogException(e);
}
}
}
public static void GetMenuEntries(IReadOnlyList<ClipAction> actions, List<MenuActionItem> menuItems)
{
var clips = SelectionManager.SelectedClips();
bool any = clips.Any();
if (!clips.Any())
return;
foreach (var action in actions)
{
try
{
if (action is EditSubTimeline editSubTimelineAction)
editSubTimelineAction.AddMenuItem(menuItems);
else if (any)
BuildMenu(action, clips, menuItems);
}
catch (Exception e)
{
Debug.LogException(e);
}
}
}
public static void GetMenuEntries(IReadOnlyList<MarkerAction> actions, List<MenuActionItem> menuItems)
{
var markers = SelectionManager.SelectedMarkers();
if (!markers.Any())
return;
foreach (var action in actions)
{
try
{
BuildMenu(action, markers, menuItems);
}
catch (Exception e)
{
Debug.LogException(e);
}
}
}
static void BuildMenu(TimelineAction action, ActionContext context, List<MenuActionItem> menuItems)
{
BuildMenu(action, action.Validate(context), () => ExecuteTimelineAction(action, context), menuItems);
}
static void BuildMenu(TrackAction action, IEnumerable<TrackAsset> tracks, List<MenuActionItem> menuItems)
{
BuildMenu(action, action.Validate(tracks), () => ExecuteTrackAction(action, tracks), menuItems);
}
static void BuildMenu(ClipAction action, IEnumerable<TimelineClip> clips, List<MenuActionItem> menuItems)
{
BuildMenu(action, action.Validate(clips), () => ExecuteClipAction(action, clips), menuItems);
}
static void BuildMenu(MarkerAction action, IEnumerable<IMarker> markers, List<MenuActionItem> menuItems)
{
BuildMenu(action, action.Validate(markers), () => ExecuteMarkerAction(action, markers), menuItems);
}
static void BuildMenu(IAction action, ActionValidity validity, GenericMenu.MenuFunction executeFunction, List<MenuActionItem> menuItems)
{
var menuAttribute = action.GetType().GetCustomAttribute<MenuEntryAttribute>(false);
if (menuAttribute == null)
return;
if (validity == ActionValidity.NotApplicable)
return;
var menuActionItem = new MenuActionItem
{
state = validity,
entryName = action.GetMenuEntryName(),
priority = menuAttribute.priority,
category = menuAttribute.subMenuPath,
isActiveInMode = action.IsActionActiveInMode(TimelineWindow.instance.currentMode.mode),
shortCut = action.GetShortcut(),
callback = executeFunction,
isChecked = action.IsChecked()
};
menuItems.Add(menuActionItem);
}
internal static void BuildMenu(GenericMenu menu, List<MenuActionItem> items)
{
// sorted the outer menu by priority, then sort the innermenu by priority
var sortedItems =
items.GroupBy(x => string.IsNullOrEmpty(x.category) ? x.entryName : x.category).OrderBy(x => x.Min(y => y.priority)).SelectMany(x => x.OrderBy(z => z.priority));
int lastPriority = Int32.MinValue;
string lastCategory = string.Empty;
foreach (var s in sortedItems)
{
if (s.state == ActionValidity.NotApplicable)
continue;
var priority = s.priority;
if (lastPriority != int.MinValue && priority / MenuPriority.separatorAt > lastPriority / MenuPriority.separatorAt)
{
string path = string.Empty;
if (lastCategory == s.category)
path = s.category;
menu.AddSeparator(path);
}
lastPriority = priority;
lastCategory = s.category;
string entry = s.category + s.entryName;
if (!string.IsNullOrEmpty(s.shortCut))
entry += " " + s.shortCut;
if (s.state == ActionValidity.Valid && s.isActiveInMode)
menu.AddItem(new GUIContent(entry), s.isChecked, s.callback);
else
menu.AddDisabledItem(new GUIContent(entry), s.isChecked);
}
}
public static bool HandleShortcut(Event evt)
{
if (EditorGUI.IsEditingTextField())
return false;
return HandleShortcut(evt, TimelineActionsWithShortcuts, (x) => ExecuteTimelineAction(x, TimelineEditor.CurrentContext())) ||
HandleShortcut(evt, ClipActionsWithShortcuts, (x => ExecuteClipAction(x, SelectionManager.SelectedClips()))) ||
HandleShortcut(evt, TrackActionsWithShortcuts, (x => ExecuteTrackAction(x, SelectionManager.SelectedTracks()))) ||
HandleShortcut(evt, MarkerActionsWithShortcuts, (x => ExecuteMarkerAction(x, SelectionManager.SelectedMarkers())));
}
public static bool HandleShortcut<T>(Event evt, IReadOnlyList<T> actions, Func<T, bool> invoke) where T : class, IAction
{
for (int i = 0; i < actions.Count; i++)
{
var action = actions[i];
var attr = action.GetType().GetCustomAttributes(typeof(ShortcutAttribute), true);
foreach (ShortcutAttribute shortcut in attr)
{
if (shortcut.MatchesEvent(evt))
{
if (s_ShowActionTriggeredByShortcut)
Debug.Log(action.GetType().Name);
if (!action.IsActionActiveInMode(TimelineWindow.instance.currentMode.mode))
continue;
if (invoke(action))
return true;
}
}
}
return false;
}
public static bool ExecuteTimelineAction(TimelineAction timelineAction, ActionContext context)
{
if (timelineAction.Validate(context) == ActionValidity.Valid)
{
if (timelineAction.HasAutoUndo())
UndoExtensions.RegisterContext(context, timelineAction.GetUndoName());
return timelineAction.Execute(context);
}
return false;
}
public static bool ExecuteTrackAction(TrackAction trackAction, IEnumerable<TrackAsset> tracks)
{
if (tracks != null && tracks.Any() && trackAction.Validate(tracks) == ActionValidity.Valid)
{
if (trackAction.HasAutoUndo())
UndoExtensions.RegisterTracks(tracks, trackAction.GetUndoName());
return trackAction.Execute(tracks);
}
return false;
}
public static bool ExecuteClipAction(ClipAction clipAction, IEnumerable<TimelineClip> clips)
{
if (clips != null && clips.Any() && clipAction.Validate(clips) == ActionValidity.Valid)
{
if (clipAction.HasAutoUndo())
UndoExtensions.RegisterClips(clips, clipAction.GetUndoName());
return clipAction.Execute(clips);
}
return false;
}
public static bool ExecuteMarkerAction(MarkerAction markerAction, IEnumerable<IMarker> markers)
{
if (markers != null && markers.Any() && markerAction.Validate(markers) == ActionValidity.Valid)
{
if (markerAction.HasAutoUndo())
UndoExtensions.RegisterMarkers(markers, markerAction.GetUndoName());
return markerAction.Execute(markers);
}
return false;
}
static List<T> InstantiateClassesOfType<T>() where T : class
{
var typeCollection = TypeCache.GetTypesDerivedFrom(typeof(T));
var list = new List<T>(typeCollection.Count);
for (int i = 0; i < typeCollection.Count; i++)
{
if (typeCollection[i].IsAbstract || typeCollection[i].IsGenericType)
continue;
if (typeCollection[i].GetConstructor(Type.EmptyTypes) == null)
{
Debug.LogWarning($"{typeCollection[i].FullName} requires a default constructor to be automatically instantiated by Timeline");
continue;
}
list.Add((T)Activator.CreateInstance(typeCollection[i]));
}
return list;
}
static List<T> ActionsWithShortCuts<T>(IReadOnlyList<T> list)
{
return list.Where(x => x.GetType().GetCustomAttributes(typeof(ShortcutAttribute), true).Length > 0).ToList();
}
static HashSet<System.Type> TypesWithAttribute<T>() where T : Attribute
{
var hashSet = new HashSet<System.Type>();
var typeCollection = TypeCache.GetTypesWithAttribute(typeof(T));
for (int i = 0; i < typeCollection.Count; i++)
{
hashSet.Add(typeCollection[i]);
}
return hashSet;
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Base class for a clip action.
/// Inherit from this class to make an action that would react on selected clips after a menu click and/or a key shortcut.
/// </summary>
/// <example>
/// Simple Clip Action example (with context menu and shortcut support).
/// <code source="../../DocCodeExamples/ActionExamples.cs" region="declare-sampleClipAction" title="SampleClipAction"/>
/// </example>
/// <remarks>
/// To add an action as a menu item in the Timeline context menu, add <see cref="MenuEntryAttribute"/> on the action class.
/// To make an action to react to a shortcut, use the Shortcut Manager API with <see cref="TimelineShortcutAttribute"/>.
/// <seealso cref="UnityEditor.ShortcutManagement.ShortcutAttribute"/>
/// </remarks>
[ActiveInMode(TimelineModes.Default)]
public abstract class ClipAction : IAction
{
/// <summary>
/// Execute the action based on clips.
/// </summary>
/// <param name="clips">clips that the action will act on.</param>
/// <returns>Returns true if the action has been correctly executed, false otherwise.</returns>
public abstract bool Execute(IEnumerable<TimelineClip> clips);
/// <summary>
/// Defines the validity of an Action for a given set of clips.
/// </summary>
/// <param name="clips">The clips that the action will act on.</param>
/// <returns>The validity of the set of clips.</returns>
public abstract ActionValidity Validate(IEnumerable<TimelineClip> clips);
}
}

View File

@@ -0,0 +1,392 @@
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using UnityEditor.Timeline.Actions;
using UnityEngine;
using UnityEngine.Timeline;
using UnityEngine.Playables;
namespace UnityEditor.Timeline
{
[MenuEntry("Edit in Animation Window", MenuPriority.ClipEditActionSection.editInAnimationWindow), UsedImplicitly]
class EditClipInAnimationWindow : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
if (!GetEditableClip(clips, out _, out _))
return ActionValidity.NotApplicable;
return ActionValidity.Valid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
TimelineClip clip;
AnimationClip clipToEdit;
if (!GetEditableClip(clips, out clip, out clipToEdit))
return false;
GameObject gameObject = null;
if (TimelineEditor.inspectedDirector != null)
gameObject = TimelineUtility.GetSceneGameObject(TimelineEditor.inspectedDirector, clip.GetParentTrack());
var timeController = TimelineAnimationUtilities.CreateTimeController(clip);
TimelineAnimationUtilities.EditAnimationClipWithTimeController(
clipToEdit, timeController, clip.animationClip != null ? gameObject : null);
return true;
}
private static bool GetEditableClip(IEnumerable<TimelineClip> clips, out TimelineClip clip, out AnimationClip animClip)
{
clip = null;
animClip = null;
if (clips.Count() != 1)
return false;
clip = clips.FirstOrDefault();
if (clip == null)
return false;
if (clip.animationClip != null)
animClip = clip.animationClip;
else if (clip.curves != null && !clip.curves.empty)
animClip = clip.curves;
return animClip != null;
}
}
[MenuEntry("Edit Sub-Timeline", MenuPriority.ClipEditActionSection.editSubTimeline), UsedImplicitly]
class EditSubTimeline : ClipAction
{
private static readonly string MultiItemPrefix = "Edit Sub-Timelines/";
private static readonly string SingleItemPrefix = "Edit ";
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
if (clips == null || clips.Count() != 1 || TimelineEditor.inspectedDirector == null)
return ActionValidity.NotApplicable;
var clip = clips.First();
var directors = TimelineUtility.GetSubTimelines(clip, TimelineEditor.inspectedDirector);
return directors.Any(x => x != null) ? ActionValidity.Valid : ActionValidity.NotApplicable;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
if (Validate(clips) != ActionValidity.Valid) return false;
var clip = clips.First();
var directors = TimelineUtility.GetSubTimelines(clip, TimelineEditor.inspectedDirector);
ExecuteInternal(directors, 0, clip);
return true;
}
static void ExecuteInternal(IList<PlayableDirector> directors, int directorIndex, TimelineClip clip)
{
SelectionManager.Clear();
TimelineWindow.instance.SetCurrentTimeline(directors[directorIndex], clip);
}
internal void AddMenuItem(List<MenuActionItem> menuItems)
{
var clips = TimelineEditor.selectedClips;
if (clips == null || clips.Length != 1)
return;
var mode = TimelineWindow.instance.currentMode.mode;
MenuEntryAttribute menuAttribute = GetType().GetCustomAttributes(typeof(MenuEntryAttribute), false).OfType<MenuEntryAttribute>().FirstOrDefault();
var menuItem = new MenuActionItem()
{
category = menuAttribute.subMenuPath ?? string.Empty,
entryName = menuAttribute.name,
isActiveInMode = this.IsActionActiveInMode(mode),
priority = menuAttribute.priority,
state = Validate(clips),
callback = null
};
var subDirectors = TimelineUtility.GetSubTimelines(clips[0], TimelineEditor.inspectedDirector);
if (subDirectors.Count == 1)
{
menuItem.entryName = SingleItemPrefix + DisplayNameHelper.GetDisplayName(subDirectors[0]);
menuItem.callback = () =>
{
Execute(clips);
};
menuItems.Add(menuItem);
}
else
{
for (int i = 0; i < subDirectors.Count; i++)
{
var index = i;
menuItem.category = MultiItemPrefix;
menuItem.entryName = DisplayNameHelper.GetDisplayName(subDirectors[i]);
menuItem.callback = () =>
{
ExecuteInternal(subDirectors, index, clips[0]);
};
menuItems.Add(menuItem);
}
}
}
}
[MenuEntry("Editing/Trim Start", MenuPriority.ClipActionSection.trimStart)]
[Shortcut(Shortcuts.Clip.trimStart), UsedImplicitly]
class TrimStart : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
return clips.All(x => TimelineEditor.inspectedSequenceTime <= x.start || TimelineEditor.inspectedSequenceTime >= x.start + x.duration) ? ActionValidity.Invalid : ActionValidity.Valid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.TrimStart(clips, TimelineEditor.inspectedSequenceTime);
}
}
[MenuEntry("Editing/Trim End", MenuPriority.ClipActionSection.trimEnd), UsedImplicitly]
[Shortcut(Shortcuts.Clip.trimEnd)]
class TrimEnd : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
return clips.All(x => TimelineEditor.inspectedSequenceTime <= x.start || TimelineEditor.inspectedSequenceTime >= x.start + x.duration) ? ActionValidity.Invalid : ActionValidity.Valid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.TrimEnd(clips, TimelineEditor.inspectedSequenceTime);
}
}
[Shortcut(Shortcuts.Clip.split)]
[MenuEntry("Editing/Split", MenuPriority.ClipActionSection.split), UsedImplicitly]
class Split : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
return clips.All(x => TimelineEditor.inspectedSequenceTime <= x.start || TimelineEditor.inspectedSequenceTime >= x.start + x.duration) ? ActionValidity.Invalid : ActionValidity.Valid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
bool success = ClipModifier.Split(clips, TimelineEditor.inspectedSequenceTime, TimelineEditor.inspectedDirector);
if (success)
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
return success;
}
}
[MenuEntry("Editing/Complete Last Loop", MenuPriority.ClipActionSection.completeLastLoop), UsedImplicitly]
class CompleteLastLoop : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
bool canDisplay = clips.Any(TimelineHelpers.HasUsableAssetDuration);
return canDisplay ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.CompleteLastLoop(clips);
}
}
[MenuEntry("Editing/Trim Last Loop", MenuPriority.ClipActionSection.trimLastLoop), UsedImplicitly]
class TrimLastLoop : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
bool canDisplay = clips.Any(TimelineHelpers.HasUsableAssetDuration);
return canDisplay ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.TrimLastLoop(clips);
}
}
[MenuEntry("Editing/Match Duration", MenuPriority.ClipActionSection.matchDuration), UsedImplicitly]
class MatchDuration : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
return clips.Count() > 1 ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.MatchDuration(clips);
}
}
[MenuEntry("Editing/Double Speed", MenuPriority.ClipActionSection.doubleSpeed), UsedImplicitly]
class DoubleSpeed : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
bool canDisplay = clips.All(x => x.SupportsSpeedMultiplier());
return canDisplay ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.DoubleSpeed(clips);
}
}
[MenuEntry("Editing/Half Speed", MenuPriority.ClipActionSection.halfSpeed), UsedImplicitly]
class HalfSpeed : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
bool canDisplay = clips.All(x => x.SupportsSpeedMultiplier());
return canDisplay ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.HalfSpeed(clips);
}
}
[MenuEntry("Editing/Reset Duration", MenuPriority.ClipActionSection.resetDuration), UsedImplicitly]
class ResetDuration : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
bool canDisplay = clips.Any(TimelineHelpers.HasUsableAssetDuration);
return canDisplay ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.ResetEditing(clips);
}
}
[MenuEntry("Editing/Reset Speed", MenuPriority.ClipActionSection.resetSpeed), UsedImplicitly]
class ResetSpeed : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
bool canDisplay = clips.All(x => x.SupportsSpeedMultiplier());
return canDisplay ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.ResetSpeed(clips);
}
}
[MenuEntry("Editing/Reset All", MenuPriority.ClipActionSection.resetAll), UsedImplicitly]
class ResetAll : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
bool canDisplay = clips.Any(TimelineHelpers.HasUsableAssetDuration) || clips.All(x => x.SupportsSpeedMultiplier());
return canDisplay ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
var speedResult = ClipModifier.ResetSpeed(clips);
var editResult = ClipModifier.ResetEditing(clips);
return speedResult || editResult;
}
}
[MenuEntry("Tile", MenuPriority.ClipActionSection.tile), UsedImplicitly]
class Tile : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
return clips.Count() > 1 ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
return ClipModifier.Tile(clips);
}
}
[MenuEntry("Find Source Asset", MenuPriority.ClipActionSection.findSourceAsset), UsedImplicitly]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class FindSourceAsset : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
if (clips.Count() > 1)
return ActionValidity.Invalid;
if (GetUnderlyingAsset(clips.First()) == null)
return ActionValidity.Invalid;
return ActionValidity.Valid;
}
public override bool Execute(IEnumerable<TimelineClip> clips)
{
EditorGUIUtility.PingObject(GetUnderlyingAsset(clips.First()));
return true;
}
private static UnityEngine.Object GetExternalPlayableAsset(TimelineClip clip)
{
if (clip.asset == null)
return null;
if ((clip.asset.hideFlags & HideFlags.HideInHierarchy) != 0)
return null;
return clip.asset;
}
private static UnityEngine.Object GetUnderlyingAsset(TimelineClip clip)
{
var asset = clip.asset as ScriptableObject;
if (asset == null)
return null;
var fields = ObjectReferenceField.FindObjectReferences(asset.GetType());
if (fields.Length == 0)
return GetExternalPlayableAsset(clip);
// Find the first non-null field
foreach (var field in fields)
{
// skip scene refs in asset mode
if (TimelineEditor.inspectedDirector == null && field.isSceneReference)
continue;
var obj = field.Find(asset, TimelineEditor.inspectedDirector);
if (obj != null)
return obj;
}
return GetExternalPlayableAsset(clip);
}
}
class CopyClipsToClipboard : ClipAction
{
public override ActionValidity Validate(IEnumerable<TimelineClip> clips) => ActionValidity.Valid;
public override bool Execute(IEnumerable<TimelineClip> clips)
{
TimelineEditor.clipboard.CopyItems(clips.ToItems());
return true;
}
}
}

View File

@@ -0,0 +1,123 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using UnityEditor.ShortcutManagement;
using UnityEngine;
namespace UnityEditor.Timeline.Actions
{
/// interface indicating an Action class
interface IAction {}
/// extension methods for IActions
static class ActionExtensions
{
const string kActionPostFix = "Action";
public static string GetUndoName(this IAction action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
var attr = action.GetType().GetCustomAttribute<ApplyDefaultUndoAttribute>(false);
if (attr != null && !string.IsNullOrWhiteSpace(attr.UndoTitle))
return attr.UndoTitle;
return action.GetDisplayName();
}
public static string GetMenuEntryName(this IAction action)
{
var menuAction = action as IMenuName;
if (menuAction != null && !string.IsNullOrWhiteSpace(menuAction.menuName))
return menuAction.menuName;
var attr = action.GetType().GetCustomAttribute<MenuEntryAttribute>(false);
if (attr != null && !string.IsNullOrWhiteSpace(attr.name))
return attr.name;
return action.GetDisplayName();
}
public static string GetDisplayName(this IAction action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
var attr = action.GetType().GetCustomAttribute<DisplayNameAttribute>(false);
if (attr != null && !string.IsNullOrEmpty(attr.DisplayName))
return attr.DisplayName;
var name = action.GetType().Name;
if (name.EndsWith(kActionPostFix))
return ObjectNames.NicifyVariableName(name.Substring(0, name.Length - kActionPostFix.Length));
return ObjectNames.NicifyVariableName(name);
}
public static bool HasAutoUndo(this IAction action)
{
return action != null && ActionManager.ActionsWithAutoUndo.Contains(action.GetType());
}
public static bool IsChecked(this IAction action)
{
return (action is IMenuChecked menuAction) && menuAction.isChecked;
}
public static bool IsActionActiveInMode(this IAction action, TimelineModes mode)
{
var attr = action.GetType().GetCustomAttribute<ActiveInModeAttribute>(true);
return attr != null && (attr.modes & mode) != 0;
}
public static string GetShortcut(this IAction action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
var shortcutAttribute = GetShortcutAttributeForAction(action);
var shortCut = shortcutAttribute == null ? string.Empty : shortcutAttribute.GetMenuShortcut();
if (string.IsNullOrWhiteSpace(shortCut))
{
//Check if there is a static method with attribute
var customShortcutMethod = action.GetType().GetMethods().FirstOrDefault(m => m.GetCustomAttribute<TimelineShortcutAttribute>(true) != null);
if (customShortcutMethod != null)
{
var shortcutId = customShortcutMethod.GetCustomAttribute<TimelineShortcutAttribute>(true).identifier;
var shortcut = ShortcutIntegration.instance.directory.FindShortcutEntry(shortcutId);
if (shortcut != null && shortcut.combinations.Any())
shortCut = KeyCombination.SequenceToMenuString(shortcut.combinations);
}
}
return shortCut;
}
static ShortcutAttribute GetShortcutAttributeForAction(this IAction action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
var shortcutAttributes = action.GetType()
.GetCustomAttributes(typeof(ShortcutAttribute), true)
.Cast<ShortcutAttribute>();
foreach (var shortcutAttribute in shortcutAttributes)
{
if (shortcutAttribute is ShortcutPlatformOverrideAttribute shortcutOverride)
{
if (shortcutOverride.MatchesCurrentPlatform())
return shortcutOverride;
}
else
{
return shortcutAttribute;
}
}
return null;
}
}
}

View File

@@ -0,0 +1,9 @@
using System;
namespace UnityEditor.Timeline
{
interface IMenuChecked
{
bool isChecked { get; }
}
}

View File

@@ -0,0 +1,9 @@
using System;
namespace UnityEditor.Timeline
{
interface IMenuName
{
string menuName { get; }
}
}

View File

@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Class containing methods to invoke actions.
/// </summary>
public static class Invoker
{
/// <summary>
/// Execute a given action with a context parameter.
/// </summary>
/// <typeparam name="T">Action type to execute.</typeparam>
/// <param name="context">Context for the action.</param>
/// <returns>True if the action has been executed, false otherwise.</returns>
public static bool Invoke<T>(this ActionContext context) where T : TimelineAction
{
var action = ActionManager.TimelineActions.GetCachedAction<T, TimelineAction>();
return ActionManager.ExecuteTimelineAction(action, context);
}
/// <summary>
/// Execute a given action with tracks
/// </summary>
/// <typeparam name="T">Action type to execute.</typeparam>
/// <param name="tracks">Tracks that the action will act on.</param>
/// <returns>True if the action has been executed, false otherwise.</returns>
public static bool Invoke<T>(this IEnumerable<TrackAsset> tracks) where T : TrackAction
{
var action = ActionManager.TrackActions.GetCachedAction<T, TrackAction>();
return ActionManager.ExecuteTrackAction(action, tracks);
}
/// <summary>
/// Execute a given action with clips
/// </summary>
/// <typeparam name="T">Action type to execute.</typeparam>
/// <param name="clips">Clips that the action will act on.</param>
/// <returns>True if the action has been executed, false otherwise.</returns>
public static bool Invoke<T>(this IEnumerable<TimelineClip> clips) where T : ClipAction
{
var action = ActionManager.ClipActions.GetCachedAction<T, ClipAction>();
return ActionManager.ExecuteClipAction(action, clips);
}
/// <summary>
/// Execute a given action with markers
/// </summary>
/// <typeparam name="T">Action type to execute.</typeparam>
/// <param name="markers">Markers that the action will act on.</param>
/// <returns>True if the action has been executed, false otherwise.</returns>
public static bool Invoke<T>(this IEnumerable<IMarker> markers) where T : MarkerAction
{
var action = ActionManager.MarkerActions.GetCachedAction<T, MarkerAction>();
return ActionManager.ExecuteMarkerAction(action, markers);
}
/// <summary>
/// Execute a given timeline action with the selected clips, tracks and markers.
/// </summary>
/// <typeparam name="T">Action type to execute.</typeparam>
/// <returns>True if the action has been executed, false otherwise.</returns>
public static bool InvokeWithSelected<T>() where T : TimelineAction
{
return Invoke<T>(TimelineEditor.CurrentContext());
}
/// <summary>
/// Execute a given clip action with the selected clips.
/// </summary>
/// <typeparam name="T">Action type to execute.</typeparam>
/// <returns>True if the action has been executed, false otherwise.</returns>
public static bool InvokeWithSelectedClips<T>() where T : ClipAction
{
return Invoke<T>(SelectionManager.SelectedClips());
}
/// <summary>
/// Execute a given track action with the selected tracks.
/// </summary>
/// <typeparam name="T">Action type to execute.</typeparam>
/// <returns>True if the action has been executed, false otherwise.</returns>
public static bool InvokeWithSelectedTracks<T>() where T : TrackAction
{
return Invoke<T>(SelectionManager.SelectedTracks());
}
/// <summary>
/// Execute a given marker action with the selected markers.
/// </summary>
/// <typeparam name="T">Action type to execute.</typeparam>
/// <returns>True if the action has been executed, false otherwise.</returns>
public static bool InvokeWithSelectedMarkers<T>() where T : MarkerAction
{
return Invoke<T>(SelectionManager.SelectedMarkers());
}
}
}

View File

@@ -0,0 +1,35 @@
using System.Collections.Generic;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Base class for a marker action.
/// Inherit from this class to make an action that would react on selected markers after a menu click and/or a key shortcut.
/// </summary>
/// <example>
/// Simple track Action example (with context menu and shortcut support).
/// <code source="../../DocCodeExamples/ActionExamples.cs" region="declare-sampleMarkerAction" title="SampleMarkerAction"/>
/// </example>
/// <remarks>
/// To add an action as a menu item in the Timeline context menu, add <see cref="MenuEntryAttribute"/> on the action class.
/// To make an action to react to a shortcut, use the Shortcut Manager API with <see cref="TimelineShortcutAttribute"/>.
/// <seealso cref="UnityEditor.ShortcutManagement.ShortcutAttribute"/>
/// </remarks>
[ActiveInMode(TimelineModes.Default)]
public abstract class MarkerAction : IAction
{
/// <summary>
/// Execute the action.
/// </summary>
/// <param name="markers">Markers that will be used for the action. </param>
/// <returns>true if the action has been executed. false otherwise</returns>
public abstract bool Execute(IEnumerable<IMarker> markers);
/// <summary>
/// Defines the validity of an Action for a given set of markers.
/// </summary>
/// <param name="markers">Markers that will be used for the action. </param>
/// <returns>The validity of the set of markers.</returns>
public abstract ActionValidity Validate(IEnumerable<IMarker> markers);
}
}

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using UnityEditor.Timeline.Actions;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
[UsedImplicitly]
class CopyMarkersToClipboard : MarkerAction
{
public override ActionValidity Validate(IEnumerable<IMarker> markers) => ActionValidity.Valid;
public override bool Execute(IEnumerable<IMarker> markers)
{
TimelineEditor.clipboard.CopyItems(markers.ToItems());
return true;
}
}
}

View File

@@ -0,0 +1,36 @@
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Indicates the validity of an action for a given data set.
/// </summary>
public enum ActionValidity
{
/// <summary>
/// Action is valid in the provided context.
/// If the action is linked to a menu item, the menu item will be visible.
/// </summary>
Valid,
/// <summary>
/// Action is not applicable in the current context.
/// If the action is linked to a menu item, the menu item will not be shown.
/// </summary>
NotApplicable,
/// <summary>
/// Action is not valid in the current context.
/// If the action is linked to a menu item, the menu item will be shown but grayed out.
/// </summary>
Invalid
}
struct MenuActionItem
{
public string category;
public string entryName;
public string shortCut;
public int priority;
public bool isActiveInMode;
public ActionValidity state;
public bool isChecked;
public GenericMenu.MenuFunction callback;
}
}

View File

@@ -0,0 +1,381 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.Timeline.Actions;
using UnityEngine;
using UnityEngine.Timeline;
using Object = UnityEngine.Object;
namespace UnityEditor.Timeline
{
static class SequencerContextMenu
{
static class Styles
{
public static readonly string addItemFromAssetTemplate = L10n.Tr("Add {0} From {1}");
public static readonly string addSingleItemFromAssetTemplate = L10n.Tr("Add From {1}");
public static readonly string addItemTemplate = L10n.Tr("Add {0}");
public static readonly string typeSelectorTemplate = L10n.Tr("Select {0}");
public static readonly string trackGroup = L10n.Tr("Track Group");
public static readonly string trackSubGroup = L10n.Tr("Track Sub-Group");
public static readonly string addTrackLayer = L10n.Tr("Add Layer");
public static readonly string layerName = L10n.Tr("Layer {0}");
}
public static void ShowNewTracksContextMenu(ICollection<TrackAsset> tracks, WindowState state)
{
var menu = new GenericMenu();
List<MenuActionItem> items = new List<MenuActionItem>(100);
BuildNewTracksContextMenu(items, tracks, state);
ActionManager.BuildMenu(menu, items);
menu.ShowAsContext();
}
public static void ShowNewTracksContextMenu(ICollection<TrackAsset> tracks, WindowState state, Rect rect)
{
var menu = new GenericMenu();
List<MenuActionItem> items = new List<MenuActionItem>(100);
BuildNewTracksContextMenu(items, tracks, state);
ActionManager.BuildMenu(menu, items);
menu.DropDown(rect);
}
public static void ShowTrackContextMenu(Vector2? mousePosition)
{
var items = new List<MenuActionItem>();
var menu = new GenericMenu();
BuildTrackContextMenu(items, mousePosition);
ActionManager.BuildMenu(menu, items);
menu.ShowAsContext();
}
public static void ShowItemContextMenu(Vector2 mousePosition)
{
var menu = new GenericMenu();
var items = new List<MenuActionItem>();
BuildItemContextMenu(items, mousePosition);
ActionManager.BuildMenu(menu, items);
menu.ShowAsContext();
}
public static void BuildItemContextMenu(List<MenuActionItem> items, Vector2 mousePosition)
{
ActionManager.GetMenuEntries(ActionManager.TimelineActions, mousePosition, items);
ActionManager.GetMenuEntries(ActionManager.ClipActions, items);
ActionManager.GetMenuEntries(ActionManager.MarkerActions, items);
var clips = TimelineEditor.selectedClips;
if (clips.Length > 0)
AddMarkerMenuCommands(items, clips.Select(c => c.GetParentTrack()).Distinct().ToList(), TimelineHelpers.GetCandidateTime(mousePosition));
}
public static void BuildNewTracksContextMenu(List<MenuActionItem> menuItems, ICollection<TrackAsset> parentTracks, WindowState state, string format = null)
{
if (parentTracks == null)
parentTracks = new TrackAsset[0];
if (string.IsNullOrEmpty(format))
format = "{0}";
// Add Group or SubGroup
var title = string.Format(format, parentTracks.Any(t => t != null) ? Styles.trackSubGroup : Styles.trackGroup);
var menuState = ActionValidity.Valid;
if (state.editSequence.isReadOnly)
menuState = ActionValidity.Invalid;
if (parentTracks.Any() && parentTracks.Any(t => t != null && t.lockedInHierarchy))
menuState = ActionValidity.Invalid;
GenericMenu.MenuFunction command = () =>
{
SelectionManager.Clear();
if (parentTracks.Count == 0)
Selection.Add(TimelineHelpers.CreateTrack<GroupTrack>(null, title));
foreach (var parentTrack in parentTracks)
Selection.Add(TimelineHelpers.CreateTrack<GroupTrack>(parentTrack, title));
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
};
menuItems.Add(
new MenuActionItem()
{
category = string.Empty,
entryName = title,
isActiveInMode = true,
priority = MenuPriority.AddItem.addGroup,
state = menuState,
isChecked = false,
callback = command
}
);
var allTypes = TypeUtility.AllTrackTypes().Where(x => x != typeof(GroupTrack) && !TypeUtility.IsHiddenInMenu(x)).ToList();
int builtInPriority = MenuPriority.AddItem.addTrack;
int customPriority = MenuPriority.AddItem.addCustomTrack;
foreach (var trackType in allTypes)
{
var trackItemType = trackType;
command = () =>
{
SelectionManager.Clear();
if (parentTracks.Count == 0)
SelectionManager.Add(TimelineHelpers.CreateTrack((Type)trackItemType, null));
foreach (var parentTrack in parentTracks)
SelectionManager.Add(TimelineHelpers.CreateTrack((Type)trackItemType, parentTrack));
};
menuItems.Add(
new MenuActionItem()
{
category = TimelineHelpers.GetTrackCategoryName(trackType),
entryName = string.Format(format, TimelineHelpers.GetTrackMenuName(trackItemType)),
isActiveInMode = true,
priority = TypeUtility.IsBuiltIn(trackType) ? builtInPriority++ : customPriority++,
state = menuState,
callback = command
}
);
}
}
public static void BuildTrackContextMenu(List<MenuActionItem> items, Vector2? mousePosition)
{
var tracks = SelectionManager.SelectedTracks().ToArray();
if (tracks.Length == 0)
return;
ActionManager.GetMenuEntries(ActionManager.TimelineActions, mousePosition, items);
ActionManager.GetMenuEntries(ActionManager.TrackActions, items);
AddLayeredTrackCommands(items, tracks);
var first = tracks.First().GetType();
var allTheSame = tracks.All(t => t.GetType() == first);
if (allTheSame)
{
if (first != typeof(GroupTrack))
{
var candidateTime = TimelineHelpers.GetCandidateTime(mousePosition, tracks);
AddClipMenuCommands(items, tracks, candidateTime);
AddMarkerMenuCommands(items, tracks, candidateTime);
}
else
{
BuildNewTracksContextMenu(items, tracks, TimelineWindow.instance.state, Styles.addItemTemplate);
}
}
}
static void AddLayeredTrackCommands(List<MenuActionItem> menuItems, ICollection<TrackAsset> tracks)
{
if (tracks.Count == 0)
return;
var layeredType = tracks.First().GetType();
// animation tracks have a special menu.
if (layeredType == typeof(AnimationTrack))
return;
// must implement ILayerable
if (!typeof(UnityEngine.Timeline.ILayerable).IsAssignableFrom(layeredType))
return;
if (tracks.Any(t => t.GetType() != layeredType))
return;
// only supported on the master track no nesting.
if (tracks.Any(t => t.isSubTrack))
return;
var enabled = tracks.All(t => t != null && !t.lockedInHierarchy) && !TimelineWindow.instance.state.editSequence.isReadOnly;
int priority = MenuPriority.AddTrackMenu.addLayerTrack;
GenericMenu.MenuFunction menuCallback = () =>
{
foreach (var track in tracks)
TimelineHelpers.CreateTrack(layeredType, track, string.Format(Styles.layerName, track.GetChildTracks().Count() + 1));
};
var entryName = Styles.addTrackLayer;
menuItems.Add(
new MenuActionItem()
{
category = string.Empty,
entryName = entryName,
isActiveInMode = true,
priority = priority++,
state = enabled ? ActionValidity.Valid : ActionValidity.Invalid,
callback = menuCallback
}
);
}
static void AddClipMenuCommands(List<MenuActionItem> menuItems, ICollection<TrackAsset> tracks, double candidateTime)
{
if (!tracks.Any())
return;
var trackAsset = tracks.First();
var trackType = trackAsset.GetType();
if (tracks.Any(t => t.GetType() != trackType))
return;
var enabled = tracks.All(t => t != null && !t.lockedInHierarchy) && !TimelineWindow.instance.state.editSequence.isReadOnly;
var assetTypes = TypeUtility.GetPlayableAssetsHandledByTrack(trackType);
var visibleAssetTypes = TypeUtility.GetVisiblePlayableAssetsHandledByTrack(trackType);
// skips the name if there is only a single type
var commandNameTemplate = assetTypes.Count() == 1 ? Styles.addSingleItemFromAssetTemplate : Styles.addItemFromAssetTemplate;
int builtInPriority = MenuPriority.AddItem.addClip;
int customPriority = MenuPriority.AddItem.addCustomClip;
foreach (var assetType in assetTypes)
{
var assetItemType = assetType;
var category = TimelineHelpers.GetItemCategoryName(assetType);
Action<Object> onObjectChanged = obj =>
{
if (obj != null)
{
foreach (var t in tracks)
{
TimelineHelpers.CreateClipOnTrack(assetItemType, obj, t, candidateTime);
}
}
};
foreach (var objectReference in TypeUtility.ObjectReferencesForType(assetType))
{
var isSceneReference = objectReference.isSceneReference;
var dataType = objectReference.type;
GenericMenu.MenuFunction menuCallback = () =>
{
ObjectSelector.get.Show(null, dataType, null, isSceneReference, null, (obj) => onObjectChanged(obj), null);
ObjectSelector.get.titleContent = EditorGUIUtility.TrTextContent(string.Format(Styles.typeSelectorTemplate, TypeUtility.GetDisplayName(dataType)));
};
menuItems.Add(
new MenuActionItem()
{
category = category,
entryName = string.Format(commandNameTemplate, TypeUtility.GetDisplayName(assetType), TypeUtility.GetDisplayName(objectReference.type)),
isActiveInMode = true,
priority = TypeUtility.IsBuiltIn(assetType) ? builtInPriority++ : customPriority++,
state = enabled ? ActionValidity.Valid : ActionValidity.Invalid,
callback = menuCallback
}
);
}
}
foreach (var assetType in visibleAssetTypes)
{
var assetItemType = assetType;
var category = TimelineHelpers.GetItemCategoryName(assetType);
var commandName = string.Format(Styles.addItemTemplate, TypeUtility.GetDisplayName(assetType));
GenericMenu.MenuFunction command = () =>
{
foreach (var t in tracks)
{
TimelineHelpers.CreateClipOnTrack(assetItemType, t, candidateTime);
}
};
menuItems.Add(
new MenuActionItem()
{
category = category,
entryName = commandName,
isActiveInMode = true,
priority = TypeUtility.IsBuiltIn(assetItemType) ? builtInPriority++ : customPriority++,
state = enabled ? ActionValidity.Valid : ActionValidity.Invalid,
callback = command
}
);
}
}
static void AddMarkerMenuCommands(List<MenuActionItem> menu, IEnumerable<Type> markerTypes, Action<Type, Object> addMarkerCommand, bool enabled)
{
int builtInPriority = MenuPriority.AddItem.addMarker;
int customPriority = MenuPriority.AddItem.addCustomMarker;
foreach (var markerType in markerTypes)
{
var markerItemType = markerType;
string category = TimelineHelpers.GetItemCategoryName(markerItemType);
menu.Add(
new MenuActionItem()
{
category = category,
entryName = string.Format(Styles.addItemTemplate, TypeUtility.GetDisplayName(markerType)),
isActiveInMode = true,
priority = TypeUtility.IsBuiltIn(markerType) ? builtInPriority++ : customPriority++,
state = enabled ? ActionValidity.Valid : ActionValidity.Invalid,
callback = () => addMarkerCommand(markerItemType, null)
}
);
foreach (var objectReference in TypeUtility.ObjectReferencesForType(markerType))
{
var isSceneReference = objectReference.isSceneReference;
GenericMenu.MenuFunction menuCallback = () =>
{
Type assetDataType = objectReference.type;
ObjectSelector.get.titleContent = EditorGUIUtility.TrTextContent(string.Format(Styles.typeSelectorTemplate, TypeUtility.GetDisplayName(assetDataType)));
ObjectSelector.get.Show(null, assetDataType, null, isSceneReference, null, obj =>
{
if (obj != null)
addMarkerCommand(markerItemType, obj);
}, null);
};
menu.Add(
new MenuActionItem
{
category = TimelineHelpers.GetItemCategoryName(markerItemType),
entryName = string.Format(Styles.addItemFromAssetTemplate, TypeUtility.GetDisplayName(markerType), TypeUtility.GetDisplayName(objectReference.type)),
isActiveInMode = true,
priority = TypeUtility.IsBuiltIn(markerType) ? builtInPriority++ : customPriority++,
state = enabled ? ActionValidity.Valid : ActionValidity.Invalid,
callback = menuCallback
}
);
}
}
}
static void AddMarkerMenuCommands(List<MenuActionItem> menuItems, ICollection<TrackAsset> tracks, double candidateTime)
{
if (tracks.Count == 0)
return;
var enabled = tracks.All(t => !t.lockedInHierarchy) && !TimelineWindow.instance.state.editSequence.isReadOnly;
var addMarkerCommand = new Action<Type, Object>((type, obj) => AddMarkersCallback(tracks, type, candidateTime, obj));
AddMarkerMenuCommands(menuItems, tracks, addMarkerCommand, enabled);
}
static void AddMarkerMenuCommands(List<MenuActionItem> menuItems, ICollection<TrackAsset> tracks, Action<Type, Object> command, bool enabled)
{
var markerTypes = TypeUtility.GetBuiltInMarkerTypes().Union(TypeUtility.GetUserMarkerTypes());
if (tracks != null)
markerTypes = markerTypes.Where(x => tracks.All(track => (track == null) || TypeUtility.DoesTrackSupportMarkerType(track, x))); // null track indicates marker track to be created
AddMarkerMenuCommands(menuItems, markerTypes, command, enabled);
}
static void AddMarkersCallback(ICollection<TrackAsset> targets, Type markerType, double time, Object obj)
{
SelectionManager.Clear();
foreach (var target in targets)
{
var marker = TimelineHelpers.CreateMarkerOnTrack(markerType, obj, target, time);
SelectionManager.Add(marker);
}
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
}
}
}

View File

@@ -0,0 +1,34 @@
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Base class for a timeline action.
/// Inherit from this class to make an action on a timeline after a menu click and/or a key shortcut.
/// </summary>
/// <remarks>
/// To add an action as a menu item in the Timeline context menu, add <see cref="MenuEntryAttribute"/> on the action class.
/// To make an action to react to a shortcut, use the Shortcut Manager API with <see cref="TimelineShortcutAttribute"/>.
/// <seealso cref="UnityEditor.ShortcutManagement.ShortcutAttribute"/>
/// <seealso cref="ActiveInModeAttribute"/>
/// </remarks>
/// <example>
/// Simple Timeline Action example (with context menu and shortcut support).
/// <code source="../../DocCodeExamples/ActionExamples.cs" region="declare-sampleTimelineAction" title="SampleTimelineAction"/>
/// </example>
[ActiveInMode(TimelineModes.Default)]
public abstract class TimelineAction : IAction
{
/// <summary>
/// Execute the action.
/// </summary>
/// <param name="context">Context for the action.</param>
/// <returns>true if the action has been executed. false otherwise</returns>
public abstract bool Execute(ActionContext context);
/// <summary>
/// Defines the validity of an Action based on the context.
/// </summary>
/// <param name="context">Context for the action.</param>
/// <returns>Visual state of the menu for the action.</returns>
public abstract ActionValidity Validate(ActionContext context);
}
}

View File

@@ -0,0 +1,952 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.ShortcutManagement;
using UnityEditor.Timeline.Actions;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
[MenuEntry("Copy", MenuPriority.TimelineActionSection.copy)]
[Shortcut("Main Menu/Edit/Copy", EventCommandNames.Copy)]
class CopyAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context)
{
if (SelectionManager.Count() == 0)
return ActionValidity.NotApplicable;
if (context.tracks.ContainsTimelineMarkerTrack(context.timeline))
return ActionValidity.NotApplicable;
return ActionValidity.Valid;
}
public override bool Execute(ActionContext context)
{
TimelineEditor.clipboard.Clear();
var clips = context.clips;
if (clips.Any())
{
clips.Invoke<CopyClipsToClipboard>();
}
var markers = context.markers;
if (markers.Any())
{
markers.Invoke<CopyMarkersToClipboard>();
}
var tracks = context.tracks;
if (tracks.Any())
{
CopyTracksToClipboard.Do(tracks.ToArray());
}
return true;
}
}
[MenuEntry("Paste", MenuPriority.TimelineActionSection.paste)]
[Shortcut("Main Menu/Edit/Paste", EventCommandNames.Paste)]
class PasteAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context)
{
return CanPaste(context.invocationTime) ? ActionValidity.Valid : ActionValidity.Invalid;
}
public override bool Execute(ActionContext context)
{
if (!CanPaste(context.invocationTime))
return false;
PasteItems(context.invocationTime);
PasteTracks();
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
return true;
}
static bool CanPaste(double? invocationTime)
{
var copiedItems = TimelineEditor.clipboard.GetCopiedItems().ToList();
if (!copiedItems.Any())
return TimelineEditor.clipboard.GetTracks().Any();
return CanPasteItems(copiedItems, invocationTime);
}
static bool CanPasteItems(ICollection<ItemsPerTrack> itemsGroups, double? invocationTime)
{
var hasItemsCopiedFromMultipleTracks = itemsGroups.Count > 1;
var allItemsCopiedFromCurrentAsset = itemsGroups.All(x => x.targetTrack.timelineAsset == TimelineEditor.inspectedAsset);
var hasUsedShortcut = invocationTime == null;
var anySourceLocked = itemsGroups.Any(x => x.targetTrack != null && x.targetTrack.lockedInHierarchy);
var targetTrack = GetPickedTrack();
if (targetTrack == null)
targetTrack = SelectionManager.SelectedTracks().FirstOrDefault();
//do not paste if the user copied items from another timeline
//if the copied items comes from > 1 track (since we do not know where to paste the copied items)
//or if a keyboard shortcut was used (since the user will not see the paste result)
if (!allItemsCopiedFromCurrentAsset)
{
var isSelectedTrackInCurrentAsset = targetTrack != null && targetTrack.timelineAsset == TimelineEditor.inspectedAsset;
if (hasItemsCopiedFromMultipleTracks || (hasUsedShortcut && !isSelectedTrackInCurrentAsset))
return false;
}
// pasting to items to their source track, if items from multiple tracks are selected
// and no track is in the selection items will each be pasted to their respective track.
if (targetTrack == null || itemsGroups.All(x => x.targetTrack == targetTrack))
return !anySourceLocked;
if (hasItemsCopiedFromMultipleTracks)
{
//do not paste if the track which received the paste action does not contain a copied clip
return !anySourceLocked && itemsGroups.Select(x => x.targetTrack).Contains(targetTrack);
}
var copiedItems = itemsGroups.SelectMany(i => i.items);
return IsTrackValidForItems(targetTrack, copiedItems);
}
static void PasteItems(double? invocationTime)
{
var copiedItems = TimelineEditor.clipboard.GetCopiedItems().ToList();
var numberOfUniqueParentsInClipboard = copiedItems.Count();
if (numberOfUniqueParentsInClipboard == 0) return;
List<ITimelineItem> newItems;
//if the copied items were on a single parent, then use the mouse position to get the parent OR the original parent
if (numberOfUniqueParentsInClipboard == 1)
{
var itemsGroup = copiedItems.First();
TrackAsset target = null;
if (invocationTime.HasValue)
target = GetPickedTrack();
if (target == null)
target = FindSuitableParentForSingleTrackPasteWithoutMouse(itemsGroup);
var candidateTime = invocationTime ?? TimelineHelpers.GetCandidateTime(null, target);
newItems = TimelineHelpers.DuplicateItemsUsingCurrentEditMode(TimelineEditor.clipboard.exposedPropertyTable, TimelineEditor.inspectedDirector, itemsGroup, target, candidateTime, "Paste Items").ToList();
}
//if copied items were on multiple parents, then the destination parents are the same as the original parents
else
{
var time = invocationTime ?? TimelineHelpers.GetCandidateTime(null, copiedItems.Select(c => c.targetTrack).ToArray());
newItems = TimelineHelpers.DuplicateItemsUsingCurrentEditMode(TimelineEditor.clipboard.exposedPropertyTable, TimelineEditor.inspectedDirector, copiedItems, time, "Paste Items").ToList();
}
TimelineHelpers.FrameItems(newItems);
SelectionManager.RemoveTimelineSelection();
foreach (var item in newItems)
{
SelectionManager.Add(item);
}
}
static TrackAsset FindSuitableParentForSingleTrackPasteWithoutMouse(ItemsPerTrack itemsGroup)
{
var groupParent = itemsGroup.targetTrack; //set a main parent in the clipboard
var selectedTracks = SelectionManager.SelectedTracks();
if (selectedTracks.Contains(groupParent))
{
return groupParent;
}
//find a selected track suitable for all items
var itemsToPaste = itemsGroup.items;
var compatibleTrack = selectedTracks.FirstOrDefault(t => IsTrackValidForItems(t, itemsToPaste));
return compatibleTrack != null ? compatibleTrack : groupParent;
}
static bool IsTrackValidForItems(TrackAsset track, IEnumerable<ITimelineItem> items)
{
if (track == null || track.lockedInHierarchy) return false;
return items.All(i => i.IsCompatibleWithTrack(track));
}
static TrackAsset GetPickedTrack()
{
if (PickerUtils.pickedElements == null)
return null;
var rowGUI = PickerUtils.pickedElements.OfType<IRowGUI>().FirstOrDefault();
if (rowGUI != null)
return rowGUI.asset;
return null;
}
static void PasteTracks()
{
var trackData = TimelineEditor.clipboard.GetTracks().ToList();
if (trackData.Any())
{
SelectionManager.RemoveTimelineSelection();
}
foreach (var track in trackData)
{
var newTrack = track.item.Duplicate(TimelineEditor.clipboard.exposedPropertyTable, TimelineEditor.inspectedDirector, TimelineEditor.inspectedAsset);
if (track.binding != null)
{
BindingUtility.Bind(TimelineEditor.inspectedDirector, newTrack, track.binding);
}
SelectionManager.Add(newTrack);
foreach (var childTrack in newTrack.GetFlattenedChildTracks())
{
SelectionManager.Add(childTrack);
}
if (track.parent != null && track.parent.timelineAsset == TimelineEditor.inspectedAsset)
{
TrackExtensions.ReparentTracks(new List<TrackAsset> { newTrack }, track.parent, track.item);
}
}
}
}
[MenuEntry("Duplicate", MenuPriority.TimelineActionSection.duplicate)]
[Shortcut("Main Menu/Edit/Duplicate", EventCommandNames.Duplicate)]
class DuplicateAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context)
{
IEnumerable<TrackAsset> tracks = context.tracks.RemoveTimelineMarkerTrackFromList(context.timeline);
return context.clips.Any() || tracks.Any() || context.markers.Any() ? ActionValidity.Valid : ActionValidity.NotApplicable;
}
public bool Execute(Func<ITimelineItem, ITimelineItem, double> gapBetweenItems)
{
return Execute(TimelineEditor.CurrentContext(), gapBetweenItems);
}
public override bool Execute(ActionContext context)
{
return Execute(context, (item1, item2) => ItemsUtils.TimeGapBetweenItems(item1, item2));
}
internal bool Execute(ActionContext context, Func<ITimelineItem, ITimelineItem, double> gapBetweenItems)
{
List<ITimelineItem> items = new List<ITimelineItem>();
items.AddRange(context.clips.Select(p => p.ToItem()));
items.AddRange(context.markers.Select(p => p.ToItem()));
List<ItemsPerTrack> selectedItems = items.ToItemsPerTrack().ToList();
if (selectedItems.Any())
{
var requestedTime = CalculateDuplicateTime(selectedItems, gapBetweenItems);
var duplicatedItems = TimelineHelpers.DuplicateItemsUsingCurrentEditMode(TimelineEditor.inspectedDirector, TimelineEditor.inspectedDirector, selectedItems, requestedTime, "Duplicate Items");
TimelineHelpers.FrameItems(duplicatedItems);
SelectionManager.RemoveTimelineSelection();
foreach (var item in duplicatedItems)
SelectionManager.Add(item);
}
var tracks = context.tracks.ToArray();
if (tracks.Length > 0)
tracks.Invoke<DuplicateTracks>();
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
return true;
}
static double CalculateDuplicateTime(IEnumerable<ItemsPerTrack> duplicatedItems, Func<ITimelineItem, ITimelineItem, double> gapBetweenItems)
{
//Find the end time of the rightmost item
var itemsOnTracks = duplicatedItems.SelectMany(i => i.targetTrack.GetItems()).ToList();
var time = itemsOnTracks.Max(i => i.end);
//From all the duplicated items, select the leftmost items
var firstDuplicatedItems = duplicatedItems.Select(i => i.leftMostItem);
var leftMostDuplicatedItems = firstDuplicatedItems.OrderBy(i => i.start).GroupBy(i => i.start).FirstOrDefault();
if (leftMostDuplicatedItems == null) return 0.0;
foreach (var leftMostItem in leftMostDuplicatedItems)
{
var siblings = leftMostItem.parentTrack.GetItems();
var rightMostSiblings = siblings.OrderByDescending(i => i.end).GroupBy(i => i.end).FirstOrDefault();
if (rightMostSiblings == null) continue;
foreach (var sibling in rightMostSiblings)
time = Math.Max(time, sibling.end + gapBetweenItems(leftMostItem, sibling));
}
return time;
}
}
[MenuEntry("Delete", MenuPriority.TimelineActionSection.delete)]
[Shortcut("Main Menu/Edit/Delete", EventCommandNames.Delete)]
[ShortcutPlatformOverride(RuntimePlatform.OSXEditor, KeyCode.Backspace, ShortcutModifiers.Action)]
[ActiveInMode(TimelineModes.Default)]
class DeleteAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context)
{
return CanDelete(context) ? ActionValidity.Valid : ActionValidity.Invalid;
}
static bool CanDelete(ActionContext context)
{
if (TimelineWindow.instance.state.editSequence.isReadOnly)
return false;
if (context.tracks.ContainsTimelineMarkerTrack(context.timeline))
return false;
// All() returns true when empty
return context.tracks.All(x => !x.lockedInHierarchy) &&
context.clips.All(x => x.GetParentTrack() == null || !x.GetParentTrack().lockedInHierarchy) &&
context.markers.All(x => x.parent == null || !x.parent.lockedInHierarchy);
}
public override bool Execute(ActionContext context)
{
if (!CanDelete(context))
return false;
var selectedItems = context.clips.Select(p => p.ToItem()).ToList();
selectedItems.AddRange(context.markers.Select(p => p.ToItem()));
DeleteItems(selectedItems);
if (context.tracks.Any() && SelectionManager.GetCurrentInlineEditorCurve() == null)
context.tracks.Invoke<DeleteTracks>();
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
return selectedItems.Any() || context.tracks.Any();
}
internal static void DeleteItems(IEnumerable<ITimelineItem> items)
{
var tracks = items.GroupBy(c => c.parentTrack);
foreach (var track in tracks)
TimelineUndo.PushUndo(track.Key, L10n.Tr("Delete Items"));
TimelineAnimationUtilities.UnlinkAnimationWindowFromClips(items.OfType<ClipItem>().Select(i => i.clip));
EditMode.PrepareItemsDelete(ItemsUtils.ToItemsPerTrack(items));
EditModeUtils.Delete(items);
SelectionManager.RemoveAllClips();
}
}
[MenuEntry("Match Content", MenuPriority.TimelineActionSection.matchContent)]
[Shortcut(Shortcuts.Timeline.matchContent)]
class MatchContent : TimelineAction
{
public override ActionValidity Validate(ActionContext actionContext)
{
var clips = actionContext.clips;
if (!clips.Any())
return ActionValidity.NotApplicable;
return clips.Any(TimelineHelpers.HasUsableAssetDuration)
? ActionValidity.Valid
: ActionValidity.Invalid;
}
public override bool Execute(ActionContext actionContext)
{
var clips = actionContext.clips;
return clips.Any() && ClipModifier.MatchContent(clips);
}
}
[Shortcut(Shortcuts.Timeline.play)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class PlayTimelineAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
var currentState = TimelineEditor.state.playing;
TimelineEditor.state.SetPlaying(!currentState);
return true;
}
}
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class SelectAllAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
// otherwise select all tracks.
SelectionManager.Clear();
TimelineWindow.instance.allTracks.ForEach(x => SelectionManager.Add(x.track));
return true;
}
}
[Shortcut(Shortcuts.Timeline.previousFrame)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class PreviousFrameAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
if (TimelineEditor.inspectedAsset == null)
return false;
var inspectedFrame = TimeUtility.ToFrames(TimelineEditor.inspectedSequenceTime, TimelineEditor.inspectedAsset.editorSettings.fps);
inspectedFrame = Mathf.Max(0, inspectedFrame - 1);
TimelineEditor.inspectedSequenceTime = TimeUtility.FromFrames(inspectedFrame, TimelineEditor.inspectedAsset.editorSettings.fps);
return true;
}
}
[Shortcut(Shortcuts.Timeline.nextFrame)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class NextFrameAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
if (TimelineEditor.inspectedAsset == null)
return false;
var inspectedFrame = TimeUtility.ToFrames(TimelineEditor.inspectedSequenceTime, TimelineEditor.inspectedAsset.editorSettings.fps);
inspectedFrame++;
TimelineEditor.inspectedSequenceTime = TimeUtility.FromFrames(inspectedFrame, TimelineEditor.inspectedAsset.editorSettings.fps);
return true;
}
}
[Shortcut(Shortcuts.Timeline.frameAll)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class FrameAllAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context)
{
if (context.timeline != null && !context.timeline.flattenedTracks.Any())
return ActionValidity.NotApplicable;
return ActionValidity.Valid;
}
public override bool Execute(ActionContext actionContext)
{
var inlineCurveEditor = SelectionManager.GetCurrentInlineEditorCurve();
if (FrameSelectedAction.ShouldHandleInlineCurve(inlineCurveEditor))
{
FrameSelectedAction.FrameInlineCurves(inlineCurveEditor, false);
return true;
}
if (TimelineWindow.instance.state.IsCurrentEditingASequencerTextField())
return false;
var visibleTracks = TimelineWindow.instance.treeView.visibleTracks.ToList();
if (TimelineEditor.inspectedAsset != null && TimelineEditor.inspectedAsset.markerTrack != null)
visibleTracks.Add(TimelineEditor.inspectedAsset.markerTrack);
if (visibleTracks.Count == 0)
return false;
var startTime = float.MaxValue;
var endTime = float.MinValue;
foreach (var t in visibleTracks)
{
if (t == null)
continue;
// time range based on track's curves and clips.
double trackStart, trackEnd, trackDuration;
t.GetSequenceTime(out trackStart, out trackDuration);
trackEnd = trackStart + trackDuration;
// take track's markers into account
double itemsStart, itemsEnd;
ItemsUtils.GetItemRange(t, out itemsStart, out itemsEnd);
startTime = Mathf.Min(startTime, (float)trackStart, (float)itemsStart);
endTime = Mathf.Max(endTime, (float)(trackEnd), (float)itemsEnd);
}
FrameSelectedAction.FrameRange(startTime, endTime);
return true;
}
}
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class FrameSelectedAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public static void FrameRange(float startTime, float endTime)
{
if (startTime > endTime)
{
return;
}
var halfDuration = endTime - Math.Max(0.0f, startTime);
if (halfDuration > 0.0f)
{
TimelineEditor.visibleTimeRange = new Vector2(Mathf.Max(0.0f, startTime - (halfDuration * 0.1f)), endTime + (halfDuration * 0.1f));
}
else
{
// start == end
// keep the zoom level constant, only pan the time area to center the item
var currentRange = TimelineEditor.visibleTimeRange.y - TimelineEditor.visibleTimeRange.x;
TimelineEditor.visibleTimeRange = new Vector2(startTime - currentRange / 2, startTime + currentRange / 2);
}
TimelineZoomManipulator.InvalidateWheelZoom();
TimelineEditor.Refresh(RefreshReason.SceneNeedsUpdate);
}
public override bool Execute(ActionContext actionContext)
{
var inlineCurveEditor = SelectionManager.GetCurrentInlineEditorCurve();
if (ShouldHandleInlineCurve(inlineCurveEditor))
{
FrameInlineCurves(inlineCurveEditor, true);
return true;
}
if (TimelineWindow.instance.state.IsCurrentEditingASequencerTextField())
return false;
if (SelectionManager.Count() == 0)
{
actionContext.Invoke<FrameAllAction>();
return true;
}
var startTime = float.MaxValue;
var endTime = float.MinValue;
var clips = actionContext.clips.Select(ItemToItemGui.GetGuiForClip);
var markers = actionContext.markers;
if (!clips.Any() && !markers.Any())
return false;
foreach (var c in clips)
{
startTime = Mathf.Min(startTime, (float)c.clip.start);
endTime = Mathf.Max(endTime, (float)c.clip.end);
if (c.clipCurveEditor != null)
{
c.clipCurveEditor.FrameClip();
}
}
foreach (var marker in markers)
{
startTime = Mathf.Min(startTime, (float)marker.time);
endTime = Mathf.Max(endTime, (float)marker.time);
}
FrameRange(startTime, endTime);
return true;
}
public static bool ShouldHandleInlineCurve(IClipCurveEditorOwner curveEditorOwner)
{
return curveEditorOwner?.clipCurveEditor != null &&
curveEditorOwner.inlineCurvesSelected &&
curveEditorOwner.owner != null &&
curveEditorOwner.owner.GetShowInlineCurves();
}
public static void FrameInlineCurves(IClipCurveEditorOwner curveEditorOwner, bool selectionOnly)
{
var curveEditor = curveEditorOwner.clipCurveEditor.curveEditor;
var frameBounds = selectionOnly ? curveEditor.GetSelectionBounds() : curveEditor.GetClipBounds();
var clipGUI = curveEditorOwner as TimelineClipGUI;
var areaOffset = 0.0f;
if (clipGUI != null)
{
areaOffset = (float)Math.Max(0.0, clipGUI.clip.FromLocalTimeUnbound(0.0));
var timeScale = (float)clipGUI.clip.timeScale; // Note: The getter for clip.timeScale is guaranteed to never be zero.
// Apply scaling
var newMin = frameBounds.min.x / timeScale;
var newMax = (frameBounds.max.x - frameBounds.min.x) / timeScale + newMin;
frameBounds.SetMinMax(
new Vector3(newMin, frameBounds.min.y, frameBounds.min.z),
new Vector3(newMax, frameBounds.max.y, frameBounds.max.z));
}
curveEditor.Frame(frameBounds, true, true);
var area = curveEditor.shownAreaInsideMargins;
area.x += areaOffset;
var curveStart = curveEditorOwner.clipCurveEditor.dataSource.start;
FrameRange(curveStart + frameBounds.min.x, curveStart + frameBounds.max.x);
}
}
[Shortcut(Shortcuts.Timeline.previousKey)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class PrevKeyAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
if (TimelineEditor.inspectedAsset == null)
return false;
var keyTraverser = new Utilities.KeyTraverser(TimelineEditor.inspectedAsset, 0.01f / TimelineEditor.inspectedAsset.editorSettings.fps);
var time = keyTraverser.GetPrevKey((float)TimelineEditor.inspectedSequenceTime, TimelineWindow.instance.state.dirtyStamp);
if (time != TimelineEditor.inspectedSequenceTime)
{
TimelineEditor.inspectedSequenceTime = time;
}
return true;
}
}
[Shortcut(Shortcuts.Timeline.nextKey)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class NextKeyAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
if (TimelineEditor.inspectedAsset == null)
return false;
var keyTraverser = new Utilities.KeyTraverser(TimelineEditor.inspectedAsset, 0.01f / TimelineEditor.inspectedAsset.editorSettings.fps);
var time = keyTraverser.GetNextKey((float)TimelineEditor.inspectedSequenceTime, TimelineWindow.instance.state.dirtyStamp);
if (time != TimelineEditor.inspectedSequenceTime)
{
TimelineEditor.inspectedSequenceTime = time;
}
return true;
}
}
[Shortcut(Shortcuts.Timeline.goToStart)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class GotoStartAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
TimelineEditor.inspectedSequenceTime = 0.0f;
TimelineWindow.instance.state.EnsurePlayHeadIsVisible();
return true;
}
}
[Shortcut(Shortcuts.Timeline.goToEnd)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class GotoEndAction : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
TimelineEditor.inspectedSequenceTime = TimelineWindow.instance.state.editSequence.duration;
TimelineWindow.instance.state.EnsurePlayHeadIsVisible();
return true;
}
}
[Shortcut(Shortcuts.Timeline.zoomIn)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class ZoomIn : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
TimelineZoomManipulator.Instance.DoZoom(1.15f);
return true;
}
}
[Shortcut(Shortcuts.Timeline.zoomOut)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class ZoomOut : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
TimelineZoomManipulator.Instance.DoZoom(0.85f);
return true;
}
}
[Shortcut(Shortcuts.Timeline.collapseGroup)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class CollapseGroup : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.CollapseGroup(actionContext.tracks);
}
}
[Shortcut(Shortcuts.Timeline.unCollapseGroup)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class UnCollapseGroup : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.UnCollapseGroup(actionContext.tracks);
}
}
[Shortcut(Shortcuts.Timeline.selectLeftItem)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class SelectLeftClip : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
// Switches to track header if no left track exists
return KeyboardNavigation.SelectLeftItem();
}
}
[Shortcut(Shortcuts.Timeline.selectRightItem)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class SelectRightClip : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.SelectRightItem();
}
}
[Shortcut(Shortcuts.Timeline.selectUpItem)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class SelectUpClip : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.SelectUpItem();
}
}
[Shortcut(Shortcuts.Timeline.selectUpTrack)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class SelectUpTrack : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.SelectUpTrack();
}
}
[Shortcut(Shortcuts.Timeline.selectDownItem)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class SelectDownClip : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.SelectDownItem();
}
}
[Shortcut(Shortcuts.Timeline.selectDownTrack)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class SelectDownTrack : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
if (!KeyboardNavigation.ClipAreaActive() && !KeyboardNavigation.TrackHeadActive())
return KeyboardNavigation.FocusFirstVisibleItem();
else
return KeyboardNavigation.SelectDownTrack();
}
}
[Shortcut(Shortcuts.Timeline.multiSelectLeft)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class MultiselectLeftClip : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.SelectLeftItem(true);
}
}
[Shortcut(Shortcuts.Timeline.multiSelectRight)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class MultiselectRightClip : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.SelectRightItem(true);
}
}
[Shortcut(Shortcuts.Timeline.multiSelectUp)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class MultiselectUpTrack : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.SelectUpTrack(true);
}
}
[Shortcut(Shortcuts.Timeline.multiSelectDown)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class MultiselectDownTrack : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
return KeyboardNavigation.SelectDownTrack(true);
}
}
[Shortcut(Shortcuts.Timeline.toggleClipTrackArea)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class ToggleClipTrackArea : TimelineAction
{
public override ActionValidity Validate(ActionContext context) => ActionValidity.Valid;
public override bool Execute(ActionContext actionContext)
{
if (KeyboardNavigation.TrackHeadActive())
return KeyboardNavigation.FocusFirstVisibleItem(actionContext.tracks);
if (!KeyboardNavigation.ClipAreaActive())
return KeyboardNavigation.FocusFirstVisibleItem();
var item = KeyboardNavigation.GetVisibleSelectedItems().LastOrDefault();
if (item != null)
SelectionManager.SelectOnly(item.parentTrack);
return true;
}
}
[MenuEntry("Key All Animated", MenuPriority.TimelineActionSection.keyAllAnimated)]
[Shortcut(Shortcuts.Timeline.keyAllAnimated)]
class KeyAllAnimated : TimelineAction
{
public override ActionValidity Validate(ActionContext actionContext)
{
return CanExecute(TimelineEditor.state, actionContext)
? ActionValidity.Valid
: ActionValidity.NotApplicable;
}
public override bool Execute(ActionContext actionContext)
{
WindowState state = TimelineEditor.state;
PlayableDirector director = TimelineEditor.inspectedDirector;
if (!CanExecute(state, actionContext) || director == null)
return false;
IEnumerable<TrackAsset> keyableTracks = GetKeyableTracks(state, actionContext);
var curveSelected = SelectionManager.GetCurrentInlineEditorCurve();
if (curveSelected != null)
{
var sel = curveSelected.clipCurveEditor.GetSelectedProperties().ToList();
var go = (director.GetGenericBinding(curveSelected.owner) as Component).gameObject;
if (sel.Count > 0)
{
TimelineRecording.KeyProperties(go, state, sel);
}
else
{
var binding = director.GetGenericBinding(curveSelected.owner) as Component;
TimelineRecording.KeyAllProperties(binding, state);
}
}
else
{
foreach (var track in keyableTracks)
{
var binding = director.GetGenericBinding(track) as Component;
TimelineRecording.KeyAllProperties(binding, state);
}
}
return true;
}
static IEnumerable<TrackAsset> GetKeyableTracks(WindowState state, ActionContext context)
{
if (!context.clips.Any() && !context.tracks.Any()) //no selection -> animate all recorded tracks
return state.editSequence.asset.flattenedTracks.Where(state.IsArmedForRecord);
List<TrackAsset> parentTracks = context.tracks.ToList();
parentTracks.AddRange(context.clips.Select(clip => clip.GetParentTrack()).Distinct());
if (!parentTracks.All(state.IsArmedForRecord))
return Enumerable.Empty<TrackAsset>();
return parentTracks;
}
static bool CanExecute(WindowState state, ActionContext context)
{
if (context.markers.Any())
return false;
if (context.tracks.ContainsTimelineMarkerTrack(state.editSequence.asset))
return false;
IClipCurveEditorOwner curveSelected = SelectionManager.GetCurrentInlineEditorCurve();
// Can't have an inline curve selected and have multiple tracks also.
if (curveSelected != null)
{
return state.IsArmedForRecord(curveSelected.owner);
}
return GetKeyableTracks(state, context).Any();
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Base class for a track action.
/// Inherit from this class to make an action that would react on selected tracks after a menu click and/or a key shortcut.
/// </summary>
/// <example>
/// Simple track Action example (with context menu and shortcut support).
/// <code source="../../DocCodeExamples/ActionExamples.cs" region="declare-sampleTrackAction" title="SampleTrackAction"/>
/// </example>
/// <remarks>
/// To add an action as a menu item in the Timeline context menu, add <see cref="MenuEntryAttribute"/> on the action class.
/// To make an action to react to a shortcut, use the Shortcut Manager API with <see cref="TimelineShortcutAttribute"/>.
/// <seealso cref="UnityEditor.ShortcutManagement.ShortcutAttribute"/>
/// </remarks>
[ActiveInMode(TimelineModes.Default)]
public abstract class TrackAction : IAction
{
/// <summary>
/// Execute the action.
/// </summary>
/// <param name="tracks">Tracks that will be used for the action. </param>
/// <returns>true if the action has been executed. false otherwise</returns>
public abstract bool Execute(IEnumerable<TrackAsset> tracks);
/// <summary>
/// Defines the validity of an Action for a given set of tracks.
/// </summary>
/// <param name="tracks">tracks that the action would act on.</param>
/// <returns>The validity of the set of tracks.</returns>
public abstract ActionValidity Validate(IEnumerable<TrackAsset> tracks);
}
}

View File

@@ -0,0 +1,457 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using UnityEditor.Timeline.Actions;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
[MenuEntry("Edit in Animation Window", MenuPriority.TrackActionSection.editInAnimationWindow)]
class EditTrackInAnimationWindow : TrackAction
{
public static bool Do(TrackAsset track)
{
AnimationClip clipToEdit = null;
AnimationTrack animationTrack = track as AnimationTrack;
if (animationTrack != null)
{
if (!animationTrack.CanConvertToClipMode())
return false;
clipToEdit = animationTrack.infiniteClip;
}
else if (track.hasCurves)
{
clipToEdit = track.curves;
}
if (clipToEdit == null)
return false;
GameObject gameObject = null;
if (TimelineEditor.inspectedDirector != null)
gameObject = TimelineUtility.GetSceneGameObject(TimelineEditor.inspectedDirector, track);
var timeController = TimelineAnimationUtilities.CreateTimeController(CreateTimeControlClipData(track));
TimelineAnimationUtilities.EditAnimationClipWithTimeController(clipToEdit, timeController, gameObject);
return true;
}
public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
{
if (!tracks.Any())
return ActionValidity.Invalid;
var firstTrack = tracks.First();
if (firstTrack is AnimationTrack)
{
var animTrack = firstTrack as AnimationTrack;
if (animTrack.CanConvertToClipMode())
return ActionValidity.Valid;
}
else if (firstTrack.hasCurves)
{
return ActionValidity.Valid;
}
return ActionValidity.NotApplicable;
}
public override bool Execute(IEnumerable<TrackAsset> tracks)
{
return Do(tracks.First());
}
static TimelineWindowTimeControl.ClipData CreateTimeControlClipData(TrackAsset track)
{
var data = new TimelineWindowTimeControl.ClipData();
data.track = track;
data.start = track.start;
data.duration = track.duration;
return data;
}
}
[MenuEntry("Lock selected track only", MenuPriority.TrackActionSection.lockSelected)]
class LockSelectedTrack : TrackAction, IMenuName
{
public static readonly string LockSelectedTrackOnlyText = L10n.Tr("Lock selected track only");
public static readonly string UnlockSelectedTrackOnlyText = L10n.Tr("Unlock selected track only");
public string menuName { get; private set; }
public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
{
UpdateMenuName(tracks);
if (tracks.Any(track => TimelineUtility.IsLockedFromGroup(track) || track is GroupTrack || !track.subTracksObjects.Any()))
return ActionValidity.NotApplicable;
return ActionValidity.Valid;
}
public override bool Execute(IEnumerable<TrackAsset> tracks)
{
if (!tracks.Any()) return false;
var hasUnlockedTracks = tracks.Any(x => !x.locked);
Lock(tracks.Where(p => !(p is GroupTrack)).ToArray(), hasUnlockedTracks);
return true;
}
void UpdateMenuName(IEnumerable<TrackAsset> tracks)
{
menuName = tracks.All(t => t.locked) ? UnlockSelectedTrackOnlyText : LockSelectedTrackOnlyText;
}
public static void Lock(TrackAsset[] tracks, bool shouldlock)
{
if (tracks.Length == 0)
return;
foreach (var track in tracks.Where(t => !TimelineUtility.IsLockedFromGroup(t)))
{
TimelineUndo.PushUndo(track, L10n.Tr("Lock Tracks"));
track.locked = shouldlock;
}
TimelineEditor.Refresh(RefreshReason.WindowNeedsRedraw);
}
}
[MenuEntry("Lock", MenuPriority.TrackActionSection.lockTrack)]
[Shortcut(Shortcuts.Timeline.toggleLock)]
class LockTrack : TrackAction, IMenuName
{
static readonly string k_LockText = L10n.Tr("Lock");
static readonly string k_UnlockText = L10n.Tr("Unlock");
public string menuName { get; private set; }
void UpdateMenuName(IEnumerable<TrackAsset> tracks)
{
menuName = tracks.Any(x => !x.locked) ? k_LockText : k_UnlockText;
}
public override bool Execute(IEnumerable<TrackAsset> tracks)
{
if (!tracks.Any()) return false;
var hasUnlockedTracks = tracks.Any(x => !x.locked);
SetLockState(tracks, hasUnlockedTracks);
return true;
}
public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
{
UpdateMenuName(tracks);
tracks = tracks.RemoveTimelineMarkerTrackFromList(TimelineEditor.inspectedAsset);
if (!tracks.Any())
return ActionValidity.NotApplicable;
if (tracks.Any(TimelineUtility.IsLockedFromGroup))
return ActionValidity.Invalid;
return ActionValidity.Valid;
}
public static void SetLockState(IEnumerable<TrackAsset> tracks, bool shouldLock)
{
if (!tracks.Any())
return;
foreach (var track in tracks)
{
if (TimelineUtility.IsLockedFromGroup(track))
continue;
if (track as GroupTrack == null)
SetLockState(track.GetChildTracks().ToArray(), shouldLock);
TimelineUndo.PushUndo(track, L10n.Tr("Lock Tracks"));
track.locked = shouldLock;
}
// find the tracks we've locked. unselect anything locked and remove recording.
foreach (var track in tracks)
{
if (TimelineUtility.IsLockedFromGroup(track) || !track.locked)
continue;
var flattenedChildTracks = track.GetFlattenedChildTracks();
foreach (var i in track.clips)
SelectionManager.Remove(i);
track.UnarmForRecord();
foreach (var child in flattenedChildTracks)
{
SelectionManager.Remove(child);
child.UnarmForRecord();
foreach (var clip in child.GetClips())
SelectionManager.Remove(clip);
}
}
// no need to rebuild, just repaint (including inspectors)
InspectorWindow.RepaintAllInspectors();
TimelineEditor.Refresh(RefreshReason.WindowNeedsRedraw);
}
}
[UsedImplicitly]
[MenuEntry("Show Markers", MenuPriority.TrackActionSection.showHideMarkers)]
[ActiveInMode(TimelineModes.Default | TimelineModes.ReadOnly)]
class ShowHideMarkers : TrackAction, IMenuChecked
{
public bool isChecked { get; private set; }
void UpdateCheckedStatus(IEnumerable<TrackAsset> tracks)
{
isChecked = tracks.All(x => x.GetShowMarkers());
}
public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
{
UpdateCheckedStatus(tracks);
if (tracks.Any(x => x is GroupTrack) || tracks.Any(t => t.GetMarkerCount() == 0))
return ActionValidity.NotApplicable;
if (tracks.Any(t => t.lockedInHierarchy))
{
return ActionValidity.Invalid;
}
return ActionValidity.Valid;
}
public override bool Execute(IEnumerable<TrackAsset> tracks)
{
if (!tracks.Any()) return false;
var hasUnlockedTracks = tracks.Any(x => !x.GetShowMarkers());
ShowHide(tracks, hasUnlockedTracks);
return true;
}
static void ShowHide(IEnumerable<TrackAsset> tracks, bool shouldLock)
{
if (!tracks.Any())
return;
foreach (var track in tracks)
track.SetShowTrackMarkers(shouldLock);
TimelineEditor.Refresh(RefreshReason.WindowNeedsRedraw);
}
}
[MenuEntry("Mute selected track only", MenuPriority.TrackActionSection.muteSelected), UsedImplicitly]
class MuteSelectedTrack : TrackAction, IMenuName
{
public static readonly string MuteSelectedText = L10n.Tr("Mute selected track only");
public static readonly string UnmuteSelectedText = L10n.Tr("Unmute selected track only");
public string menuName { get; private set; }
public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
{
UpdateMenuName(tracks);
if (tracks.Any(track => TimelineUtility.IsParentMuted(track) || track is GroupTrack || !track.subTracksObjects.Any()))
return ActionValidity.NotApplicable;
return ActionValidity.Valid;
}
public override bool Execute(IEnumerable<TrackAsset> tracks)
{
if (!tracks.Any())
return false;
var hasUnmutedTracks = tracks.Any(x => !x.muted);
Mute(tracks.Where(p => !(p is GroupTrack)).ToArray(), hasUnmutedTracks);
return true;
}
void UpdateMenuName(IEnumerable<TrackAsset> tracks)
{
menuName = tracks.All(t => t.muted) ? UnmuteSelectedText : MuteSelectedText;
}
public static void Mute(TrackAsset[] tracks, bool shouldMute)
{
if (tracks.Length == 0)
return;
foreach (var track in tracks.Where(t => !TimelineUtility.IsParentMuted(t)))
{
TimelineUndo.PushUndo(track, L10n.Tr("Mute Tracks"));
track.muted = shouldMute;
}
TimelineEditor.Refresh(RefreshReason.ContentsModified);
}
}
[MenuEntry("Mute", MenuPriority.TrackActionSection.mute)]
[Shortcut(Shortcuts.Timeline.toggleMute)]
class MuteTrack : TrackAction, IMenuName
{
static readonly string k_MuteText = L10n.Tr("Mute");
static readonly string k_UnMuteText = L10n.Tr("Unmute");
public string menuName { get; private set; }
void UpdateMenuName(IEnumerable<TrackAsset> tracks)
{
menuName = tracks.Any(x => !x.muted) ? k_MuteText : k_UnMuteText;
}
public override bool Execute(IEnumerable<TrackAsset> tracks)
{
if (!tracks.Any() || tracks.Any(TimelineUtility.IsParentMuted))
return false;
var hasUnmutedTracks = tracks.Any(x => !x.muted);
Mute(tracks, hasUnmutedTracks);
return true;
}
public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
{
UpdateMenuName(tracks);
if (tracks.Any(TimelineUtility.IsLockedFromGroup))
return ActionValidity.Invalid;
return ActionValidity.Valid;
}
public static void Mute(IEnumerable<TrackAsset> tracks, bool shouldMute)
{
if (!tracks.Any())
return;
foreach (var track in tracks)
{
if (track as GroupTrack == null)
Mute(track.GetChildTracks().ToArray(), shouldMute);
TimelineUndo.PushUndo(track, L10n.Tr("Mute Tracks"));
track.muted = shouldMute;
}
TimelineEditor.Refresh(RefreshReason.ContentsModified);
}
}
class DeleteTracks : TrackAction
{
public static void Do(TimelineAsset timeline, TrackAsset track)
{
SelectionManager.Remove(track);
TrackModifier.DeleteTrack(timeline, track);
}
public override ActionValidity Validate(IEnumerable<TrackAsset> tracks) => ActionValidity.Valid;
public override bool Execute(IEnumerable<TrackAsset> tracks)
{
tracks = tracks.RemoveTimelineMarkerTrackFromList(TimelineEditor.inspectedAsset);
// disable preview mode so deleted tracks revert to default state
// Case 956129: Disable preview mode _before_ deleting the tracks, since clip data is still needed
TimelineEditor.state.previewMode = false;
TimelineAnimationUtilities.UnlinkAnimationWindowFromTracks(tracks);
foreach (var track in tracks)
Do(TimelineEditor.inspectedAsset, track);
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
return true;
}
}
class CopyTracksToClipboard : TrackAction
{
public static bool Do(TrackAsset[] tracks)
{
var action = new CopyTracksToClipboard();
return action.Execute(tracks);
}
public override ActionValidity Validate(IEnumerable<TrackAsset> tracks) => ActionValidity.Valid;
public override bool Execute(IEnumerable<TrackAsset> tracks)
{
tracks = tracks.RemoveTimelineMarkerTrackFromList(TimelineEditor.inspectedAsset);
TimelineEditor.clipboard.CopyTracks(tracks);
return true;
}
}
class DuplicateTracks : TrackAction
{
public override ActionValidity Validate(IEnumerable<TrackAsset> tracks) => ActionValidity.Valid;
public override bool Execute(IEnumerable<TrackAsset> tracks)
{
tracks = tracks.RemoveTimelineMarkerTrackFromList(TimelineEditor.inspectedAsset);
if (tracks.Any())
{
SelectionManager.RemoveTimelineSelection();
}
foreach (var track in TrackExtensions.FilterTracks(tracks))
{
var newTrack = track.Duplicate(TimelineEditor.inspectedDirector, TimelineEditor.inspectedDirector);
SelectionManager.Add(newTrack);
foreach (var childTrack in newTrack.GetFlattenedChildTracks())
{
SelectionManager.Add(childTrack);
}
if (TimelineEditor.inspectedDirector != null)
{
var binding = TimelineEditor.inspectedDirector.GetGenericBinding(track);
if (binding != null)
{
TimelineUndo.PushUndo(TimelineEditor.inspectedDirector, L10n.Tr("Duplicate"));
TimelineEditor.inspectedDirector.SetGenericBinding(newTrack, binding);
}
}
}
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
return true;
}
}
[MenuEntry("Remove Invalid Markers", MenuPriority.TrackActionSection.removeInvalidMarkers), UsedImplicitly]
class RemoveInvalidMarkersAction : TrackAction
{
public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
{
if (tracks.Any(target => target != null && target.GetMarkerCount() != target.GetMarkersRaw().Count()))
return ActionValidity.Valid;
return ActionValidity.NotApplicable;
}
public override bool Execute(IEnumerable<TrackAsset> tracks)
{
bool anyRemoved = false;
foreach (var target in tracks)
{
var invalids = target.GetMarkersRaw().Where(x => !(x is IMarker)).ToList();
foreach (var m in invalids)
{
anyRemoved = true;
target.DeleteMarkerRaw(m);
}
}
if (anyRemoved)
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
return anyRemoved;
}
}
}

View File

@@ -0,0 +1,56 @@
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.Timeline;
using UnityEngine.Playables;
namespace UnityEditor.Timeline
{
[UsedImplicitly]
[CustomTimelineEditor(typeof(ActivationTrack))]
class ActivationTrackEditor : TrackEditor
{
static readonly string ClipText = L10n.Tr("Active");
static readonly string k_ErrorParentString = L10n.Tr("The bound GameObject is a parent of the PlayableDirector.");
static readonly string k_ErrorString = L10n.Tr("The bound GameObject contains the PlayableDirector.");
public override TrackDrawOptions GetTrackOptions(TrackAsset track, Object binding)
{
var options = base.GetTrackOptions(track, binding);
options.errorText = GetErrorText(track, binding);
return options;
}
string GetErrorText(TrackAsset track, Object binding)
{
var gameObject = binding as GameObject;
var currentDirector = TimelineEditor.inspectedDirector;
if (gameObject != null && currentDirector != null)
{
var director = gameObject.GetComponent<PlayableDirector>();
if (currentDirector == director)
{
return k_ErrorString;
}
if (currentDirector.gameObject.transform.IsChildOf(gameObject.transform))
{
return k_ErrorParentString;
}
}
return base.GetErrorText(track, binding, TrackBindingErrors.PrefabBound);
}
public override void OnCreate(TrackAsset track, TrackAsset copiedFrom)
{
// Add a default clip to the newly created track
if (copiedFrom == null)
{
var clip = track.CreateClip(0);
clip.displayName = ClipText;
clip.duration = System.Math.Max(clip.duration, track.timelineAsset.duration * 0.5f);
}
}
}
}

View File

@@ -0,0 +1,43 @@
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
[CustomEditor(typeof(ActivationTrack))]
class ActivationTrackInspector : TrackAssetInspector
{
static class Styles
{
public static readonly GUIContent PostPlaybackStateText = L10n.TextContent("Post-playback state");
}
SerializedProperty m_PostPlaybackProperty;
public override void OnInspectorGUI()
{
using (new EditorGUI.DisabledScope(IsTrackLocked()))
{
serializedObject.Update();
EditorGUI.BeginChangeCheck();
if (m_PostPlaybackProperty != null)
EditorGUILayout.PropertyField(m_PostPlaybackProperty, Styles.PostPlaybackStateText);
if (EditorGUI.EndChangeCheck())
{
serializedObject.ApplyModifiedProperties();
var activationTrack = target as ActivationTrack;
if (activationTrack != null)
activationTrack.UpdateTrackMode();
}
}
}
public override void OnEnable()
{
base.OnEnable();
m_PostPlaybackProperty = serializedObject.FindProperty("m_PostPlaybackState");
}
}
}

View File

@@ -0,0 +1,170 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEngine.Playables;
using UnityEngine.SceneManagement;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline.Analytics
{
class TimelineSceneInfo
{
public Dictionary<string, int> trackCount = new Dictionary<string, int>
{
{"ActivationTrack", 0},
{"AnimationTrack", 0},
{"AudioTrack", 0},
{"ControlTrack", 0},
{"PlayableTrack", 0},
{"UserType", 0},
{"Other", 0}
};
public Dictionary<string, int> userTrackTypesCount = new Dictionary<string, int>();
public HashSet<TimelineAsset> uniqueDirectors = new HashSet<TimelineAsset>();
public int numTracks = 0;
public int minDuration = int.MaxValue;
public int maxDuration = int.MinValue;
public int minNumTracks = int.MaxValue;
public int maxNumTracks = int.MinValue;
public int numRecorded = 0;
}
[Serializable]
struct TrackInfo
{
public string name;
public double percent;
}
[Serializable]
class TimelineEventInfo
{
public int num_timelines;
public int min_duration, max_duration;
public int min_num_tracks, max_num_tracks;
public double recorded_percent;
public List<TrackInfo> track_info = new List<TrackInfo>();
public string most_popular_user_track = string.Empty;
public TimelineEventInfo(TimelineSceneInfo sceneInfo)
{
num_timelines = sceneInfo.uniqueDirectors.Count;
min_duration = sceneInfo.minDuration;
max_duration = sceneInfo.maxDuration;
min_num_tracks = sceneInfo.minNumTracks;
max_num_tracks = sceneInfo.maxNumTracks;
recorded_percent = Math.Round(100.0 * sceneInfo.numRecorded / sceneInfo.numTracks, 1);
foreach (KeyValuePair<string, int> kv in sceneInfo.trackCount.Where(x => x.Value > 0))
{
track_info.Add(new TrackInfo()
{
name = kv.Key,
percent = Math.Round(100.0 * kv.Value / sceneInfo.numTracks, 1)
});
}
if (sceneInfo.userTrackTypesCount.Any())
{
most_popular_user_track = sceneInfo.userTrackTypesCount
.First(x => x.Value == sceneInfo.userTrackTypesCount.Values.Max()).Key;
}
}
public static bool IsUserType(Type t)
{
string nameSpace = t.Namespace;
return string.IsNullOrEmpty(nameSpace) || !nameSpace.StartsWith("UnityEngine.Timeline");
}
}
static class TimelineAnalytics
{
static TimelineSceneInfo _timelineSceneInfo = new TimelineSceneInfo();
class TimelineAnalyticsPreProcess : IPreprocessBuildWithReport
{
public int callbackOrder { get { return 0; } }
public void OnPreprocessBuild(BuildReport report)
{
_timelineSceneInfo = new TimelineSceneInfo();
}
}
class TimelineAnalyticsProcess : IProcessSceneWithReport
{
public int callbackOrder
{
get { return 0; }
}
public void OnProcessScene(Scene scene, BuildReport report)
{
var timelines = UnityEngine.Object.FindObjectsOfType<PlayableDirector>().Select(pd => pd.playableAsset).OfType<TimelineAsset>().Distinct();
foreach (var timeline in timelines)
{
if (_timelineSceneInfo.uniqueDirectors.Add(timeline))
{
_timelineSceneInfo.numTracks += timeline.flattenedTracks.Count();
_timelineSceneInfo.minDuration = Math.Min(_timelineSceneInfo.minDuration, (int)(timeline.duration * 1000));
_timelineSceneInfo.maxDuration = Math.Max(_timelineSceneInfo.maxDuration, (int)(timeline.duration * 1000));
_timelineSceneInfo.minNumTracks = Math.Min(_timelineSceneInfo.minNumTracks, timeline.flattenedTracks.Count());
_timelineSceneInfo.maxNumTracks = Math.Max(_timelineSceneInfo.maxNumTracks, timeline.flattenedTracks.Count());
foreach (var track in timeline.flattenedTracks)
{
string key = track.GetType().Name;
if (_timelineSceneInfo.trackCount.ContainsKey(key))
{
_timelineSceneInfo.trackCount[key]++;
}
else
{
if (TimelineEventInfo.IsUserType(track.GetType()))
{
_timelineSceneInfo.trackCount["UserType"]++;
if (_timelineSceneInfo.userTrackTypesCount.ContainsKey(key))
_timelineSceneInfo.userTrackTypesCount[key]++;
else
_timelineSceneInfo.userTrackTypesCount[key] = 1;
}
else
_timelineSceneInfo.trackCount["Other"]++;
}
if (track.clips.Any(x => x.recordable))
_timelineSceneInfo.numRecorded++;
else
{
var animationTrack = track as AnimationTrack;
if (animationTrack != null)
{
if (animationTrack.CanConvertToClipMode())
_timelineSceneInfo.numRecorded++;
}
}
}
}
}
}
}
class TimelineAnalyticsPostProcess : IPostprocessBuildWithReport
{
public int callbackOrder {get { return 0; }}
public void OnPostprocessBuild(BuildReport report)
{
if (_timelineSceneInfo.uniqueDirectors.Count > 0)
{
var timelineEvent = new TimelineEventInfo(_timelineSceneInfo);
EditorAnalytics.SendEventTimelineInfo(timelineEvent);
}
}
}
}
}

View File

@@ -0,0 +1,101 @@
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using UnityEditor.Timeline.Actions;
using UnityEngine;
using UnityEngine.Timeline;
using UnityEngine.Playables;
namespace UnityEditor.Timeline
{
[ApplyDefaultUndo("Match Offsets")]
[MenuEntry("Match Offsets To Previous Clip", MenuPriority.CustomClipActionSection.matchPrevious), UsedImplicitly]
class MatchOffsetsPreviousAction : ClipAction
{
public override bool Execute(IEnumerable<TimelineClip> clips)
{
if (clips == null || !clips.Any())
return false;
AnimationOffsetMenu.MatchClipsToPrevious(clips.Where(x => IsValidClip(x, TimelineEditor.inspectedDirector)).ToArray());
return true;
}
static bool IsValidClip(TimelineClip clip, PlayableDirector director)
{
return clip != null &&
clip.GetParentTrack() != null &&
(clip.asset as AnimationPlayableAsset) != null &&
clip.GetParentTrack().clips.Any(x => x.start < clip.start) &&
TimelineUtility.GetSceneGameObject(director, clip.GetParentTrack()) != null;
}
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
if (!clips.All(TimelineAnimationUtilities.IsAnimationClip))
return ActionValidity.NotApplicable;
var director = TimelineEditor.inspectedDirector;
if (TimelineEditor.inspectedDirector == null)
return ActionValidity.NotApplicable;
if (clips.Any(c => IsValidClip(c, director)))
return ActionValidity.Valid;
return ActionValidity.NotApplicable;
}
}
[ApplyDefaultUndo("Match Offsets")]
[MenuEntry("Match Offsets To Next Clip", MenuPriority.CustomClipActionSection.matchNext), UsedImplicitly]
class MatchOffsetsNextAction : ClipAction
{
public override bool Execute(IEnumerable<TimelineClip> clips)
{
AnimationOffsetMenu.MatchClipsToNext(clips.Where(x => IsValidClip(x, TimelineEditor.inspectedDirector)).ToArray());
return true;
}
static bool IsValidClip(TimelineClip clip, PlayableDirector director)
{
return clip != null &&
clip.GetParentTrack() != null &&
(clip.asset as AnimationPlayableAsset) != null &&
clip.GetParentTrack().clips.Any(x => x.start > clip.start) &&
TimelineUtility.GetSceneGameObject(director, clip.GetParentTrack()) != null;
}
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
if (!clips.All(TimelineAnimationUtilities.IsAnimationClip))
return ActionValidity.NotApplicable;
var director = TimelineEditor.inspectedDirector;
if (TimelineEditor.inspectedDirector == null)
return ActionValidity.NotApplicable;
if (clips.Any(c => IsValidClip(c, director)))
return ActionValidity.Valid;
return ActionValidity.NotApplicable;
}
}
[ApplyDefaultUndo]
[MenuEntry("Reset Offsets", MenuPriority.CustomClipActionSection.resetOffset), UsedImplicitly]
class ResetOffsets : ClipAction
{
public override bool Execute(IEnumerable<TimelineClip> clips)
{
AnimationOffsetMenu.ResetClipOffsets(clips.Where(TimelineAnimationUtilities.IsAnimationClip).ToArray());
return true;
}
public override ActionValidity Validate(IEnumerable<TimelineClip> clips)
{
if (!clips.All(TimelineAnimationUtilities.IsAnimationClip))
return ActionValidity.NotApplicable;
return ActionValidity.Valid;
}
}
}

View File

@@ -0,0 +1,427 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEditorInternal;
namespace UnityEditor.Timeline
{
struct CurveBindingPair
{
public EditorCurveBinding binding;
public AnimationCurve curve;
public ObjectReferenceKeyframe[] objectCurve;
}
class CurveBindingGroup
{
public CurveBindingPair[] curveBindingPairs { get; set; }
public Vector2 timeRange { get; set; }
public Vector2 valueRange { get; set; }
public bool isFloatCurve
{
get
{
return curveBindingPairs != null && curveBindingPairs.Length > 0 &&
curveBindingPairs[0].curve != null;
}
}
public bool isObjectCurve
{
get
{
return curveBindingPairs != null && curveBindingPairs.Length > 0 &&
curveBindingPairs[0].objectCurve != null;
}
}
public int count
{
get
{
if (curveBindingPairs == null)
return 0;
return curveBindingPairs.Length;
}
}
}
class AnimationClipCurveInfo
{
bool m_CurveDirty = true;
bool m_KeysDirty = true;
public bool dirty
{
get { return m_CurveDirty; }
set
{
m_CurveDirty = value;
if (m_CurveDirty)
{
m_KeysDirty = true;
if (m_groupings != null)
m_groupings.Clear();
}
}
}
public AnimationCurve[] curves;
public EditorCurveBinding[] bindings;
public EditorCurveBinding[] objectBindings;
public List<ObjectReferenceKeyframe[]> objectCurves;
Dictionary<string, CurveBindingGroup> m_groupings;
// to tell whether the cache has changed
public int version { get; private set; }
float[] m_KeyTimes;
Dictionary<EditorCurveBinding, float[]> m_individualBindinsKey;
public float[] keyTimes
{
get
{
if (m_KeysDirty || m_KeyTimes == null)
{
RebuildKeyCache();
}
return m_KeyTimes;
}
}
public float[] GetCurveTimes(EditorCurveBinding curve)
{
return GetCurveTimes(new[] { curve });
}
public float[] GetCurveTimes(EditorCurveBinding[] curves)
{
if (m_KeysDirty || m_KeyTimes == null)
{
RebuildKeyCache();
}
var keyTimes = new List<float>();
for (int i = 0; i < curves.Length; i++)
{
var c = curves[i];
if (m_individualBindinsKey.ContainsKey(c))
{
keyTimes.AddRange(m_individualBindinsKey[c]);
}
}
return keyTimes.ToArray();
}
void RebuildKeyCache()
{
m_individualBindinsKey = new Dictionary<EditorCurveBinding, float[]>();
List<float> keys = curves.SelectMany(y => y.keys).Select(z => z.time).ToList();
for (int i = 0; i < objectCurves.Count; i++)
{
var kf = objectCurves[i];
keys.AddRange(kf.Select(x => x.time));
}
for (int b = 0; b < bindings.Count(); b++)
{
m_individualBindinsKey.Add(bindings[b], curves[b].keys.Select(k => k.time).Distinct().ToArray());
}
m_KeyTimes = keys.OrderBy(x => x).Distinct().ToArray();
m_KeysDirty = false;
}
public void Update(AnimationClip clip)
{
List<EditorCurveBinding> postfilter = new List<EditorCurveBinding>();
var clipBindings = AnimationUtility.GetCurveBindings(clip);
for (int i = 0; i < clipBindings.Length; i++)
{
var bind = clipBindings[i];
if (!bind.propertyName.Contains("LocalRotation.w"))
postfilter.Add(RotationCurveInterpolation.RemapAnimationBindingForRotationCurves(bind, clip));
}
bindings = postfilter.ToArray();
curves = new AnimationCurve[bindings.Length];
for (int i = 0; i < bindings.Length; i++)
{
curves[i] = AnimationUtility.GetEditorCurve(clip, bindings[i]);
}
objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(clip);
objectCurves = new List<ObjectReferenceKeyframe[]>(objectBindings.Length);
for (int i = 0; i < objectBindings.Length; i++)
{
objectCurves.Add(AnimationUtility.GetObjectReferenceCurve(clip, objectBindings[i]));
}
m_CurveDirty = false;
m_KeysDirty = true;
version = version + 1;
}
public bool GetBindingForCurve(AnimationCurve curve, ref EditorCurveBinding binding)
{
for (int i = 0; i < curves.Length; i++)
{
if (curve == curves[i])
{
binding = bindings[i];
return true;
}
}
return false;
}
public AnimationCurve GetCurveForBinding(EditorCurveBinding binding)
{
for (int i = 0; i < curves.Length; i++)
{
if (binding.Equals(bindings[i]))
{
return curves[i];
}
}
return null;
}
public ObjectReferenceKeyframe[] GetObjectCurveForBinding(EditorCurveBinding binding)
{
if (objectCurves == null)
return null;
for (int i = 0; i < objectCurves.Count; i++)
{
if (binding.Equals(objectBindings[i]))
{
return objectCurves[i];
}
}
return null;
}
// given a groupID, get the list of curve bindings
public CurveBindingGroup GetGroupBinding(string groupID)
{
if (m_groupings == null)
m_groupings = new Dictionary<string, CurveBindingGroup>();
CurveBindingGroup result = null;
if (!m_groupings.TryGetValue(groupID, out result))
{
result = new CurveBindingGroup();
result.timeRange = new Vector2(float.MaxValue, float.MinValue);
result.valueRange = new Vector2(float.MaxValue, float.MinValue);
List<CurveBindingPair> found = new List<CurveBindingPair>();
for (int i = 0; i < bindings.Length; i++)
{
if (bindings[i].GetGroupID() == groupID)
{
CurveBindingPair pair = new CurveBindingPair();
pair.binding = bindings[i];
pair.curve = curves[i];
found.Add(pair);
for (int k = 0; k < curves[i].keys.Length; k++)
{
var key = curves[i].keys[k];
result.timeRange = new Vector2(Mathf.Min(key.time, result.timeRange.x), Mathf.Max(key.time, result.timeRange.y));
result.valueRange = new Vector2(Mathf.Min(key.value, result.valueRange.x), Mathf.Max(key.value, result.valueRange.y));
}
}
}
for (int i = 0; i < objectBindings.Length; i++)
{
if (objectBindings[i].GetGroupID() == groupID)
{
CurveBindingPair pair = new CurveBindingPair();
pair.binding = objectBindings[i];
pair.objectCurve = objectCurves[i];
found.Add(pair);
for (int k = 0; k < objectCurves[i].Length; k++)
{
var key = objectCurves[i][k];
result.timeRange = new Vector2(Mathf.Min(key.time, result.timeRange.x), Mathf.Max(key.time, result.timeRange.y));
}
}
}
result.curveBindingPairs = found.OrderBy(x => AnimationWindowUtility.GetComponentIndex(x.binding.propertyName)).ToArray();
m_groupings.Add(groupID, result);
}
return result;
}
}
// Cache for storing the animation clip data
class AnimationClipCurveCache
{
static AnimationClipCurveCache s_Instance;
Dictionary<AnimationClip, AnimationClipCurveInfo> m_ClipCache = new Dictionary<AnimationClip, AnimationClipCurveInfo>();
bool m_IsEnabled;
public static AnimationClipCurveCache Instance
{
get
{
if (s_Instance == null)
{
s_Instance = new AnimationClipCurveCache();
}
return s_Instance;
}
}
public void OnEnable()
{
if (!m_IsEnabled)
{
AnimationUtility.onCurveWasModified += OnCurveWasModified;
m_IsEnabled = true;
}
}
public void OnDisable()
{
if (m_IsEnabled)
{
AnimationUtility.onCurveWasModified -= OnCurveWasModified;
m_IsEnabled = false;
}
}
// callback when a curve is edited. Force the cache to update next time it's accessed
void OnCurveWasModified(AnimationClip clip, EditorCurveBinding binding, AnimationUtility.CurveModifiedType modification)
{
AnimationClipCurveInfo data;
if (m_ClipCache.TryGetValue(clip, out data))
{
data.dirty = true;
}
}
public AnimationClipCurveInfo GetCurveInfo(AnimationClip clip)
{
AnimationClipCurveInfo data;
if (clip == null)
return null;
if (!m_ClipCache.TryGetValue(clip, out data))
{
data = new AnimationClipCurveInfo();
data.dirty = true;
m_ClipCache[clip] = data;
}
if (data.dirty)
{
data.Update(clip);
}
return data;
}
public void ClearCachedProxyClips()
{
var toRemove = new List<AnimationClip>();
foreach (var entry in m_ClipCache)
{
var clip = entry.Key;
if (clip != null && (clip.hideFlags & HideFlags.HideAndDontSave) == HideFlags.HideAndDontSave)
toRemove.Add(clip);
}
foreach (var clip in toRemove)
{
m_ClipCache.Remove(clip);
Object.DestroyImmediate(clip, true);
}
}
public void Clear()
{
ClearCachedProxyClips();
m_ClipCache.Clear();
}
}
static class EditorCurveBindingExtension
{
// identifier to generate an id thats the same for all curves in the same group
public static string GetGroupID(this EditorCurveBinding binding)
{
return binding.type + AnimationWindowUtility.GetPropertyGroupName(binding.propertyName);
}
}
static class CurveBindingGroupExtensions
{
// Extentions to determine curve types
public static bool IsEnableGroup(this CurveBindingGroup curves)
{
return curves.isFloatCurve && curves.count == 1 && curves.curveBindingPairs[0].binding.propertyName == "m_Enabled";
}
public static bool IsVectorGroup(this CurveBindingGroup curves)
{
if (!curves.isFloatCurve)
return false;
if (curves.count <= 1 || curves.count > 4)
return false;
char l = curves.curveBindingPairs[0].binding.propertyName.Last();
return l == 'x' || l == 'y' || l == 'z' || l == 'w';
}
public static bool IsColorGroup(this CurveBindingGroup curves)
{
if (!curves.isFloatCurve)
return false;
if (curves.count != 3 && curves.count != 4)
return false;
char l = curves.curveBindingPairs[0].binding.propertyName.Last();
return l == 'r' || l == 'g' || l == 'b' || l == 'a';
}
public static string GetDescription(this CurveBindingGroup group, float t)
{
string result = string.Empty;
if (group.isFloatCurve)
{
if (group.count > 1)
{
result += "(" + group.curveBindingPairs[0].curve.Evaluate(t).ToString("0.##");
for (int j = 1; j < group.curveBindingPairs.Length; j++)
{
result += "," + group.curveBindingPairs[j].curve.Evaluate(t).ToString("0.##");
}
result += ")";
}
else
{
result = group.curveBindingPairs[0].curve.Evaluate(t).ToString("0.##");
}
}
else if (group.isObjectCurve)
{
Object obj = null;
if (group.curveBindingPairs[0].objectCurve.Length > 0)
obj = CurveEditUtility.Evaluate(group.curveBindingPairs[0].objectCurve, t);
result = (obj == null ? "None" : obj.name);
}
return result;
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
using UnityEngine;
namespace UnityEditor.Timeline
{
static class AnimationClipExtensions
{
public static UInt64 ClipVersion(this AnimationClip clip)
{
if (clip == null)
return 0;
var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
var version = (UInt32)info.version;
var count = (UInt32)info.curves.Length;
var result = (UInt64)version;
result |= ((UInt64)count) << 32;
return result;
}
public static CurveChangeType GetChangeType(this AnimationClip clip, ref UInt64 curveVersion)
{
var version = clip.ClipVersion();
var changeType = CurveChangeType.None;
if ((curveVersion >> 32) != (version >> 32))
changeType = CurveChangeType.CurveAddedOrRemoved;
else if (curveVersion != version)
changeType = CurveChangeType.CurveModified;
curveVersion = version;
return changeType;
}
}
}

View File

@@ -0,0 +1,73 @@
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
static class AnimationOffsetMenu
{
public static string MatchFieldsPrefix = L10n.Tr("Match Offsets Fields/");
static bool EnforcePreviewMode()
{
TimelineEditor.state.previewMode = true; // try and set the preview mode
if (!TimelineEditor.state.previewMode)
{
Debug.LogError("Match clips cannot be completed because preview mode cannot be enabed");
return false;
}
return true;
}
internal static void MatchClipsToPrevious(TimelineClip[] clips)
{
if (!EnforcePreviewMode())
return;
clips = clips.OrderBy(x => x.start).ToArray();
foreach (var clip in clips)
{
var sceneObject = TimelineUtility.GetSceneGameObject(TimelineEditor.inspectedDirector, clip.GetParentTrack());
if (sceneObject != null)
{
TimelineAnimationUtilities.MatchPrevious(clip, sceneObject.transform, TimelineEditor.inspectedDirector);
}
}
InspectorWindow.RepaintAllInspectors();
TimelineEditor.Refresh(RefreshReason.ContentsModified);
}
internal static void MatchClipsToNext(TimelineClip[] clips)
{
if (!EnforcePreviewMode())
return;
clips = clips.OrderByDescending(x => x.start).ToArray();
foreach (var clip in clips)
{
var sceneObject = TimelineUtility.GetSceneGameObject(TimelineEditor.inspectedDirector, clip.GetParentTrack());
if (sceneObject != null)
{
TimelineAnimationUtilities.MatchNext(clip, sceneObject.transform, TimelineEditor.inspectedDirector);
}
}
InspectorWindow.RepaintAllInspectors();
TimelineEditor.Refresh(RefreshReason.ContentsModified);
}
public static void ResetClipOffsets(TimelineClip[] clips)
{
foreach (var clip in clips)
{
var asset = clip.asset as AnimationPlayableAsset;
if (asset != null)
asset.ResetOffsets();
}
InspectorWindow.RepaintAllInspectors();
TimelineEditor.Refresh(RefreshReason.ContentsModified);
}
}
}

View File

@@ -0,0 +1,65 @@
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
[CustomTimelineEditor(typeof(AnimationPlayableAsset)), UsedImplicitly]
class AnimationPlayableAssetEditor : ClipEditor
{
public static readonly string k_NoClipAssignedError = L10n.Tr("No animation clip assigned");
public static readonly string k_LegacyClipError = L10n.Tr("Legacy animation clips are not supported");
static readonly string k_MotionCurveError = L10n.Tr("You are using motion curves without applyRootMotion enabled on the Animator. The root transform will not be animated");
static readonly string k_RootCurveError = L10n.Tr("You are using root curves without applyRootMotion enabled on the Animator. The root transform will not be animated");
/// <inheritdoc/>
public override ClipDrawOptions GetClipOptions(TimelineClip clip)
{
var clipOptions = base.GetClipOptions(clip);
var asset = clip.asset as AnimationPlayableAsset;
if (asset != null)
clipOptions.errorText = GetErrorText(asset, clip.GetParentTrack() as AnimationTrack, clipOptions.errorText);
if (clip.recordable)
clipOptions.highlightColor = DirectorStyles.Instance.customSkin.colorAnimationRecorded;
return clipOptions;
}
/// <inheritdoc />
public override void OnCreate(TimelineClip clip, TrackAsset track, TimelineClip clonedFrom)
{
var asset = clip.asset as AnimationPlayableAsset;
if (asset != null && asset.clip != null && asset.clip.legacy)
{
asset.clip = null;
Debug.LogError("Legacy Animation Clips are not supported");
}
}
string GetErrorText(AnimationPlayableAsset animationAsset, AnimationTrack track, string defaultError)
{
if (animationAsset.clip == null)
return k_NoClipAssignedError;
if (animationAsset.clip.legacy)
return k_LegacyClipError;
if (animationAsset.clip.hasMotionCurves || animationAsset.clip.hasRootCurves)
{
if (track != null && track.trackOffset == TrackOffset.Auto)
{
var animator = track.GetBinding(TimelineEditor.inspectedDirector);
if (animator != null && !animator.applyRootMotion && !animationAsset.clip.hasGenericRootTransform)
{
if (animationAsset.clip.hasMotionCurves)
return k_MotionCurveError;
return k_RootCurveError;
}
}
}
return defaultError;
}
}
}

View File

@@ -0,0 +1,151 @@
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using UnityEditor.Timeline.Actions;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
[MenuEntry("Add Override Track", MenuPriority.CustomTrackActionSection.addOverrideTrack), UsedImplicitly]
class AddOverrideTrackAction : TrackAction
{
public override bool Execute(IEnumerable<TrackAsset> tracks)
{
foreach (var animTrack in tracks.OfType<AnimationTrack>())
{
TimelineHelpers.CreateTrack(typeof(AnimationTrack), animTrack, "Override " + animTrack.GetChildTracks().Count());
}
return true;
}
public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
{
if (tracks.Any(t => t.isSubTrack || !t.GetType().IsAssignableFrom(typeof(AnimationTrack))))
return ActionValidity.NotApplicable;
if (tracks.Any(t => t.lockedInHierarchy))
return ActionValidity.Invalid;
return ActionValidity.Valid;
}
}
[MenuEntry("Convert To Clip Track", MenuPriority.CustomTrackActionSection.convertToClipMode), UsedImplicitly]
class ConvertToClipModeAction : TrackAction
{
public override bool Execute(IEnumerable<TrackAsset> tracks)
{
foreach (var animTrack in tracks.OfType<AnimationTrack>())
animTrack.ConvertToClipMode();
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
return true;
}
public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
{
if (tracks.Any(t => !t.GetType().IsAssignableFrom(typeof(AnimationTrack))))
return ActionValidity.NotApplicable;
if (tracks.Any(t => t.lockedInHierarchy))
return ActionValidity.Invalid;
if (tracks.OfType<AnimationTrack>().All(a => a.CanConvertToClipMode()))
return ActionValidity.Valid;
return ActionValidity.NotApplicable;
}
}
[MenuEntry("Convert To Infinite Clip", MenuPriority.CustomTrackActionSection.convertFromClipMode), UsedImplicitly]
class ConvertFromClipTrackAction : TrackAction
{
public override bool Execute(IEnumerable<TrackAsset> tracks)
{
foreach (var animTrack in tracks.OfType<AnimationTrack>())
animTrack.ConvertFromClipMode(TimelineEditor.inspectedAsset);
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
return true;
}
public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
{
if (tracks.Any(t => !t.GetType().IsAssignableFrom(typeof(AnimationTrack))))
return ActionValidity.NotApplicable;
if (tracks.Any(t => t.lockedInHierarchy))
return ActionValidity.Invalid;
if (tracks.OfType<AnimationTrack>().All(a => a.CanConvertFromClipMode()))
return ActionValidity.Valid;
return ActionValidity.NotApplicable;
}
}
abstract class TrackOffsetBaseAction : TrackAction
{
public abstract TrackOffset trackOffset { get; }
public override ActionValidity Validate(IEnumerable<TrackAsset> tracks)
{
if (tracks.Any(t => !t.GetType().IsAssignableFrom(typeof(AnimationTrack))))
return ActionValidity.NotApplicable;
if (tracks.Any(t => t.lockedInHierarchy))
{
return ActionValidity.Invalid;
}
return ActionValidity.Valid;
}
public override bool Execute(IEnumerable<TrackAsset> tracks)
{
foreach (var animTrack in tracks.OfType<AnimationTrack>())
{
animTrack.UnarmForRecord();
animTrack.trackOffset = trackOffset;
}
TimelineEditor.Refresh(RefreshReason.ContentsModified);
return true;
}
}
[MenuEntry("Track Offsets/Apply Transform Offsets", MenuPriority.CustomTrackActionSection.applyTrackOffset), UsedImplicitly]
[ApplyDefaultUndo]
class ApplyTransformOffsetAction : TrackOffsetBaseAction
{
public override TrackOffset trackOffset
{
get { return TrackOffset.ApplyTransformOffsets; }
}
}
[MenuEntry("Track Offsets/Apply Scene Offsets", MenuPriority.CustomTrackActionSection.applySceneOffset), UsedImplicitly]
[ApplyDefaultUndo]
class ApplySceneOffsetAction : TrackOffsetBaseAction
{
public override TrackOffset trackOffset
{
get { return TrackOffset.ApplySceneOffsets; }
}
}
[MenuEntry("Track Offsets/Auto (Deprecated)", MenuPriority.CustomTrackActionSection.applyAutoOffset), UsedImplicitly]
[ApplyDefaultUndo]
class ApplyAutoAction : TrackOffsetBaseAction
{
public override TrackOffset trackOffset
{
get { return TrackOffset.Auto; }
}
}
}

View File

@@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.IMGUI.Controls;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class BindingSelector
{
TreeViewController m_TreeView;
public TreeViewController treeViewController
{
get { return m_TreeView; }
}
TreeViewState m_TrackGlobalTreeViewState;
TreeViewState m_TreeViewState;
BindingTreeViewDataSource m_TreeViewDataSource;
CurveDataSource m_CurveDataSource;
TimelineWindow m_Window;
CurveEditor m_CurveEditor;
ReorderableList m_DopeLines;
string[] m_StringList = {};
int[] m_Selection;
bool m_PartOfSelection;
public BindingSelector(EditorWindow window, CurveEditor curveEditor, TreeViewState trackGlobalTreeViewState)
{
m_Window = window as TimelineWindow;
m_CurveEditor = curveEditor;
m_TrackGlobalTreeViewState = trackGlobalTreeViewState;
m_DopeLines = new ReorderableList(m_StringList, typeof(string), false, false, false, false);
m_DopeLines.drawElementBackgroundCallback = null;
m_DopeLines.showDefaultBackground = false;
m_DopeLines.index = 0;
m_DopeLines.headerHeight = 0;
m_DopeLines.elementHeight = 20;
m_DopeLines.draggable = false;
}
public void OnGUI(Rect targetRect)
{
if (m_TreeView == null)
return;
m_TreeView.OnEvent();
m_TreeView.OnGUI(targetRect, GUIUtility.GetControlID(FocusType.Passive));
}
public void InitIfNeeded(Rect rect, CurveDataSource dataSource, bool isNewSelection)
{
if (Event.current.type != EventType.Layout)
return;
m_CurveDataSource = dataSource;
var clip = dataSource.animationClip;
List<EditorCurveBinding> allBindings = new List<EditorCurveBinding>();
allBindings.Add(new EditorCurveBinding { propertyName = "Summary" });
if (clip != null)
allBindings.AddRange(AnimationUtility.GetCurveBindings(clip));
m_DopeLines.list = allBindings.ToArray();
if (m_TreeViewState != null)
{
if (isNewSelection)
RefreshAll();
return;
}
m_TreeViewState = m_TrackGlobalTreeViewState != null ? m_TrackGlobalTreeViewState : new TreeViewState();
m_TreeView = new TreeViewController(m_Window, m_TreeViewState)
{
useExpansionAnimation = false,
deselectOnUnhandledMouseDown = true
};
m_TreeView.selectionChangedCallback += OnItemSelectionChanged;
m_TreeViewDataSource = new BindingTreeViewDataSource(m_TreeView, clip, m_CurveDataSource);
m_TreeView.Init(rect, m_TreeViewDataSource, new BindingTreeViewGUI(m_TreeView), null);
m_TreeViewDataSource.UpdateData();
RefreshSelection();
}
void OnItemSelectionChanged(int[] selection)
{
RefreshSelection(selection);
}
void RefreshAll()
{
RefreshTree();
RefreshSelection();
}
void RefreshSelection()
{
RefreshSelection(m_TreeViewState.selectedIDs != null ? m_TreeViewState.selectedIDs.ToArray() : null);
}
void RefreshSelection(int[] selection)
{
if (selection == null || selection.Length == 0)
{
// select all.
if (m_TreeViewDataSource.GetRows().Count > 0)
{
m_Selection = m_TreeViewDataSource.GetRows().Select(r => r.id).ToArray();
}
}
else
{
m_Selection = selection;
}
RefreshCurves();
}
public void RefreshCurves()
{
if (m_CurveDataSource == null || m_Selection == null)
return;
var bindings = new HashSet<EditorCurveBinding>(AnimationPreviewUtilities.EditorCurveBindingComparer.Instance);
foreach (int s in m_Selection)
{
var item = (CurveTreeViewNode)m_TreeView.FindItem(s);
if (item != null && item.bindings != null)
bindings.UnionWith(item.bindings);
}
var wrappers = m_CurveDataSource.GenerateWrappers(bindings);
m_CurveEditor.animationCurves = wrappers.ToArray();
}
public void RefreshTree()
{
if (m_TreeViewDataSource == null)
return;
if (m_Selection == null)
m_Selection = new int[0];
// get the names of the previous items
var selected = m_Selection.Select(x => m_TreeViewDataSource.FindItem(x)).Where(t => t != null).Select(c => c.displayName).ToArray();
// update the source
m_TreeViewDataSource.UpdateData();
// find the same items
var reselected = m_TreeViewDataSource.GetRows().Where(x => selected.Contains(x.displayName)).Select(x => x.id).ToArray();
if (!reselected.Any())
{
if (m_TreeViewDataSource.GetRows().Count > 0)
{
reselected = new[] { m_TreeViewDataSource.GetItem(0).id };
}
}
// update the selection
OnItemSelectionChanged(reselected);
}
internal virtual bool IsRenamingNodeAllowed(TreeViewItem node)
{
return false;
}
}
}

View File

@@ -0,0 +1,220 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.IMGUI.Controls;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.Timeline;
#if !UNITY_2020_2_OR_NEWER
using L10n = UnityEditor.Timeline.L10n;
#endif
namespace UnityEditor.Timeline
{
class BindingTreeViewDataSource : TreeViewDataSource
{
struct BindingGroup : IEquatable<BindingGroup>, IComparable<BindingGroup>
{
public readonly string GroupName;
public readonly string Path;
public readonly Type Type;
public BindingGroup(string path, string groupName, Type type)
{
Path = path;
GroupName = groupName;
Type = type;
}
public string groupDisplayName => string.IsNullOrEmpty(Path) ? GroupName : string.Format($"{Path} : {GroupName}");
public bool Equals(BindingGroup other) => GroupName == other.GroupName && Type == other.Type && Path == other.Path;
public int CompareTo(BindingGroup other) => GetHashCode() - other.GetHashCode();
public override bool Equals(object obj) => obj is BindingGroup other && Equals(other);
public override int GetHashCode()
{
return HashUtility.CombineHash(GroupName != null ? GroupName.GetHashCode() : 0, Type != null ? Type.GetHashCode() : 0, Path != null ? Path.GetHashCode() : 0);
}
}
static readonly string s_DefaultValue = L10n.Tr("{0} (Default Value)");
public const int RootID = int.MinValue;
public const int GroupID = -1;
private readonly AnimationClip m_Clip;
private readonly CurveDataSource m_CurveDataSource;
public BindingTreeViewDataSource(
TreeViewController treeView, AnimationClip clip, CurveDataSource curveDataSource)
: base(treeView)
{
m_Clip = clip;
showRootItem = false;
m_CurveDataSource = curveDataSource;
}
void SetupRootNodeSettings()
{
showRootItem = false;
SetExpanded(RootID, true);
SetExpanded(GroupID, true);
}
public override void FetchData()
{
if (m_Clip == null)
return;
var bindings = AnimationUtility.GetCurveBindings(m_Clip)
.Union(AnimationUtility.GetObjectReferenceCurveBindings(m_Clip))
.ToArray();
// a sorted linear list of nodes
var results = bindings.GroupBy(GetBindingGroup, p => p, CreateTuple)
.OrderBy(t => t.Item1.Path)
.ThenBy(NamePrioritySort)
// this makes component ordering match the animation window
.ThenBy(t => t.Item1.Type.ToString())
.ThenBy(t => t.Item1.GroupName).ToArray();
m_RootItem = new CurveTreeViewNode(RootID, null, "root", null)
{
children = new List<TreeViewItem>(1)
};
if (results.Any())
{
var groupingNode = new CurveTreeViewNode(GroupID, m_RootItem, m_CurveDataSource.groupingName, bindings)
{
children = new List<TreeViewItem>()
};
m_RootItem.children.Add(groupingNode);
foreach (var r in results)
{
var key = r.Item1;
var nodeBindings = r.Item2;
FillMissingTransformCurves(nodeBindings);
if (nodeBindings.Count == 1)
groupingNode.children.Add(CreateLeafNode(nodeBindings[0], groupingNode, PropertyName(nodeBindings[0], true)));
else if (nodeBindings.Count > 1)
{
var childBindings = nodeBindings.OrderBy(BindingSort).ToArray();
var parent = new CurveTreeViewNode(key.GetHashCode(), groupingNode, key.groupDisplayName, childBindings) {children = new List<TreeViewItem>()};
groupingNode.children.Add(parent);
foreach (var b in childBindings)
parent.children.Add(CreateLeafNode(b, parent, PropertyName(b, false)));
}
}
SetupRootNodeSettings();
}
m_NeedRefreshRows = true;
}
public void UpdateData()
{
m_TreeView.ReloadData();
}
string GroupName(EditorCurveBinding binding)
{
var propertyName = m_CurveDataSource.ModifyPropertyDisplayName(binding.path, binding.propertyName);
return CleanUpArrayBinding(AnimationWindowUtility.NicifyPropertyGroupName(binding.type, propertyName), true);
}
static string CleanUpArrayBinding(string propertyName, bool isGroup)
{
const string arrayIndicator = ".Array.data[";
const string arrayDisplay = ".data[";
var arrayIndex = propertyName.LastIndexOf(arrayIndicator, StringComparison.Ordinal);
if (arrayIndex == -1)
return propertyName;
if (isGroup)
propertyName = propertyName.Substring(0, arrayIndex);
return propertyName.Replace(arrayIndicator, arrayDisplay);
}
string PropertyName(EditorCurveBinding binding, bool prependPathName)
{
var propertyName = m_CurveDataSource.ModifyPropertyDisplayName(binding.path, binding.propertyName);
propertyName = CleanUpArrayBinding(AnimationWindowUtility.GetPropertyDisplayName(propertyName), false);
if (binding.isPhantom)
propertyName = string.Format(s_DefaultValue, propertyName);
if (prependPathName && !string.IsNullOrEmpty(binding.path))
propertyName = $"{binding.path} : {propertyName}";
return propertyName;
}
BindingGroup GetBindingGroup(EditorCurveBinding binding)
{
return new BindingGroup(binding.path ?? string.Empty, GroupName(binding), binding.type);
}
static CurveTreeViewNode CreateLeafNode(EditorCurveBinding binding, TreeViewItem parent, string displayName)
{
return new CurveTreeViewNode(binding.GetHashCode(), parent, displayName, new[] { binding }, AnimationWindowUtility.ForceGrouping(binding));
}
static void FillMissingTransformCurves(List<EditorCurveBinding> bindings)
{
if (!AnimationWindowUtility.IsActualTransformCurve(bindings[0]) || bindings.Count >= 3)
return;
var binding = bindings[0];
var prefixPropertyName = binding.propertyName.Split('.').First();
binding.isPhantom = true;
if (!bindings.Any(p => p.propertyName.EndsWith(".x")))
{
binding.propertyName = prefixPropertyName + ".x";
bindings.Insert(0, binding);
}
if (!bindings.Any(p => p.propertyName.EndsWith(".y")))
{
binding.propertyName = prefixPropertyName + ".y";
bindings.Insert(1, binding);
}
if (!bindings.Any(p => p.propertyName.EndsWith(".z")))
{
binding.propertyName = prefixPropertyName + ".z";
bindings.Insert(2, binding);
}
}
// make sure vectors and colors are sorted correctly in their subgroups
static int BindingSort(EditorCurveBinding b)
{
return AnimationWindowUtility.GetComponentIndex(b.propertyName);
}
static int NamePrioritySort(ValueTuple<BindingGroup, List<EditorCurveBinding>> group)
{
if (group.Item1.Type != typeof(Transform))
return 0;
switch (group.Item1.GroupName)
{
case "Position": return Int32.MinValue;
case "Rotation": return Int32.MinValue + 1;
case "Scale": return Int32.MinValue + 2;
default: return 0;
}
}
static ValueTuple<BindingGroup, List<EditorCurveBinding>> CreateTuple(BindingGroup key, IEnumerable<EditorCurveBinding> items)
{
return new ValueTuple<BindingGroup, List<EditorCurveBinding>>()
{
Item1 = key,
Item2 = items.ToList()
};
}
}
}

View File

@@ -0,0 +1,113 @@
using System.Linq;
using UnityEditor.IMGUI.Controls;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class BindingTreeViewGUI : TreeViewGUI
{
static readonly float s_RowRightOffset = 10;
static readonly float s_ColorIndicatorTopMargin = 3;
static readonly Color s_KeyColorForNonCurves = new Color(0.7f, 0.7f, 0.7f, 0.5f);
static readonly Color s_ChildrenCurveLabelColor = new Color(1.0f, 1.0f, 1.0f, 0.7f);
static readonly Color s_PhantomPropertyLabelColor = new Color(0.0f, 0.8f, 0.8f, 1f);
static readonly Texture2D s_DefaultScriptTexture = EditorGUIUtility.LoadIcon("cs Script Icon");
static readonly Texture2D s_TrackDefault = EditorGUIUtility.LoadIcon("UnityEngine/ScriptableObject Icon");
public BindingTreeViewGUI(TreeViewController treeView)
: base(treeView, true)
{
k_IconWidth = 13.0f;
iconOverlayGUI += OnItemIconOverlay;
}
public override void OnRowGUI(Rect rowRect, TreeViewItem node, int row, bool selected, bool focused)
{
Color originalColor = GUI.color;
bool leafNode = node.parent != null && node.parent.id != BindingTreeViewDataSource.RootID && node.parent.id != BindingTreeViewDataSource.GroupID;
GUI.color = Color.white;
if (leafNode)
{
CurveTreeViewNode curveNode = node as CurveTreeViewNode;
if (curveNode != null && curveNode.bindings.Any() && curveNode.bindings.First().isPhantom)
GUI.color = s_PhantomPropertyLabelColor;
else
GUI.color = s_ChildrenCurveLabelColor;
}
base.OnRowGUI(rowRect, node, row, selected, focused);
GUI.color = originalColor;
DoCurveColorIndicator(rowRect, node as CurveTreeViewNode);
}
protected override bool IsRenaming(int id)
{
return false;
}
public override bool BeginRename(TreeViewItem item, float delay)
{
return false;
}
static void DoCurveColorIndicator(Rect rect, CurveTreeViewNode node)
{
if (node == null)
return;
if (Event.current.type != EventType.Repaint)
return;
Color originalColor = GUI.color;
if (node.bindings.Length == 1 && !node.bindings[0].isPPtrCurve)
GUI.color = CurveUtility.GetPropertyColor(node.bindings[0].propertyName);
else
GUI.color = s_KeyColorForNonCurves;
Texture icon = CurveUtility.GetIconCurve();
rect = new Rect(rect.xMax - s_RowRightOffset - (icon.width * 0.5f) - 5, rect.yMin + s_ColorIndicatorTopMargin, icon.width, icon.height);
GUI.DrawTexture(rect, icon, ScaleMode.ScaleToFit, true, 1);
GUI.color = originalColor;
}
protected override Texture GetIconForItem(TreeViewItem item)
{
var node = item as CurveTreeViewNode;
if (node == null)
return null;
var type = node.iconType;
if (type == null)
return null;
// track type icon
if (typeof(TrackAsset).IsAssignableFrom(type))
{
var icon = TrackResourceCache.GetTrackIconForType(type);
return icon == s_TrackDefault ? s_DefaultScriptTexture : icon;
}
// custom clip icons always use the script texture
if (typeof(PlayableAsset).IsAssignableFrom(type))
return s_DefaultScriptTexture;
// this will return null for MonoBehaviours without a custom icon.
// use the scripting icon instead
return AssetPreview.GetMiniTypeThumbnail(type) ?? s_DefaultScriptTexture;
}
static void OnItemIconOverlay(TreeViewItem item, Rect rect)
{
var curveNodeItem = item as CurveTreeViewNode;
if (curveNodeItem != null && curveNodeItem.iconOverlay != null)
GUI.Label(rect, curveNodeItem.iconOverlay);
}
}
}

View File

@@ -0,0 +1,367 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.Timeline;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor
{
class ClipCurveEditor
{
static readonly GUIContent s_RemoveCurveContent = new GUIContent(L10n.Tr("Remove Curve"));
static readonly GUIContent s_RemoveCurvesContent = new GUIContent(L10n.Tr("Remove Curves"));
internal readonly CurveEditor m_CurveEditor;
static readonly CurveEditorSettings s_CurveEditorSettings = new CurveEditorSettings
{
hSlider = false,
vSlider = false,
hRangeLocked = false,
vRangeLocked = false,
scaleWithWindow = true,
hRangeMin = 0.0f,
showAxisLabels = true,
allowDeleteLastKeyInCurve = true,
rectangleToolFlags = CurveEditorSettings.RectangleToolFlags.MiniRectangleTool
};
static readonly float s_GridLabelWidth = 40.0f;
readonly BindingSelector m_BindingHierarchy;
public BindingSelector bindingHierarchy
{
get { return m_BindingHierarchy; }
}
public Rect shownAreaInsideMargins
{
get { return m_CurveEditor != null ? m_CurveEditor.shownAreaInsideMargins : new Rect(1, 1, 1, 1); }
}
Vector2 m_ScrollPosition = Vector2.zero;
readonly CurveDataSource m_DataSource;
float m_LastFrameRate = 30.0f;
UInt64 m_LastClipVersion = UInt64.MaxValue;
TrackViewModelData m_ViewModel;
bool m_ShouldRestoreShownArea;
bool isNewSelection
{
get
{
if (m_ViewModel == null || m_DataSource == null)
return true;
return m_ViewModel.lastInlineCurveDataID != m_DataSource.id;
}
}
internal CurveEditor curveEditor
{
get { return m_CurveEditor; }
}
public ClipCurveEditor(CurveDataSource dataSource, TimelineWindow parentWindow, TrackAsset hostTrack)
{
m_DataSource = dataSource;
m_CurveEditor = new CurveEditor(new Rect(0, 0, 1000, 100), new CurveWrapper[0], false);
s_CurveEditorSettings.vTickStyle = new TickStyle
{
tickColor = { color = DirectorStyles.Instance.customSkin.colorInlineCurveVerticalLines },
distLabel = 20,
stubs = true
};
s_CurveEditorSettings.hTickStyle = new TickStyle
{
// hide horizontal lines by giving them a transparent color
tickColor = { color = new Color(0.0f, 0.0f, 0.0f, 0.0f) },
distLabel = 0
};
m_CurveEditor.settings = s_CurveEditorSettings;
m_ViewModel = TimelineWindowViewPrefs.GetTrackViewModelData(hostTrack);
m_ShouldRestoreShownArea = true;
m_CurveEditor.ignoreScrollWheelUntilClicked = true;
m_CurveEditor.curvesUpdated = OnCurvesUpdated;
m_BindingHierarchy = new BindingSelector(parentWindow, m_CurveEditor, m_ViewModel.inlineCurvesState);
}
public void SelectAllKeys()
{
m_CurveEditor.SelectAll();
}
public void FrameClip()
{
m_CurveEditor.InvalidateBounds();
m_CurveEditor.FrameClip(false, true);
}
public CurveDataSource dataSource
{
get { return m_DataSource; }
}
// called when curves are edited
internal void OnCurvesUpdated()
{
if (m_DataSource == null)
return;
if (m_CurveEditor == null)
return;
if (m_CurveEditor.animationCurves.Length == 0)
return;
List<CurveWrapper> curvesToUpdate = m_CurveEditor.animationCurves.Where(c => c.changed).ToList();
// nothing changed, return.
if (curvesToUpdate.Count == 0)
return;
// something changed, manage the undo properly.
m_DataSource.ApplyCurveChanges(curvesToUpdate);
m_LastClipVersion = m_DataSource.GetClipVersion();
}
public void DrawHeader(Rect headerRect)
{
m_BindingHierarchy.InitIfNeeded(headerRect, m_DataSource, isNewSelection);
try
{
GUILayout.BeginArea(headerRect);
m_ScrollPosition = GUILayout.BeginScrollView(m_ScrollPosition, GUIStyle.none, GUI.skin.verticalScrollbar);
m_BindingHierarchy.OnGUI(new Rect(0, 0, headerRect.width, headerRect.height));
if (m_BindingHierarchy.treeViewController != null)
m_BindingHierarchy.treeViewController.contextClickItemCallback = ContextClickItemCallback;
GUILayout.EndScrollView();
GUILayout.EndArea();
}
catch (Exception e)
{
Debug.LogException(e);
}
}
void ContextClickItemCallback(int obj)
{
GenerateContextMenu(obj);
}
void GenerateContextMenu(int obj = -1)
{
if (Event.current.type != EventType.ContextClick)
return;
var selectedCurves = GetSelectedProperties().ToArray();
if (selectedCurves.Length > 0)
{
var menu = new GenericMenu();
var content = selectedCurves.Length == 1 ? s_RemoveCurveContent : s_RemoveCurvesContent;
menu.AddItem(content,
false,
() => RemoveCurves(selectedCurves)
);
menu.ShowAsContext();
}
}
public IEnumerable<EditorCurveBinding> GetSelectedProperties(bool useForcedGroups = false)
{
var bindings = new HashSet<EditorCurveBinding>();
var bindingTree = m_BindingHierarchy.treeViewController.data as BindingTreeViewDataSource;
foreach (var selectedId in m_BindingHierarchy.treeViewController.GetSelection())
{
var node = bindingTree.FindItem(selectedId) as CurveTreeViewNode;
if (node == null)
continue;
var curveNodeParent = node.parent as CurveTreeViewNode;
if (useForcedGroups && node.forceGroup && curveNodeParent != null)
bindings.UnionWith(curveNodeParent.bindings);
else
bindings.UnionWith(node.bindings);
}
return bindings;
}
public void RemoveCurves(IEnumerable<EditorCurveBinding> bindings)
{
m_DataSource.RemoveCurves(bindings);
m_BindingHierarchy.RefreshTree();
TimelineWindow.instance.state.CalculateRowRects();
m_LastClipVersion = m_DataSource.GetClipVersion();
}
class CurveEditorState : ICurveEditorState
{
public TimeArea.TimeFormat timeFormat { get; set; }
public Vector2 timeRange => new Vector2(0, 1);
public bool rippleTime => false;
}
void UpdateCurveEditorIfNeeded(WindowState state)
{
if ((Event.current.type != EventType.Layout) || (m_DataSource == null) || (m_BindingHierarchy == null))
return;
// check if the curves have changed externally
var curveChange = m_DataSource.UpdateExternalChanges(ref m_LastClipVersion);
if (curveChange == CurveChangeType.None)
return;
if (curveChange == CurveChangeType.CurveAddedOrRemoved)
m_BindingHierarchy.RefreshTree();
else // curve modified
m_BindingHierarchy.RefreshCurves();
m_CurveEditor.InvalidateSelectionBounds();
m_CurveEditor.state = new CurveEditorState() {timeFormat = state.timeFormat.ToTimeAreaFormat()};
m_CurveEditor.invSnap = state.referenceSequence.frameRate;
}
public void DrawCurveEditor(Rect rect, WindowState state, Vector2 clipRange, bool loop, bool selected)
{
SetupMarginsAndRect(rect, state);
UpdateCurveEditorIfNeeded(state);
if (m_ShouldRestoreShownArea)
RestoreShownArea();
var curveVisibleTimeRange = CalculateCurveVisibleTimeRange(state.timeAreaShownRange, m_DataSource);
m_CurveEditor.SetShownHRangeInsideMargins(curveVisibleTimeRange.x, curveVisibleTimeRange.y); //align the curve with the clip.
if (m_LastFrameRate != state.referenceSequence.frameRate)
{
m_CurveEditor.hTicks.SetTickModulosForFrameRate(state.referenceSequence.frameRate);
m_LastFrameRate = state.referenceSequence.frameRate;
}
foreach (var cw in m_CurveEditor.animationCurves)
cw.renderer.SetWrap(WrapMode.Default, loop ? WrapMode.Loop : WrapMode.Default);
using (new GUIGroupScope(rect))
{
var localRect = new Rect(0.0f, 0.0f, rect.width, rect.height);
var localClipRange = new Vector2(Mathf.Floor(clipRange.x - rect.xMin), Mathf.Ceil(clipRange.y - rect.xMin));
var curveStartPosX = Mathf.Floor(state.TimeToPixel(m_DataSource.start) - rect.xMin);
EditorGUI.DrawRect(new Rect(curveStartPosX, 0.0f, 1.0f, rect.height), new Color(1.0f, 1.0f, 1.0f, 0.5f));
DrawCurveEditorBackground(localRect);
if (selected)
{
var selectionRect = new Rect(localClipRange.x, 0.0f, localClipRange.y - localClipRange.x, localRect.height);
DrawOutline(selectionRect);
}
EditorGUI.BeginChangeCheck();
{
var evt = Event.current;
if (evt.type == EventType.Layout || evt.type == EventType.Repaint || selected)
m_CurveEditor.CurveGUI();
}
if (EditorGUI.EndChangeCheck())
OnCurvesUpdated();
DrawOverlay(localRect, localClipRange, DirectorStyles.Instance.customSkin.colorInlineCurveOutOfRangeOverlay);
DrawGrid(localRect, curveStartPosX);
}
}
static Vector2 CalculateCurveVisibleTimeRange(Vector2 timeAreaShownRange, CurveDataSource curve)
{
var curveVisibleTimeRange = new Vector2
{
x = Math.Max(0.0f, timeAreaShownRange.x - curve.start),
y = timeAreaShownRange.y - curve.start
};
return curveVisibleTimeRange * curve.timeScale;
}
void SetupMarginsAndRect(Rect rect, WindowState state)
{
var startX = state.TimeToPixel(m_DataSource.start) - rect.x;
var timelineWidth = state.timeAreaRect.width;
m_CurveEditor.rect = new Rect(0.0f, 0.0f, timelineWidth, rect.height);
m_CurveEditor.leftmargin = Math.Max(startX, 0.0f);
m_CurveEditor.rightmargin = 0.0f;
m_CurveEditor.topmargin = m_CurveEditor.bottommargin = CalculateTopMargin(rect.height);
}
void RestoreShownArea()
{
if (isNewSelection)
FrameClip();
else
m_CurveEditor.shownAreaInsideMargins = m_ViewModel.inlineCurvesShownAreaInsideMargins;
m_ShouldRestoreShownArea = false;
}
static void DrawCurveEditorBackground(Rect rect)
{
if (EditorGUIUtility.isProSkin)
return;
var animEditorBackgroundRect = Rect.MinMaxRect(0.0f, rect.yMin, rect.xMax, rect.yMax);
// Curves are not legible in Personal Skin so we need to darken the background a bit.
EditorGUI.DrawRect(animEditorBackgroundRect, DirectorStyles.Instance.customSkin.colorInlineCurvesBackground);
}
static float CalculateTopMargin(float height)
{
return Mathf.Clamp(0.15f * height, 10.0f, 40.0f);
}
static void DrawOutline(Rect rect, float thickness = 2.0f)
{
// Draw top selected lines.
EditorGUI.DrawRect(new Rect(rect.xMin, rect.yMin, rect.width, thickness), Color.white);
// Draw bottom selected lines.
EditorGUI.DrawRect(new Rect(rect.xMin, rect.yMax - thickness, rect.width, thickness), Color.white);
// Draw Left Selected Lines
EditorGUI.DrawRect(new Rect(rect.xMin, rect.yMin, thickness, rect.height), Color.white);
// Draw Right Selected Lines
EditorGUI.DrawRect(new Rect(rect.xMax - thickness, rect.yMin, thickness, rect.height), Color.white);
}
static void DrawOverlay(Rect rect, Vector2 clipRange, Color color)
{
var leftSide = new Rect(rect.xMin, rect.yMin, clipRange.x - rect.xMin, rect.height);
EditorGUI.DrawRect(leftSide, color);
var rightSide = new Rect(Mathf.Max(0.0f, clipRange.y), rect.yMin, rect.xMax, rect.height);
EditorGUI.DrawRect(rightSide, color);
}
void DrawGrid(Rect rect, float curveXPosition)
{
var gridXPos = Mathf.Max(curveXPosition - s_GridLabelWidth, rect.xMin);
var gridRect = new Rect(gridXPos, rect.y, s_GridLabelWidth, rect.height);
var originalRect = m_CurveEditor.rect;
m_CurveEditor.rect = new Rect(0.0f, 0.0f, rect.width, rect.height);
using (new GUIGroupScope(gridRect))
m_CurveEditor.GridGUI();
m_CurveEditor.rect = originalRect;
}
}
}

View File

@@ -0,0 +1,439 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
enum CurveChangeType
{
None,
CurveModified,
CurveAddedOrRemoved
}
abstract class CurveDataSource
{
public static CurveDataSource Create(IRowGUI trackGUI)
{
if (trackGUI.asset is AnimationTrack)
return new InfiniteClipCurveDataSource(trackGUI);
return new TrackParametersCurveDataSource(trackGUI);
}
public static CurveDataSource Create(TimelineClipGUI clipGUI)
{
if (clipGUI.clip.animationClip != null)
return new ClipAnimationCurveDataSource(clipGUI);
return new ClipParametersCurveDataSource(clipGUI);
}
int? m_ID = null;
public int id
{
get
{
if (!m_ID.HasValue)
m_ID = CreateHashCode();
return m_ID.Value;
}
}
readonly IRowGUI m_TrackGUI;
protected CurveDataSource(IRowGUI trackGUI)
{
m_TrackGUI = trackGUI;
}
public abstract AnimationClip animationClip { get; }
public abstract float start { get; }
public abstract float timeScale { get; }
public abstract string groupingName { get; }
// Applies changes from the visual curve in the curve wrapper back to the animation clips
public virtual void ApplyCurveChanges(IEnumerable<CurveWrapper> updatedCurves)
{
Undo.RegisterCompleteObjectUndo(animationClip, "Edit Clip Curve");
foreach (CurveWrapper c in updatedCurves)
{
if (c.curve.length > 0)
AnimationUtility.SetEditorCurve(animationClip, c.binding, c.curve);
else
RemoveCurves(new[] {c.binding});
c.changed = false;
}
}
/// <summary>The clip version is a value that will change when a curve gets updated.
/// it's used to detect when an animation clip has been changed externally </summary>
/// <returns>A versioning value indicating the state of the curve. If the curve is updated externally this value will change. </returns>
public virtual UInt64 GetClipVersion()
{
return animationClip.ClipVersion();
}
/// <summary>Call this method to check if the underlying clip has changed</summary>
/// <param name="curveVersion">A versioning value. This will be updated to the latest version</param>
/// <returns>A value indicating how the clip has changed</returns>
public virtual CurveChangeType UpdateExternalChanges(ref UInt64 curveVersion)
{
return animationClip.GetChangeType(ref curveVersion);
}
public virtual string ModifyPropertyDisplayName(string path, string propertyName) => propertyName;
public virtual void RemoveCurves(IEnumerable<EditorCurveBinding> bindings)
{
Undo.RegisterCompleteObjectUndo(animationClip, "Remove Curve(s)");
foreach (var binding in bindings)
{
if (binding.isPPtrCurve)
AnimationUtility.SetObjectReferenceCurve(animationClip, binding, null);
else
AnimationUtility.SetEditorCurve(animationClip, binding, null);
}
}
public Rect GetBackgroundRect(WindowState state)
{
var trackRect = m_TrackGUI.boundingRect;
return new Rect(
state.timeAreaTranslation.x + trackRect.xMin,
trackRect.y,
(float)state.editSequence.asset.duration * state.timeAreaScale.x,
trackRect.height
);
}
public List<CurveWrapper> GenerateWrappers(IEnumerable<EditorCurveBinding> bindings)
{
var wrappers = new List<CurveWrapper>(bindings.Count());
int curveWrapperId = 0;
foreach (EditorCurveBinding b in bindings)
{
// General configuration
var wrapper = new CurveWrapper
{
id = curveWrapperId++,
binding = b,
groupId = -1,
hidden = false,
readOnly = false,
getAxisUiScalarsCallback = () => new Vector2(1, 1)
};
// Specific configuration
ConfigureCurveWrapper(wrapper);
wrappers.Add(wrapper);
}
return wrappers;
}
protected virtual void ConfigureCurveWrapper(CurveWrapper wrapper)
{
wrapper.color = CurveUtility.GetPropertyColor(wrapper.binding.propertyName);
wrapper.renderer = new NormalCurveRenderer(AnimationUtility.GetEditorCurve(animationClip, wrapper.binding));
wrapper.renderer.SetCustomRange(0.0f, animationClip.length);
}
protected virtual int CreateHashCode()
{
return m_TrackGUI.asset.GetHashCode();
}
}
class ClipAnimationCurveDataSource : CurveDataSource
{
static readonly string k_GroupingName = L10n.Tr("Animated Values");
readonly TimelineClipGUI m_ClipGUI;
public ClipAnimationCurveDataSource(TimelineClipGUI clipGUI) : base(clipGUI.parent)
{
m_ClipGUI = clipGUI;
}
public override AnimationClip animationClip
{
get { return m_ClipGUI.clip.animationClip; }
}
public override float start
{
get { return (float)m_ClipGUI.clip.FromLocalTimeUnbound(0.0); }
}
public override float timeScale
{
get { return (float)m_ClipGUI.clip.timeScale; }
}
public override string groupingName
{
get { return k_GroupingName; }
}
protected override int CreateHashCode()
{
return base.CreateHashCode().CombineHash(m_ClipGUI.clip.GetHashCode());
}
public override string ModifyPropertyDisplayName(string path, string propertyName)
{
if (!AnimatedPropertyUtility.IsMaterialProperty(propertyName))
return propertyName;
var track = m_ClipGUI.clip.GetParentTrack();
if (track == null)
return propertyName;
var gameObjectBinding = TimelineUtility.GetSceneGameObject(TimelineEditor.inspectedDirector, track);
if (gameObjectBinding == null)
return propertyName;
if (!string.IsNullOrEmpty(path))
{
var transform = gameObjectBinding.transform.Find(path);
if (transform == null)
return propertyName;
gameObjectBinding = transform.gameObject;
}
return AnimatedPropertyUtility.RemapMaterialName(gameObjectBinding, propertyName);
}
}
class ClipParametersCurveDataSource : CurveDataSource
{
static readonly string k_GroupingName = L10n.Tr("Clip Properties");
readonly TimelineClipGUI m_ClipGUI;
readonly CurvesProxy m_CurvesProxy;
private int m_ClipDirtyVersion;
public ClipParametersCurveDataSource(TimelineClipGUI clipGUI) : base(clipGUI.parent)
{
m_ClipGUI = clipGUI;
m_CurvesProxy = new CurvesProxy(clipGUI.clip);
}
public override AnimationClip animationClip
{
get { return m_CurvesProxy.curves; }
}
public override UInt64 GetClipVersion()
{
return sourceAnimationClip.ClipVersion();
}
public override CurveChangeType UpdateExternalChanges(ref ulong curveVersion)
{
if (m_ClipGUI == null || m_ClipGUI.clip == null)
return CurveChangeType.None;
var changeType = sourceAnimationClip.GetChangeType(ref curveVersion);
if (changeType != CurveChangeType.None)
{
m_CurvesProxy.ApplyExternalChangesToProxy();
}
else if (m_ClipDirtyVersion != m_ClipGUI.clip.DirtyIndex)
{
m_CurvesProxy.UpdateProxyCurves();
if (changeType == CurveChangeType.None)
changeType = CurveChangeType.CurveModified;
}
m_ClipDirtyVersion = m_ClipGUI.clip.DirtyIndex;
return changeType;
}
public override float start
{
get { return (float)m_ClipGUI.clip.FromLocalTimeUnbound(0.0); }
}
public override float timeScale
{
get { return (float)m_ClipGUI.clip.timeScale; }
}
public override string groupingName
{
get { return k_GroupingName; }
}
public override void RemoveCurves(IEnumerable<EditorCurveBinding> bindings)
{
m_CurvesProxy.RemoveCurves(bindings);
}
public override void ApplyCurveChanges(IEnumerable<CurveWrapper> updatedCurves)
{
m_CurvesProxy.UpdateCurves(updatedCurves);
}
protected override void ConfigureCurveWrapper(CurveWrapper wrapper)
{
m_CurvesProxy.ConfigureCurveWrapper(wrapper);
}
protected override int CreateHashCode()
{
return base.CreateHashCode().CombineHash(m_ClipGUI.clip.GetHashCode());
}
private AnimationClip sourceAnimationClip
{
get
{
if (m_ClipGUI == null || m_ClipGUI.clip == null || m_ClipGUI.clip.curves == null)
return null;
return m_ClipGUI.clip.curves;
}
}
}
class InfiniteClipCurveDataSource : CurveDataSource
{
static readonly string k_GroupingName = L10n.Tr("Animated Values");
readonly AnimationTrack m_AnimationTrack;
public InfiniteClipCurveDataSource(IRowGUI trackGui) : base(trackGui)
{
m_AnimationTrack = trackGui.asset as AnimationTrack;
}
public override AnimationClip animationClip
{
get { return m_AnimationTrack.infiniteClip; }
}
public override float start
{
get { return 0.0f; }
}
public override float timeScale
{
get { return 1.0f; }
}
public override string groupingName
{
get { return k_GroupingName; }
}
public override string ModifyPropertyDisplayName(string path, string propertyName)
{
if (m_AnimationTrack == null || !AnimatedPropertyUtility.IsMaterialProperty(propertyName))
return propertyName;
var binding = m_AnimationTrack.GetBinding(TimelineEditor.inspectedDirector);
if (binding == null)
return propertyName;
var target = binding.transform;
if (!string.IsNullOrEmpty(path))
target = target.Find(path);
if (target == null)
return propertyName;
return AnimatedPropertyUtility.RemapMaterialName(target.gameObject, propertyName);
}
}
class TrackParametersCurveDataSource : CurveDataSource
{
static readonly string k_GroupingName = L10n.Tr("Track Properties");
readonly CurvesProxy m_CurvesProxy;
private int m_TrackDirtyVersion;
public TrackParametersCurveDataSource(IRowGUI trackGui) : base(trackGui)
{
m_CurvesProxy = new CurvesProxy(trackGui.asset);
}
public override AnimationClip animationClip
{
get { return m_CurvesProxy.curves; }
}
public override UInt64 GetClipVersion()
{
return sourceAnimationClip.ClipVersion();
}
public override CurveChangeType UpdateExternalChanges(ref ulong curveVersion)
{
if (m_CurvesProxy.targetTrack == null)
return CurveChangeType.None;
var changeType = sourceAnimationClip.GetChangeType(ref curveVersion);
if (changeType != CurveChangeType.None)
{
m_CurvesProxy.ApplyExternalChangesToProxy();
}
// track property has changed externally, update the curve proxies
else if (m_TrackDirtyVersion != m_CurvesProxy.targetTrack.DirtyIndex)
{
if (changeType == CurveChangeType.None)
changeType = CurveChangeType.CurveModified;
m_CurvesProxy.UpdateProxyCurves();
}
m_TrackDirtyVersion = m_CurvesProxy.targetTrack.DirtyIndex;
return changeType;
}
public override float start
{
get { return 0.0f; }
}
public override float timeScale
{
get { return 1.0f; }
}
public override string groupingName
{
get { return k_GroupingName; }
}
public override void RemoveCurves(IEnumerable<EditorCurveBinding> bindings)
{
m_CurvesProxy.RemoveCurves(bindings);
}
public override void ApplyCurveChanges(IEnumerable<CurveWrapper> updatedCurves)
{
m_CurvesProxy.UpdateCurves(updatedCurves);
}
protected override void ConfigureCurveWrapper(CurveWrapper wrapper)
{
m_CurvesProxy.ConfigureCurveWrapper(wrapper);
}
private AnimationClip sourceAnimationClip
{
get
{
if (m_CurvesProxy.targetTrack == null || m_CurvesProxy.targetTrack.curves == null)
return null;
return m_CurvesProxy.targetTrack.curves;
}
}
}
}

View File

@@ -0,0 +1,39 @@
using System.Linq;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
namespace UnityEditor.Timeline
{
class CurveTreeViewNode : TreeViewItem
{
public bool forceGroup { get; }
public System.Type iconType { get; }
public GUIContent iconOverlay { get; }
EditorCurveBinding[] m_Bindings;
public EditorCurveBinding[] bindings
{
get { return m_Bindings; }
}
public CurveTreeViewNode(int id, TreeViewItem parent, string displayName, EditorCurveBinding[] bindings, bool _forceGroup = false)
: base(id, parent != null ? parent.depth + 1 : -1, parent, displayName)
{
m_Bindings = bindings;
forceGroup = _forceGroup;
// capture the preview icon type. If all subbindings are the same type, use that. Otherwise use null as a default
iconType = null;
if (parent != null && parent.depth >= 0 && bindings != null && bindings.Length > 0 && bindings.All(b => b.type == bindings[0].type))
{
iconType = bindings[0].type;
// for components put the component type in a tooltip
if (iconType != null && typeof(Component).IsAssignableFrom(iconType))
iconOverlay = new GUIContent(string.Empty, ObjectNames.NicifyVariableName(iconType.Name));
}
}
}
}

View File

@@ -0,0 +1,358 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.Timeline;
using UnityObject = UnityEngine.Object;
namespace UnityEditor.Timeline
{
class CurvesProxy : ICurvesOwner
{
public AnimationClip curves
{
get { return proxyCurves != null ? proxyCurves : m_OriginalOwner.curves; }
}
public bool hasCurves
{
get { return m_IsAnimatable || m_OriginalOwner.hasCurves; }
}
public double duration
{
get { return m_OriginalOwner.duration; }
}
public string defaultCurvesName
{
get { return m_OriginalOwner.defaultCurvesName; }
}
public UnityObject asset
{
get { return m_OriginalOwner.asset; }
}
public UnityObject assetOwner
{
get { return m_OriginalOwner.assetOwner; }
}
public TrackAsset targetTrack
{
get { return m_OriginalOwner.targetTrack; }
}
readonly ICurvesOwner m_OriginalOwner;
readonly bool m_IsAnimatable;
readonly Dictionary<EditorCurveBinding, SerializedProperty> m_PropertiesMap = new Dictionary<EditorCurveBinding, SerializedProperty>();
int m_ProxyIsRebuilding = 0;
AnimationClip m_ProxyCurves;
AnimationClip proxyCurves
{
get
{
if (!m_IsAnimatable) return null;
if (m_ProxyCurves == null)
RebuildProxyCurves();
return m_ProxyCurves;
}
}
public CurvesProxy([NotNull] ICurvesOwner originalOwner)
{
m_OriginalOwner = originalOwner;
m_IsAnimatable = originalOwner.HasAnyAnimatableParameters();
RebuildProxyCurves();
}
public void CreateCurves(string curvesClipName)
{
m_OriginalOwner.CreateCurves(curvesClipName);
}
public void ConfigureCurveWrapper(CurveWrapper wrapper)
{
var color = CurveUtility.GetPropertyColor(wrapper.binding.propertyName);
wrapper.color = color;
float h, s, v;
Color.RGBToHSV(color, out h, out s, out v);
wrapper.wrapColorMultiplier = Color.HSVToRGB(h, s * 0.33f, v * 1.15f);
var curve = AnimationUtility.GetEditorCurve(proxyCurves, wrapper.binding);
wrapper.renderer = new NormalCurveRenderer(curve);
// Use curve length instead of animation clip length
wrapper.renderer.SetCustomRange(0.0f, curve.keys.Last().time);
}
public void RebuildCurves()
{
RebuildProxyCurves();
}
public void RemoveCurves(IEnumerable<EditorCurveBinding> bindings)
{
if (m_ProxyIsRebuilding > 0 || !m_OriginalOwner.hasCurves)
return;
Undo.RegisterCompleteObjectUndo(m_OriginalOwner.curves, L10n.Tr("Remove Clip Curve"));
foreach (var binding in bindings)
AnimationUtility.SetEditorCurve(m_OriginalOwner.curves, binding, null);
m_OriginalOwner.SanitizeCurvesData();
RebuildProxyCurves();
}
public void UpdateCurves(IEnumerable<CurveWrapper> updatedCurves)
{
if (m_ProxyIsRebuilding > 0)
return;
Undo.RegisterCompleteObjectUndo(m_OriginalOwner.asset, L10n.Tr("Edit Clip Curve"));
if (m_OriginalOwner.curves != null)
Undo.RegisterCompleteObjectUndo(m_OriginalOwner.curves, L10n.Tr("Edit Clip Curve"));
var requireRebuild = false;
foreach (var curve in updatedCurves)
{
requireRebuild |= curve.curve.length == 0;
UpdateCurve(curve.binding, curve.curve);
}
if (requireRebuild)
m_OriginalOwner.SanitizeCurvesData();
AnimatedParameterUtility.UpdateSerializedPlayableAsset(m_OriginalOwner.asset);
}
public void ApplyExternalChangesToProxy()
{
using (new RebuildGuard(this))
{
if (m_OriginalOwner.curves == null)
return;
var curveInfo = AnimationClipCurveCache.Instance.GetCurveInfo(m_OriginalOwner.curves);
for (int i = 0; i < curveInfo.bindings.Length; i++)
{
if (curveInfo.curves[i] != null && curveInfo.curves.Length != 0)
{
if (m_PropertiesMap.TryGetValue(curveInfo.bindings[i], out var prop) && AnimatedParameterUtility.IsParameterAnimatable(prop))
AnimationUtility.SetEditorCurve(m_ProxyCurves, curveInfo.bindings[i], curveInfo.curves[i]);
}
}
}
}
void UpdateCurve(EditorCurveBinding binding, AnimationCurve curve)
{
ApplyConstraints(binding, curve);
if (curve.length == 0)
{
HandleAllKeysDeleted(binding);
return;
}
// there is no curve in the animation clip, this is a proxy curve
if (IsConstantCurve(binding, curve))
HandleConstantCurveValueChanged(binding, curve);
else
HandleCurveUpdated(binding, curve);
}
bool IsConstantCurve(EditorCurveBinding binding, AnimationCurve curve)
{
if (curve.length != 1)
return false;
return m_OriginalOwner.curves == null || AnimationUtility.GetEditorCurve(m_OriginalOwner.curves, binding) == null;
}
void ApplyConstraints(EditorCurveBinding binding, AnimationCurve curve)
{
if (curve.length == 0)
return;
var curveUpdated = false;
var property = m_PropertiesMap[binding];
if (property.propertyType == SerializedPropertyType.Boolean)
{
TimelineAnimationUtilities.ConstrainCurveToBooleanValues(curve);
curveUpdated = true;
}
else
{
var range = AnimatedParameterUtility.GetAttributeForProperty<RangeAttribute>(property);
if (range != null)
{
TimelineAnimationUtilities.ConstrainCurveToRange(curve, range.min, range.max);
curveUpdated = true;
}
}
if (!curveUpdated)
return;
using (new RebuildGuard(this))
{
AnimationUtility.SetEditorCurve(m_ProxyCurves, binding, curve);
}
}
void HandleCurveUpdated(EditorCurveBinding binding, AnimationCurve updatedCurve)
{
if (!m_OriginalOwner.hasCurves)
m_OriginalOwner.CreateCurves(null);
AnimationUtility.SetEditorCurve(m_OriginalOwner.curves, binding, updatedCurve);
AnimationUtility.SetEditorCurve(m_ProxyCurves, binding, updatedCurve);
}
void HandleConstantCurveValueChanged(EditorCurveBinding binding, AnimationCurve updatedCurve)
{
var prop = m_PropertiesMap[binding];
if (prop == null)
return;
Undo.RegisterCompleteObjectUndo(prop.serializedObject.targetObject, L10n.Tr("Edit Clip Curve"));
prop.serializedObject.UpdateIfRequiredOrScript();
CurveEditUtility.SetFromKeyValue(prop, updatedCurve.keys[0].value);
prop.serializedObject.ApplyModifiedProperties();
AnimationUtility.SetEditorCurve(m_ProxyCurves, binding, updatedCurve);
}
void HandleAllKeysDeleted(EditorCurveBinding binding)
{
if (m_OriginalOwner.hasCurves)
{
// Remove curve from original asset
AnimationUtility.SetEditorCurve(m_OriginalOwner.curves, binding, null);
SetProxyCurve(m_PropertiesMap[binding], binding);
}
}
void RebuildProxyCurves()
{
if (!m_IsAnimatable)
return;
using (new RebuildGuard(this))
{
if (m_ProxyCurves == null)
{
m_ProxyCurves = new AnimationClip
{
legacy = true,
name = "Constant Curves",
hideFlags = HideFlags.HideAndDontSave,
frameRate = m_OriginalOwner.targetTrack.timelineAsset == null
? TimelineAsset.EditorSettings.kDefaultFps
: m_OriginalOwner.targetTrack.timelineAsset.editorSettings.fps
};
}
else
{
m_ProxyCurves.ClearCurves();
}
m_OriginalOwner.SanitizeCurvesData();
AnimatedParameterUtility.UpdateSerializedPlayableAsset(m_OriginalOwner.asset);
var parameters = m_OriginalOwner.GetAllAnimatableParameters().ToArray();
foreach (var param in parameters)
CreateProxyCurve(param, m_ProxyCurves, m_OriginalOwner.asset, param.propertyPath);
AnimationClipCurveCache.Instance.GetCurveInfo(m_ProxyCurves).dirty = true;
}
}
// updates the just the proxied values. This can be called when the asset changes, so the proxy values are properly updated
public void UpdateProxyCurves()
{
if (!m_IsAnimatable || m_ProxyCurves == null || m_ProxyCurves.empty)
return;
AnimatedParameterUtility.UpdateSerializedPlayableAsset(m_OriginalOwner.asset);
var parameters = m_OriginalOwner.GetAllAnimatableParameters().ToArray();
using (new RebuildGuard(this))
{
if (m_OriginalOwner.hasCurves)
{
var bindingInfo = AnimationClipCurveCache.Instance.GetCurveInfo(m_OriginalOwner.curves);
foreach (var param in parameters)
{
var binding = AnimatedParameterUtility.GetCurveBinding(m_OriginalOwner.asset, param.propertyPath);
if (!bindingInfo.bindings.Contains(binding, AnimationPreviewUtilities.EditorCurveBindingComparer.Instance))
SetProxyCurve(param, AnimatedParameterUtility.GetCurveBinding(m_OriginalOwner.asset, param.propertyPath));
}
}
else
{
foreach (var param in parameters)
SetProxyCurve(param, AnimatedParameterUtility.GetCurveBinding(m_OriginalOwner.asset, param.propertyPath));
}
}
AnimationClipCurveCache.Instance.GetCurveInfo(m_ProxyCurves).dirty = true;
}
void CreateProxyCurve(SerializedProperty prop, AnimationClip clip, UnityObject owner, string propertyName)
{
var binding = AnimatedParameterUtility.GetCurveBinding(owner, propertyName);
var originalCurve = m_OriginalOwner.hasCurves
? AnimationUtility.GetEditorCurve(m_OriginalOwner.curves, binding)
: null;
if (originalCurve != null)
{
AnimationUtility.SetEditorCurve(clip, binding, originalCurve);
}
else
{
SetProxyCurve(prop, binding);
}
m_PropertiesMap[binding] = prop;
}
void SetProxyCurve(SerializedProperty prop, EditorCurveBinding binding)
{
var curve = new AnimationCurve();
CurveEditUtility.AddKeyFrameToCurve(
curve, 0.0f, m_ProxyCurves.frameRate, CurveEditUtility.GetKeyValue(prop),
prop.propertyType == SerializedPropertyType.Boolean);
AnimationUtility.SetEditorCurve(m_ProxyCurves, binding, curve);
}
struct RebuildGuard : IDisposable
{
CurvesProxy m_Owner;
AnimationUtility.OnCurveWasModified m_Callback;
public RebuildGuard(CurvesProxy owner)
{
m_Callback = AnimationUtility.onCurveWasModified;
AnimationUtility.onCurveWasModified = null;
m_Owner = owner;
m_Owner.m_ProxyIsRebuilding++;
}
public void Dispose()
{
AnimationUtility.onCurveWasModified = m_Callback;
m_Owner.m_ProxyIsRebuilding--;
m_Owner = null;
}
}
}
}

View File

@@ -0,0 +1,435 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEngineInternal;
using UnityEngine.Timeline;
using UnityEngine.Playables;
using Object = UnityEngine.Object;
namespace UnityEditor.Timeline
{
class TimelineAnimationUtilities
{
public enum OffsetEditMode
{
None = -1,
Translation = 0,
Rotation = 1
}
public static bool ValidateOffsetAvailabitity(PlayableDirector director, Animator animator)
{
if (director == null || animator == null)
return false;
return true;
}
public static TimelineClip GetPreviousClip(TimelineClip clip)
{
TimelineClip previousClip = null;
foreach (var c in clip.GetParentTrack().clips)
{
if (c.start < clip.start && (previousClip == null || c.start >= previousClip.start))
previousClip = c;
}
return previousClip;
}
public static TimelineClip GetNextClip(TimelineClip clip)
{
return clip.GetParentTrack().clips.Where(c => c.start > clip.start).OrderBy(c => c.start).FirstOrDefault();
}
public struct RigidTransform
{
public Vector3 position;
public Quaternion rotation;
public static RigidTransform Compose(Vector3 pos, Quaternion rot)
{
RigidTransform ret;
ret.position = pos;
ret.rotation = rot;
return ret;
}
public static RigidTransform Mul(RigidTransform a, RigidTransform b)
{
RigidTransform ret;
ret.rotation = a.rotation * b.rotation;
ret.position = a.position + a.rotation * b.position;
return ret;
}
public static RigidTransform Inverse(RigidTransform a)
{
RigidTransform ret;
ret.rotation = Quaternion.Inverse(a.rotation);
ret.position = ret.rotation * (-a.position);
return ret;
}
public static RigidTransform identity
{
get { return Compose(Vector3.zero, Quaternion.identity); }
}
}
private static Matrix4x4 GetTrackMatrix(Transform transform, AnimationTrack track)
{
Matrix4x4 trackMatrix = Matrix4x4.TRS(track.position, track.rotation, Vector3.one);
// in scene off mode, the track offsets are set to the preview position which is stored in the track
if (track.trackOffset == TrackOffset.ApplySceneOffsets)
{
trackMatrix = Matrix4x4.TRS(track.sceneOffsetPosition, Quaternion.Euler(track.sceneOffsetRotation), Vector3.one);
}
// put the parent transform on to the track matrix
if (transform.parent != null)
{
trackMatrix = transform.parent.localToWorldMatrix * trackMatrix;
}
return trackMatrix;
}
// Given a world space position and rotation, updates the clip offsets to match that
public static RigidTransform UpdateClipOffsets(AnimationPlayableAsset asset, AnimationTrack track, Transform transform, Vector3 globalPosition, Quaternion globalRotation)
{
Matrix4x4 worldToLocal = transform.worldToLocalMatrix;
Matrix4x4 clipMatrix = Matrix4x4.TRS(asset.position, asset.rotation, Vector3.one);
Matrix4x4 trackMatrix = GetTrackMatrix(transform, track);
// Use the transform to find the proper goal matrix with scale taken into account
var oldPos = transform.position;
var oldRot = transform.rotation;
transform.position = globalPosition;
transform.rotation = globalRotation;
Matrix4x4 goal = transform.localToWorldMatrix;
transform.position = oldPos;
transform.rotation = oldRot;
// compute the new clip matrix.
Matrix4x4 newClip = trackMatrix.inverse * goal * worldToLocal * trackMatrix * clipMatrix;
return RigidTransform.Compose(newClip.GetColumn(3), MathUtils.QuaternionFromMatrix(newClip));
}
public static RigidTransform GetTrackOffsets(AnimationTrack track, Transform transform)
{
Vector3 position = track.position;
Quaternion rotation = track.rotation;
if (transform != null && transform.parent != null)
{
position = transform.parent.TransformPoint(position);
rotation = transform.parent.rotation * rotation;
MathUtils.QuaternionNormalize(ref rotation);
}
return RigidTransform.Compose(position, rotation);
}
public static void UpdateTrackOffset(AnimationTrack track, Transform transform, RigidTransform offsets)
{
if (transform != null && transform.parent != null)
{
offsets.position = transform.parent.InverseTransformPoint(offsets.position);
offsets.rotation = Quaternion.Inverse(transform.parent.rotation) * offsets.rotation;
MathUtils.QuaternionNormalize(ref offsets.rotation);
}
track.position = offsets.position;
track.eulerAngles = AnimationUtility.GetClosestEuler(offsets.rotation, track.eulerAngles, RotationOrder.OrderZXY);
track.UpdateClipOffsets();
}
static MatchTargetFields GetMatchFields(TimelineClip clip)
{
var track = clip.GetParentTrack() as AnimationTrack;
if (track == null)
return MatchTargetFieldConstants.None;
var asset = clip.asset as AnimationPlayableAsset;
var fields = track.matchTargetFields;
if (asset != null && !asset.useTrackMatchFields)
fields = asset.matchTargetFields;
return fields;
}
static void WriteMatchFields(AnimationPlayableAsset asset, RigidTransform result, MatchTargetFields fields)
{
Vector3 position = asset.position;
position.x = fields.HasAny(MatchTargetFields.PositionX) ? result.position.x : position.x;
position.y = fields.HasAny(MatchTargetFields.PositionY) ? result.position.y : position.y;
position.z = fields.HasAny(MatchTargetFields.PositionZ) ? result.position.z : position.z;
asset.position = position;
// check first to avoid unnecessary conversion errors
if (fields.HasAny(MatchTargetFieldConstants.Rotation))
{
Vector3 eulers = asset.eulerAngles;
Vector3 resultEulers = result.rotation.eulerAngles;
eulers.x = fields.HasAny(MatchTargetFields.RotationX) ? resultEulers.x : eulers.x;
eulers.y = fields.HasAny(MatchTargetFields.RotationY) ? resultEulers.y : eulers.y;
eulers.z = fields.HasAny(MatchTargetFields.RotationZ) ? resultEulers.z : eulers.z;
asset.eulerAngles = AnimationUtility.GetClosestEuler(Quaternion.Euler(eulers), asset.eulerAngles, RotationOrder.OrderZXY);
}
}
public static void MatchPrevious(TimelineClip currentClip, Transform matchPoint, PlayableDirector director)
{
const double timeEpsilon = 0.00001;
MatchTargetFields matchFields = GetMatchFields(currentClip);
if (matchFields == MatchTargetFieldConstants.None || matchPoint == null)
return;
double cachedTime = director.time;
// finds previous clip
TimelineClip previousClip = GetPreviousClip(currentClip);
if (previousClip == null || currentClip == previousClip)
return;
// make sure the transform is properly updated before modifying the graph
director.Evaluate();
var parentTrack = currentClip.GetParentTrack() as AnimationTrack;
var blendIn = currentClip.blendInDuration;
currentClip.blendInDuration = 0;
var blendOut = previousClip.blendOutDuration;
previousClip.blendOutDuration = 0;
//evaluate previous without current
parentTrack.RemoveClip(currentClip);
director.RebuildGraph();
double previousEndTime = currentClip.start > previousClip.end ? previousClip.end : currentClip.start;
director.time = previousEndTime - timeEpsilon;
director.Evaluate(); // add port to evaluate only track
var targetPosition = matchPoint.position;
var targetRotation = matchPoint.rotation;
// evaluate current without previous
parentTrack.AddClip(currentClip);
parentTrack.RemoveClip(previousClip);
director.RebuildGraph();
director.time = currentClip.start + timeEpsilon;
director.Evaluate();
//////////////////////////////////////////////////////////////////////
//compute offsets
var animationPlayable = currentClip.asset as AnimationPlayableAsset;
var match = UpdateClipOffsets(animationPlayable, parentTrack, matchPoint, targetPosition, targetRotation);
WriteMatchFields(animationPlayable, match, matchFields);
//////////////////////////////////////////////////////////////////////
currentClip.blendInDuration = blendIn;
previousClip.blendOutDuration = blendOut;
parentTrack.AddClip(previousClip);
director.RebuildGraph();
director.time = cachedTime;
director.Evaluate();
}
public static void MatchNext(TimelineClip currentClip, Transform matchPoint, PlayableDirector director)
{
const double timeEpsilon = 0.00001;
MatchTargetFields matchFields = GetMatchFields(currentClip);
if (matchFields == MatchTargetFieldConstants.None || matchPoint == null)
return;
double cachedTime = director.time;
// finds next clip
TimelineClip nextClip = GetNextClip(currentClip);
if (nextClip == null || currentClip == nextClip)
return;
// make sure the transform is properly updated before modifying the graph
director.Evaluate();
var parentTrack = currentClip.GetParentTrack() as AnimationTrack;
var blendOut = currentClip.blendOutDuration;
var blendIn = nextClip.blendInDuration;
currentClip.blendOutDuration = 0;
nextClip.blendInDuration = 0;
//evaluate previous without current
parentTrack.RemoveClip(currentClip);
director.RebuildGraph();
director.time = nextClip.start + timeEpsilon;
director.Evaluate(); // add port to evaluate only track
var targetPosition = matchPoint.position;
var targetRotation = matchPoint.rotation;
// evaluate current without next
parentTrack.AddClip(currentClip);
parentTrack.RemoveClip(nextClip);
director.RebuildGraph();
director.time = Math.Min(nextClip.start, currentClip.end - timeEpsilon);
director.Evaluate();
//////////////////////////////////////////////////////////////////////
//compute offsets
var animationPlayable = currentClip.asset as AnimationPlayableAsset;
var match = UpdateClipOffsets(animationPlayable, parentTrack, matchPoint, targetPosition, targetRotation);
WriteMatchFields(animationPlayable, match, matchFields);
//////////////////////////////////////////////////////////////////////
currentClip.blendOutDuration = blendOut;
nextClip.blendInDuration = blendIn;
parentTrack.AddClip(nextClip);
director.RebuildGraph();
director.time = cachedTime;
director.Evaluate();
}
public static TimelineWindowTimeControl CreateTimeController(TimelineClip clip)
{
var animationWindow = EditorWindow.GetWindow<AnimationWindow>();
var timeController = ScriptableObject.CreateInstance<TimelineWindowTimeControl>();
timeController.Init(animationWindow.state, clip);
return timeController;
}
public static TimelineWindowTimeControl CreateTimeController(TimelineWindowTimeControl.ClipData clipData)
{
var animationWindow = EditorWindow.GetWindow<AnimationWindow>();
var timeController = ScriptableObject.CreateInstance<TimelineWindowTimeControl>();
timeController.Init(animationWindow.state, clipData);
return timeController;
}
public static void EditAnimationClipWithTimeController(AnimationClip animationClip, TimelineWindowTimeControl timeController, Object sourceObject)
{
var animationWindow = EditorWindow.GetWindow<AnimationWindow>();
animationWindow.EditSequencerClip(animationClip, sourceObject, timeController);
}
public static void UnlinkAnimationWindowFromTracks(IEnumerable<TrackAsset> tracks)
{
var clips = new List<AnimationClip>();
foreach (var track in tracks)
{
var animationTrack = track as AnimationTrack;
if (animationTrack != null && animationTrack.infiniteClip != null)
clips.Add(animationTrack.infiniteClip);
GetAnimationClips(track.GetClips(), clips);
}
UnlinkAnimationWindowFromAnimationClips(clips);
}
public static void UnlinkAnimationWindowFromClips(IEnumerable<TimelineClip> timelineClips)
{
var clips = new List<AnimationClip>();
GetAnimationClips(timelineClips, clips);
UnlinkAnimationWindowFromAnimationClips(clips);
}
public static void UnlinkAnimationWindowFromAnimationClips(ICollection<AnimationClip> clips)
{
if (clips.Count == 0)
return;
UnityEngine.Object[] windows = Resources.FindObjectsOfTypeAll(typeof(AnimationWindow));
foreach (var animWindow in windows.OfType<AnimationWindow>())
{
if (animWindow != null && animWindow.state != null && animWindow.state.linkedWithSequencer && clips.Contains(animWindow.state.activeAnimationClip))
animWindow.UnlinkSequencer();
}
}
public static void UnlinkAnimationWindow()
{
UnityEngine.Object[] windows = Resources.FindObjectsOfTypeAll(typeof(AnimationWindow));
foreach (var animWindow in windows.OfType<AnimationWindow>())
{
if (animWindow != null && animWindow.state != null && animWindow.state.linkedWithSequencer)
animWindow.UnlinkSequencer();
}
}
private static void GetAnimationClips(IEnumerable<TimelineClip> timelineClips, List<AnimationClip> clips)
{
foreach (var timelineClip in timelineClips)
{
if (timelineClip.curves != null)
clips.Add(timelineClip.curves);
AnimationPlayableAsset apa = timelineClip.asset as AnimationPlayableAsset;
if (apa != null && apa.clip != null)
clips.Add(apa.clip);
}
}
public static int GetAnimationWindowCurrentFrame()
{
var animationWindow = EditorWindow.GetWindow<AnimationWindow>();
if (animationWindow)
return animationWindow.state.currentFrame;
return -1;
}
public static void SetAnimationWindowCurrentFrame(int frame)
{
var animationWindow = EditorWindow.GetWindow<AnimationWindow>();
if (animationWindow)
animationWindow.state.currentFrame = frame;
}
public static void ConstrainCurveToBooleanValues(AnimationCurve curve)
{
// Clamp the values first
var keys = curve.keys;
for (var i = 0; i < keys.Length; i++)
{
var key = keys[i];
key.value = key.value < 0.5f ? 0.0f : 1.0f;
keys[i] = key;
}
curve.keys = keys;
// Update the tangents once all the values are clamped
for (var i = 0; i < curve.length; i++)
{
AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.Constant);
AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Constant);
}
}
public static void ConstrainCurveToRange(AnimationCurve curve, float minValue, float maxValue)
{
var keys = curve.keys;
for (var i = 0; i < keys.Length; i++)
{
var key = keys[i];
key.value = Mathf.Clamp(key.value, minValue, maxValue);
keys[i] = key;
}
curve.keys = keys;
}
public static bool IsAnimationClip(TimelineClip clip)
{
return clip != null && (clip.asset as AnimationPlayableAsset) != null;
}
}
}

View File

@@ -0,0 +1,26 @@
using System;
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Define the activeness of an action depending on its timeline mode.
/// </summary>
/// <seealso cref="TimelineModes"/>
[AttributeUsage(AttributeTargets.Class)]
public class ActiveInModeAttribute : Attribute
{
/// <summary>
/// Modes that will be used for activeness of an action.
/// </summary>
public TimelineModes modes { get; }
/// <summary>
/// Defines in which mode the action will be active.
/// </summary>
/// <param name="timelineModes">Modes that will define activeness of the action.</param>
public ActiveInModeAttribute(TimelineModes timelineModes)
{
modes = timelineModes;
}
}
}

View File

@@ -0,0 +1,51 @@
using System;
using UnityEngine;
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Use this attribute to add a menu item to a context menu.
/// Used to indicate path and priority that are auto added to the menu
/// (examples can be found on <see href="https://docs.unity3d.com/ScriptReference/MenuItem.html"/>).
/// </summary>
/// <example>
/// <code source="../../DocCodeExamples/TimelineAttributesExamples.cs" region="declare-menuEntryAttribute" title="menuEntryAttr"/>
/// </example>
/// <remarks>
/// Unlike Menu item, MenuEntryAttribute doesn't handle shortcuts in the menu name. See <see cref="TimelineShortcutAttribute"/>.
/// </remarks>
[AttributeUsage(AttributeTargets.Class)]
public class MenuEntryAttribute : Attribute
{
internal readonly int priority;
internal readonly string name;
internal readonly string subMenuPath;
/// <summary>
/// Constructor for Menu Entry Attribute to define information about the menu item for an action.
/// </summary>
/// <param name="path">Path to the menu. If there is a "/" in the path, it will create one (or multiple) submenu items.</param>
/// <param name="priority">Priority to decide where the menu will be positioned in the menu.
/// The lower the priority, the higher the menu item will be in the context menu.
/// </param>
/// <seealso cref="MenuPriority"/>
public MenuEntryAttribute(string path = default, int priority = MenuPriority.defaultPriority)
{
path = path ?? string.Empty;
path = L10n.Tr(path);
this.priority = priority;
var index = path.LastIndexOf('/');
if (index >= 0)
{
name = (index == path.Length - 1) ? string.Empty : path.Substring(index + 1);
subMenuPath = path.Substring(0, index + 1);
}
else
{
name = path;
subMenuPath = string.Empty;
}
}
}
}

View File

@@ -0,0 +1,71 @@
using System;
using System.Linq;
using UnityEditor.ShortcutManagement;
using UnityEngine;
namespace UnityEditor.Timeline
{
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
class ShortcutAttribute : Attribute
{
readonly string m_Identifier;
readonly string m_EventCommandName;
readonly string m_MenuShortcut;
public ShortcutAttribute(string identifier)
{
m_Identifier = identifier;
m_EventCommandName = identifier;
}
public ShortcutAttribute(string identifier, string commandName)
{
m_Identifier = identifier;
m_EventCommandName = commandName;
}
public ShortcutAttribute(KeyCode key, ShortcutModifiers modifiers = ShortcutModifiers.None)
{
m_MenuShortcut = new KeyCombination(key, modifiers).ToMenuShortcutString();
}
public string GetMenuShortcut()
{
if (m_MenuShortcut != null)
return m_MenuShortcut;
//find the mapped shortcut in the shortcut manager
var shortcut = ShortcutIntegration.instance.directory.FindShortcutEntry(m_Identifier);
if (shortcut != null && shortcut.combinations.Any())
{
return KeyCombination.SequenceToMenuString(shortcut.combinations);
}
return string.Empty;
}
public bool MatchesEvent(Event evt)
{
if (evt.type != EventType.ExecuteCommand)
return false;
return evt.commandName == m_EventCommandName;
}
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
class ShortcutPlatformOverrideAttribute : ShortcutAttribute
{
RuntimePlatform platform { get; }
public ShortcutPlatformOverrideAttribute(RuntimePlatform platform, KeyCode key, ShortcutModifiers modifiers = ShortcutModifiers.None)
: base(key, modifiers)
{
this.platform = platform;
}
public bool MatchesCurrentPlatform()
{
return Application.platform == platform;
}
}
}

View File

@@ -0,0 +1,24 @@
using UnityEditor.ShortcutManagement;
using UnityEngine;
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Use this attribute to make an action work with the shortcut system.
/// </summary>
/// <example>
/// TimelineShortcutAttribute needs to be added to a static method.
/// <code source="../../DocCodeExamples/TimelineAttributesExamples.cs" region="declare-timelineShortcutAttr" title="TimelineShortcutAttr"/>
/// </example>
public class TimelineShortcutAttribute : ShortcutManagement.ShortcutAttribute
{
/// <summary>
/// TimelineShortcutAttribute Constructor
/// </summary>
/// <param name="id">Id to register the shortcut. It will automatically be prefix by 'Timeline/' in order to be in the 'Timeline' section of the shortcut manager.</param>
/// <param name="defaultKeyCode">Optional key code for default binding.</param>
/// <param name="defaultShortcutModifiers">Optional shortcut modifiers for default binding.</param>
public TimelineShortcutAttribute(string id, KeyCode defaultKeyCode, ShortcutModifiers defaultShortcutModifiers = ShortcutModifiers.None)
: base("Timeline/" + id, typeof(TimelineWindow), defaultKeyCode, defaultShortcutModifiers) {}
}
}

View File

@@ -0,0 +1,84 @@
using System;
using System.Globalization;
using System.Linq;
using System.Text;
using JetBrains.Annotations;
using UnityEditor;
using UnityEditor.Timeline;
#if !UNITY_2020_2_OR_NEWER
using L10n = UnityEditor.Timeline.L10n;
#endif
namespace UnityEngine.Timeline
{
[CustomPropertyDrawer(typeof(AudioClipProperties))]
class AudioClipPropertiesDrawer : PropertyDrawer
{
[UsedImplicitly] // Also used by tests
internal static class Styles
{
public const string VolumeControl = "AudioClipPropertiesDrawer.volume";
const string k_Indent = " ";
public const string valuesFormatter = "0.###";
public static string mixedPropertiesInfo = L10n.Tr("The final {3} is {0}\n") +
L10n.Tr("Calculated from:\n") +
k_Indent + L10n.Tr("Clip: {1}\n") +
k_Indent + L10n.Tr("Track: {2}");
public static string audioSourceContribution = k_Indent + L10n.Tr("AudioSource: {0}");
}
static StringBuilder s_MixInfoBuilder = new StringBuilder();
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
var volumeProp = property.FindPropertyRelative("volume");
GUI.SetNextControlName(Styles.VolumeControl);
EditorGUI.Slider(position, volumeProp, 0.0f, 1.0f, AudioSourceInspector.Styles.volumeLabel);
if (TimelineEditor.inspectedDirector == null)
// Nothing more to do in asset mode
return;
var clip = SelectionManager.SelectedClips().FirstOrDefault(c => c.asset == property.serializedObject.targetObject);
if (clip == null || clip.GetParentTrack() == null)
return;
var clipVolume = volumeProp.floatValue;
var trackVolume = new SerializedObject(clip.GetParentTrack()).FindProperty("m_TrackProperties.volume").floatValue;
var binding = TimelineEditor.inspectedDirector.GetGenericBinding(clip.GetParentTrack()) as AudioSource;
if (Math.Abs(clipVolume) < float.Epsilon &&
Math.Abs(trackVolume) < float.Epsilon &&
(binding == null || Math.Abs(binding.volume) < float.Epsilon))
return;
if (Math.Abs(clipVolume - 1) < float.Epsilon &&
Math.Abs(trackVolume - 1) < float.Epsilon &&
(binding == null || Math.Abs(binding.volume - 1) < float.Epsilon))
return;
s_MixInfoBuilder.Length = 0;
var audioSourceVolume = binding == null ? 1.0f : binding.volume;
s_MixInfoBuilder.AppendFormat(
Styles.mixedPropertiesInfo,
(clipVolume * trackVolume * audioSourceVolume).ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
clipVolume.ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
trackVolume.ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
AudioSourceInspector.Styles.volumeLabel.text);
if (binding != null)
s_MixInfoBuilder.Append("\n")
.AppendFormat(Styles.audioSourceContribution,
audioSourceVolume.ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture));
EditorGUILayout.Space();
EditorGUILayout.HelpBox(new GUIContent(s_MixInfoBuilder.ToString()));
}
}
}

View File

@@ -0,0 +1,104 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
[CustomTimelineEditor(typeof(AudioPlayableAsset)), UsedImplicitly]
class AudioPlayableAssetEditor : ClipEditor
{
readonly string k_NoClipAssignedError = L10n.Tr("No audio clip assigned");
readonly Dictionary<TimelineClip, WaveformPreview> m_PersistentPreviews = new Dictionary<TimelineClip, WaveformPreview>();
ColorSpace m_ColorSpace = ColorSpace.Uninitialized;
public override ClipDrawOptions GetClipOptions(TimelineClip clip)
{
var clipOptions = base.GetClipOptions(clip);
var audioAsset = clip.asset as AudioPlayableAsset;
if (audioAsset != null && audioAsset.clip == null)
clipOptions.errorText = k_NoClipAssignedError;
return clipOptions;
}
public override void DrawBackground(TimelineClip clip, ClipBackgroundRegion region)
{
if (!TimelineWindow.instance.state.showAudioWaveform)
return;
var rect = region.position;
if (rect.width <= 0)
return;
var audioClip = clip.asset as AudioClip;
if (audioClip == null)
{
var audioPlayableAsset = clip.asset as AudioPlayableAsset;
if (audioPlayableAsset != null)
audioClip = audioPlayableAsset.clip;
}
if (audioClip == null)
return;
var quantizedRect = new Rect(Mathf.Ceil(rect.x), Mathf.Ceil(rect.y), Mathf.Ceil(rect.width), Mathf.Ceil(rect.height));
WaveformPreview preview = GetOrCreateWaveformPreview(clip, audioClip, quantizedRect, region.startTime, region.endTime);
if (Event.current.type == EventType.Repaint)
DrawWaveformPreview(preview, quantizedRect);
}
public WaveformPreview GetOrCreateWaveformPreview(TimelineClip clip, AudioClip audioClip, Rect rect, double startTime, double endTime)
{
if (QualitySettings.activeColorSpace != m_ColorSpace)
{
m_ColorSpace = QualitySettings.activeColorSpace;
m_PersistentPreviews.Clear();
}
bool previewExists = m_PersistentPreviews.TryGetValue(clip, out WaveformPreview preview);
bool audioClipHasChanged = preview != null && audioClip != preview.presentedObject;
if (!previewExists || audioClipHasChanged)
{
if (AssetDatabase.Contains(audioClip))
preview = CreateWaveformPreview(audioClip, rect);
m_PersistentPreviews[clip] = preview;
}
if (preview == null)
return null;
preview.looping = clip.SupportsLooping();
preview.SetTimeInfo(startTime, endTime - startTime);
preview.OptimizeForSize(rect.size);
return preview;
}
public static void DrawWaveformPreview(WaveformPreview preview, Rect rect)
{
if (preview != null)
{
preview.ApplyModifications();
preview.Render(rect);
}
}
static WaveformPreview CreateWaveformPreview(AudioClip audioClip, Rect quantizedRect)
{
WaveformPreview preview = WaveformPreviewFactory.Create((int)quantizedRect.width, audioClip);
Color waveColour = GammaCorrect(DirectorStyles.Instance.customSkin.colorAudioWaveform);
Color transparent = waveColour;
transparent.a = 0;
preview.backgroundColor = transparent;
preview.waveColor = waveColour;
preview.SetChannelMode(WaveformPreview.ChannelMode.MonoSum);
preview.updated += () => TimelineEditor.Refresh(RefreshReason.WindowNeedsRedraw);
return preview;
}
static Color GammaCorrect(Color color)
{
return (QualitySettings.activeColorSpace == ColorSpace.Linear) ? color.gamma : color;
}
}
}

View File

@@ -0,0 +1,27 @@
using UnityEditor;
using UnityEditor.Timeline;
using UnityEngine.Playables;
namespace UnityEngine.Timeline
{
[CustomEditor(typeof(AudioPlayableAsset))]
class AudioPlayableAssetInspector : BasicAssetInspector
{
public override void ApplyChanges()
{
// At this point, we are guaranteed that the Timeline window is focused on
// the correct asset and that a single clip is selected (see ClipInspector)
if (TimelineEditor.inspectedDirector == null)
// Do nothing if in asset mode
return;
var asset = (AudioPlayableAsset)target;
if (TimelineEditor.inspectedDirector.state == PlayState.Playing)
asset.LiveLink();
else
TimelineEditor.Refresh(RefreshReason.ContentsModified);
}
}
}

View File

@@ -0,0 +1,167 @@
using System;
using System.Globalization;
using System.Linq;
using System.Text;
using JetBrains.Annotations;
using UnityEditor;
using UnityEditor.Timeline;
using UnityEngine.Playables;
#if !UNITY_2020_2_OR_NEWER
using L10n = UnityEditor.Timeline.L10n;
#endif
namespace UnityEngine.Timeline
{
[CustomEditor(typeof(AudioTrack))]
[CanEditMultipleObjects]
class AudioTrackInspector : TrackAssetInspector
{
[UsedImplicitly] // Also used by tests
internal static class Styles
{
public const string VolumeControl = "AudioTrackInspector.volume";
public const string StereoPanControl = "AudioTrackInspector.stereoPan";
public const string SpatialBlendControl = "AudioTrackInspector.spatialBlend";
const string k_Indent = " ";
public const string valuesFormatter = "0.###";
public const string mixInfoSectionSeparator = "\n\n";
public static string mixedPropertiesInfo = L10n.Tr("The final {3} is {0}\n") +
L10n.Tr("Calculated from:\n") +
k_Indent + L10n.Tr("Track: {1}\n") +
k_Indent + L10n.Tr("AudioSource: {2}");
}
static StringBuilder s_MixInfoBuilder = new StringBuilder();
SerializedProperty m_VolumeProperty;
SerializedProperty m_StereoPanProperty;
SerializedProperty m_SpatialBlendProperty;
PlayableDirector m_Director;
public override void OnEnable()
{
base.OnEnable();
if (((AudioTrack)target).timelineAsset == TimelineEditor.inspectedAsset)
m_Director = TimelineEditor.inspectedDirector;
m_VolumeProperty = serializedObject.FindProperty("m_TrackProperties.volume");
m_StereoPanProperty = serializedObject.FindProperty("m_TrackProperties.stereoPan");
m_SpatialBlendProperty = serializedObject.FindProperty("m_TrackProperties.spatialBlend");
}
protected override void DrawTrackProperties()
{
// Volume
GUI.SetNextControlName(Styles.VolumeControl);
EditorGUILayout.Slider(m_VolumeProperty, 0.0f, 1.0f, AudioSourceInspector.Styles.volumeLabel);
EditorGUILayout.Space();
// Stereo Pan
GUI.SetNextControlName(Styles.StereoPanControl);
EditorGUIUtility.sliderLabels.SetLabels(AudioSourceInspector.Styles.panLeftLabel, AudioSourceInspector.Styles.panRightLabel);
EditorGUILayout.Slider(m_StereoPanProperty, -1.0f, 1.0f, AudioSourceInspector.Styles.panStereoLabel);
EditorGUIUtility.sliderLabels.SetLabels(null, null);
EditorGUILayout.Space();
// Spatial Blend
using (new EditorGUI.DisabledScope(ShouldDisableSpatialBlend()))
{
GUI.SetNextControlName(Styles.SpatialBlendControl);
EditorGUIUtility.sliderLabels.SetLabels(AudioSourceInspector.Styles.spatialLeftLabel, AudioSourceInspector.Styles.spatialRightLabel);
EditorGUILayout.Slider(m_SpatialBlendProperty, 0.0f, 1.0f, AudioSourceInspector.Styles.spatialBlendLabel);
EditorGUIUtility.sliderLabels.SetLabels(null, null);
}
DrawMixInfoSection();
}
void DrawMixInfoSection()
{
if (m_Director == null || targets.Length > 1)
return;
var binding = m_Director.GetGenericBinding(target) as AudioSource;
if (binding == null)
return;
var audioSourceVolume = binding.volume;
var audioSourcePan = binding.panStereo;
var audioSourceBlend = binding.spatialBlend;
var trackVolume = m_VolumeProperty.floatValue;
var trackPan = m_StereoPanProperty.floatValue;
var trackBlend = m_SpatialBlendProperty.floatValue;
// Skip sections when result is obvious
var skipVolumeInfo = Math.Abs(audioSourceVolume) < float.Epsilon && Math.Abs(trackVolume) < float.Epsilon || // All muted
Math.Abs(audioSourceVolume - 1) < float.Epsilon && Math.Abs(trackVolume - 1) < float.Epsilon; // All max volume
var skipPanInfo = Math.Abs(audioSourcePan) < float.Epsilon && Math.Abs(trackPan) < float.Epsilon || // All centered
Math.Abs(audioSourcePan - 1) < float.Epsilon && Math.Abs(trackPan - 1) < float.Epsilon || // All right
Math.Abs(audioSourcePan - (-1.0f)) < float.Epsilon && Math.Abs(trackPan - (-1.0f)) < float.Epsilon; // All left
var skipBlendInfo = Math.Abs(audioSourceBlend) < float.Epsilon && Math.Abs(trackBlend) < float.Epsilon || // All 2D
Math.Abs(audioSourceBlend - 1) < float.Epsilon && Math.Abs(trackBlend - 1) < float.Epsilon; // All 3D
if (skipVolumeInfo && skipPanInfo && skipBlendInfo)
return;
s_MixInfoBuilder.Length = 0;
if (!skipVolumeInfo)
s_MixInfoBuilder.AppendFormat(
Styles.mixedPropertiesInfo,
(audioSourceVolume * trackVolume).ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
trackVolume.ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
audioSourceVolume.ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
AudioSourceInspector.Styles.volumeLabel.text);
if (!skipVolumeInfo && !skipPanInfo)
s_MixInfoBuilder.Append(Styles.mixInfoSectionSeparator);
if (!skipPanInfo)
s_MixInfoBuilder.AppendFormat(
Styles.mixedPropertiesInfo,
Mathf.Clamp(audioSourcePan + trackPan, -1.0f, 1.0f).ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
trackPan.ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
audioSourcePan.ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
AudioSourceInspector.Styles.panStereoLabel.text);
if ((!skipVolumeInfo || !skipPanInfo) && !skipBlendInfo)
s_MixInfoBuilder.Append(Styles.mixInfoSectionSeparator);
if (!skipBlendInfo)
s_MixInfoBuilder.AppendFormat(
Styles.mixedPropertiesInfo,
Mathf.Clamp01(audioSourceBlend + trackBlend).ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
trackBlend.ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
audioSourceBlend.ToString(Styles.valuesFormatter, CultureInfo.InvariantCulture),
AudioSourceInspector.Styles.spatialBlendLabel.text);
EditorGUILayout.Space();
EditorGUILayout.HelpBox(new GUIContent(s_MixInfoBuilder.ToString()));
}
protected override void ApplyChanges()
{
var track = (AudioTrack)target;
if (TimelineEditor.inspectedAsset != track.timelineAsset || TimelineEditor.inspectedDirector == null)
return;
if (TimelineEditor.inspectedDirector.state == PlayState.Playing)
track.LiveLink();
else
TimelineEditor.Refresh(RefreshReason.ContentsModified);
}
bool ShouldDisableSpatialBlend()
{
return m_Director == null ||
targets.Any(selectedTrack => m_Director.GetGenericBinding(selectedTrack) == null);
}
}
}

View File

@@ -0,0 +1,69 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Timeline;
using UnityEngine.Playables;
namespace UnityEditor.Timeline
{
[CustomTimelineEditor(typeof(ControlPlayableAsset))]
class ControlPlayableAssetEditor : ClipEditor
{
static readonly Texture2D[] s_ParticleSystemIcon = {AssetPreview.GetMiniTypeThumbnail(typeof(ParticleSystem))};
public override ClipDrawOptions GetClipOptions(TimelineClip clip)
{
var asset = (ControlPlayableAsset)clip.asset;
var options = base.GetClipOptions(clip);
if (asset.updateParticle && TimelineEditor.inspectedDirector != null && asset.controllingParticles)
options.icons = s_ParticleSystemIcon;
return options;
}
public override void OnCreate(TimelineClip clip, TrackAsset track, TimelineClip clonedFrom)
{
var asset = (ControlPlayableAsset)clip.asset;
GameObject sourceObject = null;
// go by sourceObject first, then by prefab
if (TimelineEditor.inspectedDirector != null)
sourceObject = asset.sourceGameObject.Resolve(TimelineEditor.inspectedDirector);
if (sourceObject == null && asset.prefabGameObject != null)
sourceObject = asset.prefabGameObject;
if (sourceObject)
{
var directors = asset.GetComponent<PlayableDirector>(sourceObject);
var particleSystems = asset.GetComponent<ParticleSystem>(sourceObject);
// update the duration and loop values (used for UI purposes) here
// so they are tied to the latest gameObject bound
asset.UpdateDurationAndLoopFlag(directors, particleSystems);
clip.displayName = sourceObject.name;
}
}
public override void GetSubTimelines(TimelineClip clip, PlayableDirector director, List<PlayableDirector> subTimelines)
{
var asset = (ControlPlayableAsset)clip.asset;
// If there is a prefab, it will override the source GameObject
if (!asset.updateDirector || asset.prefabGameObject != null || director == null)
return;
var go = asset.sourceGameObject.Resolve(director);
if (go == null)
return;
foreach (var subTimeline in asset.GetComponent<PlayableDirector>(go))
{
if (subTimeline == director || subTimeline == TimelineEditor.masterDirector || subTimeline == TimelineEditor.inspectedDirector)
continue;
if (subTimeline.playableAsset is TimelineAsset)
subTimelines.Add(subTimeline);
}
}
}
}

View File

@@ -0,0 +1,657 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditorInternal;
using UnityEngine;
using Object = UnityEngine.Object;
namespace UnityEditor.Timeline
{
// Utility class for editing animation clips from serialized properties
static class CurveEditUtility
{
public static bool IsRotationKey(EditorCurveBinding binding)
{
return binding.propertyName.Contains("localEulerAnglesRaw");
}
public static void AddKey(AnimationClip clip, EditorCurveBinding sourceBinding, SerializedProperty prop, double time)
{
if (sourceBinding.isPPtrCurve)
{
AddObjectKey(clip, sourceBinding, prop, time);
}
else if (IsRotationKey(sourceBinding))
{
AddRotationKey(clip, sourceBinding, prop, time);
}
else
{
AddFloatKey(clip, sourceBinding, prop, time);
}
}
static void AddObjectKey(AnimationClip clip, EditorCurveBinding sourceBinding, SerializedProperty prop, double time)
{
if (prop.propertyType != SerializedPropertyType.ObjectReference)
return;
ObjectReferenceKeyframe[] curve = null;
var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
var curveIndex = Array.IndexOf(info.objectBindings, sourceBinding);
if (curveIndex >= 0)
{
curve = info.objectCurves[curveIndex];
// where in the array does the evaluation land?
var evalIndex = EvaluateIndex(curve, (float)time);
if (KeyCompare(curve[evalIndex].time, (float)time, clip.frameRate) == 0)
{
curve[evalIndex].value = prop.objectReferenceValue;
}
// check the next key (always return the minimum value)
else if (evalIndex < curve.Length - 1 && KeyCompare(curve[evalIndex + 1].time, (float)time, clip.frameRate) == 0)
{
curve[evalIndex + 1].value = prop.objectReferenceValue;
}
// resize the array
else
{
if (time > curve[0].time)
evalIndex++;
var key = new ObjectReferenceKeyframe();
key.time = (float)time;
key.value = prop.objectReferenceValue;
ArrayUtility.Insert(ref curve, evalIndex, key);
}
}
else // curve doesn't exist, add it
{
curve = new ObjectReferenceKeyframe[1];
curve[0].time = (float)time;
curve[0].value = prop.objectReferenceValue;
}
AnimationUtility.SetObjectReferenceCurve(clip, sourceBinding, curve);
EditorUtility.SetDirty(clip);
}
static void AddRotationKey(AnimationClip clip, EditorCurveBinding sourceBind, SerializedProperty prop, double time)
{
if (prop.propertyType != SerializedPropertyType.Quaternion)
{
return;
}
var updateCurves = new List<AnimationCurve>();
var updateBindings = new List<EditorCurveBinding>();
var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
for (var i = 0; i < info.bindings.Length; i++)
{
if (sourceBind.type != info.bindings[i].type)
continue;
if (info.bindings[i].propertyName.Contains("localEuler"))
{
updateBindings.Add(info.bindings[i]);
updateCurves.Add(info.curves[i]);
}
}
// use this instead of serialized properties because the editor will attempt to maintain
// correct localeulers
var eulers = ((Transform)prop.serializedObject.targetObject).localEulerAngles;
if (updateBindings.Count == 0)
{
var propName = AnimationWindowUtility.GetPropertyGroupName(sourceBind.propertyName);
updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".x"));
updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".y"));
updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".z"));
var curveX = new AnimationCurve();
var curveY = new AnimationCurve();
var curveZ = new AnimationCurve();
AddKeyFrameToCurve(curveX, (float)time, clip.frameRate, eulers.x, false);
AddKeyFrameToCurve(curveY, (float)time, clip.frameRate, eulers.y, false);
AddKeyFrameToCurve(curveZ, (float)time, clip.frameRate, eulers.z, false);
updateCurves.Add(curveX);
updateCurves.Add(curveY);
updateCurves.Add(curveZ);
}
for (var i = 0; i < updateBindings.Count; i++)
{
var c = updateBindings[i].propertyName.Last();
var value = eulers.x;
if (c == 'y') value = eulers.y;
else if (c == 'z') value = eulers.z;
AddKeyFrameToCurve(updateCurves[i], (float)time, clip.frameRate, value, false);
}
UpdateEditorCurves(clip, updateBindings, updateCurves);
}
// Add a floating point curve key
static void AddFloatKey(AnimationClip clip, EditorCurveBinding sourceBind, SerializedProperty prop, double time)
{
var updateCurves = new List<AnimationCurve>();
var updateBindings = new List<EditorCurveBinding>();
var updated = false;
var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
for (var i = 0; i < info.bindings.Length; i++)
{
var binding = info.bindings[i];
if (binding.type != sourceBind.type)
continue;
SerializedProperty valProp = null;
var curve = info.curves[i];
// perfect match on property path, editting a float
if (prop.propertyPath.Equals(binding.propertyName))
{
valProp = prop;
}
// this is a child object
else if (binding.propertyName.Contains(prop.propertyPath))
{
valProp = prop.serializedObject.FindProperty(binding.propertyName);
}
if (valProp != null)
{
var value = GetKeyValue(valProp);
if (!float.IsNaN(value)) // Nan indicates an error retrieving the property value
{
updated = true;
AddKeyFrameToCurve(curve, (float)time, clip.frameRate, value, valProp.propertyType == SerializedPropertyType.Boolean);
updateCurves.Add(curve);
updateBindings.Add(binding);
}
}
}
// Curves don't exist, add them
if (!updated)
{
var propName = AnimationWindowUtility.GetPropertyGroupName(sourceBind.propertyName);
if (!prop.hasChildren)
{
var value = GetKeyValue(prop);
if (!float.IsNaN(value))
{
updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, sourceBind.propertyName));
var curve = new AnimationCurve();
AddKeyFrameToCurve(curve, (float)time, clip.frameRate, value, prop.propertyType == SerializedPropertyType.Boolean);
updateCurves.Add(curve);
}
}
else
{
// special case because subproperties on color aren't 'visible' so you can't iterate over them
if (prop.propertyType == SerializedPropertyType.Color)
{
updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".r"));
updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".g"));
updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".b"));
updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, propName + ".a"));
var c = prop.colorValue;
for (var i = 0; i < 4; i++)
{
var curve = new AnimationCurve();
AddKeyFrameToCurve(curve, (float)time, clip.frameRate, c[i], prop.propertyType == SerializedPropertyType.Boolean);
updateCurves.Add(curve);
}
}
else
{
prop = prop.Copy();
foreach (SerializedProperty cp in prop)
{
updateBindings.Add(EditorCurveBinding.FloatCurve(sourceBind.path, sourceBind.type, cp.propertyPath));
var curve = new AnimationCurve();
AddKeyFrameToCurve(curve, (float)time, clip.frameRate, GetKeyValue(cp), cp.propertyType == SerializedPropertyType.Boolean);
updateCurves.Add(curve);
}
}
}
}
UpdateEditorCurves(clip, updateBindings, updateCurves);
}
public static void RemoveKey(AnimationClip clip, EditorCurveBinding sourceBinding, SerializedProperty prop, double time)
{
if (sourceBinding.isPPtrCurve)
{
RemoveObjectKey(clip, sourceBinding, time);
}
else if (IsRotationKey(sourceBinding))
{
RemoveRotationKey(clip, sourceBinding, prop, time);
}
else
{
RemoveFloatKey(clip, sourceBinding, prop, time);
}
}
public static void RemoveObjectKey(AnimationClip clip, EditorCurveBinding sourceBinding, double time)
{
var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
var curveIndex = Array.IndexOf(info.objectBindings, sourceBinding);
if (curveIndex >= 0)
{
var curve = info.objectCurves[curveIndex];
var evalIndex = GetKeyframeAtTime(curve, (float)time, clip.frameRate);
if (evalIndex >= 0)
{
ArrayUtility.RemoveAt(ref curve, evalIndex);
AnimationUtility.SetObjectReferenceCurve(clip, sourceBinding, curve.Length == 0 ? null : curve);
EditorUtility.SetDirty(clip);
}
}
}
public static int GetObjectKeyCount(AnimationClip clip, EditorCurveBinding sourceBinding)
{
var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
var curveIndex = Array.IndexOf(info.objectBindings, sourceBinding);
if (curveIndex >= 0)
{
var curve = info.objectCurves[curveIndex];
return curve.Length;
}
return 0;
}
static void RemoveRotationKey(AnimationClip clip, EditorCurveBinding sourceBind, SerializedProperty prop, double time)
{
if (prop.propertyType != SerializedPropertyType.Quaternion)
{
return;
}
var updateCurves = new List<AnimationCurve>();
var updateBindings = new List<EditorCurveBinding>();
var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
for (var i = 0; i < info.bindings.Length; i++)
{
if (sourceBind.type != info.bindings[i].type)
continue;
if (info.bindings[i].propertyName.Contains("localEuler"))
{
updateBindings.Add(info.bindings[i]);
updateCurves.Add(info.curves[i]);
}
}
foreach (var c in updateCurves)
{
RemoveKeyFrameFromCurve(c, (float)time, clip.frameRate);
}
UpdateEditorCurves(clip, updateBindings, updateCurves);
}
// Removes the float keys from curves
static void RemoveFloatKey(AnimationClip clip, EditorCurveBinding sourceBind, SerializedProperty prop, double time)
{
var updateCurves = new List<AnimationCurve>();
var updateBindings = new List<EditorCurveBinding>();
var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
for (var i = 0; i < info.bindings.Length; i++)
{
var binding = info.bindings[i];
if (binding.type != sourceBind.type)
continue;
SerializedProperty valProp = null;
var curve = info.curves[i];
// perfect match on property path, editting a float
if (prop.propertyPath.Equals(binding.propertyName))
{
valProp = prop;
}
// this is a child object
else if (binding.propertyName.Contains(prop.propertyPath))
{
valProp = prop.serializedObject.FindProperty(binding.propertyName);
}
if (valProp != null)
{
RemoveKeyFrameFromCurve(curve, (float)time, clip.frameRate);
updateCurves.Add(curve);
updateBindings.Add(binding);
}
}
// update the curve. Do this last to not mess with the curve caches we are iterating over
UpdateEditorCurves(clip, updateBindings, updateCurves);
}
static void UpdateEditorCurve(AnimationClip clip, EditorCurveBinding binding, AnimationCurve curve)
{
if (curve.keys.Length == 0)
AnimationUtility.SetEditorCurve(clip, binding, null);
else
AnimationUtility.SetEditorCurve(clip, binding, curve);
}
static void UpdateEditorCurves(AnimationClip clip, List<EditorCurveBinding> bindings, List<AnimationCurve> curves)
{
if (curves.Count == 0)
return;
for (var i = 0; i < curves.Count; i++)
{
UpdateEditorCurve(clip, bindings[i], curves[i]);
}
EditorUtility.SetDirty(clip);
}
public static void RemoveCurves(AnimationClip clip, SerializedProperty prop)
{
if (clip == null || prop == null)
return;
var toRemove = new List<EditorCurveBinding>();
var info = AnimationClipCurveCache.Instance.GetCurveInfo(clip);
for (var i = 0; i < info.bindings.Length; i++)
{
var binding = info.bindings[i];
// check if we match directly, or with a child object
if (prop.propertyPath.Equals(binding.propertyName) || binding.propertyName.Contains(prop.propertyPath))
{
toRemove.Add(binding);
}
}
for (int i = 0; i < toRemove.Count; i++)
{
AnimationUtility.SetEditorCurve(clip, toRemove[i], null);
}
}
// adds a stepped key frame to the given curve
public static void AddKeyFrameToCurve(AnimationCurve curve, float time, float framerate, float value, bool stepped)
{
var key = new Keyframe();
bool add = true;
var keyIndex = GetKeyframeAtTime(curve, time, framerate);
if (keyIndex != -1)
{
add = false;
key = curve[keyIndex]; // retain the tangents and mode
curve.RemoveKey(keyIndex);
}
key.value = value;
key.time = GetKeyTime(time, framerate);
keyIndex = curve.AddKey(key);
if (stepped)
{
AnimationUtility.SetKeyBroken(curve, keyIndex, stepped);
AnimationUtility.SetKeyLeftTangentMode(curve, keyIndex, AnimationUtility.TangentMode.Constant);
AnimationUtility.SetKeyRightTangentMode(curve, keyIndex, AnimationUtility.TangentMode.Constant);
key.outTangent = Mathf.Infinity;
key.inTangent = Mathf.Infinity;
}
else if (add)
{
AnimationUtility.SetKeyLeftTangentMode(curve, keyIndex, AnimationUtility.TangentMode.ClampedAuto);
AnimationUtility.SetKeyRightTangentMode(curve, keyIndex, AnimationUtility.TangentMode.ClampedAuto);
}
if (keyIndex != -1 && !stepped)
{
AnimationUtility.UpdateTangentsFromModeSurrounding(curve, keyIndex);
AnimationUtility.SetKeyBroken(curve, keyIndex, false);
}
}
// Removes a keyframe at the given time from the animation curve
public static bool RemoveKeyFrameFromCurve(AnimationCurve curve, float time, float framerate)
{
var keyIndex = GetKeyframeAtTime(curve, time, framerate);
if (keyIndex == -1)
return false;
curve.RemoveKey(keyIndex);
return true;
}
// gets the value of the key
public static float GetKeyValue(SerializedProperty prop)
{
switch (prop.propertyType)
{
case SerializedPropertyType.Integer:
return prop.intValue;
case SerializedPropertyType.Boolean:
return prop.boolValue ? 1.0f : 0.0f;
case SerializedPropertyType.Float:
return prop.floatValue;
default:
Debug.LogError("Could not convert property type " + prop.propertyType.ToString() + " to float");
break;
}
return float.NaN;
}
public static void SetFromKeyValue(SerializedProperty prop, float keyValue)
{
switch (prop.propertyType)
{
case SerializedPropertyType.Float:
{
prop.floatValue = keyValue;
return;
}
case SerializedPropertyType.Integer:
{
prop.intValue = (int)keyValue;
return;
}
case SerializedPropertyType.Boolean:
{
prop.boolValue = Math.Abs(keyValue) > 0.001f;
return;
}
}
Debug.LogError("Could not convert float to property type " + prop.propertyType.ToString());
}
// gets the index of the key, -1 if not found
public static int GetKeyframeAtTime(AnimationCurve curve, float time, float frameRate)
{
var range = 0.5f / frameRate;
var keys = curve.keys;
for (var i = 0; i < keys.Length; i++)
{
var k = keys[i];
if (k.time >= time - range && k.time < time + range)
{
return i;
}
}
return -1;
}
public static int GetKeyframeAtTime(ObjectReferenceKeyframe[] curve, float time, float frameRate)
{
if (curve == null || curve.Length == 0)
return -1;
var range = 0.5f / frameRate;
for (var i = 0; i < curve.Length; i++)
{
var t = curve[i].time;
if (t >= time - range && t < time + range)
{
return i;
}
}
return -1;
}
public static float GetKeyTime(float time, float frameRate)
{
return Mathf.Round(time * frameRate) / frameRate;
}
public static int KeyCompare(float timeA, float timeB, float frameRate)
{
if (Mathf.Abs(timeA - timeB) <= 0.5f / frameRate)
return 0;
return timeA < timeB ? -1 : 1;
}
// Evaluates an object (bool curve)
public static Object Evaluate(ObjectReferenceKeyframe[] curve, float time)
{
return curve[EvaluateIndex(curve, time)].value;
}
// returns the index from evaluation
public static int EvaluateIndex(ObjectReferenceKeyframe[] curve, float time)
{
if (curve == null || curve.Length == 0)
throw new InvalidOperationException("Can not evaluate a PPtr curve with no entries");
// clamp conditions
if (time <= curve[0].time)
return 0;
if (time >= curve.Last().time)
return curve.Length - 1;
// binary search
var max = curve.Length - 1;
var min = 0;
while (max - min > 1)
{
var imid = (min + max) / 2;
if (Mathf.Approximately(curve[imid].time, time))
return imid;
if (curve[imid].time < time)
min = imid;
else if (curve[imid].time > time)
max = imid;
}
return min;
}
// Shifts the animation clip so the time start at 0
public static void ShiftBySeconds(this AnimationClip clip, float time)
{
var floatBindings = AnimationUtility.GetCurveBindings(clip);
var objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(clip);
// update the float curves
foreach (var bind in floatBindings)
{
var curve = AnimationUtility.GetEditorCurve(clip, bind);
var keys = curve.keys;
for (var i = 0; i < keys.Length; i++)
keys[i].time += time;
curve.keys = keys;
AnimationUtility.SetEditorCurve(clip, bind, curve);
}
// update the PPtr curves
foreach (var bind in objectBindings)
{
var curve = AnimationUtility.GetObjectReferenceCurve(clip, bind);
for (var i = 0; i < curve.Length; i++)
curve[i].time += time;
AnimationUtility.SetObjectReferenceCurve(clip, bind, curve);
}
EditorUtility.SetDirty(clip);
}
public static void ScaleTime(this AnimationClip clip, float scale)
{
var floatBindings = AnimationUtility.GetCurveBindings(clip);
var objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(clip);
// update the float curves
foreach (var bind in floatBindings)
{
var curve = AnimationUtility.GetEditorCurve(clip, bind);
var keys = curve.keys;
for (var i = 0; i < keys.Length; i++)
keys[i].time *= scale;
curve.keys = keys.OrderBy(x => x.time).ToArray();
AnimationUtility.SetEditorCurve(clip, bind, curve);
}
// update the PPtr curves
foreach (var bind in objectBindings)
{
var curve = AnimationUtility.GetObjectReferenceCurve(clip, bind);
for (var i = 0; i < curve.Length; i++)
curve[i].time *= scale;
curve = curve.OrderBy(x => x.time).ToArray();
AnimationUtility.SetObjectReferenceCurve(clip, bind, curve);
}
EditorUtility.SetDirty(clip);
}
// Creates an opposing blend curve that matches the given curve to make sure the result is normalized
public static AnimationCurve CreateMatchingCurve(AnimationCurve curve)
{
Keyframe[] keys = curve.keys;
for (var i = 0; i != keys.Length; i++)
{
if (!Single.IsPositiveInfinity(keys[i].inTangent))
keys[i].inTangent = -keys[i].inTangent;
if (!Single.IsPositiveInfinity(keys[i].outTangent))
keys[i].outTangent = -keys[i].outTangent;
keys[i].value = 1.0f - keys[i].value;
}
return new AnimationCurve(keys);
}
// Sanitizes the keys on an animation to force the property to be normalized
public static Keyframe[] SanitizeCurveKeys(Keyframe[] keys, bool easeIn)
{
if (keys.Length < 2)
{
if (easeIn)
keys = new[] { new Keyframe(0, 0), new Keyframe(1, 1) };
else
keys = new[] { new Keyframe(0, 1), new Keyframe(1, 0) };
}
else if (easeIn)
{
keys[0].time = 0;
keys[keys.Length - 1].time = 1;
keys[keys.Length - 1].value = 1;
}
else
{
keys[0].time = 0;
keys[0].value = 1;
keys[keys.Length - 1].time = 1;
}
return keys;
}
}
}

View File

@@ -0,0 +1,307 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
/// <summary>
/// Description of the on-screen area where a clip is drawn
/// </summary>
public struct ClipBackgroundRegion
{
/// <summary>
/// The rectangle where the background of the clip is drawn.
/// </summary>
/// <remarks>
/// The rectangle is clipped to the screen. The rectangle does not include clip borders.
/// </remarks>
public Rect position { get; private set; }
/// <summary>
/// The start time of the region, relative to the clip.
/// </summary>
public double startTime { get; private set; }
/// <summary>
/// The end time of the region, relative to the clip.
/// </summary>
public double endTime { get; private set; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="_position"></param>
/// <param name="_startTime"></param>
/// <param name="_endTime"></param>
public ClipBackgroundRegion(Rect _position, double _startTime, double _endTime)
{
position = _position;
startTime = _startTime;
endTime = _endTime;
}
/// <summary>
/// Indicates whether this instance and a specified object are equal.
/// </summary>
/// <param name="obj">The object to compare with the current instance.</param>
/// <returns>Returns <c>true</c> if <paramref name="obj"/> and this instance are the same type and represent the same value.</returns>
public override bool Equals(object obj)
{
if (!(obj is ClipBackgroundRegion))
return false;
return Equals((ClipBackgroundRegion)obj);
}
/// <summary>
/// Compares this object with another <c>ClipBackgroundRegion</c>.
/// </summary>
/// <param name="other">The object to compare with.</param>
/// <returns>Returns true if <c>this</c> and <paramref name="other"/> are equal.</returns>
public bool Equals(ClipBackgroundRegion other)
{
return position.Equals(other.position) &&
startTime == other.startTime &&
endTime == other.endTime;
}
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns>A 32-bit signed integer that is the hash code for this instance.</returns>
public override int GetHashCode()
{
return HashUtility.CombineHash(
position.GetHashCode(),
startTime.GetHashCode(),
endTime.GetHashCode()
);
}
/// <summary>
/// Compares two <c>ClipBackgroundRegion</c> objects.
/// </summary>
/// <param name="region1">The first object.</param>
/// <param name="region2">The second object.</param>
/// <returns>Returns true if they are equal.</returns>
public static bool operator==(ClipBackgroundRegion region1, ClipBackgroundRegion region2)
{
return region1.Equals(region2);
}
/// <summary>
/// Compares two <c>ClipBackgroundRegion</c> objects.
/// </summary>
/// <param name="region1">The first object.</param>
/// <param name="region2">The second object.</param>
/// <returns>Returns true if they are not equal.</returns>
public static bool operator!=(ClipBackgroundRegion region1, ClipBackgroundRegion region2)
{
return !region1.Equals(region2);
}
}
/// <summary>
/// The user-defined options for drawing a clip.
/// </summary>
public struct ClipDrawOptions
{
private IEnumerable<Texture2D> m_Icons;
/// <summary>
/// Text that indicates if the clip should display an error.
/// </summary>
/// <remarks>
/// If the error text is not empty or null, then the clip displays a warning. The error text is used as the tooltip.
/// </remarks>
public string errorText { get; set; }
/// <summary>
/// The tooltip to show for the clip.
/// </summary>
public string tooltip { get; set; }
/// <summary>
/// The color drawn under the clip. By default, the color is the same as the track color.
/// </summary>
public Color highlightColor { get; set; }
/// <summary>
/// Icons to display on the clip.
/// </summary>
public IEnumerable<Texture2D> icons
{
get { return m_Icons ?? System.Linq.Enumerable.Empty<Texture2D>(); }
set { m_Icons = value;}
}
/// <summary>
/// Indicates whether this instance and a specified object are equal.
/// </summary>
/// <param name="obj">The object to compare with the current instance.</param>
/// <returns>Returns <c>true</c> if <paramref name="obj"/> and this instance are the same type and represent the same value.</returns>
public override bool Equals(object obj)
{
if (!(obj is ClipDrawOptions))
return false;
return Equals((ClipDrawOptions)obj);
}
/// <summary>
/// Compares this object with another <c>ClipDrawOptions</c>.
/// </summary>
/// <param name="other">The object to compare with.</param>
/// <returns>Returns true if <c>this</c> and <paramref name="other"/> are equal.</returns>
public bool Equals(ClipDrawOptions other)
{
return errorText == other.errorText &&
tooltip == other.tooltip &&
highlightColor == other.highlightColor &&
System.Linq.Enumerable.SequenceEqual(icons, other.icons);
}
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns>A 32-bit signed integer that is the hash code for this instance.</returns>
public override int GetHashCode()
{
return HashUtility.CombineHash(
errorText != null ? errorText.GetHashCode() : 0,
tooltip != null ? tooltip.GetHashCode() : 0,
highlightColor.GetHashCode(),
icons != null ? icons.GetHashCode() : 0
);
}
/// <summary>
/// Compares two <c>ClipDrawOptions</c> objects.
/// </summary>
/// <param name="options1">The first object.</param>
/// <param name="options2">The second object.</param>
/// <returns>Returns true if they are equal.</returns>
public static bool operator==(ClipDrawOptions options1, ClipDrawOptions options2)
{
return options1.Equals(options2);
}
/// <summary>
/// Compares two <c>ClipDrawOptions</c> objects.
/// </summary>
/// <param name="options1">The first object.</param>
/// <param name="options2">The second object.</param>
/// <returns>Returns true if they are not equal.</returns>
public static bool operator!=(ClipDrawOptions options1, ClipDrawOptions options2)
{
return !options1.Equals(options2);
}
}
/// <summary>
/// Use this class to customize clip types in the TimelineEditor.
/// </summary>
public class ClipEditor
{
static readonly string k_NoPlayableAssetError = L10n.Tr("This clip does not contain a valid playable asset");
static readonly string k_ScriptLoadError = L10n.Tr("The associated script can not be loaded");
internal readonly bool supportsSubTimelines;
/// <summary>
/// Default constructor
/// </summary>
public ClipEditor()
{
supportsSubTimelines = TypeUtility.HasOverrideMethod(GetType(), nameof(GetSubTimelines));
}
/// <summary>
/// Implement this method to override the default options for drawing a clip.
/// </summary>
/// <param name="clip">The clip being drawn.</param>
/// <returns>The options for drawing a clip.</returns>
public virtual ClipDrawOptions GetClipOptions(TimelineClip clip)
{
return new ClipDrawOptions()
{
errorText = GetErrorText(clip),
tooltip = string.Empty,
highlightColor = GetDefaultHighlightColor(clip),
icons = System.Linq.Enumerable.Empty<Texture2D>()
};
}
/// <summary>
/// Override this method to draw a background for a clip .
/// </summary>
/// <param name="clip">The clip being drawn.</param>
/// <param name="region">The on-screen area where the clip is drawn.</param>
public virtual void DrawBackground(TimelineClip clip, ClipBackgroundRegion region)
{
}
/// <summary>
/// Called when a clip is created.
/// </summary>
/// <param name="clip">The newly created clip.</param>
/// <param name="track">The track that the clip is assigned to.</param>
/// <param name="clonedFrom">The source that the clip was copied from. This can be set to null if the clip is not a copy.</param>
/// <remarks>
/// The callback occurs before the clip is assigned to the track.
/// </remarks>
public virtual void OnCreate(TimelineClip clip, TrackAsset track, TimelineClip clonedFrom)
{
}
/// <summary>
/// Gets the error text for the specified clip.
/// </summary>
/// <param name="clip">The clip being drawn.</param>
/// <returns>Returns the error text to be displayed as the tool tip for the clip. If there is no error to be displayed, this method returns string.Empty.</returns>
public string GetErrorText(TimelineClip clip)
{
if (clip == null || clip.asset == null)
return k_NoPlayableAssetError;
var playableAsset = clip.asset as ScriptableObject;
if (playableAsset == null || MonoScript.FromScriptableObject(playableAsset) == null)
return k_ScriptLoadError;
return string.Empty;
}
/// <summary>
/// The color drawn under the clip. By default, the color is the same as the track color.
/// </summary>
/// <param name="clip">The clip being drawn.</param>
/// <returns>Returns the highlight color of the clip being drawn.</returns>
public Color GetDefaultHighlightColor(TimelineClip clip)
{
if (clip == null)
return Color.white;
return TrackResourceCache.GetTrackColor(clip.GetParentTrack());
}
/// <summary>
/// Called when a clip is changed by the Editor.
/// </summary>
/// <param name="clip">The clip that changed.</param>
public virtual void OnClipChanged(TimelineClip clip)
{
}
/// <summary>
/// Gets the sub-timelines for a specific clip. Implement this method if your clip supports playing nested timelines.
/// </summary>
/// <param name="clip">The clip with the ControlPlayableAsset.</param>
/// <param name="director">The playable director driving the Timeline Clip. This may not be the same as TimelineEditor.inspectedDirector.</param>
/// <param name="subTimelines">Specify the sub-timelines to control.</param>
public virtual void GetSubTimelines(TimelineClip clip, PlayableDirector director, List<PlayableDirector> subTimelines)
{
}
}
}

View File

@@ -0,0 +1,155 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class CustomTimelineEditorCache
{
static class SubClassCache<TEditorClass> where TEditorClass : class, new()
{
private static Type[] s_SubClasses = null;
private static readonly TEditorClass s_DefaultInstance = new TEditorClass();
private static readonly Dictionary<System.Type, TEditorClass> s_TypeMap = new Dictionary<Type, TEditorClass>();
public static TEditorClass DefaultInstance
{
get { return s_DefaultInstance; }
}
static Type[] SubClasses
{
get
{
// order the subclass array by built-ins then user defined so built-in classes are chosen first
return s_SubClasses ??
(s_SubClasses = TypeCache.GetTypesDerivedFrom<TEditorClass>().OrderBy(t => t.Assembly == typeof(UnityEditor.Timeline.TimelineEditor).Assembly ? 1 : 0).ToArray());
}
}
public static TEditorClass GetEditorForType(Type type)
{
TEditorClass editorClass = null;
if (!s_TypeMap.TryGetValue(type, out editorClass) || editorClass == null)
{
Type editorClassType = null;
Type searchType = type;
while (searchType != null)
{
// search our way up the runtime class hierarchy so we get the best match
editorClassType = GetExactEditorClassForType(searchType);
if (editorClassType != null)
break;
searchType = searchType.BaseType;
}
if (editorClassType == null)
{
editorClass = s_DefaultInstance;
}
else
{
try
{
editorClass = (TEditorClass)Activator.CreateInstance(editorClassType);
}
catch (Exception e)
{
Debug.LogWarningFormat("Could not create a Timeline editor class of type {0}: {1}", editorClassType, e.Message);
editorClass = s_DefaultInstance;
}
}
s_TypeMap[type] = editorClass;
}
return editorClass;
}
private static Type GetExactEditorClassForType(Type type)
{
foreach (var subClass in SubClasses)
{
// first check for exact match
var attr = (CustomTimelineEditorAttribute)Attribute.GetCustomAttribute(subClass, typeof(CustomTimelineEditorAttribute), false);
if (attr != null && attr.classToEdit == type)
{
return subClass;
}
}
return null;
}
public static void Clear()
{
s_TypeMap.Clear();
s_SubClasses = null;
}
}
public static TEditorClass GetEditorForType<TEditorClass, TRuntimeClass>(Type type) where TEditorClass : class, new()
{
if (type == null)
throw new ArgumentNullException(nameof(type));
if (!typeof(TRuntimeClass).IsAssignableFrom(type))
throw new ArgumentException(type.FullName + " does not inherit from" + typeof(TRuntimeClass));
return SubClassCache<TEditorClass>.GetEditorForType(type);
}
public static void ClearCache<TEditorClass>() where TEditorClass : class, new()
{
SubClassCache<TEditorClass>.Clear();
}
public static ClipEditor GetClipEditor(TimelineClip clip)
{
if (clip == null)
throw new ArgumentNullException(nameof(clip));
var type = typeof(IPlayableAsset);
if (clip.asset != null)
type = clip.asset.GetType();
if (!typeof(IPlayableAsset).IsAssignableFrom(type))
return GetDefaultClipEditor();
return GetEditorForType<ClipEditor, IPlayableAsset>(type);
}
public static ClipEditor GetDefaultClipEditor()
{
return SubClassCache<ClipEditor>.DefaultInstance;
}
public static TrackEditor GetTrackEditor(TrackAsset track)
{
if (track == null)
throw new ArgumentNullException(nameof(track));
return GetEditorForType<TrackEditor, TrackAsset>(track.GetType());
}
public static TrackEditor GetDefaultTrackEditor()
{
return SubClassCache<TrackEditor>.DefaultInstance;
}
public static MarkerEditor GetMarkerEditor(IMarker marker)
{
if (marker == null)
throw new ArgumentNullException(nameof(marker));
return GetEditorForType<MarkerEditor, IMarker>(marker.GetType());
}
public static MarkerEditor GetDefaultMarkerEditor()
{
return SubClassCache<MarkerEditor>.DefaultInstance;
}
}
}

View File

@@ -0,0 +1,292 @@
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
/// <summary>
/// The flags that indicate the view status of a marker.
/// </summary>
[System.Flags]
public enum MarkerUIStates
{
/// <summary>
/// No extra state specified.
/// </summary>
None = 0,
/// <summary>
/// The marker is selected.
/// </summary>
Selected = 1 << 0,
/// <summary>
/// The marker is in a collapsed state.
/// </summary>
Collapsed = 1 << 1
}
/// <summary>
/// The user-defined options for drawing a marker.
/// </summary>
public struct MarkerDrawOptions
{
/// <summary>
/// The tooltip for the marker.
/// </summary>
public string tooltip { get; set; }
/// <summary>
/// Text that indicates if the marker should display an error.
/// </summary>
/// <remarks>
/// If the error text is not empty or null, then the marker displays a warning. The error text is used as the tooltip.
/// </remarks>
public string errorText { get; set; }
/// <summary>
/// Indicates whether this instance and a specified object are equal.
/// </summary>
/// <param name="obj">The object to compare with the current instance.</param>
/// <returns>Returns <c>true</c> if <paramref name="obj"/> and this instance are the same type and represent the same value.</returns>
public override bool Equals(object obj)
{
if (!(obj is MarkerDrawOptions))
return false;
return Equals((MarkerDrawOptions)obj);
}
/// <summary>
/// Compares this object with another <c>MarkerDrawOptions</c>.
/// </summary>
/// <param name="other">The object to compare with.</param>
/// <returns>Returns true if <c>this</c> and <paramref name="other"/> are equal.</returns>
public bool Equals(MarkerDrawOptions other)
{
return errorText == other.errorText &&
tooltip == other.tooltip;
}
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns>A 32-bit signed integer that is the hash code for this instance.</returns>
public override int GetHashCode()
{
return HashUtility.CombineHash(
errorText != null ? errorText.GetHashCode() : 0,
tooltip != null ? tooltip.GetHashCode() : 0
);
}
/// <summary>
/// Compares two <c>MarkerDrawOptions</c> objects.
/// </summary>
/// <param name="options1">The first object.</param>
/// <param name="options2">The second object.</param>
/// <returns>Returns true if they are equal.</returns>
public static bool operator==(MarkerDrawOptions options1, MarkerDrawOptions options2)
{
return options1.Equals(options2);
}
/// <summary>
/// Compares two <c>MarkerDrawOptions</c> objects.
/// </summary>
/// <param name="options1">The first object.</param>
/// <param name="options2">The second object.</param>
/// <returns>Returns true if they are not equal.</returns>
public static bool operator!=(MarkerDrawOptions options1, MarkerDrawOptions options2)
{
return !options1.Equals(options2);
}
}
/// <summary>
/// The description of the on-screen area where the marker is drawn.
/// </summary>
public struct MarkerOverlayRegion
{
/// <summary>
/// The area where the marker is being drawn.
/// </summary>
public Rect markerRegion { get; private set; }
/// <summary>
/// The area where the overlay is being drawn.
///
/// This region extends from the top of the time ruler to the bottom of the window, excluding any scrollbars.
/// </summary>
public Rect timelineRegion { get; private set; }
/// <summary>
/// The sub-area of the timelineRegion where the tracks are drawn.
///
/// The region extends from the bottom of the time ruler, or the timeline marker region if not hidden.
/// Use this region to clip overlays that should not be drawn over the timeline marker region or time ruler.
/// </summary>
/// <example>
/// <code source="../../DocCodeExamples/MarkerEditorExamples.cs" region="declare-trackRegion" title="trackRegion"/>
/// </example>
public Rect trackRegion => Rect.MinMaxRect(timelineRegion.xMin, timelineRegion.yMin + m_TrackOffset, timelineRegion.xMax, timelineRegion.yMax);
/// <summary>
/// The start time of the visible region of the window.
/// </summary>
public double startTime { get; private set; }
/// <summary>
/// The end time of the visible region of the window.
/// </summary>
public double endTime { get; private set; }
private float m_TrackOffset;
/// <summary>Constructor</summary>
/// <param name="_markerRegion">The area where the marker is being drawn.</param>
/// <param name="_timelineRegion">The area where the overlay is being drawn.</param>
/// <param name="_startTime">The start time of the visible region of the window.</param>
/// <param name="_endTime">The end time of the visible region of the window.</param>
public MarkerOverlayRegion(Rect _markerRegion, Rect _timelineRegion, double _startTime, double _endTime)
: this(_markerRegion, _timelineRegion, _startTime, _endTime, 19.0f)
{
}
/// <summary>Constructor</summary>
/// <param name="_markerRegion">The area where the marker is being drawn.</param>
/// <param name="_timelineRegion">The area where the overlay is being drawn.</param>
/// <param name="_startTime">The start time of the visible region of the window.</param>
/// <param name="_endTime">The end time of the visible region of the window.</param>
/// <param name="_trackOffset">The offset from the timelineRegion to the trackRegion</param>
public MarkerOverlayRegion(Rect _markerRegion, Rect _timelineRegion, double _startTime, double _endTime, float _trackOffset)
{
markerRegion = _markerRegion;
timelineRegion = _timelineRegion;
startTime = _startTime;
endTime = _endTime;
m_TrackOffset = _trackOffset;
}
/// <summary>
/// Indicates whether this instance and a specified object are equal.
/// </summary>
/// <param name="obj">The object to compare with the current instance.</param>
/// <returns>Returns <c>true</c> if <paramref name="obj"/> and this instance are the same type and represent the same value.</returns>
public override bool Equals(object obj)
{
if (!(obj is MarkerOverlayRegion))
return false;
return Equals((MarkerOverlayRegion)obj);
}
/// <summary>
/// Compares this object with another <c>MarkerOverlayRegion</c>.
/// </summary>
/// <param name="other">The object to compare with.</param>
/// <returns>Returns true if <c>this</c> and <paramref name="other"/> are equal.</returns>
public bool Equals(MarkerOverlayRegion other)
{
return markerRegion == other.markerRegion &&
timelineRegion == other.timelineRegion &&
startTime == other.startTime &&
endTime == other.endTime &&
m_TrackOffset == other.m_TrackOffset;
}
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns>A 32-bit signed integer that is the hash code for this instance.</returns>
public override int GetHashCode()
{
return HashUtility.CombineHash(
markerRegion.GetHashCode(),
timelineRegion.GetHashCode(),
startTime.GetHashCode(),
endTime.GetHashCode(),
m_TrackOffset.GetHashCode()
);
}
/// <summary>
/// Compares two <c>MarkerOverlayRegion</c> objects.
/// </summary>
/// <param name="region1">The first object.</param>
/// <param name="region2">The second object.</param>
/// <returns>Returns true if they are equal.</returns>
public static bool operator==(MarkerOverlayRegion region1, MarkerOverlayRegion region2)
{
return region1.Equals(region2);
}
/// <summary>
/// Compares two <c>MarkerOverlayRegion</c> objects.
/// </summary>
/// <param name="region1">The first object.</param>
/// <param name="region2">The second object.</param>
/// <returns>Returns true if they are not equal.</returns>
public static bool operator!=(MarkerOverlayRegion region1, MarkerOverlayRegion region2)
{
return !region1.Equals(region2);
}
}
/// <summary>
/// Use this class to customize marker types in the TimelineEditor.
/// </summary>
public class MarkerEditor
{
internal readonly bool supportsDrawOverlay;
/// <summary>
/// Default constructor
/// </summary>
public MarkerEditor()
{
supportsDrawOverlay = TypeUtility.HasOverrideMethod(GetType(), nameof(DrawOverlay));
}
/// <summary>
/// Implement this method to override the default options for drawing a marker.
/// </summary>
/// <param name="marker">The marker to draw.</param>
/// <returns></returns>
public virtual MarkerDrawOptions GetMarkerOptions(IMarker marker)
{
return new MarkerDrawOptions()
{
tooltip = string.Empty,
errorText = string.Empty,
};
}
/// <summary>
/// Called when a marker is created.
/// </summary>
/// <param name="marker">The marker that is created.</param>
/// <param name="clonedFrom">TThe source that the marker was copied from. This can be set to null if the marker is not a copy.</param>
/// <remarks>
/// The callback occurs before the marker is assigned to the track.
/// </remarks>
public virtual void OnCreate(IMarker marker, IMarker clonedFrom)
{
}
/// <summary>
/// Draws additional overlays for a marker.
/// </summary>
/// <param name="marker">The marker to draw.</param>
/// <param name="uiState">The visual state of the marker.</param>
/// <param name="region">The on-screen area where the marker is being drawn.</param>
/// <remarks>
/// Notes:
/// * It is only called during TimelineWindow's Repaint step.
/// * If there are multiple markers on top of each other, only the topmost marker receives the DrawOverlay call.
/// </remarks>
public virtual void DrawOverlay(IMarker marker, MarkerUIStates uiState, MarkerOverlayRegion region)
{
}
}
}

View File

@@ -0,0 +1,18 @@
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
[CustomTimelineEditor(typeof(MarkerTrack))]
class MarkerTrackEditor : TrackEditor
{
public static readonly float DefaultMarkerTrackHeight = 24;
public override TrackDrawOptions GetTrackOptions(TrackAsset track, Object binding)
{
var options = base.GetTrackOptions(track, binding);
options.minimumHeight = DefaultMarkerTrackHeight;
return options;
}
}
}

View File

@@ -0,0 +1,417 @@
using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
/// <summary>
/// The user-defined options for drawing a track."
/// </summary>
public struct TrackDrawOptions
{
/// <summary>
/// Text that indicates if the track should display an error.
/// </summary>
/// <remarks>
/// If the error text is not empty or null, then the track displays a warning. The error text is used as the tooltip.
/// </remarks>
public string errorText { get; set; }
/// <summary>
/// The highlight color of the track.
/// </summary>
public Color trackColor { get; set; }
/// <summary>
/// The minimum height of the track.
/// </summary>
public float minimumHeight { get; set; }
/// <summary>
/// The icon displayed on the track header.
/// </summary>
/// <remarks>
/// If this value is null, then the default icon for the track is used.
/// </remarks>
public Texture2D icon { get; set; }
/// <summary>
/// Indicates whether this instance and a specified object are equal.
/// </summary>
/// <param name="obj">The object to compare with the current instance.</param>
/// <returns>Returns <c>true</c> if <paramref name="obj"/> and this instance are the same type and represent the same value.</returns>
public override bool Equals(object obj)
{
if (!(obj is TrackDrawOptions))
return false;
return Equals((TrackDrawOptions)obj);
}
/// <summary>
/// Compares this object with another <c>TrackDrawOptions</c>.
/// </summary>
/// <param name="other">The object to compare with.</param>
/// <returns>Returns true if <c>this</c> and <paramref name="other"/> are equal.</returns>
public bool Equals(TrackDrawOptions other)
{
return errorText == other.errorText &&
trackColor == other.trackColor &&
minimumHeight == other.minimumHeight &&
icon == other.icon;
}
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns>A 32-bit signed integer that is the hash code for this instance.</returns>
public override int GetHashCode()
{
return HashUtility.CombineHash(
errorText != null ? errorText.GetHashCode() : 0,
trackColor.GetHashCode(),
minimumHeight.GetHashCode(),
icon != null ? icon.GetHashCode() : 0
);
}
/// <summary>
/// Compares two <c>TrackDrawOptions</c> objects.
/// </summary>
/// <param name="options1">The first object.</param>
/// <param name="options2">The second object.</param>
/// <returns>Returns true if they are equal.</returns>
public static bool operator==(TrackDrawOptions options1, TrackDrawOptions options2)
{
return options1.Equals(options2);
}
/// <summary>
/// Compares two <c>TrackDrawOptions</c> objects.
/// </summary>
/// <param name="options1">The first object.</param>
/// <param name="options2">The second object.</param>
/// <returns>Returns true if they are not equal.</returns>
public static bool operator!=(TrackDrawOptions options1, TrackDrawOptions options2)
{
return !options1.Equals(options2);
}
}
/// <summary>
/// The errors displayed for the track binding.
/// </summary>
public enum TrackBindingErrors
{
/// <summary>
/// Select no errors.
/// </summary>
None = 0,
/// <summary>
/// The bound GameObject is disabled.
/// </summary>
BoundGameObjectDisabled = 1 << 0,
/// <summary>
/// The bound GameObject does not have a valid component.
/// </summary>
NoValidComponent = 1 << 1,
/// <summary>
/// The bound Object is a disabled Behaviour.
/// </summary>
BehaviourIsDisabled = 1 << 2,
/// <summary>
/// The bound Object is not of the correct type.
/// </summary>
InvalidBinding = 1 << 3,
/// <summary>
/// The bound Object is part of a prefab, and not an instance.
/// </summary>
PrefabBound = 1 << 4,
/// <summary>
/// Select all errors.
/// </summary>
All = Int32.MaxValue
}
/// <summary>
/// Use this class to customize track types in the TimelineEditor.
/// </summary>
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<TrackAsset, System.Type> m_BindingCache = new Dictionary<TrackAsset, System.Type>();
/// <summary>
/// The default height of a track.
/// </summary>
public static readonly float DefaultTrackHeight = 30.0f;
/// <summary>
/// The minimum unscaled height of a track.
/// </summary>
public static readonly float MinimumTrackHeight = 10.0f;
/// <summary>
/// The maximum height of a track.
/// </summary>
public static readonly float MaximumTrackHeight = 256.0f;
/// <summary>
/// Implement this method to override the default options for drawing a track.
/// </summary>
/// <param name="track">The track from which track options are retrieved.</param>
/// <param name="binding">The binding for the track.</param>
/// <returns>The options for drawing the track.</returns>
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
};
}
/// <summary>
/// Gets the error text for the specified track.
/// </summary>
/// <param name="track">The track to retrieve options for.</param>
/// <param name="boundObject">The binding for the track.</param>
/// <param name="detectErrors">The errors to check for.</param>
/// <returns>An error to be displayed on the track, or string.Empty if there is no error.</returns>
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;
}
/// <summary>
/// Gets the color information of a track.
/// </summary>
/// <param name="track"></param>
/// <returns>Returns the color for the specified track.</returns>
public Color GetTrackColor(TrackAsset track)
{
return TrackResourceCache.GetTrackColor(track);
}
/// <summary>
/// Gets the binding type for a track.
/// </summary>
/// <param name="track">The track to retrieve the binding type from.</param>
/// <returns>Returns the binding type for the specified track. Returns null if the track does not have binding.</returns>
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;
}
/// <summary>
/// Callback for when a track is created.
/// </summary>
/// <param name="track">The track that is created.</param>
/// <param name="copiedFrom">The source that the track is copied from. This can be set to null if the track is not a copy.</param>
public virtual void OnCreate(TrackAsset track, TrackAsset copiedFrom)
{
}
/// <summary>
/// Callback for when a track is changed.
/// </summary>
/// <param name="track">The track that is changed.</param>
public virtual void OnTrackChanged(TrackAsset track)
{
}
static bool HasFlag(TrackBindingErrors errors, TrackBindingErrors flag)
{
return (errors & flag) != 0;
}
/// <summary>
/// Override this method to validate if a binding for <paramref name="track"/>
/// can be determined from <paramref name="candidate"/>.
///
/// The default implementation of this method will return true if
/// - <paramref name="candidate"/> is not null or,
/// - <paramref name="candidate"/> is not part of a Prefab Asset or,
/// - <paramref name="candidate"/> is a Component that can be bound to <paramref name="track"/>
/// </summary>
/// <param name="candidate"></param>
/// <param name="track">TBD</param>
/// <returns>True if a binding can be determined from <paramref name="candidate"/>.</returns>
/// <seealso cref="UnityEngine.Timeline.TrackBindingTypeAttribute"/>
/// <seealso cref="UnityEngine.Timeline.TrackAsset"/>
public virtual bool IsBindingAssignableFrom(UnityEngine.Object candidate, TrackAsset track)
{
var action = BindingUtility.GetBindingAction(GetBindingType(track), candidate);
return action != BindingUtility.BindingAction.DoNotBind;
}
/// <summary>
/// Override this method to determine which object to bind to <paramref name="track"/>.
/// A binding object should be determined from <paramref name="candidate"/>.
///
/// By default, the `TrackBindingType` attribute from <paramref name="track"/> will be used to determine the binding.
/// </summary>
/// <param name="candidate">The source object from which a track binding should be determined.</param>
/// <param name="track">The track to bind an object to.</param>
/// <returns>The object to bind to <paramref name="track"/>.</returns>
/// <seealso cref="UnityEngine.Timeline.TrackBindingTypeAttribute"/>
/// <seealso cref="UnityEngine.Timeline.TrackAsset"/>
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);
}
}
}
}

View File

@@ -0,0 +1,288 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text;
using UnityEditor.Timeline;
namespace UnityEngine.Timeline
{
[Serializable]
class DirectorNamedColor : ScriptableObject
{
[SerializeField]
public Color colorPlayhead;
[SerializeField]
public Color colorSelection;
[SerializeField]
public Color colorEndmarker;
[SerializeField]
public Color colorGroup;
[SerializeField]
public Color colorGroupTrackBackground;
[SerializeField]
public Color colorAnimation;
[SerializeField]
public Color colorAnimationRecorded;
[SerializeField]
public Color colorAudio;
[SerializeField]
public Color colorAudioWaveform;
[SerializeField]
public Color colorActivation;
[SerializeField]
public Color colorDropTarget;
[SerializeField]
public Color colorClipFont;
[SerializeField]
public Color colorInvalidClipOverlay;
[SerializeField]
public Color colorTrackBackground;
[SerializeField]
public Color colorTrackHeaderBackground;
[SerializeField]
public Color colorTrackDarken;
[SerializeField]
public Color colorTrackBackgroundRecording;
[SerializeField]
public Color colorInfiniteTrackBackgroundRecording;
[SerializeField]
public Color colorTrackBackgroundSelected;
[SerializeField]
public Color colorTrackFont;
[SerializeField]
public Color colorClipUnion;
[SerializeField]
public Color colorTopOutline3;
[SerializeField]
public Color colorDurationLine;
[SerializeField]
public Color colorRange;
[SerializeField]
public Color colorSequenceBackground;
[SerializeField]
public Color colorTooltipBackground;
[SerializeField]
public Color colorInfiniteClipLine;
[SerializeField]
public Color colorDefaultTrackDrawer;
[SerializeField]
public Color colorDuration = new Color(0.66f, 0.66f, 0.66f, 1.0f);
[SerializeField]
public Color colorRecordingClipOutline = new Color(1, 0, 0, 0.9f);
[SerializeField]
public Color colorAnimEditorBinding = new Color(54.0f / 255.0f, 54.0f / 255.0f, 54.0f / 255.0f);
[SerializeField]
public Color colorTimelineBackground = new Color(0.2f, 0.2f, 0.2f, 1.0f);
[SerializeField]
public Color colorLockTextBG = Color.red;
[SerializeField]
public Color colorInlineCurveVerticalLines = new Color(1.0f, 1.0f, 1.0f, 0.2f);
[SerializeField]
public Color colorInlineCurveOutOfRangeOverlay = new Color(0.0f, 0.0f, 0.0f, 0.5f);
[SerializeField]
public Color colorInlineCurvesBackground;
[SerializeField]
public Color markerDrawerBackgroundColor = new Color(0.4f, 0.4f, 0.4f , 1.0f);
[SerializeField]
public Color markerHeaderDrawerBackgroundColor = new Color(0.5f, 0.5f, 0.5f , 1.0f);
[SerializeField]
public Color colorControl = new Color(0.2313f, 0.6353f, 0.5843f, 1.0f);
[SerializeField]
public Color colorSubSequenceBackground = new Color(0.1294118f, 0.1764706f, 0.1764706f, 1.0f);
[SerializeField]
public Color colorTrackSubSequenceBackground = new Color(0.1607843f, 0.2156863f, 0.2156863f, 1.0f);
[SerializeField]
public Color colorTrackSubSequenceBackgroundSelected = new Color(0.0726923f, 0.252f, 0.252f, 1.0f);
[SerializeField]
public Color colorSubSequenceOverlay = new Color(0.02f, 0.025f, 0.025f, 0.30f);
[SerializeField]
public Color colorSubSequenceDurationLine = new Color(0.0f, 1.0f, 0.88f, 0.46f);
[SerializeField]
public Color clipBckg = new Color(0.5f, 0.5f, 0.5f, 1.0f);
[SerializeField]
public Color clipSelectedBckg = new Color(0.7f, 0.7f, 0.7f, 1.0f);
[SerializeField]
public Color clipBorderColor = new Color(0.4f, 0.4f, 0.4f, 1.0f);
[SerializeField]
public Color clipEaseBckgColor = new Color(0.4f, 0.4f, 0.4f, 1.0f);
[SerializeField]
public Color clipBlendIn;
[SerializeField]
public Color clipBlendInSelected;
[SerializeField]
public Color clipBlendOut;
[SerializeField]
public Color clipBlendOutSelected;
public void SetDefault()
{
colorPlayhead = DirectorStyles.Instance.timeCursor.normal.textColor;
colorSelection = DirectorStyles.Instance.selectedStyle.normal.textColor;
colorEndmarker = DirectorStyles.Instance.endmarker.normal.textColor;
colorGroup = new Color(0.094f, 0.357f, 0.384f, 0.310f);
colorGroupTrackBackground = new Color(0f, 0f, 0f, 1f);
colorAnimation = new Color(0.3f, 0.39f, 0.46f, 1.0f);
colorAnimationRecorded = new Color(colorAnimation.r * 0.75f, colorAnimation.g * 0.75f, colorAnimation.b * 0.75f, 1.0f);
colorAudio = new Color(1f, 0.635f, 0f);
colorAudioWaveform = new Color(0.129f, 0.164f, 0.254f);
colorActivation = Color.green;
colorDropTarget = new Color(0.514f, 0.627f, 0.827f);
colorClipFont = DirectorStyles.Instance.fontClip.normal.textColor;
colorTrackBackground = new Color(0.2f, 0.2f, 0.2f, 1.0f);
colorTrackBackgroundSelected = new Color(1f, 1f, 1f, 0.33f);
colorInlineCurvesBackground = new Color(0.25f, 0.25f, 0.25f, 0.6f);
colorTrackFont = DirectorStyles.Instance.trackHeaderFont.normal.textColor;
colorClipUnion = new Color(0.72f, 0.72f, 0.72f, 0.8f);
colorTopOutline3 = new Color(0.274f, 0.274f, 0.274f, 1.0f);
colorDurationLine = new Color(33.0f / 255.0f, 109.0f / 255.0f, 120.0f / 255.0f);
colorRange = new Color(0.733f, 0.733f, 0.733f, 0.70f);
colorSequenceBackground = new Color(0.16f, 0.16f, 0.16f, 1.0f);
colorTooltipBackground = new Color(29.0f / 255.0f, 32.0f / 255.0f, 33.0f / 255.0f);
colorInfiniteClipLine = new Color(72.0f / 255.0f, 78.0f / 255.0f, 82.0f / 255.0f);
colorTrackBackgroundRecording = new Color(1, 0, 0, 0.1f);
colorTrackDarken = new Color(0.0f, 0.0f, 0.0f, 0.4f);
colorTrackHeaderBackground = new Color(51.0f / 255.0f, 51.0f / 255.0f, 51.0f / 255.0f, 1.0f);
colorDefaultTrackDrawer = new Color(218.0f / 255.0f, 220.0f / 255.0f, 222.0f / 255.0f);
colorRecordingClipOutline = new Color(1, 0, 0, 0.9f);
colorInlineCurveVerticalLines = new Color(1.0f, 1.0f, 1.0f, 0.2f);
colorInlineCurveOutOfRangeOverlay = new Color(0.0f, 0.0f, 0.0f, 0.5f);
}
public void ToText(string path)
{
StringBuilder builder = new StringBuilder();
var fields = GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly);
foreach (var f in fields)
{
if (f.FieldType != typeof(Color))
continue;
Color c = (Color)f.GetValue(this);
builder.Append(f.Name + "," + c);
builder.Append("\n");
}
File.WriteAllText(path, builder.ToString());
}
public void FromText(string text)
{
// parse to a map
string[] lines = text.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
var map = new Dictionary<string, Color>();
foreach (var line in lines)
{
var pieces = line.Replace("RGBA(", "").Replace(")", "").Split(',');
if (pieces.Length == 5)
{
string name = pieces[0].Trim();
Color c = Color.black;
bool b = ParseFloat(pieces[1], out c.r) &&
ParseFloat(pieces[2], out c.g) &&
ParseFloat(pieces[3], out c.b) &&
ParseFloat(pieces[4], out c.a);
if (b)
{
map[name] = c;
}
}
}
var fields = typeof(DirectorNamedColor).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly);
foreach (var f in fields)
{
if (f.FieldType != typeof(Color))
continue;
Color c = Color.black;
if (map.TryGetValue(f.Name, out c))
{
f.SetValue(this, c);
}
}
}
// Case 938534 - Timeline window has white background when running on .NET 4.6 depending on the set system language
// Make sure we're using an invariant culture so "0.35" is parsed as 0.35 and not 35
static bool ParseFloat(string str, out float f)
{
return float.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out f);
}
public static DirectorNamedColor CreateAndLoadFromText(string text)
{
DirectorNamedColor instance = CreateInstance<DirectorNamedColor>();
instance.FromText(text);
return instance;
}
}
}

View File

@@ -0,0 +1,370 @@
using UnityEditor.Experimental;
using UnityEditor.StyleSheets;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class DirectorStyles
{
const string k_Elipsis = "…";
const string k_ImagePath = "Packages/com.unity.timeline/Editor/StyleSheets/Images/Icons/{0}.png";
public const string resourcesPath = "Packages/com.unity.timeline/Editor/StyleSheets/res/";
//Timeline resources
public const string newTimelineDefaultNameSuffix = "Timeline";
public static readonly GUIContent referenceTrackLabel = TrTextContent("R", "This track references an external asset");
public static readonly GUIContent recordingLabel = TrTextContent("Recording...");
public static readonly GUIContent noTimelineAssetSelected = TrTextContent("To start creating a timeline, select a GameObject");
public static readonly GUIContent createTimelineOnSelection = TrTextContent("To begin a new timeline with {0}, create {1}");
public static readonly GUIContent noTimelinesInScene = TrTextContent("No timeline found in the scene");
public static readonly GUIContent createNewTimelineText = TrTextContent("Create a new Timeline and Director Component for Game Object");
public static readonly GUIContent previewContent = TrTextContent("Preview", "Enable/disable scene preview mode");
public static readonly GUIContent previewDisabledContent = L10n.TextContentWithIcon("Preview", "Scene preview is disabled for this TimelineAsset", MessageType.Info);
public static readonly GUIContent mixOff = TrIconContent("TimelineEditModeMixOFF", "Mix Mode (1)");
public static readonly GUIContent mixOn = TrIconContent("TimelineEditModeMixON", "Mix Mode (1)");
public static readonly GUIContent rippleOff = TrIconContent("TimelineEditModeRippleOFF", "Ripple Mode (2)");
public static readonly GUIContent rippleOn = TrIconContent("TimelineEditModeRippleON", "Ripple Mode (2)");
public static readonly GUIContent replaceOff = TrIconContent("TimelineEditModeReplaceOFF", "Replace Mode (3)");
public static readonly GUIContent replaceOn = TrIconContent("TimelineEditModeReplaceON", "Replace Mode (3)");
public static readonly GUIContent showMarkersOn = TrIconContent("TimelineCollapseMarkerButtonEnabled", "Show / Hide Timeline Markers");
public static readonly GUIContent showMarkersOff = TrIconContent("TimelineCollapseMarkerButtonDisabled", "Show / Hide Timeline Markers");
public static readonly GUIContent showMarkersOnTimeline = TrTextContent("Show markers");
public static readonly GUIContent timelineMarkerTrackHeader = TrTextContentWithIcon("Markers", string.Empty, "TimelineHeaderMarkerIcon");
public static readonly GUIContent signalTrackIcon = IconContent("TimelineSignal");
//Unity Default Resources
public static readonly GUIContent playContent = L10n.IconContent("Animation.Play", "Play the timeline (Space)");
public static readonly GUIContent gotoBeginingContent = L10n.IconContent("Animation.FirstKey", "Go to the beginning of the timeline (Shift+<)");
public static readonly GUIContent gotoEndContent = L10n.IconContent("Animation.LastKey", "Go to the end of the timeline (Shift+>)");
public static readonly GUIContent nextFrameContent = L10n.IconContent("Animation.NextKey", "Go to the next frame");
public static readonly GUIContent previousFrameContent = L10n.IconContent("Animation.PrevKey", "Go to the previous frame");
public static readonly GUIContent newContent = L10n.IconContent("CreateAddNew", "Add new tracks.");
public static readonly GUIContent optionsCogIcon = L10n.IconContent("_Popup", "Options");
public static readonly GUIContent animationTrackIcon = EditorGUIUtility.IconContent("AnimationClip Icon");
public static readonly GUIContent audioTrackIcon = EditorGUIUtility.IconContent("AudioSource Icon");
public static readonly GUIContent playableTrackIcon = EditorGUIUtility.IconContent("cs Script Icon");
public static readonly GUIContent timelineSelectorArrow = L10n.IconContent("icon dropdown", "Timeline Selector");
public GUIContent playrangeContent;
public static readonly float kBaseIndent = 15.0f;
public static readonly float kDurationGuiThickness = 5.0f;
// matches dark skin warning color.
public static readonly Color kClipErrorColor = new Color(0.957f, 0.737f, 0.008f, 1f);
// TODO: Make skinnable? If we do, we should probably also make the associated cursors skinnable...
public static readonly Color kMixToolColor = Color.white;
public static readonly Color kRippleToolColor = new Color(255f / 255f, 210f / 255f, 51f / 255f);
public static readonly Color kReplaceToolColor = new Color(165f / 255f, 30f / 255f, 30f / 255f);
public const string markerDefaultStyle = "MarkerItem";
public GUIStyle groupBackground;
public GUIStyle displayBackground;
public GUIStyle fontClip;
public GUIStyle fontClipLoop;
public GUIStyle trackHeaderFont;
public GUIStyle trackGroupAddButton;
public GUIStyle groupFont;
public GUIStyle timeCursor;
public GUIStyle endmarker;
public GUIStyle tinyFont;
public GUIStyle foldout;
public GUIStyle trackMuteButton;
public GUIStyle trackLockButton;
public GUIStyle trackRecordButton;
public GUIStyle playTimeRangeStart;
public GUIStyle playTimeRangeEnd;
public GUIStyle selectedStyle;
public GUIStyle trackSwatchStyle;
public GUIStyle connector;
public GUIStyle keyframe;
public GUIStyle warning;
public GUIStyle extrapolationHold;
public GUIStyle extrapolationLoop;
public GUIStyle extrapolationPingPong;
public GUIStyle extrapolationContinue;
public GUIStyle trackMarkerButton;
public GUIStyle markerMultiOverlay;
public GUIStyle bottomShadow;
public GUIStyle trackOptions;
public GUIStyle infiniteTrack;
public GUIStyle clipOut;
public GUIStyle clipIn;
public GUIStyle trackCurvesButton;
public GUIStyle trackLockOverlay;
public GUIStyle activation;
public GUIStyle playrange;
public GUIStyle timelineLockButton;
public GUIStyle trackAvatarMaskButton;
public GUIStyle markerWarning;
public GUIStyle editModeBtn;
public GUIStyle showMarkersBtn;
public GUIStyle sequenceSwitcher;
public GUIStyle inlineCurveHandle;
public GUIStyle timeReferenceButton;
public GUIStyle trackButtonSuite;
public GUIStyle previewButtonDisabled;
static internal DirectorStyles s_Instance;
DirectorNamedColor m_DarkSkinColors;
DirectorNamedColor m_LightSkinColors;
DirectorNamedColor m_DefaultSkinColors;
const string k_DarkSkinPath = resourcesPath + "Timeline_DarkSkin.txt";
const string k_LightSkinPath = resourcesPath + "Timeline_LightSkin.txt";
static readonly GUIContent s_TempContent = new GUIContent();
public static bool IsInitialized
{
get { return s_Instance != null; }
}
public static DirectorStyles Instance
{
get
{
if (s_Instance == null)
{
s_Instance = new DirectorStyles();
s_Instance.Initialize();
}
return s_Instance;
}
}
public static void ReloadStylesIfNeeded()
{
if (Instance.ShouldLoadStyles())
{
Instance.LoadStyles();
if (!Instance.ShouldLoadStyles())
Instance.Initialize();
}
}
public DirectorNamedColor customSkin
{
get { return EditorGUIUtility.isProSkin ? m_DarkSkinColors : m_LightSkinColors; }
internal set
{
if (EditorGUIUtility.isProSkin)
m_DarkSkinColors = value;
else
m_LightSkinColors = value;
}
}
DirectorNamedColor LoadColorSkin(string path)
{
var asset = EditorGUIUtility.LoadRequired(path) as TextAsset;
if (asset != null && !string.IsNullOrEmpty(asset.text))
{
return DirectorNamedColor.CreateAndLoadFromText(asset.text);
}
return m_DefaultSkinColors;
}
static DirectorNamedColor CreateDefaultSkin()
{
var nc = ScriptableObject.CreateInstance<DirectorNamedColor>();
nc.SetDefault();
return nc;
}
public void ExportSkinToFile()
{
if (customSkin == m_DarkSkinColors)
customSkin.ToText(k_DarkSkinPath);
if (customSkin == m_LightSkinColors)
customSkin.ToText(k_LightSkinPath);
}
public void ReloadSkin()
{
if (customSkin == m_DarkSkinColors)
{
m_DarkSkinColors = LoadColorSkin(k_DarkSkinPath);
}
else if (customSkin == m_LightSkinColors)
{
m_LightSkinColors = LoadColorSkin(k_LightSkinPath);
}
}
public void Initialize()
{
m_DefaultSkinColors = CreateDefaultSkin();
m_DarkSkinColors = LoadColorSkin(k_DarkSkinPath);
m_LightSkinColors = LoadColorSkin(k_LightSkinPath);
// add the built in colors (control track uses attribute)
TrackResourceCache.ClearTrackColorCache();
TrackResourceCache.SetTrackColor<AnimationTrack>(customSkin.colorAnimation);
TrackResourceCache.SetTrackColor<PlayableTrack>(Color.white);
TrackResourceCache.SetTrackColor<AudioTrack>(customSkin.colorAudio);
TrackResourceCache.SetTrackColor<ActivationTrack>(customSkin.colorActivation);
TrackResourceCache.SetTrackColor<GroupTrack>(customSkin.colorGroup);
TrackResourceCache.SetTrackColor<ControlTrack>(customSkin.colorControl);
// add default icons
TrackResourceCache.ClearTrackIconCache();
TrackResourceCache.SetTrackIcon<AnimationTrack>(animationTrackIcon);
TrackResourceCache.SetTrackIcon<AudioTrack>(audioTrackIcon);
TrackResourceCache.SetTrackIcon<PlayableTrack>(playableTrackIcon);
TrackResourceCache.SetTrackIcon<ActivationTrack>(new GUIContent(GetBackgroundImage(activation)));
TrackResourceCache.SetTrackIcon<SignalTrack>(signalTrackIcon);
}
DirectorStyles()
{
LoadStyles();
}
bool ShouldLoadStyles()
{
return endmarker == null ||
endmarker.name == GUISkin.error.name;
}
void LoadStyles()
{
endmarker = GetGUIStyle("Icon-Endmarker");
groupBackground = GetGUIStyle("groupBackground");
displayBackground = GetGUIStyle("sequenceClip");
fontClip = GetGUIStyle("Font-Clip");
trackHeaderFont = GetGUIStyle("sequenceTrackHeaderFont");
trackGroupAddButton = GetGUIStyle("sequenceTrackGroupAddButton");
groupFont = GetGUIStyle("sequenceGroupFont");
timeCursor = GetGUIStyle("Icon-TimeCursor");
tinyFont = GetGUIStyle("tinyFont");
foldout = GetGUIStyle("Icon-Foldout");
trackMuteButton = GetGUIStyle("trackMuteButton");
trackLockButton = GetGUIStyle("trackLockButton");
trackRecordButton = GetGUIStyle("trackRecordButton");
playTimeRangeStart = GetGUIStyle("Icon-PlayAreaStart");
playTimeRangeEnd = GetGUIStyle("Icon-PlayAreaEnd");
selectedStyle = GetGUIStyle("Color-Selected");
trackSwatchStyle = GetGUIStyle("Icon-TrackHeaderSwatch");
connector = GetGUIStyle("Icon-Connector");
keyframe = GetGUIStyle("Icon-Keyframe");
warning = GetGUIStyle("Icon-Warning");
extrapolationHold = GetGUIStyle("Icon-ExtrapolationHold");
extrapolationLoop = GetGUIStyle("Icon-ExtrapolationLoop");
extrapolationPingPong = GetGUIStyle("Icon-ExtrapolationPingPong");
extrapolationContinue = GetGUIStyle("Icon-ExtrapolationContinue");
bottomShadow = GetGUIStyle("Icon-Shadow");
trackOptions = GetGUIStyle("Icon-TrackOptions");
infiniteTrack = GetGUIStyle("Icon-InfiniteTrack");
clipOut = GetGUIStyle("Icon-ClipOut");
clipIn = GetGUIStyle("Icon-ClipIn");
trackCurvesButton = GetGUIStyle("trackCurvesButton");
trackLockOverlay = GetGUIStyle("trackLockOverlay");
activation = GetGUIStyle("Icon-Activation");
playrange = GetGUIStyle("Icon-Playrange");
timelineLockButton = GetGUIStyle("IN LockButton");
trackAvatarMaskButton = GetGUIStyle("trackAvatarMaskButton");
trackMarkerButton = GetGUIStyle("trackCollapseMarkerButton");
markerMultiOverlay = GetGUIStyle("MarkerMultiOverlay");
editModeBtn = GetGUIStyle("editModeBtn");
showMarkersBtn = GetGUIStyle("showMarkerBtn");
markerWarning = GetGUIStyle("markerWarningOverlay");
sequenceSwitcher = GetGUIStyle("sequenceSwitcher");
inlineCurveHandle = GetGUIStyle("RL DragHandle");
timeReferenceButton = GetGUIStyle("timeReferenceButton");
trackButtonSuite = GetGUIStyle("trackButtonSuite");
previewButtonDisabled = GetGUIStyle("previewButtonDisabled");
playrangeContent = new GUIContent(GetBackgroundImage(playrange)) { tooltip = L10n.Tr("Toggle play range markers.") };
fontClipLoop = new GUIStyle(fontClip) { fontStyle = FontStyle.Bold };
}
public static GUIStyle GetGUIStyle(string s)
{
return EditorStyles.FromUSS(s);
}
public static GUIContent TrIconContent(string iconName, string tooltip = null)
{
return L10n.IconContent(iconName == null ? null : ResolveIcon(iconName), tooltip);
}
public static GUIContent IconContent(string iconName)
{
return EditorGUIUtility.IconContent(iconName == null ? null : ResolveIcon(iconName));
}
public static GUIContent TrTextContentWithIcon(string text, string tooltip, string iconName)
{
return L10n.TextContentWithIcon(text, tooltip, iconName == null ? null : ResolveIcon(iconName));
}
public static GUIContent TrTextContent(string text, string tooltip = null)
{
return L10n.TextContent(text, tooltip);
}
public static Texture2D LoadIcon(string iconName)
{
return EditorGUIUtility.LoadIconRequired(iconName == null ? null : ResolveIcon(iconName));
}
static string ResolveIcon(string icon)
{
return string.Format(k_ImagePath, icon);
}
public static string Elipsify(string label, Rect rect, GUIStyle style)
{
var ret = label;
if (label.Length == 0)
return ret;
s_TempContent.text = label;
float neededWidth = style.CalcSize(s_TempContent).x;
return Elipsify(label, rect.width, neededWidth);
}
public static string Elipsify(string label, float destinationWidth, float neededWidth)
{
var ret = label;
if (label.Length == 0)
return ret;
if (destinationWidth < neededWidth)
{
float averageWidthOfOneChar = neededWidth / label.Length;
int floor = Mathf.Max((int)Mathf.Floor(destinationWidth / averageWidthOfOneChar), 0);
if (floor < k_Elipsis.Length)
ret = string.Empty;
else if (floor == k_Elipsis.Length)
ret = k_Elipsis;
else if (floor < label.Length)
ret = label.Substring(0, floor - k_Elipsis.Length) + k_Elipsis;
}
return ret;
}
public static Texture2D GetBackgroundImage(GUIStyle style, StyleState state = StyleState.normal)
{
var blockName = GUIStyleExtensions.StyleNameToBlockName(style.name, false);
var styleBlock = EditorResources.GetStyle(blockName, state);
return styleBlock.GetTexture(StyleCatalogKeyword.backgroundImage);
}
}
}

View File

@@ -0,0 +1,151 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
static class AnimatedParameterExtensions
{
public static bool HasAnyAnimatableParameters(this ICurvesOwner curvesOwner)
{
return AnimatedParameterUtility.HasAnyAnimatableParameters(curvesOwner.asset);
}
public static IEnumerable<SerializedProperty> GetAllAnimatableParameters(this ICurvesOwner curvesOwner)
{
return AnimatedParameterUtility.GetAllAnimatableParameters(curvesOwner.asset);
}
public static bool IsParameterAnimatable(this ICurvesOwner curvesOwner, string parameterName)
{
return AnimatedParameterUtility.IsParameterAnimatable(curvesOwner.asset, parameterName);
}
public static bool IsParameterAnimated(this ICurvesOwner curvesOwner, string parameterName)
{
return AnimatedParameterUtility.IsParameterAnimated(curvesOwner.asset, curvesOwner.curves, parameterName);
}
public static EditorCurveBinding GetCurveBinding(this ICurvesOwner curvesOwner, string parameterName)
{
return AnimatedParameterUtility.GetCurveBinding(curvesOwner.asset, parameterName);
}
public static string GetUniqueRecordedClipName(this ICurvesOwner curvesOwner)
{
return AnimationTrackRecorder.GetUniqueRecordedClipName(curvesOwner.assetOwner, curvesOwner.defaultCurvesName);
}
public static AnimationCurve GetAnimatedParameter(this ICurvesOwner curvesOwner, string bindingName)
{
return AnimatedParameterUtility.GetAnimatedParameter(curvesOwner.asset, curvesOwner.curves, bindingName);
}
public static bool AddAnimatedParameterValueAt(this ICurvesOwner curvesOwner, string parameterName, float value, float time)
{
if (!curvesOwner.IsParameterAnimatable(parameterName))
return false;
if (curvesOwner.curves == null)
curvesOwner.CreateCurves(curvesOwner.GetUniqueRecordedClipName());
var binding = curvesOwner.GetCurveBinding(parameterName);
var curve = AnimationUtility.GetEditorCurve(curvesOwner.curves, binding) ?? new AnimationCurve();
var serializedObject = AnimatedParameterUtility.GetSerializedPlayableAsset(curvesOwner.asset);
var property = serializedObject.FindProperty(parameterName);
bool isStepped = property.propertyType == SerializedPropertyType.Boolean ||
property.propertyType == SerializedPropertyType.Integer ||
property.propertyType == SerializedPropertyType.Enum;
TimelineUndo.PushUndo(curvesOwner.curves, "Set Key");
CurveEditUtility.AddKeyFrameToCurve(curve, time, curvesOwner.curves.frameRate, value, isStepped);
AnimationUtility.SetEditorCurve(curvesOwner.curves, binding, curve);
return true;
}
public static void SanitizeCurvesData(this ICurvesOwner curvesOwner)
{
var curves = curvesOwner.curves;
if (curves == null)
return;
// Remove any 0-length curves
foreach (var binding in AnimationUtility.GetCurveBindings(curves))
{
var curve = AnimationUtility.GetEditorCurve(curves, binding);
if (curve.length == 0)
AnimationUtility.SetEditorCurve(curves, binding, null);
}
// If no curves remain, delete the curves asset
if (curves.empty)
{
var track = curvesOwner.targetTrack;
var timeline = track != null ? track.timelineAsset : null;
TimelineUndo.PushDestroyUndo(timeline, track, curves);
}
}
public static bool AddAnimatedParameter(this ICurvesOwner curvesOwner, string parameterName)
{
var newBinding = new EditorCurveBinding();
SerializedProperty property;
if (!InternalAddParameter(curvesOwner, parameterName, ref newBinding, out property))
return false;
var duration = (float)curvesOwner.duration;
CurveEditUtility.AddKey(curvesOwner.curves, newBinding, property, 0);
CurveEditUtility.AddKey(curvesOwner.curves, newBinding, property, duration);
return true;
}
public static bool RemoveAnimatedParameter(this ICurvesOwner curvesOwner, string parameterName)
{
if (!curvesOwner.IsParameterAnimated(parameterName) || curvesOwner.curves == null)
return false;
var binding = curvesOwner.GetCurveBinding(parameterName);
AnimationUtility.SetEditorCurve(curvesOwner.curves, binding, null);
return true;
}
// Set an animated parameter. Requires the field identifier 'position.x', but will add default curves to all fields
public static bool SetAnimatedParameter(this ICurvesOwner curvesOwner, string parameterName, AnimationCurve curve)
{
// this will add a basic curve for all the related parameters
if (!curvesOwner.IsParameterAnimated(parameterName) && !curvesOwner.AddAnimatedParameter(parameterName))
return false;
var binding = curvesOwner.GetCurveBinding(parameterName);
AnimationUtility.SetEditorCurve(curvesOwner.curves, binding, curve);
return true;
}
static bool InternalAddParameter([NotNull] ICurvesOwner curvesOwner, string parameterName, ref EditorCurveBinding binding, out SerializedProperty property)
{
property = null;
if (curvesOwner.IsParameterAnimated(parameterName))
return false;
var serializedObject = AnimatedParameterUtility.GetSerializedPlayableAsset(curvesOwner.asset);
if (serializedObject == null)
return false;
property = serializedObject.FindProperty(parameterName);
if (property == null || !AnimatedParameterUtility.IsTypeAnimatable(property.propertyType))
return false;
if (curvesOwner.curves == null)
curvesOwner.CreateCurves(curvesOwner.GetUniqueRecordedClipName());
binding = curvesOwner.GetCurveBinding(parameterName);
return true;
}
}
}

View File

@@ -0,0 +1,206 @@
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
/// <summary>
/// Extension Methods for AnimationTracks that require the Unity Editor, and may require the Timeline containing the Animation Track to be currently loaded in the Timeline Editor Window.
/// </summary>
public static class AnimationTrackExtensions
{
/// <summary>
/// Determines whether the Timeline window can enable recording mode on an AnimationTrack.
/// For a track to support recording, it needs to have a valid scene binding,
/// its offset mode should not be Auto and needs to be currently visible in the Timeline Window.
/// </summary>
/// <param name="track">The track to query.</param>
/// <returns>True if recording can start, False otherwise.</returns>
public static bool CanStartRecording(this AnimationTrack track)
{
if (track == null)
{
throw new ArgumentNullException(nameof(track));
}
if (TimelineEditor.state == null)
{
return false;
}
var director = TimelineEditor.inspectedDirector;
var animTrack = TimelineUtility.GetSceneReferenceTrack(track) as AnimationTrack;
return animTrack != null && animTrack.trackOffset != TrackOffset.Auto &&
TimelineEditor.inspectedAsset == animTrack.timelineAsset &&
director != null && TimelineUtility.GetSceneGameObject(director, animTrack) != null;
}
/// <summary>
/// Method that allows querying if a track is current enabled for animation recording.
/// </summary>
/// <param name="track">The track to query.</param>
/// <returns>True if currently recording and False otherwise.</returns>
public static bool IsRecording(this AnimationTrack track)
{
if (track == null)
{
throw new ArgumentNullException(nameof(track));
}
return TimelineEditor.state != null && TimelineEditor.state.IsArmedForRecord(track);
}
/// <summary>
/// Method that enables animation recording for an AnimationTrack.
/// </summary>
/// <param name="track">The AnimationTrack which will be put in recording mode.</param>
/// <returns>True if track was put successfully in recording mode, False otherwise. </returns>
public static bool StartRecording(this AnimationTrack track)
{
if (!CanStartRecording(track))
{
return false;
}
TimelineEditor.state.ArmForRecord(track);
return true;
}
/// <summary>
/// Disables recording mode of an AnimationTrack.
/// </summary>
/// <param name="track">The AnimationTrack which will be taken out of recording mode.</param>
public static void StopRecording(this AnimationTrack track)
{
if (!IsRecording(track) || TimelineEditor.state == null)
{
return;
}
TimelineEditor.state.UnarmForRecord(track);
}
internal static void ConvertToClipMode(this AnimationTrack track)
{
if (!track.CanConvertToClipMode())
return;
UndoExtensions.RegisterTrack(track, L10n.Tr("Convert To Clip"));
if (!track.infiniteClip.empty)
{
var animClip = track.infiniteClip;
TimelineUndo.PushUndo(animClip, L10n.Tr("Convert To Clip"));
UndoExtensions.RegisterTrack(track, L10n.Tr("Convert To Clip"));
var start = AnimationClipCurveCache.Instance.GetCurveInfo(animClip).keyTimes.FirstOrDefault();
animClip.ShiftBySeconds(-start);
track.infiniteClip = null;
var clip = track.CreateClip(animClip);
clip.start = start;
clip.preExtrapolationMode = track.infiniteClipPreExtrapolation;
clip.postExtrapolationMode = track.infiniteClipPostExtrapolation;
clip.recordable = true;
if (Mathf.Abs(animClip.length) < TimelineClip.kMinDuration)
{
clip.duration = 1;
}
var animationAsset = clip.asset as AnimationPlayableAsset;
if (animationAsset)
{
animationAsset.position = track.infiniteClipOffsetPosition;
animationAsset.eulerAngles = track.infiniteClipOffsetEulerAngles;
// going to / from infinite mode should reset this. infinite mode
animationAsset.removeStartOffset = track.infiniteClipRemoveOffset;
animationAsset.applyFootIK = track.infiniteClipApplyFootIK;
animationAsset.loop = track.infiniteClipLoop;
track.infiniteClipOffsetPosition = Vector3.zero;
track.infiniteClipOffsetEulerAngles = Vector3.zero;
}
track.CalculateExtrapolationTimes();
}
track.infiniteClip = null;
EditorUtility.SetDirty(track);
}
internal static void ConvertFromClipMode(this AnimationTrack track, TimelineAsset timeline)
{
if (!track.CanConvertFromClipMode())
return;
UndoExtensions.RegisterTrack(track, L10n.Tr("Convert From Clip"));
var clip = track.clips[0];
var delta = (float)clip.start;
track.infiniteClipTimeOffset = 0.0f;
track.infiniteClipPreExtrapolation = clip.preExtrapolationMode;
track.infiniteClipPostExtrapolation = clip.postExtrapolationMode;
var animAsset = clip.asset as AnimationPlayableAsset;
if (animAsset)
{
track.infiniteClipOffsetPosition = animAsset.position;
track.infiniteClipOffsetEulerAngles = animAsset.eulerAngles;
track.infiniteClipRemoveOffset = animAsset.removeStartOffset;
track.infiniteClipApplyFootIK = animAsset.applyFootIK;
track.infiniteClipLoop = animAsset.loop;
}
// clone it, it may not be in the same asset
var animClip = clip.animationClip;
float scale = (float)clip.timeScale;
if (!Mathf.Approximately(scale, 1.0f))
{
if (!Mathf.Approximately(scale, 0.0f))
scale = 1.0f / scale;
animClip.ScaleTime(scale);
}
TimelineUndo.PushUndo(animClip, L10n.Tr("Convert From Clip"));
animClip.ShiftBySeconds(delta);
// manually delete the clip
var asset = clip.asset;
clip.asset = null;
// Remove the clip, remove old assets
ClipModifier.Delete(timeline, clip);
TimelineUndo.PushDestroyUndo(null, track, asset);
track.infiniteClip = animClip;
EditorUtility.SetDirty(track);
}
internal static bool CanConvertToClipMode(this AnimationTrack track)
{
if (track == null || track.inClipMode)
return false;
return (track.infiniteClip != null && !track.infiniteClip.empty);
}
// Requirements to go from clip mode
// - one clip, recordable, and animation clip belongs to the same asset as the track
internal static bool CanConvertFromClipMode(this AnimationTrack track)
{
if ((track == null) ||
(!track.inClipMode) ||
(track.clips.Length != 1) ||
(track.clips[0].start < 0) ||
(!track.clips[0].recordable))
return false;
var asset = track.clips[0].asset as AnimationPlayableAsset;
if (asset == null)
return false;
return TimelineHelpers.HaveSameContainerAsset(track, asset.clip);
}
}
}

View File

@@ -0,0 +1,524 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
using UnityEngine.Playables;
namespace UnityEditor.Timeline
{
/// <summary>
/// Extension Methods for Tracks that require the Unity Editor, and may require the Timeline containing the Track to be currently loaded in the Timeline Editor Window.
/// </summary>
public static class TrackExtensions
{
static readonly double kMinOverlapTime = TimeUtility.kTimeEpsilon * 1000;
/// <summary>
/// Queries whether the children of the Track are currently visible in the Timeline Editor.
/// </summary>
/// <param name="track">The track asset to query.</param>
/// <returns>True if the track is collapsed and false otherwise.</returns>
public static bool IsCollapsed(this TrackAsset track)
{
return TimelineWindowViewPrefs.IsTrackCollapsed(track);
}
/// <summary>
/// Sets whether the children of the Track are currently visible in the Timeline Editor.
/// </summary>
/// <param name="track">The track asset to collapsed state to modify.</param>
/// <param name="collapsed">`true` to collapse children, false otherwise.</param>
/// <remarks> The track collapsed state is not serialized inside the asset and is lost from one checkout of the project to another. </remarks>>
public static void SetCollapsed(this TrackAsset track, bool collapsed)
{
TimelineWindowViewPrefs.SetTrackCollapsed(track, collapsed);
}
/// <summary>
/// Queries whether any parent of the track is collapsed, rendering the track not visible to the user.
/// </summary>
/// <param name="track">The track asset to query.</param>
/// <returns>True if all parents are not collapsed, false otherwise.</returns>
public static bool IsVisibleInHierarchy(this TrackAsset track)
{
var t = track;
while ((t = t.parent as TrackAsset) != null)
{
if (t.IsCollapsed())
return false;
}
return true;
}
internal static AnimationClip GetOrCreateClip(this AnimationTrack track)
{
if (track.infiniteClip == null && !track.inClipMode)
track.CreateInfiniteClip(AnimationTrackRecorder.GetUniqueRecordedClipName(track, AnimationTrackRecorder.kRecordClipDefaultName));
return track.infiniteClip;
}
internal static TimelineClip CreateClip(this TrackAsset track, double time)
{
var attr = track.GetType().GetCustomAttributes(typeof(TrackClipTypeAttribute), true);
if (attr.Length == 0)
return null;
if (TimelineWindow.instance.state == null)
return null;
if (attr.Length == 1)
{
var clipClass = (TrackClipTypeAttribute)attr[0];
var clip = TimelineHelpers.CreateClipOnTrack(clipClass.inspectedType, track, time);
return clip;
}
return null;
}
static bool Overlaps(TimelineClip blendOut, TimelineClip blendIn)
{
if (blendIn == blendOut)
return false;
if (Math.Abs(blendIn.start - blendOut.start) < TimeUtility.kTimeEpsilon)
{
return blendIn.duration >= blendOut.duration;
}
return blendIn.start >= blendOut.start && blendIn.start < blendOut.end;
}
internal static void ComputeBlendsFromOverlaps(this TrackAsset asset)
{
ComputeBlendsFromOverlaps(asset.clips);
}
internal static void ComputeBlendsFromOverlaps(TimelineClip[] clips)
{
foreach (var clip in clips)
{
clip.blendInDuration = -1;
clip.blendOutDuration = -1;
}
Array.Sort(clips, (c1, c2) =>
Math.Abs(c1.start - c2.start) < TimeUtility.kTimeEpsilon ? c1.duration.CompareTo(c2.duration) : c1.start.CompareTo(c2.start));
for (var i = 0; i < clips.Length; i++)
{
var clip = clips[i];
if (!clip.SupportsBlending())
continue;
var blendIn = clip;
TimelineClip blendOut = null;
var blendOutCandidate = clips[Math.Max(i - 1, 0)];
if (Overlaps(blendOutCandidate, blendIn))
blendOut = blendOutCandidate;
if (blendOut != null)
{
UpdateClipIntersection(blendOut, blendIn);
}
}
}
static void UpdateClipIntersection(TimelineClip blendOutClip, TimelineClip blendInClip)
{
if (!blendOutClip.SupportsBlending() || !blendInClip.SupportsBlending())
return;
if (blendInClip.start - blendOutClip.start < blendOutClip.duration - blendInClip.duration)
return;
double duration = Math.Max(0, blendOutClip.start + blendOutClip.duration - blendInClip.start);
duration = duration <= kMinOverlapTime ? 0 : duration;
blendOutClip.blendOutDuration = duration;
blendInClip.blendInDuration = duration;
var blendInMode = blendInClip.blendInCurveMode;
var blendOutMode = blendOutClip.blendOutCurveMode;
if (blendInMode == TimelineClip.BlendCurveMode.Manual && blendOutMode == TimelineClip.BlendCurveMode.Auto)
{
blendOutClip.mixOutCurve = CurveEditUtility.CreateMatchingCurve(blendInClip.mixInCurve);
}
else if (blendInMode == TimelineClip.BlendCurveMode.Auto && blendOutMode == TimelineClip.BlendCurveMode.Manual)
{
blendInClip.mixInCurve = CurveEditUtility.CreateMatchingCurve(blendOutClip.mixOutCurve);
}
else if (blendInMode == TimelineClip.BlendCurveMode.Auto && blendOutMode == TimelineClip.BlendCurveMode.Auto)
{
blendInClip.mixInCurve = null; // resets to default curves
blendOutClip.mixOutCurve = null;
}
}
static void RecursiveSubtrackClone(TrackAsset source, TrackAsset duplicate, IExposedPropertyTable sourceTable, IExposedPropertyTable destTable, PlayableAsset assetOwner)
{
var subtracks = source.GetChildTracks();
foreach (var sub in subtracks)
{
var newSub = TimelineHelpers.Clone(duplicate, sub, sourceTable, destTable, assetOwner);
duplicate.AddChild(newSub);
RecursiveSubtrackClone(sub, newSub, sourceTable, destTable, assetOwner);
// Call the custom editor on Create
var customEditor = CustomTimelineEditorCache.GetTrackEditor(newSub);
customEditor.OnCreate_Safe(newSub, sub);
// registration has to happen AFTER recursion
TimelineCreateUtilities.SaveAssetIntoObject(newSub, assetOwner);
TimelineUndo.RegisterCreatedObjectUndo(newSub, L10n.Tr("Duplicate"));
}
}
internal static TrackAsset Duplicate(this TrackAsset track, IExposedPropertyTable sourceTable, IExposedPropertyTable destTable,
TimelineAsset destinationTimeline = null)
{
if (track == null)
return null;
// if the destination is us, clear to avoid bad parenting (case 919421)
if (destinationTimeline == track.timelineAsset)
destinationTimeline = null;
var timelineParent = track.parent as TimelineAsset;
var trackParent = track.parent as TrackAsset;
if (timelineParent == null && trackParent == null)
{
Debug.LogWarning("Cannot duplicate track because it is not parented to known type");
return null;
}
// Determine who the final parent is. If we are pasting into another track, it's always the timeline.
// Otherwise it's the original parent
PlayableAsset finalParent = destinationTimeline != null ? destinationTimeline : track.parent;
// grab the list of tracks to generate a name from (923360) to get the list of names
// no need to do this part recursively
var finalTrackParent = finalParent as TrackAsset;
var finalTimelineAsset = finalParent as TimelineAsset;
var otherTracks = (finalTimelineAsset != null) ? finalTimelineAsset.trackObjects : finalTrackParent.subTracksObjects;
// Important to create the new objects before pushing the original undo, or redo breaks the
// sequence
var newTrack = TimelineHelpers.Clone(finalParent, track, sourceTable, destTable, finalParent);
newTrack.name = TimelineCreateUtilities.GenerateUniqueActorName(otherTracks, newTrack.name);
RecursiveSubtrackClone(track, newTrack, sourceTable, destTable, finalParent);
TimelineCreateUtilities.SaveAssetIntoObject(newTrack, finalParent);
TimelineUndo.RegisterCreatedObjectUndo(newTrack, L10n.Tr("Duplicate"));
UndoExtensions.RegisterPlayableAsset(finalParent, L10n.Tr("Duplicate"));
if (destinationTimeline != null) // other timeline
destinationTimeline.AddTrackInternal(newTrack);
else if (timelineParent != null) // this timeline, no parent
ReparentTracks(new List<TrackAsset> { newTrack }, timelineParent, timelineParent.GetRootTracks().Last(), false);
else // this timeline, with parent
trackParent.AddChild(newTrack);
// Call the custom editor. this check prevents the call when copying to the clipboard
if (destinationTimeline == null || destinationTimeline == TimelineEditor.inspectedAsset)
{
var customEditor = CustomTimelineEditorCache.GetTrackEditor(newTrack);
customEditor.OnCreate_Safe(newTrack, track);
}
return newTrack;
}
// Reparents a list of tracks to a new parent
// the new parent cannot be null (has to be track asset or sequence)
// the insertAfter can be null (will not reorder)
internal static bool ReparentTracks(List<TrackAsset> tracksToMove, PlayableAsset targetParent,
TrackAsset insertMarker = null, bool insertBefore = false)
{
var targetParentTrack = targetParent as TrackAsset;
var targetSequenceTrack = targetParent as TimelineAsset;
if (tracksToMove == null || tracksToMove.Count == 0 || (targetParentTrack == null && targetSequenceTrack == null))
return false;
// invalid parent type on a track
if (targetParentTrack != null && tracksToMove.Any(x => !TimelineCreateUtilities.ValidateParentTrack(targetParentTrack, x.GetType())))
return false;
// no valid tracks means this is simply a rearrangement
var validTracks = tracksToMove.Where(x => x.parent != targetParent).ToList();
if (insertMarker == null && !validTracks.Any())
return false;
var parents = validTracks.Select(x => x.parent).Where(x => x != null).Distinct().ToList();
// push the current state of the tracks that will change
foreach (var p in parents)
{
UndoExtensions.RegisterPlayableAsset(p, "Reparent");
}
UndoExtensions.RegisterTracks(validTracks, "Reparent");
UndoExtensions.RegisterPlayableAsset(targetParent, "Reparent");
// need to reparent tracks first, before moving them.
foreach (var t in validTracks)
{
if (t.parent != targetParent)
{
TrackAsset toMoveParent = t.parent as TrackAsset;
TimelineAsset toMoveTimeline = t.parent as TimelineAsset;
if (toMoveTimeline != null)
{
toMoveTimeline.RemoveTrack(t);
}
else if (toMoveParent != null)
{
toMoveParent.RemoveSubTrack(t);
}
if (targetParentTrack != null)
{
targetParentTrack.AddChild(t);
targetParentTrack.SetCollapsed(false);
}
else
{
targetSequenceTrack.AddTrackInternal(t);
}
}
}
if (insertMarker != null)
{
// re-ordering track. This is using internal APIs, so invalidation of the tracks must be done manually to avoid
// cache mismatches
var children = targetParentTrack != null ? targetParentTrack.subTracksObjects : targetSequenceTrack.trackObjects;
TimelineUtility.ReorderTracks(children, tracksToMove, insertMarker, insertBefore);
if (targetParentTrack != null)
targetParentTrack.Invalidate();
if (insertMarker.timelineAsset != null)
insertMarker.timelineAsset.Invalidate();
}
return true;
}
internal static IEnumerable<TrackAsset> FilterTracks(IEnumerable<TrackAsset> tracks)
{
var nTracks = tracks.Count();
// Duplicate is recursive. If should not have parent and child in the list
var hash = new HashSet<TrackAsset>(tracks);
var take = new Dictionary<TrackAsset, bool>(nTracks);
foreach (var track in tracks)
{
var parent = track.parent as TrackAsset;
var foundParent = false;
// go up the hierarchy
while (parent != null && !foundParent)
{
if (hash.Contains(parent))
{
foundParent = true;
}
parent = parent.parent as TrackAsset;
}
take[track] = !foundParent;
}
foreach (var track in tracks)
{
if (take[track])
yield return track;
}
}
internal static bool GetShowMarkers(this TrackAsset track)
{
return TimelineWindowViewPrefs.IsShowMarkers(track);
}
internal static void SetShowMarkers(this TrackAsset track, bool collapsed)
{
TimelineWindowViewPrefs.SetTrackShowMarkers(track, collapsed);
}
internal static bool GetShowInlineCurves(this TrackAsset track)
{
return TimelineWindowViewPrefs.GetShowInlineCurves(track);
}
internal static void SetShowInlineCurves(this TrackAsset track, bool inlineOn)
{
TimelineWindowViewPrefs.SetShowInlineCurves(track, inlineOn);
}
internal static bool ShouldShowInfiniteClipEditor(this AnimationTrack track)
{
return track != null && !track.inClipMode && track.infiniteClip != null;
}
// Special method to remove a track that is in a broken state. i.e. the script won't load
internal static bool RemoveBrokenTrack(PlayableAsset parent, ScriptableObject track)
{
var parentTrack = parent as TrackAsset;
var parentTimeline = parent as TimelineAsset;
if (parentTrack == null && parentTimeline == null)
throw new ArgumentException("parent is not a valid parent type", "parent");
// this object must be a Unity null, but not actually null;
object trackAsObject = track;
if (trackAsObject == null || track as TrackAsset != null) // yes, this is correct
throw new ArgumentException("track is not in a broken state");
// this belongs to a parent track
if (parentTrack != null)
{
int index = parentTrack.subTracksObjects.FindIndex(t => t.GetInstanceID() == track.GetInstanceID());
if (index >= 0)
{
UndoExtensions.RegisterTrack(parentTrack, L10n.Tr("Remove Track"));
parentTrack.subTracksObjects.RemoveAt(index);
parentTrack.Invalidate();
Undo.DestroyObjectImmediate(track);
return true;
}
}
else if (parentTimeline != null)
{
int index = parentTimeline.trackObjects.FindIndex(t => t.GetInstanceID() == track.GetInstanceID());
if (index >= 0)
{
UndoExtensions.RegisterPlayableAsset(parentTimeline, L10n.Tr("Remove Track"));
parentTimeline.trackObjects.RemoveAt(index);
parentTimeline.Invalidate();
Undo.DestroyObjectImmediate(track);
return true;
}
}
return false;
}
// Find the gap at the given time
// return true if there is a gap, false if there is an intersection
// endGap will be Infinity if the gap has no end
internal static bool GetGapAtTime(this TrackAsset track, double time, out double startGap, out double endGap)
{
startGap = 0;
endGap = Double.PositiveInfinity;
if (track == null || !track.GetClips().Any())
{
return false;
}
var discreteTime = new DiscreteTime(time);
track.SortClips();
var sortedByStartTime = track.clips;
for (int i = 0; i < sortedByStartTime.Length; i++)
{
var clip = sortedByStartTime[i];
// intersection
if (discreteTime >= new DiscreteTime(clip.start) && discreteTime < new DiscreteTime(clip.end))
{
endGap = time;
startGap = time;
return false;
}
if (clip.end < time)
{
startGap = clip.end;
}
if (clip.start > time)
{
endGap = clip.start;
break;
}
}
if (endGap - startGap < TimelineClip.kMinDuration)
{
startGap = time;
endGap = time;
return false;
}
return true;
}
internal static bool IsCompatibleWithClip(this TrackAsset track, TimelineClip clip)
{
if (track == null || clip == null || clip.asset == null)
return false;
return TypeUtility.GetPlayableAssetsHandledByTrack(track.GetType()).Contains(clip.asset.GetType());
}
// Get a flattened list of all child tracks
static void GetFlattenedChildTracks(this TrackAsset asset, List<TrackAsset> list)
{
if (asset == null || list == null)
return;
foreach (var track in asset.GetChildTracks())
{
list.Add(track);
GetFlattenedChildTracks(track, list);
}
}
internal static IEnumerable<TrackAsset> GetFlattenedChildTracks(this TrackAsset asset)
{
if (asset == null || !asset.GetChildTracks().Any())
return Enumerable.Empty<TrackAsset>();
var flattenedChildTracks = new List<TrackAsset>();
GetFlattenedChildTracks(asset, flattenedChildTracks);
return flattenedChildTracks;
}
internal static void UnarmForRecord(this TrackAsset track)
{
TimelineWindow.instance.state.UnarmForRecord(track);
}
internal static void SetShowTrackMarkers(this TrackAsset track, bool showMarkers)
{
var currentValue = track.GetShowMarkers();
if (currentValue != showMarkers)
{
TimelineUndo.PushUndo(TimelineWindow.instance.state.editSequence.viewModel, L10n.Tr("Toggle Show Markers"));
track.SetShowMarkers(showMarkers);
if (!showMarkers)
{
foreach (var marker in track.GetMarkers())
{
SelectionManager.Remove(marker);
}
}
}
}
internal static IEnumerable<TrackAsset> RemoveTimelineMarkerTrackFromList(this IEnumerable<TrackAsset> tracks, TimelineAsset asset)
{
return tracks.Where(t => t != asset.markerTrack);
}
internal static bool ContainsTimelineMarkerTrack(this IEnumerable<TrackAsset> tracks, TimelineAsset asset)
{
return tracks.Contains(asset.markerTrack);
}
}
}

View File

@@ -0,0 +1,145 @@
using System;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class ClipItem : IBlendable, ITrimmable
{
readonly TimelineClip m_Clip;
public TimelineClip clip
{
get { return m_Clip; }
}
public ClipItem(TimelineClip clip)
{
m_Clip = clip;
}
public TrackAsset parentTrack
{
get { return m_Clip.GetParentTrack(); }
set { m_Clip.SetParentTrack_Internal(value); }
}
public double start
{
get { return m_Clip.start; }
set { m_Clip.start = value; }
}
public double end
{
get { return m_Clip.end; }
}
public double duration
{
get { return m_Clip.duration; }
}
public bool IsCompatibleWithTrack(TrackAsset track)
{
return track.IsCompatibleWithClip(m_Clip);
}
public void PushUndo(string operation)
{
UndoExtensions.RegisterClip(m_Clip, operation);
}
public TimelineItemGUI gui
{
get { return ItemToItemGui.GetGuiForClip(m_Clip); }
}
public bool supportsBlending
{
get { return m_Clip.SupportsBlending(); }
}
public bool hasLeftBlend
{
get { return m_Clip.hasBlendIn; }
}
public bool hasRightBlend
{
get { return m_Clip.hasBlendOut; }
}
public double leftBlendDuration
{
get { return m_Clip.hasBlendIn ? m_Clip.blendInDuration : m_Clip.easeInDuration; }
}
public double rightBlendDuration
{
get { return m_Clip.hasBlendOut ? m_Clip.blendOutDuration : m_Clip.easeOutDuration; }
}
public void SetStart(double time, bool affectTimeScale)
{
ClipModifier.SetStart(m_Clip, time, affectTimeScale);
m_Clip.ConformEaseValues();
}
public void SetEnd(double time, bool affectTimeScale)
{
ClipModifier.SetEnd(m_Clip, time, affectTimeScale);
m_Clip.ConformEaseValues();
}
public void Delete()
{
EditorClipFactory.RemoveEditorClip(m_Clip);
ClipModifier.Delete(m_Clip.GetParentTrack().timelineAsset, m_Clip);
}
public void TrimStart(double time)
{
ClipModifier.TrimStart(m_Clip, time);
}
public void TrimEnd(double time)
{
ClipModifier.TrimEnd(m_Clip, time);
}
public ITimelineItem CloneTo(TrackAsset parent, double time)
{
return new ClipItem(TimelineHelpers.Clone(m_Clip, TimelineEditor.inspectedDirector, TimelineEditor.inspectedDirector, time, parent));
}
public override string ToString()
{
return m_Clip.ToString();
}
public bool Equals(ClipItem otherClip)
{
if (ReferenceEquals(null, otherClip)) return false;
if (ReferenceEquals(this, otherClip)) return true;
return Equals(m_Clip, otherClip.m_Clip);
}
public override int GetHashCode()
{
return (m_Clip != null ? m_Clip.GetHashCode() : 0);
}
public bool Equals(ITimelineItem other)
{
return Equals((object)other);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
var other = obj as ClipItem;
return other != null && Equals(other);
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
interface ITimelineItem : IEquatable<ITimelineItem>
{
double start { get; set; }
double end { get; }
double duration { get; }
TrackAsset parentTrack { get; set; }
bool IsCompatibleWithTrack(TrackAsset track);
void Delete();
ITimelineItem CloneTo(TrackAsset parent, double time);
void PushUndo(string operation);
TimelineItemGUI gui { get; }
}
interface ITrimmable : ITimelineItem
{
void SetStart(double time, bool affectTimeScale);
void SetEnd(double time, bool affectTimeScale);
void TrimStart(double time);
void TrimEnd(double time);
}
interface IBlendable : ITimelineItem
{
bool supportsBlending { get; }
bool hasLeftBlend { get; }
bool hasRightBlend { get; }
double leftBlendDuration { get; }
double rightBlendDuration { get; }
}
}

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace UnityEditor.Timeline
{
class ItemsGroup
{
readonly ITimelineItem[] m_Items;
readonly ITimelineItem m_LeftMostItem;
readonly ITimelineItem m_RightMostItem;
public ITimelineItem[] items
{
get { return m_Items; }
}
public double start
{
get { return m_LeftMostItem.start; }
set
{
var offset = value - m_LeftMostItem.start;
foreach (var clip in m_Items)
clip.start += offset;
}
}
public double end
{
get { return m_RightMostItem.end; }
}
public ITimelineItem leftMostItem
{
get { return m_LeftMostItem; }
}
public ITimelineItem rightMostItem
{
get { return m_RightMostItem; }
}
public ItemsGroup(IEnumerable<ITimelineItem> items)
{
Debug.Assert(items != null && items.Any());
m_Items = items.ToArray();
m_LeftMostItem = null;
m_RightMostItem = null;
foreach (var item in m_Items)
{
if (m_LeftMostItem == null || item.start < m_LeftMostItem.start)
m_LeftMostItem = item;
if (m_RightMostItem == null || item.end > m_RightMostItem.end)
m_RightMostItem = item;
}
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class ItemsPerTrack
{
public virtual TrackAsset targetTrack { get; }
public IEnumerable<ITimelineItem> items
{
get { return m_ItemsGroup.items; }
}
public IEnumerable<TimelineClip> clips
{
get { return m_ItemsGroup.items.OfType<ClipItem>().Select(i => i.clip); }
}
public IEnumerable<IMarker> markers
{
get { return m_ItemsGroup.items.OfType<MarkerItem>().Select(i => i.marker); }
}
public ITimelineItem leftMostItem
{
get { return m_ItemsGroup.leftMostItem; }
}
public ITimelineItem rightMostItem
{
get { return m_ItemsGroup.rightMostItem; }
}
protected readonly ItemsGroup m_ItemsGroup;
public ItemsPerTrack(TrackAsset targetTrack, IEnumerable<ITimelineItem> items)
{
this.targetTrack = targetTrack;
m_ItemsGroup = new ItemsGroup(items);
}
}
}

View File

@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
static class ItemsUtils
{
static readonly Dictionary<TimelineClip, ClipItem> s_ClipCache = new Dictionary<TimelineClip, ClipItem>();
static readonly Dictionary<IMarker, MarkerItem> s_MarkerCache = new Dictionary<IMarker, MarkerItem>();
public static IEnumerable<ItemsPerTrack> ToItemsPerTrack(this IEnumerable<ITimelineItem> items)
{
var groupedItems = items.GroupBy(c => c.parentTrack);
foreach (var group in groupedItems)
{
yield return new ItemsPerTrack(group.Key, group.ToArray());
}
}
public static ITimelineItem ToItem(this TimelineClip clip)
{
if (s_ClipCache.ContainsKey(clip))
return s_ClipCache[clip];
var ret = new ClipItem(clip);
s_ClipCache.Add(clip, ret);
return ret;
}
public static ITimelineItem ToItem(this IMarker marker)
{
if (s_MarkerCache.ContainsKey(marker))
return s_MarkerCache[marker];
var ret = new MarkerItem(marker);
s_MarkerCache.Add(marker, ret);
return ret;
}
public static IEnumerable<ITimelineItem> ToItems(this IEnumerable<TimelineClip> clips)
{
return clips.Select(ToItem);
}
public static IEnumerable<ITimelineItem> ToItems(this IEnumerable<IMarker> markers)
{
return markers.Select(ToItem);
}
public static IEnumerable<ITimelineItem> GetItems(this TrackAsset track)
{
var list = track.clips.Select(clip => (ITimelineItem) new ClipItem(clip)).ToList();
list.AddRange(track.GetMarkers().Select(marker => (ITimelineItem) new MarkerItem(marker)));
list = list.OrderBy(x => x.start).ThenBy(x => x.end).ToList();
return list;
}
public static void GetItemRange(this TrackAsset track, out double start, out double end)
{
start = 0;
end = 0;
var items = track.GetItems().ToList();
if (items.Any())
{
start = items.Min(p => p.start);
end = items.Max(p => p.end);
}
}
public static IEnumerable<ITimelineItem> GetItemsExcept(this TrackAsset track, IEnumerable<ITimelineItem> items)
{
return GetItems(track).Except(items);
}
public static IEnumerable<Type> GetItemTypes(IEnumerable<ITimelineItem> items)
{
var types = new List<Type>();
if (items.OfType<ClipItem>().Any())
types.Add(typeof(ClipItem));
if (items.OfType<MarkerItem>().Any())
types.Add(typeof(MarkerItem));
return types;
}
public static IEnumerable<Type> GetItemTypes(IEnumerable<ItemsPerTrack> itemsGroups)
{
return GetItemTypes(itemsGroups.SelectMany(i => i.items)).Distinct();
}
public static void SetItemsStartTime(IEnumerable<ItemsPerTrack> newItems, double time)
{
var startTimes = newItems.Select(d => d.items.Min(x => x.start)).ToList();
var min = startTimes.Min();
startTimes = startTimes.Select(x => x - min + time).ToList();
for (int i = 0; i < newItems.Count(); ++i)
EditModeUtils.SetStart(newItems.ElementAt(i).items, startTimes[i]);
}
public static double TimeGapBetweenItems(ITimelineItem leftItem, ITimelineItem rightItem)
{
if (leftItem is MarkerItem && rightItem is MarkerItem)
{
var markerType = ((MarkerItem)leftItem).marker.GetType();
var gap = TimeReferenceUtility.PixelToTime(StyleManager.UssStyleForType(markerType).fixedWidth) - TimeReferenceUtility.PixelToTime(0);
return gap;
}
return 0.0;
}
}
}

View File

@@ -0,0 +1,98 @@
using System;
using UnityEngine.Timeline;
using Object = UnityEngine.Object;
namespace UnityEditor.Timeline
{
class MarkerItem : ITimelineItem
{
readonly IMarker m_Marker;
public IMarker marker
{
get { return m_Marker; }
}
public MarkerItem(IMarker marker)
{
m_Marker = marker;
}
public TrackAsset parentTrack
{
get { return m_Marker.parent; }
set {}
}
public double start
{
get { return m_Marker.time; }
set { m_Marker.time = value; }
}
public double end
{
get { return m_Marker.time; }
}
public double duration
{
get { return 0.0; }
}
public bool IsCompatibleWithTrack(TrackAsset track)
{
return TypeUtility.DoesTrackSupportMarkerType(track, m_Marker.GetType());
}
public void PushUndo(string operation)
{
UndoExtensions.RegisterMarker(m_Marker, operation);
}
public TimelineItemGUI gui
{
get { return ItemToItemGui.GetGuiForMarker(m_Marker); }
}
public void Delete()
{
MarkerModifier.DeleteMarker(m_Marker);
}
public ITimelineItem CloneTo(TrackAsset parent, double time)
{
var item = new MarkerItem(MarkerModifier.CloneMarkerToParent(m_Marker, parent));
item.start = time;
return item;
}
protected bool Equals(MarkerItem otherMarker)
{
return Equals(m_Marker, otherMarker.m_Marker);
}
public override int GetHashCode()
{
return (m_Marker != null ? m_Marker.GetHashCode() : 0);
}
public override string ToString()
{
return m_Marker.ToString();
}
public bool Equals(ITimelineItem other)
{
return Equals((object)other);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
var other = obj as MarkerItem;
return other != null && Equals(other);
}
}
}

View File

@@ -0,0 +1,103 @@
#if UNITY_2020_2_OR_NEWER
[assembly: UnityEditor.Localization]
#else
using UnityEngine;
using UnityEditor;
namespace UnityEditor.Timeline
{
// dummy functions
internal static class L10n
{
public static string Tr(string str)
{
return str;
}
public static string[] Tr(string[] str_list)
{
return str_list;
}
public static string Tr(string str, string groupName)
{
return str;
}
public static string TrPath(string path)
{
return path;
}
public static GUIContent TextContent(string text, string tooltip = null, Texture icon = null)
{
return EditorGUIUtility.TrTextContent(text, tooltip, icon);
}
public static GUIContent TextContent(string text, string tooltip, string iconName)
{
return EditorGUIUtility.TrTextContent(text, tooltip, iconName);
}
public static GUIContent TextContent(string text, Texture icon)
{
return EditorGUIUtility.TrTextContent(text, icon);
}
public static GUIContent TextContentWithIcon(string text, Texture icon)
{
return EditorGUIUtility.TrTextContentWithIcon(text, icon);
}
public static GUIContent TextContentWithIcon(string text, string iconName)
{
return EditorGUIUtility.TrTextContentWithIcon(text, iconName);
}
public static GUIContent TextContentWithIcon(string text, string tooltip, string iconName)
{
return EditorGUIUtility.TrTextContentWithIcon(text, tooltip, iconName);
}
public static GUIContent TextContentWithIcon(string text, string tooltip, Texture icon)
{
return EditorGUIUtility.TrTextContentWithIcon(text, tooltip, icon);
}
public static GUIContent TextContentWithIcon(string text, string tooltip, MessageType messageType)
{
return EditorGUIUtility.TrTextContentWithIcon(text, tooltip, messageType);
}
public static GUIContent TextContentWithIcon(string text, MessageType messageType)
{
return EditorGUIUtility.TrTextContentWithIcon(text, messageType);
}
public static GUIContent IconContent(string iconName, string tooltip = null)
{
return EditorGUIUtility.TrIconContent(iconName, tooltip);
}
public static GUIContent IconContent(Texture icon, string tooltip = null)
{
return EditorGUIUtility.TrIconContent(icon, tooltip);
}
public static GUIContent TempContent(string t)
{
return EditorGUIUtility.TrTempContent(t);
}
public static GUIContent[] TempContent(string[] texts)
{
return EditorGUIUtility.TrTempContent(texts);
}
public static GUIContent[] TempContent(string[] texts, string[] tooltips)
{
return EditorGUIUtility.TrTempContent(texts, tooltips);
}
}
}
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace UnityEditor.Timeline
{
class AddDeleteItemModeMix : IAddDeleteItemMode
{
public void InsertItemsAtTime(IEnumerable<ItemsPerTrack> itemsGroups, double requestedTime)
{
ItemsUtils.SetItemsStartTime(itemsGroups, requestedTime);
EditModeMixUtils.PrepareItemsForInsertion(itemsGroups);
if (!EditModeMixUtils.CanInsert(itemsGroups))
{
var validTime = itemsGroups.Select(c => c.targetTrack).Max(parent => parent.end);
ItemsUtils.SetItemsStartTime(itemsGroups, validTime);
}
}
public void RemoveItems(IEnumerable<ItemsPerTrack> itemsGroups)
{
// Nothing
}
}
}

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace UnityEditor.Timeline
{
class AddDeleteItemModeReplace : IAddDeleteItemMode
{
public void InsertItemsAtTime(IEnumerable<ItemsPerTrack> itemsGroups, double requestedTime)
{
ItemsUtils.SetItemsStartTime(itemsGroups, requestedTime);
EditModeReplaceUtils.Insert(itemsGroups);
}
public void RemoveItems(IEnumerable<ItemsPerTrack> itemsGroups)
{
// Nothing
}
}
}

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace UnityEditor.Timeline
{
class AddDeleteItemModeRipple : IAddDeleteItemMode
{
public void InsertItemsAtTime(IEnumerable<ItemsPerTrack> itemsGroups, double requestedTime)
{
ItemsUtils.SetItemsStartTime(itemsGroups, requestedTime);
EditModeRippleUtils.Insert(itemsGroups);
}
public void RemoveItems(IEnumerable<ItemsPerTrack> itemsGroups)
{
EditModeRippleUtils.Remove(itemsGroups);
}
}
}

View File

@@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace UnityEditor.Timeline
{
interface IAddDeleteItemMode
{
void InsertItemsAtTime(IEnumerable<ItemsPerTrack> itemsGroups, double requestedTime);
void RemoveItems(IEnumerable<ItemsPerTrack> itemsGroups);
}
}

View File

@@ -0,0 +1,95 @@
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.Timeline
{
class TimelineCursors
{
public enum CursorType
{
MixBoth,
MixLeft,
MixRight,
Replace,
Ripple,
Pan
}
class CursorInfo
{
public readonly string assetPath;
public readonly Vector2 hotSpot;
public readonly MouseCursor mouseCursorType;
public CursorInfo(string assetPath, Vector2 hotSpot, MouseCursor mouseCursorType)
{
this.assetPath = assetPath;
this.hotSpot = hotSpot;
this.mouseCursorType = mouseCursorType;
}
}
const string k_CursorAssetRoot = "Cursors/";
const string k_CursorAssetsNamespace = "Timeline.";
const string k_CursorAssetExtension = ".png";
const string k_MixBothCursorAssetName = k_CursorAssetsNamespace + "MixBoth" + k_CursorAssetExtension;
const string k_MixLeftCursorAssetName = k_CursorAssetsNamespace + "MixLeft" + k_CursorAssetExtension;
const string k_MixRightCursorAssetName = k_CursorAssetsNamespace + "MixRight" + k_CursorAssetExtension;
const string k_ReplaceCursorAssetName = k_CursorAssetsNamespace + "Replace" + k_CursorAssetExtension;
const string k_RippleCursorAssetName = k_CursorAssetsNamespace + "Ripple" + k_CursorAssetExtension;
static readonly string s_PlatformPath = (Application.platform == RuntimePlatform.WindowsEditor) ? "Windows/" : "macOS/";
static readonly string s_CursorAssetDirectory = k_CursorAssetRoot + s_PlatformPath;
static readonly Dictionary<CursorType, CursorInfo> s_CursorInfoLookup = new Dictionary<CursorType, CursorInfo>
{
{CursorType.MixBoth, new CursorInfo(s_CursorAssetDirectory + k_MixBothCursorAssetName, new Vector2(16, 18), MouseCursor.CustomCursor)},
{CursorType.MixLeft, new CursorInfo(s_CursorAssetDirectory + k_MixLeftCursorAssetName, new Vector2(7, 18), MouseCursor.CustomCursor)},
{CursorType.MixRight, new CursorInfo(s_CursorAssetDirectory + k_MixRightCursorAssetName, new Vector2(25, 18), MouseCursor.CustomCursor)},
{CursorType.Replace, new CursorInfo(s_CursorAssetDirectory + k_ReplaceCursorAssetName, new Vector2(16, 28), MouseCursor.CustomCursor)},
{CursorType.Ripple, new CursorInfo(s_CursorAssetDirectory + k_RippleCursorAssetName, new Vector2(26, 19), MouseCursor.CustomCursor)},
{CursorType.Pan, new CursorInfo(null, Vector2.zero, MouseCursor.Pan)}
};
static readonly Dictionary<string, Texture2D> s_CursorAssetCache = new Dictionary<string, Texture2D>();
static CursorType? s_CurrentCursor;
public static void SetCursor(CursorType cursorType)
{
if (s_CurrentCursor.HasValue && s_CurrentCursor.Value == cursorType) return;
s_CurrentCursor = cursorType;
var cursorInfo = s_CursorInfoLookup[cursorType];
Texture2D cursorAsset = null;
if (cursorInfo.mouseCursorType == MouseCursor.CustomCursor)
{
cursorAsset = LoadCursorAsset(cursorInfo.assetPath);
}
EditorGUIUtility.SetCurrentViewCursor(cursorAsset, cursorInfo.hotSpot, cursorInfo.mouseCursorType);
}
public static void ClearCursor()
{
if (!s_CurrentCursor.HasValue) return;
EditorGUIUtility.ClearCurrentViewCursor();
s_CurrentCursor = null;
}
static Texture2D LoadCursorAsset(string assetPath)
{
if (!s_CursorAssetCache.ContainsKey(assetPath))
{
s_CursorAssetCache.Add(assetPath, (Texture2D)EditorGUIUtility.Load(assetPath));
}
return s_CursorAssetCache[assetPath];
}
}
}

View File

@@ -0,0 +1,344 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
static class EditMode
{
public enum EditType
{
None = -1,
Mix = 0,
Ripple = 1,
Replace = 2
}
interface ISubEditMode
{
IMoveItemMode moveItemMode { get; }
IMoveItemDrawer moveItemDrawer { get; }
ITrimItemMode trimItemMode { get; }
ITrimItemDrawer trimItemDrawer { get; }
IAddDeleteItemMode addDeleteItemMode { get; }
Color color { get; }
KeyCode clutchKey { get; }
void Reset();
}
class SubEditMode<TMoveMode, TTrimMode, TAddDeleteMode>: ISubEditMode
where TMoveMode : class, IMoveItemMode, IMoveItemDrawer, new()
where TTrimMode : class, ITrimItemMode, ITrimItemDrawer, new()
where TAddDeleteMode : class, IAddDeleteItemMode, new()
{
public SubEditMode(Color guiColor, KeyCode key)
{
color = guiColor;
clutchKey = key;
Reset();
}
public void Reset()
{
m_MoveItemMode = new TMoveMode();
m_TrimItemMode = new TTrimMode();
m_AddDeleteItemMode = new TAddDeleteMode();
}
TMoveMode m_MoveItemMode;
TTrimMode m_TrimItemMode;
TAddDeleteMode m_AddDeleteItemMode;
public IMoveItemMode moveItemMode { get { return m_MoveItemMode; } }
public IMoveItemDrawer moveItemDrawer { get { return m_MoveItemMode; } }
public ITrimItemMode trimItemMode { get { return m_TrimItemMode; } }
public ITrimItemDrawer trimItemDrawer { get { return m_TrimItemMode; } }
public IAddDeleteItemMode addDeleteItemMode { get { return m_AddDeleteItemMode; } }
public Color color { get; }
public KeyCode clutchKey { get; }
}
static readonly Dictionary<EditType, ISubEditMode> k_EditModes = new Dictionary<EditType, ISubEditMode>
{
{ EditType.Mix, new SubEditMode<MoveItemModeMix, TrimItemModeMix, AddDeleteItemModeMix>(DirectorStyles.kMixToolColor, KeyCode.Alpha1) },
{ EditType.Ripple, new SubEditMode<MoveItemModeRipple, TrimItemModeRipple, AddDeleteItemModeRipple>(DirectorStyles.kRippleToolColor, KeyCode.Alpha2) },
{ EditType.Replace, new SubEditMode<MoveItemModeReplace, TrimItemModeReplace, AddDeleteItemModeReplace>(DirectorStyles.kReplaceToolColor, KeyCode.Alpha3) }
};
static EditType s_CurrentEditType = EditType.Mix;
static EditType s_OverrideEditType = EditType.None;
static ITrimmable s_CurrentTrimItem;
static TrimEdge s_CurrentTrimDirection;
static MoveItemHandler s_CurrentMoveItemHandler;
static EditModeInputHandler s_InputHandler = new EditModeInputHandler();
static ITrimItemMode trimMode
{
get { return GetSubEditMode(editType).trimItemMode; }
}
static ITrimItemDrawer trimDrawer
{
get { return GetSubEditMode(editType).trimItemDrawer; }
}
static IMoveItemMode moveMode
{
get { return GetSubEditMode(editType).moveItemMode; }
}
static IMoveItemDrawer moveDrawer
{
get { return GetSubEditMode(editType).moveItemDrawer; }
}
static IAddDeleteItemMode addDeleteMode
{
get { return GetSubEditMode(editType).addDeleteItemMode; }
}
public static EditModeInputHandler inputHandler
{
get { return s_InputHandler; }
}
static Color modeColor
{
get { return GetSubEditMode(editType).color; }
}
public static EditType editType
{
get
{
if (s_OverrideEditType != EditType.None)
return s_OverrideEditType;
var window = TimelineWindow.instance;
if (window != null)
s_CurrentEditType = window.state.editType;
return s_CurrentEditType;
}
set
{
s_CurrentEditType = value;
var window = TimelineWindow.instance;
if (window != null)
window.state.editType = value;
s_OverrideEditType = EditType.None;
}
}
static ISubEditMode GetSubEditMode(EditType type)
{
var subEditMode = k_EditModes[type];
if (subEditMode != null)
return subEditMode;
Debug.LogError("Unsupported editmode type");
return null;
}
static EditType GetSubEditType(KeyCode key)
{
foreach (var subEditMode in k_EditModes)
{
if (subEditMode.Value.clutchKey == key)
return subEditMode.Key;
}
return EditType.None;
}
public static void ClearEditMode()
{
k_EditModes[editType].Reset();
}
public static void BeginTrim(ITimelineItem item, TrimEdge trimDirection)
{
var itemToTrim = item as ITrimmable;
if (itemToTrim == null) return;
s_CurrentTrimItem = itemToTrim;
s_CurrentTrimDirection = trimDirection;
trimMode.OnBeforeTrim(itemToTrim, trimDirection);
UndoExtensions.RegisterTrack(itemToTrim.parentTrack, L10n.Tr("Trim Clip"));
}
public static void TrimStart(ITimelineItem item, double time, bool affectTimeScale)
{
var itemToTrim = item as ITrimmable;
if (itemToTrim == null) return;
trimMode.TrimStart(itemToTrim, time, affectTimeScale);
}
public static void TrimEnd(ITimelineItem item, double time, bool affectTimeScale)
{
var itemToTrim = item as ITrimmable;
if (itemToTrim == null) return;
trimMode.TrimEnd(itemToTrim, time, affectTimeScale);
}
public static void DrawTrimGUI(WindowState state, TimelineItemGUI item, TrimEdge edge)
{
trimDrawer.DrawGUI(state, item.rect, modeColor, edge);
}
public static void FinishTrim()
{
s_CurrentTrimItem = null;
TimelineCursors.ClearCursor();
ClearEditMode();
TimelineEditor.Refresh(RefreshReason.ContentsModified);
}
public static void BeginMove(MoveItemHandler moveItemHandler)
{
s_CurrentMoveItemHandler = moveItemHandler;
moveMode.BeginMove(s_CurrentMoveItemHandler.movingItems);
}
public static void UpdateMove()
{
moveMode.UpdateMove(s_CurrentMoveItemHandler.movingItems);
}
public static void OnTrackDetach(IEnumerable<ItemsPerTrack> grabbedTrackItems)
{
moveMode.OnTrackDetach(grabbedTrackItems);
}
public static void HandleTrackSwitch(IEnumerable<ItemsPerTrack> grabbedTrackItems)
{
moveMode.HandleTrackSwitch(grabbedTrackItems);
}
public static bool AllowTrackSwitch()
{
return moveMode.AllowTrackSwitch();
}
public static double AdjustStartTime(WindowState state, ItemsPerTrack itemsGroup, double time)
{
return moveMode.AdjustStartTime(state, itemsGroup, time);
}
public static bool ValidateDrag(ItemsPerTrack itemsGroup)
{
return moveMode.ValidateMove(itemsGroup);
}
public static void DrawMoveGUI(WindowState state, IEnumerable<MovingItems> movingItems)
{
moveDrawer.DrawGUI(state, movingItems, modeColor);
}
public static void FinishMove()
{
var manipulatedItemsList = s_CurrentMoveItemHandler.movingItems;
moveMode.FinishMove(manipulatedItemsList);
foreach (var itemsGroup in manipulatedItemsList)
foreach (var item in itemsGroup.items)
item.parentTrack = itemsGroup.targetTrack;
s_CurrentMoveItemHandler = null;
TimelineCursors.ClearCursor();
ClearEditMode();
TimelineEditor.Refresh(RefreshReason.ContentsModified);
}
public static void FinalizeInsertItemsAtTime(IEnumerable<ItemsPerTrack> newItems, double requestedTime)
{
addDeleteMode.InsertItemsAtTime(newItems, requestedTime);
}
public static void PrepareItemsDelete(IEnumerable<ItemsPerTrack> newItems)
{
addDeleteMode.RemoveItems(newItems);
}
public static void HandleModeClutch()
{
if (Event.current.type == EventType.KeyDown && EditorGUI.IsEditingTextField())
return;
var prevType = editType;
if (Event.current.type == EventType.KeyDown)
{
var clutchEditType = GetSubEditType(Event.current.keyCode);
if (clutchEditType != EditType.None)
{
s_OverrideEditType = clutchEditType;
Event.current.Use();
}
}
else if (Event.current.type == EventType.KeyUp)
{
var clutchEditType = GetSubEditType(Event.current.keyCode);
if (clutchEditType == s_OverrideEditType)
{
s_OverrideEditType = EditType.None;
Event.current.Use();
}
}
if (prevType != editType)
{
if (s_CurrentTrimItem != null)
{
trimMode.OnBeforeTrim(s_CurrentTrimItem, s_CurrentTrimDirection);
}
else if (s_CurrentMoveItemHandler != null)
{
if (s_CurrentMoveItemHandler.movingItems == null)
{
s_CurrentMoveItemHandler = null;
return;
}
foreach (var movingItems in s_CurrentMoveItemHandler.movingItems)
{
if (movingItems != null && movingItems.HasAnyDetachedParents())
{
foreach (var items in movingItems.items)
{
items.parentTrack = movingItems.originalTrack;
}
}
}
var movingSelection = s_CurrentMoveItemHandler.movingItems;
// Handle clutch key transition if needed
GetSubEditMode(prevType).moveItemMode.OnModeClutchExit(movingSelection);
moveMode.OnModeClutchEnter(movingSelection);
moveMode.BeginMove(movingSelection);
moveMode.HandleTrackSwitch(movingSelection);
UpdateMove();
s_CurrentMoveItemHandler.RefreshPreviewItems();
TimelineWindow.instance.state.rebuildGraph = true; // TODO Rebuild only if parent changed
}
TimelineWindow.instance.Repaint(); // TODO Refresh the toolbar without doing a full repaint?
}
}
}
}

View File

@@ -0,0 +1,199 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace UnityEditor.Timeline
{
class EditModeInputHandler
{
readonly MoveInputHandler m_MoveHandler;
readonly TrimInputHandler m_TrimHandler;
public EditModeInputHandler()
{
m_MoveHandler = new MoveInputHandler();
m_TrimHandler = new TrimInputHandler();
}
public void ProcessMove(InputEvent action, double value)
{
if (TimelineWindow.instance != null && TimelineWindow.instance.state != null)
ProcessInputAction(m_MoveHandler, action, value, TimelineWindow.instance.state);
}
public void ProcessTrim(InputEvent action, double value, bool stretch)
{
if (TimelineWindow.instance != null && TimelineWindow.instance.state != null)
{
m_TrimHandler.stretch = stretch;
ProcessInputAction(m_TrimHandler, action, value, TimelineWindow.instance.state);
}
}
public void SetValueForEdge(IEnumerable<ITimelineItem> items, AttractedEdge edge, double value)
{
if (TimelineWindow.instance != null && TimelineWindow.instance.state != null)
MoveInputHandler.SetValueForEdge(items, edge, value, TimelineWindow.instance.state);
}
public void OnGUI(WindowState state, Event evt)
{
if (TimelineWindow.instance != null && TimelineWindow.instance.state != null)
{
m_MoveHandler.OnGUI(evt);
m_TrimHandler.OnGUI(state);
}
}
static void ProcessInputAction(IInputHandler handler, InputEvent action, double value, WindowState state)
{
var items = SelectionManager.SelectedItems();
switch (action)
{
case InputEvent.None:
return;
case InputEvent.DragEnter:
handler.OnEnterDrag(items, state);
break;
case InputEvent.Drag:
handler.OnDrag(value, state);
break;
case InputEvent.DragExit:
handler.OnExitDrag();
break;
case InputEvent.KeyboardInput:
handler.OnSetValue(items, value, state);
break;
default:
return;
}
}
interface IInputHandler
{
void OnEnterDrag(IEnumerable<ITimelineItem> items, WindowState state);
void OnDrag(double value, WindowState state);
void OnExitDrag();
void OnSetValue(IEnumerable<ITimelineItem> items, double value, WindowState state);
}
class TrimInputHandler : IInputHandler
{
bool isDragging { get; set; }
public bool stretch { get; set; }
IEnumerable<ITimelineItem> grabbedItems { get; set; }
public void OnEnterDrag(IEnumerable<ITimelineItem> items, WindowState state)
{
grabbedItems = items.OfType<ITrimmable>().ToArray();
foreach (var item in grabbedItems)
{
EditMode.BeginTrim(item, TrimEdge.End);
}
isDragging = true;
}
public void OnDrag(double endValue, WindowState state)
{
var trimValue = endValue;
trimValue = TimeReferenceUtility.SnapToFrameIfRequired(trimValue);
foreach (var item in grabbedItems)
{
EditMode.TrimEnd(item, trimValue, stretch);
}
state.UpdateRootPlayableDuration(state.editSequence.duration);
}
public void OnExitDrag()
{
isDragging = false;
EditMode.FinishTrim();
TimelineWindow.instance.Repaint();
}
public void OnSetValue(IEnumerable<ITimelineItem> items, double endValue, WindowState state)
{
foreach (var item in items.OfType<ITrimmable>())
{
EditMode.BeginTrim(item, TrimEdge.End);
EditMode.TrimEnd(item, endValue, stretch);
EditMode.FinishTrim();
}
state.UpdateRootPlayableDuration(state.editSequence.duration);
}
public void OnGUI(WindowState state)
{
if (!isDragging) return;
foreach (var item in grabbedItems)
{
EditMode.DrawTrimGUI(state, item.gui, TrimEdge.End);
}
}
}
class MoveInputHandler : IInputHandler
{
MoveItemHandler m_MoveItemHandler;
public void OnEnterDrag(IEnumerable<ITimelineItem> items, WindowState state)
{
if (items.Any())
{
m_MoveItemHandler = new MoveItemHandler(state);
m_MoveItemHandler.Grab(items, items.First().parentTrack);
}
}
public void OnDrag(double value, WindowState state)
{
if (m_MoveItemHandler == null) return;
var startValue = value;
startValue = TimeReferenceUtility.SnapToFrameIfRequired(startValue);
m_MoveItemHandler.OnAttractedEdge(null, ManipulateEdges.Both, AttractedEdge.None, startValue);
}
public void OnExitDrag()
{
if (m_MoveItemHandler == null) return;
m_MoveItemHandler.Drop();
m_MoveItemHandler = null;
GUIUtility.ExitGUI();
}
public void OnSetValue(IEnumerable<ITimelineItem> items, double value, WindowState state)
{
if (!items.Any()) return;
m_MoveItemHandler = new MoveItemHandler(state);
m_MoveItemHandler.Grab(items, items.First().parentTrack);
m_MoveItemHandler.OnAttractedEdge(null, ManipulateEdges.Both, AttractedEdge.None, value);
m_MoveItemHandler.Drop();
m_MoveItemHandler = null;
}
public void OnGUI(Event evt)
{
if (m_MoveItemHandler != null)
m_MoveItemHandler.OnGUI(evt);
}
public static void SetValueForEdge(IEnumerable<ITimelineItem> items, AttractedEdge edge, double value, WindowState state)
{
var handler = new MoveItemHandler(state);
foreach (var item in items)
{
handler.Grab(new[] {item}, item.parentTrack);
handler.OnAttractedEdge(null, ManipulateEdges.Both, edge, value);
handler.Drop();
}
}
}
}
}

View File

@@ -0,0 +1,49 @@
using System;
using UnityEngine;
namespace UnityEditor.Timeline
{
class HeaderSplitterManipulator : Manipulator
{
bool m_Captured;
protected override bool MouseDown(Event evt, WindowState state)
{
Rect headerSplitterRect = state.GetWindow().headerSplitterRect;
if (headerSplitterRect.Contains(evt.mousePosition))
{
m_Captured = true;
state.AddCaptured(this);
return true;
}
return false;
}
protected override bool MouseDrag(Event evt, WindowState state)
{
if (!m_Captured)
return false;
state.sequencerHeaderWidth = evt.mousePosition.x;
return true;
}
protected override bool MouseUp(Event evt, WindowState state)
{
if (!m_Captured)
return false;
state.RemoveCaptured(this);
m_Captured = false;
return true;
}
public override void Overlay(Event evt, WindowState state)
{
Rect rect = state.GetWindow().sequenceRect;
EditorGUIUtility.AddCursorRect(rect, MouseCursor.SplitResizeLeftRight);
}
}
}

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.Timeline
{
interface IMoveItemMode
{
void OnTrackDetach(IEnumerable<ItemsPerTrack> itemsGroups);
void HandleTrackSwitch(IEnumerable<ItemsPerTrack> itemsGroups);
bool AllowTrackSwitch();
double AdjustStartTime(WindowState state, ItemsPerTrack itemsGroup, double time);
void OnModeClutchEnter(IEnumerable<ItemsPerTrack> itemsGroups);
void OnModeClutchExit(IEnumerable<ItemsPerTrack> itemsGroups);
void BeginMove(IEnumerable<ItemsPerTrack> itemsGroups);
void UpdateMove(IEnumerable<ItemsPerTrack> itemsGroups);
void FinishMove(IEnumerable<ItemsPerTrack> itemsGroups);
bool ValidateMove(ItemsPerTrack itemsGroup);
}
interface IMoveItemDrawer
{
void DrawGUI(WindowState state, IEnumerable<MovingItems> movingItems, Color color);
}
}

View File

@@ -0,0 +1,311 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class MoveItemHandler : IAttractable, IAttractionHandler
{
bool m_Grabbing;
MovingItems m_LeftMostMovingItems;
MovingItems m_RightMostMovingItems;
HashSet<TimelineItemGUI> m_ItemGUIs;
ItemsGroup m_ItemsGroup;
public TrackAsset targetTrack { get; private set; }
public bool allowTrackSwitch { get; private set; }
int m_GrabbedModalUndoGroup = -1;
readonly WindowState m_State;
public MovingItems[] movingItems { get; private set; }
public MoveItemHandler(WindowState state)
{
m_State = state;
}
public void Grab(IEnumerable<ITimelineItem> items, TrackAsset referenceTrack)
{
Grab(items, referenceTrack, Vector2.zero);
}
public void Grab(IEnumerable<ITimelineItem> items, TrackAsset referenceTrack, Vector2 mousePosition)
{
if (items == null) return;
items = items.ToArray(); // Cache enumeration result
if (!items.Any()) return;
m_GrabbedModalUndoGroup = Undo.GetCurrentGroup();
var trackItems = items.GroupBy(c => c.parentTrack).ToArray();
var trackItemsCount = trackItems.Length;
var tracks = items.Select(c => c.parentTrack).Where(x => x != null).Distinct();
movingItems = new MovingItems[trackItemsCount];
allowTrackSwitch = trackItemsCount == 1 && !trackItems.SelectMany(x => x).Any(x => x is MarkerItem); // For now, track switch is only supported when all items are on the same track and there are no items
// one push per track handles all the clips on the track
UndoExtensions.RegisterTracks(tracks, L10n.Tr("Move Items"));
foreach (var sourceTrack in tracks)
{
// push all markers on the track because of ripple
UndoExtensions.RegisterMarkers(sourceTrack.GetMarkers(), L10n.Tr("Move Items"));
}
for (var i = 0; i < trackItemsCount; ++i)
{
var track = trackItems[i].Key;
var grabbedItems = new MovingItems(m_State, track, trackItems[i].ToArray(), referenceTrack, mousePosition, allowTrackSwitch);
movingItems[i] = grabbedItems;
}
m_LeftMostMovingItems = null;
m_RightMostMovingItems = null;
foreach (var grabbedTrackItems in movingItems)
{
if (m_LeftMostMovingItems == null || m_LeftMostMovingItems.start > grabbedTrackItems.start)
m_LeftMostMovingItems = grabbedTrackItems;
if (m_RightMostMovingItems == null || m_RightMostMovingItems.end < grabbedTrackItems.end)
m_RightMostMovingItems = grabbedTrackItems;
}
m_ItemGUIs = new HashSet<TimelineItemGUI>();
m_ItemsGroup = new ItemsGroup(items);
foreach (var item in items)
m_ItemGUIs.Add(item.gui);
targetTrack = referenceTrack;
EditMode.BeginMove(this);
m_Grabbing = true;
}
public void Drop()
{
if (IsValidDrop())
{
foreach (var grabbedItems in movingItems)
{
var track = grabbedItems.targetTrack;
UndoExtensions.RegisterTrack(track, L10n.Tr("Move Items"));
if (EditModeUtils.IsInfiniteTrack(track) && grabbedItems.clips.Any())
((AnimationTrack)track).ConvertToClipMode();
}
EditMode.FinishMove();
Done();
}
else
{
Cancel();
}
EditMode.ClearEditMode();
}
bool IsValidDrop()
{
return movingItems.All(g => g.canDrop);
}
void Cancel()
{
if (!m_Grabbing)
return;
// TODO fix undo reselection persistency
// identify the clips by their playable asset, since that reference will survive the undo
// This is a workaround, until a more persistent fix for selection of clips across Undo can be found
var assets = movingItems.SelectMany(x => x.clips).Select(x => x.asset);
Undo.RevertAllDownToGroup(m_GrabbedModalUndoGroup);
// reselect the clips from the original clip
var clipsToSelect = movingItems.Select(x => x.originalTrack).SelectMany(x => x.GetClips()).Where(x => assets.Contains(x.asset)).ToArray();
SelectionManager.RemoveTimelineSelection();
foreach (var c in clipsToSelect)
SelectionManager.Add(c);
Done();
}
void Done()
{
foreach (var movingItem in movingItems)
{
foreach (var item in movingItem.items)
{
if (item.gui != null)
item.gui.isInvalid = false;
}
}
movingItems = null;
m_LeftMostMovingItems = null;
m_RightMostMovingItems = null;
m_Grabbing = false;
m_State.Refresh();
}
public double start { get { return m_ItemsGroup.start; } }
public double end { get { return m_ItemsGroup.end; } }
public bool ShouldSnapTo(ISnappable snappable)
{
var itemGUI = snappable as TimelineItemGUI;
return itemGUI != null && !m_ItemGUIs.Contains(itemGUI);
}
public void UpdateTrackTarget(TrackAsset track)
{
if (!EditMode.AllowTrackSwitch())
return;
targetTrack = track;
var targetTracksChanged = false;
foreach (var grabbedItem in movingItems)
{
var prevTrackGUI = grabbedItem.targetTrack;
grabbedItem.SetReferenceTrack(track);
targetTracksChanged = grabbedItem.targetTrack != prevTrackGUI;
}
if (targetTracksChanged)
EditMode.HandleTrackSwitch(movingItems);
RefreshPreviewItems();
m_State.rebuildGraph |= targetTracksChanged;
}
public void OnGUI(Event evt)
{
if (!m_Grabbing)
return;
if (evt.type != EventType.Repaint)
return;
var isValid = IsValidDrop();
using (new GUIViewportScope(m_State.GetWindow().sequenceContentRect))
{
foreach (var grabbedClip in movingItems)
{
grabbedClip.RefreshBounds(m_State, evt.mousePosition);
if (!grabbedClip.HasAnyDetachedParents())
continue;
grabbedClip.Draw(isValid);
}
if (isValid)
{
EditMode.DrawMoveGUI(m_State, movingItems);
}
else
{
TimelineCursors.ClearCursor();
}
}
}
public void OnAttractedEdge(IAttractable attractable, ManipulateEdges manipulateEdges, AttractedEdge edge, double time)
{
double offset;
if (edge == AttractedEdge.Right)
{
var duration = end - start;
var startTime = time - duration;
startTime = EditMode.AdjustStartTime(m_State, m_RightMostMovingItems, startTime);
offset = startTime + duration - end;
}
else
{
if (edge == AttractedEdge.Left)
time = EditMode.AdjustStartTime(m_State, m_LeftMostMovingItems, time);
offset = time - start;
}
if (start + offset < 0.0)
offset = -start;
if (!offset.Equals(0.0))
{
foreach (var grabbedClips in movingItems)
grabbedClips.start += offset;
EditMode.UpdateMove();
RefreshPreviewItems();
}
}
public void RefreshPreviewItems()
{
foreach (var movingItemsGroup in movingItems)
{
// Check validity
var valid = ValidateItemDrag(movingItemsGroup);
foreach (var item in movingItemsGroup.items)
{
if (item.gui != null)
item.gui.isInvalid = !valid;
}
movingItemsGroup.canDrop = valid;
}
}
static bool ValidateItemDrag(ItemsPerTrack itemsGroup)
{
//TODO-marker: this is to prevent the drag operation from being canceled when moving only markers
if (itemsGroup.clips.Any())
{
if (itemsGroup.targetTrack == null)
return false;
if (itemsGroup.targetTrack.lockedInHierarchy)
return false;
if (itemsGroup.items.Any(i => !i.IsCompatibleWithTrack(itemsGroup.targetTrack)))
return false;
return EditMode.ValidateDrag(itemsGroup);
}
return true;
}
public void OnTrackDetach()
{
EditMode.OnTrackDetach(movingItems);
}
}
}

View File

@@ -0,0 +1,161 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class MoveItemModeMix : IMoveItemMode, IMoveItemDrawer
{
TimelineClip[] m_ClipsMoved;
Dictionary<TimelineClip, double> m_OriginalEaseInDuration = new Dictionary<TimelineClip, double>();
Dictionary<TimelineClip, double> m_OriginalEaseOutDuration = new Dictionary<TimelineClip, double>();
public void OnTrackDetach(IEnumerable<ItemsPerTrack> itemsGroups)
{
// Nothing
}
public void HandleTrackSwitch(IEnumerable<ItemsPerTrack> itemsGroups)
{
foreach (var itemsGroup in itemsGroups)
{
var targetTrack = itemsGroup.targetTrack;
if (targetTrack != null && itemsGroup.items.Any())
{
var compatible = itemsGroup.items.First().IsCompatibleWithTrack(targetTrack) &&
!EditModeUtils.IsInfiniteTrack(targetTrack);
var track = compatible ? targetTrack : null;
UndoExtensions.RegisterTrack(track, L10n.Tr("Move Items"));
EditModeUtils.SetParentTrack(itemsGroup.items, track);
}
else
{
EditModeUtils.SetParentTrack(itemsGroup.items, null);
}
}
}
public bool AllowTrackSwitch()
{
return true;
}
public double AdjustStartTime(WindowState state, ItemsPerTrack itemsGroup, double time)
{
return time;
}
public void OnModeClutchEnter(IEnumerable<ItemsPerTrack> itemsGroups)
{
// Nothing
}
public void OnModeClutchExit(IEnumerable<ItemsPerTrack> itemsGroups)
{
// Nothing
}
public void BeginMove(IEnumerable<ItemsPerTrack> itemsGroups)
{
m_ClipsMoved = itemsGroups.SelectMany(i => i.clips).ToArray();
foreach (var clip in m_ClipsMoved)
{
m_OriginalEaseInDuration[clip] = clip.easeInDuration;
m_OriginalEaseOutDuration[clip] = clip.easeOutDuration;
}
}
public void UpdateMove(IEnumerable<ItemsPerTrack> itemsGroups)
{
//Compute Blends before updating ease values.
foreach(var t in itemsGroups.Select(i=>i.targetTrack).Where(t => t != null))
t.ComputeBlendsFromOverlaps();
//Reset to original ease values. The trim operation will calculate the proper blend values.
foreach(var clip in m_ClipsMoved)
{
clip.easeInDuration = m_OriginalEaseInDuration[clip];
clip.easeOutDuration = m_OriginalEaseOutDuration[clip];
EditorUtility.SetDirty(clip.asset);
}
}
public void FinishMove(IEnumerable<ItemsPerTrack> itemsGroups)
{
var allClips = itemsGroups.Select(i=>i.targetTrack)
.Where(t=>t != null).SelectMany(t => t.clips);
// update easeIn easeOut durations to apply any modifications caused by blends created or modified by clip move.
foreach (var clip in allClips)
{
clip.easeInDuration = clip.easeInDuration;
clip.easeOutDuration = clip.easeOutDuration;
}
}
public bool ValidateMove(ItemsPerTrack itemsGroup)
{
var track = itemsGroup.targetTrack;
var items = itemsGroup.items;
if (EditModeUtils.IsInfiniteTrack(track))
{
double startTime;
double stopTime;
EditModeUtils.GetInfiniteClipBoundaries(track, out startTime, out stopTime);
return items.All(item =>
!EditModeUtils.IsItemWithinRange(item, startTime, stopTime) &&
!EditModeUtils.IsRangeWithinItem(startTime, stopTime, item));
}
var siblings = ItemsUtils.GetItemsExcept(itemsGroup.targetTrack, items);
return items.All(item => EditModeMixUtils.GetPlacementValidity(item, siblings) == PlacementValidity.Valid);
}
public void DrawGUI(WindowState state, IEnumerable<MovingItems> movingItems, Color color)
{
var selectionHasAnyBlendIn = false;
var selectionHasAnyBlendOut = false;
foreach (var grabbedItems in movingItems)
{
var bounds = grabbedItems.onTrackItemsBounds;
var counter = 0;
foreach (var item in grabbedItems.items.OfType<IBlendable>())
{
if (item.hasLeftBlend)
{
EditModeGUIUtils.DrawBoundsEdge(bounds[counter], color, TrimEdge.Start);
selectionHasAnyBlendIn = true;
}
if (item.hasRightBlend)
{
EditModeGUIUtils.DrawBoundsEdge(bounds[counter], color, TrimEdge.End);
selectionHasAnyBlendOut = true;
}
counter++;
}
}
if (selectionHasAnyBlendIn && selectionHasAnyBlendOut)
{
TimelineCursors.SetCursor(TimelineCursors.CursorType.MixBoth);
}
else if (selectionHasAnyBlendIn)
{
TimelineCursors.SetCursor(TimelineCursors.CursorType.MixLeft);
}
else if (selectionHasAnyBlendOut)
{
TimelineCursors.SetCursor(TimelineCursors.CursorType.MixRight);
}
else
{
TimelineCursors.ClearCursor();
}
}
}
}

View File

@@ -0,0 +1,99 @@
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditor.Timeline
{
class MoveItemModeReplace : IMoveItemMode, IMoveItemDrawer
{
public void OnTrackDetach(IEnumerable<ItemsPerTrack> itemsGroups)
{
// Nothing
}
public void HandleTrackSwitch(IEnumerable<ItemsPerTrack> itemsGroups)
{
// Nothing
}
public bool AllowTrackSwitch()
{
return true;
}
public double AdjustStartTime(WindowState state, ItemsPerTrack itemsGroup, double time)
{
return time;
}
public void OnModeClutchEnter(IEnumerable<ItemsPerTrack> itemsGroups)
{
// TODO
}
public void OnModeClutchExit(IEnumerable<ItemsPerTrack> itemsGroups)
{
// TODO
}
public void BeginMove(IEnumerable<ItemsPerTrack> itemsGroups)
{
foreach (var itemsGroup in itemsGroups)
{
EditModeUtils.SetParentTrack(itemsGroup.items, null);
}
}
public void UpdateMove(IEnumerable<ItemsPerTrack> itemsGroups)
{
// Nothing
}
public void FinishMove(IEnumerable<ItemsPerTrack> itemsGroups)
{
EditModeReplaceUtils.Insert(itemsGroups);
}
public bool ValidateMove(ItemsPerTrack itemsGroup)
{
return true;
}
public void DrawGUI(WindowState state, IEnumerable<MovingItems> movingItems, Color color)
{
var operationWillReplace = false;
foreach (var itemsPerTrack in movingItems)
{
var bounds = itemsPerTrack.onTrackItemsBounds;
var counter = 0;
foreach (var item in itemsPerTrack.items)
{
if (EditModeUtils.GetFirstIntersectedItem(itemsPerTrack.items, item.start) != null)
{
EditModeGUIUtils.DrawBoundsEdge(bounds[counter], color, TrimEdge.Start);
operationWillReplace = true;
}
if (EditModeUtils.GetFirstIntersectedItem(itemsPerTrack.items, item.end) != null)
{
EditModeGUIUtils.DrawBoundsEdge(bounds[counter], color, TrimEdge.End);
operationWillReplace = true;
}
counter++;
// TODO Display swallowed clips?
}
}
if (operationWillReplace)
{
TimelineCursors.SetCursor(TimelineCursors.CursorType.Replace);
}
else
{
TimelineCursors.ClearCursor();
}
}
}
}

View File

@@ -0,0 +1,271 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Object = UnityEngine.Object;
namespace UnityEditor.Timeline
{
class MoveItemModeRipple : IMoveItemMode, IMoveItemDrawer
{
const float k_SnapToEdgeDistance = 30.0f;
class PrevItemInfo
{
public ITimelineItem item;
public ITimelineItem firstSelectedItem;
public bool blending;
public PrevItemInfo(ITimelineItem item, ITimelineItem firstSelectedItem)
{
this.item = item;
this.firstSelectedItem = firstSelectedItem;
blending = item != null && item.end > firstSelectedItem.start;
}
}
readonly Dictionary<Object, List<ITimelineItem>> m_NextItems = new Dictionary<Object, List<ITimelineItem>>();
readonly Dictionary<Object, PrevItemInfo> m_PreviousItem = new Dictionary<Object, PrevItemInfo>();
double m_PreviousEnd;
bool m_TrackLocked;
bool m_Detached;
public void OnTrackDetach(IEnumerable<ItemsPerTrack> itemsGroups)
{
if (m_TrackLocked)
return;
if (m_Detached)
return;
if (itemsGroups.Any(x => x.markers.Any()))
return;
// Ripple can either remove or not clips when detaching them from their track.
// Keep it off for now. TODO: add clutch key to toggle this feature?
//EditModeRippleUtils.Remove(manipulatedClipsList);
StartDetachMode(itemsGroups);
}
public void HandleTrackSwitch(IEnumerable<ItemsPerTrack> itemsGroups)
{
// Nothing
}
public bool AllowTrackSwitch()
{
return !m_TrackLocked;
}
public double AdjustStartTime(WindowState state, ItemsPerTrack itemsGroup, double time)
{
var track = itemsGroup.targetTrack;
if (track == null)
return time;
double start;
double end;
if (EditModeUtils.IsInfiniteTrack(track))
{
EditModeUtils.GetInfiniteClipBoundaries(track, out start, out end);
}
else
{
var siblings = ItemsUtils.GetItemsExcept(track, itemsGroup.items);
var firstIntersectedItem = EditModeUtils.GetFirstIntersectedItem(siblings, time);
if (firstIntersectedItem == null)
return time;
start = firstIntersectedItem.start;
end = firstIntersectedItem.end;
}
var closestTime = Math.Abs(time - start) < Math.Abs(time - end) ? start : end;
var pixelTime = state.TimeToPixel(time);
var pixelClosestTime = state.TimeToPixel(closestTime);
if (Math.Abs(pixelTime - pixelClosestTime) < k_SnapToEdgeDistance)
return closestTime;
return time;
}
void StartDetachMode(IEnumerable<ItemsPerTrack> itemsGroups)
{
m_Detached = true;
foreach (var itemsGroup in itemsGroups)
EditModeUtils.SetParentTrack(itemsGroup.items, null);
}
public void OnModeClutchEnter(IEnumerable<ItemsPerTrack> itemsGroups)
{
StartDetachMode(itemsGroups);
m_TrackLocked = false;
}
public void OnModeClutchExit(IEnumerable<ItemsPerTrack> itemsGroups)
{
m_Detached = false;
m_TrackLocked = false;
}
public void BeginMove(IEnumerable<ItemsPerTrack> itemsGroups)
{
m_NextItems.Clear();
m_PreviousItem.Clear();
var itemTypes = ItemsUtils.GetItemTypes(itemsGroups).ToList();
foreach (var itemsGroup in itemsGroups)
{
//can only ripple items of the same type as those selected
var sortedSelectedItems = itemsGroup.items.OrderBy(i => i.start).ToList();
var siblings = itemsGroup.targetTrack.GetItemsExcept(itemsGroup.items);
var sortedSiblingsToRipple = siblings.Where(i => itemTypes.Contains(i.GetType())).OrderBy(i => i.start).ToList();
var start = sortedSelectedItems.First().start;
m_NextItems.Add(itemsGroup.targetTrack, sortedSiblingsToRipple.Where(i => i.start > start).ToList());
m_PreviousItem.Add(itemsGroup.targetTrack, CalculatePrevItemInfo(sortedSelectedItems, sortedSiblingsToRipple, itemTypes));
}
m_PreviousEnd = itemsGroups.Max(m => m.items.Max(c => c.end));
}
public void UpdateMove(IEnumerable<ItemsPerTrack> itemsGroups)
{
if (m_Detached)
return;
m_TrackLocked = true;
var overlap = 0.0;
foreach (var itemsGroup in itemsGroups)
{
var track = itemsGroup.targetTrack;
if (track == null) continue;
var prevItemInfo = m_PreviousItem[track];
if (prevItemInfo.item != null)
{
var prevItem = prevItemInfo.item;
var firstItem = prevItemInfo.firstSelectedItem;
if (prevItemInfo.blending)
prevItemInfo.blending = prevItem.end > firstItem.start;
if (prevItemInfo.blending)
{
var b = EditModeUtils.BlendDuration(firstItem, TrimEdge.End);
overlap = Math.Max(overlap, Math.Max(prevItem.start, prevItem.end - firstItem.end + firstItem.start + b) - firstItem.start);
}
else
{
overlap = Math.Max(overlap, prevItem.end - firstItem.start);
}
}
}
if (overlap > 0)
{
foreach (var itemsGroup in itemsGroups)
{
foreach (var item in itemsGroup.items)
item.start += overlap;
}
}
var newEnd = itemsGroups.Max(m => m.items.Max(c => c.end));
var offset = newEnd - m_PreviousEnd;
m_PreviousEnd = newEnd;
foreach (var itemsGroup in itemsGroups)
{
foreach (var item in m_NextItems[itemsGroup.targetTrack])
item.start += offset;
}
}
static PrevItemInfo CalculatePrevItemInfo(List<ITimelineItem> orderedSelection, List<ITimelineItem> orderedSiblings, IEnumerable<Type> itemTypes)
{
ITimelineItem previousItem = null;
ITimelineItem firstSelectedItem = null;
var gap = double.PositiveInfinity;
foreach (var type in itemTypes)
{
var firstSelectedItemOfType = orderedSelection.FirstOrDefault(i => i.GetType() == type);
if (firstSelectedItemOfType == null) continue;
var previousItemOfType = orderedSiblings.LastOrDefault(i => i.GetType() == type && i.start < firstSelectedItemOfType.start);
if (previousItemOfType == null) continue;
var currentGap = firstSelectedItemOfType.start - previousItemOfType.end;
if (currentGap < gap)
{
gap = currentGap;
firstSelectedItem = firstSelectedItemOfType;
previousItem = previousItemOfType;
}
}
return new PrevItemInfo(previousItem, firstSelectedItem);
}
public bool ValidateMove(ItemsPerTrack itemsGroup)
{
return true;
}
public void FinishMove(IEnumerable<ItemsPerTrack> itemsGroups)
{
if (m_Detached)
EditModeRippleUtils.Insert(itemsGroups);
m_Detached = false;
m_TrackLocked = false;
}
public void DrawGUI(WindowState state, IEnumerable<MovingItems> movingItems, Color color)
{
if (m_Detached)
{
var xMin = float.MaxValue;
var xMax = float.MinValue;
foreach (var grabbedItems in movingItems)
{
xMin = Math.Min(xMin, grabbedItems.onTrackItemsBounds.Min(b => b.xMin)); // TODO Cache this?
xMax = Math.Max(xMax, grabbedItems.onTrackItemsBounds.Max(b => b.xMax));
}
foreach (var grabbedItems in movingItems)
{
var bounds = Rect.MinMaxRect(xMin, grabbedItems.onTrackItemsBounds[0].yMin,
xMax, grabbedItems.onTrackItemsBounds[0].yMax);
EditModeGUIUtils.DrawOverlayRect(bounds, new Color(1.0f, 1.0f, 1.0f, 0.5f));
EditModeGUIUtils.DrawBoundsEdge(bounds, color, TrimEdge.Start);
}
}
else
{
foreach (var grabbedItems in movingItems)
{
var bounds = Rect.MinMaxRect(grabbedItems.onTrackItemsBounds.Min(b => b.xMin), grabbedItems.onTrackItemsBounds[0].yMin,
grabbedItems.onTrackItemsBounds.Max(b => b.xMax), grabbedItems.onTrackItemsBounds[0].yMax);
EditModeGUIUtils.DrawBoundsEdge(bounds, color, TrimEdge.Start);
}
}
TimelineCursors.SetCursor(TimelineCursors.CursorType.Ripple);
}
}
}

View File

@@ -0,0 +1,137 @@
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class MovingItems : ItemsPerTrack
{
TrackAsset m_ReferenceTrack;
readonly bool m_AllowTrackSwitch;
readonly Rect[] m_ItemsBoundsOnTrack;
readonly Vector2[] m_ItemsMouseOffsets;
static readonly Rect s_InvisibleBounds = new Rect(float.MaxValue, float.MaxValue, 0.0f, 0.0f);
public TrackAsset originalTrack { get; }
public override TrackAsset targetTrack
{
get
{
if (m_AllowTrackSwitch)
return m_ReferenceTrack;
return originalTrack;
}
}
public bool canDrop;
public double start
{
get { return m_ItemsGroup.start; }
set { m_ItemsGroup.start = value; }
}
public double end
{
get { return m_ItemsGroup.end; }
}
public Rect[] onTrackItemsBounds
{
get { return m_ItemsBoundsOnTrack; }
}
public MovingItems(WindowState state, TrackAsset parentTrack, ITimelineItem[] items, TrackAsset referenceTrack, Vector2 mousePosition, bool allowTrackSwitch)
: base(parentTrack, items)
{
originalTrack = parentTrack;
m_ReferenceTrack = referenceTrack;
m_AllowTrackSwitch = allowTrackSwitch;
m_ItemsBoundsOnTrack = new Rect[items.Length];
m_ItemsMouseOffsets = new Vector2[items.Length];
for (int i = 0; i < items.Length; ++i)
{
var itemGUi = items[i].gui;
if (itemGUi != null)
{
m_ItemsBoundsOnTrack[i] = itemGUi.rect;
m_ItemsMouseOffsets[i] = mousePosition - m_ItemsBoundsOnTrack[i].position;
}
}
canDrop = true;
}
public void SetReferenceTrack(TrackAsset track)
{
m_ReferenceTrack = track;
}
public bool HasAnyDetachedParents()
{
return m_ItemsGroup.items.Any(x => x.parentTrack == null);
}
public void RefreshBounds(WindowState state, Vector2 mousePosition)
{
for (int i = 0; i < m_ItemsGroup.items.Length; ++i)
{
var item = m_ItemsGroup.items[i];
var itemGUI = item.gui;
if (item.parentTrack != null)
{
m_ItemsBoundsOnTrack[i] = itemGUI.visible ? itemGUI.rect : s_InvisibleBounds;
}
else
{
if (targetTrack != null)
{
var trackGUI = (TimelineTrackGUI)TimelineWindow.instance.allTracks.FirstOrDefault(t => t.track == targetTrack);
if (trackGUI == null) return;
var trackRect = trackGUI.boundingRect;
m_ItemsBoundsOnTrack[i] = itemGUI.RectToTimeline(trackRect, state);
}
else
{
m_ItemsBoundsOnTrack[i].position = mousePosition - m_ItemsMouseOffsets[i];
}
}
}
}
public void Draw(bool isValid)
{
for (int i = 0; i < m_ItemsBoundsOnTrack.Length; ++i)
{
var rect = m_ItemsBoundsOnTrack[i];
DrawItemInternal(m_ItemsGroup.items[i], rect, isValid);
}
}
static void DrawItemInternal(ITimelineItem item, Rect rect, bool isValid)
{
var clipGUI = item.gui as TimelineClipGUI;
if (clipGUI != null)
{
if (isValid)
{
clipGUI.DrawGhostClip(rect);
}
else
{
clipGUI.DrawInvalidClip(rect);
}
}
}
}
}

View File

@@ -0,0 +1,140 @@
using System;
using System.Text;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class EaseClip : Manipulator
{
bool m_IsCaptured;
bool m_UndoSaved;
TimelineClipHandle m_EaseClipHandler;
ManipulateEdges m_Edges;
TimelineClip m_Clip;
StringBuilder m_OverlayText = new StringBuilder("");
double m_OriginalValue;
public static readonly string EaseInClipText = L10n.Tr("Ease In Clip");
public static readonly string EaseOutClipText = L10n.Tr("Ease Out Clip");
public static readonly string EaseInText = L10n.Tr("Ease In");
public static readonly string EaseOutText = L10n.Tr("Ease Out");
public static readonly string DurationText = L10n.Tr("Duration: ");
protected override bool MouseDown(Event evt, WindowState state)
{
if (evt.modifiers != ManipulatorsUtils.actionModifier)
return false;
return MouseDownInternal(evt, state, PickerUtils.TopmostPickedItem() as TimelineClipHandle);
}
protected bool MouseDownInternal(Event evt, WindowState state, TimelineClipHandle handle)
{
if (handle == null)
return false;
if (handle.clipGUI.clip != null && !handle.clipGUI.clip.clipCaps.HasAny(ClipCaps.Blending))
return false;
m_Edges = ManipulateEdges.Right;
if (handle.trimDirection == TrimEdge.Start)
m_Edges = ManipulateEdges.Left;
if (m_Edges == ManipulateEdges.Left && handle.clipGUI.clip.hasBlendIn || m_Edges == ManipulateEdges.Right && handle.clipGUI.clip.hasBlendOut)
return false;
m_IsCaptured = true;
m_UndoSaved = false;
m_EaseClipHandler = handle;
m_Clip = handle.clipGUI.clip;
m_OriginalValue = m_Edges == ManipulateEdges.Left ? m_Clip.easeInDuration : m_Clip.easeOutDuration;
// Change cursor only when OnGUI Process (not in test)
if (GUIUtility.guiDepth > 0)
TimelineCursors.SetCursor(m_Edges == ManipulateEdges.Left ? TimelineCursors.CursorType.MixRight : TimelineCursors.CursorType.MixLeft);
state.AddCaptured(this);
return true;
}
protected override bool MouseUp(Event evt, WindowState state)
{
if (!m_IsCaptured)
return false;
m_IsCaptured = false;
m_UndoSaved = false;
state.captured.Clear();
// Clear cursor only when OnGUI Process (not in test)
if (GUIUtility.guiDepth > 0)
TimelineCursors.ClearCursor();
return true;
}
protected override bool MouseDrag(Event evt, WindowState state)
{
if (!m_IsCaptured)
return false;
if (!m_UndoSaved)
{
var uiClip = m_EaseClipHandler.clipGUI;
string undoName = m_Edges == ManipulateEdges.Left ? EaseInClipText : EaseOutClipText;
UndoExtensions.RegisterClip(uiClip.clip, undoName);
m_UndoSaved = true;
}
double d = state.PixelDeltaToDeltaTime(evt.delta.x);
var duration = m_Clip.duration;
var easeInDurationLimit = duration - m_Clip.easeOutDuration;
var easeOutDurationLimit = duration - m_Clip.easeInDuration;
if (m_Edges == ManipulateEdges.Left)
{
m_Clip.easeInDuration = Math.Min(easeInDurationLimit, Math.Max(0, m_Clip.easeInDuration + d));
}
else if (m_Edges == ManipulateEdges.Right)
{
m_Clip.easeOutDuration = Math.Min(easeOutDurationLimit, Math.Max(0, m_Clip.easeOutDuration - d));
}
RefreshOverlayStrings(m_EaseClipHandler, state);
return true;
}
public override void Overlay(Event evt, WindowState state)
{
if (!m_IsCaptured)
return;
if (m_OverlayText.Length > 0)
{
int stringLength = m_OverlayText.Length;
var r = new Rect(evt.mousePosition.x - (stringLength / 2.0f),
m_EaseClipHandler.clipGUI.rect.yMax,
stringLength, 20);
GUI.Label(r, m_OverlayText.ToString(), TimelineWindow.styles.tinyFont);
}
}
void RefreshOverlayStrings(TimelineClipHandle handle, WindowState state)
{
m_OverlayText.Length = 0;
m_OverlayText.Append(m_Edges == ManipulateEdges.Left ? EaseInText : EaseOutText);
var easeDuration = m_Edges == ManipulateEdges.Left ? m_Clip.easeInDuration : m_Clip.easeOutDuration;
var deltaDuration = easeDuration - m_OriginalValue;
// round to frame so we don't show partial time codes due to no frame snapping
if (state.timeFormat == TimeFormat.Timecode)
{
easeDuration = TimeUtility.RoundToFrame(easeDuration, state.referenceSequence.frameRate);
deltaDuration = TimeUtility.RoundToFrame(deltaDuration, state.referenceSequence.frameRate);
}
m_OverlayText.Append(DurationText);
m_OverlayText.Append(state.timeFormat.ToTimeStringWithDelta(easeDuration, state.referenceSequence.frameRate, deltaDuration));
}
}
}

View File

@@ -0,0 +1,61 @@
using UnityEditor.ShortcutManagement;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class Jog : Manipulator
{
Vector2 m_MouseDownOrigin = Vector2.zero;
[ClutchShortcut("Timeline/Jog", typeof(TimelineWindow), KeyCode.J)]
static void JogShortcut(ShortcutArguments args)
{
if (args.stage == ShortcutStage.Begin)
{
(args.context as TimelineWindow).state.isJogging = true;
}
else if (args.stage == ShortcutStage.End)
{
(args.context as TimelineWindow).state.isJogging = false;
}
}
protected override bool MouseDown(Event evt, WindowState state)
{
if (!state.isJogging)
return false;
m_MouseDownOrigin = evt.mousePosition;
state.playbackSpeed = 0.0f;
state.Play();
return true;
}
protected override bool MouseUp(Event evt, WindowState state)
{
if (!state.isJogging)
{
return false;
}
m_MouseDownOrigin = evt.mousePosition;
state.playbackSpeed = 0.0f;
state.Play();
return false;
}
protected override bool MouseDrag(Event evt, WindowState state)
{
if (!state.isJogging)
return false;
var distance = evt.mousePosition - m_MouseDownOrigin;
state.playbackSpeed = distance.x * 0.002f;
state.Play();
return true;
}
}
}

View File

@@ -0,0 +1,36 @@
using System;
using UnityEngine;
namespace UnityEditor.Timeline
{
class MarkerHeaderTrackManipulator : Manipulator
{
protected override bool ContextClick(Event evt, WindowState state)
{
if (!IsMouseOverMarkerHeader(evt.mousePosition, state))
return false;
SelectionManager.SelectOnly(state.editSequence.asset.markerTrack);
SequencerContextMenu.ShowTrackContextMenu(evt.mousePosition);
return true;
}
protected override bool MouseDown(Event evt, WindowState state)
{
if (evt.button != 0 || !IsMouseOverMarkerHeader(evt.mousePosition, state))
return false;
SelectionManager.SelectOnly(state.editSequence.asset.markerTrack);
return true;
}
static bool IsMouseOverMarkerHeader(Vector2 mousePosition, WindowState state)
{
if (!state.showMarkerHeader)
return false;
return state.GetWindow().markerHeaderRect.Contains(mousePosition)
|| state.GetWindow().markerContentRect.Contains(mousePosition);
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Linq;
using UnityEngine;
namespace UnityEditor.Timeline
{
class RectangleSelect : RectangleTool
{
protected override bool enableAutoPan { get { return false; } }
protected override bool CanStartRectangle(Event evt)
{
if (evt.button != 0 || evt.alt)
return false;
return PickerUtils.pickedElements.All(e => e is IRowGUI);
}
protected override bool OnFinish(Event evt, WindowState state, Rect rect)
{
var selectables = state.spacePartitioner.GetItemsInArea<ISelectable>(rect).ToList();
if (!selectables.Any())
return false;
if (ItemSelection.CanClearSelection(evt))
SelectionManager.Clear();
foreach (var selectable in selectables)
{
ItemSelection.HandleItemSelection(evt, selectable);
}
return true;
}
}
}

View File

@@ -0,0 +1,175 @@
using System;
using UnityEngine;
namespace UnityEditor.Timeline
{
abstract class RectangleTool
{
readonly struct TimelinePoint
{
readonly double m_Time;
readonly float m_YPos;
readonly float m_YScrollPos;
readonly WindowState m_State;
readonly TimelineTreeViewGUI m_TreeViewGUI;
public TimelinePoint(WindowState state, Vector2 mousePosition)
{
m_State = state;
m_TreeViewGUI = state.GetWindow().treeView;
m_Time = m_State.PixelToTime(mousePosition.x);
m_YPos = mousePosition.y;
m_YScrollPos = m_TreeViewGUI.scrollPosition.y;
}
public Vector2 ToPixel()
{
return new Vector2(m_State.TimeToPixel(m_Time), m_YPos - (m_TreeViewGUI.scrollPosition.y - m_YScrollPos));
}
}
const float k_HeaderSplitterOverlap = WindowConstants.headerSplitterWidth / 2;
TimeAreaAutoPanner m_TimeAreaAutoPanner;
TimelinePoint m_StartPoint;
Vector2 m_EndPixel = Vector2.zero;
Rect m_ActiveRect;
protected abstract bool enableAutoPan { get; }
protected abstract bool CanStartRectangle(Event evt);
protected abstract bool OnFinish(Event evt, WindowState state, Rect rect);
int m_Id;
public void OnGUI(WindowState state, EventType rawType, Vector2 mousePosition)
{
if (m_Id == 0)
m_Id = GUIUtility.GetPermanentControlID();
if (state == null || state.GetWindow().treeView == null)
return;
var evt = Event.current;
if (rawType == EventType.MouseDown || evt.type == EventType.MouseDown)
{
if (state.IsCurrentEditingASequencerTextField())
return;
m_ActiveRect = TimelineWindow.instance.sequenceContentRect;
//remove the track header splitter overlap
m_ActiveRect.x += k_HeaderSplitterOverlap;
m_ActiveRect.width -= k_HeaderSplitterOverlap;
if (!m_ActiveRect.Contains(mousePosition))
return;
if (!CanStartRectangle(evt))
return;
if (enableAutoPan)
m_TimeAreaAutoPanner = new TimeAreaAutoPanner(state);
m_StartPoint = new TimelinePoint(state, mousePosition);
m_EndPixel = mousePosition;
GUIUtility.hotControl = m_Id; //HACK: Because the treeView eats all the events, steal the hotControl if necessary...
evt.Use();
return;
}
switch (evt.GetTypeForControl(m_Id))
{
case EventType.KeyDown:
{
if (GUIUtility.hotControl == m_Id)
{
if (evt.keyCode == KeyCode.Escape)
{
m_TimeAreaAutoPanner = null;
GUIUtility.hotControl = 0;
evt.Use();
}
}
return;
}
case EventType.MouseDrag:
{
if (GUIUtility.hotControl != m_Id)
return;
m_EndPixel = mousePosition;
evt.Use();
return;
}
case EventType.MouseUp:
{
if (GUIUtility.hotControl != m_Id)
return;
m_TimeAreaAutoPanner = null;
var rect = CurrentRectangle();
if (IsValidRect(rect))
OnFinish(evt, state, rect);
GUIUtility.hotControl = 0;
evt.Use();
return;
}
}
if (GUIUtility.hotControl == m_Id)
{
if (evt.type == EventType.Repaint)
{
var r = CurrentRectangle();
if (IsValidRect(r))
{
using (new GUIViewportScope(m_ActiveRect))
{
DrawRectangle(r);
}
}
}
if (m_TimeAreaAutoPanner != null)
m_TimeAreaAutoPanner.OnGUI(evt);
}
}
static void DrawRectangle(Rect rect)
{
EditorStyles.selectionRect.Draw(rect, GUIContent.none, false, false, false, false);
}
static bool IsValidRect(Rect rect)
{
return rect.width >= 1.0f && rect.height >= 1.0f;
}
Rect CurrentRectangle()
{
var startPixel = m_StartPoint.ToPixel();
return Rect.MinMaxRect(
Math.Min(startPixel.x, m_EndPixel.x),
Math.Min(startPixel.y, m_EndPixel.y),
Math.Max(startPixel.x, m_EndPixel.x),
Math.Max(startPixel.y, m_EndPixel.y));
}
}
}

View File

@@ -0,0 +1,23 @@
using UnityEngine;
namespace UnityEditor.Timeline
{
class RectangleZoom : RectangleTool
{
protected override bool enableAutoPan { get { return true; } }
protected override bool CanStartRectangle(Event evt)
{
return evt.button == 1 && evt.modifiers == (EventModifiers.Alt | EventModifiers.Shift);
}
protected override bool OnFinish(Event evt, WindowState state, Rect rect)
{
var x = state.PixelToTime(rect.xMin);
var y = state.PixelToTime(rect.xMax);
state.SetTimeAreaShownRange(x, y);
return true;
}
}
}

View File

@@ -0,0 +1,297 @@
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class ClearSelection : Manipulator
{
protected override bool MouseDown(Event evt, WindowState state)
{
// If we hit this point this means no one used the mouse down events. We can safely clear the selection if needed
if (evt.button != 0)
return false;
var window = state.GetWindow();
if (!window.sequenceRect.Contains(evt.mousePosition))
return false;
if (ItemSelection.CanClearSelection(evt))
{
SelectionManager.Clear();
return true;
}
return false;
}
}
static class ItemSelection
{
public static bool CanClearSelection(Event evt)
{
return !evt.control && !evt.command && !evt.shift;
}
public static void RangeSelectItems(ITimelineItem lastItemToSelect)
{
var selectSorted = SelectionManager.SelectedItems().ToList();
var firstSelect = selectSorted.FirstOrDefault();
if (firstSelect == null)
{
SelectionManager.Add(lastItemToSelect);
return;
}
var allTracks = TimelineEditor.inspectedAsset.flattenedTracks;
var allItems = allTracks.SelectMany(ItemsUtils.GetItems).ToList();
TimelineHelpers.RangeSelect(allItems, selectSorted, lastItemToSelect, SelectionManager.Add, SelectionManager.Remove);
}
public static ISelectable HandleSingleSelection(Event evt)
{
var item = PickerUtils.TopmostPickedItemOfType<ISelectable>(i => i.CanSelect(evt));
if (item != null)
{
var selected = item.IsSelected();
if (!selected && CanClearSelection(evt))
SelectionManager.Clear();
if (evt.modifiers == EventModifiers.Shift)
{
if (!selected)
RangeSelectItems((item as TimelineItemGUI)?.item);
}
else
{
HandleItemSelection(evt, item);
}
}
return item;
}
public static void HandleItemSelection(Event evt, ISelectable item)
{
if (evt.modifiers == ManipulatorsUtils.actionModifier)
{
if (item.IsSelected())
item.Deselect();
else
item.Select();
}
else
{
if (!item.IsSelected())
item.Select();
}
}
}
class SelectAndMoveItem : Manipulator
{
bool m_Dragged;
SnapEngine m_SnapEngine;
TimeAreaAutoPanner m_TimeAreaAutoPanner;
Vector2 m_MouseDownPosition;
bool m_HorizontalMovementDone;
bool m_VerticalMovementDone;
MoveItemHandler m_MoveItemHandler;
bool m_CycleMarkersPending;
protected override bool MouseDown(Event evt, WindowState state)
{
if (evt.alt || evt.button != 0)
return false;
m_Dragged = false;
// Cycling markers and selection are mutually exclusive operations
if (!HandleMarkerCycle() && !HandleSingleSelection(evt))
return false;
m_MouseDownPosition = evt.mousePosition;
m_VerticalMovementDone = false;
m_HorizontalMovementDone = false;
return true;
}
protected override bool MouseUp(Event evt, WindowState state)
{
if (!m_Dragged)
{
var item = PickerUtils.TopmostPickedItem() as ISelectable;
if (item == null)
return false;
if (!item.IsSelected())
return false;
// Re-selecting an item part of a multi-selection should only keep this item selected.
if (SelectionManager.Count() > 1 && ItemSelection.CanClearSelection(evt))
{
SelectionManager.Clear();
item.Select();
return true;
}
if (m_CycleMarkersPending)
{
m_CycleMarkersPending = false;
TimelineMarkerClusterGUI.CycleMarkers();
return true;
}
return false;
}
m_TimeAreaAutoPanner = null;
DropItems();
m_SnapEngine = null;
m_MoveItemHandler = null;
state.Evaluate();
state.RemoveCaptured(this);
m_Dragged = false;
TimelineCursors.ClearCursor();
return true;
}
protected override bool DoubleClick(Event evt, WindowState state)
{
return MouseDown(evt, state) && MouseUp(evt, state);
}
protected override bool MouseDrag(Event evt, WindowState state)
{
if (state.editSequence.isReadOnly)
return false;
// case 1099285 - ctrl-click can cause no clips to be selected
var selectedItemsGUI = SelectionManager.SelectedItems();
if (!selectedItemsGUI.Any())
{
m_Dragged = false;
return false;
}
const float hDeadZone = 5.0f;
const float vDeadZone = 5.0f;
bool vDone = m_VerticalMovementDone || Math.Abs(evt.mousePosition.y - m_MouseDownPosition.y) > vDeadZone;
bool hDone = m_HorizontalMovementDone || Math.Abs(evt.mousePosition.x - m_MouseDownPosition.x) > hDeadZone;
m_CycleMarkersPending = false;
if (!m_Dragged)
{
var canStartMove = vDone || hDone;
if (canStartMove)
{
state.AddCaptured(this);
m_Dragged = true;
var referenceTrack = GetTrackDropTargetAt(state, m_MouseDownPosition);
foreach (var item in selectedItemsGUI)
item.gui.StartDrag();
m_MoveItemHandler = new MoveItemHandler(state);
m_MoveItemHandler.Grab(selectedItemsGUI, referenceTrack, m_MouseDownPosition);
m_SnapEngine = new SnapEngine(m_MoveItemHandler, m_MoveItemHandler, ManipulateEdges.Both,
state, m_MouseDownPosition);
m_TimeAreaAutoPanner = new TimeAreaAutoPanner(state);
}
}
if (!m_VerticalMovementDone)
{
m_VerticalMovementDone = vDone;
if (m_VerticalMovementDone)
m_MoveItemHandler.OnTrackDetach();
}
if (!m_HorizontalMovementDone)
{
m_HorizontalMovementDone = hDone;
}
if (m_Dragged)
{
if (m_HorizontalMovementDone)
m_SnapEngine.Snap(evt.mousePosition, evt.modifiers);
if (m_VerticalMovementDone)
{
var track = GetTrackDropTargetAt(state, evt.mousePosition);
m_MoveItemHandler.UpdateTrackTarget(track);
}
state.Evaluate();
}
return true;
}
public override void Overlay(Event evt, WindowState state)
{
if (!m_Dragged)
return;
if (m_TimeAreaAutoPanner != null)
m_TimeAreaAutoPanner.OnGUI(evt);
m_MoveItemHandler.OnGUI(evt);
if (!m_MoveItemHandler.allowTrackSwitch || m_MoveItemHandler.targetTrack != null)
{
TimeIndicator.Draw(state, m_MoveItemHandler.start, m_MoveItemHandler.end);
m_SnapEngine.OnGUI();
}
}
bool HandleMarkerCycle()
{
m_CycleMarkersPending = TimelineMarkerClusterGUI.CanCycleMarkers();
return m_CycleMarkersPending;
}
static bool HandleSingleSelection(Event evt)
{
return ItemSelection.HandleSingleSelection(evt) != null;
}
void DropItems()
{
// Order matters here: m_MoveItemHandler.movingItems is destroyed during call to Drop()
foreach (var movingItem in m_MoveItemHandler.movingItems)
{
foreach (var item in movingItem.items)
item.gui.StopDrag();
}
m_MoveItemHandler.Drop();
}
static TrackAsset GetTrackDropTargetAt(WindowState state, Vector2 point)
{
var track = state.spacePartitioner.GetItemsAtPosition<IRowGUI>(point).FirstOrDefault();
return track != null ? track.asset : null;
}
}
}

View File

@@ -0,0 +1,20 @@
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class TrackZoom : Manipulator
{
// only handles 'vertical' zoom. horizontal is handled in timelineGUI
protected override bool MouseWheel(Event evt, WindowState state)
{
if (EditorGUI.actionKey)
{
state.trackScale = Mathf.Min(Mathf.Max(state.trackScale + (evt.delta.y * 0.1f), 1.0f), 100.0f);
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,205 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class TrimClip : Manipulator
{
private readonly string kDurationText = L10n.Tr("Duration:");
private readonly string kSpeedText = L10n.Tr("Speed:");
class TrimClipAttractionHandler : IAttractionHandler
{
public void OnAttractedEdge(IAttractable attractable, ManipulateEdges manipulateEdges, AttractedEdge edge, double time)
{
var clipGUI = attractable as TimelineClipGUI;
if (clipGUI == null)
return;
var clipItem = ItemsUtils.ToItem(clipGUI.clip);
if (manipulateEdges == ManipulateEdges.Right)
{
bool affectTimeScale = IsAffectingTimeScale(clipGUI.clip);
EditMode.TrimEnd(clipItem, time, affectTimeScale);
}
else if (manipulateEdges == ManipulateEdges.Left)
{
bool affectTimeScale = IsAffectingTimeScale(clipGUI.clip);
EditMode.TrimStart(clipItem, time, affectTimeScale);
}
}
private bool IsAffectingTimeScale(TimelineClip clip)
{
bool autoScale = (clip.clipCaps & ClipCaps.AutoScale) == ClipCaps.AutoScale;
// TODO Do not use Event.current from here.
bool affectTimeScale = (autoScale && (Event.current.modifiers != EventModifiers.Shift))
|| (!autoScale && (Event.current.modifiers == EventModifiers.Shift));
return affectTimeScale;
}
}
bool m_IsCaptured;
TimelineClipHandle m_TrimClipHandler;
double m_OriginalDuration;
double m_OriginalTimeScale;
double m_OriginalEaseInDuration;
double m_OriginalEaseOutDuration;
bool m_UndoSaved;
SnapEngine m_SnapEngine;
readonly List<string> m_OverlayStrings = new List<string>();
static readonly double kEpsilon = 0.0000001;
protected override bool MouseDown(Event evt, WindowState state)
{
var handle = PickerUtils.TopmostPickedItem() as TimelineClipHandle;
if (handle == null)
return false;
if (handle.clipGUI.clip.GetParentTrack() != null && handle.clipGUI.clip.GetParentTrack().lockedInHierarchy)
return false;
m_TrimClipHandler = handle;
m_IsCaptured = true;
state.AddCaptured(this);
m_UndoSaved = false;
var clip = m_TrimClipHandler.clipGUI.clip;
m_OriginalDuration = clip.duration;
m_OriginalTimeScale = clip.timeScale;
m_OriginalEaseInDuration = clip.easeInDuration;
m_OriginalEaseOutDuration = clip.easeOutDuration;
RefreshOverlayStrings(m_TrimClipHandler, state);
// in ripple trim, the right edge moves and needs to snap
var edges = ManipulateEdges.Right;
if (EditMode.editType != EditMode.EditType.Ripple && m_TrimClipHandler.trimDirection == TrimEdge.Start)
edges = ManipulateEdges.Left;
m_SnapEngine = new SnapEngine(m_TrimClipHandler.clipGUI, new TrimClipAttractionHandler(), edges, state,
evt.mousePosition);
EditMode.BeginTrim(ItemsUtils.ToItem(clip), m_TrimClipHandler.trimDirection);
return true;
}
protected override bool MouseUp(Event evt, WindowState state)
{
if (!m_IsCaptured)
return false;
m_IsCaptured = false;
m_TrimClipHandler = null;
m_UndoSaved = false;
m_SnapEngine = null;
EditMode.FinishTrim();
state.captured.Clear();
return true;
}
protected override bool MouseDrag(Event evt, WindowState state)
{
if (state.editSequence.isReadOnly)
return false;
if (!m_IsCaptured)
return false;
var uiClip = m_TrimClipHandler.clipGUI;
if (!m_UndoSaved)
{
UndoExtensions.RegisterClip(uiClip.clip, L10n.Tr("Trim Clip"));
if (TimelineUtility.IsRecordableAnimationClip(uiClip.clip))
{
TimelineUndo.PushUndo(uiClip.clip.animationClip, L10n.Tr("Trim Clip"));
}
m_UndoSaved = true;
}
//Reset to original ease values. The trim operation will calculate the proper blend values.
uiClip.clip.easeInDuration = m_OriginalEaseInDuration;
uiClip.clip.easeOutDuration = m_OriginalEaseOutDuration;
if (m_SnapEngine != null)
m_SnapEngine.Snap(evt.mousePosition, evt.modifiers);
RefreshOverlayStrings(m_TrimClipHandler, state);
if (Selection.activeObject != null)
EditorUtility.SetDirty(Selection.activeObject);
// updates the duration of the graph without rebuilding
state.UpdateRootPlayableDuration(state.editSequence.duration);
return true;
}
public override void Overlay(Event evt, WindowState state)
{
if (!m_IsCaptured)
return;
EditMode.DrawTrimGUI(state, m_TrimClipHandler.clipGUI, m_TrimClipHandler.trimDirection);
bool trimStart = m_TrimClipHandler.trimDirection == TrimEdge.Start;
TimeIndicator.Draw(state, trimStart ? m_TrimClipHandler.clipGUI.start : m_TrimClipHandler.clipGUI.end);
if (m_SnapEngine != null)
m_SnapEngine.OnGUI(trimStart, !trimStart);
if (m_OverlayStrings.Count > 0)
{
const float padding = 4.0f;
var labelStyle = TimelineWindow.styles.tinyFont;
var longestLine = labelStyle.CalcSize(
new GUIContent(m_OverlayStrings.Aggregate("", (max, cur) => max.Length > cur.Length ? max : cur)));
var stringLength = longestLine.x + padding;
var lineHeight = longestLine.y + padding;
var r = new Rect(evt.mousePosition.x - (stringLength / 2.0f),
m_TrimClipHandler.clipGUI.rect.yMax,
stringLength, lineHeight);
foreach (var s in m_OverlayStrings)
{
GUI.Label(r, s, labelStyle);
r.y += lineHeight;
}
}
}
void RefreshOverlayStrings(TimelineClipHandle handle, WindowState state)
{
m_OverlayStrings.Clear();
var differenceDuration = handle.clipGUI.clip.duration - m_OriginalDuration;
m_OverlayStrings.Add($"{kDurationText} {state.timeFormat.ToTimeStringWithDelta(handle.clipGUI.clip.duration, state.referenceSequence.frameRate, differenceDuration)}");
var differenceSpeed = m_OriginalTimeScale - handle.clipGUI.clip.timeScale;
if (Math.Abs(differenceSpeed) > kEpsilon)
{
var sign = differenceSpeed > 0 ? "+" : "";
var timeScale = handle.clipGUI.clip.timeScale.ToString("f2");
var deltaSpeed = differenceSpeed.ToString("p2");
m_OverlayStrings.Add($"{kSpeedText} {timeScale} ({sign}{deltaSpeed}) ");
}
}
}
}

View File

@@ -0,0 +1,72 @@
using UnityEngine;
namespace UnityEditor.Timeline
{
class TimeAreaAutoPanner
{
readonly WindowState m_State;
readonly TimelineWindow m_Window;
readonly Rect m_ViewRect;
const float k_PixelDistanceToMaxSpeed = 100.0f;
const float k_MaxPanSpeed = 30.0f;
public TimeAreaAutoPanner(WindowState state)
{
m_State = state;
m_Window = m_State.GetWindow();
var shownRange = m_State.timeAreaShownRange;
var trackViewBounds = m_Window.sequenceRect;
m_ViewRect = Rect.MinMaxRect(m_State.TimeToPixel(shownRange.x), trackViewBounds.yMin,
m_State.TimeToPixel(shownRange.y), trackViewBounds.yMax);
}
public void OnGUI(Event evt)
{
if (evt.type != EventType.Layout)
return;
var hFactor = 0.0f;
var vFactor = 0.0f;
bool horizontalPan = GetPanFactor(evt.mousePosition.x, m_ViewRect.xMin, m_ViewRect.xMax, out hFactor);
bool verticalPan = GetPanFactor(evt.mousePosition.y, m_ViewRect.yMin, m_ViewRect.yMax, out vFactor);
if (horizontalPan)
{
var translation = m_State.timeAreaTranslation;
translation.x += hFactor * k_MaxPanSpeed;
m_State.SetTimeAreaTransform(translation, m_State.timeAreaScale);
}
if (verticalPan)
{
var translation = m_Window.treeView.scrollPosition;
translation.y -= vFactor * k_MaxPanSpeed;
m_Window.treeView.scrollPosition = translation;
}
}
static bool GetPanFactor(float v, float min, float max, out float factor)
{
factor = 0.0f;
if (v < min)
{
factor = Mathf.Clamp01((min - v) / k_PixelDistanceToMaxSpeed);
return true;
}
if (v > max)
{
factor = -Mathf.Clamp01((v - max) / k_PixelDistanceToMaxSpeed);
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,47 @@
using UnityEngine;
namespace UnityEditor.Timeline
{
static class TimeIndicator
{
static readonly Tooltip s_Tooltip = new Tooltip(DirectorStyles.Instance.displayBackground, DirectorStyles.Instance.tinyFont);
public static void Draw(WindowState state, double time)
{
var bounds = state.timeAreaRect;
bounds.xMin = Mathf.Max(bounds.xMin, state.TimeToTimeAreaPixel(time));
using (new GUIViewportScope(state.timeAreaRect))
{
s_Tooltip.text = TimeReferenceUtility.ToTimeString(time);
var tooltipBounds = s_Tooltip.bounds;
tooltipBounds.xMin = bounds.xMin - (tooltipBounds.width / 2.0f);
tooltipBounds.y = bounds.y;
s_Tooltip.bounds = tooltipBounds;
if (time >= 0)
s_Tooltip.Draw();
}
if (time >= 0)
{
Graphics.DrawLineAtTime(state, time, Color.black, true);
}
}
public static void Draw(WindowState state, double start, double end)
{
var bounds = state.timeAreaRect;
bounds.xMin = Mathf.Max(bounds.xMin, state.TimeToTimeAreaPixel(start));
bounds.xMax = Mathf.Min(bounds.xMax, state.TimeToTimeAreaPixel(end));
var color = DirectorStyles.Instance.selectedStyle.focused.textColor;
color.a = 0.12f;
EditorGUI.DrawRect(bounds, color);
Draw(state, start);
Draw(state, end);
}
}
}

View File

@@ -0,0 +1,54 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class TimelineClipGroup
{
readonly TimelineClip[] m_Clips;
readonly TimelineClip m_LeftMostClip;
readonly TimelineClip m_RightMostClip;
public TimelineClip[] clips
{
get { return m_Clips; }
}
public double start
{
get { return m_LeftMostClip.start; }
set
{
var offset = value - m_LeftMostClip.start;
foreach (var clip in m_Clips)
clip.start += offset;
}
}
public double end
{
get { return m_RightMostClip.end; }
}
public TimelineClipGroup(IEnumerable<TimelineClip> clips)
{
Debug.Assert(clips != null && clips.Any());
m_Clips = clips.ToArray();
m_LeftMostClip = null;
m_RightMostClip = null;
foreach (var clip in m_Clips)
{
if (m_LeftMostClip == null || clip.start < m_LeftMostClip.start)
m_LeftMostClip = clip;
if (m_RightMostClip == null || clip.end > m_RightMostClip.end)
m_RightMostClip = clip;
}
}
}
}

View File

@@ -0,0 +1,23 @@
using UnityEngine;
namespace UnityEditor.Timeline
{
enum TrimEdge
{
Start,
End
}
interface ITrimItemMode
{
void OnBeforeTrim(ITrimmable item, TrimEdge trimDirection);
void TrimStart(ITrimmable item, double time, bool affectTimeScale);
void TrimEnd(ITrimmable item, double time, bool affectTimeScale);
}
interface ITrimItemDrawer
{
void DrawGUI(WindowState state, Rect bounds, Color color, TrimEdge edge);
}
}

View File

@@ -0,0 +1,102 @@
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class TrimItemModeMix : ITrimItemMode, ITrimItemDrawer
{
ITrimmable m_Item;
double m_Min;
double m_Max;
public void OnBeforeTrim(ITrimmable item, TrimEdge trimDirection)
{
m_Item = item;
var sortedItems = ItemsUtils.GetItemsExcept(item.parentTrack, new[] {item})
.OfType<ITrimmable>()
.OrderBy(c => c.start);
var itemStart = (DiscreteTime)item.start;
var itemEnd = (DiscreteTime)item.end;
var overlapped = sortedItems.LastOrDefault(c => (DiscreteTime)c.start == itemStart && (DiscreteTime)c.end == itemEnd);
ITrimmable nextItem;
ITrimmable prevItem;
m_Min = 0.0;
m_Max = double.PositiveInfinity;
var blendableItem = item as IBlendable;
if (blendableItem != null && blendableItem.supportsBlending)
{
if (trimDirection == TrimEdge.Start)
{
nextItem = sortedItems.FirstOrDefault(c => (DiscreteTime)c.start >= itemStart && (DiscreteTime)c.end > itemEnd);
prevItem = sortedItems.LastOrDefault(c => (DiscreteTime)c.start <= itemStart && (DiscreteTime)c.end < itemEnd);
if (prevItem != null)
m_Min = prevItem.start + EditModeUtils.BlendDuration(prevItem, TrimEdge.Start);
if (nextItem != null)
m_Max = nextItem.start;
}
else
{
nextItem = sortedItems.FirstOrDefault(c => c != overlapped && (DiscreteTime)c.start >= itemStart && (DiscreteTime)c.end >= itemEnd);
prevItem = sortedItems.LastOrDefault(c => c != overlapped && (DiscreteTime)c.start <= itemStart && (DiscreteTime)c.end <= itemEnd);
if (prevItem != null)
m_Min = prevItem.end;
if (nextItem != null)
m_Max = nextItem.end - EditModeUtils.BlendDuration(nextItem, TrimEdge.End);
}
}
else
{
nextItem = sortedItems.FirstOrDefault(c => (DiscreteTime)c.start > itemStart);
prevItem = sortedItems.LastOrDefault(c => (DiscreteTime)c.start < itemStart);
if (prevItem != null)
m_Min = prevItem.end;
if (nextItem != null)
m_Max = nextItem.start;
}
}
public void TrimStart(ITrimmable item, double time, bool affectTimeScale)
{
time = Math.Min(Math.Max(time, m_Min), m_Max);
item.SetStart(time, affectTimeScale);
}
public void TrimEnd(ITrimmable item, double time, bool affectTimeScale)
{
time = Math.Min(Math.Max(time, m_Min), m_Max);
item.SetEnd(time, affectTimeScale);
}
public void DrawGUI(WindowState state, Rect bounds, Color color, TrimEdge edge)
{
if (EditModeUtils.HasBlends(m_Item, edge))
{
EditModeGUIUtils.DrawBoundsEdge(bounds, color, edge);
var cursorType = (edge == TrimEdge.End)
? TimelineCursors.CursorType.MixRight
: TimelineCursors.CursorType.MixLeft;
TimelineCursors.SetCursor(cursorType);
}
else
{
TimelineCursors.ClearCursor();
}
}
}
}

View File

@@ -0,0 +1,139 @@
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class TrimItemModeReplace : ITrimItemMode, ITrimItemDrawer
{
ITrimmable m_Item;
ITrimmable m_ItemToBeReplaced;
double m_ClipOriginalEdgeValue;
bool m_TrimReplace;
double m_Min;
double m_Max;
public void OnBeforeTrim(ITrimmable item, TrimEdge trimDirection)
{
m_Item = item;
var sortedClips = ItemsUtils.GetItemsExcept(item.parentTrack, new[] { item })
.OfType<ITrimmable>()
.OrderBy(c => c.start);
var clipStart = (DiscreteTime)item.start;
var clipEnd = (DiscreteTime)item.end;
var overlapped = sortedClips.LastOrDefault(c => (DiscreteTime)c.start == clipStart && (DiscreteTime)c.end == clipEnd);
ITrimmable nextItem;
ITrimmable prevItem;
m_Min = 0.0;
m_Max = double.PositiveInfinity;
if (trimDirection == TrimEdge.Start)
{
nextItem = sortedClips.FirstOrDefault(c => (DiscreteTime)c.start >= clipStart && (DiscreteTime)c.end > clipEnd);
prevItem = sortedClips.LastOrDefault(c => (DiscreteTime)c.start <= clipStart && (DiscreteTime)c.end < clipEnd);
if (prevItem != null)
m_Min = prevItem.start + EditModeUtils.BlendDuration(prevItem, TrimEdge.Start) + TimelineClip.kMinDuration;
if (nextItem != null)
m_Max = nextItem.start;
m_ItemToBeReplaced = prevItem;
if (m_ItemToBeReplaced != null)
m_ClipOriginalEdgeValue = m_ItemToBeReplaced.end;
}
else
{
nextItem = sortedClips.FirstOrDefault(c => c != overlapped && (DiscreteTime)c.start >= clipStart && (DiscreteTime)c.end >= clipEnd);
prevItem = sortedClips.LastOrDefault(c => c != overlapped && (DiscreteTime)c.start <= clipStart && (DiscreteTime)c.end <= clipEnd);
if (prevItem != null)
m_Min = prevItem.end;
if (nextItem != null)
m_Max = nextItem.end - EditModeUtils.BlendDuration(nextItem, TrimEdge.End) - TimelineClip.kMinDuration;
m_ItemToBeReplaced = nextItem;
if (m_ItemToBeReplaced != null)
m_ClipOriginalEdgeValue = m_ItemToBeReplaced.start;
}
m_TrimReplace = false;
}
public void TrimStart(ITrimmable item, double time, bool affectTimeScale)
{
time = Math.Min(Math.Max(time, m_Min), m_Max);
if (m_ItemToBeReplaced != null)
{
if (!m_TrimReplace)
m_TrimReplace = item.start >= m_ItemToBeReplaced.end;
}
time = Math.Max(time, 0.0);
item.SetStart(time, affectTimeScale);
if (m_ItemToBeReplaced != null && m_TrimReplace)
{
var prevEnd = Math.Min(item.start, m_ClipOriginalEdgeValue);
m_ItemToBeReplaced.SetEnd(prevEnd, false);
}
}
public void TrimEnd(ITrimmable item, double time, bool affectTimeScale)
{
time = Math.Min(Math.Max(time, m_Min), m_Max);
if (m_ItemToBeReplaced != null)
{
if (!m_TrimReplace)
m_TrimReplace = item.end <= m_ItemToBeReplaced.start;
}
item.SetEnd(time, affectTimeScale);
if (m_ItemToBeReplaced != null && m_TrimReplace)
{
var nextStart = Math.Max(item.end, m_ClipOriginalEdgeValue);
m_ItemToBeReplaced.SetStart(nextStart, false);
}
}
public void DrawGUI(WindowState state, Rect bounds, Color color, TrimEdge edge)
{
bool shouldDraw = m_ItemToBeReplaced != null && (edge == TrimEdge.End && m_Item.end > m_ClipOriginalEdgeValue) ||
(edge == TrimEdge.Start && m_Item.start < m_ClipOriginalEdgeValue);
if (shouldDraw)
{
var cursorType = TimelineCursors.CursorType.Replace;
if (EditModeUtils.HasBlends(m_Item, edge))
{
color = DirectorStyles.kMixToolColor;
cursorType = (edge == TrimEdge.End)
? TimelineCursors.CursorType.MixRight
: TimelineCursors.CursorType.MixLeft;
}
EditModeGUIUtils.DrawBoundsEdge(bounds, color, edge);
TimelineCursors.SetCursor(cursorType);
}
else
{
TimelineCursors.ClearCursor();
}
}
}
}

View File

@@ -0,0 +1,97 @@
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
class TrimItemModeRipple : ITrimItemMode, ITrimItemDrawer
{
double m_OriginalClipStart;
double m_OriginalClipEnd;
ITrimmable[] m_NextItems;
double m_BlendDuration;
double m_TrimStartShift;
public void OnBeforeTrim(ITrimmable item, TrimEdge trimDirection)
{
m_OriginalClipStart = item.start;
m_OriginalClipEnd = item.end;
m_TrimStartShift = 0.0;
var sortedClips = ItemsUtils.GetItemsExcept(item.parentTrack, new[] { item })
.OfType<ITrimmable>()
.OrderBy(c => c.start);
var clipStart = (DiscreteTime)item.start;
var clipEnd = (DiscreteTime)item.end;
m_NextItems = sortedClips.Where(c => (DiscreteTime)c.start >= clipStart && (DiscreteTime)c.end >= clipEnd).ToArray();
var overlapped = sortedClips.LastOrDefault(c => (DiscreteTime)c.start == clipStart && (DiscreteTime)c.end == clipEnd);
if (overlapped != null)
{
m_BlendDuration = overlapped.end - overlapped.start;
}
else
{
m_BlendDuration = 0.0;
var prevClip = sortedClips.LastOrDefault(c => (DiscreteTime)c.start <= clipStart && (DiscreteTime)c.end <= clipEnd);
if (prevClip != null)
m_BlendDuration += Math.Max(prevClip.end - item.start, 0.0);
var nextClip = sortedClips.FirstOrDefault(c => (DiscreteTime)c.start >= clipStart && (DiscreteTime)c.end >= clipEnd);
if (nextClip != null)
m_BlendDuration += Math.Max(item.end - nextClip.start, 0.0);
}
}
public void TrimStart(ITrimmable item, double time, bool affectTimeScale)
{
var prevEnd = item.end;
// HACK If time is negative, make sure we shift the SetStart operation to a positive space.
if (time < 0.0)
m_TrimStartShift = Math.Max(-time, m_TrimStartShift);
item.start = m_OriginalClipEnd - item.duration + m_TrimStartShift;
time += m_TrimStartShift;
if (m_BlendDuration > 0.0)
time = Math.Min(time, item.end - m_BlendDuration);
item.SetStart(time, affectTimeScale);
item.start = m_OriginalClipStart;
var offset = item.end - prevEnd;
foreach (var timelineClip in m_NextItems)
timelineClip.start += offset;
}
public void TrimEnd(ITrimmable item, double time, bool affectTimeScale)
{
var prevEnd = item.end;
if (m_BlendDuration > 0.0)
time = Math.Max(time, item.start + m_BlendDuration);
item.SetEnd(time, affectTimeScale);
var offset = item.end - prevEnd;
foreach (var timelineClip in m_NextItems)
timelineClip.start += offset;
}
public void DrawGUI(WindowState state, Rect bounds, Color color, TrimEdge edge)
{
EditModeGUIUtils.DrawBoundsEdge(bounds, color, edge);
TimelineCursors.SetCursor(TimelineCursors.CursorType.Ripple);
}
}
}

View File

@@ -0,0 +1,27 @@
using UnityEngine;
namespace UnityEditor.Timeline
{
static class EditModeGUIUtils
{
public static void DrawBoundsEdge(Rect bounds, Color color, TrimEdge edge, float width = 4.0f)
{
var r = bounds;
r.yMin += 2.0f;
r.yMax -= 2.0f;
r.width = width;
r.x = edge == TrimEdge.End ? bounds.xMax : bounds.xMin - width;
EditorGUI.DrawRect(r, color);
}
public static void DrawOverlayRect(Rect bounds, Color overlayColor)
{
var c = overlayColor;
c.a = 0.2f;
EditorGUI.DrawRect(bounds, c);
EditorGUI.DrawOutline(bounds, 1.0f, new Color(1.0f, 1.0f, 1.0f, 0.5f));
}
}
}

View File

@@ -0,0 +1,138 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
static class EditModeMixUtils
{
static readonly List<PlacementValidity> k_UnrecoverablePlacements = new List<PlacementValidity>
{
PlacementValidity.InvalidIsWithin,
PlacementValidity.InvalidStartsInBlend,
PlacementValidity.InvalidContainsBlend,
PlacementValidity.InvalidOverlapWithNonBlendableClip
};
public static bool CanInsert(IEnumerable<ItemsPerTrack> itemsGroups)
{
foreach (var itemsGroup in itemsGroups)
{
List<ITimelineItem> siblings = ItemsUtils.GetItemsExcept(itemsGroup.targetTrack, itemsGroup.items).ToList();
foreach (var item in itemsGroup.items)
{
var placementValidity = GetPlacementValidity(item, siblings);
if (k_UnrecoverablePlacements.Contains(placementValidity))
{
return false;
}
}
}
return true;
}
//Corrects clips durations to fit at insertion point, if needed
public static void PrepareItemsForInsertion(IEnumerable<ItemsPerTrack> itemsGroups)
{
foreach (var itemsGroup in itemsGroups)
{
var siblings = ItemsUtils.GetItemsExcept(itemsGroup.targetTrack, itemsGroup.items);
foreach (var item in itemsGroup.items.OfType<ITrimmable>())
{
var eatenItems = siblings.Where(c => EditModeUtils.IsItemWithinRange(c, item.start, item.end)).ToList();
var intersectedItem = EditModeUtils.GetFirstIntersectedItem(siblings, item.end);
if (intersectedItem != null)
eatenItems.Add(intersectedItem);
var blendableItems = eatenItems.OfType<IBlendable>();
if (blendableItems.Any())
{
var minTime = blendableItems.Min(c => c.end - c.rightBlendDuration);
if (item.end > minTime)
item.SetEnd(minTime, false);
}
}
}
}
public static PlacementValidity GetPlacementValidity(ITimelineItem item, IEnumerable<ITimelineItem> otherItems)
{
if (item.duration <= 0.0)
return PlacementValidity.Valid; //items without any duration can always be placed
var sortedItems = otherItems.Where(i => i.duration > 0.0).OrderBy(c => c.start);
var candidates = new List<ITimelineItem>();
foreach (var sortedItem in sortedItems)
{
if ((DiscreteTime)sortedItem.start >= (DiscreteTime)item.end)
{
// No need to process further
break;
}
if ((DiscreteTime)sortedItem.end <= (DiscreteTime)item.start)
{
// Skip
continue;
}
candidates.Add(sortedItem);
}
var discreteStart = (DiscreteTime)item.start;
var discreteEnd = (DiscreteTime)item.end;
// Note: Order of tests matters
for (int i = 0, n = candidates.Count; i < n; i++)
{
var candidate = candidates[i];
var blendItem = item as IBlendable;
if (blendItem != null && blendItem.supportsBlending)
{
if (EditModeUtils.Contains(candidate.start, candidate.end, item))
return PlacementValidity.InvalidIsWithin;
if (i < n - 1)
{
var nextCandidate = candidates[i + 1];
var discreteNextCandidateStart = (DiscreteTime)nextCandidate.start;
var discreteCandidateEnd = (DiscreteTime)candidate.end;
if (discreteCandidateEnd > discreteNextCandidateStart)
{
if (discreteStart >= discreteNextCandidateStart)
{
// Note: In case the placement is fully within a blend,
// InvalidStartsInBlend MUST have priority
return PlacementValidity.InvalidStartsInBlend;
}
if (discreteEnd > discreteNextCandidateStart && discreteEnd <= discreteCandidateEnd)
return PlacementValidity.InvalidEndsInBlend;
if (discreteStart < discreteNextCandidateStart && discreteEnd > discreteCandidateEnd)
return PlacementValidity.InvalidContainsBlend;
}
}
if (EditModeUtils.Contains(item.start, item.end, candidate))
return PlacementValidity.InvalidContains;
}
else
{
if (EditModeUtils.Overlaps(item, candidate.start, candidate.end)
|| EditModeUtils.Overlaps(candidate, item.start, item.end))
return PlacementValidity.InvalidOverlapWithNonBlendableClip;
}
}
return PlacementValidity.Valid;
}
}
}

View File

@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
static class EditModeReplaceUtils
{
public static void Insert(IEnumerable<ItemsPerTrack> itemsGroups)
{
foreach (var itemsGroup in itemsGroups)
{
Insert(itemsGroup.targetTrack, itemsGroup.items);
}
}
static void Insert(TrackAsset track, IEnumerable<ITimelineItem> items)
{
if (track == null) return;
var orderedItems = ItemsUtils.GetItemsExcept(track, items)
.OfType<ITrimmable>()
.OrderBy(i => i.start).ToArray();
foreach (var item in items.OfType<ITrimmable>())
{
var from = item.start;
var to = item.end;
var overlappedItems = orderedItems.Where(i => EditModeUtils.Overlaps(i, from, to));
foreach (var overlappedItem in overlappedItems)
{
if (EditModeUtils.IsItemWithinRange(overlappedItem, from, to))
{
overlappedItem.Delete();
}
else
{
if (overlappedItem.start >= from)
overlappedItem.TrimStart(to);
else
overlappedItem.TrimEnd(from);
}
}
var includingItems = orderedItems.Where(c => c.start<from && c.end> to);
foreach (var includingItem in includingItems)
{
var newItem = includingItem.CloneTo(track, includingItem.start) as ITrimmable;
includingItem.TrimStart(to);
if (newItem != null)
newItem.SetEnd(from, false);
}
}
}
}
}

View File

@@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
static class EditModeRippleUtils
{
public static void Insert(IEnumerable<ItemsPerTrack> itemsGroups)
{
var start = double.MaxValue;
var end = double.MinValue;
foreach (var itemsGroup in itemsGroups)
{
start = Math.Min(start, itemsGroup.items.Min(c => c.start));
end = Math.Max(end, itemsGroup.items.Max(c => c.end));
}
var offset = 0.0;
var discreteStart = (DiscreteTime)start;
var discreteEnd = (DiscreteTime)end;
var itemTypes = ItemsUtils.GetItemTypes(itemsGroups);
var siblingsToRipple = new List<ITimelineItem>();
foreach (var itemsGroup in itemsGroups)
{
//can only ripple items of the same type as those selected
siblingsToRipple.AddRange(ItemsUtils.GetItemsExcept(itemsGroup.targetTrack, itemsGroup.items).Where(i => itemTypes.Contains(i.GetType())));
foreach (var item in siblingsToRipple)
{
var discreteItemStart = (DiscreteTime)item.start;
var discreteItemEnd = (DiscreteTime)item.end;
if ((discreteItemStart < discreteStart && discreteItemEnd > discreteStart) || (discreteItemStart >= discreteStart && discreteItemStart < discreteEnd))
offset = Math.Max(offset, end - item.start);
}
}
if (offset > 0.0)
{
foreach (var sibling in siblingsToRipple)
{
if ((DiscreteTime)sibling.end > (DiscreteTime)start)
sibling.start += offset;
}
}
}
public static void Remove(IEnumerable<ItemsPerTrack> itemsGroups)
{
foreach (var itemsGroup in itemsGroups)
Remove(itemsGroup.targetTrack, itemsGroup.items);
}
static void Remove(TrackAsset track, IEnumerable<ITimelineItem> items)
{
if (track == null) return;
//can only ripple items of the same type as those selected
var itemTypes = ItemsUtils.GetItemTypes(items);
var siblingsToRipple = ItemsUtils.GetItemsExcept(track, items)
.Where(i => itemTypes.Contains(i.GetType()))
.OrderBy(c => c.start)
.ToArray();
var orderedItems = items
.OrderBy(c => c.start)
.ToArray();
var cumulativeOffset = 0.0;
foreach (var item in orderedItems)
{
var offset = item.end - item.start;
var start = item.start - cumulativeOffset;
var end = item.end - cumulativeOffset;
var nextItem = siblingsToRipple.FirstOrDefault(c => (DiscreteTime)c.start > (DiscreteTime)start && (DiscreteTime)c.start < (DiscreteTime)end);
if (nextItem != null)
{
offset -= end - nextItem.start;
}
var prevItem = siblingsToRipple.FirstOrDefault(c => (DiscreteTime)c.end > (DiscreteTime)start && (DiscreteTime)c.end < (DiscreteTime)end);
if (prevItem != null)
{
offset -= prevItem.end - start;
}
if (offset <= 0.0)
continue;
cumulativeOffset += offset;
for (int i = siblingsToRipple.Length - 1; i >= 0; --i)
{
var c = siblingsToRipple[i];
if ((DiscreteTime)c.start < (DiscreteTime)start)
break;
c.start = c.start - offset;
}
}
}
}
}

View File

@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
static class EditModeUtils
{
public static void Delete(IEnumerable<ITimelineItem> items)
{
if (items == null)
return;
foreach (var item in items)
item.Delete();
}
public static void SetStart(IEnumerable<ITimelineItem> items, double time)
{
var offset = time - items.Min(c => c.start);
foreach (var item in items)
item.start += offset;
}
public static void SetParentTrack(IEnumerable<ITimelineItem> items, TrackAsset parentTrack)
{
foreach (var item in items)
{
if (item.parentTrack == parentTrack)
continue;
item.parentTrack = parentTrack;
var clipGUI = item.gui as TimelineClipGUI;
if (clipGUI != null)
{
clipGUI.clipCurveEditor = null;
}
}
}
public static ITimelineItem GetFirstIntersectedItem(IEnumerable<ITimelineItem> items, double time)
{
return items.FirstOrDefault(c => Intersects(time, c.start, c.end));
}
static bool Intersects(double time, double start, double end)
{
var discreteTime = (DiscreteTime)time;
return discreteTime > (DiscreteTime)start && discreteTime < (DiscreteTime)end;
}
public static bool Overlaps(ITimelineItem item, double from, double to)
{
var discreteFrom = (DiscreteTime)from;
var discreteTo = (DiscreteTime)to;
var discreteStart = (DiscreteTime)item.start;
if (discreteStart >= discreteFrom && discreteStart < discreteTo)
return true;
var discreteEnd = (DiscreteTime)item.end;
if (discreteEnd > discreteFrom && discreteEnd <= discreteTo)
return true;
return false;
}
public static bool IsItemWithinRange(ITimelineItem item, double from, double to)
{
return (DiscreteTime)item.start >= (DiscreteTime)from && (DiscreteTime)item.end <= (DiscreteTime)to;
}
public static bool IsRangeWithinItem(double from, double to, ITimelineItem item)
{
return (DiscreteTime)from >= (DiscreteTime)item.start && (DiscreteTime)to <= (DiscreteTime)item.end;
}
public static bool Contains(double from, double to, ITimelineItem item)
{
return (DiscreteTime)from < (DiscreteTime)item.start && (DiscreteTime)to > (DiscreteTime)item.end;
}
public static bool HasBlends(ITimelineItem item, TrimEdge edge)
{
var blendable = item as IBlendable;
if (blendable == null) return false;
return edge == TrimEdge.Start && blendable.hasLeftBlend || edge == TrimEdge.End && blendable.hasRightBlend;
}
public static double BlendDuration(ITimelineItem item, TrimEdge edge)
{
var blendable = item as IBlendable;
if (blendable == null) return 0.0;
return edge == TrimEdge.Start ? blendable.leftBlendDuration : blendable.rightBlendDuration;
}
public static bool IsInfiniteTrack(TrackAsset track)
{
var aTrack = track as AnimationTrack;
return aTrack != null && aTrack.CanConvertToClipMode();
}
public static void GetInfiniteClipBoundaries(TrackAsset track, out double start, out double end)
{
var info = AnimationClipCurveCache.Instance.GetCurveInfo(((AnimationTrack)track).infiniteClip);
if (info.keyTimes.Length > 0)
{
start = info.keyTimes.Min();
end = info.keyTimes.Max();
}
else
{
start = end = 0.0f;
}
}
}
}

View File

@@ -0,0 +1,19 @@
using UnityEngine;
namespace UnityEditor.Timeline
{
static class ManipulatorsUtils
{
public static EventModifiers actionModifier
{
get
{
if (Application.platform == RuntimePlatform.OSXEditor ||
Application.platform == RuntimePlatform.OSXPlayer)
return EventModifiers.Command;
return EventModifiers.Control;
}
}
}
}

View File

@@ -0,0 +1,13 @@
namespace UnityEditor.Timeline
{
enum PlacementValidity
{
Valid,
InvalidContains,
InvalidIsWithin,
InvalidStartsInBlend,
InvalidEndsInBlend,
InvalidContainsBlend,
InvalidOverlapWithNonBlendableClip
}
}

View File

@@ -0,0 +1,316 @@
using System;
using UnityEngine;
namespace UnityEditor.Timeline.Actions
{
/// <summary>
/// Priorities for menu item ordering. See <see cref="MenuEntryAttribute"/>.
/// </summary>
public static class MenuPriority
{
/// <summary>
/// Default priority for a menu. It will add at the end of the context menu before the 'add' menus.
/// </summary>
public const int defaultPriority = 9000;
/// <summary>
/// This value is the separator difference that will be needed to create a separator between menu item.
/// </summary>
public const int separatorAt = 1000;
/// <summary>
/// Priorities for Timeline Action menu items.
/// </summary>
public static class TimelineActionSection
{
/// <summary>
/// First Timeline action menu item priority.
/// </summary>
public const int start = 1000;
/// <summary>
/// Copy menu item priority.
/// </summary>
public const int copy = start + 100;
/// <summary>
/// Paste menu item priority.
/// </summary>
public const int paste = start + 200;
/// <summary>
/// Duplicate menu item priority.
/// </summary>
public const int duplicate = start + 300;
/// <summary>
/// Delete menu item priority.
/// </summary>
public const int delete = start + 400;
/// <summary>
/// Keyframe All animated item priority.
/// </summary>
public const int keyAllAnimated = start + 450;
/// <summary>
/// Match Content menu item priority.
/// </summary>
public const int matchContent = start + 500;
}
/// <summary>
/// Priorities for Track action menu items.
/// </summary>
public static class TrackActionSection
{
/// <summary>
/// First Track action menu item priority.
/// </summary>
public const int start = TimelineActionSection.start + separatorAt;
/// <summary>
/// Lock track menu item priority.
/// </summary>
public const int lockTrack = start + 100;
/// <summary>
/// Lock selected track menu item priority.
/// </summary>
public const int lockSelected = start + 150;
/// <summary>
/// Mute track menu item priority.
/// </summary>
public const int mute = start + 200;
/// <summary>
/// Mute selected track menu item priority.
/// </summary>
public const int muteSelected = start + 250;
/// <summary>
/// Show hide marker menu item priority.
/// </summary>
public const int showHideMarkers = start + 300;
/// <summary>
/// Remove Invalid Markers menu item priority.
/// </summary>
public const int removeInvalidMarkers = start + 400;
/// <summary>
/// Edit Track In Animation Window menu item priority.
/// </summary>
public const int editInAnimationWindow = start + 800;
}
/// <summary>
/// Priorities for Add Tracks menu items.
/// </summary>
public static class AddTrackMenu
{
/// <summary>
/// First Add Track menu item priority.
/// </summary>
public const int start = TrackActionSection.start + separatorAt;
/// <summary>
/// Add Layer Track menu item priority.
/// </summary>
public const int addLayerTrack = start;
}
/// <summary>
/// Priorities for Clip edition menu items.
/// </summary>
public static class ClipEditActionSection
{
/// <summary>
/// First Edit Clip menu item priority.
/// </summary>
public const int start = AddTrackMenu.start + separatorAt;
/// <summary>
/// Edit Clip In Animation Window menu item priority.
/// </summary>
public const int editInAnimationWindow = start + 100;
/// <summary>
/// Edit Clip Sub Timeline menu item priority.
/// </summary>
public const int editSubTimeline = start + 200;
}
/// <summary>
/// Priorities for Clip action menu items.
/// </summary>
public static class ClipActionSection
{
/// <summary>
/// First Clip action menu item priority.
/// </summary>
public const int start = ClipEditActionSection.start + separatorAt;
/// <summary>
/// Trim start menu item priority.
/// </summary>
public const int trimStart = start + 100;
/// <summary>
/// Trim end menu item priority.
/// </summary>
public const int trimEnd = start + 110;
/// <summary>
/// Split menu item priority.
/// </summary>
public const int split = start + 120;
/// <summary>
/// Complete Last Loop menu item priority.
/// </summary>
public const int completeLastLoop = start + separatorAt;
/// <summary>
/// Trim Last Loop menu item priority.
/// </summary>
public const int trimLastLoop = start + separatorAt + 110;
/// <summary>
/// Match duration menu item priority.
/// </summary>
public const int matchDuration = start + separatorAt + 120;
/// <summary>
/// Double Speed menu item priority.
/// </summary>
public const int doubleSpeed = start + 2 * separatorAt;
/// <summary>
/// Half Speed menu item priority.
/// </summary>
public const int halfSpeed = start + 2 * separatorAt + 110;
/// <summary>
/// Reset Duration menu item priority.
/// </summary>
public const int resetDuration = start + 3 * separatorAt;
/// <summary>
/// Reset Speed menu item priority.
/// </summary>
public const int resetSpeed = start + 3 * separatorAt + 110;
/// <summary>
/// Reset All menu item priority.
/// </summary>
public const int resetAll = start + 3 * separatorAt + 120;
/// <summary>
/// Tile menu item priority.
/// </summary>
public const int tile = start + 300;
/// <summary>
/// Find source asset menu item priority.
/// </summary>
public const int findSourceAsset = start + 400;
}
/// <summary>
/// Priorities for Marker action menu items.
/// </summary>
public static class MarkerActionSection
{
/// <summary>
/// First Marker action menu item priority.
/// </summary>
public const int start = ClipActionSection.start + separatorAt;
}
/// <summary>
/// Priorities for custom Timeline action menu items.
/// </summary>
public static class CustomTimelineActionSection
{
/// <summary>
/// First custom Timeline action menu item priority.
/// </summary>
public const int start = MarkerActionSection.start + separatorAt;
}
/// <summary>
/// Priorities for Custom Track action menu items.
/// </summary>
public static class CustomTrackActionSection
{
/// <summary>
/// First custom track action menu item priority.
/// </summary>
public const int start = CustomTimelineActionSection.start + separatorAt;
/// <summary>
/// Convert Animation to clip menu item priority.
/// </summary>
public const int convertToClipMode = start + 100;
/// <summary>
/// Convert Clip to animation menu item priority.
/// </summary>
public const int convertFromClipMode = start + 200;
/// <summary>
/// Apply Track offset menu item priority.
/// </summary>
public const int applyTrackOffset = start + 300;
/// <summary>
/// Apply Scene offset menu item priority.
/// </summary>
public const int applySceneOffset = start + 310;
/// <summary>
/// Apply Auto offset menu item priority.
/// </summary>
public const int applyAutoOffset = start + 320;
/// <summary>
/// Add override track menu item priority.
/// </summary>
public const int addOverrideTrack = start + 500;
/// <summary>
/// User custom track action menu item priority.
/// </summary>
public const int customTrackAction = start + 900;
}
/// <summary>
/// Custom clip action menu item priority.
/// </summary>
public static class CustomClipActionSection
{
/// <summary>
/// First custom clip action menu item priority.
/// </summary>
public const int start = CustomTrackActionSection.start + separatorAt;
/// <summary>
/// Match previous menu item priority.
/// </summary>
public const int matchPrevious = start + 100;
/// <summary>
/// Match next menu item priority.
/// </summary>
public const int matchNext = start + 110;
/// <summary>
/// Reset offset menu item priority.
/// </summary>
public const int resetOffset = start + 120;
/// <summary>
/// User custom clip action menu item priority.
/// </summary>
public const int customClipAction = start + 900;
}
/// <summary>
/// Priorities for menu entries to create Timeline items.
/// </summary>
public static class AddItem
{
/// <summary>
/// Add group menu item priority.
/// </summary>
public const int addGroup = defaultPriority + separatorAt;
/// <summary>
/// Add track menu item priority.
/// </summary>
public const int addTrack = addGroup + separatorAt;
/// <summary>
/// Add custom track menu item priority.
/// </summary>
public const int addCustomTrack = addTrack + separatorAt;
/// <summary>
/// Add clip menu item priority.
/// </summary>
public const int addClip = addCustomTrack + separatorAt;
/// <summary>
/// Add custom clip menu item priority.
/// </summary>
public const int addCustomClip = addClip + separatorAt;
/// <summary>
/// Add marker menu item priority.
/// </summary>
public const int addMarker = addCustomClip + separatorAt;
/// <summary>
/// Add custom marker menu item priority.
/// </summary>
public const int addCustomMarker = addMarker + separatorAt;
}
}
}

View File

@@ -0,0 +1,212 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
namespace UnityEditor.Timeline
{
[CustomEditor(typeof(ControlPlayableAsset)), CanEditMultipleObjects]
class ControlPlayableInspector : Editor
{
static class Styles
{
static string s_DisabledBecauseOfSelfControlTooltip = "Must be disabled when the Source Game Object references the same PlayableDirector component that is being controlled";
public static readonly GUIContent activationContent = L10n.TextContent("Control Activation", "When checked the clip will control the active state of the source game object");
public static readonly GUIContent activationDisabledContent = L10n.TextContent("Control Activation", s_DisabledBecauseOfSelfControlTooltip);
public static readonly GUIContent prefabContent = L10n.TextContent("Prefab", "A prefab to instantiate as a child object of the source game object");
public static readonly GUIContent advancedContent = L10n.TextContent("Advanced");
public static readonly GUIContent updateParticleSystemsContent = L10n.TextContent("Control Particle Systems", "Synchronize the time between the clip and any particle systems on the game object");
public static readonly GUIContent updatePlayableDirectorContent = L10n.TextContent("Control Playable Directors", "Synchronize the time between the clip and any playable directors on the game object");
public static readonly GUIContent updatePlayableDirectorDisabledContent = L10n.TextContent("Control Playable Directors", s_DisabledBecauseOfSelfControlTooltip);
public static readonly GUIContent updateITimeControlContent = L10n.TextContent("Control ITimeControl", "Synchronize the time between the clip and any Script that implements the ITimeControl interface on the game object");
public static readonly GUIContent updateHierarchy = L10n.TextContent("Control Children", "Search child game objects for particle systems and playable directors");
public static readonly GUIContent randomSeedContent = L10n.TextContent("Random Seed", "A random seed to provide the particle systems for consistent previews. This will only be used on particle systems where AutoRandomSeed is on.");
public static readonly GUIContent postPlayableContent = L10n.TextContent("Post Playback", "The active state to the leave the game object when the timeline is finished. \n\nRevert will leave the game object in the state it was prior to the timeline being run");
}
SerializedProperty m_SourceObject;
SerializedProperty m_PrefabObject;
SerializedProperty m_UpdateParticle;
SerializedProperty m_UpdateDirector;
SerializedProperty m_UpdateITimeControl;
SerializedProperty m_SearchHierarchy;
SerializedProperty m_UseActivation;
SerializedProperty m_PostPlayback;
SerializedProperty m_RandomSeed;
bool m_CycleReference;
GUIContent m_SourceObjectLabel = new GUIContent();
// the director that the selection was made with. Normally this matches the active director in timeline,
// but persists if the active timeline changes (case 962516)
private PlayableDirector contextDirector
{
get
{
if (serializedObject == null)
return null;
return serializedObject.context as PlayableDirector;
}
}
public void OnEnable()
{
if (target == null) // case 946080
return;
m_SourceObject = serializedObject.FindProperty("sourceGameObject");
m_PrefabObject = serializedObject.FindProperty("prefabGameObject");
m_UpdateParticle = serializedObject.FindProperty("updateParticle");
m_UpdateDirector = serializedObject.FindProperty("updateDirector");
m_UpdateITimeControl = serializedObject.FindProperty("updateITimeControl");
m_SearchHierarchy = serializedObject.FindProperty("searchHierarchy");
m_UseActivation = serializedObject.FindProperty("active");
m_PostPlayback = serializedObject.FindProperty("postPlayback");
m_RandomSeed = serializedObject.FindProperty("particleRandomSeed");
CheckForCyclicReference();
}
public override void OnInspectorGUI()
{
if (target == null)
return;
serializedObject.Update();
m_SourceObjectLabel.text = m_SourceObject.displayName;
if (m_PrefabObject.objectReferenceValue != null)
m_SourceObjectLabel.text = L10n.Tr("Parent Object");
bool selfControlled = false;
EditorGUI.BeginChangeCheck();
using (new GUIMixedValueScope(m_SourceObject.hasMultipleDifferentValues))
EditorGUILayout.PropertyField(m_SourceObject, m_SourceObjectLabel);
var sourceGameObject = m_SourceObject.exposedReferenceValue as GameObject;
selfControlled = m_PrefabObject.objectReferenceValue == null && TimelineWindow.instance != null && TimelineWindow.instance.state != null &&
contextDirector != null && sourceGameObject == contextDirector.gameObject;
if (EditorGUI.EndChangeCheck())
{
CheckForCyclicReference();
if (!selfControlled)
DisablePlayOnAwake(sourceGameObject);
}
if (selfControlled)
{
EditorGUILayout.HelpBox(L10n.Tr("The assigned GameObject references the same PlayableDirector component being controlled."), MessageType.Warning);
}
else if (m_CycleReference)
{
EditorGUILayout.HelpBox(L10n.Tr("The assigned GameObject contains a PlayableDirector component that results in a circular reference."), MessageType.Warning);
}
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(m_PrefabObject, Styles.prefabContent);
EditorGUI.indentLevel--;
using (new EditorGUI.DisabledScope(selfControlled))
{
EditorGUILayout.PropertyField(m_UseActivation, selfControlled ? Styles.activationDisabledContent : Styles.activationContent);
if (m_UseActivation.boolValue)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(m_PostPlayback, Styles.postPlayableContent);
EditorGUI.indentLevel--;
}
}
m_SourceObject.isExpanded = EditorGUILayout.Foldout(m_SourceObject.isExpanded, Styles.advancedContent, true);
if (m_SourceObject.isExpanded)
{
EditorGUI.indentLevel++;
using (new EditorGUI.DisabledScope(selfControlled && !m_SearchHierarchy.boolValue))
{
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_UpdateDirector, selfControlled ? Styles.updatePlayableDirectorDisabledContent : Styles.updatePlayableDirectorContent);
if (EditorGUI.EndChangeCheck())
{
CheckForCyclicReference();
}
}
EditorGUILayout.PropertyField(m_UpdateParticle, Styles.updateParticleSystemsContent);
if (m_UpdateParticle.boolValue)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(m_RandomSeed, Styles.randomSeedContent);
EditorGUI.indentLevel--;
}
EditorGUILayout.PropertyField(m_UpdateITimeControl, Styles.updateITimeControlContent);
EditorGUILayout.PropertyField(m_SearchHierarchy, Styles.updateHierarchy);
EditorGUI.indentLevel--;
}
serializedObject.ApplyModifiedProperties();
}
//
// Fix for a workflow issue where scene objects with directors have play on awake by default enabled.
// This causes confusion when the director is played within another director, so we disable it on assignment
// to avoid the issue, but not force the issue on the user
public void DisablePlayOnAwake(GameObject sourceObject)
{
if (sourceObject != null && m_UpdateDirector.boolValue)
{
if (m_SearchHierarchy.boolValue)
{
var directors = sourceObject.GetComponentsInChildren<PlayableDirector>();
foreach (var d in directors)
{
DisablePlayOnAwake(d);
}
}
else
{
DisablePlayOnAwake(sourceObject.GetComponent<PlayableDirector>());
}
}
}
public void DisablePlayOnAwake(PlayableDirector director)
{
if (director == null)
return;
var obj = new SerializedObject(director);
var prop = obj.FindProperty("m_InitialState");
prop.enumValueIndex = (int)PlayState.Paused;
obj.ApplyModifiedProperties();
}
void CheckForCyclicReference()
{
serializedObject.ApplyModifiedProperties();
m_CycleReference = false;
PlayableDirector director = contextDirector;
if (contextDirector == null)
return;
foreach (var asset in targets.OfType<ControlPlayableAsset>())
{
if (ControlPlayableUtility.DetectCycle(asset, director))
{
m_CycleReference = true;
return;
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More