JD2022-TU1/main/tools/framework/JD.Timeline/View/Actions/ClipViewDragDropHandler.cs

387 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using JD.Application.View.Timeline;
using JD.Collections.Extensions;
using JD.Timeline.Model;
using JD.Timeline.Model.Clips;
using JD.Timeline.Model.Utility;
using JD.Timeline.Plugin;
using JD.Timeline.View.ClipView;
using JD.Timeline.View.ClipView.Widgets;
using JD.Timeline.View.Pads;
using TeaBox;
using JD.Core;
using JD.Core.ProjectManaging;
using JD.Kernel.Classes;
using JD.Kernel.StaticHelpers;
using JD.Sequencers.Windowlesses;
using JD.Serialization;
using TeaBox.Core.Source.Gui;
using Path = System.IO.Path;
namespace JD.Timeline.View.Actions
{
enum DragDropType
{
File,
MoveModel,
PictoModel,
Lyrics
}
/// <summary>
/// Handles the drag drop logic on the Track View.
/// </summary>
public class ClipViewDragDropHandler
{
private const int ClipDurationInPixel = 200;
private readonly List<Clip> _draggingClips = new List<Clip>();
private readonly ClipViewControl _clipView;
private IDisposable _suspendEngineReplicationScope;
private DragDropType DragDropType { get; set; }
public ClipViewDragDropHandler(ClipViewControl clipView)
{
Throw.IfNull(clipView, "trackView");
_clipView = clipView;
clipView.DragOver += TrackViewDragOver;
clipView.DragDrop += TrackViewDragDrop;
clipView.DragLeave += TrackViewDragLeave;
}
#region Event Handlers
void TrackViewDragDrop(object sender, DragEventArgs e)
{
// Commit the operation.
InitializeSoundSets();
SetDraggingEdges(Edge.None);
_clipView.ClearSelection();
foreach (var clip in _draggingClips)
{
_clipView.AddToSelection(clip);
}
_draggingClips.Clear();
DisposableHelper.SafeDispose(ref _suspendEngineReplicationScope);
}
void TrackViewDragOver(object sender, DragEventArgs e)
{
bool canDrop = CanDrop;
bool isDragging = _draggingClips.Count != 0;
if (canDrop && !isDragging)
{
StartDrag(e);
}
else if (!canDrop && isDragging)
{
CancelDrag();
}
if (_draggingClips.Count > 0)
{
AssignClipBounds();
}
e.Effect = _draggingClips.Count > 0 ? DragDropEffects.Copy : DragDropEffects.None;
}
void TrackViewDragLeave(object sender, EventArgs e)
{
CancelDrag();
}
private void CancelDrag()
{
SetDraggingEdges(Edge.None);
foreach (var clip in _draggingClips)
{
_clipView.Tape.Clips.Remove(clip);
}
_draggingClips.Clear();
_clipView.Invalidate();
DisposableHelper.SafeDispose(ref _suspendEngineReplicationScope);
}
private void StartDrag(DragEventArgs e)
{
DisposableHelper.SafeDispose(ref _suspendEngineReplicationScope);
_suspendEngineReplicationScope = TimelinePlugin.Instance.BatchOperation.Begin();
_draggingClips.Clear();
_draggingClips.AddRange(CreateClipsForDragData(e));
// validate after the events are created so we can test if the current list count fits the space
// in the tracks.
if (!CanDrop)
{
CancelDrag();
}
else
{
AssignClipBounds();
}
}
#endregion
#region Utility
private void SetDraggingEdges(Edge edges)
{
foreach (var widget in DraggingClipWidgets)
{
widget.DraggingEdges = edges;
}
}
private IEnumerable<ClipWidget> DraggingClipWidgets
{
get { return _draggingClips.Select(clip => _clipView.ClipWidgets.FirstOrDefault(widget => widget.Model == clip)).NotNulls(); }
}
/// <summary>
/// True if the current position allows dropping the current events.
/// </summary>
protected bool CanDrop
{
get
{
var target = DropTarget;
return target?.Index + _draggingClips.Count - 1 < _clipView.Tape.Tracks.Count;
}
}
private TapeTrack DropTarget
{
get
{
var point = _clipView.PointToClient(Cursor.Position);
return _clipView.TrackFromPoint(point.Y);
}
}
private IEnumerable<Clip> CreateClipsForDragData(DragEventArgs e)
{
if (e.Data.GetDataPresent(System.Windows.DataFormats.Serializable))
{
var data = e.Data.GetData(System.Windows.DataFormats.Serializable);
var point = _clipView.PointToClient(Cursor.Position);
int startFrame = (int)_clipView.FrameFromPoint(point.X);
var moveModelDragData = data as MoveModelDragData;
if (moveModelDragData != null)
{
if (DropTarget is MoveTrack)
{
DragDropType = DragDropType.MoveModel;
yield return DropTarget.InstantiateMotionClip(moveModelDragData.ModelName, startFrame);
}
}
var pictoModelDragData = data as PictoModelDragData;
if (pictoModelDragData != null)
{
if (DropTarget is PictoTrack)
{
DragDropType = DragDropType.PictoModel;
yield return DropTarget.InstantiatePictogramClip(pictoModelDragData.ModelName, startFrame);
}
}
var lyricParagraph = data as LyricParagraph;
if (lyricParagraph != null)
{
if (DropTarget is KaraokeTrack)
{
DragDropType = DragDropType.Lyrics;
foreach (var line in lyricParagraph.Lines)
{
var clip = DropTarget.InstantiateKaraokeClip(line.ToString(), startFrame);
clip.IsEndOfLine = true;
yield return clip;
}
}
}
var lyricLine = data as LyricLine;
if (lyricLine != null)
{
if (DropTarget is KaraokeTrack)
{
DragDropType = DragDropType.Lyrics;
var clip = DropTarget.InstantiateKaraokeClip(lyricLine.ToString(), startFrame);
clip.IsEndOfLine = true;
yield return clip;
}
}
}
else if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
DragDropType = DragDropType.File;
var files = (string[])e.Data.GetData(DataFormats.FileDrop);
foreach (var file in files)
{
string extension = Path.GetExtension(file);
string relativeFile;
if (DataRoot.ExtractRelativePath(file, out relativeFile))
{
foreach (var newClip in CreateClipsForExtension(extension, relativeFile))
{
yield return newClip;
}
}
else
{
string dataFolder = DataPath.Value.Replace("\\", "/");
string message = $"Dropped file is not in data folder.\n (The path must be under \"{dataFolder}\")";
MessageService.MessageService.ShowBalloon(TimeSpan.FromSeconds(15), "Could not import file", message, ToolTipIcon.Error);
}
}
}
}
private IEnumerable<Clip> CreateClipsForExtension(string extension, string relativeFile)
{
string actorName = Path.GetFileNameWithoutExtension(relativeFile);
switch (extension.ToLower())
{
case FileExtensions.Animation:
yield return new AnimationClip { ActorName = actorName };
break;
case FileExtensions.Actor:
case FileExtensions.Template:
{
var absolutePath = new JD.Serialization.Path(relativeFile).getAbsolute();
var components = (JD.Serialization.Serializers.LuaSerializer.LoadObjectFromFile(absolutePath) as Actor_Template)?.COMPONENTS;
// create audio clip present in actor
if (components != null && components.Any())
{
var soundComponents = components.OfType<SoundComponent_Template>().ToList();
if (soundComponents.Any())
{
yield return new SoundSetClip { SoundSetPath = new JD.Serialization.Path(relativeFile) };
}
// stop if actor contains only audio components
if (components.Count == soundComponents.Count)
{
break;
}
}
var existingActors = TimelinePlugin.Instance.World.Actors.Select(actor => actor.Name).ToArray();
var spawner = new SpawnActorClip
{
ActorPath = new JD.Serialization.Path(relativeFile),
ActorName = actorName.MakeUnique(existingActors)
};
yield return spawner;
break;
}
case FileExtensions.SoundSet:
{
string path = Path.ChangeExtension(relativeFile, "tpl");
yield return new SoundSetClip { SoundSetPath = new JD.Serialization.Path(path) };
break;
}
case FileExtensions.Tape:
{
yield return new TapeReferenceClip { TapeId = relativeFile };
break;
}
case FileExtensions.Vibration:
{
yield return new VibrationClip { VibrationFilePath = new JD.Serialization.Path(relativeFile) };
break;
}
}
}
private void InitializeSoundSets()
{
foreach (SoundSetClip soundSet in _draggingClips.OfType<SoundSetClip>())
{
soundSet.CreateTemplateFile();
}
}
/// <summary>
/// Assigns the bounds corresponding to current cursor location.
/// </summary>
private void AssignClipBounds()
{
SetDraggingEdges(Edge.Left | Edge.Right);
var point = _clipView.PointToClient(Cursor.Position);
TapeTrack track = DropTarget;
int startFrame = (int)_clipView.FrameFromPoint(point.X);
var clipsToConvert = _draggingClips.Where(x => x.TrackId != DropTarget.Id || x.Tape == null).ToList();
foreach (var clip in clipsToConvert)
{
var newClip = clip.ConvertToFit(DropTarget);
if (_clipView.Tape.Clips.Contains(clip))
{
_clipView.Tape.Clips.Remove(clip);
}
_draggingClips.Remove(clip);
_clipView.Tape.Clips.Add(newClip);
_draggingClips.Add(newClip);
}
foreach (var newClip in _draggingClips)
{
newClip.StartFrame = startFrame;
newClip.Track = track;
//The default behavior is to cascade every clips on different tracks
//we don't want that for lyrics
if (newClip != _draggingClips.Last() && DragDropType != DragDropType.Lyrics)
{
track = _clipView.Tape.GetOrCreateNextTrack(track);
}
}
if (DragDropType == DragDropType.File)
{
int duration = ConvertPixelDurationToFrame(ClipDurationInPixel);
foreach (var newClip in _draggingClips)
{
newClip.Duration = duration;
}
}
if (DragDropType == DragDropType.Lyrics)
{
//When we drag&drop lyrics from lyrics pad, we want to stretch all lyrics lines equally
//inside music structure section, at the time under the mouse cursor
var sectionStart = _clipView.Timeline.Wave().Annotations.SongSections.Reverse().FirstOrDefault(x => x.Time.Value < _draggingClips.First().StartBeat);
var sectionEnd = _clipView.Timeline.Wave().Annotations.SongSections.FirstOrDefault(x => x.Time.Value > _draggingClips.First().StartBeat);
if (sectionStart != null && sectionEnd != null)
{
var clipDuration = (sectionEnd.Time.Value - sectionStart.Time.Value) / _draggingClips.Count;
for (int i = 0 ; i < _draggingClips.Count ; i++)
{
var clip = _draggingClips[i];
clip.StartBeat = i > 0 ? _draggingClips[i - 1].StopBeat : sectionStart.Time.Value;
clip.StopBeat = i < _draggingClips.Count ? clip.StartBeat + clipDuration : sectionEnd.Time.Value;
}
}
}
}
private int ConvertPixelDurationToFrame(int frameLength)
{
return (int)_clipView.Timeline.FrameFromPixelDuration(frameLength);
}
#endregion
}
}