From aac156d6a0c2000b39d1e1c0317bab343bc815b1 Mon Sep 17 00:00:00 2001 From: NeoCodeMatrix86 <40752681+NeoCoderMatrix86@users.noreply.github.com> Date: Tue, 13 Oct 2026 10:26:30 +0200 Subject: [PATCH 01/10] add buttons for moving several positions --- .../Shared/TrackList/TrackList.razor | 1 + .../TrackList/TrackListControlButtons.de.resx | 6 +++++ .../TrackList/TrackListControlButtons.razor | 24 ++++++++++++++++--- .../TrackList/TrackListControlButtons.resx | 6 +++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackList.razor b/AudioCuesheetEditor/Shared/TrackList/TrackList.razor index 60c33574..16767616 100644 --- a/AudioCuesheetEditor/Shared/TrackList/TrackList.razor +++ b/AudioCuesheetEditor/Shared/TrackList/TrackList.razor @@ -118,6 +118,7 @@ along with Foobar. If not, see @code { + //TODO: Add handlers for multiple moves HashSet _selectedTracks = new(); MudMenu? _trackContextMenu; Track? _contextMenuTrack; diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.de.resx b/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.de.resx index 3b992c4d..c9deb962 100644 --- a/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.de.resx +++ b/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.de.resx @@ -144,7 +144,13 @@ Ausgewählte Titel nach unten bewegen + + Ausgewählte Titel mehrere Positionen nach unten bewegen + Ausgewählte Titel nach oben bewegen + + Ausgewählte Titel mehrere Positionen nach oben bewegen + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.razor b/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.razor index e8255a42..4491b0c6 100644 --- a/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.razor +++ b/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.razor @@ -60,18 +60,30 @@ along with Foobar. If not, see @_localizer["Move selected tracks up"] + @_localizer["Move selected tracks up serveral positions"] @_localizer["Move selected tracks down"] + @_localizer["Move selected tracks down serveral positions"] - + + + + - + + @_localizer["Move selected tracks up serveral positions"] + + + + - + + @_localizer["Move selected tracks down serveral positions"] + @@ -115,12 +127,18 @@ along with Foobar. If not, see [Parameter] public EventCallback MoveTracksUpClicked { get; set; } + [Parameter] + public EventCallback MoveTracksUpSeveralClicked { get; set; } + [Parameter] public Boolean MoveTracksUpDisabled { get; set; } [Parameter] public EventCallback MoveTracksDownClicked { get; set; } + [Parameter] + public EventCallback MoveTracksDownSeveralClicked { get; set; } + [Parameter] public Boolean MoveTracksDownDisabled { get; set; } diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.resx b/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.resx index d2db673d..4df21bbf 100644 --- a/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.resx +++ b/AudioCuesheetEditor/Shared/TrackList/TrackListControlButtons.resx @@ -144,7 +144,13 @@ Move selected tracks down + + Move selected tracks down serveral positions + Move selected tracks up + + Move selected tracks up serveral positions + \ No newline at end of file From fef7807e76cf6d0ea58f9cc3cb6349b7a78cee56 Mon Sep 17 00:00:00 2001 From: NeoCodeMatrix86 <40752681+NeoCoderMatrix86@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:02:06 +0200 Subject: [PATCH 02/10] Update TrackList.razor --- AudioCuesheetEditor/Shared/TrackList/TrackList.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackList.razor b/AudioCuesheetEditor/Shared/TrackList/TrackList.razor index 16767616..ecdd2fa8 100644 --- a/AudioCuesheetEditor/Shared/TrackList/TrackList.razor +++ b/AudioCuesheetEditor/Shared/TrackList/TrackList.razor @@ -39,7 +39,7 @@ along with Foobar. If not, see DeleteSelectedTracksClicked="DeleteSelectedTracksClicked" DeleteSelectedTracksDisabled="_selectedTracks.Count == 0" DeleteAllTracksClicked="OnDeleteAllTracksClicked" DeleteAllTracksDisabled="Audiofile?.Tracks.Any() == false" MoveTracksUpDisabled="!_cuesheetManager.IsMoveUpPossible(_selectedTracks)" MoveTracksUpClicked="() => _cuesheetManager.MoveUp(_selectedTracks)" - MoveTracksDownDisabled="!_cuesheetManager.IsMoveDownPossible(_selectedTracks)" MoveTracksDownClicked="(MoveTracksDownClicked) => _cuesheetManager.MoveDown(_selectedTracks)" + MoveTracksDownDisabled="!_cuesheetManager.IsMoveDownPossible(_selectedTracks)" MoveTracksDownClicked="() => _cuesheetManager.MoveDown(_selectedTracks)" CopySelectedTracksDisabled="_selectedTracks.Count != 1" CopySelectedTracksClicked="() => CopyTrackClicked()" FixedHeader="_fixedTableHeader" FixedHeaderClicked="() => _fixedTableHeader = !_fixedTableHeader" /> From 1ae00104d1e2324e00d556ad44d419dcbf6a4f54 Mon Sep 17 00:00:00 2001 From: NeoCodeMatrix86 <40752681+NeoCoderMatrix86@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:13:26 +0200 Subject: [PATCH 03/10] Update TrackList.razor --- .../Shared/TrackList/TrackList.razor | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackList.razor b/AudioCuesheetEditor/Shared/TrackList/TrackList.razor index ecdd2fa8..274e053a 100644 --- a/AudioCuesheetEditor/Shared/TrackList/TrackList.razor +++ b/AudioCuesheetEditor/Shared/TrackList/TrackList.razor @@ -37,7 +37,7 @@ along with Foobar. If not, see
-
break; } @@ -287,4 +288,13 @@ along with Foobar. If not, see await _form.ValidateAsync(); } } + + async Task MoveTracksUpSeveralClicked() + { + var result = await _dialogManager.ShowInputDialogAsync(@_localizer["Move tracks up"], @_localizer["Number of positions to move up"], @_localizer["Enter positions to move up"], 1); + if (result.IsSuccess) + { + _cuesheetManager.MoveUp(_selectedTracks, result.Value); + } + } } \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackList.resx b/AudioCuesheetEditor/Shared/TrackList/TrackList.resx index 3501160a..49046fbc 100644 --- a/AudioCuesheetEditor/Shared/TrackList/TrackList.resx +++ b/AudioCuesheetEditor/Shared/TrackList/TrackList.resx @@ -159,4 +159,13 @@ Link this track to previous track + + Move tracks up + + + Number of positions to move up + + + Enter positions to move up + \ No newline at end of file From 46cbab4c10d689f4606d550d43878ff9f4f5974e Mon Sep 17 00:00:00 2001 From: NeoCodeMatrix86 <40752681+NeoCoderMatrix86@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:43:35 +0200 Subject: [PATCH 09/10] move tracks down several positions --- .../AudioCuesheet/CuesheetManagerTests.cs | 72 +++++++++++++++++++ .../Services/AudioCuesheet/CuesheetManager.cs | 51 +++++++------ .../AudioCuesheet/ICuesheetManager.cs | 3 +- .../Shared/TrackList/TrackList.de.resx | 9 +++ .../Shared/TrackList/TrackList.razor | 13 +++- .../Shared/TrackList/TrackList.resx | 9 +++ 6 files changed, 133 insertions(+), 24 deletions(-) diff --git a/AudioCuesheetEditor.Tests/Services/AudioCuesheet/CuesheetManagerTests.cs b/AudioCuesheetEditor.Tests/Services/AudioCuesheet/CuesheetManagerTests.cs index 55029281..dea190de 100644 --- a/AudioCuesheetEditor.Tests/Services/AudioCuesheet/CuesheetManagerTests.cs +++ b/AudioCuesheetEditor.Tests/Services/AudioCuesheet/CuesheetManagerTests.cs @@ -1129,6 +1129,78 @@ public void MoveDown_TracksBelow_ReturnsSuccess() _traceChangeManager.VerifySet(x => x.BulkEdit = false, Times.Once); } + [TestMethod] + public void MoveDown_Move2Positions_ReturnsSuccess() + { + // Arrange + var track1End = new TimeSpan(0, 3, 12); + var track1 = new Track() + { + Position = 1, + Begin = TimeSpan.Zero, + End = track1End, + IsLinkedToPreviousTrack = true + }; + var track2End = new TimeSpan(0, 7, 34); + var track2 = new Track() + { + Position = 2, + Begin = track1End, + End = track2End, + IsLinkedToPreviousTrack = true + }; + var track3End = new TimeSpan(0, 10, 4); + var track3 = new Track() + { + Position = 3, + Begin = track2End, + End = track3End, + IsLinkedToPreviousTrack = true + }; + var track4 = new Track() + { + Position = 4, + Begin = track3End, + End = new TimeSpan(0, 13, 45), + IsLinkedToPreviousTrack = true + }; + var cuesheet = new Cuesheet() + { + Audiofiles = [ + new() { + Tracks = [track4, track3, track2, track1] + } + ] + }; + track1.Cuesheet = cuesheet; + track2.Cuesheet = cuesheet; + track3.Cuesheet = cuesheet; + track4.Cuesheet = cuesheet; + track1.Audiofile = cuesheet.Audiofiles.First(); + track2.Audiofile = cuesheet.Audiofiles.First(); + track3.Audiofile = cuesheet.Audiofiles.First(); + track4.Audiofile = cuesheet.Audiofiles.First(); + _sessionStateContainer.Setup(x => x.ActiveCuesheet).Returns(cuesheet); + // Act + var result = _cuesheetManager.MoveDown([track2, track1], 2); + // Assert + Assert.IsTrue(result.IsSuccess); + Assert.AreEqual((ushort?)1, track3.Position); + Assert.AreEqual(TimeSpan.Zero, track3.Begin); + Assert.AreEqual(track1End, track3.End); + Assert.AreEqual((ushort?)2, track4.Position); + Assert.AreEqual(track1End, track4.Begin); + Assert.AreEqual(track2End, track4.End); + Assert.AreEqual((ushort?)3, track1.Position); + Assert.AreEqual(track2End, track1.Begin); + Assert.AreEqual(track3End, track1.End); + Assert.AreEqual((ushort?)4, track2.Position); + Assert.AreEqual(track3End, track2.Begin); + _traceChangeManager.Verify(x => x.AddChange(It.Is(y => y.TraceableObject == cuesheet.Audiofiles.First() && y.TraceableChange.PropertyName == nameof(Audiofile.Tracks))), Times.Never); + _traceChangeManager.VerifySet(x => x.BulkEdit = true, Times.Once); + _traceChangeManager.VerifySet(x => x.BulkEdit = false, Times.Once); + } + [TestMethod] public void MoveDown_NoTracksBelow_ReturnsFailure() { diff --git a/AudioCuesheetEditor/Services/AudioCuesheet/CuesheetManager.cs b/AudioCuesheetEditor/Services/AudioCuesheet/CuesheetManager.cs index 7f9156a0..6333479a 100644 --- a/AudioCuesheetEditor/Services/AudioCuesheet/CuesheetManager.cs +++ b/AudioCuesheetEditor/Services/AudioCuesheet/CuesheetManager.cs @@ -225,40 +225,49 @@ public Result MoveUp(HashSet selectedAudiofiles) } /// - public Result MoveDown(HashSet selectedTracks) + public Result MoveDown(HashSet selectedTracks, uint positions = 1) { if (IsMoveDownPossible(selectedTracks) == false) { return Result.Failure(new Error(ErrorType.NotPossible, "Moving tracks down is not possible!")); } _traceChangeManager.BulkEdit = true; - var cuesheet = _sessionStateContainer.ActiveCuesheet; - foreach (var selectedTrack in selectedTracks.OrderByDescending(x => x.Position)) + while (positions > 0) { - var nextTrack = cuesheet?.Audiofiles.SelectMany(x => x.Tracks).FirstOrDefault(x => x.Position == selectedTrack.Position + 1); - if (nextTrack?.Audiofile != null && nextTrack.Audiofile != selectedTrack.Audiofile) + if (IsMoveDownPossible(selectedTracks) == false) { - SwitchAudiofile(selectedTrack, nextTrack.Audiofile); + positions = 0; + break; } - else + var cuesheet = _sessionStateContainer.ActiveCuesheet; + foreach (var selectedTrack in selectedTracks.OrderByDescending(x => x.Position)) { - var newBegin = nextTrack?.Begin; - var newEnd = nextTrack?.End; - if (nextTrack != null) + var nextTrack = cuesheet?.Audiofiles.SelectMany(x => x.Tracks).FirstOrDefault(x => x.Position == selectedTrack.Position + 1); + if (nextTrack?.Audiofile != null && nextTrack.Audiofile != selectedTrack.Audiofile) { - _trackManager.SetProperty(nextTrack, x => x.Position, selectedTrack.Position); - _trackManager.SetProperty(nextTrack, x => x.Begin, selectedTrack.Begin); - _trackManager.SetProperty(nextTrack, x => x.End, selectedTrack.End); + SwitchAudiofile(selectedTrack, nextTrack.Audiofile); + } + else + { + var newBegin = nextTrack?.Begin; + var newEnd = nextTrack?.End; + if (nextTrack != null) + { + _trackManager.SetProperty(nextTrack, x => x.Position, selectedTrack.Position); + _trackManager.SetProperty(nextTrack, x => x.Begin, selectedTrack.Begin); + _trackManager.SetProperty(nextTrack, x => x.End, selectedTrack.End); + } + _trackManager.SetProperty(selectedTrack, x => x.Position, (ushort?)(selectedTrack.Position + 1)); + _trackManager.SetProperty(selectedTrack, x => x.Begin, newBegin); + _trackManager.SetProperty(selectedTrack, x => x.End, newEnd); } - _trackManager.SetProperty(selectedTrack, x => x.Position, (ushort?)(selectedTrack.Position + 1)); - _trackManager.SetProperty(selectedTrack, x => x.Begin, newBegin); - _trackManager.SetProperty(selectedTrack, x => x.End, newEnd); } - } - foreach (var audiofile in cuesheet!.Audiofiles) - { - var orderedTracks = audiofile.Tracks.OrderBy(x => x.Position).ToList(); - _audiofileManager.SetProperty(audiofile, x => x.Tracks, orderedTracks); + foreach (var audiofile in cuesheet!.Audiofiles) + { + var orderedTracks = audiofile.Tracks.OrderBy(x => x.Position).ToList(); + _audiofileManager.SetProperty(audiofile, x => x.Tracks, orderedTracks); + } + positions--; } _traceChangeManager.BulkEdit = false; return Result.Success(); diff --git a/AudioCuesheetEditor/Services/AudioCuesheet/ICuesheetManager.cs b/AudioCuesheetEditor/Services/AudioCuesheet/ICuesheetManager.cs index 74548307..66631da8 100644 --- a/AudioCuesheetEditor/Services/AudioCuesheet/ICuesheetManager.cs +++ b/AudioCuesheetEditor/Services/AudioCuesheet/ICuesheetManager.cs @@ -91,8 +91,9 @@ public interface ICuesheetManager /// Moves selected tracks down /// /// + /// /// - Result MoveDown(HashSet selectedTracks); + Result MoveDown(HashSet selectedTracks, uint positions = 1); /// /// Moves selected audiofiles down /// diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackList.de.resx b/AudioCuesheetEditor/Shared/TrackList/TrackList.de.resx index 90e2561b..40d84338 100644 --- a/AudioCuesheetEditor/Shared/TrackList/TrackList.de.resx +++ b/AudioCuesheetEditor/Shared/TrackList/TrackList.de.resx @@ -168,4 +168,13 @@ Anzahl der Positionen angeben + + Titel nach unten bewegen + + + Anzahl der Positionen die nach unten bewegt werden soll + + + Anzahl der Positionen angeben + \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackList.razor b/AudioCuesheetEditor/Shared/TrackList/TrackList.razor index 0de6fb50..ea5c8c95 100644 --- a/AudioCuesheetEditor/Shared/TrackList/TrackList.razor +++ b/AudioCuesheetEditor/Shared/TrackList/TrackList.razor @@ -42,7 +42,8 @@ along with Foobar. If not, see MoveTracksUpDisabled="!_cuesheetManager.IsMoveUpPossible(_selectedTracks)" MoveTracksUpClicked="() => _cuesheetManager.MoveUp(_selectedTracks)" MoveTracksDownDisabled="!_cuesheetManager.IsMoveDownPossible(_selectedTracks)" MoveTracksDownClicked="() => _cuesheetManager.MoveDown(_selectedTracks)" CopySelectedTracksDisabled="_selectedTracks.Count != 1" CopySelectedTracksClicked="() => CopyTrackClicked()" - FixedHeader="_fixedTableHeader" FixedHeaderClicked="() => _fixedTableHeader = !_fixedTableHeader" MoveTracksUpSeveralClicked="MoveTracksUpSeveralClicked" /> + FixedHeader="_fixedTableHeader" FixedHeaderClicked="() => _fixedTableHeader = !_fixedTableHeader" MoveTracksUpSeveralClicked="MoveTracksUpSeveralClicked" + MoveTracksDownSeveralClicked="MoveTracksDownSeveralClicked" /> break; } @@ -119,7 +120,6 @@ along with Foobar. If not, see @code { - //TODO: Add handlers for multiple moves HashSet _selectedTracks = new(); MudMenu? _trackContextMenu; Track? _contextMenuTrack; @@ -297,4 +297,13 @@ along with Foobar. If not, see _cuesheetManager.MoveUp(_selectedTracks, result.Value); } } + + async Task MoveTracksDownSeveralClicked() + { + var result = await _dialogManager.ShowInputDialogAsync(@_localizer["Move tracks down"], @_localizer["Number of positions to move down"], @_localizer["Enter positions to move down"], 1); + if (result.IsSuccess) + { + _cuesheetManager.MoveDown(_selectedTracks, result.Value); + } + } } \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackList.resx b/AudioCuesheetEditor/Shared/TrackList/TrackList.resx index 49046fbc..084dd685 100644 --- a/AudioCuesheetEditor/Shared/TrackList/TrackList.resx +++ b/AudioCuesheetEditor/Shared/TrackList/TrackList.resx @@ -168,4 +168,13 @@ Enter positions to move up + + Move tracks down + + + Number of positions to move down + + + Enter positions to move down + \ No newline at end of file From a68f4db06d3a8f7dff7c39348926690a594e96fb Mon Sep 17 00:00:00 2001 From: NeoCodeMatrix86 <40752681+NeoCoderMatrix86@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:14:17 +0200 Subject: [PATCH 10/10] fix trace redo bug --- .../Services/UI/TraceChangeManagerTests.cs | 35 +++++++++++++++++++ .../Services/UI/TraceChangeManager.cs | 1 + AudioCuesheetEditor/Shared/AppBar.razor | 9 +++-- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/AudioCuesheetEditor.Tests/Services/UI/TraceChangeManagerTests.cs b/AudioCuesheetEditor.Tests/Services/UI/TraceChangeManagerTests.cs index 06613a8b..f1eeef6f 100644 --- a/AudioCuesheetEditor.Tests/Services/UI/TraceChangeManagerTests.cs +++ b/AudioCuesheetEditor.Tests/Services/UI/TraceChangeManagerTests.cs @@ -19,6 +19,7 @@ using AudioCuesheetEditor.Tests.Utility; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; +using System.Collections.Generic; using System.Linq; namespace AudioCuesheetEditor.Tests.Services.UI @@ -112,6 +113,40 @@ public void BulkEdit_AlreadyEnabledBulkEdit_DoesntDropPreviousEdits() Assert.IsTrue(_traceChangeManager.CanRedo); } + + [TestMethod()] + public void BulkEditFalse_AfterBulkEditAndUndo_ResetsRedoStack() + { + // Arrange + var audiofile = new Audiofile(); + var track1 = new Track(); + var track2 = new Track(); + var track3 = new Track(); + _traceChangeManager.BulkEdit = true; + _traceChangeManager.AddChange(new(audiofile, new(new List() { track1 }, nameof(Audiofile.Tracks)))); + track1.Audiofile = audiofile; + _traceChangeManager.BulkEdit = false; + _traceChangeManager.BulkEdit = true; + _traceChangeManager.AddChange(new(audiofile, new(new List() { track1, track2 }, nameof(Audiofile.Tracks)))); + track1.Audiofile = audiofile; + track2.Audiofile = audiofile; + _traceChangeManager.BulkEdit = false; + _traceChangeManager.Undo(); + _traceChangeManager.BulkEdit = true; + _traceChangeManager.AddChange(new(audiofile, new(new List() { track1, track2, track3 }, nameof(Audiofile.Tracks)))); + track1.Audiofile = audiofile; + track2.Audiofile = audiofile; + track3.Audiofile = audiofile; + _traceChangeManager.BulkEdit = false; + _traceChangeManager.BulkEdit = true; + _traceChangeManager.AddChange(new(track2, new("Testartist", nameof(Track.Artist)))); + // Act + _traceChangeManager.BulkEdit = false; + // Assert + Assert.IsTrue(_traceChangeManager.CanUndo); + Assert.IsFalse(_traceChangeManager.CanRedo); + } + [TestMethod()] public void Undo_SingleChange_UndoesChange() { diff --git a/AudioCuesheetEditor/Services/UI/TraceChangeManager.cs b/AudioCuesheetEditor/Services/UI/TraceChangeManager.cs index 79a53ef1..192c3866 100644 --- a/AudioCuesheetEditor/Services/UI/TraceChangeManager.cs +++ b/AudioCuesheetEditor/Services/UI/TraceChangeManager.cs @@ -175,6 +175,7 @@ public bool BulkEdit if (_bulkEditTracedChanges?.Count > 0) { _undoStack.Add(new TracedChanges(_bulkEditTracedChanges)); + _redoStack.Clear(); TracedObjectHistoryChanged?.Invoke(this, EventArgs.Empty); } _bulkEditTracedChanges = null; diff --git a/AudioCuesheetEditor/Shared/AppBar.razor b/AudioCuesheetEditor/Shared/AppBar.razor index fac86015..4fad7e10 100644 --- a/AudioCuesheetEditor/Shared/AppBar.razor +++ b/AudioCuesheetEditor/Shared/AppBar.razor @@ -37,10 +37,10 @@ along with Foobar. If not, see { - + - + } @@ -90,7 +90,6 @@ along with Foobar. If not, see @code { - //TODO: Redo not reset when adding a new change (like adding another track) HotKeysContext? _hotKeysContext; MudFileUpload? _mudFileUpload; @@ -98,8 +97,8 @@ along with Foobar. If not, see { base.OnInitialized(); _hotKeysContext = _hotKeys.CreateContext() - .Add(ModKey.Ctrl, Key.z, () => TraceChangeManager.Undo()) - .Add(ModKey.Ctrl, Key.y, () => TraceChangeManager.Redo()) + .Add(ModKey.Ctrl, Key.z, TraceChangeManager.Undo) + .Add(ModKey.Ctrl, Key.y, TraceChangeManager.Redo) .Add(ModKey.Ctrl, Key.s, DownloadProjectfileClicked) .Add(ModKey.Ctrl, Key.e, DownloadExportClicked) .Add(ModKey.Ctrl, Key.u, DownloadCuesheetClicked)