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 { /// /// The ClipViewControl is based on the WindowLess framework. It allows manage clips (add clip, move, delete, ...) /// 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(); 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 Selection => TrackView.Selection.OfType(); public void AddToSelection(Clip model) { var toSelect = TrackView.ControlsRecursive().OfType().FirstOrDefault(view => view.Model == model); if (toSelect != null) { TrackView.Selection.Add(toSelect); } } public TapeTrackWidget this[TapeTrack _track] => _tapeViewReplicator[_track]; public IEnumerable ClipWidgets => TrackView.ControlsRecursive().OfType(); public void ClearSelection() { TrackView.Selection.Clear(); } #endregion #region Wave Control Specialization /// /// The range with a little buffer (waiting for the start and end beats). /// protected override Range ComputeRange() { return new Range(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); } /// /// Resize tracks on shift wheel. /// 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() 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.Empty; if (editionContext != null) { var start = editionContext.StartBeat; activeArea = new Range(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(); } /// /// Selects next clip, by cycling on clips sorted by start time and then track index /// 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; } } } /// /// Selects previous clip, by cycling on clips sorted by start time and then track index /// 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(); 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(); 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(); 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); } /// /// Finds on which track to paste the current clips. /// private int? ComputeTargetTrackToPaste() { int? targetIndex = null; var track = TrackView.FindControl(PointToClient(Cursor.Position)); if (track != null) { targetIndex = track.Track.Index; } else if (TrackView.ActiveTrack is TapeTrackWidget) { targetIndex = ((TapeTrackWidget)TrackView.ActiveTrack).Track.Index; } return targetIndex; } /// /// After a paste operation moves the clip so the first clip matches the current time. /// 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 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 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(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 clips) { if (clips.Any()) { var tracksOfClips = clips.Select(clip => clip.Track.Index).ToArray(); if (tracksOfClips.Any()) { TrackView.ScrollIntoView(new Range(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(start, visibleRange.Max); } else if (visibleRange.Max < end) { Timeline.VisibleTimeRange = new Range(visibleRange.Min, end); } } } public void CenterView(Clip clip) { CenterView(new[] { clip }); } public void CenterView(IEnumerable clips) { if (clips.Any()) { var tracksOfClips = clips.Select(clip => clip.Track.Index).ToArray(); if (tracksOfClips.Any()) { TrackView.ScrollIntoView(new Range(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 /// /// Listens to the Actor collection and invalidates clips when their actor change. /// private class ActorInvalidator : IDisposable { private readonly TrackView _trackView; private Set _clipsWithUnresolvedActors = new Set(); 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 ComputeUnresolvedClips() { return _trackView.FindControls(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(); } } /// /// Disable the live preview in the engine when dragging clips ... engine responds slowly and it doesnt really help anyway to preview those values. /// 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; } } }