Rasagar/Library/PackageCache/com.unity.timeline/Editor/treeview/TimelineDragging.cs
2024-08-26 23:07:20 +03:00

645 lines
27 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.IMGUI.Controls;
using UnityEditor.Timeline;
using UnityEngine;
using UnityEngine.Timeline;
using UnityEngine.Playables;
using UnityObject = UnityEngine.Object;
namespace UnityEditor
{
class TimelineDragging : TreeViewDragging
{
public delegate bool TypeResolver(IEnumerable<Type> types, Action<Type> onComplete, string format);
private static readonly string k_SelectTrackWithBinding = L10n.Tr("Add {0}");
private static readonly string k_SelectTrackWithClip = L10n.Tr("Add Clip With {0}");
private static readonly string k_SelectClip = L10n.Tr("Add {0}");
const string k_GenericDragId = "TimelineDragging";
readonly int kDragSensitivity = 2;
readonly TimelineAsset m_Timeline;
readonly TimelineWindow m_Window;
class TimelineDragData
{
public TimelineDragData(List<TreeViewItem> draggedItems)
{
this.draggedItems = draggedItems;
}
public readonly List<TreeViewItem> draggedItems;
}
public TimelineDragging(TreeViewController treeView, TimelineWindow window, TimelineAsset data)
: base(treeView)
{
m_Timeline = data;
m_Window = window;
}
public override bool CanStartDrag(TreeViewItem targetItem, List<int> draggedItemIDs, Vector2 mouseDownPosition)
{
if (Event.current.modifiers != EventModifiers.None)
return false;
// Can only drag when starting in the track header area
if (mouseDownPosition.x > m_Window.sequenceHeaderRect.xMax)
return false;
var trackBaseGUI = targetItem as TimelineTrackBaseGUI;
if (trackBaseGUI == null || trackBaseGUI.track == null)
return false;
if (trackBaseGUI.track.lockedInHierarchy)
return false;
if (Event.current.type == EventType.MouseDrag && Mathf.Abs(Event.current.delta.y) < kDragSensitivity)
return false;
// Make sure dragged items are selected
// TODO Use similar system than the SceneHierarchyWindow in order to handle selection between treeView and tracks.
SelectionManager.Clear();
var draggedTrackGUIs = m_Window.allTracks.Where(t => draggedItemIDs.Contains(t.id));
foreach (var trackGUI in draggedTrackGUIs)
SelectionManager.Add(trackGUI.track);
return true;
}
public override void StartDrag(TreeViewItem draggedNode, List<int> draggedItemIDs)
{
DragAndDrop.PrepareStartDrag();
var tvItems = SelectionManager.SelectedTrackGUI().Cast<TreeViewItem>().ToList();
DragAndDrop.SetGenericData(k_GenericDragId, new TimelineDragData(tvItems));
DragAndDrop.objectReferences = new UnityObject[] { }; // this IS required for dragging to work
string title = draggedItemIDs.Count + (draggedItemIDs.Count > 1 ? "s" : ""); // title is only shown on OSX (at the cursor)
TimelineGroupGUI groupGui = draggedNode as TimelineGroupGUI;
if (groupGui != null)
{
title = groupGui.displayName;
}
DragAndDrop.StartDrag(title);
}
public static bool ResolveType(IEnumerable<System.Type> types, Action<Type> onComplete, string formatString)
{
if (!types.Any() || onComplete == null)
return false;
if (types.Count() == 1)
{
onComplete(types.First());
return true;
}
var menu = new GenericMenu();
var builtInTypes = types.Where(TypeUtility.IsBuiltIn).OrderBy(TypeUtility.GetDisplayName).ToArray();
var customTypes = types.Where(x => !TypeUtility.IsBuiltIn(x)).OrderBy(TypeUtility.GetDisplayName).ToArray();
foreach (var t in builtInTypes)
{
menu.AddItem(new GUIContent(string.Format(formatString, TypeUtility.GetDisplayName(t))), false, s => onComplete((System.Type)s), t);
}
if (builtInTypes.Length != 0 && customTypes.Length != 0)
menu.AddSeparator(string.Empty);
foreach (var t in customTypes)
{
menu.AddItem(new GUIContent(string.Format(formatString, TypeUtility.GetDisplayName(t))), false, s => onComplete((System.Type)s), t);
}
menu.ShowAsContext();
return true;
}
public override bool DragElement(TreeViewItem targetItem, Rect targetItemRect, int row)
{
if (TimelineWindow.instance.state.editSequence.isReadOnly)
return false;
// the drop rect contains the row rect plus additional spacing. The base drag element overlaps 1/2 the height of the next track
// which interferes with track bindings
var targetTrack = targetItem as TimelineGroupGUI;
if (row > 0 && targetTrack != null && !targetTrack.dropRect.Contains(Event.current.mousePosition))
return false;
return base.DragElement(targetItem, targetItemRect, row);
}
TreeViewItem GetNextItem(TreeViewItem item)
{
if (item == null)
return null;
if (item.parent == null)
{
int row = m_Window.treeView.data.GetRow(item.id);
var items = m_Window.treeView.data.GetRows();
if (items.Count > row + 1)
return items[row + 1];
return null;
}
var children = item.parent.children;
if (children == null)
return null;
for (int i = 0; i < children.Count - 1; i++)
{
if (children[i] == item)
return children[i + 1];
}
return null;
}
private static TrackAsset GetTrack(TreeViewItem item)
{
TimelineTrackBaseGUI baseGui = item as TimelineTrackBaseGUI;
if (baseGui == null)
return null;
return baseGui.track;
}
// The drag and drop may be over an expanded group but might be between tracks
private void HandleNestedItemGUI(ref TreeViewItem parentItem, ref TreeViewItem targetItem, ref TreeViewItem insertBefore)
{
const float kTopPad = 5;
const float kBottomPad = 5;
insertBefore = null;
if (!ShouldUseHierarchyDragAndDrop())
return;
var targetTrack = targetItem as TimelineGroupGUI;
if (targetTrack == null)
return;
var mousePosition = Event.current.mousePosition;
var dropBefore = targetTrack.rowRect.yMin + kTopPad > mousePosition.y;
var dropAfter = !(targetTrack.track is GroupTrack) && (targetTrack.rowRect.yMax - kBottomPad < mousePosition.y);
targetTrack.drawInsertionMarkerBefore = dropBefore;
targetTrack.drawInsertionMarkerAfter = dropAfter;
if (dropBefore)
{
targetItem = parentItem;
parentItem = targetItem != null ? targetItem.parent : null;
insertBefore = targetTrack;
}
else if (dropAfter)
{
targetItem = parentItem;
parentItem = targetItem != null ? targetItem.parent : null;
insertBefore = GetNextItem(targetTrack);
}
else if (targetTrack.track is GroupTrack)
{
targetTrack.isDropTarget = true;
}
}
public override DragAndDropVisualMode DoDrag(TreeViewItem parentItem, TreeViewItem targetItem, bool perform, DropPosition dropPos)
{
m_Window.isDragging = false;
var retMode = DragAndDropVisualMode.None;
var trackDragData = DragAndDrop.GetGenericData(k_GenericDragId) as TimelineDragData;
if (trackDragData != null)
{
retMode = HandleTrackDrop(parentItem, targetItem, perform, dropPos);
if (retMode == DragAndDropVisualMode.Copy && targetItem != null && Event.current.type == EventType.DragUpdated)
{
var targetActor = targetItem as TimelineGroupGUI;
if (targetActor != null)
targetActor.isDropTarget = true;
}
}
else if (DragAndDrop.objectReferences.Any())
{
var objectsBeingDropped = DragAndDrop.objectReferences.OfType<UnityObject>();
var director = m_Window.state.editSequence.director;
if (ShouldUseHierarchyDragAndDrop())
{
// for object drawing
var originalTarget = targetItem;
TreeViewItem insertBeforeItem = null;
HandleNestedItemGUI(ref parentItem, ref targetItem, ref insertBeforeItem);
var track = GetTrack(targetItem);
var parent = GetTrack(parentItem);
var insertBefore = GetTrack(insertBeforeItem);
retMode = HandleHierarchyPaneDragAndDrop(objectsBeingDropped, track, perform, m_Timeline, director, ResolveType, insertBefore);
// fallback to old clip behaviour
if (retMode == DragAndDropVisualMode.None)
{
retMode = HandleClipPaneObjectDragAndDrop(objectsBeingDropped, track, perform, m_Timeline, parent, director, m_Window.state.timeAreaShownRange.x, ResolveType, insertBefore);
}
// if we are rejected, clear any drop markers
if (retMode == DragAndDropVisualMode.Rejected && targetItem != null)
{
ClearInsertionMarkers(originalTarget);
ClearInsertionMarkers(targetItem);
ClearInsertionMarkers(parentItem);
ClearInsertionMarkers(insertBeforeItem);
}
}
else
{
var candidateTime = TimelineHelpers.GetCandidateTime(Event.current.mousePosition);
retMode = HandleClipPaneObjectDragAndDrop(objectsBeingDropped, GetTrack(targetItem), perform, m_Timeline, GetTrack(parentItem), director, candidateTime, ResolveType);
}
}
m_Window.isDragging = false;
return retMode;
}
void ClearInsertionMarkers(TreeViewItem item)
{
var trackGUI = item as TimelineTrackBaseGUI;
if (trackGUI != null)
{
trackGUI.drawInsertionMarkerAfter = false;
trackGUI.drawInsertionMarkerBefore = false;
trackGUI.isDropTarget = false;
}
}
bool ShouldUseHierarchyDragAndDrop()
{
return m_Window.state.IsEditingAnEmptyTimeline() || m_Window.state.sequencerHeaderWidth > Event.current.mousePosition.x;
}
public static DragAndDropVisualMode HandleHierarchyPaneDragAndDrop(IEnumerable<UnityObject> objectsBeingDropped, TrackAsset targetTrack, bool perform, TimelineAsset timeline, PlayableDirector director, TypeResolver typeResolver, TrackAsset insertBefore = null)
{
if (timeline == null)
return DragAndDropVisualMode.Rejected;
// if we are over a target track, defer to track binding system (implemented in TrackGUIs), unless we are a groupTrack
if (targetTrack != null && (targetTrack as GroupTrack) == null)
return DragAndDropVisualMode.Rejected;
if (targetTrack != null && targetTrack.lockedInHierarchy)
return DragAndDropVisualMode.Rejected;
var tracksWithBinding = objectsBeingDropped.SelectMany(TypeUtility.GetTracksCreatableFromObject).Distinct();
if (!tracksWithBinding.Any())
return DragAndDropVisualMode.None;
if (perform)
{
Action<Type> onResolve = trackType =>
{
foreach (var obj in objectsBeingDropped)
{
if (!obj.IsPrefab() && TypeUtility.IsTrackCreatableFromObject(obj, trackType))
{
var newTrack = TimelineHelpers.CreateTrack(timeline, trackType, targetTrack, string.Empty);
if (insertBefore != null)
{
if (targetTrack != null)
targetTrack.MoveLastTrackBefore(insertBefore);
else
timeline.MoveLastTrackBefore(insertBefore);
}
BindingUtility.BindWithEditorValidation(director, newTrack, obj);
}
}
TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);
};
typeResolver(tracksWithBinding, onResolve, k_SelectTrackWithBinding);
}
return DragAndDropVisualMode.Copy;
}
public static DragAndDropVisualMode HandleClipPaneObjectDragAndDrop(IEnumerable<UnityObject> objectsBeingDropped, TrackAsset targetTrack, bool perform, TimelineAsset timeline, TrackAsset parent, PlayableDirector director, double candidateTime, TypeResolver typeResolver, TrackAsset insertBefore = null)
{
if (timeline == null)
return DragAndDropVisualMode.Rejected;
// locked tracks always reject
if (targetTrack != null && targetTrack.lockedInHierarchy)
return DragAndDropVisualMode.Rejected;
// treat group tracks as having no track
if (targetTrack is GroupTrack)
{
parent = targetTrack;
targetTrack = null;
}
// Special case for monoscripts, since they describe the type
if (objectsBeingDropped.Any(o => o is MonoScript))
return HandleClipPaneMonoScriptDragAndDrop(objectsBeingDropped.OfType<MonoScript>(), targetTrack, perform, timeline, parent, director, candidateTime);
// no unity objects, or explicit exceptions
if (!objectsBeingDropped.Any() || objectsBeingDropped.Any(o => !ValidateObjectDrop(o)))
return DragAndDropVisualMode.Rejected;
// reject scene references if we have no context
if (director == null && objectsBeingDropped.Any(o => o.IsSceneObject()))
return DragAndDropVisualMode.Rejected;
var validTrackTypes = objectsBeingDropped.SelectMany(o => TypeUtility.GetTrackTypesForObject(o)).Distinct().ToList();
// special case for playable assets
if (objectsBeingDropped.Any(o => TypeUtility.IsConcretePlayableAsset(o.GetType())))
{
var playableAssets = objectsBeingDropped.OfType<IPlayableAsset>().Where(o => TypeUtility.IsConcretePlayableAsset(o.GetType()));
return HandleClipPanePlayableAssetDragAndDrop(playableAssets, targetTrack, perform, timeline, parent, director, candidateTime, typeResolver);
}
var markerTypes = objectsBeingDropped.SelectMany(o => TypeUtility.MarkerTypesWithFieldForObject(o)).Distinct();
// No tracks or markers support this object
if (!(markerTypes.Any() || validTrackTypes.Any()))
{
return DragAndDropVisualMode.Rejected;
}
// track is not compatible with marker
if (targetTrack != null && markerTypes.Any(o => !TypeUtility.DoesTrackSupportMarkerType(targetTrack, o)))
{
// track is not compatible with object
if (!validTrackTypes.Contains(targetTrack.GetType()))
return DragAndDropVisualMode.Rejected;
}
// there is no target track, dropping to empty space, or onto a group
if (perform)
{
// choose track and then clip
if (targetTrack == null)
{
var createdTrack = HandleTrackAndItemCreation(objectsBeingDropped, candidateTime, typeResolver, timeline, parent, validTrackTypes, insertBefore);
if (!createdTrack)
{
timeline.CreateMarkerTrack();
HandleItemCreation(objectsBeingDropped, timeline.markerTrack, candidateTime, typeResolver, true); // menu is always popped if ambiguous choice
}
}
// just choose clip/marker
else
{
HandleItemCreation(objectsBeingDropped, targetTrack, candidateTime, typeResolver, true); // menu is always popped if ambiguous choice
}
}
return DragAndDropVisualMode.Copy;
}
static bool HandleTrackAndItemCreation(IEnumerable<UnityEngine.Object> objectsBeingDropped, double candidateTime, TypeResolver typeResolver, TimelineAsset timeline, TrackAsset parent, IEnumerable<Type> validTrackTypes, TrackAsset insertBefore = null)
{
Action<Type> onResolved = t =>
{
var newTrack = TimelineHelpers.CreateTrack(timeline, t, parent, string.Empty);
if (insertBefore != null)
{
if (parent != null)
parent.MoveLastTrackBefore(insertBefore);
else
timeline.MoveLastTrackBefore(insertBefore);
}
HandleItemCreation(objectsBeingDropped, newTrack, candidateTime, typeResolver, validTrackTypes.Count() == 1); // menu is popped if ambiguous clip choice and unambiguous track choice
};
return typeResolver(validTrackTypes, t => onResolved(t), k_SelectTrackWithClip); // Did it create a track
}
static void HandleItemCreation(IEnumerable<UnityEngine.Object> objectsBeingDropped, TrackAsset targetTrack, double candidateTime, TypeResolver typeResolver, bool allowMenu)
{
var assetTypes = objectsBeingDropped.Select(o =>
TypeUtility.GetAssetTypesForObject(targetTrack.GetType(), o)
.Union(TypeUtility.MarkerTypesWithFieldForObject(o))).ToList();
Action<Type> onCreateItem = assetType =>
{
if (typeof(PlayableAsset).IsAssignableFrom(assetType))
{
TimelineHelpers.CreateClipsFromObjects(assetType, targetTrack, candidateTime,
objectsBeingDropped);
}
else
{
TimelineHelpers.CreateMarkersFromObjects(assetType, targetTrack, candidateTime, objectsBeingDropped);
}
};
var flatAssetTypes = assetTypes.SelectMany(x => x).Distinct();
// If there is a one to one mapping between assets and timeline types, no need to go through the type resolution, not ambiguous.
if (assetTypes.All(x => x.Count() <= 1))
{
foreach (var type in flatAssetTypes)
{
onCreateItem(type);
}
}
else
{
if (!allowMenu) // If we already popped a menu, and are presented with an ambiguous choice, take the first entry
{
flatAssetTypes = new[] { flatAssetTypes.First() };
}
typeResolver(flatAssetTypes, onCreateItem, k_SelectClip);
}
}
/// Handles drag and drop of a mono script.
public static DragAndDropVisualMode HandleClipPaneMonoScriptDragAndDrop(IEnumerable<MonoScript> scriptsBeingDropped, TrackAsset targetTrack, bool perform, TimelineAsset timeline, TrackAsset parent, PlayableDirector director, double candidateTime)
{
var playableAssetTypes = scriptsBeingDropped.Select(s => s.GetClass()).Where(TypeUtility.IsConcretePlayableAsset).Distinct();
if (!playableAssetTypes.Any())
return DragAndDropVisualMode.Rejected;
var targetTrackType = typeof(PlayableTrack);
if (targetTrack != null)
targetTrackType = targetTrack.GetType();
var trackAssetsTypes = TypeUtility.GetPlayableAssetsHandledByTrack(targetTrackType);
var supportedTypes = trackAssetsTypes.Intersect(playableAssetTypes);
if (!supportedTypes.Any())
return DragAndDropVisualMode.Rejected;
if (perform)
{
if (targetTrack == null)
targetTrack = TimelineHelpers.CreateTrack(timeline, targetTrackType, parent, string.Empty);
TimelineHelpers.CreateClipsFromTypes(supportedTypes, targetTrack, candidateTime);
}
return DragAndDropVisualMode.Copy;
}
public static DragAndDropVisualMode HandleClipPanePlayableAssetDragAndDrop(IEnumerable<IPlayableAsset> assetsBeingDropped, TrackAsset targetTrack, bool perform, TimelineAsset timeline, TrackAsset parent, PlayableDirector director, double candidateTime, TypeResolver typeResolver)
{
// get the list of supported track types
var assetTypes = assetsBeingDropped.Select(x => x.GetType()).Distinct();
IEnumerable<Type> supportedTypes = null;
if (targetTrack == null)
{
supportedTypes = TypeUtility.AllTrackTypes().Where(t => TypeUtility.GetPlayableAssetsHandledByTrack(t).Intersect(assetTypes).Any()).ToList();
}
else
{
supportedTypes = Enumerable.Empty<Type>();
var trackAssetTypes = TypeUtility.GetPlayableAssetsHandledByTrack(targetTrack.GetType());
if (trackAssetTypes.Intersect(assetTypes).Any())
supportedTypes = new[] { targetTrack.GetType() };
}
if (!supportedTypes.Any())
return DragAndDropVisualMode.Rejected;
if (perform)
{
Action<Type> onResolved = (t) =>
{
if (targetTrack == null)
targetTrack = TimelineHelpers.CreateTrack(timeline, t, parent, string.Empty);
var clipTypes = TypeUtility.GetPlayableAssetsHandledByTrack(targetTrack.GetType());
foreach (var asset in assetsBeingDropped)
{
if (clipTypes.Contains(asset.GetType()))
TimelineHelpers.CreateClipOnTrackFromPlayableAsset(asset, targetTrack, candidateTime);
}
};
typeResolver(supportedTypes, onResolved, k_SelectTrackWithClip);
}
return DragAndDropVisualMode.Copy;
}
static bool ValidateObjectDrop(UnityObject obj)
{
// legacy animation clips are not supported at all
AnimationClip clip = obj as AnimationClip;
if (clip != null && clip.legacy)
return false;
return !(obj is TimelineAsset);
}
public DragAndDropVisualMode HandleTrackDrop(TreeViewItem parentItem, TreeViewItem targetItem, bool perform, DropPosition dropPos)
{
((TimelineTreeView)m_Window.treeView.gui).showInsertionMarker = false;
var trackDragData = (TimelineDragData)DragAndDrop.GetGenericData(k_GenericDragId);
bool validDrag = ValidDrag(targetItem, trackDragData.draggedItems);
if (!validDrag)
return DragAndDropVisualMode.None;
var draggedTracks = trackDragData.draggedItems.OfType<TimelineGroupGUI>().Select(x => x.track).ToList();
if (draggedTracks.Count == 0)
return DragAndDropVisualMode.None;
if (parentItem != null)
{
var parentActor = parentItem as TimelineGroupGUI;
if (parentActor != null && parentActor.track != null)
{
if (parentActor.track.lockedInHierarchy)
return DragAndDropVisualMode.Rejected;
if (draggedTracks.Any(x => !TimelineCreateUtilities.ValidateParentTrack(parentActor.track, x.GetType())))
return DragAndDropVisualMode.Rejected;
}
}
var insertAfterItem = targetItem as TimelineGroupGUI;
if (insertAfterItem != null && insertAfterItem.track != null)
{
((TimelineTreeView)m_Window.treeView.gui).showInsertionMarker = true;
}
if (dropPos == DropPosition.Upon)
{
var groupGUI = targetItem as TimelineGroupGUI;
if (groupGUI != null)
groupGUI.isDropTarget = true;
}
if (perform)
{
PlayableAsset targetParent = m_Timeline;
var parentActor = parentItem as TimelineGroupGUI;
if (parentActor != null && parentActor.track != null)
targetParent = parentActor.track;
TrackAsset siblingTrack = insertAfterItem != null ? insertAfterItem.track : null;
// where the user drops after the last track, make sure to place it after all the tracks
if (targetParent == m_Timeline && dropPos == DropPosition.Below && siblingTrack == null)
{
siblingTrack = m_Timeline.GetRootTracks().LastOrDefault(x => !draggedTracks.Contains(x));
}
if (TrackExtensions.ReparentTracks(TrackExtensions.FilterTracks(draggedTracks).ToList(), targetParent, siblingTrack, dropPos == DropPosition.Above))
{
m_Window.state.Refresh();
}
}
return DragAndDropVisualMode.Move;
}
public static void OnTrackBindingDragUpdate(TrackAsset dropTarget)
{
if (DragAndDrop.objectReferences.Length == 0)
{
OnRejectTrackBindingDragUpdate();
return;
}
var trackEditor = CustomTimelineEditorCache.GetTrackEditor(dropTarget);
var isDragValid = trackEditor.IsBindingAssignableFrom_Safe(DragAndDrop.objectReferences[0], dropTarget);
if (isDragValid)
OnAcceptTrackBindingDragUpdate();
else
OnRejectTrackBindingDragUpdate();
}
static void OnAcceptTrackBindingDragUpdate()
{
DragAndDrop.visualMode = DragAndDropVisualMode.Link;
}
static void OnRejectTrackBindingDragUpdate()
{
DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
DragAndDrop.activeControlID = 0;
}
static bool ValidDrag(TreeViewItem target, List<TreeViewItem> draggedItems)
{
TreeViewItem currentParent = target;
while (currentParent != null)
{
if (draggedItems.Contains(currentParent))
return false;
currentParent = currentParent.parent;
}
// dragging into the sequence itself
return true;
}
}
}