JD2022-TU1/main/tools/framework/JD.Timeline/View/ClipView/ClipViewControl.cs

693 lines
24 KiB
C#

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using JD.Application.Utils;
using JD.Application.View;
using JD.Application.View.Misc;
using JD.Application.View.Timeline;
using JD.Application.View.WindowLess;
using JD.Audio.Annotations;
using JD.Collections;
using JD.Collections.Extensions;
using JD.Collections.Interfaces;
using JD.Kernel.Classes;
using JD.Kernel.Structs;
using JD.Maths.Utils;
using JD.Sequencers.Times;
using JD.Timeline.Document;
using JD.Timeline.Karaoke;
using JD.Timeline.Model;
using JD.Timeline.Model.Clips;
using JD.Timeline.Plugin;
using JD.Timeline.View.Actions;
using JD.Timeline.View.Actions.Refactor;
using JD.Timeline.View.ClipView.Widgets;
using JD.Timeline.View.DataBinding;
using TeaBox;
using JD.Serialization;
namespace JD.Timeline.View.ClipView
{
/// <summary>
/// The ClipViewControl is based on the WindowLess framework. It allows manage clips (add clip, move, delete, ...)
/// </summary>
public class ClipViewControl : WaveControl
{
#region Variables
private readonly Tape _tape;
private readonly TapeViewReplicator _tapeViewReplicator;
private readonly ClipViewDragDropHandler _clipViewDragDropHandler;
private readonly ActorInvalidator _actorInvalidator;
private readonly LyricsSplittingTool _lyricsSplittingTool;
private readonly ClipCollisionComputer _clipCollisionComputer;
#endregion
#region Constructor
public ClipViewControl(Tape tape)
{
_tape = tape;
_tape.PropertyChanged += Tape_PropertyChanged;
_tape.EditionContextChanged += tape_EditionContextChanged;
SongBackground.Paint += SongBackground_Paint;
tape_EditionContextChanged(null, null);
Tape_PropertyChanged(null, null);
Timeline.ShowInBeats = true;
_tapeViewReplicator = new TapeViewReplicator(_tape, TrackView);
_clipViewDragDropHandler = new ClipViewDragDropHandler(this);
var draggingTool = TrackView.ToolManager.MouseMoveTools.FirstOf<SequencerDraggingTool>();
new DraggingToolExtension(draggingTool);
new DraggingModerator(draggingTool);
TooltipOnBottom = true;
TooltipTextFormat = new StringFormat { LineAlignment = StringAlignment.Center, Alignment = StringAlignment.Near };
_actorInvalidator = new ActorInvalidator(TrackView);
_lyricsSplittingTool = new LyricsSplittingTool(TrackView);
TrackView.ToolManager.MouseDownTools.Insert(0, _lyricsSplittingTool);
TrackView.ToolManager.MouseUpTools.Insert(0, _lyricsSplittingTool);
TrackView.ToolManager.MouseMoveTools.Insert(0, _lyricsSplittingTool);
_clipCollisionComputer = new ClipCollisionComputer(tape);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_tape.PropertyChanged -= Tape_PropertyChanged;
_tape.EditionContextChanged -= tape_EditionContextChanged;
Timeline.Dispose();
_tapeViewReplicator.Dispose();
_actorInvalidator.Dispose();
}
base.Dispose(disposing);
}
#endregion
#region Properties
public Tape Tape => _tape;
public new IEnumerable<ClipWidget> Selection => TrackView.Selection.OfType<ClipWidget>();
public void AddToSelection(Clip model)
{
var toSelect = TrackView.ControlsRecursive().OfType<ClipWidget>().FirstOrDefault(view => view.Model == model);
if (toSelect != null)
{
TrackView.Selection.Add(toSelect);
}
}
public TapeTrackWidget this[TapeTrack _track] => _tapeViewReplicator[_track];
public IEnumerable<ClipWidget> ClipWidgets => TrackView.ControlsRecursive().OfType<ClipWidget>();
public void ClearSelection()
{
TrackView.Selection.Clear();
}
#endregion
#region Wave Control Specialization
/// <summary>
/// The range with a little buffer (waiting for the start and end beats).
/// </summary>
protected override Range<BeatUnit> ComputeRange()
{
return new Range<BeatUnit>(Wave.Annotations.StartBeat - new BeatUnit(10), ComputeEndBeat());
}
private BeatUnit ComputeEndBeat()
{
return _tape.Clips.Select(clip => new BeatUnit(clip.StartBeat + clip.BeatDuration)).Concat(Wave.Annotations.EndBeat).Max();
}
protected override void InitializeContextMenu()
{
TeaBox.ObjectModel.Application.Main.Actions.CreateContextMenu("Application/ClipView/ContextMenu", this).AssignOnControl(this);
}
/// <summary>
/// Resize tracks on shift wheel.
/// </summary>
protected override void HandleZoomOnMouseWheel(MouseEventArgs e)
{
if (ModifierKeys == Keys.Shift)
{
int increment = e.Delta < 0 ? -2 : 2;
foreach (var track in TrackView.GetTracks())
{
track.Height = (track.Height + increment).Clamp(10, 300);
}
}
else
{
base.HandleZoomOnMouseWheel(e);
}
}
void SongBackground_Paint(object sender, PaintEventArgs e)
{
if (_tape.EditionContext != null)
{
foreach (var section in _tape.EditionContext.EnumerateLoopSections())
{
int x = Timeline.PixelFromFrame(section.End);
e.Graphics.DrawLine(Pens.Gray, x, Top, x, Bottom);
}
}
}
#endregion
#region Event Creation Helper
public TapeTrack TrackFromPoint(int y)
{
return (from track in TrackView.GetTracks().OfType<TapeTrackWidget>()
where track.IsVisible && y.IsBetween(track.Bounds.Top, track.Bounds.Bottom)
select track.Track).FirstOrDefault();
}
public float FrameFromPoint(int x)
{
BeatUnit beat = Timeline.BeatFromTimeUnit(Timeline.PixelToTime(x));
var snapped = Timeline.Wave.UnitConverter.Snap(beat, EditionOptions.CurrentSnappingUnit);
return Timeline.FrameFromTime(Timeline.TimeUnitFromBeat(snapped));
}
#endregion
#region Handlers
private void Tape_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (Wave != _tape.TryGetWave())
{
Wave = _tape.TryGetWave();
Invalidate();
}
}
private void tape_EditionContextChanged(object sender, EventArgs e)
{
var editionContext = _tape.EditionContext;
var activeArea = Range<BeatUnit>.Empty;
if (editionContext != null)
{
var start = editionContext.StartBeat;
activeArea = new Range<BeatUnit>(new BeatUnit(start), new BeatUnit(start + editionContext.BeatDuration));
}
else if (Wave != null)
{
activeArea = Wave.UnitConverter.TimeRange;
}
SongBackground.ActiveArea = activeArea;
}
protected override void OnDoubleClick(LocalizedTrackItem trackItem)
{
// Do not move time cursor when double clicking on a tape reference.
var tapeReference = trackItem as TapeReferenceClipWidget;
if (tapeReference != null)
{
tapeReference.Model.OpenDocument();
return;
}
// Do not move time cursor when double clicking on a clip with actor
var clipWithActor = trackItem as ClipWithActorWidget;
if (clipWithActor != null)
{
TeaBox.ObjectModel.Application.Main.Actions.Execute("TimelineActions.BrowseForActor");
return;
}
base.OnDoubleClick(trackItem);
}
#endregion
#region Action Handlers
[ActionHandler("EditActions.MoveUp")]
protected override void HandleMoveUp()
{
Selection.Where(widget => widget.IsMoveAllowed).Select(clip => clip.Model).MoveUp(this.TrackView);
}
[ActionHandler("EditActions.MoveDown")]
protected override void HandleMoveDown()
{
Selection.Where(widget => widget.IsMoveAllowed).Select(clip => clip.Model).MoveDown(this.TrackView);
}
protected override void UpdateMoveLeft(ActionInfo info)
{
info.Enabled = Selection.Any(widget => widget.IsMoveAllowed);
}
protected override void UpdateMoveRight(ActionInfo info)
{
info.Enabled = Selection.Any(widget => widget.IsMoveAllowed);
}
[ActionHandler("EditActions.MoveLeft")]
protected override void HandleMoveLeft()
{
var pivot = Selection.Where(clip => clip.IsCurrentControl).Select(clip => clip.Model).FirstOrDefault();
Selection.Where(widget => widget.IsMoveAllowed).Select(clip => clip.Model).MoveHorizontal(Wave.UnitConverter, left: true, pivot: pivot, snap: EditionOptions.CurrentSnappingUnit);
}
[ActionHandler("EditActions.MoveRight")]
protected override void HandleMoveRight()
{
var pivot = Selection.Where(clip => clip.IsCurrentControl).Select(clip => clip.Model).FirstOrDefault();
Selection.Where(widget => widget.IsMoveAllowed).Select(clip => clip.Model).MoveHorizontal(Wave.UnitConverter, left: false, pivot: pivot, snap: EditionOptions.CurrentSnappingUnit);
}
[ActionHandler("EditActions.SelectNext")]
protected override void HandleSelectNext()
{
SelectNextClip();
}
[ActionHandler("EditActions.SelectPrevious")]
protected override void HandleSelectPrevious()
{
SelectPreviousClip();
}
/// <summary>
/// Selects next clip, by cycling on clips sorted by start time and then track index
/// </summary>
public void SelectNextClip()
{
if (!Selection.Any())
return;
var lastClipInSelection = Selection.Select(clip => clip.Model).MaxElement(x => x.StopBeat);
var clipsSorted = (from clip in Tape.Clips
orderby clip.StartBeat, clip.Track.Index
select clip).ToList();
int index = clipsSorted.IndexOf(lastClipInSelection);
var nextClip = clipsSorted[(index + 1) % clipsSorted.Count];
if (nextClip != null)
{
TrackView.Selection.Clear();
AddToSelection(nextClip);
if (!Timeline.VisibleTimeRange.Contains(nextClip.StopBeat))
{
Timeline.TimeOffset += nextClip.StopBeat - Timeline.VisibleTimeRange.Max;
}
}
}
/// <summary>
/// Selects previous clip, by cycling on clips sorted by start time and then track index
/// </summary>
public void SelectPreviousClip()
{
if (!Selection.Any())
return;
var firstClipInSelection = Selection.Select(clip => clip.Model).MinElement(x => x.StartBeat);
var clipsSorted = (from clip in Tape.Clips
orderby clip.StartBeat, clip.Track.Index
select clip).ToList();
int index = clipsSorted.IndexOf(firstClipInSelection);
var previousClip = clipsSorted[(index - 1) % clipsSorted.Count];
if (previousClip != null)
{
TrackView.Selection.Clear();
AddToSelection(previousClip);
if (!Timeline.VisibleTimeRange.Contains(previousClip.StartBeat))
{
Timeline.TimeOffset -= Timeline.VisibleTimeRange.Min - previousClip.StartBeat;
}
}
}
[ActionUpdateHandler(EditActions.Delete)]
private void UpdateDelete(ActionInfo info)
{
info.Enabled = Selection.Any();
}
[ActionHandler(EditActions.Delete)]
private void HandleDelete()
{
using (TrackView.Selection.BeginScopedUpdate())
{
foreach (var clip in Selection.Where(widget => widget.IsMoveAllowed).Select(@event => @event.Model).ToArray())
{
_tape.Clips.Remove(clip);
}
}
}
[ActionUpdateHandler(EditActions.Copy)]
private void UpdateCopy(ActionInfo actionInfo)
{
actionInfo.Enabled = Selection.Any();
}
[ActionHandler(EditActions.Copy)]
private void HandleCopy()
{
ToClipboard();
}
private class ClipboardClipData
{
[UAFSerializable(4294967295)]
public Clip Clip { get; set; }
[UAFSerializable(4294967295)]
public int TrackIndex { get; set; }
}
private void ToClipboard()
{
var clipsToCopy = new UAFList<ClipboardClipData>();
clipsToCopy.AddRange(Selection.Select(clip => new ClipboardClipData() { Clip = clip.Model, TrackIndex = clip.Model.Track.Index }));
Clipboard.SetText(new LuaSerializer().Serialize(clipsToCopy), TextDataFormat.UnicodeText);
}
[ActionUpdateHandler(EditActions.Cut)]
private void UpdateCut(ActionInfo actionInfo)
{
actionInfo.Enabled = Selection.Any();
}
[ActionHandler(EditActions.Cut)]
private void HandleCut()
{
ToClipboard();
HandleDelete();
}
[ActionHandler(EditActions.Paste)]
private void HandlePaste()
{
Paste(inPlace: false);
}
[ActionHandler("TimelineActions.PasteInPlace")]
private void HandlePasteInPlace()
{
Paste(inPlace: true);
}
private void Paste(bool inPlace)
{
var clipboardClipDataList = new UAFList<ClipboardClipData>();
try
{
var pastedData = Clipboard.GetText();
new LuaSerializer().Deserialize(pastedData, clipboardClipDataList);
foreach (var clipboardClipData in clipboardClipDataList)
{
clipboardClipData.Clip.RegenerateId();
}
}
catch (Exception)
{
MessageService.MessageService.ShowBalloon(TimeSpan.FromSeconds(10), "Could not paste the data", "Data was not in correct format", ToolTipIcon.Warning);
return;
}
var clipsConverted = new UAFList<Clip>();
int? mouseTrackId = ComputeTargetTrackToPaste();
int minTrackIndexInClips = (clipboardClipDataList.Count > 0) ? clipboardClipDataList.Select(x => x.TrackIndex).Min() : 0;
int destTrackId = mouseTrackId != null ? (int)mouseTrackId : minTrackIndexInClips;
foreach (var clipboardClipData in clipboardClipDataList)
{
//We get or create the track where we want to paste the clip
int distFromMin = clipboardClipData.TrackIndex - minTrackIndexInClips;
var trackToPasteInto = _tape.GetOrCreateTrackAt(destTrackId + distFromMin);
clipboardClipData.Clip.Tape = Tape;
//We convert the clip to fit the destination track
var clipConverted = clipboardClipData.Clip.ConvertToFit(trackToPasteInto);
clipConverted.Track = trackToPasteInto;
clipsConverted.Add(clipConverted);
}
clipsConverted.MakeUnique();
_tape.Clips.AddRange(clipsConverted);
if (!inPlace)
{
MoveClipsToCursor(clipsConverted.ToArray());
}
TrackView.Selection.Clear();
TrackView.Selection.AddRange(clipsConverted.Select(clip => _tapeViewReplicator[clip]).NotNulls());
ScrollIntoView(clipsConverted);
}
/// <summary>
/// Finds on which track to paste the current clips.
/// </summary>
private int? ComputeTargetTrackToPaste()
{
int? targetIndex = null;
var track = TrackView.FindControl<TapeTrackWidget>(PointToClient(Cursor.Position));
if (track != null)
{
targetIndex = track.Track.Index;
}
else if (TrackView.ActiveTrack is TapeTrackWidget)
{
targetIndex = ((TapeTrackWidget)TrackView.ActiveTrack).Track.Index;
}
return targetIndex;
}
/// <summary>
/// After a paste operation moves the clip so the first clip matches the current time.
/// </summary>
private void MoveClipsToCursor(Clip[] clips)
{
if (clips.Length > 0)
{
Point cursorLocation = PointToClient(Cursor.Position);
BeatUnit beat = Timeline.BeatFromTimeUnit(Timeline.PixelToTime(cursorLocation.X));
var snapped = Timeline.Wave.UnitConverter.Snap(beat, EditionOptions.CurrentSnappingUnit, 0.2f, 0.8f);
int currentFrame = (int)Timeline.FrameFromTime(Timeline.TimeUnitFromBeat(snapped));
float currentTime = Timeline.PixelToTime(cursorLocation.X);
if (Timeline.TimeCursor.IsDragging || !Timeline.VisibleTimeRange.Contains(currentTime))
{
currentFrame = (int)Math.Round(Timeline.FrameFromTime(Timeline.TimeCursor.Time));
}
int leftMost = clips.Min(clip => clip.StartFrame);
int offset = leftMost - currentFrame;
foreach (var clip in clips)
{
clip.StartFrame -= offset;
}
}
}
[ActionHandler(ZoomExtentsActions.ZoomExtents)]
public void ZoomExtents()
{
IEnumerable<Clip> toZoom = Selection.Select(view => view.Model).ToArray();
if (toZoom.IsEmpty())
{
// If no selection has been detected, we only retrieved the visible tracks.
int lastVisibleTrackIndex = TrackView.LastVisibleTrackIndex();
toZoom = _tape.Clips.Where(clip => clip.Track.Index >= TrackView.ScrollValue && clip.Track.Index <= lastVisibleTrackIndex).ToList();
}
Range<float> clipRange;
if (toZoom.IsEmpty())
{
clipRange = Timeline.TimeRange;
}
else
{
var min = toZoom.Min(clip => clip.StartBeat);
var max = toZoom.Max(clip => clip.StartBeat + clip.BeatDuration);
clipRange = new Range<float>(min, max);
clipRange = clipRange.Inflate(clipRange.Width() * .1f);
clipRange = Timeline.TimeRange.ClampRange(clipRange);
}
Timeline.ZoomExtents(clipRange);
ScrollIntoView(toZoom);
}
public void ScrollIntoView(Clip clip)
{
ScrollIntoView(new[] { clip });
}
public void ScrollIntoView(IEnumerable<Clip> clips)
{
if (clips.Any())
{
var tracksOfClips = clips.Select(clip => clip.Track.Index).ToArray();
if (tracksOfClips.Any())
{
TrackView.ScrollIntoView(new Range<int>(tracksOfClips.Min(), tracksOfClips.Max()));
}
var start = Timeline.TimeFromFrame(clips.Min(clip => clip.StartFrame));
var end = Timeline.TimeFromFrame(clips.Max(clip => clip.StopFrame));
var visibleRange = Timeline.VisibleTimeRange;
if (visibleRange.Min > start)
{
Timeline.VisibleTimeRange = new Range<float>(start, visibleRange.Max);
}
else if (visibleRange.Max < end)
{
Timeline.VisibleTimeRange = new Range<float>(visibleRange.Min, end);
}
}
}
public void CenterView(Clip clip)
{
CenterView(new[] { clip });
}
public void CenterView(IEnumerable<Clip> clips)
{
if (clips.Any())
{
var tracksOfClips = clips.Select(clip => clip.Track.Index).ToArray();
if (tracksOfClips.Any())
{
TrackView.ScrollIntoView(new Range<int>(tracksOfClips.Min(), tracksOfClips.Max()));
}
var start = Timeline.TimeFromFrame(clips.Min(clip => clip.StartFrame));
var end = Timeline.TimeFromFrame(clips.Max(clip => clip.StopFrame));
var center = start + (end - start)/2;
var visibleRange = Timeline.VisibleTimeRange;
Timeline.TimeOffset = center - Timeline.VisibleTimeRange.Width()/2;
}
}
#endregion
#region Nested
/// <summary>
/// Listens to the Actor collection and invalidates clips when their actor change.
/// </summary>
private class ActorInvalidator : IDisposable
{
private readonly TrackView _trackView;
private Set<ClipWithActorWidget> _clipsWithUnresolvedActors = new Set<ClipWithActorWidget>();
public ActorInvalidator(TrackView trackView)
{
_trackView = trackView;
if (TimelinePlugin.Instance != null)
{
TimelinePlugin.Instance.World.Actors.Refreshing += Actors_Refreshing;
TimelinePlugin.Instance.World.Actors.Refreshed += Actors_Refreshed;
}
}
public void Dispose()
{
if (TimelinePlugin.Instance != null)
{
TimelinePlugin.Instance.World.Actors.Refreshing -= Actors_Refreshing;
TimelinePlugin.Instance.World.Actors.Refreshed -= Actors_Refreshed;
}
}
private IEnumerable<ClipWithActorWidget> ComputeUnresolvedClips()
{
return _trackView.FindControls<ClipWithActorWidget>(clip => clip.Model.IsUnresolved());
}
void Actors_Refreshing(object sender, EventArgs e)
{
_clipsWithUnresolvedActors = ComputeUnresolvedClips().ToSet();
}
void Actors_Refreshed(object sender, EventArgs e)
{
ComputeUnresolvedClips().FindDifferences().Synchronize(_clipsWithUnresolvedActors, InvalidateClip, InvalidateClip);
}
private void InvalidateClip(ClipWithActorWidget widget)
{
widget.Invalidate();
}
}
/// <summary>
/// Disable the live preview in the engine when dragging clips ... engine responds slowly and it doesnt really help anyway to preview those values.
/// </summary>
private class DraggingModerator
{
private IDisposable _suspendEventHandle;
public DraggingModerator(SequencerDraggingTool tool)
{
tool.Started += tool_Started;
tool.Ended += tool_Ended;
}
void tool_Ended(object sender, EventArgs e)
{
Dispose();
}
void tool_Started(object sender, EventArgs e)
{
Dispose();
if (TimelinePlugin.Instance != null)
{
_suspendEventHandle = TimelinePlugin.Instance.BatchOperation.Begin();
}
}
private void Dispose()
{
DisposableHelper.SafeDispose(ref _suspendEventHandle);
}
}
#endregion
}
[Serializable]
public class ClipClipboardData
{
public byte[] Content { get; set; }
}
}