From fab3e2dd98fa4f9f9ff8f275bfc2f10510529428 Mon Sep 17 00:00:00 2001 From: Maiko Date: Sat, 28 Mar 2026 23:23:51 +0900 Subject: [PATCH 1/2] Support Flag Text Input --- .../Controls/NotePropertyExpression.axaml | 2 + .../Controls/NotePropertyExpression.axaml.cs | 10 +++ .../ViewModels/NotePropertiesViewModel.cs | 87 ++++++++++++++++++- 3 files changed, 97 insertions(+), 2 deletions(-) diff --git a/OpenUtau/Controls/NotePropertyExpression.axaml b/OpenUtau/Controls/NotePropertyExpression.axaml index 0d702dd6d..1e4aed58f 100644 --- a/OpenUtau/Controls/NotePropertyExpression.axaml +++ b/OpenUtau/Controls/NotePropertyExpression.axaml @@ -15,5 +15,7 @@ SelectedIndex="{Binding SelectedOption}" MinWidth="120" IsVisible="{Binding IsOptions}" VerticalAlignment="Center" IsEnabled="{Binding IsNoteSelected}" IsDropDownOpen="{Binding DropDownOpen, Mode=OneWayToSource}" Name="comboBox"/> + diff --git a/OpenUtau/Controls/NotePropertyExpression.axaml.cs b/OpenUtau/Controls/NotePropertyExpression.axaml.cs index 474de2adc..d46c505be 100644 --- a/OpenUtau/Controls/NotePropertyExpression.axaml.cs +++ b/OpenUtau/Controls/NotePropertyExpression.axaml.cs @@ -31,6 +31,16 @@ 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; + } + } + } // slider void SliderPointerPressed(object? sender, PointerPressedEventArgs args) { diff --git a/OpenUtau/ViewModels/NotePropertiesViewModel.cs b/OpenUtau/ViewModels/NotePropertiesViewModel.cs index 15df14904..52586db5f 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,20 @@ 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; + } + continue; + } + var phonemeExpression = note.phonemeExpressions.FirstOrDefault(e => e.abbr == exp.abbr && e.index == 0); if (phonemeExpression != null) { if (exp.IsNumerical) { @@ -217,6 +236,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 +254,39 @@ 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; } + exp.FlagValue = 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 +585,31 @@ public void SetOptionalExpressionsChanges(string abbr, int? value) { DocManager.Inst.EndUndoGroup(); } } + public void SetFlagFromText(string? text) { + 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) && 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)); + } + }); + DocManager.Inst.EndUndoGroup(); + } + } // presets public void SavePortamentoPreset(string name) { @@ -581,6 +650,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,6 +659,7 @@ 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; @@ -631,6 +702,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 +721,10 @@ public void SetNumericalExpressions(object? obj) { value = f; } parentViewmodel.SetNumericalExpressionsChanges(abbr, value); - this.RaisePropertyChanged(nameof(Value)); + } + + public void SetFlagFromText(string? text) { + parentViewmodel.SetFlagFromText(text); } public override string ToString() { From 122720fcf9b0adeee41a8fdff0c3dbb29baa912e Mon Sep 17 00:00:00 2001 From: Maiko Date: Sun, 29 Mar 2026 01:11:43 +0900 Subject: [PATCH 2/2] add warning --- .../Controls/NotePropertyExpression.axaml | 32 ++++++------ .../Controls/NotePropertyExpression.axaml.cs | 8 +++ OpenUtau/Strings/Strings.axaml | 1 + .../ViewModels/NotePropertiesViewModel.cs | 49 ++++++++++++++++--- 4 files changed, 70 insertions(+), 20 deletions(-) diff --git a/OpenUtau/Controls/NotePropertyExpression.axaml b/OpenUtau/Controls/NotePropertyExpression.axaml index 1e4aed58f..b24cbaf59 100644 --- a/OpenUtau/Controls/NotePropertyExpression.axaml +++ b/OpenUtau/Controls/NotePropertyExpression.axaml @@ -4,18 +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 d46c505be..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; @@ -38,6 +39,13 @@ void OnFlagBoxLostFocus(object? sender, RoutedEventArgs args) { NotePropertiesViewModel.PanelControlPressed = true; viewModel.SetFlagFromText(textBox.Text); NotePropertiesViewModel.PanelControlPressed = false; + + if (!string.IsNullOrEmpty(viewModel.Warning)) { + var scrollViewer = this.FindAncestorOfType(); + if (scrollViewer != null) { + scrollViewer.ScrollToEnd(); + } + } } } } 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 52586db5f..d78626377 100644 --- a/OpenUtau/ViewModels/NotePropertiesViewModel.cs +++ b/OpenUtau/ViewModels/NotePropertiesViewModel.cs @@ -223,6 +223,7 @@ private void AttachExpressions() { } else { exp.FlagValue = string.Empty; } + exp.Warning = string.Empty; continue; } @@ -258,8 +259,10 @@ private void AttachExpressions() { 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; } - exp.FlagValue = string.Empty; } } } @@ -585,7 +588,8 @@ public void SetOptionalExpressionsChanges(string abbr, int? value) { DocManager.Inst.EndUndoGroup(); } } - public void SetFlagFromText(string? text) { + 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)) { @@ -600,13 +604,44 @@ public void SetFlagFromText(string? text) { track.GetSupportedExps(DocManager.Inst.Project) .Where(d => d.isFlag && d.type == UExpressionType.Numerical) .ForEach(descriptor => { - if (dict.TryGetValue(descriptor.flag, out float value) && 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)); + 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(); } } @@ -664,6 +699,7 @@ public class NotePropertyExpViewModel : ViewModelBase { [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; @@ -724,7 +760,8 @@ public void SetNumericalExpressions(object? obj) { } public void SetFlagFromText(string? text) { - parentViewmodel.SetFlagFromText(text); + parentViewmodel.SetFlagFromText(text, out string? warning); + Warning = warning ?? string.Empty; } public override string ToString() {