From 3877ae0db62c79124966826d1197bc42ff0849f4 Mon Sep 17 00:00:00 2001
From: 1374232024 <1374232024@QQ.com>
Date: Wed, 24 Jun 2026 14:19:29 +0800
Subject: [PATCH 1/6] fix hide
---
OpenUtau/Controls/PianoRoll.axaml.cs | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/OpenUtau/Controls/PianoRoll.axaml.cs b/OpenUtau/Controls/PianoRoll.axaml.cs
index 2fd05db2d..cff1b9e6a 100644
--- a/OpenUtau/Controls/PianoRoll.axaml.cs
+++ b/OpenUtau/Controls/PianoRoll.axaml.cs
@@ -352,6 +352,9 @@ void OnMenuDetachPianoRoll(object sender, RoutedEventArgs args) {
}
void OnMenuHidePianoRoll(object sender, RoutedEventArgs args) {
+ if (Preferences.Default.DetachPianoRoll && MainWindow != null) {
+ MainWindow.SetPianoRollAttachment();
+ }
if (RootWindow.DataContext is MainWindowViewModel mwvm) {
mwvm.ShowPianoRoll = false;
} else {
@@ -2017,4 +2020,4 @@ public void OnNext(UCommand cmd, bool isUndo) {
}
}
}
-}
+}
\ No newline at end of file
From 1b45985b3f0c5f21731b62f43c68b66e82daa987 Mon Sep 17 00:00:00 2001
From: 1374232024 <1374232024@QQ.com>
Date: Wed, 24 Jun 2026 21:15:33 +0800
Subject: [PATCH 2/6] hide "hide"
---
OpenUtau/Controls/PianoRoll.axaml | 2 +-
OpenUtau/Controls/PianoRoll.axaml.cs | 1 +
OpenUtau/ViewModels/PianoRollViewModel.cs | 1 +
3 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/OpenUtau/Controls/PianoRoll.axaml b/OpenUtau/Controls/PianoRoll.axaml
index 647d22145..7e138bdf8 100644
--- a/OpenUtau/Controls/PianoRoll.axaml
+++ b/OpenUtau/Controls/PianoRoll.axaml
@@ -359,7 +359,7 @@
-
+
diff --git a/OpenUtau/Controls/PianoRoll.axaml.cs b/OpenUtau/Controls/PianoRoll.axaml.cs
index 2fd05db2d..5d8e51168 100644
--- a/OpenUtau/Controls/PianoRoll.axaml.cs
+++ b/OpenUtau/Controls/PianoRoll.axaml.cs
@@ -349,6 +349,7 @@ void OnMenuSearchNote(object sender, RoutedEventArgs args) {
void OnMenuDetachPianoRoll(object sender, RoutedEventArgs args) {
MainWindow?.SetPianoRollAttachment();
ViewModel.RaisePropertyChanged(nameof(ViewModel.PianoRollDetached));
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.HideMenuItemVisible));
}
void OnMenuHidePianoRoll(object sender, RoutedEventArgs args) {
diff --git a/OpenUtau/ViewModels/PianoRollViewModel.cs b/OpenUtau/ViewModels/PianoRollViewModel.cs
index f4f1bfbcb..359838e1c 100644
--- a/OpenUtau/ViewModels/PianoRollViewModel.cs
+++ b/OpenUtau/ViewModels/PianoRollViewModel.cs
@@ -68,6 +68,7 @@ public class PianoRollViewModel : ViewModelBase, ICmdSubscriber {
public bool PlaybackAutoScroll1 { get => Preferences.Default.PlaybackAutoScroll == 1 ? true : false; }
public bool PlaybackAutoScroll2 { get => Preferences.Default.PlaybackAutoScroll == 2 ? true : false; }
public bool PianoRollDetached { get => Preferences.Default.DetachPianoRoll; }
+ public bool HideMenuItemVisible => !Preferences.Default.DetachPianoRoll;
public bool ShowPhonemizerTags {
get => Preferences.Default.ShowPhonemizerTags;
set {
From 32d89b1e0efd8a731cc4809f785c6031cb2efc42 Mon Sep 17 00:00:00 2001
From: 1374232024 <1374232024@QQ.com>
Date: Thu, 25 Jun 2026 01:57:46 +0800
Subject: [PATCH 3/6] better-PianoRoll
---
OpenUtau/Controls/PianoRoll.axaml | 47 ++++++++++++++++++++++-
OpenUtau/Controls/PianoRoll.axaml.cs | 38 +++++++++++++++++-
OpenUtau/ViewModels/PianoRollViewModel.cs | 2 +-
3 files changed, 83 insertions(+), 4 deletions(-)
diff --git a/OpenUtau/Controls/PianoRoll.axaml b/OpenUtau/Controls/PianoRoll.axaml
index 7e138bdf8..f234650f8 100644
--- a/OpenUtau/Controls/PianoRoll.axaml
+++ b/OpenUtau/Controls/PianoRoll.axaml
@@ -538,6 +538,7 @@
+
+
+
@@ -626,6 +644,33 @@
+
+
+
+
+
+
+
@@ -678,4 +723,4 @@
-
+
\ No newline at end of file
diff --git a/OpenUtau/Controls/PianoRoll.axaml.cs b/OpenUtau/Controls/PianoRoll.axaml.cs
index 5d8e51168..e1e8b0397 100644
--- a/OpenUtau/Controls/PianoRoll.axaml.cs
+++ b/OpenUtau/Controls/PianoRoll.axaml.cs
@@ -11,6 +11,7 @@
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
+using Avalonia.Threading;
using OpenUtau.App.ViewModels;
using OpenUtau.App.Views;
using OpenUtau.Core;
@@ -349,10 +350,25 @@ void OnMenuSearchNote(object sender, RoutedEventArgs args) {
void OnMenuDetachPianoRoll(object sender, RoutedEventArgs args) {
MainWindow?.SetPianoRollAttachment();
ViewModel.RaisePropertyChanged(nameof(ViewModel.PianoRollDetached));
- ViewModel.RaisePropertyChanged(nameof(ViewModel.HideMenuItemVisible));
+ // 延迟通知,等窗口切换完成后再更新,避免切换过程中触发 UI 更新导致崩溃
+ Dispatcher.UIThread.Post(() => {
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.HideMenuItemVisible));
+ });
}
void OnMenuHidePianoRoll(object sender, RoutedEventArgs args) {
+ // Reset ToggleButton checked state / 重置 ToggleButton 的 checked 状态
+ // Reason / 原因:
+ // ToggleButton keeps IsChecked=true after click, causing darker background.
+ // But this close button is a one-shot action, not a state toggle.
+ // So we manually reset it to false immediately after click.
+ // Since piano roll hides instantly, users never see this momentary state change.
+ //
+ // 详细说明:
+ // ToggleButton 点击后会保持 IsChecked=true,导致底纹变深。
+ // 但关闭按钮是一次性操作,不应该保持状态,所以手动重置为 false。
+ // 因为点击后钢琴卷帘立即隐藏,用户看不到这个瞬间的状态变化。
+ HidePianoRollButton.IsChecked = false;
if (RootWindow.DataContext is MainWindowViewModel mwvm) {
mwvm.ShowPianoRoll = false;
} else {
@@ -361,6 +377,24 @@ void OnMenuHidePianoRoll(object sender, RoutedEventArgs args) {
}
// Edit Tools
+ private long _lastToolbarClickTime = 0;
+ private const long DoubleClickThreshold = 300; // 毫秒
+
+ void OnToolbarPointerPressed(object sender, PointerPressedEventArgs args) {
+ // 双击工具栏空白处关闭钢琴卷帘(仅嵌入模式生效)
+ if (!args.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
+
+ long now = DateTimeOffset.Now.ToUnixTimeMilliseconds();
+ if (now - _lastToolbarClickTime < DoubleClickThreshold) {
+ // 双击
+ if (RootWindow.DataContext is MainWindowViewModel mwvm) {
+ mwvm.ShowPianoRoll = false;
+ }
+ // 分离模式下不响应,保持和原来一模一样
+ }
+ _lastToolbarClickTime = now;
+ }
+
private CancellationTokenSource? _longPressCts;
private async void OnToolButtonPointerPressed(object? sender, PointerPressedEventArgs args) {
if (!args.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
@@ -2018,4 +2052,4 @@ public void OnNext(UCommand cmd, bool isUndo) {
}
}
}
-}
+}
\ No newline at end of file
diff --git a/OpenUtau/ViewModels/PianoRollViewModel.cs b/OpenUtau/ViewModels/PianoRollViewModel.cs
index 359838e1c..2e4907cec 100644
--- a/OpenUtau/ViewModels/PianoRollViewModel.cs
+++ b/OpenUtau/ViewModels/PianoRollViewModel.cs
@@ -320,4 +320,4 @@ public void OnNext(UCommand cmd, bool isUndo) {
#endregion
}
-}
+}
\ No newline at end of file
From ac10af509c0f45312cd2b3c3974f93808f1d62cb Mon Sep 17 00:00:00 2001
From: 1374232024 <1374232024@QQ.com>
Date: Thu, 25 Jun 2026 02:32:48 +0800
Subject: [PATCH 4/6] fix-Detach
---
OpenUtau/Controls/PianoRoll.axaml | 38 +++++--------------------------
1 file changed, 6 insertions(+), 32 deletions(-)
diff --git a/OpenUtau/Controls/PianoRoll.axaml b/OpenUtau/Controls/PianoRoll.axaml
index f234650f8..2ba480cb5 100644
--- a/OpenUtau/Controls/PianoRoll.axaml
+++ b/OpenUtau/Controls/PianoRoll.axaml
@@ -554,23 +554,10 @@
-
-
+
+
+
+
@@ -644,27 +631,14 @@
-
+
From 05942ce2fd3dc3fc90ccb6ce9c3e64c3d937fc5c Mon Sep 17 00:00:00 2001
From: 27704534 <1374232024@qq.com>
Date: Thu, 25 Jun 2026 11:10:40 +0800
Subject: [PATCH 5/6] Add files via upload
add
---
OpenUtau/Controls/PianoRoll.axaml | 1381 ++++-----
OpenUtau/Controls/PianoRoll.axaml.cs | 4076 +++++++++++++-------------
2 files changed, 2754 insertions(+), 2703 deletions(-)
diff --git a/OpenUtau/Controls/PianoRoll.axaml b/OpenUtau/Controls/PianoRoll.axaml
index 647d22145..341d88bbf 100644
--- a/OpenUtau/Controls/PianoRoll.axaml
+++ b/OpenUtau/Controls/PianoRoll.axaml
@@ -1,681 +1,700 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OpenUtau/Controls/PianoRoll.axaml.cs b/OpenUtau/Controls/PianoRoll.axaml.cs
index cff1b9e6a..b30627925 100644
--- a/OpenUtau/Controls/PianoRoll.axaml.cs
+++ b/OpenUtau/Controls/PianoRoll.axaml.cs
@@ -1,2023 +1,2055 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using System.Reactive;
-using System.Threading;
-using System.Threading.Tasks;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Controls.Primitives;
-using Avalonia.Input;
-using Avalonia.Interactivity;
-using OpenUtau.App.ViewModels;
-using OpenUtau.App.Views;
-using OpenUtau.Core;
-using OpenUtau.Core.Editing;
-using OpenUtau.Core.Ustx;
-using OpenUtau.Core.Util;
-using OpenUtau.ViewModels;
-using ReactiveUI;
-using Serilog;
-
-namespace OpenUtau.App.Controls {
- interface IValueTip {
- void ShowValueTip();
- void HideValueTip();
- void UpdateValueTip(string text);
- }
-
- public partial class PianoRoll : UserControl, IValueTip, ICmdSubscriber {
- public MainWindow? MainWindow { get; set; }
- public PianoRollViewModel ViewModel;
-
- private readonly KeyModifiers cmdKey =
- OS.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control;
- private KeyboardPlayState? keyboardPlayState;
- private NoteEditState? editState;
- private Point valueTipPointerPosition;
- private bool shouldOpenNotesContextMenu;
-
- private bool isSelectingRange;
- private Point rangeSelectStartPoint = default;
- private const double RangeSelectThreshold = 5; // pixels
-
- private ReactiveCommand? lyricsDialogCommand;
- private ReactiveCommand? noteDefaultsCommand;
- private ReactiveCommand? noteBatchEditCommand;
-
- private Window RootWindow => (Window)TopLevel.GetTopLevel(this)!;
-
- public PianoRoll(PianoRollViewModel model) {
- InitializeComponent();
- DataContext = ViewModel = model;
- ValueTip.IsVisible = false;
- SetPenToolIcon();
- penTool.AddHandler(PointerPressedEvent, OnToolButtonPointerPressed, RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true);
- this.LayoutUpdated += PianoRollLayoutUpdated;
- }
-
- private void PianoRollLayoutUpdated(object? sender, EventArgs e) {
- UpdatePortraitPosition();
- }
-
- private void UpdatePortraitPosition() {
- if (PortraitImage.DesiredSize.Width == 0 || PortraitCanvas.Bounds.Width == 0) return;
- // Position at top-right of row 3, with 100px margin from right
- Canvas.SetTop(PortraitImage, 0);
- Canvas.SetLeft(PortraitImage, PortraitCanvas.Bounds.Width - PortraitImage.DesiredSize.Width - 100);
- }
-
- public void InitializePianoRollWindowAsync() {
- noteBatchEditCommand = ReactiveCommand.Create(async edit => {
- var NotesVm = ViewModel?.NotesViewModel;
- if (NotesVm == null || NotesVm.Part == null) {
- return;
- }
- try {
- if (edit.IsAsync) {
- var mainWindow =
- (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)
- ?.MainWindow! as MainWindow;
- var name = ThemeManager.GetString(edit.Name);
- await MessageBox.ShowProcessing(RootWindow, $"{name} - ? / ?",
- ThemeManager.GetString("pianoroll.menu.batch.running"),
- (messageBox, cancellationToken) => {
- edit.RunAsync(NotesVm.Project, NotesVm.Part,
- NotesVm.Selection.ToList(), DocManager.Inst,
- (current, total) => {
- messageBox.SetText($"{name}: {current} / {total}");
- }, cancellationToken);
- },
- (Task t) => {
- var e = t.Exception;
- if (t.IsFaulted && e != null) {
- if (e != null) {
- Log.Error(e, $"Failed to run Editing Macro");
- var customEx = new MessageCustomizableException("Failed to run editing macro", "", e);
- DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(customEx));
- }
- return;
- }
- }
- );
- } else {
- edit.Run(NotesVm.Project, NotesVm.Part, NotesVm.Selection.ToList(),
- DocManager.Inst);
- }
- } catch (Exception e) {
- var customEx = new MessageCustomizableException("Failed to run editing macro", "", e);
- DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(customEx));
- }
-
- });
- ViewModel.NoteBatchEdits.AddRange(new List() {
- new LoadRenderedPitch(),
- new RefreshRealCurves(),
- new AddTailNote("-", "pianoroll.menu.notes.addtaildash"),
- new AddTailNote("R", "pianoroll.menu.notes.addtailrest"),
- new RemoveTailNote("-", "pianoroll.menu.notes.removetaildash"),
- new RemoveTailNote("R", "pianoroll.menu.notes.removetailrest"),
- new Transpose(12, "pianoroll.menu.notes.octaveup"),
- new Transpose(-12, "pianoroll.menu.notes.octavedown"),
- new AutoLegato(),
- new CommonnoteCopy(),
- new CommonnotePaste(),
- new FixOverlap(),
- new BakePitch(),
- new RandomizeTiming(),
- new RandomizePhonemeOffset()
- }.Select(edit => new MenuItemViewModel() {
- Header = ThemeManager.GetString(edit.Name),
- Command = noteBatchEditCommand,
- CommandParameter = edit,
- }));
- ViewModel.LyricBatchEdits.AddRange(new List() {
- new RomajiToHiragana(),
- new HiraganaToRomaji(),
- new JapaneseVCVtoCV(),
- new HanziToPinyin(),
- new RemoveToneSuffix(),
- new RemoveLetterSuffix(),
- new MoveSuffixToVoiceColor(),
- new RemovePhoneticHint(),
- new DashToPlus(),
- new DashToPlusTilda(),
- new InsertSlur(),
- }.Select(edit => new MenuItemViewModel() {
- Header = ThemeManager.GetString(edit.Name),
- Command = noteBatchEditCommand,
- CommandParameter = edit,
- }));
- ViewModel.ResetBatchEdits.AddRange(new List() {
- new ResetAll(),
- new ResetPitchBends(),
- new ResetAllExpressions(),
- new ClearVibratos(),
- new ResetVibratos(),
- new ClearTimings(),
- new ResetAliases(),
- }.Select(edit => new MenuItemViewModel() {
- Header = ThemeManager.GetString(edit.Name),
- Command = noteBatchEditCommand,
- CommandParameter = edit,
- }));
- try {
- ViewModel.ExternalBatchEdits.AddRange(
- DocManager.Inst.ExternalBatchEditTypes
- .Select(type => Activator.CreateInstance(type) as BatchEdit)
- .Where(edit => edit != null)
- .Select(edit => new MenuItemViewModel() {
- Header = ThemeManager.GetString(edit!.Name),
- Command = noteBatchEditCommand,
- CommandParameter = edit,
- })
- );
- } catch (Exception e) {
- Log.Error(e, "Failed to load external batch edits.");
- }
-
- DocManager.Inst.AddSubscriber(this);
-
- ViewModel.NoteBatchEdits.Insert(6, new MenuItemViewModel() {
- Header = ThemeManager.GetString("pianoroll.menu.notes.addbreath"),
- Command = ReactiveCommand.Create(() => {
- AddBreathNote();
- })
- });
- ViewModel.NoteBatchEdits.Insert(9, new MenuItemViewModel() {
- Header = ThemeManager.GetString("pianoroll.menu.notes.quantize"),
- Command = ReactiveCommand.Create(() => {
- QuantizeNotes();
- })
- });
- ViewModel.NoteBatchEdits.Add(new MenuItemViewModel() {
- Header = ThemeManager.GetString("pianoroll.menu.notes.randomizetuning"),
- Command = ReactiveCommand.Create(() => {
- RandomizeTuning();
- })
- });
- ViewModel.NoteBatchEdits.Add(new MenuItemViewModel() {
- Header = ThemeManager.GetString("pianoroll.menu.notes.lengthencrossfade"),
- Command = ReactiveCommand.Create(() => {
- LengthenCrossfade();
- })
- });
- ViewModel.LyricBatchEdits.Add(new MenuItemViewModel() {
- Header = ThemeManager.GetString("lyricsreplace.replace"),
- Command = ReactiveCommand.Create(() => {
- ReplaceLyrics();
- })
- });
- lyricsDialogCommand = ReactiveCommand.Create(() => {
- EditLyrics();
- });
- noteDefaultsCommand = ReactiveCommand.Create(() => {
- EditNoteDefaults();
- });
-
- AddHandler(KeyDownEvent, OnKeyDown, RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
-
- this.WhenAnyValue(x => x.ViewModel!.PlaybackViewModel!.PlayPosTick)
- .Subscribe(tick => {
- var notesVm = ViewModel?.NotesViewModel;
-
- if (notesVm?.Part == null) return;
- if (tick < notesVm.Part.position || tick >= notesVm.Part.End) {
- var targetPart = notesVm.Project.parts
- .OfType()
- .FirstOrDefault(p => p.trackNo == notesVm.Part.trackNo && p.position <= tick && p.End > tick);
-
- if (targetPart != null) {
- DocManager.Inst.ExecuteCmd(new LoadPartNotification(targetPart, notesVm.Project, tick));
- AttachExpressions();
- }
- }
- });
-
- DocManager.Inst.AddSubscriber(this);
- }
-
- void OnMenuClosed(object sender, RoutedEventArgs args) {
- Focus(); // Force unfocus menu for key down events.
- }
-
- void OnMenuPointerLeave(object sender, PointerEventArgs args) {
- Focus(); // Force unfocus menu for key down events.
- }
-
- // Edit menu
- void OnMenuLockPitchPoints(object sender, RoutedEventArgs args) {
- Preferences.Default.LockUnselectedNotesPitch = !Preferences.Default.LockUnselectedNotesPitch;
- Preferences.Save();
- ViewModel.RaisePropertyChanged(nameof(ViewModel.LockPitchPoints));
- }
- void OnMenuLockVibrato(object sender, RoutedEventArgs args) {
- Preferences.Default.LockUnselectedNotesVibrato = !Preferences.Default.LockUnselectedNotesVibrato;
- Preferences.Save();
- ViewModel.RaisePropertyChanged(nameof(ViewModel.LockVibrato));
- }
- void OnMenuLockExpressions(object sender, RoutedEventArgs args) {
- Preferences.Default.LockUnselectedNotesExpressions = !Preferences.Default.LockUnselectedNotesExpressions;
- Preferences.Save();
- ViewModel.RaisePropertyChanged(nameof(ViewModel.LockExpressions));
- }
-
- // View menu
- void OnMenuShowPortrait(object sender, RoutedEventArgs args) {
- Preferences.Default.ShowPortrait = !Preferences.Default.ShowPortrait;
- Preferences.Save();
- ViewModel.RaisePropertyChanged(nameof(ViewModel.ShowPortrait));
- MessageBus.Current.SendMessage(new PianorollRefreshEvent("Portrait"));
- }
- void OnMenuShowIcon(object sender, RoutedEventArgs args) {
- Preferences.Default.ShowIcon = !Preferences.Default.ShowIcon;
- Preferences.Save();
- ViewModel.RaisePropertyChanged(nameof(ViewModel.ShowIcon));
- MessageBus.Current.SendMessage(new PianorollRefreshEvent("Portrait"));
- }
- void OnMenuShowGhostNotes(object sender, RoutedEventArgs args) {
- Preferences.Default.ShowGhostNotes = !Preferences.Default.ShowGhostNotes;
- Preferences.Save();
- ViewModel.RaisePropertyChanged(nameof(ViewModel.ShowGhostNotes));
- MessageBus.Current.SendMessage(new PianorollRefreshEvent("Part"));
-
- }
- void OnMenuUseTrackColor(object sender, RoutedEventArgs args) {
- Preferences.Default.UseTrackColor = !Preferences.Default.UseTrackColor;
- Preferences.Save();
- ViewModel.RaisePropertyChanged(nameof(ViewModel.UseTrackColor));
- MessageBus.Current.SendMessage(new PianorollRefreshEvent("TrackColor"));
- }
- void OnMenuFullScreen(object sender, RoutedEventArgs args) {
- RootWindow.WindowState = RootWindow.WindowState == WindowState.FullScreen
- ? WindowState.Normal
- : WindowState.FullScreen;
- }
- void OnMenuDegreeStyle(object sender, RoutedEventArgs args) {
- if (sender is MenuItem menu && int.TryParse(menu.Tag?.ToString(), out int tag)) {
- Preferences.Default.DegreeStyle = tag;
- Preferences.Save();
- ViewModel.RaisePropertyChanged(nameof(ViewModel.DegreeStyle0));
- ViewModel.RaisePropertyChanged(nameof(ViewModel.DegreeStyle1));
- ViewModel.RaisePropertyChanged(nameof(ViewModel.DegreeStyle2));
- MessageBus.Current.SendMessage(new PianorollRefreshEvent("Part"));
- }
- }
- void OnMenuLockStartTime(object sender, RoutedEventArgs args) {
- if (sender is MenuItem menu && int.TryParse(menu.Tag?.ToString(), out int tag)) {
- Preferences.Default.LockStartTime = tag;
- Preferences.Save();
- ViewModel.RaisePropertyChanged(nameof(ViewModel.LockStartTime0));
- ViewModel.RaisePropertyChanged(nameof(ViewModel.LockStartTime1));
- ViewModel.RaisePropertyChanged(nameof(ViewModel.LockStartTime2));
- }
- }
- void OnMenuPlaybackAutoScroll(object sender, RoutedEventArgs args) {
- if (sender is MenuItem menu && int.TryParse(menu.Tag?.ToString(), out int tag)) {
- Preferences.Default.PlaybackAutoScroll = tag;
- Preferences.Save();
- ViewModel.RaisePropertyChanged(nameof(ViewModel.PlaybackAutoScroll0));
- ViewModel.RaisePropertyChanged(nameof(ViewModel.PlaybackAutoScroll1));
- ViewModel.RaisePropertyChanged(nameof(ViewModel.PlaybackAutoScroll2));
- }
- }
-
- async void OnMenuSingers(object sender, RoutedEventArgs args) {
- if (MainWindow != null) {
- await MainWindow.OpenSingersWindowAsync();
- }
- RootWindow.Activate();
- try {
- USinger? singer = null;
- UOto? oto = null;
- if (ViewModel.NotesViewModel.Part != null) {
- singer = ViewModel.NotesViewModel.Project.tracks[ViewModel.NotesViewModel.Part.trackNo].Singer;
- if (!ViewModel.NotesViewModel.Selection.IsEmpty && ViewModel.NotesViewModel.Part.phonemes.Count() > 0) {
- oto = ViewModel.NotesViewModel.Part.phonemes.First(p => p.Parent == ViewModel.NotesViewModel.Selection.First()).oto;
- }
- }
- DocManager.Inst.ExecuteCmd(new GotoOtoNotification(singer, oto));
- } catch { }
- }
-
- void OnMenuSearchNote(object sender, RoutedEventArgs args) {
- SearchNote();
- }
-
- void OnMenuDetachPianoRoll(object sender, RoutedEventArgs args) {
- MainWindow?.SetPianoRollAttachment();
- ViewModel.RaisePropertyChanged(nameof(ViewModel.PianoRollDetached));
- }
-
- void OnMenuHidePianoRoll(object sender, RoutedEventArgs args) {
- if (Preferences.Default.DetachPianoRoll && MainWindow != null) {
- MainWindow.SetPianoRollAttachment();
- }
- if (RootWindow.DataContext is MainWindowViewModel mwvm) {
- mwvm.ShowPianoRoll = false;
- } else {
- RootWindow.Hide();
- }
- }
-
- // Edit Tools
- private CancellationTokenSource? _longPressCts;
- private async void OnToolButtonPointerPressed(object? sender, PointerPressedEventArgs args) {
- if (!args.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
-
- if (sender is Control control) {
- _longPressCts = new CancellationTokenSource();
- try {
- await Task.Delay(500, _longPressCts.Token);
- if (_longPressCts != null && !_longPressCts.IsCancellationRequested) {
- FlyoutBase.ShowAttachedFlyout(control);
- }
- } catch {
- // don't open the flyout
- }
- }
- }
- private void OnToolButtonPointerReleased(object? sender, PointerReleasedEventArgs args) {
- _longPressCts?.Cancel();
- _longPressCts?.Dispose();
- _longPressCts = null;
- }
- void PenToolListBox_PointerReleased(object? sender, PointerReleasedEventArgs args) {
- FlyoutBase.GetAttachedFlyout(penTool)?.Hide(); ;
- SetPenToolIcon();
- }
- void SetPenToolIcon() {
- penTool.Classes.Remove("penTool");
- penTool.Classes.Remove("penPlusTool");
- penTool.Classes.Add(ViewModel.EditTool.PenToolVariation == 1 ? "penPlusTool" : "penTool");
- }
-
- void SearchNote() {
- if (ViewModel.NotesViewModel.Part == null || ViewModel.NotesViewModel.Part.notes.Count == 0) {
- return;
- }
- SearchBar.Show(ViewModel.NotesViewModel);
- }
-
- void ReplaceLyrics() {
- if (ViewModel.NotesViewModel.Part == null) {
- return;
- }
- if (ViewModel.NotesViewModel.Part.notes.Count < 1) {
- _ = MessageBox.Show(
- RootWindow,
- ThemeManager.GetString("lyrics.nonote"),
- ThemeManager.GetString("lyrics.caption"),
- MessageBox.MessageBoxButtons.Ok);
- return;
- }
-
- var notes = ViewModel.NotesViewModel.Selection.ToArray();
- if (notes.Length == 0) {
- notes = ViewModel.NotesViewModel.Part.notes.ToArray();
- }
- var vm = new LyricsReplaceViewModel(ViewModel.NotesViewModel.Part, notes);
- var dialog = new LyricsReplaceDialog() {
- DataContext = vm,
- };
- dialog.ShowDialog(RootWindow);
- }
-
- void OnMenuEditLyrics(object? sender, RoutedEventArgs e) {
- EditLyrics();
- }
-
- void EditLyrics() {
- if (ViewModel.NotesViewModel.Part == null) {
- return;
- }
- if (ViewModel.NotesViewModel.Part.notes.Count < 1) {
- _ = MessageBox.Show(
- RootWindow,
- ThemeManager.GetString("lyrics.nonote"),
- ThemeManager.GetString("lyrics.caption"),
- MessageBox.MessageBoxButtons.Ok);
- return;
- }
-
- var vm = new LyricsViewModel();
- var (notes, selection) = ViewModel.NotesViewModel.PrepareInsertLyrics();
- vm.Start(ViewModel.NotesViewModel.Part, notes, selection);
- var dialog = new LyricsDialog() {
- DataContext = vm,
- };
- dialog.ShowDialog(RootWindow);
- }
-
- void OnMenuNoteDefaults(object sender, RoutedEventArgs args) {
- EditNoteDefaults();
- }
-
- void EditNoteDefaults() {
- var dialog = new NoteDefaultsDialog();
- dialog.ShowDialog(RootWindow);
- if (dialog.Position.Y < 0) {
- dialog.Position = dialog.Position.WithY(0);
- }
- }
-
- void AddBreathNote() {
- var notesVM = ViewModel.NotesViewModel;
- if (notesVM.Part == null) {
- return;
- }
- if (notesVM.Selection.IsEmpty) {
- _ = MessageBox.Show(
- RootWindow,
- ThemeManager.GetString("lyrics.selectnotes"),
- ThemeManager.GetString("lyrics.caption"),
- MessageBox.MessageBoxButtons.Ok);
- return;
- }
- var dialog = new TypeInDialog() {
- Title = ThemeManager.GetString("pianoroll.menu.notes.addbreath"),
- onFinish = value => {
- if (!string.IsNullOrWhiteSpace(value)) {
- var edit = new Core.Editing.AddBreathNote(value);
- try {
- edit.Run(notesVM.Project, notesVM.Part, notesVM.Selection.ToList(), DocManager.Inst);
- } catch (Exception e) {
- var customEx = new MessageCustomizableException("Failed to run editing macro", "", e);
- DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(customEx));
- }
- }
- }
- };
- dialog.SetText("br");
- dialog.ShowDialog(RootWindow);
- }
-
- void QuantizeNotes() {
- var notesVM = ViewModel.NotesViewModel;
- if (notesVM.Part == null) {
- return;
- }
- var edit = new QuantizeNotes(notesVM.Project.resolution * 4 / notesVM.SnapDiv);
- edit.Run(notesVM.Project, notesVM.Part, notesVM.Selection.ToList(), DocManager.Inst);
- }
-
- void RandomizeTuning() {
- var notesVM = ViewModel.NotesViewModel;
- if (notesVM.Part == null) {
- return;
- }
- var dialog = new SliderDialog(ThemeManager.GetString("pianoroll.menu.notes.randomizetuning"), 20, 1, 100, 1);
- dialog.onFinish = value => {
- try {
- var edit = new RandomizeTuning((int)value);
- edit.Run(notesVM.Project, notesVM.Part, notesVM.Selection.ToList(), DocManager.Inst);
- } catch (Exception e) {
- var customEx = new MessageCustomizableException("Failed to run editing macro", "", e);
- DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(customEx));
- }
- };
- dialog.ShowDialog(RootWindow);
- }
-
- void LengthenCrossfade() {
- var notesVM = ViewModel.NotesViewModel;
- if (notesVM.Part == null) {
- return;
- }
- var dialog = new SliderDialog(ThemeManager.GetString("pianoroll.menu.notes.lengthencrossfade"), 0.5, 0, 1, 0.1);
- dialog.onFinish = value => {
- var edit = new Core.Editing.LengthenCrossfade(value);
- try {
- edit.Run(notesVM.Project, notesVM.Part, notesVM.Selection.ToList(), DocManager.Inst);
- } catch (Exception e) {
- var customEx = new MessageCustomizableException("Failed to run editing macro", "", e);
- DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(customEx));
- }
- };
- dialog.ShowDialog(RootWindow);
- }
-
- private void OnPianoRollFocus(object sender, GotFocusEventArgs e) {
- var input = e.Source as InputElement;
- if (input is TextBox or ComboBox or ComboBoxItem) {
- input.Focus();
- }
- }
-
- private void LyricBoxLostFocus(object sender, RoutedEventArgs e) {
- if (sender is InputElement { IsKeyboardFocusWithin: false }) {
- this.Focus();
- }
- }
-
- public void OnExpButtonClick(object sender, RoutedEventArgs args) {
- var notesVM = ViewModel.NotesViewModel;
- if (notesVM.Part == null) {
- return;
- }
- var dialog = new ExpressionsDialog() {
- DataContext = new ExpressionsViewModel(notesVM.Project.tracks[notesVM.Part.trackNo]),
- };
- dialog.ShowDialog(RootWindow);
- if (dialog.Position.Y < 0) {
- dialog.Position = dialog.Position.WithY(0);
- }
- }
-
- public void KeyboardPointerWheelChanged(object sender, PointerWheelEventArgs args) {
- LyricBox?.EndEdit();
- VScrollPointerWheelChanged(VScrollBar, args);
- }
-
- public void KeyboardPointerPressed(object sender, PointerPressedEventArgs args) {
- LyricBox?.EndEdit();
- if (keyboardPlayState != null) {
- return;
- }
- var element = (TrackBackground)sender;
- keyboardPlayState = new KeyboardPlayState(element, ViewModel);
- keyboardPlayState.Begin(args.Pointer, args.GetPosition(element));
- }
-
- public void KeyboardPointerMoved(object sender, PointerEventArgs args) {
- if (keyboardPlayState != null) {
- var element = (TrackBackground)sender;
- keyboardPlayState.Update(args.Pointer, args.GetPosition(element));
- }
- }
-
- public void KeyboardPointerReleased(object sender, PointerReleasedEventArgs args) {
- if (keyboardPlayState != null) {
- var element = (TrackBackground)sender;
- keyboardPlayState.End(args.Pointer, args.GetPosition(element));
- keyboardPlayState = null;
- }
- }
-
- public void HScrollPointerWheelChanged(object sender, PointerWheelEventArgs args) {
- var scrollbar = (ScrollBar)sender;
- scrollbar.Value = Math.Max(scrollbar.Minimum, Math.Min(scrollbar.Maximum, scrollbar.Value - scrollbar.SmallChange * args.Delta.Y));
- LyricBox?.EndEdit();
- }
-
- public void VScrollPointerWheelChanged(object sender, PointerWheelEventArgs args) {
- var scrollbar = (ScrollBar)sender;
- scrollbar.Value = Math.Max(scrollbar.Minimum, Math.Min(scrollbar.Maximum, scrollbar.Value - scrollbar.SmallChange * args.Delta.Y));
- LyricBox?.EndEdit();
- }
-
- public void TimelinePointerWheelChanged(object sender, PointerWheelEventArgs args) {
- var control = (Control)sender;
- var position = args.GetCurrentPoint((Visual)sender).Position;
- var size = control.Bounds.Size;
- position = position.WithX(position.X / size.Width).WithY(position.Y / size.Height);
- ViewModel.NotesViewModel.OnXZoomed(position, 0.1 * args.Delta.Y);
- LyricBox?.EndEdit();
- }
-
- public void ViewScalerPointerWheelChanged(object sender, PointerWheelEventArgs args) {
- ViewModel.NotesViewModel.OnYZoomed(new Point(0, 0.5), 0.1 * args.Delta.Y);
- LyricBox?.EndEdit();
- }
-
- public void TimelinePointerPressed(object sender, PointerPressedEventArgs args) {
- var control = (Control)sender;
- var point = args.GetCurrentPoint(control);
- if (point.Properties.IsLeftButtonPressed) {
- args.Pointer.Capture(control);
- ViewModel.NotesViewModel.PointToLineTick(point.Position, out int left, out int right);
- int tick = left + ViewModel.NotesViewModel.Part?.position ?? 0;
- ViewModel.PlaybackViewModel?.MovePlayPos(tick);
- } else if (point.Properties.IsRightButtonPressed) {
- args.Pointer.Capture(control);
- isSelectingRange = true;
- rangeSelectStartPoint = point.Position;
- LyricBox?.EndEdit();
- return;
- }
- LyricBox?.EndEdit();
- }
-
- public void TimelinePointerMoved(object sender, PointerEventArgs args) {
- var control = (Control)sender;
- var point = args.GetCurrentPoint(control);
- if (point.Properties.IsLeftButtonPressed) {
- ViewModel.NotesViewModel.PointToLineTick(point.Position, out int left, out int right);
- int tick = left + ViewModel.NotesViewModel.Part?.position ?? 0;
- ViewModel.PlaybackViewModel?.MovePlayPos(tick);
- } else if (point.Properties.IsRightButtonPressed && isSelectingRange) {
- double dx = Math.Abs(point.Position.X - rangeSelectStartPoint.X);
- if (dx >= RangeSelectThreshold) {
- UpdateRangeSelection(point.Position);
- }
- }
- }
-
- public void TimelinePointerReleased(object sender, PointerReleasedEventArgs args) {
- if (isSelectingRange && args.InitialPressMouseButton == MouseButton.Right) {
- isSelectingRange = false;
- var control = (Control)sender;
- var point = args.GetCurrentPoint(control);
- double dx = Math.Abs(point.Position.X - rangeSelectStartPoint.X);
- if (dx < RangeSelectThreshold) {
- DocManager.Inst.ExecuteCmd(new SetRangeSelectionNotification(0, 0));
- }
- args.Pointer.Capture(null);
- return;
- }
- args.Pointer.Capture(null);
- }
-
- public void TimelineDoubleTapped(object sender, TappedEventArgs args) {
- DocManager.Inst.ExecuteCmd(new SetRangeSelectionNotification(0, 0));
- }
-
- private void UpdateRangeSelection(Point currentPoint) {
- var notesVm = ViewModel.NotesViewModel;
- int partPos = notesVm.Part?.position ?? 0;
- notesVm.PointToLineTick(rangeSelectStartPoint, out int startLeft, out int startRight);
- notesVm.PointToLineTick(currentPoint, out int endLeft, out int endRight);
- int left = Math.Min(startLeft, endLeft);
- int right = Math.Max(startRight, endRight);
- DocManager.Inst.ExecuteCmd(new SetRangeSelectionNotification(left + partPos, right + partPos));
- }
-
- public void NotesCanvasPointerPressed(object sender, PointerPressedEventArgs args) {
- LyricBox?.EndEdit();
- if (ViewModel.NotesViewModel.Part == null) {
- return;
- }
- var control = (Control)sender;
- var point = args.GetCurrentPoint(control);
- if (editState != null) {
- // Finalize pitch curve in adjusting phase before starting a new edit
- if (editState is PitchCurveState pcs2 && pcs2.IsInAdjustingPhase) {
- if (point.Properties.IsLeftButtonPressed) {
- pcs2.Apply();
- pcs2.End(pointer: args.Pointer, point: point.Position);
- } else {
- // Right-click during adjusting: cancel the edit
- pcs2.Cancel(args.Pointer);
- }
- editState = null;
- } else {
- return;
- }
- }
- if (point.Properties.IsLeftButtonPressed) {
- NotesCanvasLeftPointerPressed(control, point, args);
- } else if (point.Properties.IsRightButtonPressed) {
- NotesCanvasRightPointerPressed(control, point, args);
- } else if (point.Properties.IsMiddleButtonPressed) {
- editState = new NotePanningState(control, ViewModel, this);
- Cursor = ViewConstants.cursorHand;
- }
- if (editState != null) {
- editState.Begin(point.Pointer, point.Position);
- editState.Update(point.Pointer, point.Position);
- }
- }
-
- private void NotesCanvasLeftPointerPressed(Control control, PointerPoint point, PointerPressedEventArgs args) {
- if (ViewModel.EditTool.IsPitchTool) {
- ViewModel.NotesViewModel.DeselectNotes();
- if (args.KeyModifiers != cmdKey) {
- bool overwrite = ViewModel.EditTool.OverwritePitch;
- EditTools tool = ViewModel.EditTool.CurrentTool;
- if (tool == EditTools.PitchSmoothenTool) {
- editState = new SmoothenPitchState(control, ViewModel, this, overwrite);
- } else if (tool == EditTools.DrawPitchTool) {
- editState = new DrawPitchState(control, ViewModel, this, overwrite);
- } else if (tool == EditTools.PitchLineTool) {
- editState = new PitchCurveState(control, ViewModel, this, PitchPreviewLine, PitchCurveState.CurveMode.Line, overwrite);
- } else if (tool == EditTools.PitchSCurveTool) {
- editState = new PitchCurveState(control, ViewModel, this, PitchPreviewLine, PitchCurveState.CurveMode.SCurve, overwrite);
- } else if (tool == EditTools.PitchSineWaveTool) {
- editState = new PitchCurveState(control, ViewModel, this, PitchPreviewLine, PitchCurveState.CurveMode.Sine, overwrite);
- }
- return;
- }
- }
- if (ViewModel.EditTool.CurrentTool == EditTools.EraserTool && args.KeyModifiers != cmdKey) {
- ViewModel.NotesViewModel.DeselectNotes();
- editState = new NoteEraseEditState(control, ViewModel, this, MouseButton.Left);
- Cursor = ViewConstants.cursorNo;
- return;
- }
- var pitHitInfo = ViewModel.NotesViewModel.HitTest.HitTestPitchPoint(point.Position);
- if (pitHitInfo.Note != null) {
- editState = new PitchPointEditState(control, ViewModel, this,
- pitHitInfo.Note, pitHitInfo.Index, pitHitInfo.OnPoint, pitHitInfo.X, pitHitInfo.Y);
- return;
- }
- var vbrHitInfo = ViewModel.NotesViewModel.HitTest.HitTestVibrato(point.Position);
- if (vbrHitInfo.hit) {
- if (vbrHitInfo.hitToggle) {
- ViewModel.NotesViewModel.ToggleVibrato(vbrHitInfo.note);
- return;
- }
- if (vbrHitInfo.hitStart) {
- editState = new VibratoChangeStartState(control, ViewModel, this, vbrHitInfo.note);
- return;
- }
- if (vbrHitInfo.hitIn) {
- editState = new VibratoChangeInState(control, ViewModel, this, vbrHitInfo.note);
- return;
- }
- if (vbrHitInfo.hitOut) {
- editState = new VibratoChangeOutState(control, ViewModel, this, vbrHitInfo.note);
- return;
- }
- if (vbrHitInfo.hitDepth) {
- editState = new VibratoChangeDepthState(control, ViewModel, this, vbrHitInfo.note);
- return;
- }
- if (vbrHitInfo.hitPeriod) {
- editState = new VibratoChangePeriodState(control, ViewModel, this, vbrHitInfo.note);
- return;
- }
- if (vbrHitInfo.hitShift) {
- editState = new VibratoChangeShiftState(
- control, ViewModel, this, vbrHitInfo.note, vbrHitInfo.point, vbrHitInfo.initialShift);
- return;
- }
- return;
- }
- var noteHitInfo = ViewModel.NotesViewModel.HitTest.HitTestNote(point.Position);
- if (noteHitInfo.hitBody) {
- if (noteHitInfo.hitResizeArea) {
- editState = new NoteResizeEditState(
- control, ViewModel, this, noteHitInfo.note,
- args.KeyModifiers == KeyModifiers.Alt,
- fromStart: noteHitInfo.hitResizeAreaFromStart);
- Cursor = ViewConstants.cursorSizeWE;
- } else if (args.KeyModifiers == cmdKey) {
- ViewModel.NotesViewModel.ToggleSelectNote(noteHitInfo.note);
- } else if (args.KeyModifiers == KeyModifiers.Shift) {
- ViewModel.NotesViewModel.SelectNotesUntil(noteHitInfo.note);
- } else if (ViewModel.EditTool.CurrentTool == EditTools.KnifeTool) {
- ViewModel.NotesViewModel.DeselectNotes();
- editState = new NoteSplitEditState(
- control, ViewModel, this, noteHitInfo.note);
- } else {
- editState = new NoteMoveEditState(control, ViewModel, this, noteHitInfo.note);
- Cursor = ViewConstants.cursorSizeAll;
- }
- return;
- }
- if (ViewModel.EditTool.CurrentTool == EditTools.CursorTool || args.KeyModifiers == cmdKey) {
- if (args.KeyModifiers == KeyModifiers.None) {
- // New selection.
- ViewModel.NotesViewModel.DeselectNotes();
- editState = new NoteSelectionEditState(control, ViewModel, this, SelectionBox);
- Cursor = ViewConstants.cursorCross;
- return;
- }
- if (args.KeyModifiers == cmdKey) {
- // Additional selection.
- editState = new NoteSelectionEditState(control, ViewModel, this, SelectionBox);
- Cursor = ViewConstants.cursorCross;
- return;
- }
- ViewModel.NotesViewModel.DeselectNotes();
- } else if (ViewModel.EditTool.IsMatch([EditTools.PenTool, EditTools.PenPlusTool])) {
- ViewModel.NotesViewModel.DeselectNotes();
- editState = new NoteDrawEditState(control, ViewModel, this, ViewModel.NotesViewModel.PlayTone);
- }
- }
-
- private void NotesCanvasRightPointerPressed(Control control, PointerPoint point, PointerPressedEventArgs args) {
- if (ViewModel.NotesContextMenuItems.Count > 0) {
- ViewModel.NotesContextMenuItems.Clear();
- }
- var selectedNotes = ViewModel.NotesViewModel.Selection.ToList();
- if (ViewModel.EditTool.IsPitchTool) {
- editState = new ResetPitchState(control, ViewModel, this);
- return;
- }
- if (ViewModel.NotesViewModel.ShowPitch) {
- var pitHitInfo = ViewModel.NotesViewModel.HitTest.HitTestPitchPoint(point.Position);
- if (pitHitInfo.Note != null) {
- var shapes = new List();
- var currentShape = pitHitInfo.Note.pitch.data[pitHitInfo.Index].shape;
- shapes.Add(new MenuItemViewModel(currentShape == PitchPointShape.io) {
- Header = ThemeManager.GetString("context.pitch.easeinout"),
- Command = ViewModel.PitEaseInOutCommand,
- CommandParameter = pitHitInfo,
- });
- shapes.Add(new MenuItemViewModel(currentShape == PitchPointShape.l) {
- Header = ThemeManager.GetString("context.pitch.linear"),
- Command = ViewModel.PitLinearCommand,
- CommandParameter = pitHitInfo,
- });
- shapes.Add(new MenuItemViewModel(currentShape == PitchPointShape.i) {
- Header = ThemeManager.GetString("context.pitch.easein"),
- Command = ViewModel.PitEaseInCommand,
- CommandParameter = pitHitInfo,
- });
- shapes.Add(new MenuItemViewModel(currentShape == PitchPointShape.o) {
- Header = ThemeManager.GetString("context.pitch.easeout"),
- Command = ViewModel.PitEaseOutCommand,
- CommandParameter = pitHitInfo,
- });
- shapes.Add(new MenuItemViewModel(currentShape == PitchPointShape.sp) {
- Header = ThemeManager.GetString("context.pitch.smooth"),
- Command = ViewModel.PitSplineCommand,
- CommandParameter = pitHitInfo,
- });
- ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
- Header = ThemeManager.GetString("context.pitch.shape"),
- Items = shapes,
- });
- if (pitHitInfo.OnPoint && pitHitInfo.Index == 0) {
- ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel(pitHitInfo.Note.pitch.snapFirst) {
- Header = ThemeManager.GetString("context.pitch.pointsnapprev"),
- Command = ViewModel.PitSnapCommand,
- CommandParameter = pitHitInfo,
- });
- }
- if (pitHitInfo.OnPoint && pitHitInfo.Index != 0 &&
- pitHitInfo.Index != pitHitInfo.Note.pitch.data.Count - 1) {
- ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
- Header = ThemeManager.GetString("context.pitch.pointdel"),
- Command = ViewModel.PitDelCommand,
- CommandParameter = pitHitInfo,
- });
- }
- if (!pitHitInfo.OnPoint) {
- ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
- Header = ThemeManager.GetString("context.pitch.pointadd"),
- Command = ViewModel.PitAddCommand,
- CommandParameter = pitHitInfo,
- });
- }
- shouldOpenNotesContextMenu = true;
- return;
- }
- }
- if (ViewModel.EditTool.IsMatch([EditTools.CursorTool, EditTools.PenTool, EditTools.KnifeTool])) {
- var hitInfo = ViewModel.NotesViewModel.HitTest.HitTestNote(point.Position);
- var vibHitInfo = ViewModel.NotesViewModel.HitTest.HitTestVibrato(point.Position);
- if ((hitInfo.hitBody && hitInfo.note != null) || vibHitInfo.hit) {
- if (hitInfo.note != null && !selectedNotes.Contains(hitInfo.note)) {
- ViewModel.NotesViewModel.DeselectNotes();
- ViewModel.NotesViewModel.SelectNote(hitInfo.note, false);
- }
- if (ViewModel.NotesViewModel.Selection.Count > 0) {
- ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
- Header = ThemeManager.GetString("context.note.copy"),
- Command = ViewModel.NoteCopyCommand,
- CommandParameter = hitInfo,
- InputGesture = new KeyGesture(Key.C, KeyModifiers.Control),
- });
- ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
- Header = ThemeManager.GetString("context.note.delete"),
- Command = ViewModel.NoteDeleteCommand,
- CommandParameter = hitInfo,
- InputGesture = new KeyGesture(Key.Delete),
- });
- ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
- Header = ThemeManager.GetString("context.note.pasteparameters"),
- Command = ReactiveCommand.Create(() => ViewModel.NotesViewModel.PasteSelectedParams(RootWindow)),
- InputGesture = new KeyGesture(Key.V, KeyModifiers.Alt),
- });
- ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
- Header = ThemeManager.GetString("pianoroll.menu.notes"),
- Items = ViewModel.NoteBatchEdits.ToArray(),
- });
- ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
- Header = ThemeManager.GetString("pianoroll.menu.lyrics"),
- Items = ViewModel.LyricBatchEdits.ToArray(),
- });
- ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
- Header = ThemeManager.GetString("pianoroll.menu.reset"),
- Items = ViewModel.ResetBatchEdits.ToArray(),
- });
- ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
- Header = ThemeManager.GetString("pianoroll.menu.part.legacypluginexp"),
- Items = ViewModel.LegacyPlugins.ToArray(),
- });
- ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
- Header = ThemeManager.GetString("pianoroll.menu.external"),
- Items = ViewModel.ExternalBatchEdits.ToArray(),
- });
- ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
- Header = ThemeManager.GetString("pianoroll.menu.lyrics.edit"),
- Command = lyricsDialogCommand,
- });
- ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
- Header = ThemeManager.GetString("pianoroll.menu.notedefaults"),
- Command = noteDefaultsCommand,
- });
- ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
- Header = ThemeManager.GetString("context.note.clearcache"),
- Command = ViewModel.ClearPhraseCacheCommand,
- });
- shouldOpenNotesContextMenu = true;
- return;
- }
- } else {
- ViewModel.NotesViewModel.DeselectNotes();
- }
- } else if (ViewModel.EditTool.IsMatch([EditTools.EraserTool, EditTools.PenPlusTool])) {
- ViewModel.NotesViewModel.DeselectNotes();
- editState = new NoteEraseEditState(control, ViewModel, this, MouseButton.Right);
- Cursor = ViewConstants.cursorNo;
- }
- }
-
- public void NotesCanvasPointerMoved(object sender, PointerEventArgs args) {
- var control = (Control)sender;
- var point = args.GetCurrentPoint(control);
- args.Handled = true;
- if (ValueTipCanvas != null) {
- valueTipPointerPosition = args.GetCurrentPoint(ValueTipCanvas!).Position;
- }
- if (editState != null) {
- editState.altShiftHeld = args.KeyModifiers == (KeyModifiers.Alt | KeyModifiers.Shift);
- editState.shiftHeld = args.KeyModifiers == KeyModifiers.Shift;
- editState.ctrlHeld = args.KeyModifiers == cmdKey;
- editState.altHeld = args.KeyModifiers == KeyModifiers.Alt;
- editState.Update(point.Pointer, point.Position);
- return;
- }
- if (ViewModel?.NotesViewModel?.HitTest == null) {
- return;
- }
- if (ViewModel.EditTool.IsMatch([EditTools.DrawPitchTool, EditTools.PitchLineTool, EditTools.PitchSCurveTool, EditTools.PitchSineWaveTool, EditTools.PitchSmoothenTool, EditTools.EraserTool]) && args.KeyModifiers != cmdKey) {
- Cursor = null;
- return;
- }
- var pitHitInfo = ViewModel.NotesViewModel.HitTest.HitTestPitchPoint(point.Position);
- if (pitHitInfo.Note != null) {
- Cursor = ViewConstants.cursorHand;
- return;
- }
- var vbrHitInfo = ViewModel.NotesViewModel.HitTest.HitTestVibrato(point.Position);
- if (vbrHitInfo.hit) {
- if (vbrHitInfo.hitDepth) {
- Cursor = ViewConstants.cursorSizeNS;
- } else if (vbrHitInfo.hitPeriod) {
- Cursor = ViewConstants.cursorSizeWE;
- } else {
- Cursor = ViewConstants.cursorHand;
- }
- return;
- }
- var noteHitInfo = ViewModel.NotesViewModel.HitTest.HitTestNote(point.Position);
- if (noteHitInfo.hitResizeArea) {
- Cursor = ViewConstants.cursorSizeWE;
- return;
- }
- if (!noteHitInfo.hitBody && (ViewModel.EditTool.CurrentTool == EditTools.CursorTool || args.KeyModifiers == cmdKey)) {
- Cursor = ViewConstants.cursorCross;
- return;
- }
- Cursor = null;
- }
-
- public void NotesCanvasPointerReleased(object sender, PointerReleasedEventArgs args) {
- if (editState == null) {
- return;
- }
- if (editState.MouseButton != args.InitialPressMouseButton) {
- return;
- }
- var control = (Control)sender;
- var point = args.GetCurrentPoint(control);
- editState.shiftHeld = args.KeyModifiers == KeyModifiers.Shift;
- editState.ctrlHeld = args.KeyModifiers == cmdKey;
- editState.altHeld = args.KeyModifiers == KeyModifiers.Alt;
- editState.Update(point.Pointer, point.Position);
-
- // Two-phase handling for S-curve and Sine wave tools:
- // On first mouse up, transition to adjusting phase instead of ending.
- if (editState is PitchCurveState pcs) {
- if (pcs.Mode == PitchCurveState.CurveMode.Line) {
- // Line tool: apply immediately on mouse up
- pcs.Apply();
- pcs.End(pointer: args.Pointer, point: point.Position);
- } else {
- // S-curve / Sine: transition to adjusting phase, keep pointer captured
- if (!pcs.TransitionToAdjusting(point.Position)) {
- // TransitionToAdjusting returned false (click without drag) — already cancelled
- editState = null;
- return;
- }
- return;
- }
- } else {
- editState.End(point.Pointer, point.Position);
- }
- editState = null;
- Cursor = null;
- }
-
- public void NotesCanvasDoubleTapped(object sender, TappedEventArgs args) {
- if (!(sender is Control control)) {
- return;
- }
- var point = args.GetPosition(control);
- if (editState != null) {
- editState.End(args.Pointer, point);
- editState = null;
- Cursor = null;
- }
- var noteHitInfo = ViewModel.NotesViewModel.HitTest.HitTestNote(point);
- if (noteHitInfo.hitBody && ViewModel?.NotesViewModel?.Part != null) {
- var note = noteHitInfo.note;
- LyricBox?.Show(ViewModel.NotesViewModel.Part, new LyricBoxNote(note), note.lyric);
- }
- }
-
- public void NotesCanvasPointerWheelChanged(object sender, PointerWheelEventArgs args) {
- LyricBox?.EndEdit();
- var control = (Control)sender;
- var position = args.GetCurrentPoint(control).Position;
- var size = control.Bounds.Size;
- var delta = args.Delta;
- if (args.KeyModifiers == KeyModifiers.None || args.KeyModifiers == KeyModifiers.Shift) {
- if (args.KeyModifiers == KeyModifiers.Shift) {
- delta = new Vector(delta.Y, delta.X);
- }
- if (delta.X != 0) {
- HScrollBar.Value = Math.Max(HScrollBar.Minimum,
- Math.Min(HScrollBar.Maximum, HScrollBar.Value - HScrollBar.SmallChange * delta.X));
- }
- if (delta.Y != 0) {
- VScrollBar.Value = Math.Max(VScrollBar.Minimum,
- Math.Min(VScrollBar.Maximum, VScrollBar.Value - VScrollBar.SmallChange * delta.Y));
- }
- } else if (args.KeyModifiers == KeyModifiers.Alt) {
- position = position.WithX(position.X / size.Width).WithY(position.Y / size.Height);
- ViewModel.NotesViewModel.OnYZoomed(position, 0.1 * args.Delta.Y);
- } else if (args.KeyModifiers == cmdKey) {
- TimelinePointerWheelChanged(TimelineCanvas, args);
- }
- if (editState != null) {
- var point = args.GetCurrentPoint(editState.control);
- editState.Update(point.Pointer, point.Position);
- }
- }
-
- public void NotesContextMenuOpening(object sender, CancelEventArgs args) {
- if (shouldOpenNotesContextMenu) {
- shouldOpenNotesContextMenu = false;
- } else {
- args.Cancel = true;
- }
- }
-
- public void ExpCanvasPointerPressed(object sender, PointerPressedEventArgs args) {
- LyricBox?.EndEdit();
- var notesVm = ViewModel.NotesViewModel;
- if (notesVm.Part == null) {
- return;
- }
- var control = (Control)sender;
- var point = args.GetCurrentPoint(control);
- if (editState != null) {
- return;
- }
- var track = notesVm.Project.tracks[notesVm.Part.trackNo];
- if (!track.TryGetExpDescriptor(notesVm.Project, notesVm.PrimaryKey, out var descriptor)) {
- return;
- }
- if (point.Properties.IsLeftButtonPressed) {
- if (descriptor.type == UExpressionType.Curve) {
- switch (ViewModel.CurveViewModel.CurveTool) {
- case CurveTools.CurveSelectTool:
- editState = new CurveSelectionState(control, ViewModel, this, descriptor);
- break;
- case CurveTools.CurvePenTool:
- ViewModel.CurveViewModel.ClearSelect();
- editState = new ExpSetValueState(control, ViewModel, this, descriptor);
- break;
- case CurveTools.CurveEraserTool:
- ViewModel.CurveViewModel.ClearSelect();
- editState = new ExpResetValueState(control, ViewModel, this, descriptor, MouseButton.Left);
- break;
- default:
- ViewModel.CurveViewModel.ClearSelect();
- break;
- }
- } else {
- editState = new ExpSetValueState(control, ViewModel, this, descriptor);
- }
- Cursor = null;
- } else if (point.Properties.IsRightButtonPressed) {
- if (descriptor.type == UExpressionType.Curve && ViewModel.CurveViewModel.CurveTool == CurveTools.CurveSelectTool) {
- ViewModel.CurveViewModel.ClearSelect();
- } else {
- ViewModel.CurveViewModel.ClearSelect();
- editState = new ExpResetValueState(control, ViewModel, this, descriptor);
- }
- Cursor = ViewConstants.cursorNo;
- }
- if (editState != null) {
- editState.ctrlShiftHeld = args.KeyModifiers == (cmdKey | KeyModifiers.Shift);
- editState.shiftHeld = args.KeyModifiers == KeyModifiers.Shift;
- editState.Begin(point.Pointer, point.Position);
- editState.Update(point.Pointer, point.Position);
- }
- }
-
- public void ExpCanvasPointerMoved(object sender, PointerEventArgs args) {
- var control = (Control)sender;
- var point = args.GetCurrentPoint(control);
- args.Handled = true;
- if (ValueTipCanvas != null) {
- valueTipPointerPosition = args.GetCurrentPoint(ValueTipCanvas!).Position;
- }
- if (editState != null) {
- editState.ctrlShiftHeld = args.KeyModifiers == (cmdKey | KeyModifiers.Shift);
- editState.shiftHeld = args.KeyModifiers == KeyModifiers.Shift;
- editState.Update(point.Pointer, point.Position);
- } else {
- Cursor = null;
- }
- }
-
- public void ExpCanvasPointerReleased(object sender, PointerReleasedEventArgs args) {
- if (editState == null) {
- return;
- }
- if (editState.MouseButton != args.InitialPressMouseButton) {
- return;
- }
- var control = (Control)sender;
- var point = args.GetCurrentPoint(control);
- editState.ctrlShiftHeld = args.KeyModifiers == (cmdKey | KeyModifiers.Shift);
- editState.shiftHeld = args.KeyModifiers == KeyModifiers.Shift;
- editState.Update(point.Pointer, point.Position);
- editState.End(point.Pointer, point.Position);
- editState = null;
- Cursor = null;
- }
-
- public void PhonemeCanvasDoubleTapped(object sender, TappedEventArgs args) {
- if (ViewModel?.NotesViewModel?.Part == null) {
- return;
- }
- if (sender is not Control control) {
- return;
- }
- var point = args.GetPosition(control);
- if (editState != null) {
- editState.End(args.Pointer, point);
- editState = null;
- Cursor = null;
- }
- var hitInfoAlias = ViewModel.NotesViewModel.HitTest.HitTestAlias(point);
- var phoneme = hitInfoAlias.phoneme;
- Log.Debug($"PhonemeCanvasDoubleTapped, hit = {hitInfoAlias.hit}, point = {{{hitInfoAlias.point}}}, phoneme = {phoneme?.phoneme}");
- if (hitInfoAlias.hit) {
- LyricBox?.Show(ViewModel.NotesViewModel.Part, new LyricBoxPhoneme(phoneme!), phoneme!.phoneme);
- return;
- }
- }
-
- public async void PhonemeCanvasPointerPressed(object sender, PointerPressedEventArgs args) {
- LyricBox?.EndEdit();
- if (ViewModel?.NotesViewModel?.Part == null) {
- return;
- }
- var control = (Control)sender;
- var point = args.GetCurrentPoint(control);
- if (editState != null) {
- return;
- }
- if (point.Properties.IsLeftButtonPressed) {
- if (args.KeyModifiers == cmdKey) {
- var hitAliasInfo = ViewModel.NotesViewModel.HitTest.HitTestAlias(args.GetPosition(control));
- if (hitAliasInfo.hit) {
- var singer = ViewModel.NotesViewModel.Project.tracks[ViewModel.NotesViewModel.Part.trackNo].Singer;
- if (Preferences.Default.OtoEditor == 1 && !string.IsNullOrEmpty(Preferences.Default.VLabelerPath)) {
- Integrations.VLabelerClient.Inst.GotoOto(singer, hitAliasInfo.phoneme.oto);
- } else {
- if (MainWindow != null) {
- await MainWindow.OpenSingersWindowAsync();
- }
- RootWindow.Activate();
- DocManager.Inst.ExecuteCmd(new GotoOtoNotification(singer, hitAliasInfo.phoneme.oto));
- }
- return;
- }
- }
- // Plain click on errored phoneme alias shows error details
- var clickAliasInfo = ViewModel.NotesViewModel.HitTest.HitTestAlias(args.GetPosition(control));
- if (clickAliasInfo.hit && clickAliasInfo.phoneme.Error && clickAliasInfo.phoneme.ErrorException != null) {
- _ = MessageBox.ShowError(RootWindow, clickAliasInfo.phoneme.ErrorException);
- return;
- }
- var hitInfo = ViewModel.NotesViewModel.HitTest.HitTestPhoneme(point.Position);
- if (hitInfo.hit) {
- var phoneme = hitInfo.phoneme;
- var note = phoneme.Parent;
- var index = phoneme.index;
- if (hitInfo.hitPosition) {
- editState = new PhonemeMoveState(
- control, ViewModel, this, note.Extends ?? note, phoneme, index);
- } else if (hitInfo.hitPreutter) {
- editState = new PhonemeChangePreutterState(
- control, ViewModel, this, note.Extends ?? note, phoneme, index);
- } else if (hitInfo.hitOverlap) {
- if (phoneme.Next == null || !phoneme.Next.adjacent) {
- return;
- }
- phoneme = hitInfo.phoneme.Next;
- note = phoneme.Parent;
- index = phoneme.index;
- editState = new PhonemeChangeOverlapState(
- control, ViewModel, this, note.Extends ?? note, phoneme, index);
- } else if (hitInfo.hitAttackTime) {
- editState = new PhonemeChangeAttackTimeState(
- control, ViewModel, this, note.Extends ?? note, phoneme, index);
- } else if (hitInfo.hitReleaseTime) {
- editState = new PhonemeChangeReleaseTimeState(
- control, ViewModel, this, note.Extends ?? note, phoneme, index);
- }
- }
- } else if (point.Properties.IsRightButtonPressed) {
- editState = new PhonemeResetState(control, ViewModel, this);
- Cursor = ViewConstants.cursorNo;
- }
- if (editState != null) {
- editState.Begin(point.Pointer, point.Position);
- editState.Update(point.Pointer, point.Position);
- }
- }
-
- public void PhonemeCanvasPointerMoved(object sender, PointerEventArgs args) {
- args.Handled = true;
- if (ViewModel?.NotesViewModel?.Part == null) {
- return;
- }
- if (ValueTipCanvas != null) {
- valueTipPointerPosition = args.GetCurrentPoint(ValueTipCanvas!).Position;
- }
- var control = (Control)sender;
- var point = args.GetCurrentPoint(control);
- if (editState != null) {
- editState.Update(point.Pointer, point.Position);
- return;
- }
- var aliasHitInfo = ViewModel.NotesViewModel.HitTest.HitTestAlias(point.Position);
- if (aliasHitInfo.hit) {
- ViewModel.MouseoverPhoneme(aliasHitInfo.phoneme);
- Cursor = null;
- return;
- }
- var hitInfo = ViewModel.NotesViewModel.HitTest.HitTestPhoneme(point.Position);
- var adjacent = hitInfo.phoneme != null && hitInfo.phoneme.Next != null && hitInfo.phoneme.Next.adjacent;
- if (hitInfo.hitPosition || hitInfo.hitPreutter || (hitInfo.hitOverlap && adjacent) || hitInfo.hitAttackTime || hitInfo.hitReleaseTime) {
- Cursor = ViewConstants.cursorSizeWE;
- ViewModel.MouseoverPhoneme(null);
- return;
- }
- ViewModel.MouseoverPhoneme(null);
- Cursor = null;
- }
-
- public void PhonemeCanvasPointerReleased(object sender, PointerReleasedEventArgs args) {
- if (editState == null) {
- return;
- }
- if (editState.MouseButton != args.InitialPressMouseButton) {
- return;
- }
- var control = (Control)sender;
- var point = args.GetCurrentPoint(control);
- editState.Update(point.Pointer, point.Position);
- editState.End(point.Pointer, point.Position);
- editState = null;
- Cursor = null;
- }
-
- public void BackgroundPointerMoved(object sender, PointerEventArgs args) {
- Cursor = null;
- args.Handled = true;
- }
-
- public void OnSnapDivMenuButton(object sender, RoutedEventArgs args) {
- SnapDivMenu.PlacementTarget = sender as Button;
- SnapDivMenu.Open();
- }
-
- void OnSnapDivKeyDown(object sender, KeyEventArgs e) {
- if (e.Key == Key.Enter && e.KeyModifiers == KeyModifiers.None) {
- if (sender is ContextMenu menu && menu.SelectedItem is MenuItemViewModel item) {
- item.Command?.Execute(item.CommandParameter);
- }
- }
- }
-
- public void OnKeyMenuButton(object sender, RoutedEventArgs args) {
- KeyMenu.PlacementTarget = sender as Button;
- KeyMenu.Open();
- }
-
- bool MoveToNextPart(bool next) {
- var notesVm = ViewModel.NotesViewModel;
- var playVm = ViewModel.PlaybackViewModel;
- if (notesVm?.Part == null || playVm == null) {
- return false;
- }
- // tick is the center of NotesCanvas
- var tick = (int)(notesVm.TickOffset + notesVm.Bounds.Width / notesVm.TickWidth / 2 + notesVm.Part.position);
- var parts = notesVm.Project.parts
- .Where(part => part is UVoicePart && part.position <= tick && tick <= part.End)
- .OfType()
- .OrderBy(part => part.trackNo)
- .ThenBy(part => part.position)
- .ToArray();
- if (parts.Length == 0) {
- return false;
- }
- var index = Array.IndexOf(parts, notesVm.Part);
- index = next ? index + 1 : index - 1;
- if (parts.Length <= index) {
- index = 0;
- } else if (index < 0) {
- index = parts.Length - 1;
- }
- DocManager.Inst.ExecuteCmd(new LoadPartNotification(parts[index], notesVm.Project, tick));
- AttachExpressions();
- return true;
- }
-
- void OnKeyKeyDown(object sender, KeyEventArgs e) {
- if (e.Key == Key.Enter && e.KeyModifiers == KeyModifiers.None) {
- if (sender is ContextMenu menu && menu.SelectedItem is MenuItemViewModel item) {
- item.Command?.Execute(item.CommandParameter);
- }
- }
- }
-
- #region value tip
-
- void IValueTip.ShowValueTip() {
- if (ValueTip != null) {
- ValueTip.IsVisible = true;
- }
- }
-
- void IValueTip.HideValueTip() {
- if (ValueTip != null) {
- ValueTip.IsVisible = false;
- }
- if (ValueTipText != null) {
- ValueTipText.Text = string.Empty;
- }
- }
-
- void IValueTip.UpdateValueTip(string text) {
- if (ValueTip == null || ValueTipText == null || ValueTipCanvas == null) {
- return;
- }
- ValueTipText.Text = text;
- Canvas.SetLeft(ValueTip, valueTipPointerPosition.X);
- double tipY = valueTipPointerPosition.Y + 21;
- if (tipY + 21 > ValueTipCanvas!.Bounds.Height) {
- tipY = tipY - 42;
- }
- Canvas.SetTop(ValueTip, tipY);
- }
-
- #endregion
-
- void OnKeyDown(object? sender, KeyEventArgs args) {
- var notesVm = ViewModel.NotesViewModel;
- if (notesVm.Part == null) {
- args.Handled = false;
- return;
- }
-
- if (RootWindow.FocusManager != null) {
- if (RootWindow.FocusManager.GetFocusedElement() is TextBox focusedTextBox) {
- if (focusedTextBox.IsEnabled && focusedTextBox.IsEffectivelyVisible && focusedTextBox.IsFocused) {
- args.Handled = false;
- return;
- }
- } else if (RootWindow.FocusManager.GetFocusedElement() is ComboBox or ComboBoxItem) {
- args.Handled = false;
- return;
- }
- }
- if (LyricBox.IsVisible) {
- args.Handled = false;
- return;
- }
-
- if (args.Key == Key.R && args.KeyModifiers == KeyModifiers.Control) {
- var project = DocManager.Inst.Project;
- var part = notesVm.Part;
- var selectedNotes = notesVm.Selection.ToList();
-
- if (part != null && selectedNotes.Count > 0) {
- noteBatchEditCommand?.Execute(new LoadRenderedPitch()).Subscribe();
- }
-
- args.Handled = true;
- return;
- }
-
- // returns true if handled
- args.Handled = OnKeyExtendedHandler(args);
- }
-
- bool OnKeyExtendedHandler(KeyEventArgs args) {
- var notesVm = ViewModel.NotesViewModel;
- var playVm = ViewModel.PlaybackViewModel;
- var curveVm = ViewModel.CurveViewModel;
- if (notesVm?.Part == null || playVm == null || curveVm == null) {
- return false;
- }
- var project = DocManager.Inst.Project;
- int snapUnit = project.resolution * 4 / notesVm.SnapDiv;
- int deltaTicks = notesVm.IsSnapOn ? snapUnit : 15;
-
- bool isNone = args.KeyModifiers == KeyModifiers.None;
- bool isAlt = args.KeyModifiers == KeyModifiers.Alt;
- bool isCtrl = args.KeyModifiers == cmdKey;
- bool isShift = args.KeyModifiers == KeyModifiers.Shift;
- bool isBoth = args.KeyModifiers == (cmdKey | KeyModifiers.Shift);
-
- if (PluginMenu.IsSubMenuOpen && isNone) {
- if (ViewModel.LegacyPluginShortcuts.ContainsKey(args.Key)) {
- var plugin = ViewModel.LegacyPluginShortcuts[args.Key];
- if (plugin != null && plugin.Command != null) {
- plugin.Command.Execute(plugin.CommandParameter);
- }
- }
- return true;
- }
-
- #region tool select keys
- if (isNone) {
- switch (args.Key) {
- case Key.D1: ViewModel.ToolIndex = 0; return true;
- case Key.D2: ViewModel.ToolIndex = 1; return true;
- case Key.D3: ViewModel.ToolIndex = 2; return true;
- case Key.D4: ViewModel.ToolIndex = 3; return true;
- }
- }
- if (isShift) {
- switch (args.Key) {
- case Key.D1: ViewModel.ToolIndex = 4; return true;
- case Key.D2: ViewModel.ToolIndex = 5; return true;
- case Key.D3: ViewModel.ToolIndex = 6; return true;
- case Key.D4: ViewModel.ToolIndex = 7; return true;
- case Key.D5: ViewModel.ToolIndex = 8; return true;
- }
- }
- if (isAlt) {
- switch (args.Key) {
- case Key.D1: expSelector1?.SelectExp(); return true;
- case Key.D2: expSelector2?.SelectExp(); return true;
- case Key.D3: expSelector3?.SelectExp(); return true;
- case Key.D4: expSelector4?.SelectExp(); return true;
- case Key.D5: expSelector5?.SelectExp(); return true;
- case Key.D6: expSelector6?.SelectExp(); return true;
- case Key.D7: expSelector7?.SelectExp(); return true;
- case Key.D8: expSelector8?.SelectExp(); return true;
- case Key.D9: expSelector9?.SelectExp(); return true;
- case Key.D0: expSelector10?.SelectExp(); return true;
- }
- }
- #endregion
- switch (args.Key) {
- #region document keys
- case Key.Space:
- if (isNone) {
- playVm.PlayOrPause();
- return true;
- }
- if (isAlt) {
- if (!notesVm.Selection.IsEmpty) {
- playVm.PlayOrPause(
- tick: notesVm.Part.position + notesVm.Selection.FirstOrDefault()!.position,
- endTick: notesVm.Part.position + notesVm.Selection.LastOrDefault()!.RightBound
- );
- }
- return true;
- }
- break;
- case Key.Escape:
- if (isNone) {
- // collapse/empty selection
- var numSelected = notesVm.Selection.Count;
- // if single or all notes then clear
- if (numSelected == 1 || numSelected == notesVm.Part.notes.Count) {
- notesVm.DeselectNotes();
- } else if (numSelected > 1) {
- // collapse selection
- notesVm.SelectNote(notesVm.Selection.Head!);
- }
- return true;
- }
- break;
- case Key.F4:
- if (isAlt) {
- if (RootWindow is PianoRollDetachedWindow) {
- RootWindow.Hide();
- }
- return true;
- }
- break;
- case Key.F11:
- OnMenuFullScreen(this, new RoutedEventArgs());
- return true;
- case Key.Enter:
- if (isNone) {
- if (notesVm.Selection.Count == 1) {
- var note = notesVm.Selection.First();
- LyricBox?.Show(ViewModel.NotesViewModel.Part!, new LyricBoxNote(note), note.lyric);
- } else if (notesVm.Selection.Count > 1) {
- EditLyrics();
- }
- return true;
- }
- break;
- #endregion
- #region toggle show keyws
- case Key.R:
- if (isNone) {
- notesVm.ShowFinalPitch = !notesVm.ShowFinalPitch;
- return true;
- }
- break;
- case Key.T:
- if (isNone) {
- notesVm.ShowTips = !notesVm.ShowTips;
- return true;
- }
- break;
- case Key.U:
- if (isNone) {
- notesVm.ShowVibrato = !notesVm.ShowVibrato;
- return true;
- }
- if (isCtrl) {
- notesVm.MergeSelectedNotes();
- return true;
- }
- break;
- case Key.I:
- if (isNone) {
- notesVm.ShowPitch = !notesVm.ShowPitch;
- return true;
- }
- break;
- case Key.O:
- if (isNone) {
- notesVm.ShowPhoneme = !notesVm.ShowPhoneme;
- return true;
- }
- break;
- case Key.L:
- if (isNone) {
- notesVm.ShowExpressions = !notesVm.ShowExpressions;
- return true;
- }
- break;
- case Key.P:
- if (isNone) {
- notesVm.IsSnapOn = !notesVm.IsSnapOn;
- return true;
- }
- if (isAlt) {
- SnapDivMenu.Open();
- return true;
- }
- break;
- case Key.OemPipe:
- if (isNone) {
- notesVm.ShowNoteParams = !notesVm.ShowNoteParams;
- return true;
- }
- break;
- #endregion
- #region navigate keys
- // NAVIGATE/EDIT/SELECT HANDLERS
- case Key.Up:
- if (isNone) {
- notesVm.TransposeSelection(1);
- return true;
- }
- if (isCtrl) {
- notesVm.TransposeSelection(12);
- return true;
- }
- break;
- case Key.Down:
- if (isNone) {
- notesVm.TransposeSelection(-1);
- return true;
- }
- if (isCtrl) {
- notesVm.TransposeSelection(-12);
- return true;
- }
- break;
- case Key.Left:
- if (isNone) {
- notesVm.MoveCursor(-1);
- return true;
- }
- if (isAlt) {
- notesVm.ResizeSelectedNotes(-1 * deltaTicks);
- return true;
- }
- if (isCtrl) {
- notesVm.MoveSelectedNotes(-1 * deltaTicks);
- return true;
- }
- if (isShift) {
- notesVm.ExtendSelection(-1);
- return true;
- }
- break;
- case Key.Right:
- if (isNone) {
- notesVm.MoveCursor(1);
- return true;
- }
- if (isAlt) {
- notesVm.ResizeSelectedNotes(deltaTicks);
- return true;
- }
- if (isCtrl) {
- notesVm.MoveSelectedNotes(deltaTicks);
- return true;
- }
- if (isShift) {
- notesVm.ExtendSelection(1);
- return true;
- }
- break;
- case Key.OemPlus:
- if (isNone) {
- notesVm.ResizeSelectedNotes(deltaTicks);
- return true;
- }
- break;
- case Key.OemMinus:
- if (isNone) {
- notesVm.ResizeSelectedNotes(-1 * deltaTicks);
- return true;
- }
- break;
- #endregion
- #region clipboard and edit keys
- case Key.Z:
- if (isBoth) {
- ViewModel.Redo();
- return true;
- }
- if (isCtrl) {
- ViewModel.Undo();
- return true;
- }
- break;
- case Key.Y:
- // toggle play tone
- if (isNone) {
- notesVm.PlayTone = !notesVm.PlayTone;
- return true;
- }
- if (isCtrl) {
- ViewModel.Redo();
- return true;
- }
- break;
- case Key.C:
- if (isCtrl) {
- if (curveVm.IsSelected(notesVm.PrimaryKey)) {
- curveVm.Copy(notesVm.Part);
- } else {
- notesVm.CopyNotes();
- }
- return true;
- }
- break;
- case Key.X:
- if (isCtrl) {
- if (curveVm.IsSelected(notesVm.PrimaryKey)) {
- curveVm.Cut(notesVm.Part);
- } else {
- notesVm.CutNotes();
- }
- return true;
- }
- break;
- case Key.V:
- if (isBoth) {
- notesVm.PastePlainNotes();
- return true;
- }
- if (isCtrl) {
- if (DocManager.Inst.NotesClipboard != null && DocManager.Inst.NotesClipboard.Count > 0) {
- notesVm.PasteNotes();
- } else if (DocManager.Inst.CurvesClipboard != null) {
- var track = project.tracks[notesVm.Part.trackNo];
- if (track.TryGetExpDescriptor(project, notesVm.PrimaryKey, out var descriptor)) {
- curveVm.Paste(notesVm.Part, descriptor);
- }
- }
- return true;
- }
- if (isAlt) {
- notesVm.PasteSelectedParams(RootWindow);
- return true;
- }
- break;
- case Key.N:
- if (isNone && PluginMenu.Parent is MenuItem batch) {
- batch.Open();
- PluginMenu.Open();
- return true;
- }
- break;
- // INSERT + DELETE
- case Key.Insert:
- if (isNone) {
- notesVm.InsertNote();
- return true;
- }
- break;
- case Key.Delete:
- case Key.Back:
- if (isNone) {
- notesVm.DeleteSelectedNotes();
- return true;
- }
- break;
- #endregion
- #region play position and select keys
- // PLAY POSITION + SELECTION
- case Key.Home:
- if (isNone) {
- playVm.MovePlayPos(notesVm.Part.position);
- return true;
- }
- if (isShift) {
- var first = notesVm.Part.notes.FirstOrDefault();
- if (first != null) {
- notesVm.ExtendSelection(first);
- }
- return true;
- }
- break;
- case Key.End:
- if (isNone) {
- playVm.MovePlayPos(notesVm.Part.End);
- HScrollBar.Value = HScrollBar.Maximum;
- return true;
- }
- if (isShift) {
- var last = notesVm.Part.notes.LastOrDefault();
- if (last != null) {
- notesVm.ExtendSelection(last);
- }
- return true;
- }
- break;
- case Key.OemOpenBrackets:
- // move playhead left
- if (isNone) {
- playVm.MovePlayPos(playVm.PlayPosTick - snapUnit);
- return true;
- }
- // to selection start
- if (isCtrl) {
- if (!notesVm.Selection.IsEmpty) {
- playVm.MovePlayPos(notesVm.Part.position + notesVm.Selection.FirstOrDefault()!.position);
- }
- return true;
- }
- // to view start
- if (isShift) {
- playVm.MovePlayPos(notesVm.Part.position + (int)notesVm.TickOffset);
- return true;
- }
- break;
- case Key.OemCloseBrackets:
- // move playhead right
- if (isNone) {
- playVm.MovePlayPos(playVm.PlayPosTick + snapUnit);
- return true;
- }
- // to selection end
- if (isCtrl) {
- if (!notesVm.Selection.IsEmpty) {
- playVm.MovePlayPos(notesVm.Part.position + notesVm.Selection.LastOrDefault()!.RightBound);
- }
- return true;
- }
- // to view end
- if (isShift) {
- playVm.MovePlayPos(notesVm.Part.position + (int)(notesVm.TickOffset + notesVm.Bounds.Width / notesVm.TickWidth));
- return true;
- }
- break;
-
- #endregion
- #region scroll and select keys
- // SCROLL / SELECT
- case Key.A:
- // scroll left
- if (isNone) {
- notesVm.TickOffset = Math.Max(0, notesVm.TickOffset - snapUnit);
- return true;
- }
- // select all
- if (isCtrl) {
- notesVm.SelectAllNotes();
- return true;
- }
- break;
- case Key.D:
- // scroll right
- if (isNone) {
- notesVm.TickOffset = Math.Min(notesVm.TickOffset + snapUnit, notesVm.HScrollBarMax);
- return true;
- }
- // select none
- if (isCtrl) {
- notesVm.DeselectNotes();
- return true;
- }
- break;
- case Key.W:
- // toggle show waveform
- if (isNone) {
- notesVm.ShowWaveform = !notesVm.ShowWaveform;
- return true;
- }
- // scroll up
- // NOTE set to alt to avoid conflict with showwaveform toggle
- if (isAlt) {
- notesVm.TrackOffset = Math.Max(notesVm.TrackOffset - 2, 0);
- return true;
- }
- break;
- case Key.S:
- // scroll down
- if (isAlt) {
- notesVm.TrackOffset = Math.Min(notesVm.TrackOffset + 2, notesVm.VScrollBarMax);
- return true;
- }
- if (isCtrl) {
- _ = MainWindow?.Save();
- return true;
- }
- // solo
- if (isShift) {
- var track = project.tracks[notesVm.Part.trackNo];
- MessageBus.Current.SendMessage(new TracksSoloEvent(notesVm.Part.trackNo, !track.Solo, false));
- return true;
- }
- break;
- case Key.M:
- // mute
- if (isShift) {
- MessageBus.Current.SendMessage(new TracksMuteEvent(notesVm.Part.trackNo, false));
- return true;
- }
- break;
- case Key.F:
- // scroll selection into focus
- if (isNone) {
- var note = notesVm.Selection.FirstOrDefault();
- if (note != null) {
- DocManager.Inst.ExecuteCmd(new FocusNoteNotification(notesVm.Part, note));
- }
- return true;
- }
- if (isCtrl) {
- SearchNote();
- return true;
- }
- if (isAlt) {
- if (!notesVm.Selection.IsEmpty) {
- playVm.MovePlayPos(notesVm.Part.position + notesVm.Selection.FirstOrDefault()!.position);
- }
- return true;
- }
- break;
- case Key.E:
- // zoom in
- if (isNone) {
- double x = 0;
- double y = 0;
- if (!notesVm.Selection.IsEmpty) {
- x = (notesVm.Selection.Head!.position - notesVm.TickOffset) / notesVm.ViewportTicks;
- y = (ViewConstants.MaxTone - 1 - notesVm.Selection.Head.tone - notesVm.TrackOffset) / notesVm.ViewportTracks;
- } else if (notesVm.TickOffset != 0) {
- x = 0.5;
- y = 0.5;
- }
- notesVm.OnXZoomed(new Point(x, y), 0.1);
- return true;
- }
- break;
- case Key.Q:
- // zoom out
- if (isNone) {
- double x = 0;
- double y = 0;
- if (!notesVm.Selection.IsEmpty) {
- x = (notesVm.Selection.Head!.position - notesVm.TickOffset) / notesVm.ViewportTicks;
- y = (ViewConstants.MaxTone - 1 - notesVm.Selection.Head.tone - notesVm.TrackOffset) / notesVm.ViewportTracks;
- } else if (notesVm.TickOffset != 0) {
- x = 0.5;
- y = 0.5;
- }
- notesVm.OnXZoomed(new Point(x, y), -0.1);
- return true;
- }
- break;
- #endregion
- #region move to the next track part
- case Key.PageUp: {
- if (isNone) {
- MoveToNextPart(false);
- return true;
- }
- }
- break;
- case Key.PageDown: {
- if (isNone) {
- MoveToNextPart(true);
- return true;
- }
- }
- break;
- #endregion
- }
- return false;
- }
-
- public void AttachExpressions() {
- if (expSelector1 == null) {
- return;
- }
- var exps = new ExpSelector[] { expSelector1, expSelector2, expSelector3, expSelector4, expSelector5, expSelector6, expSelector7, expSelector8, expSelector9, expSelector10 };
- exps[DocManager.Inst.Project.expSecondary].SelectExp();
- exps[DocManager.Inst.Project.expPrimary].SelectExp();
- }
-
- public void OnNext(UCommand cmd, bool isUndo) {
- if (cmd is LoadingNotification loadingNotif && loadingNotif.window == typeof(PianoRoll)) {
- if (loadingNotif.startLoading) {
- LoadingWindow.BeginLoadingImmediate(RootWindow);
- } else {
- LoadingWindow.EndLoading();
- }
- }
- }
- }
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Reactive;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
+using OpenUtau.App.ViewModels;
+using OpenUtau.App.Views;
+using OpenUtau.Core;
+using OpenUtau.Core.Editing;
+using OpenUtau.Core.Ustx;
+using OpenUtau.Core.Util;
+using OpenUtau.ViewModels;
+using ReactiveUI;
+using Serilog;
+
+namespace OpenUtau.App.Controls {
+ interface IValueTip {
+ void ShowValueTip();
+ void HideValueTip();
+ void UpdateValueTip(string text);
+ }
+
+ public partial class PianoRoll : UserControl, IValueTip, ICmdSubscriber {
+ public MainWindow? MainWindow { get; set; }
+ public PianoRollViewModel ViewModel;
+
+ private readonly KeyModifiers cmdKey =
+ OS.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control;
+ private KeyboardPlayState? keyboardPlayState;
+ private NoteEditState? editState;
+ private Point valueTipPointerPosition;
+ private bool shouldOpenNotesContextMenu;
+
+ private bool isSelectingRange;
+ private Point rangeSelectStartPoint = default;
+ private const double RangeSelectThreshold = 5; // pixels
+
+ private ReactiveCommand? lyricsDialogCommand;
+ private ReactiveCommand? noteDefaultsCommand;
+ private ReactiveCommand? noteBatchEditCommand;
+
+ private Window RootWindow => (Window)TopLevel.GetTopLevel(this)!;
+
+ public PianoRoll(PianoRollViewModel model) {
+ InitializeComponent();
+ DataContext = ViewModel = model;
+ ValueTip.IsVisible = false;
+ SetPenToolIcon();
+ penTool.AddHandler(PointerPressedEvent, OnToolButtonPointerPressed, RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true);
+ this.LayoutUpdated += PianoRollLayoutUpdated;
+ }
+
+ private void PianoRollLayoutUpdated(object? sender, EventArgs e) {
+ UpdatePortraitPosition();
+ }
+
+ private void UpdatePortraitPosition() {
+ if (PortraitImage.DesiredSize.Width == 0 || PortraitCanvas.Bounds.Width == 0) return;
+ // Position at top-right of row 3, with 100px margin from right
+ Canvas.SetTop(PortraitImage, 0);
+ Canvas.SetLeft(PortraitImage, PortraitCanvas.Bounds.Width - PortraitImage.DesiredSize.Width - 100);
+ }
+
+ public void InitializePianoRollWindowAsync() {
+ noteBatchEditCommand = ReactiveCommand.Create(async edit => {
+ var NotesVm = ViewModel?.NotesViewModel;
+ if (NotesVm == null || NotesVm.Part == null) {
+ return;
+ }
+ try {
+ if (edit.IsAsync) {
+ var mainWindow =
+ (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)
+ ?.MainWindow! as MainWindow;
+ var name = ThemeManager.GetString(edit.Name);
+ await MessageBox.ShowProcessing(RootWindow, $"{name} - ? / ?",
+ ThemeManager.GetString("pianoroll.menu.batch.running"),
+ (messageBox, cancellationToken) => {
+ edit.RunAsync(NotesVm.Project, NotesVm.Part,
+ NotesVm.Selection.ToList(), DocManager.Inst,
+ (current, total) => {
+ messageBox.SetText($"{name}: {current} / {total}");
+ }, cancellationToken);
+ },
+ (Task t) => {
+ var e = t.Exception;
+ if (t.IsFaulted && e != null) {
+ if (e != null) {
+ Log.Error(e, $"Failed to run Editing Macro");
+ var customEx = new MessageCustomizableException("Failed to run editing macro", "", e);
+ DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(customEx));
+ }
+ return;
+ }
+ }
+ );
+ } else {
+ edit.Run(NotesVm.Project, NotesVm.Part, NotesVm.Selection.ToList(),
+ DocManager.Inst);
+ }
+ } catch (Exception e) {
+ var customEx = new MessageCustomizableException("Failed to run editing macro", "", e);
+ DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(customEx));
+ }
+
+ });
+ ViewModel.NoteBatchEdits.AddRange(new List() {
+ new LoadRenderedPitch(),
+ new RefreshRealCurves(),
+ new AddTailNote("-", "pianoroll.menu.notes.addtaildash"),
+ new AddTailNote("R", "pianoroll.menu.notes.addtailrest"),
+ new RemoveTailNote("-", "pianoroll.menu.notes.removetaildash"),
+ new RemoveTailNote("R", "pianoroll.menu.notes.removetailrest"),
+ new Transpose(12, "pianoroll.menu.notes.octaveup"),
+ new Transpose(-12, "pianoroll.menu.notes.octavedown"),
+ new AutoLegato(),
+ new CommonnoteCopy(),
+ new CommonnotePaste(),
+ new FixOverlap(),
+ new BakePitch(),
+ new RandomizeTiming(),
+ new RandomizePhonemeOffset()
+ }.Select(edit => new MenuItemViewModel() {
+ Header = ThemeManager.GetString(edit.Name),
+ Command = noteBatchEditCommand,
+ CommandParameter = edit,
+ }));
+ ViewModel.LyricBatchEdits.AddRange(new List() {
+ new RomajiToHiragana(),
+ new HiraganaToRomaji(),
+ new JapaneseVCVtoCV(),
+ new HanziToPinyin(),
+ new RemoveToneSuffix(),
+ new RemoveLetterSuffix(),
+ new MoveSuffixToVoiceColor(),
+ new RemovePhoneticHint(),
+ new DashToPlus(),
+ new DashToPlusTilda(),
+ new InsertSlur(),
+ }.Select(edit => new MenuItemViewModel() {
+ Header = ThemeManager.GetString(edit.Name),
+ Command = noteBatchEditCommand,
+ CommandParameter = edit,
+ }));
+ ViewModel.ResetBatchEdits.AddRange(new List() {
+ new ResetAll(),
+ new ResetPitchBends(),
+ new ResetAllExpressions(),
+ new ClearVibratos(),
+ new ResetVibratos(),
+ new ClearTimings(),
+ new ResetAliases(),
+ }.Select(edit => new MenuItemViewModel() {
+ Header = ThemeManager.GetString(edit.Name),
+ Command = noteBatchEditCommand,
+ CommandParameter = edit,
+ }));
+ try {
+ ViewModel.ExternalBatchEdits.AddRange(
+ DocManager.Inst.ExternalBatchEditTypes
+ .Select(type => Activator.CreateInstance(type) as BatchEdit)
+ .Where(edit => edit != null)
+ .Select(edit => new MenuItemViewModel() {
+ Header = ThemeManager.GetString(edit!.Name),
+ Command = noteBatchEditCommand,
+ CommandParameter = edit,
+ })
+ );
+ } catch (Exception e) {
+ Log.Error(e, "Failed to load external batch edits.");
+ }
+
+ DocManager.Inst.AddSubscriber(this);
+
+ ViewModel.NoteBatchEdits.Insert(6, new MenuItemViewModel() {
+ Header = ThemeManager.GetString("pianoroll.menu.notes.addbreath"),
+ Command = ReactiveCommand.Create(() => {
+ AddBreathNote();
+ })
+ });
+ ViewModel.NoteBatchEdits.Insert(9, new MenuItemViewModel() {
+ Header = ThemeManager.GetString("pianoroll.menu.notes.quantize"),
+ Command = ReactiveCommand.Create(() => {
+ QuantizeNotes();
+ })
+ });
+ ViewModel.NoteBatchEdits.Add(new MenuItemViewModel() {
+ Header = ThemeManager.GetString("pianoroll.menu.notes.randomizetuning"),
+ Command = ReactiveCommand.Create(() => {
+ RandomizeTuning();
+ })
+ });
+ ViewModel.NoteBatchEdits.Add(new MenuItemViewModel() {
+ Header = ThemeManager.GetString("pianoroll.menu.notes.lengthencrossfade"),
+ Command = ReactiveCommand.Create(() => {
+ LengthenCrossfade();
+ })
+ });
+ ViewModel.LyricBatchEdits.Add(new MenuItemViewModel() {
+ Header = ThemeManager.GetString("lyricsreplace.replace"),
+ Command = ReactiveCommand.Create(() => {
+ ReplaceLyrics();
+ })
+ });
+ lyricsDialogCommand = ReactiveCommand.Create(() => {
+ EditLyrics();
+ });
+ noteDefaultsCommand = ReactiveCommand.Create(() => {
+ EditNoteDefaults();
+ });
+
+ AddHandler(KeyDownEvent, OnKeyDown, RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
+
+ this.WhenAnyValue(x => x.ViewModel!.PlaybackViewModel!.PlayPosTick)
+ .Subscribe(tick => {
+ var notesVm = ViewModel?.NotesViewModel;
+
+ if (notesVm?.Part == null) return;
+ if (tick < notesVm.Part.position || tick >= notesVm.Part.End) {
+ var targetPart = notesVm.Project.parts
+ .OfType()
+ .FirstOrDefault(p => p.trackNo == notesVm.Part.trackNo && p.position <= tick && p.End > tick);
+
+ if (targetPart != null) {
+ DocManager.Inst.ExecuteCmd(new LoadPartNotification(targetPart, notesVm.Project, tick));
+ AttachExpressions();
+ }
+ }
+ });
+
+ DocManager.Inst.AddSubscriber(this);
+ }
+
+ void OnMenuClosed(object sender, RoutedEventArgs args) {
+ Focus(); // Force unfocus menu for key down events.
+ }
+
+ void OnMenuPointerLeave(object sender, PointerEventArgs args) {
+ Focus(); // Force unfocus menu for key down events.
+ }
+
+ // Edit menu
+ void OnMenuLockPitchPoints(object sender, RoutedEventArgs args) {
+ Preferences.Default.LockUnselectedNotesPitch = !Preferences.Default.LockUnselectedNotesPitch;
+ Preferences.Save();
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.LockPitchPoints));
+ }
+ void OnMenuLockVibrato(object sender, RoutedEventArgs args) {
+ Preferences.Default.LockUnselectedNotesVibrato = !Preferences.Default.LockUnselectedNotesVibrato;
+ Preferences.Save();
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.LockVibrato));
+ }
+ void OnMenuLockExpressions(object sender, RoutedEventArgs args) {
+ Preferences.Default.LockUnselectedNotesExpressions = !Preferences.Default.LockUnselectedNotesExpressions;
+ Preferences.Save();
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.LockExpressions));
+ }
+
+ // View menu
+ void OnMenuShowPortrait(object sender, RoutedEventArgs args) {
+ Preferences.Default.ShowPortrait = !Preferences.Default.ShowPortrait;
+ Preferences.Save();
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.ShowPortrait));
+ MessageBus.Current.SendMessage(new PianorollRefreshEvent("Portrait"));
+ }
+ void OnMenuShowIcon(object sender, RoutedEventArgs args) {
+ Preferences.Default.ShowIcon = !Preferences.Default.ShowIcon;
+ Preferences.Save();
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.ShowIcon));
+ MessageBus.Current.SendMessage(new PianorollRefreshEvent("Portrait"));
+ }
+ void OnMenuShowGhostNotes(object sender, RoutedEventArgs args) {
+ Preferences.Default.ShowGhostNotes = !Preferences.Default.ShowGhostNotes;
+ Preferences.Save();
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.ShowGhostNotes));
+ MessageBus.Current.SendMessage(new PianorollRefreshEvent("Part"));
+
+ }
+ void OnMenuUseTrackColor(object sender, RoutedEventArgs args) {
+ Preferences.Default.UseTrackColor = !Preferences.Default.UseTrackColor;
+ Preferences.Save();
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.UseTrackColor));
+ MessageBus.Current.SendMessage(new PianorollRefreshEvent("TrackColor"));
+ }
+ void OnMenuFullScreen(object sender, RoutedEventArgs args) {
+ RootWindow.WindowState = RootWindow.WindowState == WindowState.FullScreen
+ ? WindowState.Normal
+ : WindowState.FullScreen;
+ }
+ void OnMenuDegreeStyle(object sender, RoutedEventArgs args) {
+ if (sender is MenuItem menu && int.TryParse(menu.Tag?.ToString(), out int tag)) {
+ Preferences.Default.DegreeStyle = tag;
+ Preferences.Save();
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.DegreeStyle0));
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.DegreeStyle1));
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.DegreeStyle2));
+ MessageBus.Current.SendMessage(new PianorollRefreshEvent("Part"));
+ }
+ }
+ void OnMenuLockStartTime(object sender, RoutedEventArgs args) {
+ if (sender is MenuItem menu && int.TryParse(menu.Tag?.ToString(), out int tag)) {
+ Preferences.Default.LockStartTime = tag;
+ Preferences.Save();
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.LockStartTime0));
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.LockStartTime1));
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.LockStartTime2));
+ }
+ }
+ void OnMenuPlaybackAutoScroll(object sender, RoutedEventArgs args) {
+ if (sender is MenuItem menu && int.TryParse(menu.Tag?.ToString(), out int tag)) {
+ Preferences.Default.PlaybackAutoScroll = tag;
+ Preferences.Save();
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.PlaybackAutoScroll0));
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.PlaybackAutoScroll1));
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.PlaybackAutoScroll2));
+ }
+ }
+
+ async void OnMenuSingers(object sender, RoutedEventArgs args) {
+ if (MainWindow != null) {
+ await MainWindow.OpenSingersWindowAsync();
+ }
+ RootWindow.Activate();
+ try {
+ USinger? singer = null;
+ UOto? oto = null;
+ if (ViewModel.NotesViewModel.Part != null) {
+ singer = ViewModel.NotesViewModel.Project.tracks[ViewModel.NotesViewModel.Part.trackNo].Singer;
+ if (!ViewModel.NotesViewModel.Selection.IsEmpty && ViewModel.NotesViewModel.Part.phonemes.Count() > 0) {
+ oto = ViewModel.NotesViewModel.Part.phonemes.First(p => p.Parent == ViewModel.NotesViewModel.Selection.First()).oto;
+ }
+ }
+ DocManager.Inst.ExecuteCmd(new GotoOtoNotification(singer, oto));
+ } catch { }
+ }
+
+ void OnMenuSearchNote(object sender, RoutedEventArgs args) {
+ SearchNote();
+ }
+
+ void OnMenuDetachPianoRoll(object sender, RoutedEventArgs args) {
+ MainWindow?.SetPianoRollAttachment();
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.PianoRollDetached));
+ // 延迟通知,等窗口切换完成后再更新,避免切换过程中触发 UI 更新导致崩溃
+ Dispatcher.UIThread.Post(() => {
+ ViewModel.RaisePropertyChanged(nameof(ViewModel.HideMenuItemVisible));
+ });
+ }
+
+ void OnMenuHidePianoRoll(object sender, RoutedEventArgs args) {
+ // Reset ToggleButton checked state / 重置 ToggleButton 的 checked 状态
+ // Reason / 原因:
+ // ToggleButton keeps IsChecked=true after click, causing darker background.
+ // But this close button is a one-shot action, not a state toggle.
+ // So we manually reset it to false immediately after click.
+ // Since piano roll hides instantly, users never see this momentary state change.
+ //
+ // 详细说明:
+ // ToggleButton 点击后会保持 IsChecked=true,导致底纹变深。
+ // 但关闭按钮是一次性操作,不应该保持状态,所以手动重置为 false。
+ // 因为点击后钢琴卷帘立即隐藏,用户看不到这个瞬间的状态变化。
+ HidePianoRollButton.IsChecked = false;
+ if (RootWindow.DataContext is MainWindowViewModel mwvm) {
+ mwvm.ShowPianoRoll = false;
+ } else {
+ RootWindow.Hide();
+ }
+ }
+
+ // Edit Tools
+ private long _lastToolbarClickTime = 0;
+ private const long DoubleClickThreshold = 300; // 毫秒
+
+ void OnToolbarPointerPressed(object sender, PointerPressedEventArgs args) {
+ // 双击工具栏空白处关闭钢琴卷帘(仅嵌入模式生效)
+ if (!args.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
+
+ long now = DateTimeOffset.Now.ToUnixTimeMilliseconds();
+ if (now - _lastToolbarClickTime < DoubleClickThreshold) {
+ // 双击
+ if (RootWindow.DataContext is MainWindowViewModel mwvm) {
+ mwvm.ShowPianoRoll = false;
+ }
+ // 分离模式下不响应,保持和原来一模一样
+ }
+ _lastToolbarClickTime = now;
+ }
+
+ private CancellationTokenSource? _longPressCts;
+ private async void OnToolButtonPointerPressed(object? sender, PointerPressedEventArgs args) {
+ if (!args.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
+
+ if (sender is Control control) {
+ _longPressCts = new CancellationTokenSource();
+ try {
+ await Task.Delay(500, _longPressCts.Token);
+ if (_longPressCts != null && !_longPressCts.IsCancellationRequested) {
+ FlyoutBase.ShowAttachedFlyout(control);
+ }
+ } catch {
+ // don't open the flyout
+ }
+ }
+ }
+ private void OnToolButtonPointerReleased(object? sender, PointerReleasedEventArgs args) {
+ _longPressCts?.Cancel();
+ _longPressCts?.Dispose();
+ _longPressCts = null;
+ }
+ void PenToolListBox_PointerReleased(object? sender, PointerReleasedEventArgs args) {
+ FlyoutBase.GetAttachedFlyout(penTool)?.Hide(); ;
+ SetPenToolIcon();
+ }
+ void SetPenToolIcon() {
+ penTool.Classes.Remove("penTool");
+ penTool.Classes.Remove("penPlusTool");
+ penTool.Classes.Add(ViewModel.EditTool.PenToolVariation == 1 ? "penPlusTool" : "penTool");
+ }
+
+ void SearchNote() {
+ if (ViewModel.NotesViewModel.Part == null || ViewModel.NotesViewModel.Part.notes.Count == 0) {
+ return;
+ }
+ SearchBar.Show(ViewModel.NotesViewModel);
+ }
+
+ void ReplaceLyrics() {
+ if (ViewModel.NotesViewModel.Part == null) {
+ return;
+ }
+ if (ViewModel.NotesViewModel.Part.notes.Count < 1) {
+ _ = MessageBox.Show(
+ RootWindow,
+ ThemeManager.GetString("lyrics.nonote"),
+ ThemeManager.GetString("lyrics.caption"),
+ MessageBox.MessageBoxButtons.Ok);
+ return;
+ }
+
+ var notes = ViewModel.NotesViewModel.Selection.ToArray();
+ if (notes.Length == 0) {
+ notes = ViewModel.NotesViewModel.Part.notes.ToArray();
+ }
+ var vm = new LyricsReplaceViewModel(ViewModel.NotesViewModel.Part, notes);
+ var dialog = new LyricsReplaceDialog() {
+ DataContext = vm,
+ };
+ dialog.ShowDialog(RootWindow);
+ }
+
+ void OnMenuEditLyrics(object? sender, RoutedEventArgs e) {
+ EditLyrics();
+ }
+
+ void EditLyrics() {
+ if (ViewModel.NotesViewModel.Part == null) {
+ return;
+ }
+ if (ViewModel.NotesViewModel.Part.notes.Count < 1) {
+ _ = MessageBox.Show(
+ RootWindow,
+ ThemeManager.GetString("lyrics.nonote"),
+ ThemeManager.GetString("lyrics.caption"),
+ MessageBox.MessageBoxButtons.Ok);
+ return;
+ }
+
+ var vm = new LyricsViewModel();
+ var (notes, selection) = ViewModel.NotesViewModel.PrepareInsertLyrics();
+ vm.Start(ViewModel.NotesViewModel.Part, notes, selection);
+ var dialog = new LyricsDialog() {
+ DataContext = vm,
+ };
+ dialog.ShowDialog(RootWindow);
+ }
+
+ void OnMenuNoteDefaults(object sender, RoutedEventArgs args) {
+ EditNoteDefaults();
+ }
+
+ void EditNoteDefaults() {
+ var dialog = new NoteDefaultsDialog();
+ dialog.ShowDialog(RootWindow);
+ if (dialog.Position.Y < 0) {
+ dialog.Position = dialog.Position.WithY(0);
+ }
+ }
+
+ void AddBreathNote() {
+ var notesVM = ViewModel.NotesViewModel;
+ if (notesVM.Part == null) {
+ return;
+ }
+ if (notesVM.Selection.IsEmpty) {
+ _ = MessageBox.Show(
+ RootWindow,
+ ThemeManager.GetString("lyrics.selectnotes"),
+ ThemeManager.GetString("lyrics.caption"),
+ MessageBox.MessageBoxButtons.Ok);
+ return;
+ }
+ var dialog = new TypeInDialog() {
+ Title = ThemeManager.GetString("pianoroll.menu.notes.addbreath"),
+ onFinish = value => {
+ if (!string.IsNullOrWhiteSpace(value)) {
+ var edit = new Core.Editing.AddBreathNote(value);
+ try {
+ edit.Run(notesVM.Project, notesVM.Part, notesVM.Selection.ToList(), DocManager.Inst);
+ } catch (Exception e) {
+ var customEx = new MessageCustomizableException("Failed to run editing macro", "", e);
+ DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(customEx));
+ }
+ }
+ }
+ };
+ dialog.SetText("br");
+ dialog.ShowDialog(RootWindow);
+ }
+
+ void QuantizeNotes() {
+ var notesVM = ViewModel.NotesViewModel;
+ if (notesVM.Part == null) {
+ return;
+ }
+ var edit = new QuantizeNotes(notesVM.Project.resolution * 4 / notesVM.SnapDiv);
+ edit.Run(notesVM.Project, notesVM.Part, notesVM.Selection.ToList(), DocManager.Inst);
+ }
+
+ void RandomizeTuning() {
+ var notesVM = ViewModel.NotesViewModel;
+ if (notesVM.Part == null) {
+ return;
+ }
+ var dialog = new SliderDialog(ThemeManager.GetString("pianoroll.menu.notes.randomizetuning"), 20, 1, 100, 1);
+ dialog.onFinish = value => {
+ try {
+ var edit = new RandomizeTuning((int)value);
+ edit.Run(notesVM.Project, notesVM.Part, notesVM.Selection.ToList(), DocManager.Inst);
+ } catch (Exception e) {
+ var customEx = new MessageCustomizableException("Failed to run editing macro", "", e);
+ DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(customEx));
+ }
+ };
+ dialog.ShowDialog(RootWindow);
+ }
+
+ void LengthenCrossfade() {
+ var notesVM = ViewModel.NotesViewModel;
+ if (notesVM.Part == null) {
+ return;
+ }
+ var dialog = new SliderDialog(ThemeManager.GetString("pianoroll.menu.notes.lengthencrossfade"), 0.5, 0, 1, 0.1);
+ dialog.onFinish = value => {
+ var edit = new Core.Editing.LengthenCrossfade(value);
+ try {
+ edit.Run(notesVM.Project, notesVM.Part, notesVM.Selection.ToList(), DocManager.Inst);
+ } catch (Exception e) {
+ var customEx = new MessageCustomizableException("Failed to run editing macro", "", e);
+ DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(customEx));
+ }
+ };
+ dialog.ShowDialog(RootWindow);
+ }
+
+ private void OnPianoRollFocus(object sender, GotFocusEventArgs e) {
+ var input = e.Source as InputElement;
+ if (input is TextBox or ComboBox or ComboBoxItem) {
+ input.Focus();
+ }
+ }
+
+ private void LyricBoxLostFocus(object sender, RoutedEventArgs e) {
+ if (sender is InputElement { IsKeyboardFocusWithin: false }) {
+ this.Focus();
+ }
+ }
+
+ public void OnExpButtonClick(object sender, RoutedEventArgs args) {
+ var notesVM = ViewModel.NotesViewModel;
+ if (notesVM.Part == null) {
+ return;
+ }
+ var dialog = new ExpressionsDialog() {
+ DataContext = new ExpressionsViewModel(notesVM.Project.tracks[notesVM.Part.trackNo]),
+ };
+ dialog.ShowDialog(RootWindow);
+ if (dialog.Position.Y < 0) {
+ dialog.Position = dialog.Position.WithY(0);
+ }
+ }
+
+ public void KeyboardPointerWheelChanged(object sender, PointerWheelEventArgs args) {
+ LyricBox?.EndEdit();
+ VScrollPointerWheelChanged(VScrollBar, args);
+ }
+
+ public void KeyboardPointerPressed(object sender, PointerPressedEventArgs args) {
+ LyricBox?.EndEdit();
+ if (keyboardPlayState != null) {
+ return;
+ }
+ var element = (TrackBackground)sender;
+ keyboardPlayState = new KeyboardPlayState(element, ViewModel);
+ keyboardPlayState.Begin(args.Pointer, args.GetPosition(element));
+ }
+
+ public void KeyboardPointerMoved(object sender, PointerEventArgs args) {
+ if (keyboardPlayState != null) {
+ var element = (TrackBackground)sender;
+ keyboardPlayState.Update(args.Pointer, args.GetPosition(element));
+ }
+ }
+
+ public void KeyboardPointerReleased(object sender, PointerReleasedEventArgs args) {
+ if (keyboardPlayState != null) {
+ var element = (TrackBackground)sender;
+ keyboardPlayState.End(args.Pointer, args.GetPosition(element));
+ keyboardPlayState = null;
+ }
+ }
+
+ public void HScrollPointerWheelChanged(object sender, PointerWheelEventArgs args) {
+ var scrollbar = (ScrollBar)sender;
+ scrollbar.Value = Math.Max(scrollbar.Minimum, Math.Min(scrollbar.Maximum, scrollbar.Value - scrollbar.SmallChange * args.Delta.Y));
+ LyricBox?.EndEdit();
+ }
+
+ public void VScrollPointerWheelChanged(object sender, PointerWheelEventArgs args) {
+ var scrollbar = (ScrollBar)sender;
+ scrollbar.Value = Math.Max(scrollbar.Minimum, Math.Min(scrollbar.Maximum, scrollbar.Value - scrollbar.SmallChange * args.Delta.Y));
+ LyricBox?.EndEdit();
+ }
+
+ public void TimelinePointerWheelChanged(object sender, PointerWheelEventArgs args) {
+ var control = (Control)sender;
+ var position = args.GetCurrentPoint((Visual)sender).Position;
+ var size = control.Bounds.Size;
+ position = position.WithX(position.X / size.Width).WithY(position.Y / size.Height);
+ ViewModel.NotesViewModel.OnXZoomed(position, 0.1 * args.Delta.Y);
+ LyricBox?.EndEdit();
+ }
+
+ public void ViewScalerPointerWheelChanged(object sender, PointerWheelEventArgs args) {
+ ViewModel.NotesViewModel.OnYZoomed(new Point(0, 0.5), 0.1 * args.Delta.Y);
+ LyricBox?.EndEdit();
+ }
+
+ public void TimelinePointerPressed(object sender, PointerPressedEventArgs args) {
+ var control = (Control)sender;
+ var point = args.GetCurrentPoint(control);
+ if (point.Properties.IsLeftButtonPressed) {
+ args.Pointer.Capture(control);
+ ViewModel.NotesViewModel.PointToLineTick(point.Position, out int left, out int right);
+ int tick = left + ViewModel.NotesViewModel.Part?.position ?? 0;
+ ViewModel.PlaybackViewModel?.MovePlayPos(tick);
+ } else if (point.Properties.IsRightButtonPressed) {
+ args.Pointer.Capture(control);
+ isSelectingRange = true;
+ rangeSelectStartPoint = point.Position;
+ LyricBox?.EndEdit();
+ return;
+ }
+ LyricBox?.EndEdit();
+ }
+
+ public void TimelinePointerMoved(object sender, PointerEventArgs args) {
+ var control = (Control)sender;
+ var point = args.GetCurrentPoint(control);
+ if (point.Properties.IsLeftButtonPressed) {
+ ViewModel.NotesViewModel.PointToLineTick(point.Position, out int left, out int right);
+ int tick = left + ViewModel.NotesViewModel.Part?.position ?? 0;
+ ViewModel.PlaybackViewModel?.MovePlayPos(tick);
+ } else if (point.Properties.IsRightButtonPressed && isSelectingRange) {
+ double dx = Math.Abs(point.Position.X - rangeSelectStartPoint.X);
+ if (dx >= RangeSelectThreshold) {
+ UpdateRangeSelection(point.Position);
+ }
+ }
+ }
+
+ public void TimelinePointerReleased(object sender, PointerReleasedEventArgs args) {
+ if (isSelectingRange && args.InitialPressMouseButton == MouseButton.Right) {
+ isSelectingRange = false;
+ var control = (Control)sender;
+ var point = args.GetCurrentPoint(control);
+ double dx = Math.Abs(point.Position.X - rangeSelectStartPoint.X);
+ if (dx < RangeSelectThreshold) {
+ DocManager.Inst.ExecuteCmd(new SetRangeSelectionNotification(0, 0));
+ }
+ args.Pointer.Capture(null);
+ return;
+ }
+ args.Pointer.Capture(null);
+ }
+
+ public void TimelineDoubleTapped(object sender, TappedEventArgs args) {
+ DocManager.Inst.ExecuteCmd(new SetRangeSelectionNotification(0, 0));
+ }
+
+ private void UpdateRangeSelection(Point currentPoint) {
+ var notesVm = ViewModel.NotesViewModel;
+ int partPos = notesVm.Part?.position ?? 0;
+ notesVm.PointToLineTick(rangeSelectStartPoint, out int startLeft, out int startRight);
+ notesVm.PointToLineTick(currentPoint, out int endLeft, out int endRight);
+ int left = Math.Min(startLeft, endLeft);
+ int right = Math.Max(startRight, endRight);
+ DocManager.Inst.ExecuteCmd(new SetRangeSelectionNotification(left + partPos, right + partPos));
+ }
+
+ public void NotesCanvasPointerPressed(object sender, PointerPressedEventArgs args) {
+ LyricBox?.EndEdit();
+ if (ViewModel.NotesViewModel.Part == null) {
+ return;
+ }
+ var control = (Control)sender;
+ var point = args.GetCurrentPoint(control);
+ if (editState != null) {
+ // Finalize pitch curve in adjusting phase before starting a new edit
+ if (editState is PitchCurveState pcs2 && pcs2.IsInAdjustingPhase) {
+ if (point.Properties.IsLeftButtonPressed) {
+ pcs2.Apply();
+ pcs2.End(pointer: args.Pointer, point: point.Position);
+ } else {
+ // Right-click during adjusting: cancel the edit
+ pcs2.Cancel(args.Pointer);
+ }
+ editState = null;
+ } else {
+ return;
+ }
+ }
+ if (point.Properties.IsLeftButtonPressed) {
+ NotesCanvasLeftPointerPressed(control, point, args);
+ } else if (point.Properties.IsRightButtonPressed) {
+ NotesCanvasRightPointerPressed(control, point, args);
+ } else if (point.Properties.IsMiddleButtonPressed) {
+ editState = new NotePanningState(control, ViewModel, this);
+ Cursor = ViewConstants.cursorHand;
+ }
+ if (editState != null) {
+ editState.Begin(point.Pointer, point.Position);
+ editState.Update(point.Pointer, point.Position);
+ }
+ }
+
+ private void NotesCanvasLeftPointerPressed(Control control, PointerPoint point, PointerPressedEventArgs args) {
+ if (ViewModel.EditTool.IsPitchTool) {
+ ViewModel.NotesViewModel.DeselectNotes();
+ if (args.KeyModifiers != cmdKey) {
+ bool overwrite = ViewModel.EditTool.OverwritePitch;
+ EditTools tool = ViewModel.EditTool.CurrentTool;
+ if (tool == EditTools.PitchSmoothenTool) {
+ editState = new SmoothenPitchState(control, ViewModel, this, overwrite);
+ } else if (tool == EditTools.DrawPitchTool) {
+ editState = new DrawPitchState(control, ViewModel, this, overwrite);
+ } else if (tool == EditTools.PitchLineTool) {
+ editState = new PitchCurveState(control, ViewModel, this, PitchPreviewLine, PitchCurveState.CurveMode.Line, overwrite);
+ } else if (tool == EditTools.PitchSCurveTool) {
+ editState = new PitchCurveState(control, ViewModel, this, PitchPreviewLine, PitchCurveState.CurveMode.SCurve, overwrite);
+ } else if (tool == EditTools.PitchSineWaveTool) {
+ editState = new PitchCurveState(control, ViewModel, this, PitchPreviewLine, PitchCurveState.CurveMode.Sine, overwrite);
+ }
+ return;
+ }
+ }
+ if (ViewModel.EditTool.CurrentTool == EditTools.EraserTool && args.KeyModifiers != cmdKey) {
+ ViewModel.NotesViewModel.DeselectNotes();
+ editState = new NoteEraseEditState(control, ViewModel, this, MouseButton.Left);
+ Cursor = ViewConstants.cursorNo;
+ return;
+ }
+ var pitHitInfo = ViewModel.NotesViewModel.HitTest.HitTestPitchPoint(point.Position);
+ if (pitHitInfo.Note != null) {
+ editState = new PitchPointEditState(control, ViewModel, this,
+ pitHitInfo.Note, pitHitInfo.Index, pitHitInfo.OnPoint, pitHitInfo.X, pitHitInfo.Y);
+ return;
+ }
+ var vbrHitInfo = ViewModel.NotesViewModel.HitTest.HitTestVibrato(point.Position);
+ if (vbrHitInfo.hit) {
+ if (vbrHitInfo.hitToggle) {
+ ViewModel.NotesViewModel.ToggleVibrato(vbrHitInfo.note);
+ return;
+ }
+ if (vbrHitInfo.hitStart) {
+ editState = new VibratoChangeStartState(control, ViewModel, this, vbrHitInfo.note);
+ return;
+ }
+ if (vbrHitInfo.hitIn) {
+ editState = new VibratoChangeInState(control, ViewModel, this, vbrHitInfo.note);
+ return;
+ }
+ if (vbrHitInfo.hitOut) {
+ editState = new VibratoChangeOutState(control, ViewModel, this, vbrHitInfo.note);
+ return;
+ }
+ if (vbrHitInfo.hitDepth) {
+ editState = new VibratoChangeDepthState(control, ViewModel, this, vbrHitInfo.note);
+ return;
+ }
+ if (vbrHitInfo.hitPeriod) {
+ editState = new VibratoChangePeriodState(control, ViewModel, this, vbrHitInfo.note);
+ return;
+ }
+ if (vbrHitInfo.hitShift) {
+ editState = new VibratoChangeShiftState(
+ control, ViewModel, this, vbrHitInfo.note, vbrHitInfo.point, vbrHitInfo.initialShift);
+ return;
+ }
+ return;
+ }
+ var noteHitInfo = ViewModel.NotesViewModel.HitTest.HitTestNote(point.Position);
+ if (noteHitInfo.hitBody) {
+ if (noteHitInfo.hitResizeArea) {
+ editState = new NoteResizeEditState(
+ control, ViewModel, this, noteHitInfo.note,
+ args.KeyModifiers == KeyModifiers.Alt,
+ fromStart: noteHitInfo.hitResizeAreaFromStart);
+ Cursor = ViewConstants.cursorSizeWE;
+ } else if (args.KeyModifiers == cmdKey) {
+ ViewModel.NotesViewModel.ToggleSelectNote(noteHitInfo.note);
+ } else if (args.KeyModifiers == KeyModifiers.Shift) {
+ ViewModel.NotesViewModel.SelectNotesUntil(noteHitInfo.note);
+ } else if (ViewModel.EditTool.CurrentTool == EditTools.KnifeTool) {
+ ViewModel.NotesViewModel.DeselectNotes();
+ editState = new NoteSplitEditState(
+ control, ViewModel, this, noteHitInfo.note);
+ } else {
+ editState = new NoteMoveEditState(control, ViewModel, this, noteHitInfo.note);
+ Cursor = ViewConstants.cursorSizeAll;
+ }
+ return;
+ }
+ if (ViewModel.EditTool.CurrentTool == EditTools.CursorTool || args.KeyModifiers == cmdKey) {
+ if (args.KeyModifiers == KeyModifiers.None) {
+ // New selection.
+ ViewModel.NotesViewModel.DeselectNotes();
+ editState = new NoteSelectionEditState(control, ViewModel, this, SelectionBox);
+ Cursor = ViewConstants.cursorCross;
+ return;
+ }
+ if (args.KeyModifiers == cmdKey) {
+ // Additional selection.
+ editState = new NoteSelectionEditState(control, ViewModel, this, SelectionBox);
+ Cursor = ViewConstants.cursorCross;
+ return;
+ }
+ ViewModel.NotesViewModel.DeselectNotes();
+ } else if (ViewModel.EditTool.IsMatch([EditTools.PenTool, EditTools.PenPlusTool])) {
+ ViewModel.NotesViewModel.DeselectNotes();
+ editState = new NoteDrawEditState(control, ViewModel, this, ViewModel.NotesViewModel.PlayTone);
+ }
+ }
+
+ private void NotesCanvasRightPointerPressed(Control control, PointerPoint point, PointerPressedEventArgs args) {
+ if (ViewModel.NotesContextMenuItems.Count > 0) {
+ ViewModel.NotesContextMenuItems.Clear();
+ }
+ var selectedNotes = ViewModel.NotesViewModel.Selection.ToList();
+ if (ViewModel.EditTool.IsPitchTool) {
+ editState = new ResetPitchState(control, ViewModel, this);
+ return;
+ }
+ if (ViewModel.NotesViewModel.ShowPitch) {
+ var pitHitInfo = ViewModel.NotesViewModel.HitTest.HitTestPitchPoint(point.Position);
+ if (pitHitInfo.Note != null) {
+ var shapes = new List();
+ var currentShape = pitHitInfo.Note.pitch.data[pitHitInfo.Index].shape;
+ shapes.Add(new MenuItemViewModel(currentShape == PitchPointShape.io) {
+ Header = ThemeManager.GetString("context.pitch.easeinout"),
+ Command = ViewModel.PitEaseInOutCommand,
+ CommandParameter = pitHitInfo,
+ });
+ shapes.Add(new MenuItemViewModel(currentShape == PitchPointShape.l) {
+ Header = ThemeManager.GetString("context.pitch.linear"),
+ Command = ViewModel.PitLinearCommand,
+ CommandParameter = pitHitInfo,
+ });
+ shapes.Add(new MenuItemViewModel(currentShape == PitchPointShape.i) {
+ Header = ThemeManager.GetString("context.pitch.easein"),
+ Command = ViewModel.PitEaseInCommand,
+ CommandParameter = pitHitInfo,
+ });
+ shapes.Add(new MenuItemViewModel(currentShape == PitchPointShape.o) {
+ Header = ThemeManager.GetString("context.pitch.easeout"),
+ Command = ViewModel.PitEaseOutCommand,
+ CommandParameter = pitHitInfo,
+ });
+ shapes.Add(new MenuItemViewModel(currentShape == PitchPointShape.sp) {
+ Header = ThemeManager.GetString("context.pitch.smooth"),
+ Command = ViewModel.PitSplineCommand,
+ CommandParameter = pitHitInfo,
+ });
+ ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
+ Header = ThemeManager.GetString("context.pitch.shape"),
+ Items = shapes,
+ });
+ if (pitHitInfo.OnPoint && pitHitInfo.Index == 0) {
+ ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel(pitHitInfo.Note.pitch.snapFirst) {
+ Header = ThemeManager.GetString("context.pitch.pointsnapprev"),
+ Command = ViewModel.PitSnapCommand,
+ CommandParameter = pitHitInfo,
+ });
+ }
+ if (pitHitInfo.OnPoint && pitHitInfo.Index != 0 &&
+ pitHitInfo.Index != pitHitInfo.Note.pitch.data.Count - 1) {
+ ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
+ Header = ThemeManager.GetString("context.pitch.pointdel"),
+ Command = ViewModel.PitDelCommand,
+ CommandParameter = pitHitInfo,
+ });
+ }
+ if (!pitHitInfo.OnPoint) {
+ ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
+ Header = ThemeManager.GetString("context.pitch.pointadd"),
+ Command = ViewModel.PitAddCommand,
+ CommandParameter = pitHitInfo,
+ });
+ }
+ shouldOpenNotesContextMenu = true;
+ return;
+ }
+ }
+ if (ViewModel.EditTool.IsMatch([EditTools.CursorTool, EditTools.PenTool, EditTools.KnifeTool])) {
+ var hitInfo = ViewModel.NotesViewModel.HitTest.HitTestNote(point.Position);
+ var vibHitInfo = ViewModel.NotesViewModel.HitTest.HitTestVibrato(point.Position);
+ if ((hitInfo.hitBody && hitInfo.note != null) || vibHitInfo.hit) {
+ if (hitInfo.note != null && !selectedNotes.Contains(hitInfo.note)) {
+ ViewModel.NotesViewModel.DeselectNotes();
+ ViewModel.NotesViewModel.SelectNote(hitInfo.note, false);
+ }
+ if (ViewModel.NotesViewModel.Selection.Count > 0) {
+ ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
+ Header = ThemeManager.GetString("context.note.copy"),
+ Command = ViewModel.NoteCopyCommand,
+ CommandParameter = hitInfo,
+ InputGesture = new KeyGesture(Key.C, KeyModifiers.Control),
+ });
+ ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
+ Header = ThemeManager.GetString("context.note.delete"),
+ Command = ViewModel.NoteDeleteCommand,
+ CommandParameter = hitInfo,
+ InputGesture = new KeyGesture(Key.Delete),
+ });
+ ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
+ Header = ThemeManager.GetString("context.note.pasteparameters"),
+ Command = ReactiveCommand.Create(() => ViewModel.NotesViewModel.PasteSelectedParams(RootWindow)),
+ InputGesture = new KeyGesture(Key.V, KeyModifiers.Alt),
+ });
+ ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
+ Header = ThemeManager.GetString("pianoroll.menu.notes"),
+ Items = ViewModel.NoteBatchEdits.ToArray(),
+ });
+ ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
+ Header = ThemeManager.GetString("pianoroll.menu.lyrics"),
+ Items = ViewModel.LyricBatchEdits.ToArray(),
+ });
+ ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
+ Header = ThemeManager.GetString("pianoroll.menu.reset"),
+ Items = ViewModel.ResetBatchEdits.ToArray(),
+ });
+ ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
+ Header = ThemeManager.GetString("pianoroll.menu.part.legacypluginexp"),
+ Items = ViewModel.LegacyPlugins.ToArray(),
+ });
+ ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
+ Header = ThemeManager.GetString("pianoroll.menu.external"),
+ Items = ViewModel.ExternalBatchEdits.ToArray(),
+ });
+ ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
+ Header = ThemeManager.GetString("pianoroll.menu.lyrics.edit"),
+ Command = lyricsDialogCommand,
+ });
+ ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
+ Header = ThemeManager.GetString("pianoroll.menu.notedefaults"),
+ Command = noteDefaultsCommand,
+ });
+ ViewModel.NotesContextMenuItems.Add(new MenuItemViewModel() {
+ Header = ThemeManager.GetString("context.note.clearcache"),
+ Command = ViewModel.ClearPhraseCacheCommand,
+ });
+ shouldOpenNotesContextMenu = true;
+ return;
+ }
+ } else {
+ ViewModel.NotesViewModel.DeselectNotes();
+ }
+ } else if (ViewModel.EditTool.IsMatch([EditTools.EraserTool, EditTools.PenPlusTool])) {
+ ViewModel.NotesViewModel.DeselectNotes();
+ editState = new NoteEraseEditState(control, ViewModel, this, MouseButton.Right);
+ Cursor = ViewConstants.cursorNo;
+ }
+ }
+
+ public void NotesCanvasPointerMoved(object sender, PointerEventArgs args) {
+ var control = (Control)sender;
+ var point = args.GetCurrentPoint(control);
+ args.Handled = true;
+ if (ValueTipCanvas != null) {
+ valueTipPointerPosition = args.GetCurrentPoint(ValueTipCanvas!).Position;
+ }
+ if (editState != null) {
+ editState.altShiftHeld = args.KeyModifiers == (KeyModifiers.Alt | KeyModifiers.Shift);
+ editState.shiftHeld = args.KeyModifiers == KeyModifiers.Shift;
+ editState.ctrlHeld = args.KeyModifiers == cmdKey;
+ editState.altHeld = args.KeyModifiers == KeyModifiers.Alt;
+ editState.Update(point.Pointer, point.Position);
+ return;
+ }
+ if (ViewModel?.NotesViewModel?.HitTest == null) {
+ return;
+ }
+ if (ViewModel.EditTool.IsMatch([EditTools.DrawPitchTool, EditTools.PitchLineTool, EditTools.PitchSCurveTool, EditTools.PitchSineWaveTool, EditTools.PitchSmoothenTool, EditTools.EraserTool]) && args.KeyModifiers != cmdKey) {
+ Cursor = null;
+ return;
+ }
+ var pitHitInfo = ViewModel.NotesViewModel.HitTest.HitTestPitchPoint(point.Position);
+ if (pitHitInfo.Note != null) {
+ Cursor = ViewConstants.cursorHand;
+ return;
+ }
+ var vbrHitInfo = ViewModel.NotesViewModel.HitTest.HitTestVibrato(point.Position);
+ if (vbrHitInfo.hit) {
+ if (vbrHitInfo.hitDepth) {
+ Cursor = ViewConstants.cursorSizeNS;
+ } else if (vbrHitInfo.hitPeriod) {
+ Cursor = ViewConstants.cursorSizeWE;
+ } else {
+ Cursor = ViewConstants.cursorHand;
+ }
+ return;
+ }
+ var noteHitInfo = ViewModel.NotesViewModel.HitTest.HitTestNote(point.Position);
+ if (noteHitInfo.hitResizeArea) {
+ Cursor = ViewConstants.cursorSizeWE;
+ return;
+ }
+ if (!noteHitInfo.hitBody && (ViewModel.EditTool.CurrentTool == EditTools.CursorTool || args.KeyModifiers == cmdKey)) {
+ Cursor = ViewConstants.cursorCross;
+ return;
+ }
+ Cursor = null;
+ }
+
+ public void NotesCanvasPointerReleased(object sender, PointerReleasedEventArgs args) {
+ if (editState == null) {
+ return;
+ }
+ if (editState.MouseButton != args.InitialPressMouseButton) {
+ return;
+ }
+ var control = (Control)sender;
+ var point = args.GetCurrentPoint(control);
+ editState.shiftHeld = args.KeyModifiers == KeyModifiers.Shift;
+ editState.ctrlHeld = args.KeyModifiers == cmdKey;
+ editState.altHeld = args.KeyModifiers == KeyModifiers.Alt;
+ editState.Update(point.Pointer, point.Position);
+
+ // Two-phase handling for S-curve and Sine wave tools:
+ // On first mouse up, transition to adjusting phase instead of ending.
+ if (editState is PitchCurveState pcs) {
+ if (pcs.Mode == PitchCurveState.CurveMode.Line) {
+ // Line tool: apply immediately on mouse up
+ pcs.Apply();
+ pcs.End(pointer: args.Pointer, point: point.Position);
+ } else {
+ // S-curve / Sine: transition to adjusting phase, keep pointer captured
+ if (!pcs.TransitionToAdjusting(point.Position)) {
+ // TransitionToAdjusting returned false (click without drag) — already cancelled
+ editState = null;
+ return;
+ }
+ return;
+ }
+ } else {
+ editState.End(point.Pointer, point.Position);
+ }
+ editState = null;
+ Cursor = null;
+ }
+
+ public void NotesCanvasDoubleTapped(object sender, TappedEventArgs args) {
+ if (!(sender is Control control)) {
+ return;
+ }
+ var point = args.GetPosition(control);
+ if (editState != null) {
+ editState.End(args.Pointer, point);
+ editState = null;
+ Cursor = null;
+ }
+ var noteHitInfo = ViewModel.NotesViewModel.HitTest.HitTestNote(point);
+ if (noteHitInfo.hitBody && ViewModel?.NotesViewModel?.Part != null) {
+ var note = noteHitInfo.note;
+ LyricBox?.Show(ViewModel.NotesViewModel.Part, new LyricBoxNote(note), note.lyric);
+ }
+ }
+
+ public void NotesCanvasPointerWheelChanged(object sender, PointerWheelEventArgs args) {
+ LyricBox?.EndEdit();
+ var control = (Control)sender;
+ var position = args.GetCurrentPoint(control).Position;
+ var size = control.Bounds.Size;
+ var delta = args.Delta;
+ if (args.KeyModifiers == KeyModifiers.None || args.KeyModifiers == KeyModifiers.Shift) {
+ if (args.KeyModifiers == KeyModifiers.Shift) {
+ delta = new Vector(delta.Y, delta.X);
+ }
+ if (delta.X != 0) {
+ HScrollBar.Value = Math.Max(HScrollBar.Minimum,
+ Math.Min(HScrollBar.Maximum, HScrollBar.Value - HScrollBar.SmallChange * delta.X));
+ }
+ if (delta.Y != 0) {
+ VScrollBar.Value = Math.Max(VScrollBar.Minimum,
+ Math.Min(VScrollBar.Maximum, VScrollBar.Value - VScrollBar.SmallChange * delta.Y));
+ }
+ } else if (args.KeyModifiers == KeyModifiers.Alt) {
+ position = position.WithX(position.X / size.Width).WithY(position.Y / size.Height);
+ ViewModel.NotesViewModel.OnYZoomed(position, 0.1 * args.Delta.Y);
+ } else if (args.KeyModifiers == cmdKey) {
+ TimelinePointerWheelChanged(TimelineCanvas, args);
+ }
+ if (editState != null) {
+ var point = args.GetCurrentPoint(editState.control);
+ editState.Update(point.Pointer, point.Position);
+ }
+ }
+
+ public void NotesContextMenuOpening(object sender, CancelEventArgs args) {
+ if (shouldOpenNotesContextMenu) {
+ shouldOpenNotesContextMenu = false;
+ } else {
+ args.Cancel = true;
+ }
+ }
+
+ public void ExpCanvasPointerPressed(object sender, PointerPressedEventArgs args) {
+ LyricBox?.EndEdit();
+ var notesVm = ViewModel.NotesViewModel;
+ if (notesVm.Part == null) {
+ return;
+ }
+ var control = (Control)sender;
+ var point = args.GetCurrentPoint(control);
+ if (editState != null) {
+ return;
+ }
+ var track = notesVm.Project.tracks[notesVm.Part.trackNo];
+ if (!track.TryGetExpDescriptor(notesVm.Project, notesVm.PrimaryKey, out var descriptor)) {
+ return;
+ }
+ if (point.Properties.IsLeftButtonPressed) {
+ if (descriptor.type == UExpressionType.Curve) {
+ switch (ViewModel.CurveViewModel.CurveTool) {
+ case CurveTools.CurveSelectTool:
+ editState = new CurveSelectionState(control, ViewModel, this, descriptor);
+ break;
+ case CurveTools.CurvePenTool:
+ ViewModel.CurveViewModel.ClearSelect();
+ editState = new ExpSetValueState(control, ViewModel, this, descriptor);
+ break;
+ case CurveTools.CurveEraserTool:
+ ViewModel.CurveViewModel.ClearSelect();
+ editState = new ExpResetValueState(control, ViewModel, this, descriptor, MouseButton.Left);
+ break;
+ default:
+ ViewModel.CurveViewModel.ClearSelect();
+ break;
+ }
+ } else {
+ editState = new ExpSetValueState(control, ViewModel, this, descriptor);
+ }
+ Cursor = null;
+ } else if (point.Properties.IsRightButtonPressed) {
+ if (descriptor.type == UExpressionType.Curve && ViewModel.CurveViewModel.CurveTool == CurveTools.CurveSelectTool) {
+ ViewModel.CurveViewModel.ClearSelect();
+ } else {
+ ViewModel.CurveViewModel.ClearSelect();
+ editState = new ExpResetValueState(control, ViewModel, this, descriptor);
+ }
+ Cursor = ViewConstants.cursorNo;
+ }
+ if (editState != null) {
+ editState.ctrlShiftHeld = args.KeyModifiers == (cmdKey | KeyModifiers.Shift);
+ editState.shiftHeld = args.KeyModifiers == KeyModifiers.Shift;
+ editState.Begin(point.Pointer, point.Position);
+ editState.Update(point.Pointer, point.Position);
+ }
+ }
+
+ public void ExpCanvasPointerMoved(object sender, PointerEventArgs args) {
+ var control = (Control)sender;
+ var point = args.GetCurrentPoint(control);
+ args.Handled = true;
+ if (ValueTipCanvas != null) {
+ valueTipPointerPosition = args.GetCurrentPoint(ValueTipCanvas!).Position;
+ }
+ if (editState != null) {
+ editState.ctrlShiftHeld = args.KeyModifiers == (cmdKey | KeyModifiers.Shift);
+ editState.shiftHeld = args.KeyModifiers == KeyModifiers.Shift;
+ editState.Update(point.Pointer, point.Position);
+ } else {
+ Cursor = null;
+ }
+ }
+
+ public void ExpCanvasPointerReleased(object sender, PointerReleasedEventArgs args) {
+ if (editState == null) {
+ return;
+ }
+ if (editState.MouseButton != args.InitialPressMouseButton) {
+ return;
+ }
+ var control = (Control)sender;
+ var point = args.GetCurrentPoint(control);
+ editState.ctrlShiftHeld = args.KeyModifiers == (cmdKey | KeyModifiers.Shift);
+ editState.shiftHeld = args.KeyModifiers == KeyModifiers.Shift;
+ editState.Update(point.Pointer, point.Position);
+ editState.End(point.Pointer, point.Position);
+ editState = null;
+ Cursor = null;
+ }
+
+ public void PhonemeCanvasDoubleTapped(object sender, TappedEventArgs args) {
+ if (ViewModel?.NotesViewModel?.Part == null) {
+ return;
+ }
+ if (sender is not Control control) {
+ return;
+ }
+ var point = args.GetPosition(control);
+ if (editState != null) {
+ editState.End(args.Pointer, point);
+ editState = null;
+ Cursor = null;
+ }
+ var hitInfoAlias = ViewModel.NotesViewModel.HitTest.HitTestAlias(point);
+ var phoneme = hitInfoAlias.phoneme;
+ Log.Debug($"PhonemeCanvasDoubleTapped, hit = {hitInfoAlias.hit}, point = {{{hitInfoAlias.point}}}, phoneme = {phoneme?.phoneme}");
+ if (hitInfoAlias.hit) {
+ LyricBox?.Show(ViewModel.NotesViewModel.Part, new LyricBoxPhoneme(phoneme!), phoneme!.phoneme);
+ return;
+ }
+ }
+
+ public async void PhonemeCanvasPointerPressed(object sender, PointerPressedEventArgs args) {
+ LyricBox?.EndEdit();
+ if (ViewModel?.NotesViewModel?.Part == null) {
+ return;
+ }
+ var control = (Control)sender;
+ var point = args.GetCurrentPoint(control);
+ if (editState != null) {
+ return;
+ }
+ if (point.Properties.IsLeftButtonPressed) {
+ if (args.KeyModifiers == cmdKey) {
+ var hitAliasInfo = ViewModel.NotesViewModel.HitTest.HitTestAlias(args.GetPosition(control));
+ if (hitAliasInfo.hit) {
+ var singer = ViewModel.NotesViewModel.Project.tracks[ViewModel.NotesViewModel.Part.trackNo].Singer;
+ if (Preferences.Default.OtoEditor == 1 && !string.IsNullOrEmpty(Preferences.Default.VLabelerPath)) {
+ Integrations.VLabelerClient.Inst.GotoOto(singer, hitAliasInfo.phoneme.oto);
+ } else {
+ if (MainWindow != null) {
+ await MainWindow.OpenSingersWindowAsync();
+ }
+ RootWindow.Activate();
+ DocManager.Inst.ExecuteCmd(new GotoOtoNotification(singer, hitAliasInfo.phoneme.oto));
+ }
+ return;
+ }
+ }
+ // Plain click on errored phoneme alias shows error details
+ var clickAliasInfo = ViewModel.NotesViewModel.HitTest.HitTestAlias(args.GetPosition(control));
+ if (clickAliasInfo.hit && clickAliasInfo.phoneme.Error && clickAliasInfo.phoneme.ErrorException != null) {
+ _ = MessageBox.ShowError(RootWindow, clickAliasInfo.phoneme.ErrorException);
+ return;
+ }
+ var hitInfo = ViewModel.NotesViewModel.HitTest.HitTestPhoneme(point.Position);
+ if (hitInfo.hit) {
+ var phoneme = hitInfo.phoneme;
+ var note = phoneme.Parent;
+ var index = phoneme.index;
+ if (hitInfo.hitPosition) {
+ editState = new PhonemeMoveState(
+ control, ViewModel, this, note.Extends ?? note, phoneme, index);
+ } else if (hitInfo.hitPreutter) {
+ editState = new PhonemeChangePreutterState(
+ control, ViewModel, this, note.Extends ?? note, phoneme, index);
+ } else if (hitInfo.hitOverlap) {
+ if (phoneme.Next == null || !phoneme.Next.adjacent) {
+ return;
+ }
+ phoneme = hitInfo.phoneme.Next;
+ note = phoneme.Parent;
+ index = phoneme.index;
+ editState = new PhonemeChangeOverlapState(
+ control, ViewModel, this, note.Extends ?? note, phoneme, index);
+ } else if (hitInfo.hitAttackTime) {
+ editState = new PhonemeChangeAttackTimeState(
+ control, ViewModel, this, note.Extends ?? note, phoneme, index);
+ } else if (hitInfo.hitReleaseTime) {
+ editState = new PhonemeChangeReleaseTimeState(
+ control, ViewModel, this, note.Extends ?? note, phoneme, index);
+ }
+ }
+ } else if (point.Properties.IsRightButtonPressed) {
+ editState = new PhonemeResetState(control, ViewModel, this);
+ Cursor = ViewConstants.cursorNo;
+ }
+ if (editState != null) {
+ editState.Begin(point.Pointer, point.Position);
+ editState.Update(point.Pointer, point.Position);
+ }
+ }
+
+ public void PhonemeCanvasPointerMoved(object sender, PointerEventArgs args) {
+ args.Handled = true;
+ if (ViewModel?.NotesViewModel?.Part == null) {
+ return;
+ }
+ if (ValueTipCanvas != null) {
+ valueTipPointerPosition = args.GetCurrentPoint(ValueTipCanvas!).Position;
+ }
+ var control = (Control)sender;
+ var point = args.GetCurrentPoint(control);
+ if (editState != null) {
+ editState.Update(point.Pointer, point.Position);
+ return;
+ }
+ var aliasHitInfo = ViewModel.NotesViewModel.HitTest.HitTestAlias(point.Position);
+ if (aliasHitInfo.hit) {
+ ViewModel.MouseoverPhoneme(aliasHitInfo.phoneme);
+ Cursor = null;
+ return;
+ }
+ var hitInfo = ViewModel.NotesViewModel.HitTest.HitTestPhoneme(point.Position);
+ var adjacent = hitInfo.phoneme != null && hitInfo.phoneme.Next != null && hitInfo.phoneme.Next.adjacent;
+ if (hitInfo.hitPosition || hitInfo.hitPreutter || (hitInfo.hitOverlap && adjacent) || hitInfo.hitAttackTime || hitInfo.hitReleaseTime) {
+ Cursor = ViewConstants.cursorSizeWE;
+ ViewModel.MouseoverPhoneme(null);
+ return;
+ }
+ ViewModel.MouseoverPhoneme(null);
+ Cursor = null;
+ }
+
+ public void PhonemeCanvasPointerReleased(object sender, PointerReleasedEventArgs args) {
+ if (editState == null) {
+ return;
+ }
+ if (editState.MouseButton != args.InitialPressMouseButton) {
+ return;
+ }
+ var control = (Control)sender;
+ var point = args.GetCurrentPoint(control);
+ editState.Update(point.Pointer, point.Position);
+ editState.End(point.Pointer, point.Position);
+ editState = null;
+ Cursor = null;
+ }
+
+ public void BackgroundPointerMoved(object sender, PointerEventArgs args) {
+ Cursor = null;
+ args.Handled = true;
+ }
+
+ public void OnSnapDivMenuButton(object sender, RoutedEventArgs args) {
+ SnapDivMenu.PlacementTarget = sender as Button;
+ SnapDivMenu.Open();
+ }
+
+ void OnSnapDivKeyDown(object sender, KeyEventArgs e) {
+ if (e.Key == Key.Enter && e.KeyModifiers == KeyModifiers.None) {
+ if (sender is ContextMenu menu && menu.SelectedItem is MenuItemViewModel item) {
+ item.Command?.Execute(item.CommandParameter);
+ }
+ }
+ }
+
+ public void OnKeyMenuButton(object sender, RoutedEventArgs args) {
+ KeyMenu.PlacementTarget = sender as Button;
+ KeyMenu.Open();
+ }
+
+ bool MoveToNextPart(bool next) {
+ var notesVm = ViewModel.NotesViewModel;
+ var playVm = ViewModel.PlaybackViewModel;
+ if (notesVm?.Part == null || playVm == null) {
+ return false;
+ }
+ // tick is the center of NotesCanvas
+ var tick = (int)(notesVm.TickOffset + notesVm.Bounds.Width / notesVm.TickWidth / 2 + notesVm.Part.position);
+ var parts = notesVm.Project.parts
+ .Where(part => part is UVoicePart && part.position <= tick && tick <= part.End)
+ .OfType()
+ .OrderBy(part => part.trackNo)
+ .ThenBy(part => part.position)
+ .ToArray();
+ if (parts.Length == 0) {
+ return false;
+ }
+ var index = Array.IndexOf(parts, notesVm.Part);
+ index = next ? index + 1 : index - 1;
+ if (parts.Length <= index) {
+ index = 0;
+ } else if (index < 0) {
+ index = parts.Length - 1;
+ }
+ DocManager.Inst.ExecuteCmd(new LoadPartNotification(parts[index], notesVm.Project, tick));
+ AttachExpressions();
+ return true;
+ }
+
+ void OnKeyKeyDown(object sender, KeyEventArgs e) {
+ if (e.Key == Key.Enter && e.KeyModifiers == KeyModifiers.None) {
+ if (sender is ContextMenu menu && menu.SelectedItem is MenuItemViewModel item) {
+ item.Command?.Execute(item.CommandParameter);
+ }
+ }
+ }
+
+ #region value tip
+
+ void IValueTip.ShowValueTip() {
+ if (ValueTip != null) {
+ ValueTip.IsVisible = true;
+ }
+ }
+
+ void IValueTip.HideValueTip() {
+ if (ValueTip != null) {
+ ValueTip.IsVisible = false;
+ }
+ if (ValueTipText != null) {
+ ValueTipText.Text = string.Empty;
+ }
+ }
+
+ void IValueTip.UpdateValueTip(string text) {
+ if (ValueTip == null || ValueTipText == null || ValueTipCanvas == null) {
+ return;
+ }
+ ValueTipText.Text = text;
+ Canvas.SetLeft(ValueTip, valueTipPointerPosition.X);
+ double tipY = valueTipPointerPosition.Y + 21;
+ if (tipY + 21 > ValueTipCanvas!.Bounds.Height) {
+ tipY = tipY - 42;
+ }
+ Canvas.SetTop(ValueTip, tipY);
+ }
+
+ #endregion
+
+ void OnKeyDown(object? sender, KeyEventArgs args) {
+ var notesVm = ViewModel.NotesViewModel;
+ if (notesVm.Part == null) {
+ args.Handled = false;
+ return;
+ }
+
+ if (RootWindow.FocusManager != null) {
+ if (RootWindow.FocusManager.GetFocusedElement() is TextBox focusedTextBox) {
+ if (focusedTextBox.IsEnabled && focusedTextBox.IsEffectivelyVisible && focusedTextBox.IsFocused) {
+ args.Handled = false;
+ return;
+ }
+ } else if (RootWindow.FocusManager.GetFocusedElement() is ComboBox or ComboBoxItem) {
+ args.Handled = false;
+ return;
+ }
+ }
+ if (LyricBox.IsVisible) {
+ args.Handled = false;
+ return;
+ }
+
+ if (args.Key == Key.R && args.KeyModifiers == KeyModifiers.Control) {
+ var project = DocManager.Inst.Project;
+ var part = notesVm.Part;
+ var selectedNotes = notesVm.Selection.ToList();
+
+ if (part != null && selectedNotes.Count > 0) {
+ noteBatchEditCommand?.Execute(new LoadRenderedPitch()).Subscribe();
+ }
+
+ args.Handled = true;
+ return;
+ }
+
+ // returns true if handled
+ args.Handled = OnKeyExtendedHandler(args);
+ }
+
+ bool OnKeyExtendedHandler(KeyEventArgs args) {
+ var notesVm = ViewModel.NotesViewModel;
+ var playVm = ViewModel.PlaybackViewModel;
+ var curveVm = ViewModel.CurveViewModel;
+ if (notesVm?.Part == null || playVm == null || curveVm == null) {
+ return false;
+ }
+ var project = DocManager.Inst.Project;
+ int snapUnit = project.resolution * 4 / notesVm.SnapDiv;
+ int deltaTicks = notesVm.IsSnapOn ? snapUnit : 15;
+
+ bool isNone = args.KeyModifiers == KeyModifiers.None;
+ bool isAlt = args.KeyModifiers == KeyModifiers.Alt;
+ bool isCtrl = args.KeyModifiers == cmdKey;
+ bool isShift = args.KeyModifiers == KeyModifiers.Shift;
+ bool isBoth = args.KeyModifiers == (cmdKey | KeyModifiers.Shift);
+
+ if (PluginMenu.IsSubMenuOpen && isNone) {
+ if (ViewModel.LegacyPluginShortcuts.ContainsKey(args.Key)) {
+ var plugin = ViewModel.LegacyPluginShortcuts[args.Key];
+ if (plugin != null && plugin.Command != null) {
+ plugin.Command.Execute(plugin.CommandParameter);
+ }
+ }
+ return true;
+ }
+
+ #region tool select keys
+ if (isNone) {
+ switch (args.Key) {
+ case Key.D1: ViewModel.ToolIndex = 0; return true;
+ case Key.D2: ViewModel.ToolIndex = 1; return true;
+ case Key.D3: ViewModel.ToolIndex = 2; return true;
+ case Key.D4: ViewModel.ToolIndex = 3; return true;
+ }
+ }
+ if (isShift) {
+ switch (args.Key) {
+ case Key.D1: ViewModel.ToolIndex = 4; return true;
+ case Key.D2: ViewModel.ToolIndex = 5; return true;
+ case Key.D3: ViewModel.ToolIndex = 6; return true;
+ case Key.D4: ViewModel.ToolIndex = 7; return true;
+ case Key.D5: ViewModel.ToolIndex = 8; return true;
+ }
+ }
+ if (isAlt) {
+ switch (args.Key) {
+ case Key.D1: expSelector1?.SelectExp(); return true;
+ case Key.D2: expSelector2?.SelectExp(); return true;
+ case Key.D3: expSelector3?.SelectExp(); return true;
+ case Key.D4: expSelector4?.SelectExp(); return true;
+ case Key.D5: expSelector5?.SelectExp(); return true;
+ case Key.D6: expSelector6?.SelectExp(); return true;
+ case Key.D7: expSelector7?.SelectExp(); return true;
+ case Key.D8: expSelector8?.SelectExp(); return true;
+ case Key.D9: expSelector9?.SelectExp(); return true;
+ case Key.D0: expSelector10?.SelectExp(); return true;
+ }
+ }
+ #endregion
+ switch (args.Key) {
+ #region document keys
+ case Key.Space:
+ if (isNone) {
+ playVm.PlayOrPause();
+ return true;
+ }
+ if (isAlt) {
+ if (!notesVm.Selection.IsEmpty) {
+ playVm.PlayOrPause(
+ tick: notesVm.Part.position + notesVm.Selection.FirstOrDefault()!.position,
+ endTick: notesVm.Part.position + notesVm.Selection.LastOrDefault()!.RightBound
+ );
+ }
+ return true;
+ }
+ break;
+ case Key.Escape:
+ if (isNone) {
+ // collapse/empty selection
+ var numSelected = notesVm.Selection.Count;
+ // if single or all notes then clear
+ if (numSelected == 1 || numSelected == notesVm.Part.notes.Count) {
+ notesVm.DeselectNotes();
+ } else if (numSelected > 1) {
+ // collapse selection
+ notesVm.SelectNote(notesVm.Selection.Head!);
+ }
+ return true;
+ }
+ break;
+ case Key.F4:
+ if (isAlt) {
+ if (RootWindow is PianoRollDetachedWindow) {
+ RootWindow.Hide();
+ }
+ return true;
+ }
+ break;
+ case Key.F11:
+ OnMenuFullScreen(this, new RoutedEventArgs());
+ return true;
+ case Key.Enter:
+ if (isNone) {
+ if (notesVm.Selection.Count == 1) {
+ var note = notesVm.Selection.First();
+ LyricBox?.Show(ViewModel.NotesViewModel.Part!, new LyricBoxNote(note), note.lyric);
+ } else if (notesVm.Selection.Count > 1) {
+ EditLyrics();
+ }
+ return true;
+ }
+ break;
+ #endregion
+ #region toggle show keyws
+ case Key.R:
+ if (isNone) {
+ notesVm.ShowFinalPitch = !notesVm.ShowFinalPitch;
+ return true;
+ }
+ break;
+ case Key.T:
+ if (isNone) {
+ notesVm.ShowTips = !notesVm.ShowTips;
+ return true;
+ }
+ break;
+ case Key.U:
+ if (isNone) {
+ notesVm.ShowVibrato = !notesVm.ShowVibrato;
+ return true;
+ }
+ if (isCtrl) {
+ notesVm.MergeSelectedNotes();
+ return true;
+ }
+ break;
+ case Key.I:
+ if (isNone) {
+ notesVm.ShowPitch = !notesVm.ShowPitch;
+ return true;
+ }
+ break;
+ case Key.O:
+ if (isNone) {
+ notesVm.ShowPhoneme = !notesVm.ShowPhoneme;
+ return true;
+ }
+ break;
+ case Key.L:
+ if (isNone) {
+ notesVm.ShowExpressions = !notesVm.ShowExpressions;
+ return true;
+ }
+ break;
+ case Key.P:
+ if (isNone) {
+ notesVm.IsSnapOn = !notesVm.IsSnapOn;
+ return true;
+ }
+ if (isAlt) {
+ SnapDivMenu.Open();
+ return true;
+ }
+ break;
+ case Key.OemPipe:
+ if (isNone) {
+ notesVm.ShowNoteParams = !notesVm.ShowNoteParams;
+ return true;
+ }
+ break;
+ #endregion
+ #region navigate keys
+ // NAVIGATE/EDIT/SELECT HANDLERS
+ case Key.Up:
+ if (isNone) {
+ notesVm.TransposeSelection(1);
+ return true;
+ }
+ if (isCtrl) {
+ notesVm.TransposeSelection(12);
+ return true;
+ }
+ break;
+ case Key.Down:
+ if (isNone) {
+ notesVm.TransposeSelection(-1);
+ return true;
+ }
+ if (isCtrl) {
+ notesVm.TransposeSelection(-12);
+ return true;
+ }
+ break;
+ case Key.Left:
+ if (isNone) {
+ notesVm.MoveCursor(-1);
+ return true;
+ }
+ if (isAlt) {
+ notesVm.ResizeSelectedNotes(-1 * deltaTicks);
+ return true;
+ }
+ if (isCtrl) {
+ notesVm.MoveSelectedNotes(-1 * deltaTicks);
+ return true;
+ }
+ if (isShift) {
+ notesVm.ExtendSelection(-1);
+ return true;
+ }
+ break;
+ case Key.Right:
+ if (isNone) {
+ notesVm.MoveCursor(1);
+ return true;
+ }
+ if (isAlt) {
+ notesVm.ResizeSelectedNotes(deltaTicks);
+ return true;
+ }
+ if (isCtrl) {
+ notesVm.MoveSelectedNotes(deltaTicks);
+ return true;
+ }
+ if (isShift) {
+ notesVm.ExtendSelection(1);
+ return true;
+ }
+ break;
+ case Key.OemPlus:
+ if (isNone) {
+ notesVm.ResizeSelectedNotes(deltaTicks);
+ return true;
+ }
+ break;
+ case Key.OemMinus:
+ if (isNone) {
+ notesVm.ResizeSelectedNotes(-1 * deltaTicks);
+ return true;
+ }
+ break;
+ #endregion
+ #region clipboard and edit keys
+ case Key.Z:
+ if (isBoth) {
+ ViewModel.Redo();
+ return true;
+ }
+ if (isCtrl) {
+ ViewModel.Undo();
+ return true;
+ }
+ break;
+ case Key.Y:
+ // toggle play tone
+ if (isNone) {
+ notesVm.PlayTone = !notesVm.PlayTone;
+ return true;
+ }
+ if (isCtrl) {
+ ViewModel.Redo();
+ return true;
+ }
+ break;
+ case Key.C:
+ if (isCtrl) {
+ if (curveVm.IsSelected(notesVm.PrimaryKey)) {
+ curveVm.Copy(notesVm.Part);
+ } else {
+ notesVm.CopyNotes();
+ }
+ return true;
+ }
+ break;
+ case Key.X:
+ if (isCtrl) {
+ if (curveVm.IsSelected(notesVm.PrimaryKey)) {
+ curveVm.Cut(notesVm.Part);
+ } else {
+ notesVm.CutNotes();
+ }
+ return true;
+ }
+ break;
+ case Key.V:
+ if (isBoth) {
+ notesVm.PastePlainNotes();
+ return true;
+ }
+ if (isCtrl) {
+ if (DocManager.Inst.NotesClipboard != null && DocManager.Inst.NotesClipboard.Count > 0) {
+ notesVm.PasteNotes();
+ } else if (DocManager.Inst.CurvesClipboard != null) {
+ var track = project.tracks[notesVm.Part.trackNo];
+ if (track.TryGetExpDescriptor(project, notesVm.PrimaryKey, out var descriptor)) {
+ curveVm.Paste(notesVm.Part, descriptor);
+ }
+ }
+ return true;
+ }
+ if (isAlt) {
+ notesVm.PasteSelectedParams(RootWindow);
+ return true;
+ }
+ break;
+ case Key.N:
+ if (isNone && PluginMenu.Parent is MenuItem batch) {
+ batch.Open();
+ PluginMenu.Open();
+ return true;
+ }
+ break;
+ // INSERT + DELETE
+ case Key.Insert:
+ if (isNone) {
+ notesVm.InsertNote();
+ return true;
+ }
+ break;
+ case Key.Delete:
+ case Key.Back:
+ if (isNone) {
+ notesVm.DeleteSelectedNotes();
+ return true;
+ }
+ break;
+ #endregion
+ #region play position and select keys
+ // PLAY POSITION + SELECTION
+ case Key.Home:
+ if (isNone) {
+ playVm.MovePlayPos(notesVm.Part.position);
+ return true;
+ }
+ if (isShift) {
+ var first = notesVm.Part.notes.FirstOrDefault();
+ if (first != null) {
+ notesVm.ExtendSelection(first);
+ }
+ return true;
+ }
+ break;
+ case Key.End:
+ if (isNone) {
+ playVm.MovePlayPos(notesVm.Part.End);
+ HScrollBar.Value = HScrollBar.Maximum;
+ return true;
+ }
+ if (isShift) {
+ var last = notesVm.Part.notes.LastOrDefault();
+ if (last != null) {
+ notesVm.ExtendSelection(last);
+ }
+ return true;
+ }
+ break;
+ case Key.OemOpenBrackets:
+ // move playhead left
+ if (isNone) {
+ playVm.MovePlayPos(playVm.PlayPosTick - snapUnit);
+ return true;
+ }
+ // to selection start
+ if (isCtrl) {
+ if (!notesVm.Selection.IsEmpty) {
+ playVm.MovePlayPos(notesVm.Part.position + notesVm.Selection.FirstOrDefault()!.position);
+ }
+ return true;
+ }
+ // to view start
+ if (isShift) {
+ playVm.MovePlayPos(notesVm.Part.position + (int)notesVm.TickOffset);
+ return true;
+ }
+ break;
+ case Key.OemCloseBrackets:
+ // move playhead right
+ if (isNone) {
+ playVm.MovePlayPos(playVm.PlayPosTick + snapUnit);
+ return true;
+ }
+ // to selection end
+ if (isCtrl) {
+ if (!notesVm.Selection.IsEmpty) {
+ playVm.MovePlayPos(notesVm.Part.position + notesVm.Selection.LastOrDefault()!.RightBound);
+ }
+ return true;
+ }
+ // to view end
+ if (isShift) {
+ playVm.MovePlayPos(notesVm.Part.position + (int)(notesVm.TickOffset + notesVm.Bounds.Width / notesVm.TickWidth));
+ return true;
+ }
+ break;
+
+ #endregion
+ #region scroll and select keys
+ // SCROLL / SELECT
+ case Key.A:
+ // scroll left
+ if (isNone) {
+ notesVm.TickOffset = Math.Max(0, notesVm.TickOffset - snapUnit);
+ return true;
+ }
+ // select all
+ if (isCtrl) {
+ notesVm.SelectAllNotes();
+ return true;
+ }
+ break;
+ case Key.D:
+ // scroll right
+ if (isNone) {
+ notesVm.TickOffset = Math.Min(notesVm.TickOffset + snapUnit, notesVm.HScrollBarMax);
+ return true;
+ }
+ // select none
+ if (isCtrl) {
+ notesVm.DeselectNotes();
+ return true;
+ }
+ break;
+ case Key.W:
+ // toggle show waveform
+ if (isNone) {
+ notesVm.ShowWaveform = !notesVm.ShowWaveform;
+ return true;
+ }
+ // scroll up
+ // NOTE set to alt to avoid conflict with showwaveform toggle
+ if (isAlt) {
+ notesVm.TrackOffset = Math.Max(notesVm.TrackOffset - 2, 0);
+ return true;
+ }
+ break;
+ case Key.S:
+ // scroll down
+ if (isAlt) {
+ notesVm.TrackOffset = Math.Min(notesVm.TrackOffset + 2, notesVm.VScrollBarMax);
+ return true;
+ }
+ if (isCtrl) {
+ _ = MainWindow?.Save();
+ return true;
+ }
+ // solo
+ if (isShift) {
+ var track = project.tracks[notesVm.Part.trackNo];
+ MessageBus.Current.SendMessage(new TracksSoloEvent(notesVm.Part.trackNo, !track.Solo, false));
+ return true;
+ }
+ break;
+ case Key.M:
+ // mute
+ if (isShift) {
+ MessageBus.Current.SendMessage(new TracksMuteEvent(notesVm.Part.trackNo, false));
+ return true;
+ }
+ break;
+ case Key.F:
+ // scroll selection into focus
+ if (isNone) {
+ var note = notesVm.Selection.FirstOrDefault();
+ if (note != null) {
+ DocManager.Inst.ExecuteCmd(new FocusNoteNotification(notesVm.Part, note));
+ }
+ return true;
+ }
+ if (isCtrl) {
+ SearchNote();
+ return true;
+ }
+ if (isAlt) {
+ if (!notesVm.Selection.IsEmpty) {
+ playVm.MovePlayPos(notesVm.Part.position + notesVm.Selection.FirstOrDefault()!.position);
+ }
+ return true;
+ }
+ break;
+ case Key.E:
+ // zoom in
+ if (isNone) {
+ double x = 0;
+ double y = 0;
+ if (!notesVm.Selection.IsEmpty) {
+ x = (notesVm.Selection.Head!.position - notesVm.TickOffset) / notesVm.ViewportTicks;
+ y = (ViewConstants.MaxTone - 1 - notesVm.Selection.Head.tone - notesVm.TrackOffset) / notesVm.ViewportTracks;
+ } else if (notesVm.TickOffset != 0) {
+ x = 0.5;
+ y = 0.5;
+ }
+ notesVm.OnXZoomed(new Point(x, y), 0.1);
+ return true;
+ }
+ break;
+ case Key.Q:
+ // zoom out
+ if (isNone) {
+ double x = 0;
+ double y = 0;
+ if (!notesVm.Selection.IsEmpty) {
+ x = (notesVm.Selection.Head!.position - notesVm.TickOffset) / notesVm.ViewportTicks;
+ y = (ViewConstants.MaxTone - 1 - notesVm.Selection.Head.tone - notesVm.TrackOffset) / notesVm.ViewportTracks;
+ } else if (notesVm.TickOffset != 0) {
+ x = 0.5;
+ y = 0.5;
+ }
+ notesVm.OnXZoomed(new Point(x, y), -0.1);
+ return true;
+ }
+ break;
+ #endregion
+ #region move to the next track part
+ case Key.PageUp: {
+ if (isNone) {
+ MoveToNextPart(false);
+ return true;
+ }
+ }
+ break;
+ case Key.PageDown: {
+ if (isNone) {
+ MoveToNextPart(true);
+ return true;
+ }
+ }
+ break;
+ #endregion
+ }
+ return false;
+ }
+
+ public void AttachExpressions() {
+ if (expSelector1 == null) {
+ return;
+ }
+ var exps = new ExpSelector[] { expSelector1, expSelector2, expSelector3, expSelector4, expSelector5, expSelector6, expSelector7, expSelector8, expSelector9, expSelector10 };
+ exps[DocManager.Inst.Project.expSecondary].SelectExp();
+ exps[DocManager.Inst.Project.expPrimary].SelectExp();
+ }
+
+ public void OnNext(UCommand cmd, bool isUndo) {
+ if (cmd is LoadingNotification loadingNotif && loadingNotif.window == typeof(PianoRoll)) {
+ if (loadingNotif.startLoading) {
+ LoadingWindow.BeginLoadingImmediate(RootWindow);
+ } else {
+ LoadingWindow.EndLoading();
+ }
+ }
+ }
+ }
}
\ No newline at end of file
From 5c917915f08be1cd467b2f4a5b4db07b0f8617b5 Mon Sep 17 00:00:00 2001
From: 27704534 <1374232024@qq.com>
Date: Thu, 25 Jun 2026 11:12:52 +0800
Subject: [PATCH 6/6] Add files via upload
add
---
OpenUtau/ViewModels/PianoRollViewModel.cs | 645 +++++++++++-----------
1 file changed, 323 insertions(+), 322 deletions(-)
diff --git a/OpenUtau/ViewModels/PianoRollViewModel.cs b/OpenUtau/ViewModels/PianoRollViewModel.cs
index f4f1bfbcb..e8572a592 100644
--- a/OpenUtau/ViewModels/PianoRollViewModel.cs
+++ b/OpenUtau/ViewModels/PianoRollViewModel.cs
@@ -1,322 +1,323 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reactive;
-using Avalonia.Input;
-using Avalonia.Threading;
-using DynamicData.Binding;
-using OpenUtau.App.Controls;
-using OpenUtau.Classic;
-using OpenUtau.Core;
-using OpenUtau.Core.Ustx;
-using OpenUtau.Core.Util;
-using OpenUtau.ViewModels;
-using ReactiveUI;
-using ReactiveUI.Fody.Helpers;
-
-namespace OpenUtau.App.ViewModels {
- public class PhonemeMouseoverEvent {
- public readonly UPhoneme? mouseoverPhoneme;
- public PhonemeMouseoverEvent(UPhoneme? mouseoverPhoneme) {
- this.mouseoverPhoneme = mouseoverPhoneme;
- }
- }
-
- public class NotesContextMenuArgs {
- public PianoRollViewModel? ViewModel { get; set; }
-
- public bool ForNote { get; set; }
- public NoteHitInfo NoteHitInfo { get; set; }
-
- public bool ForPitchPoint { get; set; }
- public bool PitchPointIsFirst { get; set; }
- public bool PitchPointCanDel { get; set; }
- public bool PitchPointCanAdd { get; set; }
- public PitchPointHitInfo PitchPointHitInfo { get; set; }
- }
-
- public class PianorollRefreshEvent {
- public readonly string refreshItem;
- public PianorollRefreshEvent(string refreshItem) {
- this.refreshItem = refreshItem;
- }
- }
-
- public class PianoRollViewModel : ViewModelBase, ICmdSubscriber {
-
- [Reactive] public NotesViewModel NotesViewModel { get; set; }
- [Reactive] public PlaybackViewModel? PlaybackViewModel { get; set; }
- [Reactive] public CurveViewModel CurveViewModel { get; set; }
-
- public double Width => Preferences.Default.PianorollWindowSize.Width;
- public double Height => Preferences.Default.PianorollWindowSize.Height;
-
- public bool LockPitchPoints { get => Preferences.Default.LockUnselectedNotesPitch; }
- public bool LockVibrato { get => Preferences.Default.LockUnselectedNotesVibrato; }
- public bool LockExpressions { get => Preferences.Default.LockUnselectedNotesExpressions; }
- public bool ShowPortrait { get => Preferences.Default.ShowPortrait; }
- public bool ShowIcon { get => Preferences.Default.ShowIcon; }
- public bool ShowGhostNotes { get => Preferences.Default.ShowGhostNotes; }
- public bool UseTrackColor { get => Preferences.Default.UseTrackColor; }
- public bool DegreeStyle0 { get => Preferences.Default.DegreeStyle == 0 ? true : false; }
- public bool DegreeStyle1 { get => Preferences.Default.DegreeStyle == 1 ? true : false; }
- public bool DegreeStyle2 { get => Preferences.Default.DegreeStyle == 2 ? true : false; }
- public bool LockStartTime0 { get => Preferences.Default.LockStartTime == 0 ? true : false; }
- public bool LockStartTime1 { get => Preferences.Default.LockStartTime == 1 ? true : false; }
- public bool LockStartTime2 { get => Preferences.Default.LockStartTime == 2 ? true : false; }
- public bool PlaybackAutoScroll0 { get => Preferences.Default.PlaybackAutoScroll == 0 ? true : false; }
- public bool PlaybackAutoScroll1 { get => Preferences.Default.PlaybackAutoScroll == 1 ? true : false; }
- public bool PlaybackAutoScroll2 { get => Preferences.Default.PlaybackAutoScroll == 2 ? true : false; }
- public bool PianoRollDetached { get => Preferences.Default.DetachPianoRoll; }
- public bool ShowPhonemizerTags {
- get => Preferences.Default.ShowPhonemizerTags;
- set {
- Preferences.Default.ShowPhonemizerTags = value;
- Preferences.Save();
- this.RaisePropertyChanged(nameof(ShowPhonemizerTags));
- }
- }
-
- public EditTool EditTool { get; set; } = Preferences.Default.EditTool;
- [Reactive] public int ToolIndex { get; set; } = Preferences.Default.EditTool.BaseTool;
- [Reactive] public int PenToolIndex { get; set; } = Preferences.Default.EditTool.PenToolVariation;
- [Reactive] public bool PitchOverwrite { get; set; } = Preferences.Default.EditTool.OverwritePitch;
-
- public ObservableCollectionExtended LegacyPlugins { get; private set; }
- = new ObservableCollectionExtended();
- public ObservableCollectionExtended NoteBatchEdits { get; private set; }
- = new ObservableCollectionExtended();
- public ObservableCollectionExtended LyricBatchEdits { get; private set; }
- = new ObservableCollectionExtended();
- public ObservableCollectionExtended ResetBatchEdits { get; private set; }
- = new ObservableCollectionExtended();
- public ObservableCollectionExtended ExternalBatchEdits { get; private set; }
- = new ObservableCollectionExtended();
- public ObservableCollectionExtended NotesContextMenuItems { get; private set; }
- = new ObservableCollectionExtended();
- public Dictionary LegacyPluginShortcuts { get; private set; }
- = new Dictionary();
-
- [Reactive] public double Progress { get; set; }
- [Reactive] public bool CanUndo { get; set; } = false;
- [Reactive] public bool CanRedo { get; set; } = false;
- [Reactive] public string UndoText { get; set; } = ThemeManager.GetString("menu.edit.undo");
- [Reactive] public string RedoText { get; set; } = ThemeManager.GetString("menu.edit.redo");
-
- public ReactiveCommand NoteDeleteCommand { get; set; }
- public ReactiveCommand NoteCopyCommand { get; set; }
- public ReactiveCommand ClearPhraseCacheCommand { get; set; }
- public ReactiveCommand PitEaseInOutCommand { get; set; }
- public ReactiveCommand PitLinearCommand { get; set; }
- public ReactiveCommand PitEaseInCommand { get; set; }
- public ReactiveCommand PitEaseOutCommand { get; set; }
- public ReactiveCommand PitSplineCommand { get; set; }
- public ReactiveCommand PitSnapCommand { get; set; }
- public ReactiveCommand PitDelCommand { get; set; }
- public ReactiveCommand PitAddCommand { get; set; }
-
- private ReactiveCommand legacyPluginCommand;
-
- public PianoRollViewModel() {
- NotesViewModel = new NotesViewModel();
- CurveViewModel = new CurveViewModel();
-
- this.WhenAnyValue(vm => vm.ToolIndex)
- .Subscribe(index => EditTool.BaseTool = index);
- this.WhenAnyValue(vm => vm.PenToolIndex)
- .Subscribe(index => EditTool.PenToolVariation = index);
- this.WhenAnyValue(vm => vm.PitchOverwrite)
- .Subscribe(val => { EditTool.OverwritePitch = val; Preferences.Default.EditTool.OverwritePitch = val; Preferences.Save(); });
-
- NoteDeleteCommand = ReactiveCommand.Create(info => {
- NotesViewModel.DeleteSelectedNotes();
- });
- NoteCopyCommand = ReactiveCommand.Create(info => {
- NotesViewModel.CopyNotes();
- });
- ClearPhraseCacheCommand = ReactiveCommand.Create(info => {
- NotesViewModel.ClearPhraseCache();
- });
- PitEaseInOutCommand = ReactiveCommand.Create(info => {
- if (NotesViewModel.Part == null) { return; }
- DocManager.Inst.StartUndoGroup("command.pitch.editpoint");
- DocManager.Inst.ExecuteCmd(new ChangePitchPointShapeCommand(NotesViewModel.Part, info.Note.pitch.data[info.Index], PitchPointShape.io));
- DocManager.Inst.EndUndoGroup();
- });
- PitLinearCommand = ReactiveCommand.Create(info => {
- if (NotesViewModel.Part == null) { return; }
- DocManager.Inst.StartUndoGroup("command.pitch.editpoint");
- DocManager.Inst.ExecuteCmd(new ChangePitchPointShapeCommand(NotesViewModel.Part, info.Note.pitch.data[info.Index], PitchPointShape.l));
- DocManager.Inst.EndUndoGroup();
- });
- PitEaseInCommand = ReactiveCommand.Create(info => {
- if (NotesViewModel.Part == null) { return; }
- DocManager.Inst.StartUndoGroup("command.pitch.editpoint");
- DocManager.Inst.ExecuteCmd(new ChangePitchPointShapeCommand(NotesViewModel.Part, info.Note.pitch.data[info.Index], PitchPointShape.i));
- DocManager.Inst.EndUndoGroup();
- });
- PitEaseOutCommand = ReactiveCommand.Create(info => {
- if (NotesViewModel.Part == null) { return; }
- DocManager.Inst.StartUndoGroup("command.pitch.editpoint");
- DocManager.Inst.ExecuteCmd(new ChangePitchPointShapeCommand(NotesViewModel.Part, info.Note.pitch.data[info.Index], PitchPointShape.o));
- DocManager.Inst.EndUndoGroup();
- });
- PitSplineCommand = ReactiveCommand.Create(info => {
- if (NotesViewModel.Part == null) { return; }
- DocManager.Inst.StartUndoGroup("command.pitch.editpoint");
- DocManager.Inst.ExecuteCmd(new ChangePitchPointShapeCommand(NotesViewModel.Part, info.Note.pitch.data[info.Index], PitchPointShape.sp));
- DocManager.Inst.EndUndoGroup();
- });
- PitSnapCommand = ReactiveCommand.Create(info => {
- if (NotesViewModel.Part == null) { return; }
- DocManager.Inst.StartUndoGroup("command.pitch.editpoint");
- DocManager.Inst.ExecuteCmd(new SnapPitchPointCommand(NotesViewModel.Part, info.Note));
- DocManager.Inst.EndUndoGroup();
- });
- PitDelCommand = ReactiveCommand.Create(info => {
- if (NotesViewModel.Part == null) { return; }
- DocManager.Inst.StartUndoGroup("command.pitch.delete");
- DocManager.Inst.ExecuteCmd(new DeletePitchPointCommand(NotesViewModel.Part, info.Note, info.Index));
- DocManager.Inst.EndUndoGroup();
- });
- PitAddCommand = ReactiveCommand.Create(info => {
- if (NotesViewModel.Part == null) { return; }
- DocManager.Inst.StartUndoGroup("command.pitch.add");
- DocManager.Inst.ExecuteCmd(new AddPitchPointCommand(NotesViewModel.Part, info.Note, new PitchPoint(info.X, info.Y, NotePresets.Default.DefaultPitchShape), info.Index + 1));
- DocManager.Inst.EndUndoGroup();
- });
-
- legacyPluginCommand = ReactiveCommand.Create(async plugin => {
- if (NotesViewModel.Part == null || NotesViewModel.Part.notes.Count == 0) {
- return;
- }
- DocManager.Inst.ExecuteCmd(new LoadingNotification(typeof(PianoRoll), true, "legacy plugin"));
-
- try {
- var part = NotesViewModel.Part;
- UNote? first;
- UNote? last;
- if (NotesViewModel.Selection.IsEmpty) {
- first = part.notes.First();
- last = part.notes.Last();
- } else {
- first = NotesViewModel.Selection.FirstOrDefault();
- last = NotesViewModel.Selection.LastOrDefault();
- }
- var runner = PluginRunner.from(PathManager.Inst, DocManager.Inst);
- await runner.Execute(NotesViewModel.Project, part, first, last, plugin);
-
- } catch (Exception e) {
- DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e));
- } finally {
- DocManager.Inst.ExecuteCmd(new LoadingNotification(typeof(PianoRoll), false, "legacy plugin"));
- }
- });
- LoadLegacyPlugins();
- DocManager.Inst.AddSubscriber(this);
- }
-
- private void SetUndoState() {
- CanUndo = DocManager.Inst.GetUndoState(out string? undoNameKey);
- if (!string.IsNullOrWhiteSpace(undoNameKey)) {
- UndoText = $"{ThemeManager.GetString("menu.edit.undo")}: {ThemeManager.GetString(undoNameKey)}";
- } else {
- UndoText = ThemeManager.GetString("menu.edit.undo");
- }
- CanRedo = DocManager.Inst.GetRedoState(out string? redoNameKey);
- if (!string.IsNullOrWhiteSpace(redoNameKey)) {
- RedoText = $"{ThemeManager.GetString("menu.edit.redo")}: {ThemeManager.GetString(redoNameKey)}";
- } else {
- RedoText = ThemeManager.GetString("menu.edit.redo");
- }
- }
-
- private void LoadLegacyPlugins() {
- LegacyPlugins.Clear();
- LegacyPlugins.AddRange(DocManager.Inst.Plugins.Select(plugin => new MenuItemViewModel() {
- Header = plugin.Name,
- Command = legacyPluginCommand,
- CommandParameter = plugin,
- }));
-
- LegacyPluginShortcuts.Clear();
- foreach (MenuItemViewModel menu in LegacyPlugins) {
- if (menu.CommandParameter is Classic.Plugin plugin) {
- if (Enum.TryParse(plugin.Shortcut, out Key key) && !LegacyPluginShortcuts.ContainsKey(key)) {
- LegacyPluginShortcuts.Add(key, menu);
- }
- }
- }
- LegacyPlugins.Add(new MenuItemViewModel() { // Separator
- Header = "-",
- Height = 1
- });
- LegacyPlugins.Add(new MenuItemViewModel() {
- Header = ThemeManager.GetString("pianoroll.menu.plugin.openfolder"),
- Command = ReactiveCommand.Create(() => {
- try {
- OS.OpenFolder(PathManager.Inst.PluginsPath);
- } catch (Exception e) {
- DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e));
- }
- })
- });
- LegacyPlugins.Add(new MenuItemViewModel() {
- Header = ThemeManager.GetString("pianoroll.menu.plugin.reload"),
- Command = ReactiveCommand.Create(() => {
- DocManager.Inst.SearchAllLegacyPlugins();
- LoadLegacyPlugins();
- })
- });
- }
-
- public void Undo() => DocManager.Inst.Undo();
- public void Redo() => DocManager.Inst.Redo();
- public void Cut() {
- if (CurveViewModel.IsSelected(NotesViewModel.PrimaryKey)) {
- CurveViewModel.Cut(NotesViewModel.Part!);
- } else {
- NotesViewModel.CutNotes();
- }
- }
- public void Copy() {
- if (CurveViewModel.IsSelected(NotesViewModel.PrimaryKey)) {
- CurveViewModel.Copy(NotesViewModel.Part!);
- } else {
- NotesViewModel.CopyNotes();
- }
- }
- public void Paste() {
- if (DocManager.Inst.NotesClipboard != null && DocManager.Inst.NotesClipboard.Count > 0) {
- NotesViewModel.PasteNotes();
- } else if (DocManager.Inst.CurvesClipboard != null && NotesViewModel.Part != null) {
- var track = NotesViewModel.Project.tracks[NotesViewModel.Part.trackNo];
- if (track.TryGetExpDescriptor(NotesViewModel.Project, NotesViewModel.PrimaryKey, out var descriptor)) {
- CurveViewModel.Paste(NotesViewModel.Part, descriptor);
- }
- }
- }
- public void PastePlain() => NotesViewModel.PastePlainNotes();
- public void Delete() => NotesViewModel.DeleteSelectedNotes();
- public void SelectAll() => NotesViewModel.SelectAllNotes();
-
- public void MouseoverPhoneme(UPhoneme? phoneme) {
- MessageBus.Current.SendMessage(new PhonemeMouseoverEvent(phoneme));
- }
-
- #region ICmdSubscriber
-
- public void OnNext(UCommand cmd, bool isUndo) {
- if (cmd is ProgressBarNotification progressBarNotification) {
- if (PianoRollDetached) {
- Dispatcher.UIThread.InvokeAsync(() => {
- Progress = progressBarNotification.Progress;
- }, DispatcherPriority.Background);
- }
- }
- SetUndoState();
- }
-
- #endregion
- }
-}
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive;
+using Avalonia.Input;
+using Avalonia.Threading;
+using DynamicData.Binding;
+using OpenUtau.App.Controls;
+using OpenUtau.Classic;
+using OpenUtau.Core;
+using OpenUtau.Core.Ustx;
+using OpenUtau.Core.Util;
+using OpenUtau.ViewModels;
+using ReactiveUI;
+using ReactiveUI.Fody.Helpers;
+
+namespace OpenUtau.App.ViewModels {
+ public class PhonemeMouseoverEvent {
+ public readonly UPhoneme? mouseoverPhoneme;
+ public PhonemeMouseoverEvent(UPhoneme? mouseoverPhoneme) {
+ this.mouseoverPhoneme = mouseoverPhoneme;
+ }
+ }
+
+ public class NotesContextMenuArgs {
+ public PianoRollViewModel? ViewModel { get; set; }
+
+ public bool ForNote { get; set; }
+ public NoteHitInfo NoteHitInfo { get; set; }
+
+ public bool ForPitchPoint { get; set; }
+ public bool PitchPointIsFirst { get; set; }
+ public bool PitchPointCanDel { get; set; }
+ public bool PitchPointCanAdd { get; set; }
+ public PitchPointHitInfo PitchPointHitInfo { get; set; }
+ }
+
+ public class PianorollRefreshEvent {
+ public readonly string refreshItem;
+ public PianorollRefreshEvent(string refreshItem) {
+ this.refreshItem = refreshItem;
+ }
+ }
+
+ public class PianoRollViewModel : ViewModelBase, ICmdSubscriber {
+
+ [Reactive] public NotesViewModel NotesViewModel { get; set; }
+ [Reactive] public PlaybackViewModel? PlaybackViewModel { get; set; }
+ [Reactive] public CurveViewModel CurveViewModel { get; set; }
+
+ public double Width => Preferences.Default.PianorollWindowSize.Width;
+ public double Height => Preferences.Default.PianorollWindowSize.Height;
+
+ public bool LockPitchPoints { get => Preferences.Default.LockUnselectedNotesPitch; }
+ public bool LockVibrato { get => Preferences.Default.LockUnselectedNotesVibrato; }
+ public bool LockExpressions { get => Preferences.Default.LockUnselectedNotesExpressions; }
+ public bool ShowPortrait { get => Preferences.Default.ShowPortrait; }
+ public bool ShowIcon { get => Preferences.Default.ShowIcon; }
+ public bool ShowGhostNotes { get => Preferences.Default.ShowGhostNotes; }
+ public bool UseTrackColor { get => Preferences.Default.UseTrackColor; }
+ public bool DegreeStyle0 { get => Preferences.Default.DegreeStyle == 0 ? true : false; }
+ public bool DegreeStyle1 { get => Preferences.Default.DegreeStyle == 1 ? true : false; }
+ public bool DegreeStyle2 { get => Preferences.Default.DegreeStyle == 2 ? true : false; }
+ public bool LockStartTime0 { get => Preferences.Default.LockStartTime == 0 ? true : false; }
+ public bool LockStartTime1 { get => Preferences.Default.LockStartTime == 1 ? true : false; }
+ public bool LockStartTime2 { get => Preferences.Default.LockStartTime == 2 ? true : false; }
+ public bool PlaybackAutoScroll0 { get => Preferences.Default.PlaybackAutoScroll == 0 ? true : false; }
+ public bool PlaybackAutoScroll1 { get => Preferences.Default.PlaybackAutoScroll == 1 ? true : false; }
+ public bool PlaybackAutoScroll2 { get => Preferences.Default.PlaybackAutoScroll == 2 ? true : false; }
+ public bool PianoRollDetached { get => Preferences.Default.DetachPianoRoll; }
+ public bool HideMenuItemVisible => !Preferences.Default.DetachPianoRoll;
+ public bool ShowPhonemizerTags {
+ get => Preferences.Default.ShowPhonemizerTags;
+ set {
+ Preferences.Default.ShowPhonemizerTags = value;
+ Preferences.Save();
+ this.RaisePropertyChanged(nameof(ShowPhonemizerTags));
+ }
+ }
+
+ public EditTool EditTool { get; set; } = Preferences.Default.EditTool;
+ [Reactive] public int ToolIndex { get; set; } = Preferences.Default.EditTool.BaseTool;
+ [Reactive] public int PenToolIndex { get; set; } = Preferences.Default.EditTool.PenToolVariation;
+ [Reactive] public bool PitchOverwrite { get; set; } = Preferences.Default.EditTool.OverwritePitch;
+
+ public ObservableCollectionExtended LegacyPlugins { get; private set; }
+ = new ObservableCollectionExtended();
+ public ObservableCollectionExtended NoteBatchEdits { get; private set; }
+ = new ObservableCollectionExtended();
+ public ObservableCollectionExtended LyricBatchEdits { get; private set; }
+ = new ObservableCollectionExtended();
+ public ObservableCollectionExtended ResetBatchEdits { get; private set; }
+ = new ObservableCollectionExtended();
+ public ObservableCollectionExtended ExternalBatchEdits { get; private set; }
+ = new ObservableCollectionExtended();
+ public ObservableCollectionExtended NotesContextMenuItems { get; private set; }
+ = new ObservableCollectionExtended();
+ public Dictionary LegacyPluginShortcuts { get; private set; }
+ = new Dictionary();
+
+ [Reactive] public double Progress { get; set; }
+ [Reactive] public bool CanUndo { get; set; } = false;
+ [Reactive] public bool CanRedo { get; set; } = false;
+ [Reactive] public string UndoText { get; set; } = ThemeManager.GetString("menu.edit.undo");
+ [Reactive] public string RedoText { get; set; } = ThemeManager.GetString("menu.edit.redo");
+
+ public ReactiveCommand NoteDeleteCommand { get; set; }
+ public ReactiveCommand NoteCopyCommand { get; set; }
+ public ReactiveCommand ClearPhraseCacheCommand { get; set; }
+ public ReactiveCommand PitEaseInOutCommand { get; set; }
+ public ReactiveCommand PitLinearCommand { get; set; }
+ public ReactiveCommand PitEaseInCommand { get; set; }
+ public ReactiveCommand PitEaseOutCommand { get; set; }
+ public ReactiveCommand PitSplineCommand { get; set; }
+ public ReactiveCommand PitSnapCommand { get; set; }
+ public ReactiveCommand PitDelCommand { get; set; }
+ public ReactiveCommand PitAddCommand { get; set; }
+
+ private ReactiveCommand legacyPluginCommand;
+
+ public PianoRollViewModel() {
+ NotesViewModel = new NotesViewModel();
+ CurveViewModel = new CurveViewModel();
+
+ this.WhenAnyValue(vm => vm.ToolIndex)
+ .Subscribe(index => EditTool.BaseTool = index);
+ this.WhenAnyValue(vm => vm.PenToolIndex)
+ .Subscribe(index => EditTool.PenToolVariation = index);
+ this.WhenAnyValue(vm => vm.PitchOverwrite)
+ .Subscribe(val => { EditTool.OverwritePitch = val; Preferences.Default.EditTool.OverwritePitch = val; Preferences.Save(); });
+
+ NoteDeleteCommand = ReactiveCommand.Create(info => {
+ NotesViewModel.DeleteSelectedNotes();
+ });
+ NoteCopyCommand = ReactiveCommand.Create(info => {
+ NotesViewModel.CopyNotes();
+ });
+ ClearPhraseCacheCommand = ReactiveCommand.Create(info => {
+ NotesViewModel.ClearPhraseCache();
+ });
+ PitEaseInOutCommand = ReactiveCommand.Create(info => {
+ if (NotesViewModel.Part == null) { return; }
+ DocManager.Inst.StartUndoGroup("command.pitch.editpoint");
+ DocManager.Inst.ExecuteCmd(new ChangePitchPointShapeCommand(NotesViewModel.Part, info.Note.pitch.data[info.Index], PitchPointShape.io));
+ DocManager.Inst.EndUndoGroup();
+ });
+ PitLinearCommand = ReactiveCommand.Create(info => {
+ if (NotesViewModel.Part == null) { return; }
+ DocManager.Inst.StartUndoGroup("command.pitch.editpoint");
+ DocManager.Inst.ExecuteCmd(new ChangePitchPointShapeCommand(NotesViewModel.Part, info.Note.pitch.data[info.Index], PitchPointShape.l));
+ DocManager.Inst.EndUndoGroup();
+ });
+ PitEaseInCommand = ReactiveCommand.Create(info => {
+ if (NotesViewModel.Part == null) { return; }
+ DocManager.Inst.StartUndoGroup("command.pitch.editpoint");
+ DocManager.Inst.ExecuteCmd(new ChangePitchPointShapeCommand(NotesViewModel.Part, info.Note.pitch.data[info.Index], PitchPointShape.i));
+ DocManager.Inst.EndUndoGroup();
+ });
+ PitEaseOutCommand = ReactiveCommand.Create(info => {
+ if (NotesViewModel.Part == null) { return; }
+ DocManager.Inst.StartUndoGroup("command.pitch.editpoint");
+ DocManager.Inst.ExecuteCmd(new ChangePitchPointShapeCommand(NotesViewModel.Part, info.Note.pitch.data[info.Index], PitchPointShape.o));
+ DocManager.Inst.EndUndoGroup();
+ });
+ PitSplineCommand = ReactiveCommand.Create(info => {
+ if (NotesViewModel.Part == null) { return; }
+ DocManager.Inst.StartUndoGroup("command.pitch.editpoint");
+ DocManager.Inst.ExecuteCmd(new ChangePitchPointShapeCommand(NotesViewModel.Part, info.Note.pitch.data[info.Index], PitchPointShape.sp));
+ DocManager.Inst.EndUndoGroup();
+ });
+ PitSnapCommand = ReactiveCommand.Create(info => {
+ if (NotesViewModel.Part == null) { return; }
+ DocManager.Inst.StartUndoGroup("command.pitch.editpoint");
+ DocManager.Inst.ExecuteCmd(new SnapPitchPointCommand(NotesViewModel.Part, info.Note));
+ DocManager.Inst.EndUndoGroup();
+ });
+ PitDelCommand = ReactiveCommand.Create(info => {
+ if (NotesViewModel.Part == null) { return; }
+ DocManager.Inst.StartUndoGroup("command.pitch.delete");
+ DocManager.Inst.ExecuteCmd(new DeletePitchPointCommand(NotesViewModel.Part, info.Note, info.Index));
+ DocManager.Inst.EndUndoGroup();
+ });
+ PitAddCommand = ReactiveCommand.Create(info => {
+ if (NotesViewModel.Part == null) { return; }
+ DocManager.Inst.StartUndoGroup("command.pitch.add");
+ DocManager.Inst.ExecuteCmd(new AddPitchPointCommand(NotesViewModel.Part, info.Note, new PitchPoint(info.X, info.Y, NotePresets.Default.DefaultPitchShape), info.Index + 1));
+ DocManager.Inst.EndUndoGroup();
+ });
+
+ legacyPluginCommand = ReactiveCommand.Create(async plugin => {
+ if (NotesViewModel.Part == null || NotesViewModel.Part.notes.Count == 0) {
+ return;
+ }
+ DocManager.Inst.ExecuteCmd(new LoadingNotification(typeof(PianoRoll), true, "legacy plugin"));
+
+ try {
+ var part = NotesViewModel.Part;
+ UNote? first;
+ UNote? last;
+ if (NotesViewModel.Selection.IsEmpty) {
+ first = part.notes.First();
+ last = part.notes.Last();
+ } else {
+ first = NotesViewModel.Selection.FirstOrDefault();
+ last = NotesViewModel.Selection.LastOrDefault();
+ }
+ var runner = PluginRunner.from(PathManager.Inst, DocManager.Inst);
+ await runner.Execute(NotesViewModel.Project, part, first, last, plugin);
+
+ } catch (Exception e) {
+ DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e));
+ } finally {
+ DocManager.Inst.ExecuteCmd(new LoadingNotification(typeof(PianoRoll), false, "legacy plugin"));
+ }
+ });
+ LoadLegacyPlugins();
+ DocManager.Inst.AddSubscriber(this);
+ }
+
+ private void SetUndoState() {
+ CanUndo = DocManager.Inst.GetUndoState(out string? undoNameKey);
+ if (!string.IsNullOrWhiteSpace(undoNameKey)) {
+ UndoText = $"{ThemeManager.GetString("menu.edit.undo")}: {ThemeManager.GetString(undoNameKey)}";
+ } else {
+ UndoText = ThemeManager.GetString("menu.edit.undo");
+ }
+ CanRedo = DocManager.Inst.GetRedoState(out string? redoNameKey);
+ if (!string.IsNullOrWhiteSpace(redoNameKey)) {
+ RedoText = $"{ThemeManager.GetString("menu.edit.redo")}: {ThemeManager.GetString(redoNameKey)}";
+ } else {
+ RedoText = ThemeManager.GetString("menu.edit.redo");
+ }
+ }
+
+ private void LoadLegacyPlugins() {
+ LegacyPlugins.Clear();
+ LegacyPlugins.AddRange(DocManager.Inst.Plugins.Select(plugin => new MenuItemViewModel() {
+ Header = plugin.Name,
+ Command = legacyPluginCommand,
+ CommandParameter = plugin,
+ }));
+
+ LegacyPluginShortcuts.Clear();
+ foreach (MenuItemViewModel menu in LegacyPlugins) {
+ if (menu.CommandParameter is Classic.Plugin plugin) {
+ if (Enum.TryParse(plugin.Shortcut, out Key key) && !LegacyPluginShortcuts.ContainsKey(key)) {
+ LegacyPluginShortcuts.Add(key, menu);
+ }
+ }
+ }
+ LegacyPlugins.Add(new MenuItemViewModel() { // Separator
+ Header = "-",
+ Height = 1
+ });
+ LegacyPlugins.Add(new MenuItemViewModel() {
+ Header = ThemeManager.GetString("pianoroll.menu.plugin.openfolder"),
+ Command = ReactiveCommand.Create(() => {
+ try {
+ OS.OpenFolder(PathManager.Inst.PluginsPath);
+ } catch (Exception e) {
+ DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e));
+ }
+ })
+ });
+ LegacyPlugins.Add(new MenuItemViewModel() {
+ Header = ThemeManager.GetString("pianoroll.menu.plugin.reload"),
+ Command = ReactiveCommand.Create(() => {
+ DocManager.Inst.SearchAllLegacyPlugins();
+ LoadLegacyPlugins();
+ })
+ });
+ }
+
+ public void Undo() => DocManager.Inst.Undo();
+ public void Redo() => DocManager.Inst.Redo();
+ public void Cut() {
+ if (CurveViewModel.IsSelected(NotesViewModel.PrimaryKey)) {
+ CurveViewModel.Cut(NotesViewModel.Part!);
+ } else {
+ NotesViewModel.CutNotes();
+ }
+ }
+ public void Copy() {
+ if (CurveViewModel.IsSelected(NotesViewModel.PrimaryKey)) {
+ CurveViewModel.Copy(NotesViewModel.Part!);
+ } else {
+ NotesViewModel.CopyNotes();
+ }
+ }
+ public void Paste() {
+ if (DocManager.Inst.NotesClipboard != null && DocManager.Inst.NotesClipboard.Count > 0) {
+ NotesViewModel.PasteNotes();
+ } else if (DocManager.Inst.CurvesClipboard != null && NotesViewModel.Part != null) {
+ var track = NotesViewModel.Project.tracks[NotesViewModel.Part.trackNo];
+ if (track.TryGetExpDescriptor(NotesViewModel.Project, NotesViewModel.PrimaryKey, out var descriptor)) {
+ CurveViewModel.Paste(NotesViewModel.Part, descriptor);
+ }
+ }
+ }
+ public void PastePlain() => NotesViewModel.PastePlainNotes();
+ public void Delete() => NotesViewModel.DeleteSelectedNotes();
+ public void SelectAll() => NotesViewModel.SelectAllNotes();
+
+ public void MouseoverPhoneme(UPhoneme? phoneme) {
+ MessageBus.Current.SendMessage(new PhonemeMouseoverEvent(phoneme));
+ }
+
+ #region ICmdSubscriber
+
+ public void OnNext(UCommand cmd, bool isUndo) {
+ if (cmd is ProgressBarNotification progressBarNotification) {
+ if (PianoRollDetached) {
+ Dispatcher.UIThread.InvokeAsync(() => {
+ Progress = progressBarNotification.Progress;
+ }, DispatcherPriority.Background);
+ }
+ }
+ SetUndoState();
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file