diff --git a/OpenUtau/Controls/ExpressionCanvas.cs b/OpenUtau/Controls/ExpressionCanvas.cs index 901ff5def..0e8d92a10 100644 --- a/OpenUtau/Controls/ExpressionCanvas.cs +++ b/OpenUtau/Controls/ExpressionCanvas.cs @@ -40,6 +40,10 @@ class ExpressionCanvas : Control { o => o.ShowRealCurve, (o, v) => o.ShowRealCurve = v); + public static readonly DirectProperty DisplayModeProperty = + AvaloniaProperty.RegisterDirect( + nameof(DisplayMode), o => o.DisplayMode, (o, v) => o.DisplayMode = v); + public double TickWidth { get => tickWidth; private set => SetAndRaise(TickWidthProperty, ref tickWidth, value); @@ -60,12 +64,18 @@ public bool ShowRealCurve { get => showRealCurve; set => SetAndRaise(ShowRealCurveProperty, ref showRealCurve, value); } + + public ExpDisMode DisplayMode { + get => displayMode; + set => SetAndRaise(DisplayModeProperty, ref displayMode, value); + } private double tickWidth; private double tickOffset; private UVoicePart? part; private string key = string.Empty; private bool showRealCurve = true; + private ExpDisMode displayMode = ExpDisMode.Visible; private HashSet selectedNotes = new HashSet(); private CurveSelection curveSelection = new CurveSelection(); @@ -99,6 +109,11 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang public override void Render(DrawingContext context) { base.Render(context); + + // Skip rendering if hidden + if (DisplayMode == ExpDisMode.Hidden) { + return; + } if (Part == null) { return; } @@ -114,7 +129,11 @@ public override void Render(DrawingContext context) { if (descriptor.max <= descriptor.min) { return; } - DrawBackgroundForHitTest(context); + + if (DisplayMode != ExpDisMode.Shadow) { + DrawBackgroundForHitTest(context); + } + double leftTick = TickOffset - 480; double rightTick = TickOffset + Bounds.Width / TickWidth + 480; double optionHeight = descriptor.type == UExpressionType.Options @@ -123,12 +142,14 @@ public override void Render(DrawingContext context) { if (descriptor.type == UExpressionType.Curve) { var curve = Part.curves.FirstOrDefault(c => c.descriptor == descriptor); double defaultHeight = Math.Round(Bounds.Height - Bounds.Height * (descriptor.defaultValue - descriptor.min) / (descriptor.max - descriptor.min)); - var lPen = ThemeManager.AccentPen1; - var lPen2 = ThemeManager.AccentPen1Thickness2; - var lPenSelected = ThemeManager.AccentPen2; - var lPen2Selected = ThemeManager.AccentPen2Thickness2; + + var lPen = DisplayMode == ExpDisMode.Shadow ? ThemeManager.NeutralAccentPen : ThemeManager.AccentPen1; + var lPen2 = DisplayMode == ExpDisMode.Shadow ? new Pen(ThemeManager.NeutralAccentBrush, 3) : ThemeManager.AccentPen1Thickness3; + var lPenSelected = DisplayMode == ExpDisMode.Shadow ? ThemeManager.NeutralAccentPen : ThemeManager.AccentPen2; + var lPen2Selected = DisplayMode == ExpDisMode.Shadow ? new Pen(ThemeManager.NeutralAccentBrush, 3) : ThemeManager.AccentPen2Thickness3; var lPen3 = new Pen(ThemeManager.NeutralAccentBrush, 1, new DashStyle(new double[] { 4, 4 }, 0)); - var brush = ThemeManager.AccentBrush1; + var brush = DisplayMode == ExpDisMode.Shadow ? ThemeManager.NeutralAccentBrush : ThemeManager.AccentBrush1; + double x3 = Math.Round(viewModel.TickToneToPoint(leftTick, 0).X); double x4 = Math.Round(viewModel.TickToneToPoint(rightTick, 0).X); context.DrawLine(lPen3, new Point(x3, defaultHeight), new Point(x4, defaultHeight)); @@ -161,6 +182,12 @@ public override void Render(DrawingContext context) { index = -index - 1; } index = Math.Max(0, index) - 1; + + // Create geometry elements for the custom curve fill + var fillGeometry = new PathGeometry(); + var fillFigure = new PathFigure { IsClosed = true }; + bool fillStarted = false; + while (index < xs.Count) { float tick1 = index < 0 ? lTick : xs[index]; float value1 = index < 0 ? descriptor.defaultValue : ys[index]; @@ -170,6 +197,18 @@ public override void Render(DrawingContext context) { float value2 = index == xs.Count - 1 ? descriptor.defaultValue : ys[index + 1]; double x2 = viewModel.TickToneToPoint(tick2, 0).X; double y2 = defaultHeight - Bounds.Height * (value2 - descriptor.defaultValue) / (descriptor.max - descriptor.min); + + if (!fillStarted) { + fillFigure.StartPoint = new Point(x1, defaultHeight); + fillFigure.Segments!.Add(new LineSegment { Point = new Point(x1, y1), IsStroked = false }); + fillStarted = true; + } + fillFigure.Segments!.Add(new LineSegment { Point = new Point(x2, y2), IsStroked = false }); + + if (tick2 >= rTick || index == xs.Count - 1) { + fillFigure.Segments!.Add(new LineSegment { Point = new Point(x2, defaultHeight), IsStroked = false }); + } + IPen pen; if (curveSelection.HasValue(descriptor.abbr)) { if (curveSelection.StartPoint.x <= tick1 && tick1 <= curveSelection.EndPoint.x @@ -182,14 +221,19 @@ public override void Render(DrawingContext context) { pen = value1 == descriptor.defaultValue && value2 == descriptor.defaultValue ? lPen : lPen2; } context.DrawLine(pen, new Point(x1, y1), new Point(x2, y2)); - //using (var state = context.PushTransform(Matrix.CreateTranslation(x1, y1))) { - // context.DrawGeometry(brush, null, pointGeometry); - //} index++; if (tick2 >= rTick) { break; } } + + if (fillStarted) { + fillGeometry.Figures!.Add(fillFigure); + using (var state = context.PushOpacity(0.2)) { + context.DrawGeometry(brush, null, fillGeometry); + } + } + if (ShowRealCurve) { int baseIndexL = curve.realXs.BinarySearch(lTick); if (baseIndexL < 0) { @@ -202,7 +246,6 @@ public override void Render(DrawingContext context) { } int offset = baseIndexL; while (offset < baseIndexR) { - // negative values are breakpoints int start = offset; while (start < baseIndexR && curve.realYs[start] < 0) ++start; int end = start; @@ -213,15 +256,16 @@ public override void Render(DrawingContext context) { } var geometry = new PathGeometry(); var figure = new PathFigure { - IsClosed = false + IsClosed = true }; for (int i = start; i < end; ++i) { float tick = curve.realXs[i]; float value = curve.realYs[i]; double x = viewModel.TickToneToPoint(tick, 0).X; double y = Bounds.Height * (1 - value / 1000.0); + if (i == start) { - figure.StartPoint = new Point(x, Bounds.Height); + figure.StartPoint = new Point(x, defaultHeight); } figure.Segments!.Add(new LineSegment { Point = new Point(x, y), @@ -229,7 +273,7 @@ public override void Render(DrawingContext context) { }); if (i == end - 1) { figure.Segments!.Add(new LineSegment { - Point = new Point(x, Bounds.Height), + Point = new Point(x, defaultHeight), IsStroked = false }); } @@ -241,6 +285,16 @@ public override void Render(DrawingContext context) { } return; } + if (descriptor.type == UExpressionType.Numerical) { + double p1 = Math.Round(viewModel.TickToneToPoint(leftTick, 0).X); + double p2 = Math.Round(viewModel.TickToneToPoint(rightTick, 0).X); + var dashedPen = new Pen(ThemeManager.NeutralAccentBrushSemi, 1, new DashStyle(new double[] { 4, 4 }, 0)); + double defaultHeight = Math.Round(Bounds.Height - Bounds.Height * (descriptor.defaultValue - descriptor.min) / (descriptor.max - descriptor.min)); + context.DrawLine(dashedPen, new Point(p1, defaultHeight), new Point(p2, defaultHeight)); + } + var shadowHPen = new Pen(ThemeManager.NeutralAccentBrush, 3); + var shadowVPen = new Pen(ThemeManager.NeutralAccentBrush, 3); + foreach (var phoneme in Part.phonemes) { if (phoneme.Error || phoneme.Parent == null) { continue; @@ -251,17 +305,36 @@ public override void Render(DrawingContext context) { continue; } var note = phoneme.Parent; - var hPen = selectedNotes.Contains(note) ? ThemeManager.AccentPen2Thickness2 : ThemeManager.AccentPen1Thickness2; - var vPen = selectedNotes.Contains(note) ? ThemeManager.AccentPen2Thickness3 : ThemeManager.AccentPen1Thickness3; - var brush = selectedNotes.Contains(note) ? ThemeManager.AccentBrush2 : ThemeManager.AccentBrush1; + + var hPen = DisplayMode == ExpDisMode.Shadow ? shadowHPen : (selectedNotes.Contains(note) ? ThemeManager.AccentPen2Thickness3 : ThemeManager.AccentPen1Thickness3); + var vPen = DisplayMode == ExpDisMode.Shadow ? shadowVPen : (selectedNotes.Contains(note) ? ThemeManager.AccentPen2Thickness3 : ThemeManager.AccentPen1Thickness3); + var brush = DisplayMode == ExpDisMode.Shadow ? ThemeManager.NeutralAccentBrush : (selectedNotes.Contains(note) ? ThemeManager.AccentBrush2 : ThemeManager.AccentBrush1); + var (value, overriden) = phoneme.GetExpression(project, track, Key); double x1 = Math.Round(viewModel.TickToneToPoint(phoneme.position, 0).X); double x2 = Math.Round(viewModel.TickToneToPoint(phoneme.End, 0).X); + if (descriptor.type == UExpressionType.Numerical) { double valueHeight = Math.Round(Bounds.Height - Bounds.Height * (value - descriptor.min) / (descriptor.max - descriptor.min)); double zeroHeight = Math.Round(Bounds.Height - Bounds.Height * (0f - descriptor.min) / (descriptor.max - descriptor.min)); + + double rectX = x1; + double rectY = Math.Min(zeroHeight, valueHeight); + double rectHeight = Math.Abs(zeroHeight - valueHeight); + double rectWidth = Math.Max(0, Math.Max(x1, x2) - rectX); + var fillRect = new Rect(rectX, rectY, rectWidth, rectHeight); + + // Use 45% opacity if edited, 15% opacity if default + double fillOpacity = overriden ? 0.45 : 0.15; + + using (var state = context.PushOpacity(fillOpacity)) { + context.DrawRectangle(brush, null, fillRect); + } + + // Vertical and horizontal lines context.DrawLine(vPen, new Point(x1 + 0.5, zeroHeight + 0.5), new Point(x1 + 0.5, valueHeight + 3)); - context.DrawLine(hPen, new Point(x1 + 3, valueHeight), new Point(Math.Max(x1 + 3, x2 - 3), valueHeight)); + context.DrawLine(hPen, new Point(x1 + 3, valueHeight), new Point(Math.Max(x1 + 3, x2), valueHeight)); + using (var state = context.PushTransform(Matrix.CreateTranslation(x1 + 0.5, valueHeight))) { context.DrawGeometry(overriden ? brush : ThemeManager.BackgroundBrush, vPen, pointGeometry); } @@ -281,7 +354,8 @@ public override void Render(DrawingContext context) { } } } - if (descriptor.type == UExpressionType.Options) { + + if (descriptor.type == UExpressionType.Options && DisplayMode != ExpDisMode.Shadow) { for (int i = 0; i < descriptor.options.Length; ++i) { string option = descriptor.options[i]; if (string.IsNullOrEmpty(option)) { diff --git a/OpenUtau/Controls/NotesCanvas.cs b/OpenUtau/Controls/NotesCanvas.cs index a0b1e46e4..0a218b62b 100644 --- a/OpenUtau/Controls/NotesCanvas.cs +++ b/OpenUtau/Controls/NotesCanvas.cs @@ -219,13 +219,37 @@ private void RenderNoteBody(UNote note, NotesViewModel viewModel, DrawingContext Size size = viewModel.TickToneToSize(note.duration, 1); size = size.WithWidth(size.Width - 1).WithHeight(Math.Floor(size.Height - 2)); Point rightBottom = new Point(leftTop.X + size.Width, leftTop.Y + size.Height); + bool hasError = note.Error; + + // Check for Phoneme Errors (mimicking PhonemeCanvas behavior) + if (!hasError && Part != null && Part.phonemes != null) { + int phonemeCount = 0; + foreach (var p in Part.phonemes) { + if (p.Parent == note) { + phonemeCount++; + // If any attached phoneme has an error, the whole note is flagged + if (p.Error) { + hasError = true; + break; + } + } + } + // Edge Case: If the note is not a continuation/rest but generated 0 phonemes, + // it means the phonemizer completely failed to process the lyric. + if (!hasError && phonemeCount == 0 && !note.lyric.StartsWith("+") && !note.lyric.StartsWith("-")) { + hasError = true; + } + } + // apply the transparent/greyed-out brush if an error was found var brush = selectedNotes.Contains(note) - ? (note.Error ? ThemeManager.AccentBrush2Semi : ThemeManager.AccentBrush2) - : (note.Error ? ThemeManager.AccentBrush1Semi : ThemeManager.AccentBrush1); + ? (hasError ? ThemeManager.AccentBrush3Semi : ThemeManager.AccentBrush2) + : (hasError ? ThemeManager.NeutralAccentBrushSemi : ThemeManager.AccentBrush1); + context.DrawRectangle(brush, null, new Rect(leftTop, rightBottom), 2, 2); if (TrackHeight < 10 || note.lyric.Length == 0) { return; } + // grey out the Phonemizer Transition Badges if (ShowPhonemizerTags && TrackHeight >= 20) { string currentOver = note.PhonemizerOverride ?? ""; bool isCurrentDefault = string.IsNullOrEmpty(currentOver) || currentOver.Equals("Default", StringComparison.OrdinalIgnoreCase); @@ -240,13 +264,12 @@ private void RenderNoteBody(UNote note, NotesViewModel viewModel, DrawingContext bool isTransition = !isContinuation && ((note.Prev == null && !isCurrentDefault) || (note.Prev != null && currentPh != prevPh)); if (isTransition) { + // Badge Background utilizes the same hasError flag var badgeBrush = selectedNotes.Contains(note) - ? (note.Error ? ThemeManager.AccentBrush2Semi : ThemeManager.AccentBrush2) - : (note.Error ? ThemeManager.AccentBrush1Semi : ThemeManager.AccentBrush1); + ? (hasError ? ThemeManager.AccentBrush3Semi : ThemeManager.AccentBrush2) + : (hasError ? ThemeManager.NeutralAccentBrushSemi : ThemeManager.AccentBrush1); if (isCurrentDefault) { - // Due to the limitation, we'll display a dot to inndicate - // the transition to default phonemizer instead of showing language tag double boxWidth = 16; double boxHeight = 16; double dotRadius = 3; diff --git a/OpenUtau/Controls/PianoRoll.axaml b/OpenUtau/Controls/PianoRoll.axaml index 7f7ff44bd..531e1f1c2 100644 --- a/OpenUtau/Controls/PianoRoll.axaml +++ b/OpenUtau/Controls/PianoRoll.axaml @@ -559,7 +559,7 @@ + ToolTip.Tip="{DynamicResource pianoroll.toggle.phonemizer}"> @@ -651,7 +651,9 @@ TickOrigin="{Binding NotesViewModel.TickOrigin}" TickOffset="{Binding NotesViewModel.TickOffset}" SnapDiv="{Binding NotesViewModel.SnapDiv, Mode=OneWay}"/> - Theme Editor Save Cancel + Alt + Click to view the full error Track Polish diff --git a/OpenUtau/ThemeManager.cs b/OpenUtau/ThemeManager.cs index 3b7bc72f1..9ea45d458 100644 --- a/OpenUtau/ThemeManager.cs +++ b/OpenUtau/ThemeManager.cs @@ -23,11 +23,13 @@ class ThemeManager { public static IPen AccentPen1 = new Pen(Brushes.White); public static IPen AccentPen1Thickness2 = new Pen(Brushes.White); public static IPen AccentPen1Thickness3 = new Pen(Brushes.White); + public static IPen AccentPen1Thickness4 = new Pen(Brushes.White); public static IBrush AccentBrush1Semi = Brushes.Gray; public static IBrush AccentBrush2 = Brushes.Gray; public static IPen AccentPen2 = new Pen(Brushes.White); public static IPen AccentPen2Thickness2 = new Pen(Brushes.White); public static IPen AccentPen2Thickness3 = new Pen(Brushes.White); + public static IPen AccentPen2Thickness4 = new Pen(Brushes.White); public static IBrush AccentBrush2Semi = Brushes.Gray; public static IBrush AccentBrush3 = Brushes.Gray; public static IPen AccentPen3 = new Pen(Brushes.White); @@ -112,6 +114,7 @@ public static void LoadTheme() { AccentPen1 = new Pen(AccentBrush1); AccentPen1Thickness2 = new Pen(AccentBrush1, 2); AccentPen1Thickness3 = new Pen(AccentBrush1, 3); + AccentPen1Thickness4 = new Pen(AccentBrush1, 4); } if (resDict.TryGetResource("AccentBrush1Semi", themeVariant, out outVar)) { AccentBrush1Semi = (IBrush)outVar!; @@ -121,6 +124,7 @@ public static void LoadTheme() { AccentPen2 = new Pen(AccentBrush2, 1); AccentPen2Thickness2 = new Pen(AccentBrush2, 2); AccentPen2Thickness3 = new Pen(AccentBrush2, 3); + AccentPen2Thickness4 = new Pen(AccentBrush2, 4); } if (resDict.TryGetResource("AccentBrush2Semi", themeVariant, out outVar)) { AccentBrush2Semi = (IBrush)outVar!; diff --git a/OpenUtau/Views/MainWindow.axaml.cs b/OpenUtau/Views/MainWindow.axaml.cs index 5ea4cdb0c..7411c3b84 100644 --- a/OpenUtau/Views/MainWindow.axaml.cs +++ b/OpenUtau/Views/MainWindow.axaml.cs @@ -26,6 +26,7 @@ using Serilog; using SharpCompress; using Point = Avalonia.Point; +using System.Runtime.InteropServices; namespace OpenUtau.App.Views { public partial class MainWindow : Window, ICmdSubscriber { @@ -258,19 +259,40 @@ void OnMainMenuPointerLeave(object sender, PointerEventArgs args) { void OnMenuOpenProjectLocation(object sender, RoutedEventArgs args) { var project = DocManager.Inst.Project; - if (string.IsNullOrEmpty(project.FilePath) || !project.Saved) { + if (string.IsNullOrEmpty(project.FilePath) || !project.Saved || !System.IO.File.Exists(project.FilePath) || !Path.IsPathRooted(project.FilePath)) { MessageBox.Show( this, ThemeManager.GetString("dialogs.export.savefirst"), ThemeManager.GetString("errors.caption"), MessageBox.MessageBoxButtons.Ok); + return; } try { - var dir = Path.GetDirectoryName(project.FilePath); - if (dir != null) { - OS.OpenFolder(dir); + var fullPath = Path.GetFullPath(project.FilePath); + var dir = Path.GetDirectoryName(fullPath); + if (!string.IsNullOrWhiteSpace(dir) && System.IO.Directory.Exists(dir)) { + // Cross-platform folder opening + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { + FileName = "explorer.exe", + Arguments = $"\"{dir}\"", // Quotes protect spaces and commas + UseShellExecute = true + }); + } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { + FileName = "open", + Arguments = $"\"{dir}\"", + UseShellExecute = false + }); + } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { + FileName = "xdg-open", + Arguments = $"\"{dir}\"", + UseShellExecute = false + }); + } } else { - Log.Error($"Failed to get project location from {dir}."); + Log.Error($"Failed to get project location from {project.FilePath}."); } } catch (Exception e) { Log.Error(e, "Failed to open project location.");