diff --git a/KaddaOK.AvaloniaApp/App.axaml b/KaddaOK.AvaloniaApp/App.axaml index 38115a2..f484f1a 100644 --- a/KaddaOK.AvaloniaApp/App.axaml +++ b/KaddaOK.AvaloniaApp/App.axaml @@ -127,6 +127,7 @@ + \ No newline at end of file diff --git a/KaddaOK.AvaloniaApp/Controls/Dialogs/EditLineTimingDialog.axaml b/KaddaOK.AvaloniaApp/Controls/Dialogs/EditLineTimingDialog.axaml index 8b4394d..0930a9b 100644 --- a/KaddaOK.AvaloniaApp/Controls/Dialogs/EditLineTimingDialog.axaml +++ b/KaddaOK.AvaloniaApp/Controls/Dialogs/EditLineTimingDialog.axaml @@ -11,7 +11,10 @@ xmlns:models="clr-namespace:KaddaOK.AvaloniaApp.Models" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="KaddaOK.AvaloniaApp.Controls.Dialogs.EditLineTimingDialog" - x:DataType="models:EditingLine"> + x:DataType="models:EditingLine" + Focusable="True" + AttachedToVisualTree="UserControl_AttachedToVisualTree" + KeyDown="UserControl_KeyDown"> + Text="{Binding Text, Mode=OneWay}" + KeyDown="TextBox_KeyDown"/> (); + } + + private void TextBox_KeyDown(object? sender, Avalonia.Input.KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + dialogHost?.CloseDialogCommand.Execute(((TextBox)sender).Text); + } + + if (e.Key == Key.Escape) + { + dialogHost?.CloseDialogCommand.Execute(null); + } + } } } diff --git a/KaddaOK.AvaloniaApp/ObjectEqualityBooleanConverter.cs b/KaddaOK.AvaloniaApp/ObjectEqualityBooleanConverter.cs new file mode 100644 index 0000000..92393be --- /dev/null +++ b/KaddaOK.AvaloniaApp/ObjectEqualityBooleanConverter.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Avalonia.Data.Converters; + +namespace KaddaOK.AvaloniaApp +{ + public class ObjectEqualityBooleanConverter : IMultiValueConverter + { + public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) + { + if (values.Count != 2 || values.Any(v => v == null || v.ToString() == "(unset)")) return null; + + return values[0] == values[1]; + } + } +} diff --git a/KaddaOK.AvaloniaApp/ObservableStack.cs b/KaddaOK.AvaloniaApp/ObservableStack.cs index 136ca92..afaa414 100644 --- a/KaddaOK.AvaloniaApp/ObservableStack.cs +++ b/KaddaOK.AvaloniaApp/ObservableStack.cs @@ -1,77 +1,37 @@ using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; +using System.Linq; namespace KaddaOK.AvaloniaApp { - /// - /// Per https://stackoverflow.com/a/56177896/ - /// - public class ObservableStack : Stack, INotifyCollectionChanged, INotifyPropertyChanged + public class ObservableStack : ObservableCollection { - #region Constructors + public T? Peek => Items.LastOrDefault(); - public ObservableStack() : base() { } - - public ObservableStack(IEnumerable collection) : base(collection) { } - - public ObservableStack(int capacity) : base(capacity) { } - - #endregion - - #region Overrides - - public new virtual T? Pop() + public T? Pop() { - var item = base.Pop(); - OnCollectionChanged(NotifyCollectionChangedAction.Remove, item); - - return item; - } - - public new virtual void Push(T? item) - { - if (item != null) + var popIndex = Items.Count - 1; + var itemToPop = Items[popIndex]; + if (itemToPop != null) { - base.Push(item); - OnCollectionChanged(NotifyCollectionChangedAction.Add, item); + RemoveAt(popIndex); } - } - public new virtual void Clear() - { - base.Clear(); - OnCollectionChanged(NotifyCollectionChangedAction.Reset, default); + return itemToPop; } - #endregion - - #region CollectionChanged - - public event NotifyCollectionChangedEventHandler? CollectionChanged; - - protected virtual void OnCollectionChanged(NotifyCollectionChangedAction action, T? item) + public void Push(T? item) { - CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs( - action - , item - , item == null ? -1 : 0) - ); - - OnPropertyChanged(nameof(Count)); + Add(item); } - #endregion - - #region PropertyChanged - - public event PropertyChangedEventHandler? PropertyChanged; - - protected virtual void OnPropertyChanged(string proertyName) + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(proertyName)); + base.OnCollectionChanged(e); + OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count))); + OnPropertyChanged(new PropertyChangedEventArgs(nameof(Peek))); } - - #endregion } } diff --git a/KaddaOK.AvaloniaApp/ViewModels/DesignTime/DesignTimeEditLinesViewModel.cs b/KaddaOK.AvaloniaApp/ViewModels/DesignTime/DesignTimeEditLinesViewModel.cs index 4b78983..b231163 100644 --- a/KaddaOK.AvaloniaApp/ViewModels/DesignTime/DesignTimeEditLinesViewModel.cs +++ b/KaddaOK.AvaloniaApp/ViewModels/DesignTime/DesignTimeEditLinesViewModel.cs @@ -14,6 +14,10 @@ public class DesignTimeEditLinesViewModel : EditLinesViewModel { public DesignTimeEditLinesViewModel() : base(DesignTimeKaraokeProcess.Get(), new LineSplitter(), new WordMerger(), new MinMaxFloatWaveStreamSampler()) { + UndoStack.Add(new ChosenLinesAction("[]", "did this before this view")); + UndoStack.Add(new ChosenLinesAction("[]", "and then I did this")); + RedoStack.Add(new ChosenLinesAction("[]", "already undid this first")); + RedoStack.Add(new ChosenLinesAction("[]", "then undid this")); } } } diff --git a/KaddaOK.AvaloniaApp/ViewModels/EditLinesViewModel.cs b/KaddaOK.AvaloniaApp/ViewModels/EditLinesViewModel.cs index cdaf8ec..9443932 100644 --- a/KaddaOK.AvaloniaApp/ViewModels/EditLinesViewModel.cs +++ b/KaddaOK.AvaloniaApp/ViewModels/EditLinesViewModel.cs @@ -5,7 +5,6 @@ using Newtonsoft.Json; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading; @@ -17,15 +16,39 @@ using NAudio.Wave.SampleProviders; using Avalonia.Controls; using KaddaOK.AvaloniaApp.Controls; +using KaddaOK.AvaloniaApp.Views; +using Avalonia.VisualTree; namespace KaddaOK.AvaloniaApp.ViewModels { + public class ChosenLinesAction : ObservableBase + { + private string? serializedLines; + public string? SerializedLines + { + get => serializedLines; + set => SetProperty(ref serializedLines, value); + } + private string? changeLabel; + public string? ChangeLabel + { + get => changeLabel; + set => SetProperty(ref changeLabel, value); + } + + public ChosenLinesAction(string? serializedLines, string? changeLabel) + { + SerializedLines = serializedLines; + ChangeLabel = changeLabel; + } + } public partial class EditLinesViewModel : TickableBase { private ILineSplitter Splitter { get; } private IWordMerger WordMerger { get; } private CancellationTokenSource AudioPlayingSource { get; } private IMinMaxFloatWaveStreamSampler Sampler { get; } + public EditLinesView EditLinesView { get; set; } // TODO: are you sure you wanna do this? It's paradigm-breakingly bad practice... public EditLinesViewModel(KaraokeProcess karaokeProcess, ILineSplitter splitter, IWordMerger merger, IMinMaxFloatWaveStreamSampler sampler) : base(karaokeProcess) { FullLengthVocalsDraw = new WaveformDraw @@ -36,8 +59,8 @@ public EditLinesViewModel(KaraokeProcess karaokeProcess, ILineSplitter splitter, Splitter = splitter; WordMerger = merger; Sampler = sampler; - UndoStack = new ObservableStack(); - RedoStack = new ObservableStack(); + UndoStack = new ObservableStack(); + RedoStack = new ObservableStack(); if ((CurrentProcess!.DetectedLinePossibilities?.Any(lp => lp.HasSelected) ?? false) && (!CurrentProcess.ChosenLines?.Any() ?? false)) { @@ -117,6 +140,13 @@ public EditingLine? LineBeingEdited set => SetProperty(ref lineBeingEdited, value); } + private LyricWord? cursorWord; + public LyricWord? CursorWord + { + get => cursorWord; + set => SetProperty(ref cursorWord, value); + } + private bool isRecording; public bool IsRecording { @@ -124,15 +154,15 @@ public bool IsRecording set => SetProperty(ref isRecording, value); } - private ObservableStack undoStack = null!; - public ObservableStack UndoStack + private ObservableStack undoStack = null!; + public ObservableStack UndoStack { get => undoStack; set => SetProperty(ref undoStack, value); } - private ObservableStack redoStack = null!; - public ObservableStack RedoStack + private ObservableStack redoStack = null!; + public ObservableStack RedoStack { get => redoStack; set => SetProperty(ref redoStack, value); @@ -181,7 +211,7 @@ private void MergeWithNext(object? parameter) } } - private void AddUndoSnapshot() + private void AddUndoSnapshot(string changeLabel, bool isRedo = false) { if (CurrentProcess?.ChosenLines != null) { @@ -190,17 +220,17 @@ private void AddUndoSnapshot() { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }); - UndoStack.Push(currentChosenLinesString); + UndoStack.Push(new ChosenLinesAction(currentChosenLinesString, changeLabel)); // clear the redo stack because now we've done something different - if (RedoStack.Any()) + if (RedoStack.Any() && !isRedo) { RedoStack.Clear(); } } } - private void AddRedoSnapshot() + private void AddRedoSnapshot(string changeLabel) { if (CurrentProcess?.ChosenLines != null) { @@ -209,62 +239,123 @@ private void AddRedoSnapshot() { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }); - RedoStack.Push(currentChosenLinesString); + RedoStack.Push(new ChosenLinesAction(currentChosenLinesString, changeLabel)); } } private void NewLineAt(LyricWord newLineHere, bool isBefore) { - AddUndoSnapshot(); + AddUndoSnapshot($"Split line {(isBefore ? "before" : "after")} \"{newLineHere.Text}\""); Splitter.SplitLineAt(CurrentProcess?.ChosenLines, newLineHere, isBefore); + EditLinesView.Focus(); } private void MergeWord(LyricWord mergeHere, bool isBefore) { - AddUndoSnapshot(); - WordMerger.MergeWord(CurrentProcess?.ChosenLines, mergeHere, isBefore); + AddUndoSnapshot($"Merge \"{mergeHere.Text}\" with {(isBefore ? "previous" : "next")} syllable"); + var resultingWord = WordMerger.MergeWord(CurrentProcess?.ChosenLines, mergeHere, isBefore); + if (resultingWord != null) + { + CursorWord = resultingWord; + EditLinesView.Focus(); + } } [RelayCommand] private void Undo(object? parameter) { - var restoreChosenLinesString = UndoStack.Pop(); - if (restoreChosenLinesString != null) + var lastCommand = UndoStack.Pop(); + if (lastCommand?.SerializedLines != null) { - var restoredChosenLines = JsonConvert.DeserializeObject>(restoreChosenLinesString); + var restoredChosenLines = JsonConvert.DeserializeObject>(lastCommand.SerializedLines); if (restoredChosenLines != null) { - AddRedoSnapshot(); + AddRedoSnapshot(lastCommand.ChangeLabel); CurrentProcess!.ChosenLines = new ObservableCollection(restoredChosenLines); } } + + // it seems to lose keyboard focus afterwards for some reason... + EditLinesView.Focus(); + // and the word cursor no longer works, unfortunately... + CursorWord = null; } [RelayCommand] private void Redo(object? parameter) { - var redoChosenLinesString = RedoStack.Pop(); - if (redoChosenLinesString != null) + var nextCommand = RedoStack.Pop(); + if (nextCommand.SerializedLines != null) { - var redoChosenLines = JsonConvert.DeserializeObject>(redoChosenLinesString); + var redoChosenLines = JsonConvert.DeserializeObject>(nextCommand.SerializedLines); if (redoChosenLines != null) { - AddUndoSnapshot(); + AddUndoSnapshot(nextCommand.ChangeLabel, true); CurrentProcess!.ChosenLines = new ObservableCollection(redoChosenLines); } } + + // it seems to lose keyboard focus afterwards for some reason... + EditLinesView.Focus(); + // and the word cursor no longer works, unfortunately... + CursorWord = null; } [RelayCommand] private void DeleteWord(object? parameter) { - if (parameter is LyricWord newLineHere) + if (parameter is LyricWord deleteThis) { - AddUndoSnapshot(); - Splitter.DeleteWord(CurrentProcess!.ChosenLines!, newLineHere); + AddUndoSnapshot($"Delete syllable \"{deleteThis.Text}\""); + + var deletingFromLine = CurrentProcess!.ChosenLines!.SingleOrDefault(l => l.Words != null && l.Words.Contains(deleteThis)); + var deletedWordIndex = deletingFromLine.Words.IndexOf(deleteThis); + + Splitter.DeleteWord(CurrentProcess!.ChosenLines!, deleteThis); + + // find new focus word + var newFocusedWord = deletingFromLine.Words.Count > deletedWordIndex + ? deletingFromLine.Words[deletedWordIndex] + : deletingFromLine.Words.LastOrDefault(); + + CursorWord = newFocusedWord; + + EditLinesView.Focus(); } } + [RelayCommand] + private void DeleteEntireLine(object? parameter) + { + LyricLine deletingLine; + if (parameter is LyricWord wordInMovingLine) + { + deletingLine = CurrentProcess!.ChosenLines!.SingleOrDefault(l => l.Words != null && l.Words.Contains(wordInMovingLine)); + } + else if (parameter is LyricLine line) + { + deletingLine = line; + } + else + { + return; + } + AddUndoSnapshot($"Delete line \"{deletingLine.Text}\""); + var deletedLineIndex = CurrentProcess!.ChosenLines.IndexOf(deletingLine); + CurrentProcess!.ChosenLines.Remove(deletingLine); + + // find new focus word + var newFocusedLine = CurrentProcess.ChosenLines.Count > deletedLineIndex + ? CurrentProcess.ChosenLines[deletedLineIndex] + : CurrentProcess.ChosenLines.LastOrDefault(); + if (newFocusedLine?.Words?.Any() ?? false) + { + CursorWord = newFocusedLine.Words.First(); + } + + EditLinesView.Focus(); + } + [RelayCommand] private async Task EditWordText(object? parameter) { @@ -273,6 +364,7 @@ private async Task EditWordText(object? parameter) EditingTextOfWord = editThisWord; if (await DialogHost.Show(this, "EditLinesViewDialogHost") is string newText) { + AddUndoSnapshot($"Change syllable \"{editThisWord.Text}\" to \"{newText}\""); var lineNeedsTiming = ApplyEditWordText(newText); if (lineNeedsTiming != null) { @@ -319,6 +411,7 @@ public void ApplyLineTimingEdit(object? parameter) { if (LineBeingEdited != null) { + AddUndoSnapshot($"Edited timing for \"{LineBeingEdited.OriginalLine.Text}\""); LineBeingEdited.OriginalLine.Words = new ObservableCollection(LineBeingEdited.NewTiming); } } @@ -326,25 +419,37 @@ public void ApplyLineTimingEdit(object? parameter) [RelayCommand] public void MoveLineToPrevious(object? parameter) { - if (parameter is LyricLine movingLine) + LyricLine movingLine; + if (parameter is LyricWord wordInMovingLine) { - var movingLineIndex = CurrentProcess!.ChosenLines!.IndexOf(movingLine); - if (movingLineIndex > 0) - { - AddUndoSnapshot(); + movingLine = CurrentProcess!.ChosenLines!.SingleOrDefault(l => l.Words != null && l.Words.Contains(wordInMovingLine)); + } + else if (parameter is LyricLine line) + { + movingLine = line; + } + else + { + return; + } - var previousLine = CurrentProcess!.ChosenLines![movingLineIndex - 1]; - // add a space to the previous last word if it doesn't have one - var previousLastWord = previousLine.Words!.Last(); - if (!previousLastWord.Text?.EndsWith(" ") ?? false) - { - previousLastWord.Text += " "; - } - var newWords = previousLine.Words!.Union(movingLine.Words!); - previousLine.Words = new ObservableCollection(newWords); - CurrentProcess.ChosenLines.Remove(movingLine); + var movingLineIndex = CurrentProcess!.ChosenLines!.IndexOf(movingLine); + if (movingLineIndex > 0) + { + AddUndoSnapshot($"Moved \"{movingLine.Text}\" onto previous line"); + + var previousLine = CurrentProcess!.ChosenLines![movingLineIndex - 1]; + // add a space to the previous last word if it doesn't have one + var previousLastWord = previousLine.Words!.Last(); + if (!previousLastWord.Text?.EndsWith(" ") ?? false) + { + previousLastWord.Text += " "; } + var newWords = previousLine.Words!.Union(movingLine.Words!); + previousLine.Words = new ObservableCollection(newWords); + CurrentProcess.ChosenLines.Remove(movingLine); } + EditLinesView.Focus(); } [RelayCommand] @@ -668,6 +773,7 @@ private async Task AddNewLine(object? parameter) var listOfNewWords = GetLyricWordsAcrossTime(enteredText, startTime, endTime); newLine.Words = new ObservableCollection(listOfNewWords); + AddUndoSnapshot($"Added new line \"{newLine.Text}\""); CurrentProcess.ChosenLines.Insert(addLineAfterIndex + 1, newLine); // go straight to the edit dialog for it @@ -705,84 +811,261 @@ private static List GetLyricWordsAcrossTime(string? enteredText, doub public void KeyPressed(KeyEventArgs keyArgs) { - if (keyArgs.Key == Key.Escape) + // TODO: this no longer needs be its own thing + HandleWordEditKeys(keyArgs); + } + + private void HandleWordEditKeys(KeyEventArgs keyArgs) + { + switch (keyArgs.Key) { - switch (modeBeingDragged) + case Key.Down: + case Key.Right: + case Key.Left: + case Key.Up: + SwitchWordSelection(keyArgs); + break; + case Key.A: + if (CursorWord != null) + { + NewLineAfter(CursorWord); + } + break; + case Key.S: + if (CursorWord != null) + { + NewLineBefore(CursorWord); + } + break; + case Key.W: + if (CursorWord != null) + { + MergeWithPrev(CursorWord); + } + break; + case Key.Q: + if (CursorWord != null) + { + MergeWithNext(CursorWord); + } + break; + case Key.E: + if (CursorWord != null) + { + EditWordText(CursorWord); + } + break; + case Key.D: + if (CursorWord != null) + { + DeleteWord(CursorWord); + } + break; + case Key.Delete: + if (CursorWord != null) + { + DeleteEntireLine(CursorWord); + } + break; + case Key.Z: + if (CursorWord != null) + { + MoveLineToPrevious(CursorWord); + } + break; + } + } + + private void SwitchWordSelection(KeyEventArgs keyArgs) + { + // this is just gonna change focus and let the handler in the view set it back... wacky I know, but it makes + // sense because of what we can and can't control around tabbing and such. + // Also, apparently the order in which it finds these isn't reliable, so we'll have to sort it by the order in + // memory... + var allWordButtonsByPhrases = EditLinesView.GetVisualDescendants().Where(x => x.Name == "PhraseWordsItemsControl") + .Select(s => new { - case WordDraggingMode.LeftSide: - wordBeingDragged!.StartSecond = OriginalValue ?? 0; + Buttons = s.GetVisualDescendants().Where(y => y.Name == "LyricWordButton").Select(y => (Button)y).ToList(), + SortOrder = CurrentProcess.ChosenLines.IndexOf(s.DataContext as LyricLine) + }).OrderBy(s => s.SortOrder) + .Select(s => s.Buttons.OrderBy(x => CurrentProcess.ChosenLines[s.SortOrder].Words.IndexOf(x.CommandParameter as LyricWord)).ToList()) + .Where(s => s.Count > 0) // I'm not sure why this even happened; virtualization maybe + .ToList(); + + + Button? destinationButton = null; + if (CursorWord == null) + { + switch (keyArgs.Key) + { + case Key.Down: + case Key.Right: + destinationButton = allWordButtonsByPhrases.FirstOrDefault()?.FirstOrDefault(); break; - case WordDraggingMode.RightSide: - wordBeingDragged!.EndSecond = OriginalValue ?? 0; + case Key.Left: + destinationButton = allWordButtonsByPhrases.FirstOrDefault()?.LastOrDefault(); + break; + case Key.Up: + destinationButton = allWordButtonsByPhrases.LastOrDefault()?.FirstOrDefault(); break; } - wordBeingDragged = null; - modeBeingDragged = WordDraggingMode.None; - keyArgs.Handled = true; } - - if (IsRecording) + else { - var positionOffsetSeconds = PlayAudio?.GetPositionTimeSpan().TotalSeconds; - if (positionOffsetSeconds == null) + var currentPhrase = allWordButtonsByPhrases.SingleOrDefault(phrase => phrase.Any(word => word.CommandParameter == CursorWord)); + if (currentPhrase != null) { - // TODO: this shouldn't happen; log it - return; - } + var currentPhraseIndex = allWordButtonsByPhrases.IndexOf(currentPhrase); + var currentWordButton = currentPhrase.FirstOrDefault(word => word.CommandParameter == CursorWord); + var currentWordIndex = currentPhrase.IndexOf(currentWordButton); + switch (keyArgs.Key) + { + case Key.Down: + if (allWordButtonsByPhrases.Count > currentPhraseIndex + 1) + { + var nextPhrase = allWordButtonsByPhrases[currentPhraseIndex + 1]; + var nextWordIndex = nextPhrase.Count > currentWordIndex + ? currentWordIndex + : nextPhrase.Count - 1; + destinationButton = nextPhrase[nextWordIndex]; + } - var positionTotalSeconds = Math.Round(positionOffsetSeconds.Value + CurrentLineOffsetSeconds ?? 0, 2); - switch (keyArgs.Key) - { - case Key.Right: - if (LineBeingEdited!.WordCurrentlyTiming == null) - { - if (LineBeingEdited!.WordTimingQueue!.Count == 0) + break; + case Key.Right: + if (currentWordIndex < currentPhrase.Count - 1) { - // TODO: this shouldn't happen; log it + destinationButton = currentPhrase[currentWordIndex + 1]; } - else + + break; + case Key.Left: + if (currentWordIndex > 0) { - // this is the first word; start it up - StartNextWord(positionTotalSeconds); + destinationButton = currentPhrase[currentWordIndex - 1]; } - } - else + + break; + case Key.Up: + if (currentPhraseIndex > 0) + { + var nextPhrase = allWordButtonsByPhrases[currentPhraseIndex - 1]; + var nextWordIndex = nextPhrase.Count > currentWordIndex + ? currentWordIndex + : nextPhrase.Count - 1; + destinationButton = nextPhrase[nextWordIndex]; + } + + break; + } + + } + } + + destinationButton?.BringIntoView(); + destinationButton?.Focus(); + + /*switch (keyArgs.Key) + { + case Key.Down: + if (CursorWord == null) + { + CursorWord = CurrentProcess.ChosenLines?.FirstOrDefault()?.Words?.FirstOrDefault(); + } + else + { + var originalLine = + CurrentProcess.ChosenLines?.SingleOrDefault(l => + l.Words != null && l.Words.Contains(CursorWord)); + if (originalLine?.Words != null) { - if (LineBeingEdited.WordCurrentlyTiming.IsRunning) + var originalLineIndex = CurrentProcess.ChosenLines.IndexOf(originalLine); + var originalWordIndex = originalLine.Words.IndexOf(CursorWord); + if (CurrentProcess.ChosenLines.Count > originalLineIndex + 1) { - // this word is going right into the next one, so end it here - StopWord(LineBeingEdited.WordCurrentlyTiming, positionTotalSeconds); + var nextLine = CurrentProcess.ChosenLines[originalLineIndex + 1]; + var nextWordIndex = nextLine.Words.Count > originalWordIndex + ? originalWordIndex + : nextLine.Words.Count - 1; + CursorWord = nextLine.Words[nextWordIndex]; } + } + } - // if there aren't any more words, stop running - if (LineBeingEdited!.WordTimingQueue!.Count == 0) + break; + case Key.Right: + if (CursorWord == null) + { + CursorWord = CurrentProcess.ChosenLines?.FirstOrDefault()?.Words?.FirstOrDefault(); + } + else + { + var originalLine = + CurrentProcess.ChosenLines?.SingleOrDefault(l => + l.Words != null && l.Words.Contains(CursorWord)); + if (originalLine?.Words != null) + { + var originalWordIndex = originalLine.Words.IndexOf(CursorWord); + if (originalWordIndex < originalLine.Words.Count - 1) { - Stop(null); + CursorWord = originalLine.Words[originalWordIndex + 1]; } - else + } + } + + break; + case Key.Left: + if (CursorWord == null) + { + CursorWord = CurrentProcess.ChosenLines?.FirstOrDefault()?.Words?.LastOrDefault(); + } + else + { + var originalLine = + CurrentProcess.ChosenLines?.SingleOrDefault(l => + l.Words != null && l.Words.Contains(CursorWord)); + if (originalLine?.Words != null) + { + var originalWordIndex = originalLine.Words.IndexOf(CursorWord); + if (originalWordIndex > 0) { - // whether the current word was running or not, the next one should start here - StartNextWord(positionTotalSeconds); + CursorWord = originalLine.Words[originalWordIndex - 1]; } } - break; - case Key.Down: - if (LineBeingEdited!.WordCurrentlyTiming?.IsRunning ?? false) + } + + break; + case Key.Up: + if (CursorWord == null) + { + CursorWord = CurrentProcess.ChosenLines?.LastOrDefault()?.Words?.FirstOrDefault(); + } + else + { + var originalLine = + CurrentProcess.ChosenLines?.SingleOrDefault(l => + l.Words != null && l.Words.Contains(CursorWord)); + if (originalLine?.Words != null) { - // just stop the current word - StopWord(LineBeingEdited.WordCurrentlyTiming, positionTotalSeconds); - // if there aren't any more words, stop running - if (LineBeingEdited!.WordTimingQueue!.Count == 0) + var originalLineIndex = CurrentProcess.ChosenLines.IndexOf(originalLine); + var originalWordIndex = originalLine.Words.IndexOf(CursorWord); + if (originalLineIndex > 0) { - Stop(null); + var nextLine = CurrentProcess.ChosenLines[originalLineIndex - 1]; + var nextWordIndex = nextLine.Words.Count > originalWordIndex + ? originalWordIndex + : nextLine.Words.Count - 1; + CursorWord = nextLine.Words[nextWordIndex]; } } - break; - } - } + } + + break; + default: + throw new InvalidOperationException($"No case for key {keyArgs.Key} in SwitchWordSelection"); + } */ } - private void StartNextWord([DisallowNull] double? positionTotalSeconds) + private void StartNextWord(double? positionTotalSeconds) { LineBeingEdited!.WordCurrentlyTiming = LineBeingEdited.WordTimingQueue!.Dequeue(); StartWord(LineBeingEdited.WordCurrentlyTiming!, positionTotalSeconds.Value); @@ -912,6 +1195,89 @@ public enum WordDraggingMode private double? dragWidth = null; private double? OriginalValue = null; + public void EditLineTimingDialogKeyDown(object? sender, KeyEventArgs keyArgs) + { + if (keyArgs.Key == Key.Escape) + { + switch (modeBeingDragged) + { + case WordDraggingMode.LeftSide: + wordBeingDragged!.StartSecond = OriginalValue ?? 0; + break; + case WordDraggingMode.RightSide: + wordBeingDragged!.EndSecond = OriginalValue ?? 0; + break; + } + wordBeingDragged = null; + modeBeingDragged = WordDraggingMode.None; + keyArgs.Handled = true; + } + + if (IsRecording) + { + var positionOffsetSeconds = PlayAudio?.GetPositionTimeSpan().TotalSeconds; + if (positionOffsetSeconds == null) + { + // TODO: this shouldn't happen; log it + return; + } + + var positionTotalSeconds = Math.Round(positionOffsetSeconds.Value + CurrentLineOffsetSeconds ?? 0, 2); + switch (keyArgs.Key) + { + case Key.Right: + if (LineBeingEdited!.WordCurrentlyTiming == null) + { + if (LineBeingEdited!.WordTimingQueue!.Count == 0) + { + // TODO: this shouldn't happen; log it + } + else + { + // this is the first word; start it up + StartNextWord(positionTotalSeconds); + } + } + else + { + if (LineBeingEdited.WordCurrentlyTiming.IsRunning) + { + // this word is going right into the next one, so end it here + StopWord(LineBeingEdited.WordCurrentlyTiming, positionTotalSeconds); + } + + // if there aren't any more words, stop running + if (LineBeingEdited!.WordTimingQueue!.Count == 0) + { + Stop(null); + } + else + { + // whether the current word was running or not, the next one should start here + StartNextWord(positionTotalSeconds); + } + } + + keyArgs.Handled = true; + break; + case Key.Down: + if (LineBeingEdited!.WordCurrentlyTiming?.IsRunning ?? false) + { + // just stop the current word + StopWord(LineBeingEdited.WordCurrentlyTiming, positionTotalSeconds); + // if there aren't any more words, stop running + if (LineBeingEdited!.WordTimingQueue!.Count == 0) + { + Stop(null); + } + + keyArgs.Handled = true; + } + break; + } + } + } + public void EditLineTimingDialogPointerPressed(object? sender, PointerPressedEventArgs args) { var control = sender as Control; diff --git a/KaddaOK.AvaloniaApp/Views/EditLinesView.axaml b/KaddaOK.AvaloniaApp/Views/EditLinesView.axaml index 11e602f..f8ca18d 100644 --- a/KaddaOK.AvaloniaApp/Views/EditLinesView.axaml +++ b/KaddaOK.AvaloniaApp/Views/EditLinesView.axaml @@ -17,7 +17,9 @@ d:DesignHeight="740" x:Class="KaddaOK.AvaloniaApp.Views.EditLinesView" x:DataType="vm:EditLinesViewModel" - KeyDown="EditLinesView_OnKeyDown"> + Focusable="True" + KeyDown="EditLinesView_OnKeyDown" + AttachedToVisualTree="EditLinesView_AttachedToVisualTree"> @@ -47,29 +49,46 @@ x:Key="WordButtonFlyout" x:DataType="library:LyricWord"> + + - - - - - + + + +