diff --git a/OpenUtau.Core/Util/Preferences.cs b/OpenUtau.Core/Util/Preferences.cs
index f14fe185d..03ab1e1cc 100644
--- a/OpenUtau.Core/Util/Preferences.cs
+++ b/OpenUtau.Core/Util/Preferences.cs
@@ -16,7 +16,15 @@ public static class Preferences {
static Preferences() {
Load();
}
+ public class ShortcutBinding {
+ public string ActionId { get; set; }
+ public string[] Shortcuts { get; set; }
+ public ShortcutBinding(string actionId, string[] shortcuts) {
+ ActionId = actionId;
+ Shortcuts = shortcuts;
+ }
+ }
public static void Save() {
try {
File.WriteAllText(PathManager.Inst.PrefsFilePath,
@@ -216,6 +224,8 @@ public class SerializablePreferences {
public bool LockUnselectedNotesPitch = true;
public bool LockUnselectedNotesVibrato = true;
public bool LockUnselectedNotesExpressions = true;
+ public ShortcutBinding[] Shortcuts = [];
+ public ShortcutBinding[] PluginShortcuts = [];
public bool LyricLivePreview = true;
public bool LyricApplySelectionOnly = true;
public bool VoicebankPublishUseIgnore = true;
diff --git a/OpenUtau/Controls/PianoRoll.axaml b/OpenUtau/Controls/PianoRoll.axaml
index 647d22145..757f7f792 100644
--- a/OpenUtau/Controls/PianoRoll.axaml
+++ b/OpenUtau/Controls/PianoRoll.axaml
@@ -117,7 +117,6 @@
Fill="{DynamicResource NeutralAccentBrushSemi}"
Data="M -6.5 0 L 6.5 0 L 6.5 3 L 0 9 L -6.5 3 Z"/>
-
-
-
-
@@ -248,17 +244,17 @@
@@ -368,17 +364,22 @@
-
+
-
+
-
+
+
+
+
+
+
+
@@ -392,31 +393,121 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
@@ -430,8 +521,13 @@
-
+
+
+
+
+
+
+
@@ -440,22 +536,37 @@
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
@@ -467,14 +578,24 @@
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
@@ -497,8 +618,13 @@
-
+
+
+
+
+
+
+
@@ -510,14 +636,24 @@
-
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
@@ -611,7 +756,6 @@
-
@@ -678,4 +822,4 @@
-
+
\ No newline at end of file
diff --git a/OpenUtau/Controls/PianoRoll.axaml.cs b/OpenUtau/Controls/PianoRoll.axaml.cs
index 2fd05db2d..04ef03c37 100644
--- a/OpenUtau/Controls/PianoRoll.axaml.cs
+++ b/OpenUtau/Controls/PianoRoll.axaml.cs
@@ -1492,508 +1492,191 @@ bool OnKeyExtendedHandler(KeyEventArgs args) {
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;
+ string? action = KeyTranslator.GetActionIdFromKey(args.Key, args.KeyModifiers);
+ if (action == null) return false;
+
+ switch (action) {
+ // Playback & Selection
+ case "PlayOrPause": playVm.PlayOrPause(); return true;
+ case "PlaySelection":
+ if (!notesVm.Selection.IsEmpty) {
+ playVm.PlayOrPause(
+ tick: notesVm.Part.position + notesVm.Selection.FirstOrDefault()!.position,
+ endTick: notesVm.Part.position + notesVm.Selection.LastOrDefault()!.RightBound
+ );
}
- 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;
- }
+ case "ClearSelection":
+ var numSelected = notesVm.Selection.Count;
+ if (numSelected == 1 || numSelected == notesVm.Part.notes.Count) notesVm.DeselectNotes();
+ else if (numSelected > 1) notesVm.SelectNote(notesVm.Selection.Head!);
+ return true;
+ case "SelectAll": notesVm.SelectAllNotes(); return true;
+ case "DeselectAll": notesVm.DeselectNotes(); return true;
+
+ // UI & Windows
+ case "CloseWindow": if (RootWindow is PianoRollDetachedWindow) RootWindow.Hide(); return true;
+ case "menu.tools.fullscreen": OnMenuFullScreen(this, new RoutedEventArgs()); return true;
+ case "OpenPluginMenu": if (PluginMenu.Parent is MenuItem batch) { batch.Open(); PluginMenu.Open(); } return true;
+
+ // Lyrics
+ case "EditLyrics":
+ if (LyricBox != null && LyricBox.IsVisible) {
+ return false;
}
- break;
- case Key.PageDown: {
- if (isNone) {
- MoveToNextPart(true);
- return true;
- }
+
+ 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();
}
- break;
- #endregion
+ return true;
+
+ // Tools
+ case "ToolSelect1": ViewModel.ToolIndex = 0; return true;
+ case "ToolSelect2Main": ViewModel.ToolIndex = 1; ViewModel.PenToolIndex = 0; return true;
+ case "ToolSelect2Alt": ViewModel.ToolIndex = 1; ViewModel.PenToolIndex = 1; return true;
+ case "ToolSelect3": ViewModel.ToolIndex = 2; return true;
+ case "ToolSelect4Main": ViewModel.ToolIndex = 3; ViewModel.PitchOverwrite = false; return true;
+ case "ToolSelect4Overwrite": ViewModel.ToolIndex = 3; ViewModel.PitchOverwrite = true; return true;
+ case "ToolSelect4Line": ViewModel.ToolIndex = 4; ViewModel.PitchOverwrite = false; return true;
+ case "ToolSelect4LineOverwrite": ViewModel.ToolIndex = 4; ViewModel.PitchOverwrite = true; return true;
+ case "ToolSelectSCurve": ViewModel.ToolIndex = 5; ViewModel.PitchOverwrite = false; return true;
+ case "ToolSelectSine": ViewModel.ToolIndex = 6; ViewModel.PitchOverwrite = false; return true;
+ case "ToolSelectSmoothen": ViewModel.ToolIndex = 7; ViewModel.PitchOverwrite = false; return true;
+ case "ToolSelect5": ViewModel.ToolIndex = 8; return true;
+
+ // Expressions
+ case "ExpSelect1": expSelector1?.SelectExp(); return true;
+ case "ExpSelect2": expSelector2?.SelectExp(); return true;
+ case "ExpSelect3": expSelector3?.SelectExp(); return true;
+ case "ExpSelect4": expSelector4?.SelectExp(); return true;
+ case "ExpSelect5": expSelector5?.SelectExp(); return true;
+ case "ExpSelect6": expSelector6?.SelectExp(); return true;
+ case "ExpSelect7": expSelector7?.SelectExp(); return true;
+ case "ExpSelect8": expSelector8?.SelectExp(); return true;
+ case "ExpSelect9": expSelector9?.SelectExp(); return true;
+ case "ExpSelect10": expSelector10?.SelectExp(); return true;
+
+ // Toggles
+ case "ToggleFinalPitch": notesVm.ShowFinalPitch = !notesVm.ShowFinalPitch; return true;
+ case "ToggleTips": notesVm.ShowTips = !notesVm.ShowTips; return true;
+ case "ToggleVibrato": notesVm.ShowVibrato = !notesVm.ShowVibrato; return true;
+ case "TogglePitch": notesVm.ShowPitch = !notesVm.ShowPitch; return true;
+ case "TogglePhoneme": notesVm.ShowPhoneme = !notesVm.ShowPhoneme; return true;
+ case "ToggleExpressions": notesVm.ShowExpressions = !notesVm.ShowExpressions; return true;
+ case "ToggleSnap": notesVm.IsSnapOn = !notesVm.IsSnapOn; return true;
+ case "OpenSnapMenu": SnapDivMenu.Open(); return true;
+ case "ToggleNoteParams": notesVm.ShowNoteParams = !notesVm.ShowNoteParams; return true;
+ case "TogglePlayTone": notesVm.PlayTone = !notesVm.PlayTone; return true;
+ case "ToggleWaveform": notesVm.ShowWaveform = !notesVm.ShowWaveform; return true;
+
+ // Transposition
+ case "TransposeUp": notesVm.TransposeSelection(1); return true;
+ case "pianoroll.menu.notes.octaveup": notesVm.TransposeSelection(12); return true;
+ case "TransposeDown": notesVm.TransposeSelection(-1); return true;
+ case "pianoroll.menu.notes.octavedown": notesVm.TransposeSelection(-12); return true;
+
+ // Note Movement & Sizing
+ case "MoveCursorLeft": notesVm.MoveCursor(-1); return true;
+ case "ResizeNotesLeft": notesVm.ResizeSelectedNotes(-1 * deltaTicks); return true;
+ case "MoveNotesLeft": notesVm.MoveSelectedNotes(-1 * deltaTicks); return true;
+ case "ExtendSelectionLeft": notesVm.ExtendSelection(-1); return true;
+ case "MoveCursorRight": notesVm.MoveCursor(1); return true;
+ case "ResizeNotesRight": notesVm.ResizeSelectedNotes(deltaTicks); return true;
+ case "MoveNotesRight": notesVm.MoveSelectedNotes(deltaTicks); return true;
+ case "ExtendSelectionRight": notesVm.ExtendSelection(1); return true;
+
+ // Edit Operations
+ case "menu.edit.undo": ViewModel.Undo(); return true;
+ case "menu.edit.redo": ViewModel.Redo(); return true;
+ case "menu.edit.copy": ViewModel.Copy(); return true;
+ case "menu.edit.cut": ViewModel.Cut(); return true;
+ case "menu.edit.paste": ViewModel.Paste(); return true;
+ case "PastePlain": notesVm.PastePlainNotes(); return true;
+ case "PasteParameters": notesVm.PasteSelectedParams(RootWindow); return true;
+ case "InsertNote": notesVm.InsertNote(); return true;
+ case "menu.edit.delete": notesVm.DeleteSelectedNotes(); return true;
+ case "MergeNotes": notesVm.MergeSelectedNotes(); return true;
+
+ // Playhead & Timeline Navigation
+ case "PlayheadHome": playVm.MovePlayPos(notesVm.Part.position); return true;
+ case "SelectToStart": if (notesVm.Part.notes.FirstOrDefault() is UNote first) notesVm.ExtendSelection(first); return true;
+ case "PlayheadEnd": playVm.MovePlayPos(notesVm.Part.End); HScrollBar.Value = HScrollBar.Maximum; return true;
+ case "SelectToEnd": if (notesVm.Part.notes.LastOrDefault() is UNote last) notesVm.ExtendSelection(last); return true;
+
+ case "PlayheadLeft": playVm.MovePlayPos(playVm.PlayPosTick - snapUnit); return true;
+ case "PlayheadToSelectionStart": if (!notesVm.Selection.IsEmpty) playVm.MovePlayPos(notesVm.Part.position + notesVm.Selection.FirstOrDefault()!.position); return true;
+ case "PlayheadToViewStart": playVm.MovePlayPos(notesVm.Part.position + (int)notesVm.TickOffset); return true;
+
+ case "PlayheadRight": playVm.MovePlayPos(playVm.PlayPosTick + snapUnit); return true;
+ case "PlayheadToSelectionEnd": if (!notesVm.Selection.IsEmpty) playVm.MovePlayPos(notesVm.Part.position + notesVm.Selection.LastOrDefault()!.RightBound); return true;
+ case "PlayheadToViewEnd": playVm.MovePlayPos(notesVm.Part.position + (int)(notesVm.TickOffset + notesVm.Bounds.Width / notesVm.TickWidth)); return true;
+
+ // Scrolling & Zooming
+ case "ScrollLeft": notesVm.TickOffset = Math.Max(0, notesVm.TickOffset - snapUnit); return true;
+ case "ScrollRight": notesVm.TickOffset = Math.Min(notesVm.TickOffset + snapUnit, notesVm.HScrollBarMax); return true;
+ case "ScrollUp": notesVm.TrackOffset = Math.Max(notesVm.TrackOffset - 2, 0); return true;
+ case "ScrollDown": notesVm.TrackOffset = Math.Min(notesVm.TrackOffset + 2, notesVm.VScrollBarMax); return true;
+ case "ZoomIn":
+ case "ZoomOut":
+ double x = 0, 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), action == "ZoomIn" ? 0.1 : -0.1);
+ return true;
+
+ // Track & Project Operations
+ case "SaveProject": _ = MainWindow?.Save(); return true;
+ case "SoloTrack": MessageBus.Current.SendMessage(new TracksSoloEvent(notesVm.Part.trackNo, !project.tracks[notesVm.Part.trackNo].Solo, false)); return true;
+ case "MuteTrack": MessageBus.Current.SendMessage(new TracksMuteEvent(notesVm.Part.trackNo, false)); return true;
+ case "FocusSelection":
+ if (notesVm.Selection.FirstOrDefault() is UNote focusNote) DocManager.Inst.ExecuteCmd(new FocusNoteNotification(notesVm.Part, focusNote));
+ return true;
+ case "SearchNote": SearchNote(); return true;
+ case "MoveToNextPart": MoveToNextPart(true); return true;
+ case "MoveToPrevPart": MoveToNextPart(false); return true;
+
+ // others
+ case "Quantize Notes": QuantizeNotes(); return true;
+ case "lyricsreplace.replace": ReplaceLyrics(); return true;
+ case "Randomize Tuning": RandomizeTuning(); return true;
+ case "Lengthen Crossfade": LengthenCrossfade(); return true;
+ case "Add Breath": AddBreathNote(); return true;
+ case "Edit Note Defaults": EditNoteDefaults(); return true;
+ case "Open Singers Window": OnMenuSingers(this, new RoutedEventArgs()); return true;
+ case "Open Expressions": OnExpButtonClick(this, new RoutedEventArgs()); return true;
+ case "Lock Pitch Points": OnMenuLockPitchPoints(this, new RoutedEventArgs()); return true;
+ case "Lock Vibrato": OnMenuLockVibrato(this, new RoutedEventArgs()); return true;
+ case "Lock Expressions": OnMenuLockExpressions(this, new RoutedEventArgs()); return true;
+ case "Show Portrait": OnMenuShowPortrait(this, new RoutedEventArgs()); return true;
+ case "Show Icon": OnMenuShowIcon(this, new RoutedEventArgs()); return true;
+ case "Show Ghost Notes": OnMenuShowGhostNotes(this, new RoutedEventArgs()); return true;
+ case "Use Track Color": OnMenuUseTrackColor(this, new RoutedEventArgs()); return true;
+ case "Detach Piano Roll": OnMenuDetachPianoRoll(this, new RoutedEventArgs()); return true;
+ case "Hide Piano Roll": OnMenuHidePianoRoll(this, new RoutedEventArgs()); return true;
+ }
+
+ // External and batch note edits
+ var allDynamicMenus = ViewModel.NoteBatchEdits
+ .Concat(ViewModel.LyricBatchEdits)
+ .Concat(ViewModel.ResetBatchEdits)
+ .Concat(ViewModel.ExternalBatchEdits);
+ foreach (var menu in allDynamicMenus) {
+ if (menu.CommandParameter is BatchEdit edit && edit.Name == action) {
+ menu.Command?.Execute(edit);
+ return true;
+ }
+ }
+ // Legacy plugins
+ foreach (var menu in ViewModel.LegacyPlugins) {
+ if (menu.Header?.ToString() == action) {
+ menu.Command?.Execute(menu.CommandParameter);
+ return true;
+ }
}
return false;
}
@@ -2017,4 +1700,4 @@ public void OnNext(UCommand cmd, bool isUndo) {
}
}
}
-}
+}
\ No newline at end of file
diff --git a/OpenUtau/Strings/Strings.axaml b/OpenUtau/Strings/Strings.axaml
index bfc779b45..2bce0ff0e 100644
--- a/OpenUtau/Strings/Strings.axaml
+++ b/OpenUtau/Strings/Strings.axaml
@@ -115,8 +115,7 @@ OpenUtau aims to be an open source editing environment for UTAU community, with
Merging Parts
Parts on different tracks cannot be merged.
Splitting Part
- There are one or more note(s) overlapping with the playhead.
-Do you want to continue by splitting at the nearest position after current playhead with no notes in the way?
+ There are one or more note(s) overlapping with the playhead. Do you want to continue by splitting at the nearest position after current playhead with no notes in the way?
Export DiffSinger Scripts
Curves
Pitch (f0)
@@ -128,8 +127,7 @@ Do you want to continue by splitting at the nearest position after current playh
No resampler! Put your favourite resampler exe or dll in the Resamplers folder and choose it in Preferences!
Previously, OpenUtau terminated abnormally. Click here to recover.
Recovery project
- This tool will change the tempo of the project without changing the actual positions and durations (in seconds) of notes.
- New BPM:
+ This tool will change the tempo of the project without changing the actual positions and durations (in seconds) of notes. New BPM:
Time Signature
Track Settings
Location
@@ -344,8 +342,7 @@ Do you want to continue by splitting at the nearest position after current playh
This tool will merge another voicebank into current voicebank as new voice colors.
Choose the voicebank that you want to merge into current voicebank
Set new names for copied folders.
- Set new prefix and suffix for subbanks.
-Syntax: prefix,suffix
+ Set new prefix and suffix for subbanks. Syntax: prefix,suffix
Set new names for voice colors (shown in CLR expression).
Merge voicebanks
@@ -360,8 +357,7 @@ Syntax: prefix,suffix
Removes last applied preset.
Save current settings to a new preset.
Reset All Settings
- Reset every setting to default values.
-Warning: this option removes custom presets.
+ Reset every setting to default values. Warning: this option removes custom presets.
Vibrato
Minimum Length
Automatic Vibrato by Length
@@ -487,7 +483,7 @@ Warning: this option removes custom presets.
Reset pitch bends
Reset vibratos
Part
- Legacy Plugin (Experimental)
+ Legacy Plugin
Singer and Oto settings
Open folder
Reload
@@ -502,52 +498,70 @@ Warning: this option removes custom presets.
Prev
Search Alias
Snap Division
- View Final Pitch to Render (R)
- View Note Parameters (\)
- View Phonemes (O)
- View Pitch Bend (I)
- Toggle Snap (P)
+ View Final Pitch to Render
+ View Note Parameters
+ View Phonemes
+ View Pitch Bend
+ Toggle Snap
Auto
Auto (triplet)
View Phonemizer Tags
- View Tips (T)
- Toggle Note Tone (Y)
- Toggle Overwrite Pitch Mode
- View Vibrato (U)
- View Waveform (W)
- View Expressions (L)
- Draw Pitch Tool (Shift + 1)
+ View Tips
+ Toggle Note Tone
+ View Vibrato
+ View Waveform
+ View Expressions
+
+ Line Pitch Tool
Left click to draw
Right click to reset
Hold Ctrl to select
- Eraser Tool (3)
- Knife Tool (4)
- Pitch Line Tool (Shift + 2)
- Left click to draw straight line
+ S-Curve Pitch Tool
+ Left click to draw
Right click to reset
Hold Ctrl to select
- Pitch S-Curve Tool (Shift + 3)
- Drag to draw S-curve, release to enter adjusting mode
- Move mouse to tune S-curve strength, click to apply
+ Sine Wave Pitch Tool
+ Left click to draw
Right click to reset
Hold Ctrl to select
- Pitch Sine Wave Tool (Shift + 4)
- Drag to draw sine wave, release to enter adjusting mode
- Move mouse to adjust wavelength and amplitude, click to apply
+ Smoothen Pitch Tool
+ Left click to smoothen
Right click to reset
Hold Ctrl to select
- Pitch Smoothen Tool (Shift + 5)
- Left click to smoothen pitch
+
+ Line Draw Pitch Tool
+ Left click to draw (draw straight line)
Right click to reset
- Hold Ctrl to select
- Pen Plus Tool (Ctrl + 2)
+ Hold Ctrl to select
+ Hold Alt to smoothen
+ Draw Pitch Tool
+ Left click to draw
+ Right click to reset
+ Hold Ctrl to select
+ Hold Alt to smoothen
+ Eraser Tool
+ Knife Tool
+ Overwrite Pitch Tool
+ Left click to draw (overwrites vibrato or mod+)
+ Right click to reset
+ Hold Ctrl to select
+ Hold Alt to smoothen
+ Overwrite Line Pitch Tool
+ Left click to draw (draw straight line overwrites the vibrato or mod+)
+ Hold Ctrl to draw sine curve
+ Hold Alt + Shift to tune S-curve
+ Hold Alt to draw S-curve
+ Right click to reset
+ Hold Ctrl to select
+ Hold Alt to smoothen
+ Pen Plus Tool
Left click to draw
Right click to delete
Hold Ctrl to select
- Pen Tool (2)
+ Pen Tool
Left click to draw
Hold Ctrl to select
- Selection Tool (1)
+ Selection Tool
Attack time delta
Overlap
Preutter
@@ -563,6 +577,96 @@ Warning: this option removes custom presets.
Stable
vLabeler Path
Wine Path (set to enable wine for compatibility)
+
+ Keyboard Shortcuts
+ Press keys...
+ Play / Pause
+ Play Selection
+ Clear Selection
+ Select All
+ Deselect All
+ Close Window
+ Open Plugin Menu
+ Edit Lyrics
+ Selection Tool
+ Pen Tool
+ Pen Tool (Alt)
+ Eraser Tool
+ Draw Pitch Tool
+ Overwrite Pitch Tool
+ Draw Line Pitch Tool
+ Overwrite Line Pitch Tool
+ S-Curve Pitch Tool
+ Sine Wave Pitch Tool
+ Smoothen Pitch Tool
+ Knife Tool
+ Select Expression 1
+ Select Expression 2
+ Select Expression 3
+ Select Expression 4
+ Select Expression 5
+ Select Expression 6
+ Select Expression 7
+ Select Expression 8
+ Select Expression 9
+ Select Expression 10
+ Toggle Final Pitch View
+ Toggle Tips View
+ Toggle Vibrato View
+ Toggle Pitch View
+ Toggle Phoneme View
+ Toggle Expressions View
+ Toggle Snap
+ Open Snap Menu
+ Toggle Note Parameters View
+ Toggle Play Tone
+ Toggle Waveform View
+ Transpose Up
+ Transpose Octave Up
+ Transpose Down
+ Transpose Octave Down
+ Move Cursor Left
+ Resize Notes Left
+ Move Notes Left
+ Extend Selection Left
+ Move Cursor Right
+ Resize Notes Right
+ Move Notes Right
+ Extend Selection Right
+ Paste as Plain Notes
+ Paste Parameters
+ Insert Note
+ Merge Selected Notes
+ Move Playhead to Start
+ Extend Selection to Start
+ Move Playhead to End
+ Extend Selection to End
+ Move Playhead Left
+ Move Playhead to Selection Start
+ Move Playhead to View Start
+ Move Playhead Right
+ Move Playhead to Selection End
+ Move Playhead to View End
+ Scroll Left
+ Scroll Right
+ Scroll Up
+ Scroll Down
+ Zoom In
+ Zoom Out
+ Save Project
+ Save Project As
+ New Project
+ Open Project
+ Solo Track
+ Mute Track
+ Focus Selection
+ Search Note
+ Move to Next Track's Part
+ Move to Previous Track's Part
+ Left click to change. Right click to reset.
+ Search by name or key...
+ The shortcut '{0}' is already assigned to '{1}'.
+
Appearance
Scale degree display style
Numbered (1 2 3 4 5 6 7)
@@ -658,8 +762,7 @@ Warning: this option removes custom presets.
Cutoff must be to the right of overlap.
Cutoff must be to the right of preutter.
There are duplicate aliases.{0}
- Duplicated file names found when ignoreing case in oto set "{0}": {1}.
-The voicebank may not work on another OS with case-sensitivity.
+ Duplicated file names found when ignoreing case in oto set "{0}": {1}. The voicebank may not work on another OS with case-sensitivity.
Failed to load voicebank.
Invalid duration {0}.
Invalid oto format.
@@ -826,7 +929,6 @@ General
Start
Template
-
Track Polish
Enable on this track
Preset
@@ -839,4 +941,4 @@ General
Delete
Default
Track Polish...
-
+
\ No newline at end of file
diff --git a/OpenUtau/ViewModels/KeyTranslator.cs b/OpenUtau/ViewModels/KeyTranslator.cs
new file mode 100644
index 000000000..162bc59c5
--- /dev/null
+++ b/OpenUtau/ViewModels/KeyTranslator.cs
@@ -0,0 +1,432 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Avalonia.Input;
+using OpenUtau.Core;
+using OpenUtau.Core.Editing;
+using OpenUtau.Core.Util;
+
+namespace OpenUtau.App.ViewModels {
+ public class ShortcutKey {
+ public string ActionId { get; set; }
+ public KeyGesture Gesture { get; set; }
+
+ public ShortcutKey(string actionId, string shortcut) {
+ ActionId = actionId;
+ Gesture = KeyTranslator.StringToGesture(shortcut);
+ }
+
+ public override string ToString() => $"{ActionId}: {KeyTranslator.GetFriendlyName(Gesture.Key, Gesture.KeyModifiers)}";
+ }
+
+ public static class KeyTranslator {
+ public static readonly bool IsMac = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
+ public static List Shortcuts { get; set; } = new List();
+ public static Preferences.ShortcutBinding[] DefShortcuts { get; }
+
+ static KeyTranslator() {
+ DefShortcuts = [
+ // Playback & Selection
+ new Preferences.ShortcutBinding("PlayOrPause", ["Space"]),
+ new Preferences.ShortcutBinding("PlaySelection", ["Alt+Space"]),
+ new Preferences.ShortcutBinding("ClearSelection", ["Escape"]),
+ new Preferences.ShortcutBinding("SelectAll", ["Ctrl+A"]),
+ new Preferences.ShortcutBinding("DeselectAll", ["Ctrl+D"]),
+
+ // UI & Windows
+ new Preferences.ShortcutBinding("CloseWindow", ["Alt+F4"]),
+ new Preferences.ShortcutBinding("menu.tools.fullscreen", ["F11"]),
+ new Preferences.ShortcutBinding("OpenPluginMenu", ["N"]),
+
+ // Lyrics
+ new Preferences.ShortcutBinding("EditLyrics", ["Return"]),
+
+ // Tools
+ new Preferences.ShortcutBinding("ToolSelect1", ["D1"]),
+ new Preferences.ShortcutBinding("ToolSelect2Main", ["D2"]),
+ new Preferences.ShortcutBinding("ToolSelect2Alt", ["Ctrl+D2"]),
+ new Preferences.ShortcutBinding("ToolSelect3", ["D3"]),
+ new Preferences.ShortcutBinding("ToolSelect4Main", ["D4"]),
+ new Preferences.ShortcutBinding("ToolSelect4Overwrite", ["Ctrl+D4"]),
+ new Preferences.ShortcutBinding("ToolSelect4Line", ["Shift+D4"]),
+ new Preferences.ShortcutBinding("ToolSelect4LineOverwrite", ["Ctrl+Shift+D4"]),
+
+ // New Pitch Curve Tools
+ new Preferences.ShortcutBinding("ToolSelectSCurve", ["D5"]),
+ new Preferences.ShortcutBinding("ToolSelectSine", ["Shift+D5"]),
+ new Preferences.ShortcutBinding("ToolSelectSmoothen", ["D6"]),
+
+ // Knife Tool (Bumped to D7)
+ new Preferences.ShortcutBinding("ToolSelect5", ["D7"]),
+
+ // Expressions
+ new Preferences.ShortcutBinding("ExpSelect1", ["Alt+D1"]),
+ new Preferences.ShortcutBinding("ExpSelect2", ["Alt+D2"]),
+ new Preferences.ShortcutBinding("ExpSelect3", ["Alt+D3"]),
+ new Preferences.ShortcutBinding("ExpSelect4", ["Alt+D4"]),
+ new Preferences.ShortcutBinding("ExpSelect5", ["Alt+D5"]),
+ new Preferences.ShortcutBinding("ExpSelect6", ["Alt+D6"]),
+ new Preferences.ShortcutBinding("ExpSelect7", ["Alt+D7"]),
+ new Preferences.ShortcutBinding("ExpSelect8", ["Alt+D8"]),
+ new Preferences.ShortcutBinding("ExpSelect9", ["Alt+D9"]),
+ new Preferences.ShortcutBinding("ExpSelect10", ["Alt+D0"]),
+
+ // Toggles
+ new Preferences.ShortcutBinding("ToggleFinalPitch", ["R"]),
+ new Preferences.ShortcutBinding("ToggleTips", ["T"]),
+ new Preferences.ShortcutBinding("ToggleVibrato", ["U"]),
+ new Preferences.ShortcutBinding("TogglePitch", ["I"]),
+ new Preferences.ShortcutBinding("TogglePhoneme", ["O"]),
+ new Preferences.ShortcutBinding("ToggleExpressions", ["L"]),
+ new Preferences.ShortcutBinding("ToggleSnap", ["P"]),
+ new Preferences.ShortcutBinding("OpenSnapMenu", ["Alt+P"]),
+ new Preferences.ShortcutBinding("ToggleNoteParams", ["OemPipe"]),
+ new Preferences.ShortcutBinding("TogglePlayTone", ["Y"]),
+ new Preferences.ShortcutBinding("ToggleWaveform", ["W"]),
+
+ // Transposition
+ new Preferences.ShortcutBinding("TransposeUp", ["Up"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.octaveup", ["Ctrl+Up"]),
+ new Preferences.ShortcutBinding("TransposeDown", ["Down"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.octavedown", ["Ctrl+Down"]),
+
+ // Note Movement & Sizing
+ new Preferences.ShortcutBinding("MoveCursorLeft", ["Left"]),
+ new Preferences.ShortcutBinding("ResizeNotesLeft", ["Alt+Left"]),
+ new Preferences.ShortcutBinding("MoveNotesLeft", ["Ctrl+Left"]),
+ new Preferences.ShortcutBinding("ExtendSelectionLeft", ["Shift+Left"]),
+ new Preferences.ShortcutBinding("MoveCursorRight", ["Right"]),
+ new Preferences.ShortcutBinding("ResizeNotesRight", ["Alt+Right"]),
+ new Preferences.ShortcutBinding("MoveNotesRight", ["Ctrl+Right"]),
+ new Preferences.ShortcutBinding("ExtendSelectionRight", ["Shift+Right"]),
+
+ // Edit Operations
+ new Preferences.ShortcutBinding("menu.edit.undo", ["Ctrl+Z"]),
+ new Preferences.ShortcutBinding("menu.edit.redo", ["Ctrl+Y", "Ctrl+Shift+Z"]),
+ new Preferences.ShortcutBinding("menu.edit.copy", ["Ctrl+C"]),
+ new Preferences.ShortcutBinding("menu.edit.cut", ["Ctrl+X"]),
+ new Preferences.ShortcutBinding("menu.edit.paste", ["Ctrl+V"]),
+ new Preferences.ShortcutBinding("PastePlain", ["Ctrl+Shift+V"]),
+ new Preferences.ShortcutBinding("PasteParameters", ["Alt+V"]),
+ new Preferences.ShortcutBinding("InsertNote", ["Insert"]),
+ new Preferences.ShortcutBinding("menu.edit.delete", ["Delete"]),
+ new Preferences.ShortcutBinding("MergeNotes", ["Ctrl+U"]),
+
+ // Playhead & Timeline Navigation
+ new Preferences.ShortcutBinding("PlayheadHome", ["Home"]),
+ new Preferences.ShortcutBinding("SelectToStart", ["Shift+Home"]),
+ new Preferences.ShortcutBinding("PlayheadEnd", ["End"]),
+ new Preferences.ShortcutBinding("SelectToEnd", ["Shift+End"]),
+ new Preferences.ShortcutBinding("PlayheadLeft", ["Oem4"]),
+ new Preferences.ShortcutBinding("PlayheadToSelectionStart", ["Ctrl+Oem4"]),
+ new Preferences.ShortcutBinding("PlayheadToViewStart", ["Shift+Oem4"]),
+ new Preferences.ShortcutBinding("PlayheadRight", ["OemCloseBrackets"]),
+ new Preferences.ShortcutBinding("PlayheadToSelectionEnd", ["Ctrl+OemCloseBrackets"]),
+ new Preferences.ShortcutBinding("PlayheadToViewEnd", ["Shift+OemCloseBrackets"]),
+
+ // Scrolling & Zooming
+ new Preferences.ShortcutBinding("ScrollLeft", ["A"]),
+ new Preferences.ShortcutBinding("ScrollRight", ["D"]),
+ new Preferences.ShortcutBinding("ScrollUp", ["Alt+W"]),
+ new Preferences.ShortcutBinding("ScrollDown", ["Alt+S"]),
+ new Preferences.ShortcutBinding("ZoomIn", ["E"]),
+ new Preferences.ShortcutBinding("ZoomOut", ["Q"]),
+
+ // Track & Project Operations
+ new Preferences.ShortcutBinding("NewProject", ["Ctrl+N"]),
+ new Preferences.ShortcutBinding("OpenProject", ["Ctrl+O"]),
+ new Preferences.ShortcutBinding("SaveProject", ["Ctrl+S"]),
+ new Preferences.ShortcutBinding("SaveProjectAs", ["Ctrl+Shift+S"]),
+ new Preferences.ShortcutBinding("SoloTrack", ["Shift+S"]),
+ new Preferences.ShortcutBinding("MuteTrack", ["Shift+M"]),
+ new Preferences.ShortcutBinding("FocusSelection", ["F"]),
+ new Preferences.ShortcutBinding("SearchNote", ["Ctrl+F"]),
+
+ // Parts Navigation
+ new Preferences.ShortcutBinding("MoveToNextPart", ["PageDown"]),
+ new Preferences.ShortcutBinding("MoveToPrevPart", ["PageUp"]),
+
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.loadrenderedpitch", ["Ctrl+R"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.refreshrealcurves", ["Ctrl+Shift+R"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.bakepitch", ["Alt+K"]),
+
+ // Tails and Overlap
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.addtaildash", ["Alt+OemMinus"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.addtailrest", ["Shift+Alt+R"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.removetaildash", ["Ctrl+Alt+OemMinus"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.removetailrest", ["Ctrl+Alt+R"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.fixoverlap", ["Alt+F"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.autolegato", ["Alt+A"]),
+
+ // Common notes
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.commonnotecopy", ["Ctrl+Shift+C"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.commonnotepaste", ["Ctrl+Shift+P"]),
+
+ // Timings
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.randomizetiming", ["Alt+T"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.randomizeoffset", ["Ctrl+Alt+T"]),
+
+ // Lang
+ new Preferences.ShortcutBinding("pianoroll.menu.lyrics.romajitohiragana", ["Ctrl+Shift+J"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.lyrics.hiraganatoromaji", ["Ctrl+Alt+J"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.lyrics.javcvtocv", ["Ctrl+Shift+K"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.hanzitopinyin", ["Ctrl+Alt+H"]),
+
+ // Suffixes and Phonetic Hints
+ new Preferences.ShortcutBinding("pianoroll.menu.lyrics.removetonesuffix", ["Ctrl+Alt+S"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.lyrics.removelettersuffix", ["Ctrl+Shift+S"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.lyrics.movesuffixtovoicecolor", ["Ctrl+Alt+C"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.lyrics.removephonetichint", ["Ctrl+Alt+P"]),
+
+ // Dash and Slur
+ new Preferences.ShortcutBinding("pianoroll.menu.lyrics.dashtoplus", ["Alt+OemPlus"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.lyrics.dashtoplustilda", ["Ctrl+Alt+OemPlus"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.lyrics.insertslur", ["Alt+I"]),
+
+ // Reset
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.reset.all", ["Ctrl+Shift+Delete"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.reset.allparameters", ["Ctrl+Alt+I"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.reset.exps", ["Ctrl+Shift+E"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.clear.vibratos", ["Ctrl+Alt+V"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.reset.vibratos", ["Ctrl+Shift+U"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.reset.pitchbends", ["Ctrl+Alt+B"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.reset.phonemetimings", ["Ctrl+Shift+T"]),
+ new Preferences.ShortcutBinding("pianoroll.menu.notes.reset.aliases", ["Ctrl+Alt+A"]),
+
+ // other toggles
+ new Preferences.ShortcutBinding("Lock Pitch Points", ["Ctrl+Shift+L"]),
+ new Preferences.ShortcutBinding("Lock Vibrato", ["Ctrl+Alt+U"]),
+ new Preferences.ShortcutBinding("Lock Expressions", ["Ctrl+Alt+E"]),
+ new Preferences.ShortcutBinding("Show Portrait", ["Shift+Alt+P"]),
+ new Preferences.ShortcutBinding("Show Icon", ["Shift+Alt+I"]),
+ new Preferences.ShortcutBinding("Show Ghost Notes", ["Alt+G"]),
+ new Preferences.ShortcutBinding("Use Track Color", ["Alt+C"]),
+ new Preferences.ShortcutBinding("Detach Piano Roll", ["Shift+Alt+D"]),
+ new Preferences.ShortcutBinding("Hide Piano Roll", ["Shift+Alt+H"]),
+ new Preferences.ShortcutBinding("lyricsreplace.replace", ["Ctrl+H"]),
+ new Preferences.ShortcutBinding("Quantize Notes", ["Alt+Q"]),
+ new Preferences.ShortcutBinding("Randomize Tuning", ["Alt+R"]),
+ new Preferences.ShortcutBinding("Lengthen Crossfade", ["Alt+L"]),
+ new Preferences.ShortcutBinding("Add Breath", ["Alt+B"]),
+ new Preferences.ShortcutBinding("Edit Note Defaults", ["Alt+N"]),
+ new Preferences.ShortcutBinding("Open Singers Window", ["Alt+O"]),
+ new Preferences.ShortcutBinding("Open Expressions", ["Alt+E"])
+ ];
+ }
+
+ public static void LoadShortcuts() {
+ Shortcuts.Clear();
+ Shortcuts = GetMergedShortcuts().SelectMany(s => s.Shortcuts
+ .Select(key => new ShortcutKey(s.ActionId, key)))
+ .Where(key => key.Gesture.Key != Key.None)
+ .ToList();
+
+ // external batch edits
+ var edits = DocManager.Inst.ExternalBatchEditTypes
+ .Select(type => Activator.CreateInstance(type) as BatchEdit)
+ .OfType();
+ Shortcuts.AddRange(Preferences.Default.PluginShortcuts.SelectMany(s => s.Shortcuts
+ .Where(key => edits.Any(edit => edit.Name == s.ActionId))
+ .Select(key => new ShortcutKey(s.ActionId, key)))
+ .Where(key => key.Gesture.Key != Key.None));
+ // legacy plugins
+ Shortcuts.AddRange(Preferences.Default.PluginShortcuts.SelectMany(s => s.Shortcuts
+ .Where(key => DocManager.Inst.Plugins.Any(plugin => plugin.Name == s.ActionId))
+ .Select(key => new ShortcutKey(s.ActionId, key)))
+ .Where(key => key.Gesture.Key != Key.None));
+ }
+
+ public static List GetMergedShortcuts() {
+ var merged = new List();
+ foreach (var sc in DefShortcuts) {
+ var customized = Preferences.Default.Shortcuts.FirstOrDefault(pref => pref.ActionId == sc.ActionId);
+ if (customized != null) {
+ merged.Add(new Preferences.ShortcutBinding(sc.ActionId, customized.Shortcuts));
+ } else {
+ merged.Add(sc);
+ }
+ }
+ return merged;
+ }
+
+ public static void SaveShortcuts(IEnumerable items) {
+ var diff = new List();
+ var plugin = new List();
+ var edits = DocManager.Inst.ExternalBatchEditTypes
+ .Select(type => Activator.CreateInstance(type) as BatchEdit)
+ .OfType();
+
+ foreach (var item in items) {
+ var defKey = DefShortcuts.FirstOrDefault(s => s.ActionId == item.ActionId);
+ if (defKey != null) {
+ item.Gestures.RemoveAll(g => g.Key == Key.None);
+ var gestures = item.Gestures.Select(GestureToString).ToArray();
+ if (defKey.Shortcuts.Length != gestures.Length) {
+ diff.Add(new Preferences.ShortcutBinding(item.ActionId, gestures));
+ } else if (!defKey.Shortcuts.OrderBy(x => x).SequenceEqual(gestures.OrderBy(x => x))) {
+ diff.Add(new Preferences.ShortcutBinding(item.ActionId, gestures));
+ }
+ } else if (edits.Any(edit => edit.Name == item.ActionId) || DocManager.Inst.Plugins.Any(plugin => plugin.Name == item.ActionId)) {
+ item.Gestures.RemoveAll(g => g.Key == Key.None);
+ if (item.Gestures.Count == 0) continue;
+ var gestures = item.Gestures.Select(GestureToString).ToArray();
+ plugin.Add(new Preferences.ShortcutBinding(item.ActionId, gestures));
+ }
+ }
+ Preferences.Default.Shortcuts = diff.ToArray();
+ Preferences.Default.PluginShortcuts = plugin.ToArray();
+ Preferences.Save();
+ LoadShortcuts();
+ }
+
+ public static void ResetShortcuts() {
+ Preferences.Default.Shortcuts = [];
+ Preferences.Default.PluginShortcuts = [];
+ Preferences.Save();
+ LoadShortcuts();
+ }
+
+ public static KeyGesture StringToGesture(string shortcut) {
+ try {
+ var gesture = KeyGesture.Parse(shortcut);
+ return GestureConverter(gesture);
+ } catch {
+ return new KeyGesture(Key.None);
+ }
+ }
+ public static string GestureToString(KeyGesture gesture) {
+ return GestureConverter(gesture).ToString();
+ }
+ private static KeyGesture GestureConverter(KeyGesture gesture) {
+ if (IsMac) {
+ var m = gesture.KeyModifiers;
+ bool hasCtrl = m.HasFlag(KeyModifiers.Control);
+ bool hasMeta = m.HasFlag(KeyModifiers.Meta);
+ if (hasCtrl) {
+ m &= ~KeyModifiers.Control;
+ m |= KeyModifiers.Meta;
+ }
+ if (hasMeta) {
+ m &= ~KeyModifiers.Meta;
+ m |= KeyModifiers.Control;
+ }
+ return new KeyGesture(gesture.Key, m);
+ } else {
+ return gesture;
+ }
+ }
+
+ public static KeyGesture? GetGestureForMenu(string actionId) {
+ // Since only one shortcut can be displayed in the menu, if there are multiple shortcuts, it returns the first one
+ return Shortcuts.FirstOrDefault(s => s.ActionId == actionId)?.Gesture;
+ }
+
+ public static string? GetActionIdFromKey(Key pressedKey, KeyModifiers pressedMods) {
+ foreach (var sc in Shortcuts) {
+ if (IsKeyMatch(sc.Gesture.Key, pressedKey) && sc.Gesture.KeyModifiers == pressedMods) {
+ return sc.ActionId;
+ }
+ }
+ return null;
+ }
+
+ public static bool IsKeyMatch(Key savedKey, Key pressedKey) {
+ if (savedKey == pressedKey) return true;
+ return savedKey switch {
+ Key.OemPipe => pressedKey == Key.Oem5 || pressedKey == Key.OemBackslash,
+ Key.OemOpenBrackets => pressedKey == Key.Oem4,
+ Key.OemCloseBrackets => pressedKey == Key.Oem6,
+ Key.OemQuotes => pressedKey == Key.Oem7,
+ Key.OemSemicolon => pressedKey == Key.Oem1,
+ Key.OemTilde => pressedKey == Key.Oem3 || pressedKey == Key.Oem8,
+ Key.OemMinus => pressedKey == Key.Subtract,
+ Key.OemPlus => pressedKey == Key.Add,
+ Key.OemQuestion => pressedKey == Key.Oem2,
+ _ => false
+ };
+ }
+
+ public static string GetFriendlyName(Key key, KeyModifiers modifiers) {
+ string mods = GetFriendlyModifiersName(modifiers);
+ string friendlyKey = GetFriendlyName(key.ToString());
+ if (!string.IsNullOrEmpty(mods)) {
+ return IsMac ? $"{mods} {friendlyKey}": $"{mods} + {friendlyKey}";
+ }
+ return friendlyKey;
+ }
+
+ public static string GetFriendlyName(string keyName) {
+ return keyName switch {
+ // Modifiers
+ "Windows" or "LWin" or "RWin" => IsMac ? "⌘" : "Win",
+ "LeftAlt" or "RightAlt" or "Alt" => IsMac ? "⌥" : "Alt",
+ "Control" or "LeftCtrl" or "RightCtrl" or "LControl" or "RControl" => IsMac ? "⌃" : "Ctrl",
+ "Shift" or "LeftShift" or "RightShift" => IsMac ? "⇧" : "Shift",
+
+ // Navigation & Editing
+ "Escape" => "Esc",
+ "Return" => "Enter",
+ "Back" => "Backspace",
+ "Delete" => "Delete",
+ "Insert" => "Ins",
+ "PageUp" => "PgUp",
+ "PageDown" => "PgDn",
+ "Capital" => "Caps Lock",
+ "Scroll" => "Scroll Lock",
+ "NumLock" => "Num Lock",
+ "Snapshot" => "Print Screen",
+
+ // Numpad
+ "Divide" => "(Num /)",
+ "Multiply" => "(Num *)",
+ "Subtract" => "(Num -)",
+ "Add" => "(Num +)",
+ "Decimal" => "(Num .)",
+ "NumPad0" => "Num 0", "NumPad1" => "Num 1", "NumPad2" => "Num 2",
+ "NumPad3" => "Num 3", "NumPad4" => "Num 4", "NumPad5" => "Num 5",
+ "NumPad6" => "Num 6", "NumPad7" => "Num 7", "NumPad8" => "Num 8",
+ "NumPad9" => "Num 9",
+
+ // Digits
+ "D1" => "1", "D2" => "2", "D3" => "3", "D4" => "4", "D5" => "5",
+ "D6" => "6", "D7" => "7", "D8" => "8", "D9" => "9", "D0" => "0",
+
+ // OEM Symbols
+ "OemTilde" or "Oem8" or "Oem3" => "~",
+ "OemMinus" or "OemMinusSign" => "-",
+ "OemPlus" or "OemPlusSign" => "=",
+ "OemOpenBrackets" or "Oem4" => "[",
+ "OemCloseBrackets" or "Oem6" => "]",
+ "OemPipe" or "Oem5" or "OemBackslash" => "\\",
+ "OemSemicolon" or "Oem1" => ";",
+ "OemQuotes" or "Oem7" => "'",
+ "OemComma" or "OemCommaSign" => ",",
+ "OemPeriod" or "OemPeriodSign" => ".",
+ "OemQuestion" or "Oem2" => "/",
+
+ _ => keyName
+ };
+ }
+
+ public static string GetFriendlyModifiersName(KeyModifiers modifiers) {
+ if (modifiers == KeyModifiers.None) return string.Empty;
+
+ var parts = new List();
+ if (modifiers.HasFlag(KeyModifiers.Control)) {
+ parts.Add(IsMac ? "⌃" : "Ctrl");
+ }
+ if (modifiers.HasFlag(KeyModifiers.Alt)) {
+ parts.Add(IsMac ? "⌥" : "Alt");
+ }
+ if (modifiers.HasFlag(KeyModifiers.Shift)) {
+ parts.Add(IsMac ? "⇧" : "Shift");
+ }
+ if (modifiers.HasFlag(KeyModifiers.Meta)) {
+ parts.Add(IsMac ? "⌘" : "Win");
+ }
+
+ return string.Join(IsMac ? " " : " + ", parts);
+ }
+ }
+}
diff --git a/OpenUtau/ViewModels/MainWindowViewModel.cs b/OpenUtau/ViewModels/MainWindowViewModel.cs
index d3090673f..2ad75cfe9 100644
--- a/OpenUtau/ViewModels/MainWindowViewModel.cs
+++ b/OpenUtau/ViewModels/MainWindowViewModel.cs
@@ -4,6 +4,7 @@
using System.Linq;
using System.Reactive;
using System.Threading.Tasks;
+using Avalonia.Input;
using Avalonia.Threading;
using DynamicData.Binding;
using OpenUtau.App.Views;
@@ -87,16 +88,27 @@ public class MainWindowViewModel : ViewModelBase, ICmdSubscriber {
[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");
+ [Reactive] public Dictionary Hotkeys { get; set; } = new Dictionary();
private ObservableCollectionExtended openRecentMenuItems
= new ObservableCollectionExtended();
private ObservableCollectionExtended openTemplatesMenuItems
= new ObservableCollectionExtended();
+ public void ReloadShortcuts() {
+ KeyTranslator.LoadShortcuts();
+ var newHotkeys = new Dictionary();
+ foreach (var sc in KeyTranslator.Shortcuts) {
+ newHotkeys.TryAdd(sc.ActionId, sc.Gesture);
+ }
+ Hotkeys = newHotkeys;
+ }
+
// view will set this to the real AskIfSaveAndContinue implementation
public Func>? AskIfSaveAndContinue { get; set; }
public MainWindowViewModel() {
+ ReloadShortcuts();
PlaybackViewModel = new PlaybackViewModel();
TracksViewModel = new TracksViewModel();
ClearCacheHeader = string.Empty;
@@ -136,6 +148,8 @@ public MainWindowViewModel() {
PianoRollMaxHeight = x ? double.PositiveInfinity : 0;
PianoRollMinHeight = x ? ViewConstants.PianoRollMinHeight : 0;
});
+ MessageBus.Current.Listen()
+ .Subscribe(_ => ReloadShortcuts());
}
public void Undo() {
diff --git a/OpenUtau/ViewModels/PianoRollViewModel.cs b/OpenUtau/ViewModels/PianoRollViewModel.cs
index f4f1bfbcb..eb07ba690 100644
--- a/OpenUtau/ViewModels/PianoRollViewModel.cs
+++ b/OpenUtau/ViewModels/PianoRollViewModel.cs
@@ -47,6 +47,7 @@ public class PianoRollViewModel : ViewModelBase, ICmdSubscriber {
[Reactive] public NotesViewModel NotesViewModel { get; set; }
[Reactive] public PlaybackViewModel? PlaybackViewModel { get; set; }
[Reactive] public CurveViewModel CurveViewModel { get; set; }
+ [Reactive] public Dictionary Hotkeys { get; set; } = new Dictionary();
public double Width => Preferences.Default.PianorollWindowSize.Width;
public double Height => Preferences.Default.PianorollWindowSize.Height;
@@ -78,9 +79,9 @@ public bool 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;
+ [Reactive] public int ToolIndex { get; set; }
+ [Reactive] public int PenToolIndex { get; set; }
+ [Reactive] public bool PitchOverwrite { get; set; }
public ObservableCollectionExtended LegacyPlugins { get; private set; }
= new ObservableCollectionExtended();
@@ -117,16 +118,70 @@ public bool ShowPhonemizerTags {
private ReactiveCommand legacyPluginCommand;
+ public void ReloadShortcuts() {
+ var newHotkeys = new Dictionary();
+ foreach (var sc in KeyTranslator.Shortcuts) {
+ newHotkeys.TryAdd(sc.ActionId, sc.Gesture);
+ }
+ Hotkeys = newHotkeys;
+ }
+
+ private int IndexToBaseTool(int index) {
+ return index switch {
+ 0 => 0, // UI Mouse -> Backend 0 (Mouse)
+ 1 => 1, // UI Draw -> Backend 1 (Draw)
+ 2 => 2, // UI Eraser -> Backend 2 (Eraser)
+ 3 => 4, // UI Draw Pitch -> Backend 4 (Draw Pitch)
+ 4 => 5, // UI Line -> Backend 5 (Line)
+ 5 => 6, // UI S-Curve -> Backend 6 (S-Curve)
+ 6 => 7, // UI Sine Wave -> Backend 7 (Sine Wave)
+ 7 => 8, // UI Smoothen -> Backend 8 (Smoothen)
+ 8 => 3, // UI Knife -> Backend 3 (Knife)
+ _ => 0
+ };
+ }
+
+ private int BaseToolToIndex(int baseTool) {
+ return baseTool switch {
+ 0 => 0, // Backend 0 (Mouse) -> UI Mouse
+ 1 => 1, // Backend 1 (Draw) -> UI Draw
+ 2 => 2, // Backend 2 (Eraser) -> UI Eraser
+ 3 => 8, // Backend 3 (Knife) -> UI Knife
+ 4 => 3, // Backend 4 (Draw Pitch) -> UI Draw Pitch
+ 5 => 4, // Backend 5 (Line) -> UI Line
+ 6 => 5, // Backend 6 (S-Curve) -> UI S-Curve
+ 7 => 6, // Backend 7 (Sine Wave) -> UI Sine Wave
+ 8 => 7, // Backend 8 (Smoothen) -> UI Smoothen
+ _ => 0
+ };
+ }
+
public PianoRollViewModel() {
+ ReloadShortcuts();
NotesViewModel = new NotesViewModel();
CurveViewModel = new CurveViewModel();
+ // Safely initialize from preferences
+ ToolIndex = BaseToolToIndex(EditTool.BaseTool);
+ PenToolIndex = EditTool.PenToolVariation;
+ PitchOverwrite = EditTool.OverwritePitch;
+
this.WhenAnyValue(vm => vm.ToolIndex)
- .Subscribe(index => EditTool.BaseTool = index);
+ .Subscribe(index => {
+ EditTool.BaseTool = IndexToBaseTool(index);
+ Preferences.Default.EditTool.BaseTool = EditTool.BaseTool;
+ Preferences.Save();
+ });
+
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(); });
+ .Subscribe(val => {
+ EditTool.OverwritePitch = val;
+ Preferences.Default.EditTool.OverwritePitch = val;
+ Preferences.Save();
+ });
NoteDeleteCommand = ReactiveCommand.Create(info => {
NotesViewModel.DeleteSelectedNotes();
@@ -214,6 +269,8 @@ public PianoRollViewModel() {
});
LoadLegacyPlugins();
DocManager.Inst.AddSubscriber(this);
+ MessageBus.Current.Listen()
+ .Subscribe(_ => ReloadShortcuts());
}
private void SetUndoState() {
@@ -235,6 +292,7 @@ private void LoadLegacyPlugins() {
LegacyPlugins.Clear();
LegacyPlugins.AddRange(DocManager.Inst.Plugins.Select(plugin => new MenuItemViewModel() {
Header = plugin.Name,
+ InputGesture = KeyTranslator.GetGestureForMenu(plugin.Name),
Command = legacyPluginCommand,
CommandParameter = plugin,
}));
@@ -319,4 +377,4 @@ public void OnNext(UCommand cmd, bool isUndo) {
#endregion
}
-}
+}
\ No newline at end of file
diff --git a/OpenUtau/ViewModels/PreferencesViewModel.cs b/OpenUtau/ViewModels/PreferencesViewModel.cs
index 0683b822e..b70291787 100644
--- a/OpenUtau/ViewModels/PreferencesViewModel.cs
+++ b/OpenUtau/ViewModels/PreferencesViewModel.cs
@@ -1,17 +1,21 @@
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Reactive;
using System.Reactive.Linq;
using System.Text.RegularExpressions;
+using Avalonia.Input;
using OpenUtau.Audio;
using OpenUtau.Classic;
using OpenUtau.Core;
+using OpenUtau.Core.Editing;
+using OpenUtau.Core.Render;
using OpenUtau.Core.Util;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
-using OpenUtau.Core.Render;
using Serilog;
namespace OpenUtau.App.ViewModels {
@@ -24,7 +28,95 @@ public override string ToString() {
return klass.Name;
}
}
+ public class ShortcutsRefreshEvent { }
+ public class ShortcutItemViewModel : ViewModelBase {
+ public string ActionName { get; } = string.Empty;
+ public string ActionId { get; } = string.Empty;
+
+ public List Gestures { get; set; } = new List();
+ public bool IsListening { get; set; } = false;
+ public bool IsAdding { get; set; } = false;
+
+ public ObservableCollection ContextMenuItems { get; set; } = new ObservableCollection();
+
+ public string DisplayString {
+ get {
+ if (IsListening) return ThemeManager.GetString("prefs.shortcuts.listening");
+ var text = string.Join(", ", Gestures.Select(g => KeyTranslator.GetFriendlyName(g.Key, g.KeyModifiers)));
+ return Gestures.Count == 0 ? "(None)" : text;
+ }
+ }
+ public Action? Save;
+ public Action? ListenForShortcut;
+
+ public ShortcutItemViewModel() {}
+ public ShortcutItemViewModel(Preferences.ShortcutBinding binding, Action save, Action? listenForShortcut, string prefix = "") {
+ ActionId = binding.ActionId;
+ ActionName = $"{prefix}{GetDisplayName(binding.ActionId)}";
+ Gestures = binding.Shortcuts.Select(KeyTranslator.StringToGesture).ToList();
+ Save = save;
+ ListenForShortcut = listenForShortcut;
+ }
+ public ShortcutItemViewModel(string actionId, Action save, Action? listenForShortcut, string prefix = "") {
+ ActionId = actionId;
+ ActionName = $"{prefix}{GetDisplayName(ActionId)}";
+ Save = save;
+ ListenForShortcut = listenForShortcut;
+ }
+ private string GetDisplayName(string actionId) {
+ string lookupKey = "shortcut." + actionId;
+ string displayName = ThemeManager.GetString(lookupKey);
+ if (string.IsNullOrEmpty(displayName) || displayName == lookupKey) {
+ displayName = ThemeManager.GetString(actionId);
+ }
+ if (string.IsNullOrEmpty(displayName)) {
+ displayName = actionId;
+ }
+ if (displayName.StartsWith("shortcut.")) {
+ displayName = displayName.Substring(9);
+ }
+ return displayName;
+ }
+
+ public void CreateMenuItem() {
+ ContextMenuItems.Clear();
+ ContextMenuItems.Add(new MenuItemViewModel() { Header = "Add", Command = ReactiveCommand.Create(() => {
+ IsAdding = true;
+ ListenForShortcut?.Invoke(this);
+ })});
+ foreach (var gesture in Gestures) {
+ var text = $"Delete \"{KeyTranslator.GetFriendlyName(gesture.Key, gesture.KeyModifiers)}\"";
+ ContextMenuItems.Add(new MenuItemViewModel() { Header = text, Command = ReactiveCommand.Create(() => Delete(gesture)) });
+ }
+ ContextMenuItems.Add(new MenuItemViewModel() { Header = "Reset", Command = ReactiveCommand.Create(() => Reset()) });
+ }
+
+ public void Delete(KeyGesture gesture) {
+ Gestures.Remove(gesture);
+ IsAdding = false;
+ IsListening = false;
+ RefreshDisplay();
+ Save?.Invoke();
+ }
+
+ public void Reset() {
+ var binding = KeyTranslator.DefShortcuts.FirstOrDefault(s => s.ActionId == ActionId);
+ if (binding == null) {
+ Gestures.Clear();
+ } else {
+ Gestures = binding.Shortcuts.Select(KeyTranslator.StringToGesture).ToList();
+ }
+ IsAdding = false;
+ IsListening = false;
+ RefreshDisplay();
+ Save?.Invoke();
+ }
+
+ public void RefreshDisplay() {
+ this.RaisePropertyChanged(nameof(DisplayString));
+ }
+ }
public class PreferencesViewModel : ViewModelBase {
// General
private CultureInfo? language;
@@ -128,6 +220,13 @@ public int SafeMaxThreadCount {
[Reactive] public bool RememberVsqx { get; set; }
public string WinePath => Preferences.Default.WinePath;
+ // Shortcuts
+ [Reactive] public ShortcutItemViewModel? ActiveShortcut { get; set; }
+ private List allShortcuts = new List();
+ public ObservableCollection FilteredShortcuts { get; } = new ObservableCollection();
+ [Reactive] public string ShortcutSearchText { get; set; } = string.Empty;
+ public ReactiveCommand ListenForShortcutCommand { get; }
+
public PreferencesViewModel() {
var audioOutput = PlaybackManager.Inst.AudioOutput;
if (audioOutput != null) {
@@ -190,6 +289,8 @@ public PreferencesViewModel() {
RememberUst = Preferences.Default.RememberUst;
RememberVsqx = Preferences.Default.RememberVsqx;
ClearCacheOnQuit = Preferences.Default.ClearCacheOnQuit;
+ ListenForShortcutCommand = ReactiveCommand.Create(ListenForShortcut);
+ LoadShortcuts();
MessageBus.Current.Listen()
.Subscribe(_ => this.RaisePropertyChanged(nameof(IsThemeEditorOpen)));
@@ -397,6 +498,18 @@ public PreferencesViewModel() {
Preferences.Default.SkipRenderingMutedTracks = skipRenderingMutedTracks;
Preferences.Save();
});
+ this.WhenAnyValue(vm => vm.ShortcutSearchText)
+ .Subscribe(text => {
+ FilteredShortcuts.Clear();
+ var lowerText = text?.ToLowerInvariant() ?? string.Empty;
+ foreach (var sc in allShortcuts) {
+ if (string.IsNullOrEmpty(lowerText) ||
+ sc.ActionName.ToLowerInvariant().Contains(lowerText) ||
+ sc.DisplayString.ToLowerInvariant().Contains(lowerText)) {
+ FilteredShortcuts.Add(sc);
+ }
+ }
+ });
}
public void TestAudioOutputDevice() {
@@ -452,5 +565,86 @@ public void RefreshThemes() {
public void ToggleOnnxGpuDisplay(bool show) {
ShowOnnxGpu = show;
}
+
+ private void LoadShortcuts() {
+ var shortcuts = KeyTranslator.GetMergedShortcuts().Select(binding => new ShortcutItemViewModel(binding, () => SaveShortcuts(), item => ListenForShortcut(item)));
+ allShortcuts.AddRange(shortcuts);
+
+ // external batch edits
+ var prefix = $"{ThemeManager.GetString("pianoroll.menu.external")}: ";
+ foreach (var type in DocManager.Inst.ExternalBatchEditTypes) {
+ if (Activator.CreateInstance(type) is BatchEdit edit) {
+ var customized = Preferences.Default.PluginShortcuts.FirstOrDefault(pref => pref.ActionId == edit.Name);
+ if (customized != null) {
+ allShortcuts.Add(new ShortcutItemViewModel(customized, () => SaveShortcuts(), item => ListenForShortcut(item), prefix));
+ } else if (!allShortcuts.Any(s => s.ActionId == edit.Name)) {
+ allShortcuts.Add(new ShortcutItemViewModel(edit.Name, () => SaveShortcuts(), item => ListenForShortcut(item), prefix));
+ }
+ }
+ }
+ // legacy plugins
+ prefix = $"{ThemeManager.GetString("pianoroll.menu.part.legacypluginexp")}: ";
+ foreach (var plugin in DocManager.Inst.Plugins) {
+ var customized = Preferences.Default.PluginShortcuts.FirstOrDefault(pref => pref.ActionId == plugin.Name);
+ if (customized != null) {
+ allShortcuts.Add(new ShortcutItemViewModel(customized, () => SaveShortcuts(), item => ListenForShortcut(item), prefix));
+ } else if (!allShortcuts.Any(s => s.ActionId == plugin.Name)) {
+ allShortcuts.Add(new ShortcutItemViewModel(plugin.Name, () => SaveShortcuts(), item => ListenForShortcut(item), prefix));
+ }
+ }
+ }
+
+ public void ListenForShortcut(ShortcutItemViewModel item) {
+ // Cancel any existing listening item
+ if (ActiveShortcut != null) {
+ ActiveShortcut.IsAdding = false;
+ ActiveShortcut.IsListening = false;
+ ActiveShortcut.RefreshDisplay();
+ }
+
+ ActiveShortcut = item;
+ ActiveShortcut.IsListening = true;
+ ActiveShortcut.RefreshDisplay();
+ }
+
+ private void SaveShortcuts() {
+ KeyTranslator.SaveShortcuts(allShortcuts);
+ MessageBus.Current.SendMessage(new ShortcutsRefreshEvent());
+ }
+
+ public void AssignShortcut(Key key, KeyModifiers modifiers) {
+ if (ActiveShortcut == null) return;
+ if (key == Key.LeftCtrl || key == Key.RightCtrl || key == Key.LeftShift || key == Key.RightShift || key == Key.LeftAlt || key == Key.RightAlt || key == Key.LWin || key == Key.RWin) {
+ return;
+ }
+
+ var duplicate = allShortcuts.FirstOrDefault(s => s != ActiveShortcut && s.Gestures.Any(g => g.Key == key && g.KeyModifiers == modifiers));
+
+ if (duplicate != null) {
+ ActiveShortcut.IsAdding = false;
+ ActiveShortcut.IsListening = false;
+ ActiveShortcut.RefreshDisplay();
+ ActiveShortcut = null;
+
+ var e = new MessageCustomizableException("The shortcut is already assigned.", "", new ArgumentException(), false, [duplicate.DisplayString, duplicate.ActionName]);
+ DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e));
+ return;
+ }
+ if (!ActiveShortcut.IsAdding) {
+ ActiveShortcut.Gestures.Clear();
+ }
+ ActiveShortcut.Gestures.Add(new KeyGesture(key, modifiers));
+ ActiveShortcut.IsAdding = false;
+ ActiveShortcut.IsListening = false;
+ ActiveShortcut.RefreshDisplay();
+ ActiveShortcut = null;
+ SaveShortcuts();
+ }
+
+ public void ResetAllShortcuts() {
+ KeyTranslator.ResetShortcuts();
+ LoadShortcuts();
+ MessageBus.Current.SendMessage(new ShortcutsRefreshEvent());
+ }
}
}
diff --git a/OpenUtau/Views/MainWindow.axaml b/OpenUtau/Views/MainWindow.axaml
index 94523d8b1..2dc8cc1ff 100644
--- a/OpenUtau/Views/MainWindow.axaml
+++ b/OpenUtau/Views/MainWindow.axaml
@@ -21,16 +21,16 @@
Opened="OnMainMenuOpened" Closed="OnMainMenuClosed" PointerExited="OnMainMenuPointerLeave"
ZIndex="1">
-
+
-
+
-
-
+
+
@@ -49,8 +49,8 @@
-
-
+
+
@@ -66,7 +66,7 @@
-
+
diff --git a/OpenUtau/Views/MainWindow.axaml.cs b/OpenUtau/Views/MainWindow.axaml.cs
index 4dcb7e32b..294609d94 100644
--- a/OpenUtau/Views/MainWindow.axaml.cs
+++ b/OpenUtau/Views/MainWindow.axaml.cs
@@ -789,86 +789,62 @@ void OnKeyDown(object sender, KeyEventArgs args) {
args.Handled = false;
return;
}
-
+ args.Handled = OnKeyExtendedHandler(args);
+ }
+ bool OnKeyExtendedHandler(KeyEventArgs args) {
var tracksVm = viewModel.TracksViewModel;
- if (args.KeyModifiers == KeyModifiers.None) {
- args.Handled = true;
- switch (args.Key) {
- case Key.Delete: viewModel.TracksViewModel.DeleteSelectedParts(); break;
- case Key.Space: PlayOrPause(); break;
- case Key.Home: viewModel.PlaybackViewModel.MovePlayPos(0); break;
- case Key.End:
- if (viewModel.TracksViewModel.Parts.Count > 0) {
- int endTick = viewModel.TracksViewModel.Parts.Max(part => part.End);
- viewModel.PlaybackViewModel.MovePlayPos(endTick);
- }
- break;
- case Key.F11:
- OnMenuFullScreen(this, new RoutedEventArgs());
- break;
- default:
- args.Handled = false;
- break;
- }
- } else if (args.KeyModifiers == KeyModifiers.Alt) {
- args.Handled = true;
- switch (args.Key) {
- case Key.F4:
- (Application.Current?.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown();
- break;
- default:
- args.Handled = false;
- break;
- }
- } else if (args.KeyModifiers == cmdKey) {
- args.Handled = true;
- switch (args.Key) {
- case Key.A: viewModel.TracksViewModel.SelectAllParts(); break;
- case Key.N: NewProject(); break;
- case Key.O: Open(); break;
- case Key.S: _ = Save(); break;
- case Key.Z: viewModel.Undo(); break;
- case Key.Y: viewModel.Redo(); break;
- case Key.C: tracksVm.CopyParts(); break;
- case Key.X: tracksVm.CutParts(); break;
- case Key.V: tracksVm.PasteParts(); break;
- default:
- args.Handled = false;
- break;
- }
- } else if (args.KeyModifiers == KeyModifiers.Shift) {
- args.Handled = true;
- switch (args.Key) {
- // solo
- case Key.S:
- if (viewModel.TracksViewModel.SelectedParts.Count > 0) {
- var part = viewModel.TracksViewModel.SelectedParts.First();
- var track = DocManager.Inst.Project.tracks[part.trackNo];
- MessageBus.Current.SendMessage(new TracksSoloEvent(part.trackNo, !track.Solo, false));
- }
- break;
- // mute
- case Key.M:
- if (viewModel.TracksViewModel.SelectedParts.Count > 0) {
- var part = viewModel.TracksViewModel.SelectedParts.First();
- MessageBus.Current.SendMessage(new TracksMuteEvent(part.trackNo, false));
- }
- break;
- default:
- args.Handled = false;
- break;
- }
- } else if (args.KeyModifiers == (cmdKey | KeyModifiers.Shift)) {
- args.Handled = true;
- switch (args.Key) {
- case Key.Z: viewModel.Redo(); break;
- case Key.S: _ = SaveAs(); break;
- default:
- args.Handled = false;
- break;
- }
+ string? action = KeyTranslator.GetActionIdFromKey(args.Key, args.KeyModifiers);
+ if (action == null) return false;
+
+ switch (action) {
+ // Playback & Selection
+ case "PlayOrPause": PlayOrPause(); return true;
+ case "SelectAll": viewModel.TracksViewModel.SelectAllParts(); return true;
+
+ // UI & Windows
+ case "CloseWindow":
+ (Application.Current?.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown();
+ return true;
+ case "menu.tools.fullscreen": OnMenuFullScreen(this, new RoutedEventArgs()); return true;
+
+ // Playhead & Timeline Navigation
+ case "PlayheadHome": viewModel.PlaybackViewModel.MovePlayPos(0); return true;
+ case "PlayheadEnd":
+ if (viewModel.TracksViewModel.Parts.Count > 0) {
+ int endTick = viewModel.TracksViewModel.Parts.Max(part => part.End);
+ viewModel.PlaybackViewModel.MovePlayPos(endTick);
+ }
+ return true;
+
+ // Track & Project Operations
+ case "NewProject": NewProject(); return true;
+ case "OpenProject": Open(); return true;
+ case "SaveProject": _ = Save(); return true;
+ case "SaveProjectAs": _ = SaveAs(); return true;
+ case "SoloTrack":
+ if (viewModel.TracksViewModel.SelectedParts.Count > 0) {
+ var part = viewModel.TracksViewModel.SelectedParts.First();
+ var track = DocManager.Inst.Project.tracks[part.trackNo];
+ MessageBus.Current.SendMessage(new TracksSoloEvent(part.trackNo, !track.Solo, false));
+ }
+ return true;
+ case "MuteTrack":
+ if (viewModel.TracksViewModel.SelectedParts.Count > 0) {
+ var part = viewModel.TracksViewModel.SelectedParts.First();
+ MessageBus.Current.SendMessage(new TracksMuteEvent(part.trackNo, false));
+ }
+ return true;
+
+ // Edit Operations
+ case "menu.edit.undo": viewModel.Undo(); return true;
+ case "menu.edit.redo": viewModel.Redo(); return true;
+ case "menu.edit.copy": tracksVm.CopyParts(); return true;
+ case "menu.edit.cut": tracksVm.CutParts(); return true;
+ case "menu.edit.paste": tracksVm.PasteParts(); return true;
+ case "menu.edit.delete": viewModel.TracksViewModel.DeleteSelectedParts(); return true;
}
+ return false;
}
void OnPointerPressed(object? sender, PointerPressedEventArgs args) {
diff --git a/OpenUtau/Views/PreferencesDialog.axaml b/OpenUtau/Views/PreferencesDialog.axaml
index 290323724..7b19fdf6c 100644
--- a/OpenUtau/Views/PreferencesDialog.axaml
+++ b/OpenUtau/Views/PreferencesDialog.axaml
@@ -5,12 +5,12 @@
xmlns:vm="using:OpenUtau.App.ViewModels"
xmlns:controls="clr-namespace:Material.Styles.Controls;assembly=Material.Styles"
xmlns:styles="clr-namespace:Material.Styles;assembly=Material.Styles"
- mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="460"
+ mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="480"
x:Class="OpenUtau.App.Views.PreferencesDialog"
Icon="/Assets/open-utau.ico"
Title="{DynamicResource prefs.caption}"
WindowStartupLocation="CenterScreen"
- Width="700" Height="460">
+ Width="700" Height="480">
@@ -60,13 +60,14 @@
+
-
+
-
+
@@ -74,7 +75,7 @@
-
+
@@ -83,42 +84,42 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -133,12 +134,12 @@
-
+
-
+
@@ -152,29 +153,33 @@
HorizontalAlignment="Stretch" Click="ReloadSingers"/>
-
+
-
+
-
+
-
+
+
+
+
+
-
+
@@ -193,17 +198,17 @@
Foreground="Red" Margin="0,0,0,4" IsVisible="{Binding HighThreads}" FontSize="11"/>
-
+
-
+
-
+
-
+
@@ -211,7 +216,7 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -286,7 +291,7 @@
-
+
@@ -299,14 +304,14 @@
-
+
-
+
-
+
-
+
@@ -318,18 +323,18 @@
TickPlacement="BottomRight" TickFrequency="1" IsSnapToTickEnabled="true"/>
-
+
-
+
-
+
@@ -339,7 +344,7 @@
+ IsVisible="{OnPlatform Windows=False, Default=True}" Margin="0,10,0,0" FontWeight="Bold"/>
@@ -353,6 +358,64 @@
HorizontalAlignment="Stretch" Click="DetectWinePath"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OpenUtau/Views/PreferencesDialog.axaml.cs b/OpenUtau/Views/PreferencesDialog.axaml.cs
index 0f3d0a0f0..a4a0556e1 100644
--- a/OpenUtau/Views/PreferencesDialog.axaml.cs
+++ b/OpenUtau/Views/PreferencesDialog.axaml.cs
@@ -8,15 +8,52 @@
using OpenUtau.App.ViewModels;
using OpenUtau.Colors;
using OpenUtau.Core;
+using Avalonia.Input;
namespace OpenUtau.App.Views {
public partial class PreferencesDialog : Window {
- private PreferencesViewModel? viewModel => this.DataContext as PreferencesViewModel;
+ private PreferencesViewModel? viewModel => this.DataContext as PreferencesViewModel;
public PreferencesDialog() {
InitializeComponent();
}
+ void OnKeyDown(object sender, KeyEventArgs e) {
+ if (DataContext is PreferencesViewModel vm && vm.ActiveShortcut != null) {
+ // If they hit escape without modifiers, cancel listening
+ if (e.Key == Key.Escape && e.KeyModifiers == KeyModifiers.None) {
+ vm.ActiveShortcut.IsAdding = false;
+ vm.ActiveShortcut.IsListening = false;
+ vm.ActiveShortcut.RefreshDisplay();
+ vm.ActiveShortcut = null;
+ e.Handled = true;
+ return;
+ }
+
+ vm.AssignShortcut(e.Key, e.KeyModifiers);
+ e.Handled = true;
+ return;
+ }
+ }
+
+ public void OnShortcutRightClick(object sender, PointerReleasedEventArgs e) {
+ if (e.InitialPressMouseButton == MouseButton.Right &&
+ sender is Button btn &&
+ btn.DataContext is ShortcutItemViewModel item) {
+ item.Reset();
+ e.Handled = true;
+ }
+ }
+
+ void OnShortcutMiscClick(object? sender, RoutedEventArgs e) {
+ if (sender is Button btn &&
+ btn.DataContext is ShortcutItemViewModel item) {
+ item.CreateMenuItem();
+ btn.ContextMenu?.Open();
+ e.Handled = true;
+ }
+ }
+
void OpenSingersFolder(object sender, RoutedEventArgs e) {
try {
Directory.CreateDirectory(viewModel!.SingerPath);
@@ -132,7 +169,7 @@ void OnCustomThemeCreate(object sender, RoutedEventArgs e) {
dialog.SetPrompt(ThemeManager.GetString("prefs.appearance.customtheme.create.prompt"));
dialog.onFinish = s => {
if (string.IsNullOrEmpty(s)) {
- MessageBox.ShowModal(this,
+ MessageBox.ShowModal(this,
ThemeManager.GetString("prefs.appearance.customtheme.create.empty"),
ThemeManager.GetString("prefs.appearance.customtheme.create.title"));
return;