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 TimelineActions = InstantiateClassesOfType(); public static readonly IReadOnlyList ClipActions = InstantiateClassesOfType(); public static readonly IReadOnlyList TrackActions = InstantiateClassesOfType(); public static readonly IReadOnlyList MarkerActions = InstantiateClassesOfType(); public static readonly IReadOnlyList TimelineActionsWithShortcuts = ActionsWithShortCuts(TimelineActions); public static readonly IReadOnlyList ClipActionsWithShortcuts = ActionsWithShortCuts(ClipActions); public static readonly IReadOnlyList TrackActionsWithShortcuts = ActionsWithShortCuts(TrackActions); public static readonly IReadOnlyList MarkerActionsWithShortcuts = ActionsWithShortCuts(MarkerActions); public static readonly HashSet ActionsWithAutoUndo = TypesWithAttribute(); public static TU GetCachedAction(this IReadOnlyList list) where T : TU { return list.FirstOrDefault(x => x.GetType() == typeof(T)); } public static void GetMenuEntries(IReadOnlyList actions, Vector2? mousePos, List menuItems, MenuFilter filter = MenuFilter.Default) { var globalContext = TimelineEditor.CurrentContext(mousePos); foreach (var action in actions) { try { BuildMenu(action, globalContext, menuItems, filter); } catch (Exception e) { Debug.LogException(e); } } } public static void GetMenuEntries(IReadOnlyList actions, List menuItems, MenuFilter filter = MenuFilter.Track) { var tracks = SelectionManager.SelectedTracks(); if (!tracks.Any()) return; foreach (var action in actions) { try { BuildMenu(action, tracks, menuItems, filter); } catch (Exception e) { Debug.LogException(e); } } } public static void GetMenuEntries(IReadOnlyList actions, List menuItems, MenuFilter filter = MenuFilter.Item) { 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, filter); } catch (Exception e) { Debug.LogException(e); } } } public static void GetMenuEntries(IReadOnlyList actions, List menuItems, MenuFilter filter = MenuFilter.Item) { var markers = SelectionManager.SelectedMarkers(); if (!markers.Any()) return; foreach (var action in actions) { try { BuildMenu(action, markers, menuItems, filter); } catch (Exception e) { Debug.LogException(e); } } } static void BuildMenu(TimelineAction action, ActionContext context, List menuItems, MenuFilter filter) { BuildMenu(action, action.Validate(context), () => ExecuteTimelineAction(action, context), menuItems, filter); } static void BuildMenu(TrackAction action, IEnumerable tracks, List menuItems, MenuFilter filter) { BuildMenu(action, action.Validate(tracks), () => ExecuteTrackAction(action, tracks), menuItems, filter); } static void BuildMenu(ClipAction action, IEnumerable clips, List menuItems, MenuFilter filter) { BuildMenu(action, action.Validate(clips), () => ExecuteClipAction(action, clips), menuItems, filter); } static void BuildMenu(MarkerAction action, IEnumerable markers, List menuItems, MenuFilter filter) { BuildMenu(action, action.Validate(markers), () => ExecuteMarkerAction(action, markers), menuItems, filter); } static void BuildMenu(IAction action, ActionValidity validity, GenericMenu.MenuFunction executeFunction, List menuItems, MenuFilter filter, bool isChecked = false) { var menuAttribute = action.GetType().GetCustomAttribute(false); if (menuAttribute == null || filter.ShouldFilterOut(menuAttribute)) 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 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(Event evt, IReadOnlyList actions, Func 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 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 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 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 InstantiateClassesOfType() where T : class { var typeCollection = TypeCache.GetTypesDerivedFrom(typeof(T)); var list = new List(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 ActionsWithShortCuts(IReadOnlyList list) { return list.Where(x => x.GetType().GetCustomAttributes(typeof(ShortcutAttribute), true).Length > 0).ToList(); } static HashSet TypesWithAttribute() where T : Attribute { var hashSet = new HashSet(); var typeCollection = TypeCache.GetTypesWithAttribute(typeof(T)); for (int i = 0; i < typeCollection.Count; i++) { hashSet.Add(typeCollection[i]); } return hashSet; } } }