diff --git a/OpenUtau/Controls/NotePropertyExpression.axaml b/OpenUtau/Controls/NotePropertyExpression.axaml index 0d702dd6d..b24cbaf59 100644 --- a/OpenUtau/Controls/NotePropertyExpression.axaml +++ b/OpenUtau/Controls/NotePropertyExpression.axaml @@ -4,16 +4,22 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:OpenUtau.App.ViewModels" x:Class="OpenUtau.App.Controls.NotePropertyExpression"> - - + + + + + diff --git a/OpenUtau/Controls/NotePropertyExpression.axaml.cs b/OpenUtau/Controls/NotePropertyExpression.axaml.cs index 474de2adc..a279060e8 100644 --- a/OpenUtau/Controls/NotePropertyExpression.axaml.cs +++ b/OpenUtau/Controls/NotePropertyExpression.axaml.cs @@ -1,6 +1,7 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.VisualTree; using OpenUtau.App.ViewModels; using OpenUtau.Core; using Serilog; @@ -31,6 +32,23 @@ void OnTextBoxLostFocus(object? sender, RoutedEventArgs args) { SetNumericalExpressions(textBox.Text); } } + void OnFlagBoxLostFocus(object? sender, RoutedEventArgs args) { + Log.Debug("Note property textbox lost focus"); + if (sender is TextBox textBox && textBoxValue != textBox.Text) { + if (DataContext is NotePropertyExpViewModel viewModel) { + NotePropertiesViewModel.PanelControlPressed = true; + viewModel.SetFlagFromText(textBox.Text); + NotePropertiesViewModel.PanelControlPressed = false; + + if (!string.IsNullOrEmpty(viewModel.Warning)) { + var scrollViewer = this.FindAncestorOfType(); + if (scrollViewer != null) { + scrollViewer.ScrollToEnd(); + } + } + } + } + } // slider void SliderPointerPressed(object? sender, PointerPressedEventArgs args) { diff --git a/OpenUtau/Strings/Strings.axaml b/OpenUtau/Strings/Strings.axaml index d6787a479..a90af87ae 100644 --- a/OpenUtau/Strings/Strings.axaml +++ b/OpenUtau/Strings/Strings.axaml @@ -171,6 +171,7 @@ Do you want to continue by splitting at the nearest position after current playh Failed to open Failed to open location Project file is newer than software! Upgrade OpenUtau! + Failed to parse the flag. Please check the Expression settings: {0} Failed to render. Failed to run editing macro Failed to save diff --git a/OpenUtau/ViewModels/NotePropertiesViewModel.cs b/OpenUtau/ViewModels/NotePropertiesViewModel.cs index 15df14904..d78626377 100644 --- a/OpenUtau/ViewModels/NotePropertiesViewModel.cs +++ b/OpenUtau/ViewModels/NotePropertiesViewModel.cs @@ -3,9 +3,13 @@ using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Linq; +using System.Text; using Avalonia.Media; +using OpenUtau.Classic; +using OpenUtau.Classic.Flags; using OpenUtau.Core; using OpenUtau.Core.Format; +using OpenUtau.Core.Render; using OpenUtau.Core.Ustx; using OpenUtau.Core.Util; using ReactiveUI; @@ -194,6 +198,10 @@ public void LoadPart(UPart? part) { Expressions.Add(viewModel); } } + if (track.RendererSettings.renderer == Renderers.CLASSIC) { + var viewModel = new NotePropertyExpViewModel(this); // FlagBox + Expressions.Add(viewModel); + } AttachExpressions(); } else { this.Part = null; @@ -204,9 +212,21 @@ private void AttachExpressions() { if (Expressions.Count > 0) { if (selectedNotes.Count > 0) { var note = selectedNotes.First(); - foreach (NotePropertyExpViewModel exp in Expressions) { exp.IsNoteSelected = true; + + if (exp.IsFlagBox) { + var phoneme = Part?.phonemes.FirstOrDefault(phoneme => phoneme.Parent == note); + if (phoneme != null) { + exp.FlagValue = string.Empty; // Assign a different value just in case the text box is empty + exp.FlagValue = GetFlagText(phoneme); + } else { + exp.FlagValue = string.Empty; + } + exp.Warning = string.Empty; + continue; + } + var phonemeExpression = note.phonemeExpressions.FirstOrDefault(e => e.abbr == exp.abbr && e.index == 0); if (phonemeExpression != null) { if (exp.IsNumerical) { @@ -217,6 +237,7 @@ private void AttachExpressions() { exp.HasValue = true; } else { if (exp.IsNumerical) { + exp.Value = exp.defaultValue + 1; // Assign a different value just in case the text box is empty exp.Value = exp.defaultValue; } else if (exp.IsOptions) { exp.SelectedOption = (int)exp.defaultValue; @@ -234,15 +255,41 @@ private void AttachExpressions() { exp.IsNoteSelected = false; exp.HasValue = false; if (exp.IsNumerical) { + exp.Value = exp.defaultValue + 1; exp.Value = exp.defaultValue; } else if (exp.IsOptions) { exp.SelectedOption = (int)exp.defaultValue; + } else if (exp.IsFlagBox) { + exp.FlagValue = string.Empty; + exp.Warning = string.Empty; } } } } } + private string GetFlagText(UPhoneme phoneme) { + if (Part == null) { + return string.Empty; + } + var track = DocManager.Inst.Project.tracks[Part.trackNo]; + if (track.RendererSettings.renderer != Renderers.CLASSIC) { + return string.Empty; + } + + var resampler = ToolsManager.Inst.GetResampler(Renderers.CLASSIC); + var flags = phoneme.GetResamplerFlags(DocManager.Inst.Project, track) + .Where(flag => flag.Item3 != null && resampler.SupportsFlag(flag.Item3)); + var builder = new StringBuilder(); + foreach (var flag in flags) { + builder.Append(flag.Item1); + if (flag.Item2.HasValue) { + builder.Append(flag.Item2.Value); + } + } + return builder.ToString(); + } + #region ICmdSubscriber public void OnNext(UCommand cmd, bool isUndo) { var note = selectedNotes.FirstOrDefault(); @@ -541,6 +588,63 @@ public void SetOptionalExpressionsChanges(string abbr, int? value) { DocManager.Inst.EndUndoGroup(); } } + public void SetFlagFromText(string? text, out string? warning) { + warning = null; + if (AllowNoteEdit && Part != null && selectedNotes.Count > 0) { + var dict = new Dictionary(); + if (!string.IsNullOrWhiteSpace(text)) { + var parser = new UstFlagParser(); + foreach (UstFlag flag in parser.Parse(text)) { + dict.Add(flag.Key, flag.Value); + } + } + + var track = DocManager.Inst.Project.tracks[Part.trackNo]; + DocManager.Inst.StartUndoGroup("command.property.edit"); + track.GetSupportedExps(DocManager.Inst.Project) + .Where(d => d.isFlag && d.type == UExpressionType.Numerical) + .ForEach(descriptor => { + if (dict.TryGetValue(descriptor.flag, out float value)) { + dict.Remove(descriptor.flag); + if (value != descriptor.CustomDefaultValue) { + value = float.Clamp(value, descriptor.min, descriptor.max); + DocManager.Inst.ExecuteCmd(new SetNotesSameExpressionCommand(DocManager.Inst.Project, track, Part, selectedNotes, descriptor.abbr, value)); + } else { + DocManager.Inst.ExecuteCmd(new SetNotesSameExpressionCommand(DocManager.Inst.Project, track, Part, selectedNotes, descriptor.abbr, null)); + } + } else { + DocManager.Inst.ExecuteCmd(new SetNotesSameExpressionCommand(DocManager.Inst.Project, track, Part, selectedNotes, descriptor.abbr, null)); + } + }); + track.GetSupportedExps(DocManager.Inst.Project) + .Where(d => d.isFlag && d.type == UExpressionType.Options) + .ForEach(descriptor => { + bool find = false; + for (int i = 0; i < descriptor.options.Length; i++) { + string option = descriptor.options[i]; + var flag = dict.FirstOrDefault(flag => option == $"{flag.Key}{flag.Value}" || option == $"{flag.Key}"); + if (!string.IsNullOrEmpty(flag.Key)) { + dict.Remove(flag.Key); + find = true; + if (i != descriptor.CustomDefaultValue) { + DocManager.Inst.ExecuteCmd(new SetNotesSameExpressionCommand(DocManager.Inst.Project, track, Part, selectedNotes, descriptor.abbr, i)); + } else { + DocManager.Inst.ExecuteCmd(new SetNotesSameExpressionCommand(DocManager.Inst.Project, track, Part, selectedNotes, descriptor.abbr, null)); + } + break; + } + } + if (!find) { + DocManager.Inst.ExecuteCmd(new SetNotesSameExpressionCommand(DocManager.Inst.Project, track, Part, selectedNotes, descriptor.abbr, null)); + } + }); + if (dict.Count > 0) { + ThemeManager.TryGetString("errors.failed.parseflag", out string str); + warning = string.Format(str, string.Join(", ", dict.Keys)); + } + DocManager.Inst.EndUndoGroup(); + } + } // presets public void SavePortamentoPreset(string name) { @@ -581,6 +685,7 @@ public class NotePropertyExpViewModel : ViewModelBase { public string Name { get; set; } public bool IsNumerical { get; set; } = false; public bool IsOptions { get; set; } = false; + public bool IsFlagBox { get; set; } = false; public float Min { get; set; } public float Max { get; set; } public ObservableCollection Options { get; set; } = new ObservableCollection(); @@ -589,10 +694,12 @@ public class NotePropertyExpViewModel : ViewModelBase { [Reactive] public bool IsNoteSelected { get; set; } = false; [Reactive] public float Value { get; set; } + [Reactive] public string FlagValue { get; set; } = string.Empty; [Reactive] public int SelectedOption { get; set; } [Reactive] public bool DropDownOpen { get; set; } [Reactive] public bool HasValue { get; set; } = false; [Reactive] public FontWeight NameFontWeight { get; set; } + [Reactive] public string Warning { get; set; } = string.Empty; private NotePropertiesViewModel parentViewmodel; @@ -631,6 +738,15 @@ public NotePropertyExpViewModel(UExpressionDescriptor descriptor, NoteProperties } }); } + // Flag text box + public NotePropertyExpViewModel(NotePropertiesViewModel parent) { + Name = "Flags"; + defaultValue = 0; + abbr = string.Empty; + IsFlagBox = true; + parentViewmodel = parent; + NameFontWeight = FontWeight.Normal; + } public void SetNumericalExpressions(object? obj) { float? value = null; @@ -641,7 +757,11 @@ public void SetNumericalExpressions(object? obj) { value = f; } parentViewmodel.SetNumericalExpressionsChanges(abbr, value); - this.RaisePropertyChanged(nameof(Value)); + } + + public void SetFlagFromText(string? text) { + parentViewmodel.SetFlagFromText(text, out string? warning); + Warning = warning ?? string.Empty; } public override string ToString() {