diff --git a/TuneLab/Views/Editor.cs b/TuneLab/Views/Editor.cs index 02a3a10..fa55717 100644 --- a/TuneLab/Views/Editor.cs +++ b/TuneLab/Views/Editor.cs @@ -540,7 +540,7 @@ public void ImportAudio() if (Project == null) return; - TrackWindow.TrackGrid.ImportAudioAt(0, Project.Tracks.Count); + TrackWindow.TrackScrollView.ImportAudioAt(0, Project.Tracks.Count); } public void ChangePlayState() diff --git a/TuneLab/Views/TrackGrid.cs b/TuneLab/Views/TrackGrid.cs deleted file mode 100644 index 6af2c37..0000000 --- a/TuneLab/Views/TrackGrid.cs +++ /dev/null @@ -1,566 +0,0 @@ -using Avalonia; -using Avalonia.Media; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using TuneLab.Base.Event; -using TuneLab.GUI.Components; -using TuneLab.Data; -using TuneLab.GUI; -using TuneLab.Base.Data; -using TuneLab.Extensions.Formats.DataInfo; -using TuneLab.Audio; -using Avalonia.Input; -using Avalonia.Controls; -using Avalonia.Platform.Storage; -using System.IO; -using TuneLab.Utils; -using TuneLab.Base.Science; -using TuneLab.Base.Utils; - -namespace TuneLab.Views; - -internal partial class TrackGrid : View -{ - public interface IDependency - { - TickAxis TickAxis { get; } - TrackVerticalAxis TrackVerticalAxis { get; } - IQuantization Quantization { get; } - IProvider ProjectProvider { get; } - IProvider EditingPart { get; } - void SwitchEditingPart(IPart part); - void EnterInputPartName(IPart part, int trackIndex); - } - - public State OperationState => mState; - - public TrackGrid(IDependency dependency) - { - mDependency = dependency; - - mMiddleDragOperation = new(this); - mSelectOperation = new(this); - mPartMoveOperation = new(this); - mPartEndResizeOperation = new(this); - mDragFileOperation = new(this); - - mDependency.ProjectProvider.ObjectChanged.Subscribe(Update, s); - mDependency.ProjectProvider.When(project => project.Modified).Subscribe(Update, s); - mDependency.EditingPart.ObjectChanged.Subscribe(InvalidateVisual, s); - mDependency.ProjectProvider.When(project => project.Tracks.Any(track => track.Parts.Any(part => part.SelectionChanged))).Subscribe(InvalidateVisual, s); - mDependency.ProjectProvider.When(project => project.Tracks.Any(track => track.Parts.Any(part => part is AudioPart audioPart ? audioPart.AudioChanged : new ActionEvent()))).Subscribe(InvalidateVisual, s); // TODO: 支持一下可空类型的event - Quantization.QuantizationChanged += InvalidateVisual; - TickAxis.AxisChanged += Update; - TrackVerticalAxis.AxisChanged += Update; - - AddHandler(DragDrop.DragEnterEvent, OnDragEnter); - AddHandler(DragDrop.DragOverEvent, OnDragOver); - AddHandler(DragDrop.DragLeaveEvent, OnDragLeave); - AddHandler(DragDrop.DropEvent, OnDrop); - } - - ~TrackGrid() - { - s.DisposeAll(); - Quantization.QuantizationChanged -= InvalidateVisual; - TickAxis.AxisChanged -= Update; - TrackVerticalAxis.AxisChanged -= Update; - } - - protected override void OnRender(DrawingContext context) - { - context.FillRectangle(Brushes.Transparent, this.Rect()); - - if (Project == null) - return; - - var timeSignatureManager = Project.TimeSignatureManager; - - double startPos = TickAxis.MinVisibleTick; - double endPos = TickAxis.MaxVisibleTick; - - var startMeter = timeSignatureManager.GetMeterStatus(startPos); - var endMeter = timeSignatureManager.GetMeterStatus(endPos); - - int startIndex = startMeter.TimeSignatureIndex; - int endIndex = endMeter.TimeSignatureIndex; - - var timeSignatures = timeSignatureManager.TimeSignatures; - IBrush barLineBrush = BarLineColor.ToBrush(); - for (int timeSignatureIndex = startIndex; timeSignatureIndex <= endIndex; timeSignatureIndex++) - { - // draw bar - int nextTimeSignatureBarIndex = timeSignatureIndex + 1 == timeSignatures.Count ? (int)Math.Ceiling(endMeter.BarIndex) : timeSignatures[timeSignatureIndex + 1].BarIndex; - int thisTimeSignatureBarIndex = Math.Max(timeSignatures[timeSignatureIndex].BarIndex, (int)Math.Floor(startMeter.BarIndex)); - for (int barIndex = thisTimeSignatureBarIndex; barIndex < nextTimeSignatureBarIndex; barIndex++) - { - double xBarIndex = TickAxis.Tick2X(timeSignatures[timeSignatureIndex].GetTickByBarIndex(barIndex)); - context.FillRectangle(barLineBrush, new Rect(xBarIndex, 0, 1, Bounds.Height)); - } - - // draw beat - double pixelsPerBeat = timeSignatures[timeSignatureIndex].TicksPerBeat() * TickAxis.PixelsPerTick; - double beatOpacity = MathUtility.LineValue(MIN_GRID_GAP, 0, MIN_REALITY_GRID_GAP, 1, pixelsPerBeat).Limit(0, 1); - if (beatOpacity == 0) - continue; - - IPen beatLinePen = new Pen(BeatLineColor.Opacity(beatOpacity).ToUInt32(), LineWidth); - for (int barIndex = thisTimeSignatureBarIndex; barIndex < nextTimeSignatureBarIndex; barIndex++) - { - for (int beatIndex = 1; beatIndex < timeSignatures[timeSignatureIndex].Numerator; beatIndex++) - { - double xBeatIndex = TickAxis.Tick2X(timeSignatures[timeSignatureIndex].GetTickByBarAndBeat(barIndex, beatIndex)); - double x = xBeatIndex + LineWidth / 2; - context.DrawLine(beatLinePen, new Point(x, 0), new Point(x, Bounds.Height)); - } - } - - // draw quantization - int quantizationBase = (int)Quantization.Base; - int ticksPerBase = timeSignatures[timeSignatureIndex].TicksPerBeat() / quantizationBase; - double pixelsPerBase = ticksPerBase * TickAxis.PixelsPerTick; - double baseOpacity = MathUtility.LineValue(MIN_GRID_GAP, 0, MIN_REALITY_GRID_GAP, 1, pixelsPerBase).Limit(0, 1); - if (baseOpacity == 0) - continue; - - IPen baseLinePen = new Pen(CellLineColor.Opacity(baseOpacity).ToUInt32(), LineWidth); - for (int barIndex = thisTimeSignatureBarIndex; barIndex < nextTimeSignatureBarIndex; barIndex++) - { - for (int beatIndex = 0; beatIndex < timeSignatures[timeSignatureIndex].Numerator; beatIndex++) - { - double beatPos = timeSignatures[timeSignatureIndex].GetTickByBarAndBeat(barIndex, beatIndex); - for (int baseIndex = 1; baseIndex < quantizationBase; baseIndex++) - { - double xBase = TickAxis.Tick2X(beatPos + baseIndex * ticksPerBase); - double x = xBase + LineWidth / 2; - context.DrawLine(baseLinePen, new Point(x, 0), new Point(x, Bounds.Height)); - } - } - } - - int quantizationDivision = (int)Quantization.Division; - int noteDivision = Math.Max(quantizationDivision * 4, timeSignatures[timeSignatureIndex].Denominator); - int beatDivision = noteDivision / timeSignatures[timeSignatureIndex].Denominator; - double thisTimeSignaturePos = timeSignatures[timeSignatureIndex].GetTickByBarIndex(thisTimeSignatureBarIndex); - for (int cellsPerBase = 2; cellsPerBase <= beatDivision; cellsPerBase *= 2) - { - int ticksPerCell = ticksPerBase / cellsPerBase; - double pixelsPerCell = ticksPerCell * TickAxis.PixelsPerTick; - double cellOpacity = MathUtility.LineValue(MIN_GRID_GAP, 0, MIN_REALITY_GRID_GAP, 1, pixelsPerCell).Limit(0, 1); - if (cellOpacity == 0) - break; - - IPen cellLinePen = new Pen(CellLineColor.Opacity(cellOpacity).ToUInt32(), LineWidth); - int cellCount = (nextTimeSignatureBarIndex - thisTimeSignatureBarIndex) * timeSignatures[timeSignatureIndex].Numerator * quantizationBase * cellsPerBase / 2; - for (int cellIndex = 0; cellIndex < cellCount; cellIndex++) - { - double cellPos = thisTimeSignaturePos + (cellIndex * 2 + 1) * ticksPerCell; - double xCell = TickAxis.Tick2X(cellPos); - double x = xCell + LineWidth / 2; - context.DrawLine(cellLinePen, new Point(x, 0), new Point(x, Bounds.Height)); - } - } - } - - var tempoManager = Project.TempoManager; - - // draw parts - IBrush lineBrush = Style.DARK.ToBrush(); - IBrush partBrush = Style.ITEM.Opacity(0.25).ToBrush(); - IBrush selectedPartBrush = Style.HIGH_LIGHT.Opacity(0.25).ToBrush(); - double partLineWidth = 1; - IPen editPartPen = new Pen(Style.LIGHT_WHITE.ToBrush(), partLineWidth); - IPen partSelectPen = new Pen(Style.HIGH_LIGHT.ToBrush(), partLineWidth); - IPen partPen = new Pen(Style.HIGH_LIGHT.Opacity(0.5).ToBrush(), partLineWidth); - IBrush titleBrush = Colors.White.Opacity(0.7).ToBrush(); - IBrush noteBrush = Style.ITEM.ToBrush(); - IBrush noteSelectBrush = Style.HIGH_LIGHT.ToBrush(); - for (int trackIndex = 0; trackIndex < Project.Tracks.Count; trackIndex++) - { - double lineBottom = TrackVerticalAxis.GetTop(trackIndex + 1); - if (lineBottom <= 0) - continue; - - double top = TrackVerticalAxis.GetTop(trackIndex); - if (top >= Bounds.Height) - break; - - context.FillRectangle(lineBrush, new(0, lineBottom - 1, Bounds.Width, 1)); - - double bottom = TrackVerticalAxis.GetBottom(trackIndex); - if (bottom <= 0) - continue; - - var track = Project.Tracks[trackIndex]; - foreach (var part in track.Parts) - { - if (part.EndPos() <= startPos) - continue; - - if (part.StartPos() >= endPos) - break; - - double left = Math.Max(TickAxis.Tick2X(part.StartPos()), -8); - double right = Math.Min(TickAxis.Tick2X(part.EndPos()), Bounds.Width + 8); - - var partRect = new Rect(left, top, right - left, bottom - top); - context.DrawRectangle(part.IsSelected ? selectedPartBrush : partBrush, part == mDependency.EditingPart.Object ? editPartPen : part.IsSelected ? partSelectPen : partPen, partRect.Inflate(-partLineWidth / 2)); - var titleRect = partRect.WithHeight(16).Adjusted(Math.Max(0, -partRect.Left) + 8, 0, -8, 0); - var contentRect = partRect.Adjusted(0, 16, 0, 0); - if (part is MidiPart midiPart) - { - using (context.PushClip(titleRect)) - { - context.DrawString(string.Format("{0}[{1}]", midiPart.Name, midiPart.Voice.Name), titleRect, titleBrush, 12, Alignment.LeftCenter, Alignment.LeftCenter); - } - - if (midiPart.Notes.IsEmpty()) - continue; - - using (context.PushClip(contentRect)) - { - var (minPitch, maxPitch) = midiPart.PitchRange(); - double pitchGap = maxPitch - minPitch + 1; - double pitchHeight = Math.Min(contentRect.Height / pitchGap, 8); - double partStartPos = Math.Max(startPos, midiPart.StartPos) - midiPart.Pos; - double partEndPos = Math.Min(endPos, midiPart.EndPos) - midiPart.Pos; - IBrush brush = part.IsSelected ? noteSelectBrush : noteBrush; - foreach (var note in midiPart.Notes) - { - if (note.EndPos() <= partStartPos) - continue; - - if (note.StartPos() >= partEndPos) - break; - - double noteLeft = TickAxis.Tick2X(note.StartPos() + midiPart.Pos); - double noteRight = TickAxis.Tick2X(note.EndPos() + midiPart.Pos); - context.FillRectangle(brush, new(noteLeft, contentRect.Y + (maxPitch - note.Pitch.Value) * pitchHeight, noteRight - noteLeft, pitchHeight)); - } - } - } - else if (part is AudioPart audioPart) - { - using (context.PushClip(titleRect)) - { - context.DrawString(string.Format("{0}[{1}]", audioPart.Name, audioPart.Path), titleRect, titleBrush, 12, Alignment.LeftCenter, Alignment.LeftCenter); - } - - if (audioPart.ChannelCount > 0) - { - for (int channelIndex = 0; channelIndex < audioPart.ChannelCount; channelIndex++) - { - if (audioPart.EndPos < TickAxis.MinVisibleTick) - continue; - - if (audioPart.StartPos > TickAxis.MaxVisibleTick) - break; - - var waveform = audioPart.GetWaveform(channelIndex); - if (waveform == null) - continue; - - double minTick = Math.Max(TickAxis.MinVisibleTick, audioPart.StartPos); - double maxTick = Math.Min(TickAxis.MaxVisibleTick, audioPart.EndPos); - double minX = TickAxis.Tick2X(minTick); - double maxX = TickAxis.Tick2X(maxTick); - var xs = new List(); - var positions = new List(); - double gap = 1; - double xp = minX - gap; - double startTime = audioPart.TempoManager.GetTime(audioPart.StartPos); - do - { - xp += gap; - xs.Add(xp); - double time = tempoManager.GetTime(TickAxis.X2Tick(xp)); - positions.Add((time - startTime) * ((IAudioSource)audioPart).SamplingRate); - } - while (xp < maxX); - - if (positions.Count < 2) - continue; - - double channelHeight = contentRect.Height / audioPart.ChannelCount; - float channelTop = (float)(contentRect.Top + channelHeight * channelIndex); - float r = (float)channelHeight / 2; - float toY(float value) => channelTop + (1 - value) * r; - - var values = waveform.GetValues(positions); - var peaks = waveform.GetPeaks(positions, values); - for (int i = 0; i < xs.Count; i++) - { - values[i] = toY(values[i]); - } - for (int i = 0; i < peaks.Length; i++) - { - peaks[i].min = toY(peaks[i].min); - peaks[i].max = toY(peaks[i].max); - } - // 性能优先先采用画矩形的方案 - using (var mask = context.PushOpacity(0.5)) - { - for (int i = 0; i < peaks.Length; i++) - { - double x = xs[i]; - var peak = peaks[i]; - context.FillRectangle(Style.LIGHT_WHITE.ToBrush(), new(xs[i], peak.max, gap, peak.min - peak.max)); - } - } - - /* - for (int i = 0; i < peaks.Length; i++) - { - double x = xs[i]; - var peak = peaks[i]; - var path = new PathGeometry(); - using (var pathContext = path.Open()) - { - pathContext.BeginFigure(new Point(x, values[i]), true); - pathContext.LineTo(new Point(x + gap * peak.minRatio, peak.min)); - pathContext.LineTo(new Point(xs[i + 1], values[i + 1])); - pathContext.LineTo(new Point(x + gap * peak.maxRatio, peak.max)); - pathContext.EndFigure(true); - } - context.DrawGeometry(Style.LIGHT_WHITE.Opacity(0.5).ToBrush(), null, path); - } - */ - /* - for (int i = 0; i < peaks.Length; i++) - { - var points = new List(); - double x = xs[i]; - var peak = peaks[i]; - points.Add(new Point(x, values[i])); - points.Add(new Point(x + gap * peak.minRatio, peak.min)); - points.Add(new Point(xs[i + 1], values[i + 1])); - points.Add(new Point(x + gap * peak.maxRatio, peak.max)); - context.DrawCurve(points, Style.LIGHT_WHITE.Opacity(0.5), gap, true); - } - */ - /* - var points = new List(); - for (int i = 0; i < peaks.Length; i++) - { - double x = xs[i]; - var peak = peaks[i]; - points.Add(new Point(x, values[i])); - points.Add(new Point(x + gap * peak.minRatio, peak.min)); - } - for (int i = peaks.Length; i > 0; i--) - { - double x = xs[i]; - var peak = peaks[i - 1]; - points.Add(new Point(x, values[i])); - points.Add(new Point(x + gap * peak.maxRatio, peak.max)); - } - context.DrawCurve(points, Style.LIGHT_WHITE.Opacity(0.5), gap, true); - */ - } - } - } - } - } - - // draw import parts - if (mDragFileOperation.IsOperating) - { - double left = Math.Max(TickAxis.Tick2X(mDragFileOperation.Pos), -8); - for (int i = 0; i < mDragFileOperation.PreImportAudioInfos.Count; i++) - { - var info = mDragFileOperation.PreImportAudioInfos[i]; - double right = Math.Min(TickAxis.Tick2X(info.EndPos), Bounds.Width + 8); - double top = TrackVerticalAxis.GetTop(mDragFileOperation.TrackIndex + i); - double bottom = TrackVerticalAxis.GetBottom(mDragFileOperation.TrackIndex + i); - var partRect = new Rect(left, top, right - left, bottom - top); - context.FillRectangle(partBrush, partRect); - - var titleRect = partRect.WithHeight(16).Adjusted(Math.Max(0, -partRect.Left) + 8, 0, -8, 0); - var contentRect = partRect.Adjusted(0, 16 + 8, 0, -8); - using (context.PushClip(titleRect)) - { - context.DrawString(string.Format("{0}[{1}]", info.name, info.path), titleRect, titleBrush, 12, Alignment.LeftCenter, Alignment.LeftCenter); - } - } - } - - // draw selection - if (mSelectOperation.IsOperating) - { - var rect = mSelectOperation.SelectionRect(); - context.DrawRectangle(SelectionColor.Opacity(0.25).ToBrush(), new Pen(SelectionColor.ToUInt32()), rect); - } - } - - public double QuantizedCellTicks() - { - int quantizationBase = (int)Quantization.Base; - double division = (int)Math.Pow(2, Math.Log2(TickAxis.PixelsPerTick * MusicTheory.RESOLUTION / quantizationBase / MIN_GRID_GAP).Floor()).Limit(1, 32); - return MusicTheory.RESOLUTION / quantizationBase / division; - } - - public double GetQuantizedTick(double tick) - { - double cell = QuantizedCellTicks(); - return (tick / cell).Round() * cell; - } - - struct PartInfosWithTrackIndex(int trackIndex, IEnumerable parts) - { - public int trackIndex = trackIndex; - public IEnumerable parts = parts; - } - - struct PartClipboard() - { - public bool IsEmpty() => content.IsEmpty(); - - public double pos = double.MaxValue; - public List content = new(); - } - - PartClipboard mPartClipboard = new(); - public void Copy() - { - if (Project == null) - return; - - var clipboard = new PartClipboard(); - for (int trackIndex = 0; trackIndex < Project.Tracks.Count; trackIndex++) - { - var selectedParts = Project.Tracks[trackIndex].Parts.AllSelectedItems(); - if (selectedParts.IsEmpty()) - continue; - - clipboard.pos = Math.Min(clipboard.pos, selectedParts.First().Pos.Value); - clipboard.content.Add(new(trackIndex, selectedParts.Select(part => part.GetInfo()))); - } - - if (clipboard.IsEmpty()) - return; - - mPartClipboard = clipboard; - } - - public void PasteAt(double pos) - { - if (Project == null) - return; - - if (mPartClipboard.IsEmpty()) - return; - - double offset = pos - mPartClipboard.pos; - Project.Tracks.SelectMany(track => track.Parts).DeselectAllItems(); - foreach (var partInfosWithTrackIndex in mPartClipboard.content) - { - var track = Project.Tracks[partInfosWithTrackIndex.trackIndex]; - foreach (var partInfo in partInfosWithTrackIndex.parts) - { - var part = track.CreatePart(partInfo); - part.Select(); - part.Pos.Set(part.Pos.Value + offset); - track.InsertPart(part); - } - } - Project.Commit(); - } - - public bool CanPaste => !mPartClipboard.IsEmpty(); - - public void Cut() - { - Copy(); - DeleteAllSelectedParts(); - } - - public void DeleteAllSelectedParts() - { - if (Project == null) - return; - - foreach (var track in Project.Tracks) - { - var selectedParts = track.Parts.AllSelectedItems(); - foreach (var part in selectedParts) - { - track.RemovePart(part); - } - } - Project.Commit(); - } - - public void DeleteTrackAt(int trackIndex) - { - if (Project == null) - return; - - Project.RemoveTrackAt(trackIndex); - Project.Commit(); - } - - public async void ImportAudioAt(double pos, int trackIndex) - { - var project = Project; - if (project == null) - return; - - var topLevel = TopLevel.GetTopLevel(this); - if (topLevel == null) - return; - - var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - { - Title = "Open File", - AllowMultiple = false, - FileTypeFilter = [new("Audio Formats") { Patterns = AudioUtils.AllDecodableFormats.Select(format => "*." + format).ToList() }] - }); - var path = files.IsEmpty() ? null : files[0].TryGetLocalPath(); - if (path == null) - return; - - if (!File.Exists(path)) - return; - - if (!AudioUtils.TryGetAudioInfo(path, out var audioInfo)) - return; - - double startTime = project.TempoManager.GetTime(pos); - double dur = project.TempoManager.GetTick(startTime + audioInfo.duration) - pos; - var name = new FileInfo(path).Name; - - bool isNewTrack = project.Tracks.Count <= trackIndex; - while (project.Tracks.Count <= trackIndex) - { - project.NewTrack(); - } - var track = project.Tracks[trackIndex]; - var part = track.CreatePart(new AudioPartInfo() { Name = name, Pos = pos, Dur = dur, Path = path }); - track.InsertPart(part); - if (isNewTrack) - { - track.Name.Set(name); - } - project.Commit(); - } - - TickAxis TickAxis => mDependency.TickAxis; - TrackVerticalAxis TrackVerticalAxis => mDependency.TrackVerticalAxis; - IQuantization Quantization => mDependency.Quantization; - Project? Project => mDependency.ProjectProvider.Object; - - const double MIN_GRID_GAP = 12; - const double MIN_REALITY_GRID_GAP = MIN_GRID_GAP * 2; - const double LineWidth = 1; - - Color BarLineColor => Style.LINE.Opacity(1.5); - Color BeatLineColor => Style.LINE.Opacity(1); - Color CellLineColor => Style.LINE.Opacity(0.5); - Color SelectionColor => Style.HIGH_LIGHT; - - readonly IDependency mDependency; - readonly DisposableManager s = new(); -} diff --git a/TuneLab/Views/TrackGridItem.cs b/TuneLab/Views/TrackGridItem.cs deleted file mode 100644 index 7b310b3..0000000 --- a/TuneLab/Views/TrackGridItem.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Media; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using TuneLab.Base.Structures; -using TuneLab.GUI.Components; -using TuneLab.Data; - -namespace TuneLab.Views; - -internal partial class TrackGrid -{ - class TrackGridItem(TrackGrid trackGrid) : Item - { - public TrackGrid TrackGrid => trackGrid; - } - - class PartItem(TrackGrid trackGrid) : TrackGridItem(trackGrid) - { - public IPart Part; - public int TrackIndex; - - public Rect Rect() - { - double top = TrackGrid.TrackVerticalAxis.GetTop(TrackIndex); - double bottom = TrackGrid.TrackVerticalAxis.GetBottom(TrackIndex); - double left = TrackGrid.TickAxis.Tick2X(Part.StartPos()); - double right = TrackGrid.TickAxis.Tick2X(Part.EndPos()); - - return new Rect(left, top, right - left, bottom - top); - } - - public override bool Raycast(Avalonia.Point point) - { - return Rect().Contains(point); - } - } - - class PartEndResizeItem(TrackGrid trackGrid) : TrackGridItem(trackGrid) - { - public IPart Part; - public int TrackIndex; - - public override bool Raycast(Avalonia.Point point) - { - double top = TrackGrid.TrackVerticalAxis.GetTop(TrackIndex); - double bottom = TrackGrid.TrackVerticalAxis.GetBottom(TrackIndex); - double x = TrackGrid.TickAxis.Tick2X(Part.EndPos()); - return point.Y >= top && point.Y <= bottom && point.X > x - 8 && point.X < x + 8; - } - } - - class PartNameItem (TrackGrid trackGrid) : TrackGridItem(trackGrid) - { - public IPart Part; - public int TrackIndex; - - public override bool Raycast(Avalonia.Point point) - { - double top = TrackGrid.TrackVerticalAxis.GetTop(TrackIndex); - double left = TrackGrid.TickAxis.Tick2X(Part.StartPos()); - double right = TrackGrid.TickAxis.Tick2X(Part.EndPos()); - - var titleRect = new Rect(left, top, right - left, 16); - return titleRect.Contains(point); - } - } -} diff --git a/TuneLab/Views/TrackScrollView.cs b/TuneLab/Views/TrackScrollView.cs index db7bbdf..4b36afe 100644 --- a/TuneLab/Views/TrackScrollView.cs +++ b/TuneLab/Views/TrackScrollView.cs @@ -1,31 +1,29 @@ -using Avalonia.Controls; -using Avalonia.Input; +using Avalonia; using Avalonia.Media; -using Avalonia; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using TuneLab.Base.Event; -using TuneLab.Data; using TuneLab.GUI.Components; -using TuneLab.Base.Utils; +using TuneLab.Data; using TuneLab.GUI; +using TuneLab.Base.Data; +using TuneLab.Extensions.Formats.DataInfo; +using TuneLab.Audio; +using Avalonia.Input; +using Avalonia.Controls; +using Avalonia.Platform.Storage; +using System.IO; +using TuneLab.Utils; using TuneLab.Base.Science; +using TuneLab.Base.Utils; namespace TuneLab.Views; -internal class TrackScrollView : Panel, TrackGrid.IDependency +internal partial class TrackScrollView : View { - public TickAxis TickAxis => mDependency.TickAxis; - public TrackVerticalAxis TrackVerticalAxis => mDependency.TrackVerticalAxis; - public IQuantization Quantization => mDependency.Quantization; - public IProvider ProjectProvider => mDependency.ProjectProvider; - public IProvider EditingPart => mDependency.EditingPart; - public void SwitchEditingPart(IPart part) => mDependency.SwitchEditingPart(part); - public TrackGrid TrackGrid => mTrackGrid; - public interface IDependency { TickAxis TickAxis { get; } @@ -36,13 +34,17 @@ public interface IDependency void SwitchEditingPart(IPart part); } + public State OperationState => mState; + public TrackScrollView(IDependency dependency) { mDependency = dependency; - mTrackGrid = new TrackGrid(this); - Children.Add(mTrackGrid); - + mMiddleDragOperation = new(this); + mSelectOperation = new(this); + mPartMoveOperation = new(this); + mPartEndResizeOperation = new(this); + mDragFileOperation = new(this); mNameInput = new TextInput() { Padding = new(8, 0), @@ -62,12 +64,505 @@ public TrackScrollView(IDependency dependency) TickAxis.AxisChanged += InvalidateArrange; TrackVerticalAxis.AxisChanged += InvalidateArrange; + + mDependency.ProjectProvider.ObjectChanged.Subscribe(Update, s); + mDependency.ProjectProvider.When(project => project.Modified).Subscribe(Update, s); + mDependency.EditingPart.ObjectChanged.Subscribe(InvalidateVisual, s); + mDependency.ProjectProvider.When(project => project.Tracks.Any(track => track.Parts.Any(part => part.SelectionChanged))).Subscribe(InvalidateVisual, s); + mDependency.ProjectProvider.When(project => project.Tracks.Any(track => track.Parts.Any(part => part is AudioPart audioPart ? audioPart.AudioChanged : new ActionEvent()))).Subscribe(InvalidateVisual, s); // TODO: 支持一下可空类型的event + Quantization.QuantizationChanged += InvalidateVisual; + TickAxis.AxisChanged += Update; + TrackVerticalAxis.AxisChanged += Update; + + AddHandler(DragDrop.DragEnterEvent, OnDragEnter); + AddHandler(DragDrop.DragOverEvent, OnDragOver); + AddHandler(DragDrop.DragLeaveEvent, OnDragLeave); + AddHandler(DragDrop.DropEvent, OnDrop); } ~TrackScrollView() { - TickAxis.AxisChanged -= InvalidateArrange; - TrackVerticalAxis.AxisChanged -= InvalidateArrange; + s.DisposeAll(); + Quantization.QuantizationChanged -= InvalidateVisual; + TickAxis.AxisChanged -= Update; + TrackVerticalAxis.AxisChanged -= Update; + } + + protected override void OnRender(DrawingContext context) + { + context.FillRectangle(Brushes.Transparent, this.Rect()); + + if (Project == null) + return; + + var timeSignatureManager = Project.TimeSignatureManager; + + double startPos = TickAxis.MinVisibleTick; + double endPos = TickAxis.MaxVisibleTick; + + var startMeter = timeSignatureManager.GetMeterStatus(startPos); + var endMeter = timeSignatureManager.GetMeterStatus(endPos); + + int startIndex = startMeter.TimeSignatureIndex; + int endIndex = endMeter.TimeSignatureIndex; + + var timeSignatures = timeSignatureManager.TimeSignatures; + IBrush barLineBrush = BarLineColor.ToBrush(); + for (int timeSignatureIndex = startIndex; timeSignatureIndex <= endIndex; timeSignatureIndex++) + { + // draw bar + int nextTimeSignatureBarIndex = timeSignatureIndex + 1 == timeSignatures.Count ? (int)Math.Ceiling(endMeter.BarIndex) : timeSignatures[timeSignatureIndex + 1].BarIndex; + int thisTimeSignatureBarIndex = Math.Max(timeSignatures[timeSignatureIndex].BarIndex, (int)Math.Floor(startMeter.BarIndex)); + for (int barIndex = thisTimeSignatureBarIndex; barIndex < nextTimeSignatureBarIndex; barIndex++) + { + double xBarIndex = TickAxis.Tick2X(timeSignatures[timeSignatureIndex].GetTickByBarIndex(barIndex)); + context.FillRectangle(barLineBrush, new Rect(xBarIndex, 0, 1, Bounds.Height)); + } + + // draw beat + double pixelsPerBeat = timeSignatures[timeSignatureIndex].TicksPerBeat() * TickAxis.PixelsPerTick; + double beatOpacity = MathUtility.LineValue(MIN_GRID_GAP, 0, MIN_REALITY_GRID_GAP, 1, pixelsPerBeat).Limit(0, 1); + if (beatOpacity == 0) + continue; + + IPen beatLinePen = new Pen(BeatLineColor.Opacity(beatOpacity).ToUInt32(), LineWidth); + for (int barIndex = thisTimeSignatureBarIndex; barIndex < nextTimeSignatureBarIndex; barIndex++) + { + for (int beatIndex = 1; beatIndex < timeSignatures[timeSignatureIndex].Numerator; beatIndex++) + { + double xBeatIndex = TickAxis.Tick2X(timeSignatures[timeSignatureIndex].GetTickByBarAndBeat(barIndex, beatIndex)); + double x = xBeatIndex + LineWidth / 2; + context.DrawLine(beatLinePen, new Point(x, 0), new Point(x, Bounds.Height)); + } + } + + // draw quantization + int quantizationBase = (int)Quantization.Base; + int ticksPerBase = timeSignatures[timeSignatureIndex].TicksPerBeat() / quantizationBase; + double pixelsPerBase = ticksPerBase * TickAxis.PixelsPerTick; + double baseOpacity = MathUtility.LineValue(MIN_GRID_GAP, 0, MIN_REALITY_GRID_GAP, 1, pixelsPerBase).Limit(0, 1); + if (baseOpacity == 0) + continue; + + IPen baseLinePen = new Pen(CellLineColor.Opacity(baseOpacity).ToUInt32(), LineWidth); + for (int barIndex = thisTimeSignatureBarIndex; barIndex < nextTimeSignatureBarIndex; barIndex++) + { + for (int beatIndex = 0; beatIndex < timeSignatures[timeSignatureIndex].Numerator; beatIndex++) + { + double beatPos = timeSignatures[timeSignatureIndex].GetTickByBarAndBeat(barIndex, beatIndex); + for (int baseIndex = 1; baseIndex < quantizationBase; baseIndex++) + { + double xBase = TickAxis.Tick2X(beatPos + baseIndex * ticksPerBase); + double x = xBase + LineWidth / 2; + context.DrawLine(baseLinePen, new Point(x, 0), new Point(x, Bounds.Height)); + } + } + } + + int quantizationDivision = (int)Quantization.Division; + int noteDivision = Math.Max(quantizationDivision * 4, timeSignatures[timeSignatureIndex].Denominator); + int beatDivision = noteDivision / timeSignatures[timeSignatureIndex].Denominator; + double thisTimeSignaturePos = timeSignatures[timeSignatureIndex].GetTickByBarIndex(thisTimeSignatureBarIndex); + for (int cellsPerBase = 2; cellsPerBase <= beatDivision; cellsPerBase *= 2) + { + int ticksPerCell = ticksPerBase / cellsPerBase; + double pixelsPerCell = ticksPerCell * TickAxis.PixelsPerTick; + double cellOpacity = MathUtility.LineValue(MIN_GRID_GAP, 0, MIN_REALITY_GRID_GAP, 1, pixelsPerCell).Limit(0, 1); + if (cellOpacity == 0) + break; + + IPen cellLinePen = new Pen(CellLineColor.Opacity(cellOpacity).ToUInt32(), LineWidth); + int cellCount = (nextTimeSignatureBarIndex - thisTimeSignatureBarIndex) * timeSignatures[timeSignatureIndex].Numerator * quantizationBase * cellsPerBase / 2; + for (int cellIndex = 0; cellIndex < cellCount; cellIndex++) + { + double cellPos = thisTimeSignaturePos + (cellIndex * 2 + 1) * ticksPerCell; + double xCell = TickAxis.Tick2X(cellPos); + double x = xCell + LineWidth / 2; + context.DrawLine(cellLinePen, new Point(x, 0), new Point(x, Bounds.Height)); + } + } + } + + var tempoManager = Project.TempoManager; + + // draw parts + IBrush lineBrush = Style.DARK.ToBrush(); + IBrush partBrush = Style.ITEM.Opacity(0.25).ToBrush(); + IBrush selectedPartBrush = Style.HIGH_LIGHT.Opacity(0.25).ToBrush(); + double partLineWidth = 1; + IPen editPartPen = new Pen(Style.LIGHT_WHITE.ToBrush(), partLineWidth); + IPen partSelectPen = new Pen(Style.HIGH_LIGHT.ToBrush(), partLineWidth); + IPen partPen = new Pen(Style.HIGH_LIGHT.Opacity(0.5).ToBrush(), partLineWidth); + IBrush titleBrush = Colors.White.Opacity(0.7).ToBrush(); + IBrush noteBrush = Style.ITEM.ToBrush(); + IBrush noteSelectBrush = Style.HIGH_LIGHT.ToBrush(); + for (int trackIndex = 0; trackIndex < Project.Tracks.Count; trackIndex++) + { + double lineBottom = TrackVerticalAxis.GetTop(trackIndex + 1); + if (lineBottom <= 0) + continue; + + double top = TrackVerticalAxis.GetTop(trackIndex); + if (top >= Bounds.Height) + break; + + context.FillRectangle(lineBrush, new(0, lineBottom - 1, Bounds.Width, 1)); + + double bottom = TrackVerticalAxis.GetBottom(trackIndex); + if (bottom <= 0) + continue; + + var track = Project.Tracks[trackIndex]; + foreach (var part in track.Parts) + { + if (part.EndPos() <= startPos) + continue; + + if (part.StartPos() >= endPos) + break; + + double left = Math.Max(TickAxis.Tick2X(part.StartPos()), -8); + double right = Math.Min(TickAxis.Tick2X(part.EndPos()), Bounds.Width + 8); + + var partRect = new Rect(left, top, right - left, bottom - top); + context.DrawRectangle(part.IsSelected ? selectedPartBrush : partBrush, part == mDependency.EditingPart.Object ? editPartPen : part.IsSelected ? partSelectPen : partPen, partRect.Inflate(-partLineWidth / 2)); + var titleRect = partRect.WithHeight(16).Adjusted(Math.Max(0, -partRect.Left) + 8, 0, -8, 0); + var contentRect = partRect.Adjusted(0, 16, 0, 0); + if (part is MidiPart midiPart) + { + using (context.PushClip(titleRect)) + { + context.DrawString(string.Format("{0}[{1}]", midiPart.Name, midiPart.Voice.Name), titleRect, titleBrush, 12, Alignment.LeftCenter, Alignment.LeftCenter); + } + + if (midiPart.Notes.IsEmpty()) + continue; + + using (context.PushClip(contentRect)) + { + var (minPitch, maxPitch) = midiPart.PitchRange(); + double pitchGap = maxPitch - minPitch + 1; + double pitchHeight = Math.Min(contentRect.Height / pitchGap, 8); + double partStartPos = Math.Max(startPos, midiPart.StartPos) - midiPart.Pos; + double partEndPos = Math.Min(endPos, midiPart.EndPos) - midiPart.Pos; + IBrush brush = part.IsSelected ? noteSelectBrush : noteBrush; + foreach (var note in midiPart.Notes) + { + if (note.EndPos() <= partStartPos) + continue; + + if (note.StartPos() >= partEndPos) + break; + + double noteLeft = TickAxis.Tick2X(note.StartPos() + midiPart.Pos); + double noteRight = TickAxis.Tick2X(note.EndPos() + midiPart.Pos); + context.FillRectangle(brush, new(noteLeft, contentRect.Y + (maxPitch - note.Pitch.Value) * pitchHeight, noteRight - noteLeft, pitchHeight)); + } + } + } + else if (part is AudioPart audioPart) + { + using (context.PushClip(titleRect)) + { + context.DrawString(string.Format("{0}[{1}]", audioPart.Name, audioPart.Path), titleRect, titleBrush, 12, Alignment.LeftCenter, Alignment.LeftCenter); + } + + if (audioPart.ChannelCount > 0) + { + for (int channelIndex = 0; channelIndex < audioPart.ChannelCount; channelIndex++) + { + if (audioPart.EndPos < TickAxis.MinVisibleTick) + continue; + + if (audioPart.StartPos > TickAxis.MaxVisibleTick) + break; + + var waveform = audioPart.GetWaveform(channelIndex); + if (waveform == null) + continue; + + double minTick = Math.Max(TickAxis.MinVisibleTick, audioPart.StartPos); + double maxTick = Math.Min(TickAxis.MaxVisibleTick, audioPart.EndPos); + double minX = TickAxis.Tick2X(minTick); + double maxX = TickAxis.Tick2X(maxTick); + var xs = new List(); + var positions = new List(); + double gap = 1; + double xp = minX - gap; + double startTime = audioPart.TempoManager.GetTime(audioPart.StartPos); + do + { + xp += gap; + xs.Add(xp); + double time = tempoManager.GetTime(TickAxis.X2Tick(xp)); + positions.Add((time - startTime) * ((IAudioSource)audioPart).SamplingRate); + } + while (xp < maxX); + + if (positions.Count < 2) + continue; + + double channelHeight = contentRect.Height / audioPart.ChannelCount; + float channelTop = (float)(contentRect.Top + channelHeight * channelIndex); + float r = (float)channelHeight / 2; + float toY(float value) => channelTop + (1 - value) * r; + + var values = waveform.GetValues(positions); + var peaks = waveform.GetPeaks(positions, values); + for (int i = 0; i < xs.Count; i++) + { + values[i] = toY(values[i]); + } + for (int i = 0; i < peaks.Length; i++) + { + peaks[i].min = toY(peaks[i].min); + peaks[i].max = toY(peaks[i].max); + } + // 性能优先先采用画矩形的方案 + using (var mask = context.PushOpacity(0.5)) + { + for (int i = 0; i < peaks.Length; i++) + { + double x = xs[i]; + var peak = peaks[i]; + context.FillRectangle(Style.LIGHT_WHITE.ToBrush(), new(xs[i], peak.max, gap, peak.min - peak.max)); + } + } + + /* + for (int i = 0; i < peaks.Length; i++) + { + double x = xs[i]; + var peak = peaks[i]; + var path = new PathGeometry(); + using (var pathContext = path.Open()) + { + pathContext.BeginFigure(new Point(x, values[i]), true); + pathContext.LineTo(new Point(x + gap * peak.minRatio, peak.min)); + pathContext.LineTo(new Point(xs[i + 1], values[i + 1])); + pathContext.LineTo(new Point(x + gap * peak.maxRatio, peak.max)); + pathContext.EndFigure(true); + } + context.DrawGeometry(Style.LIGHT_WHITE.Opacity(0.5).ToBrush(), null, path); + } + */ + /* + for (int i = 0; i < peaks.Length; i++) + { + var points = new List(); + double x = xs[i]; + var peak = peaks[i]; + points.Add(new Point(x, values[i])); + points.Add(new Point(x + gap * peak.minRatio, peak.min)); + points.Add(new Point(xs[i + 1], values[i + 1])); + points.Add(new Point(x + gap * peak.maxRatio, peak.max)); + context.DrawCurve(points, Style.LIGHT_WHITE.Opacity(0.5), gap, true); + } + */ + /* + var points = new List(); + for (int i = 0; i < peaks.Length; i++) + { + double x = xs[i]; + var peak = peaks[i]; + points.Add(new Point(x, values[i])); + points.Add(new Point(x + gap * peak.minRatio, peak.min)); + } + for (int i = peaks.Length; i > 0; i--) + { + double x = xs[i]; + var peak = peaks[i - 1]; + points.Add(new Point(x, values[i])); + points.Add(new Point(x + gap * peak.maxRatio, peak.max)); + } + context.DrawCurve(points, Style.LIGHT_WHITE.Opacity(0.5), gap, true); + */ + } + } + } + } + } + + // draw import parts + if (mDragFileOperation.IsOperating) + { + double left = Math.Max(TickAxis.Tick2X(mDragFileOperation.Pos), -8); + for (int i = 0; i < mDragFileOperation.PreImportAudioInfos.Count; i++) + { + var info = mDragFileOperation.PreImportAudioInfos[i]; + double right = Math.Min(TickAxis.Tick2X(info.EndPos), Bounds.Width + 8); + double top = TrackVerticalAxis.GetTop(mDragFileOperation.TrackIndex + i); + double bottom = TrackVerticalAxis.GetBottom(mDragFileOperation.TrackIndex + i); + var partRect = new Rect(left, top, right - left, bottom - top); + context.FillRectangle(partBrush, partRect); + + var titleRect = partRect.WithHeight(16).Adjusted(Math.Max(0, -partRect.Left) + 8, 0, -8, 0); + var contentRect = partRect.Adjusted(0, 16 + 8, 0, -8); + using (context.PushClip(titleRect)) + { + context.DrawString(string.Format("{0}[{1}]", info.name, info.path), titleRect, titleBrush, 12, Alignment.LeftCenter, Alignment.LeftCenter); + } + } + } + + // draw selection + if (mSelectOperation.IsOperating) + { + var rect = mSelectOperation.SelectionRect(); + context.DrawRectangle(SelectionColor.Opacity(0.25).ToBrush(), new Pen(SelectionColor.ToUInt32()), rect); + } + } + + public double QuantizedCellTicks() + { + int quantizationBase = (int)Quantization.Base; + double division = (int)Math.Pow(2, Math.Log2(TickAxis.PixelsPerTick * MusicTheory.RESOLUTION / quantizationBase / MIN_GRID_GAP).Floor()).Limit(1, 32); + return MusicTheory.RESOLUTION / quantizationBase / division; + } + + public double GetQuantizedTick(double tick) + { + double cell = QuantizedCellTicks(); + return (tick / cell).Round() * cell; + } + + struct PartInfosWithTrackIndex(int trackIndex, IEnumerable parts) + { + public int trackIndex = trackIndex; + public IEnumerable parts = parts; + } + + struct PartClipboard() + { + public bool IsEmpty() => content.IsEmpty(); + + public double pos = double.MaxValue; + public List content = new(); + } + + PartClipboard mPartClipboard = new(); + public void Copy() + { + if (Project == null) + return; + + var clipboard = new PartClipboard(); + for (int trackIndex = 0; trackIndex < Project.Tracks.Count; trackIndex++) + { + var selectedParts = Project.Tracks[trackIndex].Parts.AllSelectedItems(); + if (selectedParts.IsEmpty()) + continue; + + clipboard.pos = Math.Min(clipboard.pos, selectedParts.First().Pos.Value); + clipboard.content.Add(new(trackIndex, selectedParts.Select(part => part.GetInfo()))); + } + + if (clipboard.IsEmpty()) + return; + + mPartClipboard = clipboard; + } + + public void PasteAt(double pos) + { + if (Project == null) + return; + + if (mPartClipboard.IsEmpty()) + return; + + double offset = pos - mPartClipboard.pos; + Project.Tracks.SelectMany(track => track.Parts).DeselectAllItems(); + foreach (var partInfosWithTrackIndex in mPartClipboard.content) + { + var track = Project.Tracks[partInfosWithTrackIndex.trackIndex]; + foreach (var partInfo in partInfosWithTrackIndex.parts) + { + var part = track.CreatePart(partInfo); + part.Select(); + part.Pos.Set(part.Pos.Value + offset); + track.InsertPart(part); + } + } + Project.Commit(); + } + + public bool CanPaste => !mPartClipboard.IsEmpty(); + + public void Cut() + { + Copy(); + DeleteAllSelectedParts(); + } + + public void DeleteAllSelectedParts() + { + if (Project == null) + return; + + foreach (var track in Project.Tracks) + { + var selectedParts = track.Parts.AllSelectedItems(); + foreach (var part in selectedParts) + { + track.RemovePart(part); + } + } + Project.Commit(); + } + + public void DeleteTrackAt(int trackIndex) + { + if (Project == null) + return; + + Project.RemoveTrackAt(trackIndex); + Project.Commit(); + } + + public async void ImportAudioAt(double pos, int trackIndex) + { + var project = Project; + if (project == null) + return; + + var topLevel = TopLevel.GetTopLevel(this); + if (topLevel == null) + return; + + var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions + { + Title = "Open File", + AllowMultiple = false, + FileTypeFilter = [new("Audio Formats") { Patterns = AudioUtils.AllDecodableFormats.Select(format => "*." + format).ToList() }] + }); + var path = files.IsEmpty() ? null : files[0].TryGetLocalPath(); + if (path == null) + return; + + if (!File.Exists(path)) + return; + + if (!AudioUtils.TryGetAudioInfo(path, out var audioInfo)) + return; + + double startTime = project.TempoManager.GetTime(pos); + double dur = project.TempoManager.GetTick(startTime + audioInfo.duration) - pos; + var name = new FileInfo(path).Name; + + bool isNewTrack = project.Tracks.Count <= trackIndex; + while (project.Tracks.Count <= trackIndex) + { + project.NewTrack(); + } + var track = project.Tracks[trackIndex]; + var part = track.CreatePart(new AudioPartInfo() { Name = name, Pos = pos, Dur = dur, Path = path }); + track.InsertPart(part); + if (isNewTrack) + { + track.Name.Set(name); + } + project.Commit(); } public void EnterInputPartName(IPart part, int trackIndex) @@ -83,9 +578,8 @@ public void EnterInputPartName(IPart part, int trackIndex) mNameInput.SelectAll(); } - protected override Size ArrangeOverride(Size finalSize) + protected override Size OnArrangeOverride(Size finalSize) { - mTrackGrid.Arrange(new Rect(finalSize)); if (mNameInput.IsVisible) mNameInput.Arrange(NameInputRect()); @@ -128,8 +622,22 @@ Rect NameInputRect() const double NameInputMinWidth = 60; const double NameInputMaxWidth = 600; - readonly IDependency mDependency; - - readonly TrackGrid mTrackGrid; readonly TextInput mNameInput; -} \ No newline at end of file + + TickAxis TickAxis => mDependency.TickAxis; + TrackVerticalAxis TrackVerticalAxis => mDependency.TrackVerticalAxis; + IQuantization Quantization => mDependency.Quantization; + Project? Project => mDependency.ProjectProvider.Object; + + const double MIN_GRID_GAP = 12; + const double MIN_REALITY_GRID_GAP = MIN_GRID_GAP * 2; + const double LineWidth = 1; + + Color BarLineColor => Style.LINE.Opacity(1.5); + Color BeatLineColor => Style.LINE.Opacity(1); + Color CellLineColor => Style.LINE.Opacity(0.5); + Color SelectionColor => Style.HIGH_LIGHT; + + readonly IDependency mDependency; + readonly DisposableManager s = new(); +} diff --git a/TuneLab/Views/TrackScrollViewItem.cs b/TuneLab/Views/TrackScrollViewItem.cs new file mode 100644 index 0000000..86fb7f1 --- /dev/null +++ b/TuneLab/Views/TrackScrollViewItem.cs @@ -0,0 +1,72 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TuneLab.Base.Structures; +using TuneLab.GUI.Components; +using TuneLab.Data; + +namespace TuneLab.Views; + +internal partial class TrackScrollView +{ + class TrackScrollViewItem(TrackScrollView trackScrollView) : Item + { + public TrackScrollView TrackScrollView => trackScrollView; + } + + class PartItem(TrackScrollView trackScrollView) : TrackScrollViewItem(trackScrollView) + { + public IPart Part; + public int TrackIndex; + + public Rect Rect() + { + double top = TrackScrollView.TrackVerticalAxis.GetTop(TrackIndex); + double bottom = TrackScrollView.TrackVerticalAxis.GetBottom(TrackIndex); + double left = TrackScrollView.TickAxis.Tick2X(Part.StartPos()); + double right = TrackScrollView.TickAxis.Tick2X(Part.EndPos()); + + return new Rect(left, top, right - left, bottom - top); + } + + public override bool Raycast(Avalonia.Point point) + { + return Rect().Contains(point); + } + } + + class PartEndResizeItem(TrackScrollView trackScrollView) : TrackScrollViewItem(trackScrollView) + { + public IPart Part; + public int TrackIndex; + + public override bool Raycast(Avalonia.Point point) + { + double top = TrackScrollView.TrackVerticalAxis.GetTop(TrackIndex); + double bottom = TrackScrollView.TrackVerticalAxis.GetBottom(TrackIndex); + double x = TrackScrollView.TickAxis.Tick2X(Part.EndPos()); + return point.Y >= top && point.Y <= bottom && point.X > x - 8 && point.X < x + 8; + } + } + + class PartNameItem (TrackScrollView trackScrollView) : TrackScrollViewItem(trackScrollView) + { + public IPart Part; + public int TrackIndex; + + public override bool Raycast(Avalonia.Point point) + { + double top = TrackScrollView.TrackVerticalAxis.GetTop(TrackIndex); + double left = TrackScrollView.TickAxis.Tick2X(Part.StartPos()); + double right = TrackScrollView.TickAxis.Tick2X(Part.EndPos()); + + var titleRect = new Rect(left, top, right - left, 16); + return titleRect.Contains(point); + } + } +} diff --git a/TuneLab/Views/TrackGridOperation.cs b/TuneLab/Views/TrackScrollViewOperation.cs similarity index 85% rename from TuneLab/Views/TrackGridOperation.cs rename to TuneLab/Views/TrackScrollViewOperation.cs index f5d38db..a872e02 100644 --- a/TuneLab/Views/TrackGridOperation.cs +++ b/TuneLab/Views/TrackScrollViewOperation.cs @@ -24,7 +24,7 @@ namespace TuneLab.Views; -internal partial class TrackGrid +internal partial class TrackScrollView { protected override void OnScroll(WheelEventArgs e) { @@ -74,7 +74,7 @@ protected override void OnMouseDown(MouseDownEventArgs e) } else if (e.IsDoubleClick && item is PartNameItem partNameItem) { - mDependency.EnterInputPartName(partNameItem.Part, partNameItem.TrackIndex); + EnterInputPartName(partNameItem.Part, partNameItem.TrackIndex); } else if (item is PartEndResizeItem partEndResizeItem) { @@ -401,13 +401,13 @@ void OnDragLeave(object? sender, DragEventArgs e) } } - class Operation(TrackGrid trackGrid) + class Operation(TrackScrollView trackScrollView) { - public TrackGrid TrackGrid => trackGrid; - public State State { get => TrackGrid.mState; set => TrackGrid.mState = value; } + public TrackScrollView TrackScrollView => trackScrollView; + public State State { get => TrackScrollView.mState; set => TrackScrollView.mState = value; } } - class MiddleDragOperation(TrackGrid trackGrid) : Operation(trackGrid) + class MiddleDragOperation(TrackScrollView trackScrollView) : Operation(trackScrollView) { public bool IsOperating => mIsDragging; @@ -417,10 +417,10 @@ public void Down(Avalonia.Point point) return; mIsDragging = true; - mDownTick = TrackGrid.TickAxis.X2Tick(point.X); - mDownYPos = TrackGrid.TrackVerticalAxis.Coor2Pos(point.Y); - TrackGrid.TickAxis.StopMoveAnimation(); - TrackGrid.TrackVerticalAxis.StopMoveAnimation(); + mDownTick = TrackScrollView.TickAxis.X2Tick(point.X); + mDownYPos = TrackScrollView.TrackVerticalAxis.Coor2Pos(point.Y); + TrackScrollView.TickAxis.StopMoveAnimation(); + TrackScrollView.TrackVerticalAxis.StopMoveAnimation(); } public void Move(Avalonia.Point point) @@ -428,8 +428,8 @@ public void Move(Avalonia.Point point) if (!mIsDragging) return; - TrackGrid.TickAxis.MoveTickToX(mDownTick, point.X); - TrackGrid.TrackVerticalAxis.MovePosToCoor(mDownYPos, point.Y); + TrackScrollView.TickAxis.MoveTickToX(mDownTick, point.X); + TrackScrollView.TrackVerticalAxis.MovePosToCoor(mDownYPos, point.Y); } public void Up() @@ -447,7 +447,7 @@ public void Up() readonly MiddleDragOperation mMiddleDragOperation; - class SelectOperation(TrackGrid trackGrid) : Operation(trackGrid) + class SelectOperation(TrackScrollView trackScrollView) : Operation(trackScrollView) { public bool IsOperating => State == State.Selecting; @@ -456,15 +456,15 @@ public void Down(Avalonia.Point point, bool ctrl) if (State != State.None) return; - if (TrackGrid.Project == null) + if (TrackScrollView.Project == null) return; State = State.Selecting; - mDownTick = TrackGrid.TickAxis.X2Tick(point.X); - mDownPosition = TrackGrid.TrackVerticalAxis.GetPosition(point.Y); + mDownTick = TrackScrollView.TickAxis.X2Tick(point.X); + mDownPosition = TrackScrollView.TrackVerticalAxis.GetPosition(point.Y); if (ctrl) { - mSelectedParts = TrackGrid.Project.Tracks.SelectMany(track => track.Parts).AllSelectedItems(); + mSelectedParts = TrackScrollView.Project.Tracks.SelectMany(track => track.Parts).AllSelectedItems(); } Move(point); } @@ -474,12 +474,12 @@ public void Move(Avalonia.Point point) if (!IsOperating) return; - mTick = TrackGrid.TickAxis.X2Tick(point.X); - mPosition = TrackGrid.TrackVerticalAxis.GetPosition(point.Y); - if (TrackGrid.Project == null) + mTick = TrackScrollView.TickAxis.X2Tick(point.X); + mPosition = TrackScrollView.TrackVerticalAxis.GetPosition(point.Y); + if (TrackScrollView.Project == null) return; - TrackGrid.Project.Tracks.SelectMany(track => track.Parts).DeselectAllItems(); + TrackScrollView.Project.Tracks.SelectMany(track => track.Parts).DeselectAllItems(); if (mSelectedParts != null) { foreach (var part in mSelectedParts) @@ -489,17 +489,17 @@ public void Move(Avalonia.Point point) double maxTick = Math.Max(mTick, mDownTick); double minY = Math.Min(mPosition.Y, mDownPosition.Y); double maxY = Math.Max(mPosition.Y, mDownPosition.Y); - for (int i = 0; i < TrackGrid.Project.Tracks.Count; i++) + for (int i = 0; i < TrackScrollView.Project.Tracks.Count; i++) { - double bottom = TrackGrid.TrackVerticalAxis.GetBottom(i); + double bottom = TrackScrollView.TrackVerticalAxis.GetBottom(i); if (bottom <= minY) continue; - double top = TrackGrid.TrackVerticalAxis.GetTop(i); + double top = TrackScrollView.TrackVerticalAxis.GetTop(i); if (top >= maxY) break; - var track = TrackGrid.Project.Tracks[i]; + var track = TrackScrollView.Project.Tracks[i]; foreach (var part in track.Parts) { if (part.EndPos() <= minTick) @@ -511,7 +511,7 @@ public void Move(Avalonia.Point point) part.Select(); } } - TrackGrid.InvalidateVisual(); + TrackScrollView.InvalidateVisual(); } public void Up() @@ -521,15 +521,15 @@ public void Up() State = State.None; mSelectedParts = null; - TrackGrid.InvalidateVisual(); + TrackScrollView.InvalidateVisual(); } public Rect SelectionRect() { double minTick = Math.Min(mTick, mDownTick); double maxTick = Math.Max(mTick, mDownTick); - double left = TrackGrid.TickAxis.Tick2X(minTick); - double right = TrackGrid.TickAxis.Tick2X(maxTick); + double left = TrackScrollView.TickAxis.Tick2X(minTick); + double right = TrackScrollView.TickAxis.Tick2X(maxTick); double top = Math.Min(mPosition.Y, mDownPosition.Y); double bottom = Math.Max(mPosition.Y, mDownPosition.Y); return new Rect(left, top, right - left, bottom - top); @@ -544,24 +544,24 @@ public Rect SelectionRect() readonly SelectOperation mSelectOperation; - class PartMoveOperation(TrackGrid trackGrid) : Operation(trackGrid) + class PartMoveOperation(TrackScrollView trackScrollView) : Operation(trackScrollView) { public void Down(Avalonia.Point point, bool ctrl, IPart part) { - if (TrackGrid.Project == null) + if (TrackScrollView.Project == null) return; mCtrl = ctrl; mIsSelected = part.IsSelected; if (!mCtrl && !mIsSelected) { - TrackGrid.Project.Tracks.SelectMany(track => track.Parts).DeselectAllItems(); + TrackScrollView.Project.Tracks.SelectMany(track => track.Parts).DeselectAllItems(); } part.Select(); - for (int trackIndex = 0; trackIndex < TrackGrid.Project.Tracks.Count; trackIndex++) + for (int trackIndex = 0; trackIndex < TrackScrollView.Project.Tracks.Count; trackIndex++) { - var selectedParts = TrackGrid.Project.Tracks[trackIndex].Parts.AllSelectedItems(); + var selectedParts = TrackScrollView.Project.Tracks[trackIndex].Parts.AllSelectedItems(); if (selectedParts.IsEmpty()) continue; @@ -579,14 +579,14 @@ public void Down(Avalonia.Point point, bool ctrl, IPart part) mHead = part.Head; mPart = part; mDownPartPos = mPart.Pos.Value; - mTickOffset = TrackGrid.TickAxis.X2Tick(point.X) - part.Pos.Value; - mTrackIndex = TrackGrid.TrackVerticalAxis.GetPosition(point.Y).TrackIndex; - TrackGrid.TrackVerticalAxis.SetAutoContentSize(false); + mTickOffset = TrackScrollView.TickAxis.X2Tick(point.X) - part.Pos.Value; + mTrackIndex = TrackScrollView.TrackVerticalAxis.GetPosition(point.Y).TrackIndex; + TrackScrollView.TrackVerticalAxis.SetAutoContentSize(false); } public void Move(Avalonia.Point point) { - var project = TrackGrid.Project; + var project = TrackScrollView.Project; if (project == null) return; @@ -596,10 +596,10 @@ public void Move(Avalonia.Point point) if (mMoveParts.IsEmpty()) return; - var position = TrackGrid.TrackVerticalAxis.GetPosition(point.Y); + var position = TrackScrollView.TrackVerticalAxis.GetPosition(point.Y); var trackIndex = position.TrackIndex; int trackIndexOffset = Math.Max(-mMoveParts.First().trackIndex, trackIndex - mTrackIndex); - double pos = TrackGrid.GetQuantizedTick(TrackGrid.TickAxis.X2Tick(point.X) - mTickOffset); + double pos = TrackScrollView.GetQuantizedTick(TrackScrollView.TickAxis.X2Tick(point.X) - mTickOffset); double posOffset = pos - mDownPartPos; if (posOffset == mLastPosOffset && trackIndexOffset == mLastTrackIndexOffset) return; @@ -643,7 +643,7 @@ public void Up() if (mPart == null) return; - if (TrackGrid.Project == null) + if (TrackScrollView.Project == null) return; foreach (var movePart in mMoveParts.SelectMany(p => p.parts)) @@ -653,7 +653,7 @@ public void Up() } if (mMoved) { - TrackGrid.Project.Commit(); + TrackScrollView.Project.Commit(); } else { @@ -667,11 +667,11 @@ public void Up() } else { - TrackGrid.Project.Tracks.SelectMany(track => track.Parts).DeselectAllItems(); + TrackScrollView.Project.Tracks.SelectMany(track => track.Parts).DeselectAllItems(); mPart.Select(); } } - TrackGrid.TrackVerticalAxis.SetAutoContentSize(true); + TrackScrollView.TrackVerticalAxis.SetAutoContentSize(true); mMoved = false; mPart = null; mMoveParts.Clear(); @@ -702,14 +702,14 @@ struct PartsWithTrackIndex(int trackIndex, IReadOnlyCollection parts) readonly PartMoveOperation mPartMoveOperation; - class PartEndResizeOperation(TrackGrid trackGrid) : Operation(trackGrid) + class PartEndResizeOperation(TrackScrollView trackScrollView) : Operation(trackScrollView) { public void Down(double x, IPart part, ITrack track) { State = State.PartEndResizing; mPart = part; mTrack = track; - double end = TrackGrid.TickAxis.Tick2X(mPart.EndPos()); + double end = TrackScrollView.TickAxis.Tick2X(mPart.EndPos()); mOffset = x - end; mHead = mPart.Head; } @@ -724,8 +724,8 @@ public void Move(double x) mPart.DiscardTo(mHead); double end = x - mOffset; - double endTick = TrackGrid.TickAxis.X2Tick(end); - mPart.Dur.Set(Math.Max(TrackGrid.GetQuantizedTick(endTick) - mPart.Pos.Value, TrackGrid.QuantizedCellTicks())); + double endTick = TrackScrollView.TickAxis.X2Tick(end); + mPart.Dur.Set(Math.Max(TrackScrollView.GetQuantizedTick(endTick) - mPart.Pos.Value, TrackScrollView.QuantizedCellTicks())); mTrack.RemovePart(mPart); mTrack.InsertPart(mPart); } @@ -750,7 +750,7 @@ public void Up() readonly PartEndResizeOperation mPartEndResizeOperation; - class DragFileOperation(TrackGrid trackGrid) : Operation(trackGrid) + class DragFileOperation(TrackScrollView trackScrollView) : Operation(trackScrollView) { public bool IsOperating => State == State.FileDragging; public double Pos => mLastPos; @@ -782,10 +782,10 @@ public void Enter(IEnumerable files) public void Over(Avalonia.Point point) { - mLastTrackIndex = TrackGrid.TrackVerticalAxis.GetPosition(point.Y).TrackIndex; - mLastPos = TrackGrid.GetQuantizedTick(TrackGrid.TickAxis.X2Tick(point.X)); + mLastTrackIndex = TrackScrollView.TrackVerticalAxis.GetPosition(point.Y).TrackIndex; + mLastPos = TrackScrollView.GetQuantizedTick(TrackScrollView.TickAxis.X2Tick(point.X)); - TrackGrid.InvalidateVisual(); + TrackScrollView.InvalidateVisual(); } public void Leave() @@ -802,26 +802,26 @@ public void Drop() if (mPreImportAudioInfos.IsEmpty()) return; - if (TrackGrid.Project == null) + if (TrackScrollView.Project == null) return; - while (TrackGrid.Project.Tracks.Count < mLastTrackIndex + mPreImportAudioInfos.Count) - TrackGrid.Project.NewTrack(); + while (TrackScrollView.Project.Tracks.Count < mLastTrackIndex + mPreImportAudioInfos.Count) + TrackScrollView.Project.NewTrack(); var trackIndex = mLastTrackIndex; foreach (var info in mPreImportAudioInfos) { - var track = TrackGrid.Project.Tracks[trackIndex]; + var track = TrackScrollView.Project.Tracks[trackIndex]; track.InsertPart(track.CreatePart(new AudioPartInfo() { Pos = mLastPos, Dur = info.Dur, Name = info.name, Path = info.path })); trackIndex++; } - TrackGrid.Project.Commit(); + TrackScrollView.Project.Commit(); mPreImportAudioInfos.Clear(); } public class PreImportAudioInfo(DragFileOperation operation) { - public double EndPos => operation.TrackGrid.Project!.TempoManager.GetTick(operation.TrackGrid.Project.TempoManager.GetTime(operation.mLastPos) + duration); + public double EndPos => operation.TrackScrollView.Project!.TempoManager.GetTick(operation.TrackScrollView.Project.TempoManager.GetTime(operation.mLastPos) + duration); public double Dur => EndPos - operation.mLastPos; public required string path; public required string name; diff --git a/TuneLab/Views/TrackWindow.cs b/TuneLab/Views/TrackWindow.cs index da4d4c5..18354c9 100644 --- a/TuneLab/Views/TrackWindow.cs +++ b/TuneLab/Views/TrackWindow.cs @@ -29,7 +29,7 @@ internal class TrackWindow : DockPanel, TimelineView.IDependency, TrackScrollVie public IPlayhead Playhead => mDependency.Playhead; public IProvider EditingPart => mDependency.EditingPart; public Project? Project => ProjectProvider.Object; - public TrackGrid TrackGrid => mTrackScrollView.TrackGrid; + public TrackScrollView TrackScrollView => mTrackScrollView; public bool IsAutoPage => true; public interface IDependency @@ -102,25 +102,25 @@ protected override void OnKeyDown(KeyEventArgs e) if (e.IsHandledByTextBox()) return; - switch (TrackGrid.OperationState) + switch (TrackScrollView.OperationState) { - case TrackGrid.State.None: + case TrackScrollView.State.None: e.Handled = true; if (e.Match(Key.Delete)) { - TrackGrid.DeleteAllSelectedParts(); + TrackScrollView.DeleteAllSelectedParts(); } else if (e.Match(Key.C, ModifierKeys.Ctrl)) { - TrackGrid.Copy(); + TrackScrollView.Copy(); } else if (e.Match(Key.X, ModifierKeys.Ctrl)) { - TrackGrid.Cut(); + TrackScrollView.Cut(); } else if (e.Match(Key.V, ModifierKeys.Ctrl)) { - TrackGrid.PasteAt(TrackGrid.GetQuantizedTick(Playhead.Pos)); + TrackScrollView.PasteAt(TrackScrollView.GetQuantizedTick(Playhead.Pos)); } else if (e.Match(Key.A, ModifierKeys.Ctrl)) {