From b9339aa8fa3b263b6cd5a37d9ad6089d9c2b2cc7 Mon Sep 17 00:00:00 2001 From: RedBlackAka <140876408+RedBlackAka@users.noreply.github.com> Date: Thu, 19 Dec 2024 03:04:38 +0100 Subject: [PATCH 1/4] Create a check for update on start toggle --- OpenUtau.Core/Util/Preferences.cs | 771 ++++-- OpenUtau/Strings/Strings.axaml | 3 +- OpenUtau/ViewModels/PreferencesViewModel.cs | 10 +- OpenUtau/Views/MainWindow.axaml.cs | 2708 ++++++++++--------- OpenUtau/Views/PreferencesDialog.axaml | 6 +- 5 files changed, 1942 insertions(+), 1556 deletions(-) diff --git a/OpenUtau.Core/Util/Preferences.cs b/OpenUtau.Core/Util/Preferences.cs index 1c525fb54..e7f705ccc 100644 --- a/OpenUtau.Core/Util/Preferences.cs +++ b/OpenUtau.Core/Util/Preferences.cs @@ -1,200 +1,571 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Text; -using Newtonsoft.Json; -using OpenUtau.Core.Render; -using Serilog; - -namespace OpenUtau.Core.Util { - - public static class Preferences { - public static SerializablePreferences Default; - - static Preferences() { - Load(); - } - - public static void Save() { - try { - File.WriteAllText(PathManager.Inst.PrefsFilePath, - JsonConvert.SerializeObject(Default, Formatting.Indented), - Encoding.UTF8); - } catch (Exception e) { - Log.Error(e, "Failed to save prefs."); - } - } - - public static void Reset() { - Default = new SerializablePreferences(); - Save(); - } - - public static List GetSingerSearchPaths() { - return new List(Default.SingerSearchPaths); - } - - public static void SetSingerSearchPaths(List paths) { - Default.SingerSearchPaths = new List(paths); - Save(); - } - - public static void AddRecentFileIfEnabled(string filePath){ - //Users can choose adding .ust, .vsqx and .mid files to recent files or not - string ext = Path.GetExtension(filePath); - switch(ext){ - case ".ustx": - AddRecentFile(filePath); - break; - case ".mid": - case ".midi": - if(Preferences.Default.RememberMid){ - AddRecentFile(filePath); - } - break; - case ".ust": - if(Preferences.Default.RememberUst){ - AddRecentFile(filePath); - } - break; - case ".vsqx": - if(Preferences.Default.RememberVsqx){ - AddRecentFile(filePath); - } - break; - default: - break; - } - } - - private static void AddRecentFile(string filePath) { - if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) { - return; - } - var recent = Default.RecentFiles; - recent.RemoveAll(f => f == filePath); - recent.Insert(0, filePath); - recent.RemoveAll(f => string.IsNullOrEmpty(f) - || !File.Exists(f) - || f.Contains(PathManager.Inst.TemplatesPath)); - if (recent.Count > 16) { - recent.RemoveRange(16, recent.Count - 16); - } - Save(); - } - - private static void Load() { - try { - if (File.Exists(PathManager.Inst.PrefsFilePath)) { - Default = JsonConvert.DeserializeObject( - File.ReadAllText(PathManager.Inst.PrefsFilePath, Encoding.UTF8)); - if(Default == null) { - Reset(); - return; - } - - if (!ValidString(new Action(() => CultureInfo.GetCultureInfo(Default.Language)))) Default.Language = string.Empty; - if (!ValidString(new Action(() => CultureInfo.GetCultureInfo(Default.SortingOrder)))) Default.SortingOrder = string.Empty; - if (!Renderers.getRendererOptions().Contains(Default.DefaultRenderer)) Default.DefaultRenderer = string.Empty; - if (!Onnx.getRunnerOptions().Contains(Default.OnnxRunner)) Default.OnnxRunner = string.Empty; - } else { - Reset(); - } - } catch (Exception e) { - Log.Error(e, "Failed to load prefs."); - Default = new SerializablePreferences(); - } - } - - private static bool ValidString(Action action) { - try { - action(); - return true; - } catch { - return false; - } - } - - [Serializable] - public class SerializablePreferences { - public const int MidiWidth = 1024; - public const int MidiHeight = 768; - public int MainWidth = 1024; - public int MainHeight = 768; - public bool MainMaximized; - public bool MidiMaximized; - public int UndoLimit = 100; - public List SingerSearchPaths = new List(); - public string PlaybackDevice = string.Empty; - public int PlaybackDeviceNumber; - public int? PlaybackDeviceIndex; - public bool ShowPrefs = true; - public bool ShowTips = true; - public int Theme; - public bool PenPlusDefault = false; - public int DegreeStyle; - public bool UseTrackColor = false; - public bool ClearCacheOnQuit = false; - public bool PreRender = true; - public int NumRenderThreads = 2; - public string DefaultRenderer = string.Empty; - public int WorldlineR = 0; - public string OnnxRunner = string.Empty; - public int OnnxGpu = 0; - public double DiffSingerDepth = 1.0; - public int DiffSingerSteps = 20; - public bool DiffSingerTensorCache = true; - public bool SkipRenderingMutedTracks = false; - public string Language = string.Empty; - public string? SortingOrder = null; - public List RecentFiles = new List(); - public string SkipUpdate = string.Empty; - public string AdditionalSingerPath = string.Empty; - public bool InstallToAdditionalSingersPath = true; - public bool LoadDeepFolderSinger = true; - public bool PreferCommaSeparator = false; - public bool ResamplerLogging = false; - public List RecentSingers = new List(); - public List FavoriteSingers = new List(); - public Dictionary SingerPhonemizers = new Dictionary(); - public List RecentPhonemizers = new List(); - public bool PreferPortAudio = false; - public double PlayPosMarkerMargin = 0.9; - public int LockStartTime = 0; - public int PlaybackAutoScroll = 2; - public bool ReverseLogOrder = true; - public bool ShowPortrait = true; - public bool ShowIcon = true; - public bool ShowGhostNotes = true; - public bool PlayTone = true; - public bool ShowVibrato = true; - public bool ShowPitch = true; - public bool ShowFinalPitch = true; - public bool ShowWaveform = true; - public bool ShowPhoneme = true; - public bool ShowNoteParams = false; - public Dictionary DefaultResamplers = new Dictionary(); - public Dictionary DefaultWavtools = new Dictionary(); - public string LyricHelper = string.Empty; - public bool LyricsHelperBrackets = false; - public int OtoEditor = 0; - public string VLabelerPath = string.Empty; - public string SetParamPath = string.Empty; - public bool Beta = false; - public bool RememberMid = false; - public bool RememberUst = true; - public bool RememberVsqx = true; - public int ImportTempo = 0; - public string PhoneticAssistant = string.Empty; - public string RecentOpenSingerDirectory = string.Empty; - public string RecentOpenProjectDirectory = string.Empty; - public bool LockUnselectedNotesPitch = true; - public bool LockUnselectedNotesVibrato = true; - public bool LockUnselectedNotesExpressions = true; - - public bool VoicebankPublishUseIgnore = true; - public string VoicebankPublishIgnores = "#Adobe Audition\n*.pkf\n\n#UTAU Engines\n*.ctspec\n*.d4c\n*.dio\n*.frc\n*.frt\n#*.frq\n*.harvest\n*.lessaudio\n*.llsm\n*.mrq\n*.pitchtier\n*.pkf\n*.platinum\n*.pmk\n*.star\n*.uspec\n*.vs4ufrq\n\n#UTAU related tools\n$read\n*.setParam-Scache\n*.lbp\n*.lbp.caches/*\n\n#OpenUtau\nerrors.txt\n*.sc.npz"; - } - } -} + + + Copy note + Delete note + Select and paste parameters + Delete part + View file location + Rename part + Reselect audio file + Transcribe audio to create a note part + Transcribing + Ease in + Ease in/out + Ease out + Linear + Add point + Delete point + Snap to previous note + Add tempo change at {0} + Add time signature change + Delete tempo change at {0} + Delete time signature change + + Clear + Copy Log + Reverse Log Order + + About OpenUTAU + + OpenUtau aims to be an open source editing environment for UTAU community, with a modern user experience and intelligent phonological support. Visit us on Github. + + Exit OpenUtau + The current project contains unsaved changes. Do you want to save it? + Export + Save project file first + Import tracks + The imported project contains the following tempo markers. Do you want to use them? + Installing dependency + Installing + Installing phonemizer + Installing + Cancel + Copy error to clipboard + No + Ok + Yes + No resampler + No resampler! Put your favourite resampler exe or dll in the Resamplers folder and choose it in Preferences! + 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 + Renderer + Set As Default + Unsupported file format + Unsupported file format: + Voice color remapping + Applies to all notes in this track: + + Error + Error Details + Error loading vocoder + . Please download vocoder from + Encrypted archive file isn't supported. Please extract the archive file manually. + Abbreviation must be between 1 and 4 characters long + Abbreviation must be set + Abbreviations must be unique + Default value must be between min and max + Flags must be unique + The following expressions have been merged due to duplicate flags + Min must be smaller than max + Name must be set + Failed to export + Failed to import audio + Failed to import files + Failed to import midi + Failed to install singer + Failed to load + Failed to load prefs. Initialize it. + Failed to open + Failed to open location + Failed to render + Failed to run editing macro + Failed to save + Failed to save singer config file + Failed to search singers + Try installing the latest Visual C++ Redistributable. https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170 + Character not allowed in regular expression + - regular expression error - + + Abbreviation + Apply + Expressions + Default + Resampler Flag + Add all expressions suggested by renderers + Is Resampler Flag + Max + Min + Name + Option Values + Separate values by ',' + Type + Curve + Numerical + Options + + German + English + Spanish + French + Not translating + Italian + Japanese + Korean + Polish + Portuguese + Russian + Vietnamese + Chinese + Cantonese + Thai + + Apply + Cancel + Edit Lyrics + Live preview + Max + Reset + Select some notes first! + Separators + + After : + Before : + Remove alphabet + Remove non-hiragana + Remove phonetic hint + Remove spaces and earlier + Remove tone suffix + Presets + Preview + Regular expressions can be used + General lyrics replacement + + Edit + Copy + Cut + Delete + Lock editing of unselected notes + Expressions + Pitch Points + Vibrato + Paste + Redo + Select All + Undo + File + Exit + Export Audio + Export DiffSinger Scripts To + Export DiffSinger Scripts To (v2) + Export DiffSinger Scripts To (v2, without pitch curve) + Export Midi File + Mixdown To Wav File + Export Project + Export Ust Files + Export Ust Files To... + Export Wav Files + Export Wav Files To... + Import Audio... + Import Midi... + DryWetMidi Importer + Naudio Importer + Import Tracks... + New + New From Template + Open... + Open As + Open Export Location + Open Project Location + Open Recent + Save + Save As... + Save Template... + Help + About OpenUtau + Check Update + Open Logs Location + Report Issue + OpenUtau Documentation + Project + Expressions... + Adjust Tempo (Preserve Timing) + Tools + Clear Cache + Debug Window + Install Dependency (.oudep)... + Layout + Horizontal 1:1 + Horizontal 1:2 + Horizontal 1:3 + Vertical 1:1 + Vertical 1:2 + Vertical 1:3 + Preferences... + Install Singer... + Install Singer (Advanced)... + Singers... + View + + Lyric + Default Lyric + Portamento + Length + Start + Preset + New Preset Name + Remove + Removes last applied preset. + Save + Save current settings to a new preset. + Reset All Settings + Reset every setting to default values. +Warning: this option removes custom presets. + Vibrato + Minimum Length + Automatic Vibrato by Length + Depth + Drift + Fade In + Length + Fade Out + Period + Shift + Volume Link + + Note Properties + Apply + Basic + Cancel + Set + Set only on long notes + Tone + Enable + + Alias + Color + Consonant + Cutoff + Set Consonant + Set Cutoff + Set Offset + Set Overlap + Set Preutter + File + Offset + Overlap + Phonetic + Prefix + Preutter + Set + Suffix + + Phonetic Assistant + Copy + + Batch Edits + Running batch edit + Lyrics + Replace "-" with "+" + Edit Lyrics + Hiragana to Romaji + Insert slur lyric + Japanese VCV to CV + Move Suffix to VoiceColor + Remove Letter Suffix + Remove Phonetic Hint + Remove Tone Suffix + Romaji to Hiragana + Note Defaults + Notes + Add breath + Add tail "-" + Add tail "R" + Auto Legato + Convert PITD to pitch control points + Clear vibratos + Fix overlapping notes + Hanzi to pinyin + Lengthen crossfades + Load rendered pitch + Move an octave down + Move an octave up + Quantize to 1/128 + Quantize to 1/64 + Remove tail "-" + Remove tail "R" + Reset phoneme aliases + Reset notes to default + Reset all parameters + Reset all expressions + Reset phoneme timings + Reset pitch bends + Reset vibratos + Part + Legacy Plugin (Experimental) + Singer and Oto settings + Open folder + Reload + Reset + Search Note + Select All + Close + Next + Prev + View Final Pitch to Render (R) + View Note Parameters (\) + View Phonemes (O) + View Pitch Bend (I) + Toggle Snap (P) + Auto + Auto (triplet) + View Tips (T) + Toggle Note Tone (Y) + View Vibrato (U) + View Waveform (W) + Draw Pitch Tool (4) + Left click to draw + Right click to reset + Hold Ctrl to select + Hold Alt to smoothen + + Line Draw Pitch Tool (Shift + 4) + Left click to draw (draw straight line) + Right click to reset + Hold Ctrl to select + Hold Alt to smoothen + + Eraser Tool (3) + Knife Tool (5) + Overwrite Pitch Tool (Ctrl + 4) + Left click to draw (overwrites vibrato or mod+) + Right click to reset + Hold Ctrl to select + Hold Alt to smoothen + + Pen Plus Tool (Ctrl + 2) + Left click to draw + Right click to delete + Hold Ctrl to select + Pen Tool (2) + Left click to draw + Hold Ctrl to select + Selection Tool (1) + + Advanced + Beta + Check for update on start + When importing tracks, use the tempos of the imported project + Always + Ask me each time + Never + Lyrics Helper + Lyrics Helper Adds Brackets + Remember these file types in "Open Recent" + Resampler Logging + Stores resampler output in log files. This option slows down UI and rendering. + Stable + vLabeler Path + Appearance + Scale degree display style + Numbered (1 2 3 4 5 6 7) + Off + Solfège (do re mi fa sol la ti) + Language + Show other tracks' notes on piano roll + Show icon on piano roll + Show portrait on piano roll + Singer name display language + Theme + Dark + Light + Use track color in UI + Cache + Clear cache on quit + DiffSinger Tensor Cache + Preferences + Note: please restart OpenUtau after changing this item. + Off + On + Oto Editor + Default Oto Editor + setParam Path + Paths + Additional Singer Path + Install to Additional Singer Path + Load all depth folders + Reset + Select + Set Pen Plus Tool as Default + Playback + Auto-Scroll + Auto-Scroll Mode + Page Scroll + Stationary Cursor + Audio Backend + Automatic + MiniAudio + Auto-Scroll Margin + Playback Device + On Pausing + Do nothing + Move cursor and view position back to where you started playing + Move only cursor back to where you started playing + Test + Rendering + Default renderer (for classic voicebanks) + DiffSinger Render Depth + DiffSinger Render Steps + GPU + Machine Learning Runner + Phase Compensation + Pre-render + Resampler + + Warning: moresampler is not fully supported. It may be slow and cause high CPU usage. If you insist, please: + 1. Set "resampler-compatibility" to "on" in moreconfig.txt. + 2. Set "auto-update-llsm-mrq" to "off" in moreconfig.txt. + 3. Patiently wait for moresampler to generate missing llsm files. + + + Note: to use external resamplers, please add the resampler DLL or EXE file in the Resamplers folder in the OpenUtau install path and choose it in Preferences. + + Skip muted tracks + + Warning: too many render threads may cause OpenUtau to run slowly. + + Maximum Render Threads + Wavtool + + Loading Singers... + Project saved. {0} + Waiting Rendering + + Singers + Edit In setParam + Edit In vLabeler + Goto Source File + Regenerate FRQ + Regenerating FRQ + Reset Otos + Save Otos + Search Alias + Download setParam (v4.0b or higher) from http://nwp8861.blog92.fc2.com/ and set setParam path in Preferences first! + Download vLabeler (1.0.0-beta1 or higher) from https://github.com/sdercolin/vlabeler and set vLabeler path in Preferences first! + Generate Singer Error Report + Location + Move Left + Move Right + Select Next + Select Prev + Show All + Zoom In + Zoom Out + Play sample + Publish Singer + Create an optimized zip package of your singer for distribution. + Ignore these file types during packaging (gitignore syntax): + Publish + Use file type ignoring + Open readme.txt + readme.txt not found. + Refresh + Set Default Phonemizer + Set Encoding + Set Icon + Set Portrait + Set Singer Type + Cancel + Clear + Color + Add Color + Remove Color + Rename + Edit Subbanks + Export prefix.map + Import prefix.map + Reset + Save + Select All + Set + Tone + Tone Ranges + Use filename as alias + Visit Website + + Archive File Encoding + Choose an encoding that make file names look right. + Back + Install + Please add the following settings to this voicebank's character.yaml. + Next + Singer Type + Text File Encoding + Choose an encoding that make file contents look right. + + Override Alias + Left Button Draw: Set expressions +Right Button Draw: Reset expressions + Expression not supported by renderer + Tab: Next Note + Shift+Tab: Previous Note + Selection Tool + Box Select: Select notes + Ctrl + Box Select: Select more notes + Up/Down: Transpose selected notes + Ctrl + Up/Down: Transpose selected notes by octave +Pen Tool + Left Button Draw: Add note + Right Click: Remove note + Right Button Draw: Remove multiple notes +General + T: Toggle tips + Drag Note: Move note + Drag End of Note: Resize note + Alt + Drag End of Note: Resize neighbouring notes + Space: Play + ◀ Scroll here to zoom horizontally ▶ + Scroll here to zoom vertically ▶ + + Duplicate Track + Duplicate Track Settings + Install singer + More + Move Down + Move Up + Mute + Mute all others + Mute this only + Unmute all + Open singers location + Open additional singers location + Remove + Rename track + Select Renderer + Select Singer + Solo + Solo additionally (which not removes solo from other tracks) + Solo this only (which removes solo from other tracks) + Unsolo all + Change track color + Track Settings + + Segoe UI,San Francisco,Helvetica Neue + + Check for Update + Open singing synthesis platform + GitHub + Update v{0} available! + Checking for updates... + Up to date + Unable to check update + Update + + Warning + Your OpenUtau home path "{0}" contains non-ASCII character. Exe resamplers may not work. + No settings for this renderer. + \ No newline at end of file diff --git a/OpenUtau/Strings/Strings.axaml b/OpenUtau/Strings/Strings.axaml index c84ae62cd..ba99775d2 100644 --- a/OpenUtau/Strings/Strings.axaml +++ b/OpenUtau/Strings/Strings.axaml @@ -358,6 +358,7 @@ Warning: this option removes custom presets. Advanced Beta + Check for update on start When importing tracks, use the tempos of the imported project Always Ask me each time @@ -567,4 +568,4 @@ General Warning Your OpenUtau home path "{0}" contains non-ASCII character. Exe resamplers may not work. No settings for this renderer. - + \ No newline at end of file diff --git a/OpenUtau/ViewModels/PreferencesViewModel.cs b/OpenUtau/ViewModels/PreferencesViewModel.cs index 6fb623fc5..cfac5a432 100644 --- a/OpenUtau/ViewModels/PreferencesViewModel.cs +++ b/OpenUtau/ViewModels/PreferencesViewModel.cs @@ -75,6 +75,8 @@ public CultureInfo? SortingOrder { set => this.RaiseAndSetIfChanged(ref sortingOrder, value); } + [Reactive] public bool CheckForUpdateOnStart { get; set; } + [Reactive] public bool Beta { get; set; } public class LyricsHelperOption { @@ -152,6 +154,7 @@ public PreferencesViewModel() { ShowPortrait = Preferences.Default.ShowPortrait; ShowIcon = Preferences.Default.ShowIcon; ShowGhostNotes = Preferences.Default.ShowGhostNotes; + CheckForUpdateOnStart = Preferences.Default.CheckForUpdateOnStart; Beta = Preferences.Default.Beta; LyricsHelper = LyricsHelpers.FirstOrDefault(option => option.klass.Equals(ActiveLyricsHelper.Inst.GetPreferred())); LyricsHelperBrackets = Preferences.Default.LyricsHelperBrackets; @@ -261,6 +264,11 @@ public PreferencesViewModel() { Preferences.Save(); MessageBus.Current.SendMessage(new PianorollRefreshEvent("Part")); }); + this.WhenAnyValue(vm => vm.CheckForUpdateOnStart) + .Subscribe(checkForUpdateOnStart => { + Preferences.Default.CheckForUpdateOnStart = checkForUpdateOnStart; + Preferences.Save(); + }); this.WhenAnyValue(vm => vm.Beta) .Subscribe(beta => { Preferences.Default.Beta = beta; @@ -382,4 +390,4 @@ public void SetSetParamPath(string path) { this.RaisePropertyChanged(nameof(SetParamPath)); } } -} +} \ No newline at end of file diff --git a/OpenUtau/Views/MainWindow.axaml.cs b/OpenUtau/Views/MainWindow.axaml.cs index 68eaea8f7..ca890b246 100644 --- a/OpenUtau/Views/MainWindow.axaml.cs +++ b/OpenUtau/Views/MainWindow.axaml.cs @@ -1,1353 +1,1355 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -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.Controls; -using OpenUtau.App.ViewModels; -using OpenUtau.Classic; -using OpenUtau.Core; -using OpenUtau.Core.Analysis.Some; -using OpenUtau.Core.DiffSinger; -using OpenUtau.Core.Format; -using OpenUtau.Core.Ustx; -using OpenUtau.Core.Util; -using ReactiveUI; -using Serilog; -using Point = Avalonia.Point; - -namespace OpenUtau.App.Views { - public partial class MainWindow : Window, ICmdSubscriber { - private readonly KeyModifiers cmdKey = - OS.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control; - private readonly MainWindowViewModel viewModel; - - private PianoRollWindow? pianoRollWindow; - private bool openPianoRollWindow; - - private PartEditState? partEditState; - private readonly DispatcherTimer timer; - private readonly DispatcherTimer autosaveTimer; - private bool forceClose; - - private bool shouldOpenPartsContextMenu; - - private readonly ReactiveCommand PartRenameCommand; - private readonly ReactiveCommand PartGotoFileCommand; - private readonly ReactiveCommand PartReplaceAudioCommand; - private readonly ReactiveCommand PartTranscribeCommand; - - public MainWindow() { - Log.Information("Creating main window."); - InitializeComponent(); - Log.Information("Initialized main window component."); - DataContext = viewModel = new MainWindowViewModel(); - - viewModel.InitProject(); - viewModel.AddTempoChangeCmd = ReactiveCommand.Create(tick => AddTempoChange(tick)); - viewModel.DelTempoChangeCmd = ReactiveCommand.Create(tick => DelTempoChange(tick)); - viewModel.AddTimeSigChangeCmd = ReactiveCommand.Create(bar => AddTimeSigChange(bar)); - viewModel.DelTimeSigChangeCmd = ReactiveCommand.Create(bar => DelTimeSigChange(bar)); - - timer = new DispatcherTimer( - TimeSpan.FromMilliseconds(15), - DispatcherPriority.Normal, - (sender, args) => PlaybackManager.Inst.UpdatePlayPos()); - timer.Start(); - - autosaveTimer = new DispatcherTimer( - TimeSpan.FromSeconds(30), - DispatcherPriority.Normal, - (sender, args) => DocManager.Inst.AutoSave()); - autosaveTimer.Start(); - - PartRenameCommand = ReactiveCommand.Create(part => RenamePart(part)); - PartGotoFileCommand = ReactiveCommand.Create(part => GotoFile(part)); - PartReplaceAudioCommand = ReactiveCommand.Create(part => ReplaceAudio(part)); - PartTranscribeCommand = ReactiveCommand.Create(part => Transcribe(part)); - - AddHandler(DragDrop.DropEvent, OnDrop); - - DocManager.Inst.AddSubscriber(this); - - Log.Information("Main window checking Update."); - UpdaterDialog.CheckForUpdate( - dialog => dialog.Show(this), - () => (Application.Current?.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown(), - TaskScheduler.FromCurrentSynchronizationContext()); - Log.Information("Created main window."); - } - - void OnEditTimeSignature(object sender, PointerPressedEventArgs args) { - var project = DocManager.Inst.Project; - var timeSig = project.timeSignatures[0]; - var dialog = new TimeSignatureDialog(timeSig.beatPerBar, timeSig.beatUnit); - dialog.OnOk = (beatPerBar, beatUnit) => { - viewModel.PlaybackViewModel.SetTimeSignature(beatPerBar, beatUnit); - }; - dialog.ShowDialog(this); - // Workaround for https://github.com/AvaloniaUI/Avalonia/issues/3986 - args.Pointer.Capture(null); - } - - void OnEditBpm(object sender, PointerPressedEventArgs args) { - var project = DocManager.Inst.Project; - var dialog = new TypeInDialog(); - dialog.Title = "BPM"; - dialog.SetText(project.tempos[0].bpm.ToString()); - dialog.onFinish = s => { - if (double.TryParse(s, out double bpm)) { - viewModel.PlaybackViewModel.SetBpm(bpm); - } - }; - dialog.ShowDialog(this); - // Workaround for https://github.com/AvaloniaUI/Avalonia/issues/3986 - args.Pointer.Capture(null); - } - - private void AddTempoChange(int tick) { - var project = DocManager.Inst.Project; - var dialog = new TypeInDialog { - Title = "BPM" - }; - dialog.SetText(project.tempos[0].bpm.ToString()); - dialog.onFinish = s => { - if (double.TryParse(s, out double bpm)) { - DocManager.Inst.StartUndoGroup(); - DocManager.Inst.ExecuteCmd(new AddTempoChangeCommand( - project, tick, bpm)); - DocManager.Inst.EndUndoGroup(); - } - }; - dialog.ShowDialog(this); - } - - private void DelTempoChange(int tick) { - var project = DocManager.Inst.Project; - DocManager.Inst.StartUndoGroup(); - DocManager.Inst.ExecuteCmd(new DelTempoChangeCommand(project, tick)); - DocManager.Inst.EndUndoGroup(); - } - - - - void OnMenuRemapTimeaxis(object sender, RoutedEventArgs e) { - var project = DocManager.Inst.Project; - var dialog = new TypeInDialog { - Title = ThemeManager.GetString("menu.project.remaptimeaxis") - }; - dialog.Height = 200; - dialog.SetPrompt(ThemeManager.GetString("dialogs.remaptimeaxis.message")); - dialog.SetText(project.tempos[0].bpm.ToString()); - dialog.onFinish = s => { - try { - if (double.TryParse(s, out double bpm)) { - DocManager.Inst.StartUndoGroup(); - var oldTimeAxis = project.timeAxis.Clone(); - DocManager.Inst.ExecuteCmd(new BpmCommand( - project, bpm)); - foreach (var tempo in project.tempos.Skip(1)) { - DocManager.Inst.ExecuteCmd(new DelTempoChangeCommand( - project, tempo.position)); - } - viewModel.RemapTimeAxis(oldTimeAxis, project.timeAxis.Clone()); - DocManager.Inst.EndUndoGroup(); - } - } catch (Exception e) { - Log.Error(e, "Failed to open project location"); - MessageBox.ShowError(this, new MessageCustomizableException("Failed to open project location", ": project location", e)); - } - }; - dialog.ShowDialog(this); - } - - private void AddTimeSigChange(int bar) { - var project = DocManager.Inst.Project; - var timeSig = project.timeAxis.TimeSignatureAtBar(bar); - var dialog = new TimeSignatureDialog(timeSig.beatPerBar, timeSig.beatUnit); - dialog.OnOk = (beatPerBar, beatUnit) => { - DocManager.Inst.StartUndoGroup(); - DocManager.Inst.ExecuteCmd(new AddTimeSigCommand( - project, bar, dialog.BeatPerBar, dialog.BeatUnit)); - DocManager.Inst.EndUndoGroup(); - }; - dialog.ShowDialog(this); - } - - private void DelTimeSigChange(int bar) { - var project = DocManager.Inst.Project; - DocManager.Inst.StartUndoGroup(); - DocManager.Inst.ExecuteCmd(new DelTimeSigCommand(project, bar)); - DocManager.Inst.EndUndoGroup(); - } - - void OnMenuNew(object sender, RoutedEventArgs args) => NewProject(); - async void NewProject() { - if (!DocManager.Inst.ChangesSaved && !await AskIfSaveAndContinue()) { - return; - } - viewModel.NewProject(); - } - - void OnMenuOpen(object sender, RoutedEventArgs args) => Open(); - async void Open() { - if (!DocManager.Inst.ChangesSaved && !await AskIfSaveAndContinue()) { - return; - } - var files = await FilePicker.OpenFilesAboutProject( - this, "menu.file.open", - FilePicker.ProjectFiles, - FilePicker.USTX, - FilePicker.VSQX, - FilePicker.UST, - FilePicker.MIDI, - FilePicker.UFDATA); - if (files == null || files.Length == 0) { - return; - } - try { - viewModel.OpenProject(files); - } catch (Exception e) { - Log.Error(e, $"Failed to open files {string.Join("\n", files)}"); - _ = await MessageBox.ShowError(this, new MessageCustomizableException($"Failed to open files {string.Join("\n", files)}", $":\n{string.Join("\n", files)}", e)); - } - } - - void OnMainMenuOpened(object sender, RoutedEventArgs args) { - viewModel.RefreshOpenRecent(); - viewModel.RefreshTemplates(); - viewModel.RefreshCacheSize(); - } - - void OnMainMenuClosed(object sender, RoutedEventArgs args) { - Focus(); // Force unfocus menu for key down events. - } - - void OnMainMenuPointerLeave(object sender, PointerEventArgs args) { - Focus(); // Force unfocus menu for key down events. - } - - void OnMenuOpenProjectLocation(object sender, RoutedEventArgs args) { - var project = DocManager.Inst.Project; - if (string.IsNullOrEmpty(project.FilePath) || !project.Saved) { - MessageBox.Show( - this, - ThemeManager.GetString("dialogs.export.savefirst"), - ThemeManager.GetString("errors.caption"), - MessageBox.MessageBoxButtons.Ok); - } - try { - var dir = Path.GetDirectoryName(project.FilePath); - if (dir != null) { - OS.OpenFolder(dir); - } else { - Log.Error($"Failed to get project location from {dir}."); - } - } catch (Exception e) { - Log.Error(e, "Failed to open project location."); - MessageBox.ShowError(this, new MessageCustomizableException("Failed to open project location.", ": project location", e)); - } - } - - async void OnMenuSave(object sender, RoutedEventArgs args) => await Save(); - public async Task Save() { - if (!viewModel.ProjectSaved) { - await SaveAs(); - } else { - viewModel.SaveProject(); - string message = ThemeManager.GetString("progress.saved"); - message = string.Format(message, DateTime.Now); - DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, message)); - } - } - - async void OnMenuSaveAs(object sender, RoutedEventArgs args) => await SaveAs(); - async Task SaveAs() { - var file = await FilePicker.SaveFileAboutProject( - this, "menu.file.saveas", FilePicker.USTX); - if (!string.IsNullOrEmpty(file)) { - viewModel.SaveProject(file); - } - } - - void OnMenuSaveTemplate(object sender, RoutedEventArgs args) { - var project = DocManager.Inst.Project; - var dialog = new TypeInDialog(); - dialog.Title = ThemeManager.GetString("menu.file.savetemplate"); - dialog.SetText("default"); - dialog.onFinish = file => { - if (string.IsNullOrEmpty(file)) { - return; - } - file = Path.GetFileNameWithoutExtension(file); - file = $"{file}.ustx"; - file = Path.Combine(PathManager.Inst.TemplatesPath, file); - Ustx.Save(file, project.CloneAsTemplate()); - }; - dialog.ShowDialog(this); - } - - async void OnMenuImportTracks(object sender, RoutedEventArgs args) { - var files = await FilePicker.OpenFilesAboutProject( - this, "menu.file.importtracks", - FilePicker.ProjectFiles, - FilePicker.USTX, - FilePicker.VSQX, - FilePicker.UST, - FilePicker.MIDI, - FilePicker.UFDATA); - if (files == null || files.Length == 0) { - return; - } - try { - var loadedProjects = Formats.ReadProjects(files); - if (loadedProjects == null || loadedProjects.Length == 0) { - return; - } - bool importTempo = true; - switch (Preferences.Default.ImportTempo) { - case 1: - importTempo = false; - break; - case 2: - if (loadedProjects[0].tempos.Count == 0) { - importTempo = false; - break; - } - var tempoString = String.Join("\n", - loadedProjects[0].tempos - .Select(tempo => $"position: {tempo.position}, tempo: {tempo.bpm}") - ); - //ask the user - var result = await MessageBox.Show( - this, - ThemeManager.GetString("dialogs.importtracks.importtempo") + "\n" + tempoString, - ThemeManager.GetString("dialogs.importtracks.caption"), - MessageBox.MessageBoxButtons.YesNo); - if (result == MessageBox.MessageBoxResult.No) { - importTempo = false; - } - break; - } - viewModel.ImportTracks(loadedProjects, importTempo); - } catch (Exception e) { - Log.Error(e, $"Failed to import files"); - _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import files", "", e)); - } - ValidateTracksVoiceColor(); - } - - async void OnMenuImportAudio(object sender, RoutedEventArgs args) { - var file = await FilePicker.OpenFileAboutProject( - this, "menu.file.importaudio", FilePicker.AudioFiles); - if (file == null) { - return; - } - try { - viewModel.ImportAudio(file); - } catch (Exception e) { - Log.Error(e, "Failed to import audio"); - _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import audio", "", e)); - } - } - - async void OnMenuImportMidi(object sender, RoutedEventArgs args) { - var file = await FilePicker.OpenFileAboutProject( - this, "menu.file.importmidi", FilePicker.MIDI); - if (file == null) { - return; - } - try { - viewModel.ImportMidi(file); - } catch (Exception e) { - Log.Error(e, "Failed to import midi"); - _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import midi", "", e)); - } - } - - async void OnMenuExportMixdown(object sender, RoutedEventArgs args) { - var project = DocManager.Inst.Project; - var file = await FilePicker.SaveFileAboutProject( - this, "menu.file.exportmixdown", FilePicker.WAV); - if (!string.IsNullOrEmpty(file)) { - await PlaybackManager.Inst.RenderMixdown(project, file); - } - } - - async void OnMenuExportWav(object sender, RoutedEventArgs args) { - var project = DocManager.Inst.Project; - if (await WarnToSave(project)) { - var name = Path.GetFileNameWithoutExtension(project.FilePath); - var path = Path.GetDirectoryName(project.FilePath); - path = Path.Combine(path!, "Export", $"{name}.wav"); - await PlaybackManager.Inst.RenderToFiles(project, path); - } - } - - async void OnMenuExportWavTo(object sender, RoutedEventArgs args) { - var project = DocManager.Inst.Project; - var file = await FilePicker.SaveFileAboutProject( - this, "menu.file.exportwavto", FilePicker.WAV); - if (!string.IsNullOrEmpty(file)) { - await PlaybackManager.Inst.RenderToFiles(project, file); - } - } - - async void OnMenuExportDsTo(object sender, RoutedEventArgs e) { - var project = DocManager.Inst.Project; - var file = await FilePicker.SaveFileAboutProject( - this, "menu.file.exportds", FilePicker.DS); - if (!string.IsNullOrEmpty(file)) { - for (var i = 0; i < project.parts.Count; i++) { - var part = project.parts[i]; - if (part is UVoicePart voicePart) { - var savePath = PathManager.Inst.GetPartSavePath(file, voicePart.DisplayName, i)[..^4] + ".ds"; - DiffSingerScript.SavePart(project, voicePart, savePath); - DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); - } - } - } - } - - async void OnMenuExportDsV2To(object sender, RoutedEventArgs e) { - var project = DocManager.Inst.Project; - var file = await FilePicker.SaveFileAboutProject( - this, "menu.file.exportds.v2", FilePicker.DS); - if (!string.IsNullOrEmpty(file)) { - for (var i = 0; i < project.parts.Count; i++) { - var part = project.parts[i]; - if (part is UVoicePart voicePart) { - var savePath = PathManager.Inst.GetPartSavePath(file, voicePart.DisplayName, i)[..^4] + ".ds"; - DiffSingerScript.SavePart(project, voicePart, savePath, true); - DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); - } - } - } - } - - async void OnMenuExportDsV2WithoutPitchTo(object sender, RoutedEventArgs e) { - var project = DocManager.Inst.Project; - var file = await FilePicker.SaveFileAboutProject( - this, "menu.file.exportds.v2withoutpitch", FilePicker.DS); - if (!string.IsNullOrEmpty(file)) { - for (var i = 0; i < project.parts.Count; i++) { - var part = project.parts[i]; - if (part is UVoicePart voicePart) { - var savePath = PathManager.Inst.GetPartSavePath(file, voicePart.DisplayName, i)[..^4] + ".ds"; - DiffSingerScript.SavePart(project, voicePart, savePath, true, false); - DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); - } - } - } - } - - async void OnMenuExportUst(object sender, RoutedEventArgs e) { - var project = DocManager.Inst.Project; - if (await WarnToSave(project)) { - var name = Path.GetFileNameWithoutExtension(project.FilePath); - var path = Path.GetDirectoryName(project.FilePath); - path = Path.Combine(path!, "Export", $"{name}.ust"); - for (var i = 0; i < project.parts.Count; i++) { - var part = project.parts[i]; - if (part is UVoicePart voicePart) { - var savePath = PathManager.Inst.GetPartSavePath(path, voicePart.DisplayName, i); - Ust.SavePart(project, voicePart, savePath); - DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); - } - } - } - } - - async void OnMenuExportUstTo(object sender, RoutedEventArgs e) { - var project = DocManager.Inst.Project; - var file = await FilePicker.SaveFileAboutProject( - this, "menu.file.exportustto", FilePicker.UST); - if (!string.IsNullOrEmpty(file)) { - for (var i = 0; i < project.parts.Count; i++) { - var part = project.parts[i]; - if (part is UVoicePart voicePart) { - var savePath = PathManager.Inst.GetPartSavePath(file, voicePart.DisplayName, i); - Ust.SavePart(project, voicePart, savePath); - DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); - } - } - } - } - - async void OnMenuExportMidi(object sender, RoutedEventArgs e) { - var project = DocManager.Inst.Project; - var file = await FilePicker.SaveFileAboutProject( - this, "menu.file.exportmidi", FilePicker.MIDI); - if (!string.IsNullOrEmpty(file)) { - MidiWriter.Save(file, project); - } - } - - private async Task WarnToSave(UProject project) { - if (string.IsNullOrEmpty(project.FilePath)) { - await MessageBox.Show( - this, - ThemeManager.GetString("dialogs.export.savefirst"), - ThemeManager.GetString("dialogs.export.caption"), - MessageBox.MessageBoxButtons.Ok); - return false; - } - return true; - } - - void OnMenuUndo(object sender, RoutedEventArgs args) => viewModel.Undo(); - void OnMenuRedo(object sender, RoutedEventArgs args) => viewModel.Redo(); - - void OnMenuExpressionss(object sender, RoutedEventArgs args) { - var dialog = new ExpressionsDialog() { - DataContext = new ExpressionsViewModel(), - }; - dialog.ShowDialog(this); - if (dialog.Position.Y < 0) { - dialog.Position = dialog.Position.WithY(0); - } - } - - void OnMenuSingers(object sender, RoutedEventArgs args) { - OpenSingersWindow(); - } - - /// - /// Check if a track has a singer and if it exists. - /// If the user haven't selected a singer for the track, or the singer specified in ustx project doesn't exist, return null. - /// Otherwise, return the singer. - /// - public USinger? TrackSingerIfFound(UTrack track) { - if (track.Singer?.Found ?? false) { - return track.Singer; - } - return null; - } - - public void OpenSingersWindow() { - var lifetime = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; - if (lifetime == null) { - return; - } - - MessageBox.ShowLoading(this); - var dialog = lifetime.Windows.FirstOrDefault(w => w is SingersDialog); - try { - if (dialog == null) { - USinger? singer = null; - if (viewModel.TracksViewModel.SelectedParts.Count > 0) { - singer = TrackSingerIfFound(viewModel.TracksViewModel.Tracks[viewModel.TracksViewModel.SelectedParts.First().trackNo]); - } - if (singer == null && viewModel.TracksViewModel.Tracks.Count > 0) { - singer = TrackSingerIfFound(viewModel.TracksViewModel.Tracks.First()); - } - var vm = new SingersViewModel(); - if (singer != null) { - vm.Singer = singer; - } - dialog = new SingersDialog() { DataContext = vm }; - dialog.Show(); - } - if (dialog.Position.Y < 0) { - dialog.Position = dialog.Position.WithY(0); - } - } catch (Exception e) { - DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); - } finally { - MessageBox.CloseLoading(); - } - if (dialog != null) { - dialog.Activate(); - } - } - - async void OnMenuInstallSinger(object sender, RoutedEventArgs args) { - var file = await FilePicker.OpenFileAboutSinger( - this, "menu.tools.singer.install", FilePicker.ArchiveFiles); - if (file == null) { - return; - } - try { - if (file.EndsWith(Core.Vogen.VogenSingerInstaller.FileExt)) { - Core.Vogen.VogenSingerInstaller.Install(file); - return; - } - if (file.EndsWith(DependencyInstaller.FileExt)) { - DependencyInstaller.Install(file); - return; - } - - var setup = new SingerSetupDialog() { - DataContext = new SingerSetupViewModel() { - ArchiveFilePath = file, - }, - }; - _ = setup.ShowDialog(this); - if (setup.Position.Y < 0) { - setup.Position = setup.Position.WithY(0); - } - } catch (Exception e) { - Log.Error(e, $"Failed to install singer {file}"); - MessageCustomizableException mce; - if (e is MessageCustomizableException) { - mce = (MessageCustomizableException)e; - } else { - mce = new MessageCustomizableException($"Failed to install singer {file}", $": {file}", e); - } - _ = await MessageBox.ShowError(this, mce); - } - } - - async void OnMenuInstallDependency(object sender, RoutedEventArgs args) { - var file = await FilePicker.OpenFile( - this, "menu.tools.dependency.install", FilePicker.OUDEP); - if (file == null) { - return; - } - if (file.EndsWith(DependencyInstaller.FileExt)) { - DependencyInstaller.Install(file); - return; - } - } - - void OnMenuPreferences(object sender, RoutedEventArgs args) { - PreferencesViewModel dataContext; - try { - dataContext = new PreferencesViewModel(); - } catch (Exception e) { - Log.Error(e, "Failed to load prefs. Initialize it."); - MessageBox.ShowError(this, new MessageCustomizableException("Failed to load prefs. Initialize it.", "", e)); - Preferences.Reset(); - dataContext = new PreferencesViewModel(); - } - var dialog = new PreferencesDialog() { - DataContext = dataContext - }; - dialog.ShowDialog(this); - if (dialog.Position.Y < 0) { - dialog.Position = dialog.Position.WithY(0); - } - } - - void OnMenuClearCache(object sender, RoutedEventArgs args) { - Task.Run(() => { - DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, "Clearing cache...")); - PathManager.Inst.ClearCache(); - DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, "Cache cleared.")); - }); - } - - void OnMenuDebugWindow(object sender, RoutedEventArgs args) { - var desktop = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; - if (desktop == null) { - return; - } - var window = desktop.Windows.FirstOrDefault(w => w is DebugWindow); - if (window == null) { - window = new DebugWindow(); - } - window.Show(); - } - - void OnMenuPhoneticAssistant(object sender, RoutedEventArgs args) { - var desktop = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; - if (desktop == null) { - return; - } - var window = desktop.Windows.FirstOrDefault(w => w is PhoneticAssistant); - if (window == null) { - window = new PhoneticAssistant(); - } - window.Show(); - } - - void OnMenuCheckUpdate(object sender, RoutedEventArgs args) { - var dialog = new UpdaterDialog(); - dialog.ViewModel.CloseApplication = - () => (Application.Current?.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown(); - dialog.ShowDialog(this); - } - - void OnMenuLogsLocation(object sender, RoutedEventArgs args) { - try { - OS.OpenFolder(PathManager.Inst.LogsPath); - } catch (Exception e) { - DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); - } - } - - void OnMenuReportIssue(object sender, RoutedEventArgs args) { - try { - OS.OpenWeb("https://github.com/stakira/OpenUtau/issues"); - } catch (Exception e) { - DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); - } - } - - void OnMenuWiki(object sender, RoutedEventArgs args) { - try { - OS.OpenWeb("https://github.com/stakira/OpenUtau/wiki/Getting-Started"); - } catch (Exception e) { - DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); - } - } - - void OnMenuLayoutVSplit11(object sender, RoutedEventArgs args) => LayoutSplit(null, 1.0 / 2); - void OnMenuLayoutVSplit12(object sender, RoutedEventArgs args) => LayoutSplit(null, 1.0 / 3); - void OnMenuLayoutVSplit13(object sender, RoutedEventArgs args) => LayoutSplit(null, 1.0 / 4); - void OnMenuLayoutHSplit11(object sender, RoutedEventArgs args) => LayoutSplit(1.0 / 2, null); - void OnMenuLayoutHSplit12(object sender, RoutedEventArgs args) => LayoutSplit(1.0 / 3, null); - void OnMenuLayoutHSplit13(object sender, RoutedEventArgs args) => LayoutSplit(1.0 / 4, null); - - private void LayoutSplit(double? x, double? y) { - if (Screens.Primary == null) { - return; - } - var wa = Screens.Primary.WorkingArea; - WindowState = WindowState.Normal; - double titleBarHeight = 20; - if (FrameSize != null) { - double borderThickness = (FrameSize!.Value.Width - ClientSize.Width) / 2; - titleBarHeight = FrameSize!.Value.Height - ClientSize.Height - borderThickness; - } - Position = new PixelPoint(0, 0); - Width = x != null ? wa.Size.Width * x.Value : wa.Size.Width; - Height = (y != null ? wa.Size.Height * y.Value : wa.Size.Height) - titleBarHeight; - if (pianoRollWindow != null) { - pianoRollWindow.Position = new PixelPoint(x != null ? (int)Width : 0, y != null ? (int)(Height + (OS.IsMacOS() ? 25 : titleBarHeight)) : 0); - pianoRollWindow.Width = x != null ? wa.Size.Width - Width : wa.Size.Width; - pianoRollWindow.Height = (y != null ? wa.Size.Height - (Height + titleBarHeight) : wa.Size.Height) - titleBarHeight; - } - } - - void OnKeyDown(object sender, 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; - 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; - } - } - } - - void OnPointerPressed(object? sender, PointerPressedEventArgs args) { - if (!args.Handled && args.ClickCount == 1) { - FocusManager?.ClearFocus(); - } - } - - async void OnDrop(object? sender, DragEventArgs args) { - var storageItem = args.Data?.GetFiles()?.FirstOrDefault(); - if (storageItem == null) { - return; - } - string file = storageItem.Path.LocalPath; - var ext = Path.GetExtension(file); - if (ext == ".ustx" || ext == ".ust" || ext == ".vsqx" || ext == ".ufdata") { - if (!DocManager.Inst.ChangesSaved && !await AskIfSaveAndContinue()) { - return; - } - try { - viewModel.OpenProject(new string[] { file }); - } catch (Exception e) { - Log.Error(e, $"Failed to open file {file}"); - _ = await MessageBox.ShowError(this, new MessageCustomizableException($"Failed to open file {file}", $": {file}", e)); - } - } else if (ext == ".mid" || ext == ".midi") { - try { - viewModel.ImportMidi(file); - } catch (Exception e) { - Log.Error(e, "Failed to import midi"); - _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import midi", "", e)); - } - } else if (ext == ".zip" || ext == ".rar" || ext == ".uar") { - try { - var setup = new SingerSetupDialog() { - DataContext = new SingerSetupViewModel() { - ArchiveFilePath = file, - }, - }; - _ = setup.ShowDialog(this); - if (setup.Position.Y < 0) { - setup.Position = setup.Position.WithY(0); - } - } catch (Exception e) { - Log.Error(e, $"Failed to install singer {file}"); - MessageCustomizableException mce; - if (e is MessageCustomizableException) { - mce = (MessageCustomizableException)e; - } else { - mce = new MessageCustomizableException($"Failed to install singer {file}", $": {file}", e); - } - _ = await MessageBox.ShowError(this, mce); - } - } else if (ext == Core.Vogen.VogenSingerInstaller.FileExt) { - Core.Vogen.VogenSingerInstaller.Install(file); - } else if (ext == ".dll") { - var result = await MessageBox.Show( - this, - ThemeManager.GetString("dialogs.installdll.message") + file, - ThemeManager.GetString("dialogs.installdll.caption"), - MessageBox.MessageBoxButtons.OkCancel); - if (result == MessageBox.MessageBoxResult.Ok) { - Core.Api.PhonemizerInstaller.Install(file); - } - } else if (ext == ".exe") { - var setup = new ExeSetupDialog() { - DataContext = new ExeSetupViewModel(file) - }; - _ = setup.ShowDialog(this); - if (setup.Position.Y < 0) { - setup.Position = setup.Position.WithY(0); - } - } else if (ext == DependencyInstaller.FileExt) { - var result = await MessageBox.Show( - this, - ThemeManager.GetString("dialogs.installdependency.message") + file, - ThemeManager.GetString("dialogs.installdependency.caption"), - MessageBox.MessageBoxButtons.OkCancel); - if (result == MessageBox.MessageBoxResult.Ok) { - DependencyInstaller.Install(file); - } - } else if (ext == ".mp3" || ext == ".wav" || ext == ".ogg" || ext == ".flac") { - try { - viewModel.ImportAudio(file); - } catch (Exception e) { - Log.Error(e, "Failed to import audio"); - _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import audio", "", e)); - } - } else { - _ = await MessageBox.Show( - this, - ThemeManager.GetString("dialogs.unsupportedfile.message") + ext, - ThemeManager.GetString("dialogs.unsupportedfile.caption"), - MessageBox.MessageBoxButtons.Ok); - } - } - - void OnPlayOrPause(object sender, RoutedEventArgs args) { - PlayOrPause(); - } - - void PlayOrPause() { - viewModel.PlaybackViewModel.PlayOrPause(); - } - - 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)); - } - - 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)); - } - - 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.TracksViewModel.OnXZoomed(position, 0.1 * args.Delta.Y); - } - - public void ViewScalerPointerWheelChanged(object sender, PointerWheelEventArgs args) { - viewModel.TracksViewModel.OnYZoomed(new Point(0, 0.5), 0.1 * args.Delta.Y); - } - - 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.TracksViewModel.PointToLineTick(point.Position, out int left, out int right); - viewModel.PlaybackViewModel.MovePlayPos(left); - } else if (point.Properties.IsRightButtonPressed) { - int tick = viewModel.TracksViewModel.PointToTick(point.Position); - viewModel.RefreshTimelineContextMenu(tick); - } - } - - public void TimelinePointerMoved(object sender, PointerEventArgs args) { - var control = (Control)sender; - var point = args.GetCurrentPoint(control); - if (point.Properties.IsLeftButtonPressed) { - viewModel.TracksViewModel.PointToLineTick(point.Position, out int left, out int right); - viewModel.PlaybackViewModel.MovePlayPos(left); - } - } - - public void TimelinePointerReleased(object sender, PointerReleasedEventArgs args) { - args.Pointer.Capture(null); - } - - public void PartsCanvasPointerPressed(object sender, PointerPressedEventArgs args) { - var control = (Control)sender; - var point = args.GetCurrentPoint(control); - var hitControl = control.InputHitTest(point.Position); - if (partEditState != null) { - return; - } - if (point.Properties.IsLeftButtonPressed) { - if (args.KeyModifiers == cmdKey) { - partEditState = new PartSelectionEditState(control, viewModel, SelectionBox); - Cursor = ViewConstants.cursorCross; - } else if (hitControl == control) { - viewModel.TracksViewModel.DeselectParts(); - var part = viewModel.TracksViewModel.MaybeAddPart(point.Position); - if (part != null) { - // Start moving right away - partEditState = new PartMoveEditState(control, viewModel, part); - Cursor = ViewConstants.cursorSizeAll; - } - } else if (hitControl is PartControl partControl) { - bool isVoice = partControl.part is UVoicePart; - bool isWave = partControl.part is UWavePart; - bool trim = point.Position.X > partControl.Bounds.Right - ViewConstants.ResizeMargin; - bool skip = point.Position.X < partControl.Bounds.Left + ViewConstants.ResizeMargin; - if (isVoice && trim) { - partEditState = new PartResizeEditState(control, viewModel, partControl.part); - Cursor = ViewConstants.cursorSizeWE; - } else if (isWave && skip) { - // TODO - } else if (isWave && trim) { - // TODO - } else { - partEditState = new PartMoveEditState(control, viewModel, partControl.part); - Cursor = ViewConstants.cursorSizeAll; - } - } - } else if (point.Properties.IsRightButtonPressed) { - if (hitControl is PartControl partControl) { - if (!viewModel.TracksViewModel.SelectedParts.Contains(partControl.part)) { - viewModel.TracksViewModel.DeselectParts(); - viewModel.TracksViewModel.SelectPart(partControl.part); - } - if (PartsContextMenu != null && viewModel.TracksViewModel.SelectedParts.Count > 0) { - PartsContextMenu.DataContext = new PartsContextMenuArgs { - Part = partControl.part, - PartDeleteCommand = viewModel.PartDeleteCommand, - PartGotoFileCommand = PartGotoFileCommand, - PartReplaceAudioCommand = PartReplaceAudioCommand, - PartRenameCommand = PartRenameCommand, - PartTranscribeCommand = PartTranscribeCommand, - }; - shouldOpenPartsContextMenu = true; - } - } else { - viewModel.TracksViewModel.DeselectParts(); - } - } else if (point.Properties.IsMiddleButtonPressed) { - partEditState = new PartPanningState(control, viewModel); - Cursor = ViewConstants.cursorHand; - } - if (partEditState != null) { - partEditState.Begin(point.Pointer, point.Position); - partEditState.Update(point.Pointer, point.Position); - } - } - - public void PartsCanvasPointerMoved(object sender, PointerEventArgs args) { - var control = (Control)sender; - var point = args.GetCurrentPoint(control); - if (partEditState != null) { - partEditState.Update(point.Pointer, point.Position); - return; - } - var hitControl = control.InputHitTest(point.Position); - if (hitControl is PartControl partControl) { - bool isVoice = partControl.part is UVoicePart; - bool isWave = partControl.part is UWavePart; - bool trim = point.Position.X > partControl.Bounds.Right - ViewConstants.ResizeMargin; - bool skip = point.Position.X < partControl.Bounds.Left + ViewConstants.ResizeMargin; - if (isVoice && trim) { - Cursor = ViewConstants.cursorSizeWE; - } else if (isWave && (skip || trim)) { - Cursor = null; // TODO - } else { - Cursor = null; - } - } else { - Cursor = null; - } - } - - public void PartsCanvasPointerReleased(object sender, PointerReleasedEventArgs args) { - if (partEditState != null) { - if (partEditState.MouseButton != args.InitialPressMouseButton) { - return; - } - var control = (Control)sender; - var point = args.GetCurrentPoint(control); - partEditState.Update(point.Pointer, point.Position); - partEditState.End(point.Pointer, point.Position); - partEditState = null; - Cursor = null; - } - if (openPianoRollWindow) { - pianoRollWindow?.Show(); - pianoRollWindow?.Activate(); - openPianoRollWindow = false; - } - } - - public void PartsCanvasDoubleTapped(object sender, TappedEventArgs args) { - if (!(sender is Canvas canvas)) { - return; - } - var control = canvas.InputHitTest(args.GetPosition(canvas)); - if (control is PartControl partControl && partControl.part is UVoicePart) { - if (pianoRollWindow == null) { - MessageBox.ShowLoading(this); - pianoRollWindow = new PianoRollWindow() { - MainWindow = this, - }; - pianoRollWindow.ViewModel.PlaybackViewModel = viewModel.PlaybackViewModel; - MessageBox.CloseLoading(); - } - // Workaround for new window losing focus. - openPianoRollWindow = true; - int tick = viewModel.TracksViewModel.PointToTick(args.GetPosition(canvas)); - DocManager.Inst.ExecuteCmd(new LoadPartNotification(partControl.part, DocManager.Inst.Project, tick)); - pianoRollWindow.AttachExpressions(); - } - } - - public void PartsCanvasPointerWheelChanged(object sender, PointerWheelEventArgs args) { - 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) { - ViewScalerPointerWheelChanged(VScaler, args); - } else if (args.KeyModifiers == cmdKey) { - TimelinePointerWheelChanged(TimelineCanvas, args); - } - if (partEditState != null) { - var point = args.GetCurrentPoint(partEditState.control); - partEditState.Update(point.Pointer, point.Position); - } - } - - public void PartsContextMenuOpening(object sender, CancelEventArgs args) { - if (shouldOpenPartsContextMenu) { - shouldOpenPartsContextMenu = false; - } else { - args.Cancel = true; - } - } - - public void PartsContextMenuClosing(object sender, CancelEventArgs args) { - if (PartsContextMenu != null) { - PartsContextMenu.DataContext = null; - } - } - - void RenamePart(UPart part) { - var dialog = new TypeInDialog(); - dialog.Title = ThemeManager.GetString("context.part.rename"); - dialog.SetText(part.name); - dialog.onFinish = name => { - if (!string.IsNullOrWhiteSpace(name) && name != part.name) { - if (!string.IsNullOrWhiteSpace(name) && name != part.name) { - DocManager.Inst.StartUndoGroup(); - DocManager.Inst.ExecuteCmd(new RenamePartCommand(DocManager.Inst.Project, part, name)); - DocManager.Inst.EndUndoGroup(); - } - } - }; - dialog.ShowDialog(this); - } - - void GotoFile(UPart part) { - //View the location of the audio file in explorer if the part is a wave part - if (part is UWavePart wavePart) { - try { - OS.GotoFile(wavePart.FilePath); - } catch (Exception e) { - DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); - } - } - } - - async void ReplaceAudio(UPart part) { - var file = await FilePicker.OpenFileAboutProject( - this, "context.part.replaceaudio", FilePicker.AudioFiles); - if (file == null) { - return; - } - UWavePart newPart = new UWavePart() { - FilePath = file, - trackNo = part.trackNo, - position = part.position - }; - newPart.Load(DocManager.Inst.Project); - DocManager.Inst.StartUndoGroup(); - DocManager.Inst.ExecuteCmd(new ReplacePartCommand(DocManager.Inst.Project, part, newPart)); - DocManager.Inst.EndUndoGroup(); - } - - void Transcribe(UPart part) { - //Convert audio to notes - if (part is UWavePart wavePart) { - try { - string text = ThemeManager.GetString("context.part.transcribing"); - var msgbox = MessageBox.ShowModal(this, $"{text} {part.name}", text); - //Duration of the wave file in seconds - int wavDurS = (int)(wavePart.fileDurationMs / 1000.0); - var scheduler = TaskScheduler.FromCurrentSynchronizationContext(); - var transcribeTask = Task.Run(() => { - using (var some = new Some()) { - return some.Transcribe(DocManager.Inst.Project, wavePart, wavPosS => { - //msgbox?.SetText($"{text} {part.name}\n{wavPosS}/{wavDurS}"); - msgbox.SetText(string.Format("{0} {1}\n{2}s / {3}s", text, part.name, wavPosS, wavDurS)); - }); - } - }); - transcribeTask.ContinueWith(task => { - msgbox?.Close(); - if (task.IsFaulted) { - Log.Error(task.Exception, $"Failed to transcribe part {part.name}"); - MessageBox.ShowError(this, task.Exception); - return; - } - var voicePart = task.Result; - //Add voicePart into project - if (voicePart != null) { - var project = DocManager.Inst.Project; - var track = new UTrack(project); - track.TrackNo = project.tracks.Count; - voicePart.trackNo = track.TrackNo; - DocManager.Inst.StartUndoGroup(); - DocManager.Inst.ExecuteCmd(new AddTrackCommand(project, track)); - DocManager.Inst.ExecuteCmd(new AddPartCommand(project, voicePart)); - DocManager.Inst.EndUndoGroup(); - } - }, scheduler); - } catch (Exception e) { - Log.Error(e, $"Failed to transcribe part {part.name}"); - MessageBox.ShowError(this, e); - } - } - } - - async void ValidateTracksVoiceColor() { - DocManager.Inst.StartUndoGroup(); - foreach (var track in DocManager.Inst.Project.tracks) { - if (track.ValidateVoiceColor(out var oldColors, out var newColors)) { - await VoiceColorRemappingAsync(track, oldColors, newColors); - } - } - DocManager.Inst.EndUndoGroup(); - } - async Task VoiceColorRemappingAsync(UTrack track, string[] oldColors, string[] newColors) { - var parts = DocManager.Inst.Project.parts - .Where(part => part.trackNo == track.TrackNo && part is UVoicePart) - .Cast() - .Where(vpart => vpart.notes.Count > 0); - if (parts.Any()) { - var dialog = new VoiceColorMappingDialog(); - VoiceColorMappingViewModel vm = new VoiceColorMappingViewModel(oldColors, newColors, track.TrackName); - dialog.DataContext = vm; - await dialog.ShowDialog(this); - - if (dialog.Apply) { - SetVoiceColorRemapping(track, parts, vm); - } - } - } - void VoiceColorRemapping(UTrack track, string[] oldColors, string[] newColors) { - var parts = DocManager.Inst.Project.parts - .Where(part => part.trackNo == track.TrackNo && part is UVoicePart) - .Cast() - .Where(vpart => vpart.notes.Count > 0); - if (parts.Any()) { - var dialog = new VoiceColorMappingDialog(); - VoiceColorMappingViewModel vm = new VoiceColorMappingViewModel(oldColors, newColors, track.TrackName); - dialog.DataContext = vm; - dialog.onFinish = () => { - DocManager.Inst.StartUndoGroup(); - SetVoiceColorRemapping(track, parts, vm); - DocManager.Inst.EndUndoGroup(); - }; - dialog.ShowDialog(this); - } - } - void SetVoiceColorRemapping(UTrack track, IEnumerable parts, VoiceColorMappingViewModel vm) { - foreach (var part in parts) { - foreach (var phoneme in part.phonemes) { - var tuple = phoneme.GetExpression(DocManager.Inst.Project, track, Ustx.CLR); - if (vm.ColorMappings.Any(m => m.OldIndex == tuple.Item1)) { - var mapping = vm.ColorMappings.First(m => m.OldIndex == tuple.Item1); - if (mapping.OldIndex != mapping.SelectedIndex) { - if (mapping.SelectedIndex == 0) { - DocManager.Inst.ExecuteCmd(new SetPhonemeExpressionCommand(DocManager.Inst.Project, track, part, phoneme, Ustx.CLR, null)); - } else { - DocManager.Inst.ExecuteCmd(new SetPhonemeExpressionCommand(DocManager.Inst.Project, track, part, phoneme, Ustx.CLR, mapping.SelectedIndex)); - } - } - } else { - DocManager.Inst.ExecuteCmd(new SetPhonemeExpressionCommand(DocManager.Inst.Project, track, part, phoneme, Ustx.CLR, null)); - } - } - } - } - - public async void WindowClosing(object? sender, WindowClosingEventArgs e) { - if (forceClose || DocManager.Inst.ChangesSaved) { - if (Preferences.Default.ClearCacheOnQuit) { - Log.Information("Clearing cache..."); - PathManager.Inst.ClearCache(); - Log.Information("Cache cleared."); - } - return; - } - e.Cancel = true; - if (!await AskIfSaveAndContinue()) { - return; - } - pianoRollWindow?.Close(); - forceClose = true; - Close(); - } - - private async Task AskIfSaveAndContinue() { - var result = await MessageBox.Show( - this, - ThemeManager.GetString("dialogs.exitsave.message"), - ThemeManager.GetString("dialogs.exitsave.caption"), - MessageBox.MessageBoxButtons.YesNoCancel); - switch (result) { - case MessageBox.MessageBoxResult.Yes: - await Save(); - goto case MessageBox.MessageBoxResult.No; - case MessageBox.MessageBoxResult.No: - return true; // Continue. - default: - return false; // Cancel. - } - } - - public void OnNext(UCommand cmd, bool isUndo) { - if (cmd is ErrorMessageNotification notif) { - switch (notif.e) { - case Core.Render.NoResamplerException: - case Core.Render.NoWavtoolException: - MessageBox.Show( - this, - ThemeManager.GetString("dialogs.noresampler.message"), - ThemeManager.GetString("dialogs.noresampler.caption"), - MessageBox.MessageBoxButtons.Ok); - break; - default: - MessageBox.ShowError(this, notif.e, notif.message, true); - break; - } - } else if (cmd is LoadingNotification loadingNotif && loadingNotif.window == typeof(MainWindow)) { - if (loadingNotif.startLoading) { - MessageBox.ShowLoading(this); - } else { - MessageBox.CloseLoading(); - } - } else if (cmd is VoiceColorRemappingNotification voicecolorNotif) { - if (voicecolorNotif.TrackNo < 0 || DocManager.Inst.Project.tracks.Count <= voicecolorNotif.TrackNo) { - ValidateTracksVoiceColor(); - } else { - UTrack track = DocManager.Inst.Project.tracks[voicecolorNotif.TrackNo]; - if (!voicecolorNotif.Validate) { - VoiceColorRemapping(track, track.VoiceColorNames, track.VoiceColorExp.options); - } else if (track.ValidateVoiceColor(out var oldColors, out var newColors)) { - VoiceColorRemapping(track, oldColors, newColors); - } - } - } - } - } -} +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +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.Controls; +using OpenUtau.App.ViewModels; +using OpenUtau.Classic; +using OpenUtau.Core; +using OpenUtau.Core.Analysis.Some; +using OpenUtau.Core.DiffSinger; +using OpenUtau.Core.Format; +using OpenUtau.Core.Ustx; +using OpenUtau.Core.Util; +using ReactiveUI; +using Serilog; +using Point = Avalonia.Point; + +namespace OpenUtau.App.Views { + public partial class MainWindow : Window, ICmdSubscriber { + private readonly KeyModifiers cmdKey = + OS.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control; + private readonly MainWindowViewModel viewModel; + + private PianoRollWindow? pianoRollWindow; + private bool openPianoRollWindow; + + private PartEditState? partEditState; + private readonly DispatcherTimer timer; + private readonly DispatcherTimer autosaveTimer; + private bool forceClose; + + private bool shouldOpenPartsContextMenu; + + private readonly ReactiveCommand PartRenameCommand; + private readonly ReactiveCommand PartGotoFileCommand; + private readonly ReactiveCommand PartReplaceAudioCommand; + private readonly ReactiveCommand PartTranscribeCommand; + + public MainWindow() { + Log.Information("Creating main window."); + InitializeComponent(); + Log.Information("Initialized main window component."); + DataContext = viewModel = new MainWindowViewModel(); + + viewModel.InitProject(); + viewModel.AddTempoChangeCmd = ReactiveCommand.Create(tick => AddTempoChange(tick)); + viewModel.DelTempoChangeCmd = ReactiveCommand.Create(tick => DelTempoChange(tick)); + viewModel.AddTimeSigChangeCmd = ReactiveCommand.Create(bar => AddTimeSigChange(bar)); + viewModel.DelTimeSigChangeCmd = ReactiveCommand.Create(bar => DelTimeSigChange(bar)); + + timer = new DispatcherTimer( + TimeSpan.FromMilliseconds(15), + DispatcherPriority.Normal, + (sender, args) => PlaybackManager.Inst.UpdatePlayPos()); + timer.Start(); + + autosaveTimer = new DispatcherTimer( + TimeSpan.FromSeconds(30), + DispatcherPriority.Normal, + (sender, args) => DocManager.Inst.AutoSave()); + autosaveTimer.Start(); + + PartRenameCommand = ReactiveCommand.Create(part => RenamePart(part)); + PartGotoFileCommand = ReactiveCommand.Create(part => GotoFile(part)); + PartReplaceAudioCommand = ReactiveCommand.Create(part => ReplaceAudio(part)); + PartTranscribeCommand = ReactiveCommand.Create(part => Transcribe(part)); + + AddHandler(DragDrop.DropEvent, OnDrop); + + DocManager.Inst.AddSubscriber(this); + + if (Preferences.Default.CheckForUpdateOnStart) { + Log.Information("Main window checking Update."); + UpdaterDialog.CheckForUpdate( + dialog => dialog.Show(this), + () => (Application.Current?.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown(), + TaskScheduler.FromCurrentSynchronizationContext()); + } + Log.Information("Created main window."); + } + + void OnEditTimeSignature(object sender, PointerPressedEventArgs args) { + var project = DocManager.Inst.Project; + var timeSig = project.timeSignatures[0]; + var dialog = new TimeSignatureDialog(timeSig.beatPerBar, timeSig.beatUnit); + dialog.OnOk = (beatPerBar, beatUnit) => { + viewModel.PlaybackViewModel.SetTimeSignature(beatPerBar, beatUnit); + }; + dialog.ShowDialog(this); + // Workaround for https://github.com/AvaloniaUI/Avalonia/issues/3986 + args.Pointer.Capture(null); + } + + void OnEditBpm(object sender, PointerPressedEventArgs args) { + var project = DocManager.Inst.Project; + var dialog = new TypeInDialog(); + dialog.Title = "BPM"; + dialog.SetText(project.tempos[0].bpm.ToString()); + dialog.onFinish = s => { + if (double.TryParse(s, out double bpm)) { + viewModel.PlaybackViewModel.SetBpm(bpm); + } + }; + dialog.ShowDialog(this); + // Workaround for https://github.com/AvaloniaUI/Avalonia/issues/3986 + args.Pointer.Capture(null); + } + + private void AddTempoChange(int tick) { + var project = DocManager.Inst.Project; + var dialog = new TypeInDialog { + Title = "BPM" + }; + dialog.SetText(project.tempos[0].bpm.ToString()); + dialog.onFinish = s => { + if (double.TryParse(s, out double bpm)) { + DocManager.Inst.StartUndoGroup(); + DocManager.Inst.ExecuteCmd(new AddTempoChangeCommand( + project, tick, bpm)); + DocManager.Inst.EndUndoGroup(); + } + }; + dialog.ShowDialog(this); + } + + private void DelTempoChange(int tick) { + var project = DocManager.Inst.Project; + DocManager.Inst.StartUndoGroup(); + DocManager.Inst.ExecuteCmd(new DelTempoChangeCommand(project, tick)); + DocManager.Inst.EndUndoGroup(); + } + + + + void OnMenuRemapTimeaxis(object sender, RoutedEventArgs e) { + var project = DocManager.Inst.Project; + var dialog = new TypeInDialog { + Title = ThemeManager.GetString("menu.project.remaptimeaxis") + }; + dialog.Height = 200; + dialog.SetPrompt(ThemeManager.GetString("dialogs.remaptimeaxis.message")); + dialog.SetText(project.tempos[0].bpm.ToString()); + dialog.onFinish = s => { + try { + if (double.TryParse(s, out double bpm)) { + DocManager.Inst.StartUndoGroup(); + var oldTimeAxis = project.timeAxis.Clone(); + DocManager.Inst.ExecuteCmd(new BpmCommand( + project, bpm)); + foreach (var tempo in project.tempos.Skip(1)) { + DocManager.Inst.ExecuteCmd(new DelTempoChangeCommand( + project, tempo.position)); + } + viewModel.RemapTimeAxis(oldTimeAxis, project.timeAxis.Clone()); + DocManager.Inst.EndUndoGroup(); + } + } catch (Exception e) { + Log.Error(e, "Failed to open project location"); + MessageBox.ShowError(this, new MessageCustomizableException("Failed to open project location", ": project location", e)); + } + }; + dialog.ShowDialog(this); + } + + private void AddTimeSigChange(int bar) { + var project = DocManager.Inst.Project; + var timeSig = project.timeAxis.TimeSignatureAtBar(bar); + var dialog = new TimeSignatureDialog(timeSig.beatPerBar, timeSig.beatUnit); + dialog.OnOk = (beatPerBar, beatUnit) => { + DocManager.Inst.StartUndoGroup(); + DocManager.Inst.ExecuteCmd(new AddTimeSigCommand( + project, bar, dialog.BeatPerBar, dialog.BeatUnit)); + DocManager.Inst.EndUndoGroup(); + }; + dialog.ShowDialog(this); + } + + private void DelTimeSigChange(int bar) { + var project = DocManager.Inst.Project; + DocManager.Inst.StartUndoGroup(); + DocManager.Inst.ExecuteCmd(new DelTimeSigCommand(project, bar)); + DocManager.Inst.EndUndoGroup(); + } + + void OnMenuNew(object sender, RoutedEventArgs args) => NewProject(); + async void NewProject() { + if (!DocManager.Inst.ChangesSaved && !await AskIfSaveAndContinue()) { + return; + } + viewModel.NewProject(); + } + + void OnMenuOpen(object sender, RoutedEventArgs args) => Open(); + async void Open() { + if (!DocManager.Inst.ChangesSaved && !await AskIfSaveAndContinue()) { + return; + } + var files = await FilePicker.OpenFilesAboutProject( + this, "menu.file.open", + FilePicker.ProjectFiles, + FilePicker.USTX, + FilePicker.VSQX, + FilePicker.UST, + FilePicker.MIDI, + FilePicker.UFDATA); + if (files == null || files.Length == 0) { + return; + } + try { + viewModel.OpenProject(files); + } catch (Exception e) { + Log.Error(e, $"Failed to open files {string.Join("\n", files)}"); + _ = await MessageBox.ShowError(this, new MessageCustomizableException($"Failed to open files {string.Join("\n", files)}", $":\n{string.Join("\n", files)}", e)); + } + } + + void OnMainMenuOpened(object sender, RoutedEventArgs args) { + viewModel.RefreshOpenRecent(); + viewModel.RefreshTemplates(); + viewModel.RefreshCacheSize(); + } + + void OnMainMenuClosed(object sender, RoutedEventArgs args) { + Focus(); // Force unfocus menu for key down events. + } + + void OnMainMenuPointerLeave(object sender, PointerEventArgs args) { + Focus(); // Force unfocus menu for key down events. + } + + void OnMenuOpenProjectLocation(object sender, RoutedEventArgs args) { + var project = DocManager.Inst.Project; + if (string.IsNullOrEmpty(project.FilePath) || !project.Saved) { + MessageBox.Show( + this, + ThemeManager.GetString("dialogs.export.savefirst"), + ThemeManager.GetString("errors.caption"), + MessageBox.MessageBoxButtons.Ok); + } + try { + var dir = Path.GetDirectoryName(project.FilePath); + if (dir != null) { + OS.OpenFolder(dir); + } else { + Log.Error($"Failed to get project location from {dir}."); + } + } catch (Exception e) { + Log.Error(e, "Failed to open project location."); + MessageBox.ShowError(this, new MessageCustomizableException("Failed to open project location.", ": project location", e)); + } + } + + async void OnMenuSave(object sender, RoutedEventArgs args) => await Save(); + public async Task Save() { + if (!viewModel.ProjectSaved) { + await SaveAs(); + } else { + viewModel.SaveProject(); + string message = ThemeManager.GetString("progress.saved"); + message = string.Format(message, DateTime.Now); + DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, message)); + } + } + + async void OnMenuSaveAs(object sender, RoutedEventArgs args) => await SaveAs(); + async Task SaveAs() { + var file = await FilePicker.SaveFileAboutProject( + this, "menu.file.saveas", FilePicker.USTX); + if (!string.IsNullOrEmpty(file)) { + viewModel.SaveProject(file); + } + } + + void OnMenuSaveTemplate(object sender, RoutedEventArgs args) { + var project = DocManager.Inst.Project; + var dialog = new TypeInDialog(); + dialog.Title = ThemeManager.GetString("menu.file.savetemplate"); + dialog.SetText("default"); + dialog.onFinish = file => { + if (string.IsNullOrEmpty(file)) { + return; + } + file = Path.GetFileNameWithoutExtension(file); + file = $"{file}.ustx"; + file = Path.Combine(PathManager.Inst.TemplatesPath, file); + Ustx.Save(file, project.CloneAsTemplate()); + }; + dialog.ShowDialog(this); + } + + async void OnMenuImportTracks(object sender, RoutedEventArgs args) { + var files = await FilePicker.OpenFilesAboutProject( + this, "menu.file.importtracks", + FilePicker.ProjectFiles, + FilePicker.USTX, + FilePicker.VSQX, + FilePicker.UST, + FilePicker.MIDI, + FilePicker.UFDATA); + if (files == null || files.Length == 0) { + return; + } + try { + var loadedProjects = Formats.ReadProjects(files); + if (loadedProjects == null || loadedProjects.Length == 0) { + return; + } + bool importTempo = true; + switch (Preferences.Default.ImportTempo) { + case 1: + importTempo = false; + break; + case 2: + if (loadedProjects[0].tempos.Count == 0) { + importTempo = false; + break; + } + var tempoString = String.Join("\n", + loadedProjects[0].tempos + .Select(tempo => $"position: {tempo.position}, tempo: {tempo.bpm}") + ); + //ask the user + var result = await MessageBox.Show( + this, + ThemeManager.GetString("dialogs.importtracks.importtempo") + "\n" + tempoString, + ThemeManager.GetString("dialogs.importtracks.caption"), + MessageBox.MessageBoxButtons.YesNo); + if (result == MessageBox.MessageBoxResult.No) { + importTempo = false; + } + break; + } + viewModel.ImportTracks(loadedProjects, importTempo); + } catch (Exception e) { + Log.Error(e, $"Failed to import files"); + _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import files", "", e)); + } + ValidateTracksVoiceColor(); + } + + async void OnMenuImportAudio(object sender, RoutedEventArgs args) { + var file = await FilePicker.OpenFileAboutProject( + this, "menu.file.importaudio", FilePicker.AudioFiles); + if (file == null) { + return; + } + try { + viewModel.ImportAudio(file); + } catch (Exception e) { + Log.Error(e, "Failed to import audio"); + _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import audio", "", e)); + } + } + + async void OnMenuImportMidi(object sender, RoutedEventArgs args) { + var file = await FilePicker.OpenFileAboutProject( + this, "menu.file.importmidi", FilePicker.MIDI); + if (file == null) { + return; + } + try { + viewModel.ImportMidi(file); + } catch (Exception e) { + Log.Error(e, "Failed to import midi"); + _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import midi", "", e)); + } + } + + async void OnMenuExportMixdown(object sender, RoutedEventArgs args) { + var project = DocManager.Inst.Project; + var file = await FilePicker.SaveFileAboutProject( + this, "menu.file.exportmixdown", FilePicker.WAV); + if (!string.IsNullOrEmpty(file)) { + await PlaybackManager.Inst.RenderMixdown(project, file); + } + } + + async void OnMenuExportWav(object sender, RoutedEventArgs args) { + var project = DocManager.Inst.Project; + if (await WarnToSave(project)) { + var name = Path.GetFileNameWithoutExtension(project.FilePath); + var path = Path.GetDirectoryName(project.FilePath); + path = Path.Combine(path!, "Export", $"{name}.wav"); + await PlaybackManager.Inst.RenderToFiles(project, path); + } + } + + async void OnMenuExportWavTo(object sender, RoutedEventArgs args) { + var project = DocManager.Inst.Project; + var file = await FilePicker.SaveFileAboutProject( + this, "menu.file.exportwavto", FilePicker.WAV); + if (!string.IsNullOrEmpty(file)) { + await PlaybackManager.Inst.RenderToFiles(project, file); + } + } + + async void OnMenuExportDsTo(object sender, RoutedEventArgs e) { + var project = DocManager.Inst.Project; + var file = await FilePicker.SaveFileAboutProject( + this, "menu.file.exportds", FilePicker.DS); + if (!string.IsNullOrEmpty(file)) { + for (var i = 0; i < project.parts.Count; i++) { + var part = project.parts[i]; + if (part is UVoicePart voicePart) { + var savePath = PathManager.Inst.GetPartSavePath(file, voicePart.DisplayName, i)[..^4] + ".ds"; + DiffSingerScript.SavePart(project, voicePart, savePath); + DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); + } + } + } + } + + async void OnMenuExportDsV2To(object sender, RoutedEventArgs e) { + var project = DocManager.Inst.Project; + var file = await FilePicker.SaveFileAboutProject( + this, "menu.file.exportds.v2", FilePicker.DS); + if (!string.IsNullOrEmpty(file)) { + for (var i = 0; i < project.parts.Count; i++) { + var part = project.parts[i]; + if (part is UVoicePart voicePart) { + var savePath = PathManager.Inst.GetPartSavePath(file, voicePart.DisplayName, i)[..^4] + ".ds"; + DiffSingerScript.SavePart(project, voicePart, savePath, true); + DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); + } + } + } + } + + async void OnMenuExportDsV2WithoutPitchTo(object sender, RoutedEventArgs e) { + var project = DocManager.Inst.Project; + var file = await FilePicker.SaveFileAboutProject( + this, "menu.file.exportds.v2withoutpitch", FilePicker.DS); + if (!string.IsNullOrEmpty(file)) { + for (var i = 0; i < project.parts.Count; i++) { + var part = project.parts[i]; + if (part is UVoicePart voicePart) { + var savePath = PathManager.Inst.GetPartSavePath(file, voicePart.DisplayName, i)[..^4] + ".ds"; + DiffSingerScript.SavePart(project, voicePart, savePath, true, false); + DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); + } + } + } + } + + async void OnMenuExportUst(object sender, RoutedEventArgs e) { + var project = DocManager.Inst.Project; + if (await WarnToSave(project)) { + var name = Path.GetFileNameWithoutExtension(project.FilePath); + var path = Path.GetDirectoryName(project.FilePath); + path = Path.Combine(path!, "Export", $"{name}.ust"); + for (var i = 0; i < project.parts.Count; i++) { + var part = project.parts[i]; + if (part is UVoicePart voicePart) { + var savePath = PathManager.Inst.GetPartSavePath(path, voicePart.DisplayName, i); + Ust.SavePart(project, voicePart, savePath); + DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); + } + } + } + } + + async void OnMenuExportUstTo(object sender, RoutedEventArgs e) { + var project = DocManager.Inst.Project; + var file = await FilePicker.SaveFileAboutProject( + this, "menu.file.exportustto", FilePicker.UST); + if (!string.IsNullOrEmpty(file)) { + for (var i = 0; i < project.parts.Count; i++) { + var part = project.parts[i]; + if (part is UVoicePart voicePart) { + var savePath = PathManager.Inst.GetPartSavePath(file, voicePart.DisplayName, i); + Ust.SavePart(project, voicePart, savePath); + DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); + } + } + } + } + + async void OnMenuExportMidi(object sender, RoutedEventArgs e) { + var project = DocManager.Inst.Project; + var file = await FilePicker.SaveFileAboutProject( + this, "menu.file.exportmidi", FilePicker.MIDI); + if (!string.IsNullOrEmpty(file)) { + MidiWriter.Save(file, project); + } + } + + private async Task WarnToSave(UProject project) { + if (string.IsNullOrEmpty(project.FilePath)) { + await MessageBox.Show( + this, + ThemeManager.GetString("dialogs.export.savefirst"), + ThemeManager.GetString("dialogs.export.caption"), + MessageBox.MessageBoxButtons.Ok); + return false; + } + return true; + } + + void OnMenuUndo(object sender, RoutedEventArgs args) => viewModel.Undo(); + void OnMenuRedo(object sender, RoutedEventArgs args) => viewModel.Redo(); + + void OnMenuExpressionss(object sender, RoutedEventArgs args) { + var dialog = new ExpressionsDialog() { + DataContext = new ExpressionsViewModel(), + }; + dialog.ShowDialog(this); + if (dialog.Position.Y < 0) { + dialog.Position = dialog.Position.WithY(0); + } + } + + void OnMenuSingers(object sender, RoutedEventArgs args) { + OpenSingersWindow(); + } + + /// + /// Check if a track has a singer and if it exists. + /// If the user haven't selected a singer for the track, or the singer specified in ustx project doesn't exist, return null. + /// Otherwise, return the singer. + /// + public USinger? TrackSingerIfFound(UTrack track) { + if (track.Singer?.Found ?? false) { + return track.Singer; + } + return null; + } + + public void OpenSingersWindow() { + var lifetime = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; + if (lifetime == null) { + return; + } + + MessageBox.ShowLoading(this); + var dialog = lifetime.Windows.FirstOrDefault(w => w is SingersDialog); + try { + if (dialog == null) { + USinger? singer = null; + if (viewModel.TracksViewModel.SelectedParts.Count > 0) { + singer = TrackSingerIfFound(viewModel.TracksViewModel.Tracks[viewModel.TracksViewModel.SelectedParts.First().trackNo]); + } + if (singer == null && viewModel.TracksViewModel.Tracks.Count > 0) { + singer = TrackSingerIfFound(viewModel.TracksViewModel.Tracks.First()); + } + var vm = new SingersViewModel(); + if (singer != null) { + vm.Singer = singer; + } + dialog = new SingersDialog() { DataContext = vm }; + dialog.Show(); + } + if (dialog.Position.Y < 0) { + dialog.Position = dialog.Position.WithY(0); + } + } catch (Exception e) { + DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); + } finally { + MessageBox.CloseLoading(); + } + if (dialog != null) { + dialog.Activate(); + } + } + + async void OnMenuInstallSinger(object sender, RoutedEventArgs args) { + var file = await FilePicker.OpenFileAboutSinger( + this, "menu.tools.singer.install", FilePicker.ArchiveFiles); + if (file == null) { + return; + } + try { + if (file.EndsWith(Core.Vogen.VogenSingerInstaller.FileExt)) { + Core.Vogen.VogenSingerInstaller.Install(file); + return; + } + if (file.EndsWith(DependencyInstaller.FileExt)) { + DependencyInstaller.Install(file); + return; + } + + var setup = new SingerSetupDialog() { + DataContext = new SingerSetupViewModel() { + ArchiveFilePath = file, + }, + }; + _ = setup.ShowDialog(this); + if (setup.Position.Y < 0) { + setup.Position = setup.Position.WithY(0); + } + } catch (Exception e) { + Log.Error(e, $"Failed to install singer {file}"); + MessageCustomizableException mce; + if (e is MessageCustomizableException) { + mce = (MessageCustomizableException)e; + } else { + mce = new MessageCustomizableException($"Failed to install singer {file}", $": {file}", e); + } + _ = await MessageBox.ShowError(this, mce); + } + } + + async void OnMenuInstallDependency(object sender, RoutedEventArgs args) { + var file = await FilePicker.OpenFile( + this, "menu.tools.dependency.install", FilePicker.OUDEP); + if (file == null) { + return; + } + if (file.EndsWith(DependencyInstaller.FileExt)) { + DependencyInstaller.Install(file); + return; + } + } + + void OnMenuPreferences(object sender, RoutedEventArgs args) { + PreferencesViewModel dataContext; + try { + dataContext = new PreferencesViewModel(); + } catch (Exception e) { + Log.Error(e, "Failed to load prefs. Initialize it."); + MessageBox.ShowError(this, new MessageCustomizableException("Failed to load prefs. Initialize it.", "", e)); + Preferences.Reset(); + dataContext = new PreferencesViewModel(); + } + var dialog = new PreferencesDialog() { + DataContext = dataContext + }; + dialog.ShowDialog(this); + if (dialog.Position.Y < 0) { + dialog.Position = dialog.Position.WithY(0); + } + } + + void OnMenuClearCache(object sender, RoutedEventArgs args) { + Task.Run(() => { + DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, "Clearing cache...")); + PathManager.Inst.ClearCache(); + DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, "Cache cleared.")); + }); + } + + void OnMenuDebugWindow(object sender, RoutedEventArgs args) { + var desktop = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; + if (desktop == null) { + return; + } + var window = desktop.Windows.FirstOrDefault(w => w is DebugWindow); + if (window == null) { + window = new DebugWindow(); + } + window.Show(); + } + + void OnMenuPhoneticAssistant(object sender, RoutedEventArgs args) { + var desktop = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; + if (desktop == null) { + return; + } + var window = desktop.Windows.FirstOrDefault(w => w is PhoneticAssistant); + if (window == null) { + window = new PhoneticAssistant(); + } + window.Show(); + } + + void OnMenuCheckUpdate(object sender, RoutedEventArgs args) { + var dialog = new UpdaterDialog(); + dialog.ViewModel.CloseApplication = + () => (Application.Current?.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown(); + dialog.ShowDialog(this); + } + + void OnMenuLogsLocation(object sender, RoutedEventArgs args) { + try { + OS.OpenFolder(PathManager.Inst.LogsPath); + } catch (Exception e) { + DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); + } + } + + void OnMenuReportIssue(object sender, RoutedEventArgs args) { + try { + OS.OpenWeb("https://github.com/stakira/OpenUtau/issues"); + } catch (Exception e) { + DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); + } + } + + void OnMenuWiki(object sender, RoutedEventArgs args) { + try { + OS.OpenWeb("https://github.com/stakira/OpenUtau/wiki/Getting-Started"); + } catch (Exception e) { + DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); + } + } + + void OnMenuLayoutVSplit11(object sender, RoutedEventArgs args) => LayoutSplit(null, 1.0 / 2); + void OnMenuLayoutVSplit12(object sender, RoutedEventArgs args) => LayoutSplit(null, 1.0 / 3); + void OnMenuLayoutVSplit13(object sender, RoutedEventArgs args) => LayoutSplit(null, 1.0 / 4); + void OnMenuLayoutHSplit11(object sender, RoutedEventArgs args) => LayoutSplit(1.0 / 2, null); + void OnMenuLayoutHSplit12(object sender, RoutedEventArgs args) => LayoutSplit(1.0 / 3, null); + void OnMenuLayoutHSplit13(object sender, RoutedEventArgs args) => LayoutSplit(1.0 / 4, null); + + private void LayoutSplit(double? x, double? y) { + if (Screens.Primary == null) { + return; + } + var wa = Screens.Primary.WorkingArea; + WindowState = WindowState.Normal; + double titleBarHeight = 20; + if (FrameSize != null) { + double borderThickness = (FrameSize!.Value.Width - ClientSize.Width) / 2; + titleBarHeight = FrameSize!.Value.Height - ClientSize.Height - borderThickness; + } + Position = new PixelPoint(0, 0); + Width = x != null ? wa.Size.Width * x.Value : wa.Size.Width; + Height = (y != null ? wa.Size.Height * y.Value : wa.Size.Height) - titleBarHeight; + if (pianoRollWindow != null) { + pianoRollWindow.Position = new PixelPoint(x != null ? (int)Width : 0, y != null ? (int)(Height + (OS.IsMacOS() ? 25 : titleBarHeight)) : 0); + pianoRollWindow.Width = x != null ? wa.Size.Width - Width : wa.Size.Width; + pianoRollWindow.Height = (y != null ? wa.Size.Height - (Height + titleBarHeight) : wa.Size.Height) - titleBarHeight; + } + } + + void OnKeyDown(object sender, 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; + 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; + } + } + } + + void OnPointerPressed(object? sender, PointerPressedEventArgs args) { + if (!args.Handled && args.ClickCount == 1) { + FocusManager?.ClearFocus(); + } + } + + async void OnDrop(object? sender, DragEventArgs args) { + var storageItem = args.Data?.GetFiles()?.FirstOrDefault(); + if (storageItem == null) { + return; + } + string file = storageItem.Path.LocalPath; + var ext = Path.GetExtension(file); + if (ext == ".ustx" || ext == ".ust" || ext == ".vsqx" || ext == ".ufdata") { + if (!DocManager.Inst.ChangesSaved && !await AskIfSaveAndContinue()) { + return; + } + try { + viewModel.OpenProject(new string[] { file }); + } catch (Exception e) { + Log.Error(e, $"Failed to open file {file}"); + _ = await MessageBox.ShowError(this, new MessageCustomizableException($"Failed to open file {file}", $": {file}", e)); + } + } else if (ext == ".mid" || ext == ".midi") { + try { + viewModel.ImportMidi(file); + } catch (Exception e) { + Log.Error(e, "Failed to import midi"); + _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import midi", "", e)); + } + } else if (ext == ".zip" || ext == ".rar" || ext == ".uar") { + try { + var setup = new SingerSetupDialog() { + DataContext = new SingerSetupViewModel() { + ArchiveFilePath = file, + }, + }; + _ = setup.ShowDialog(this); + if (setup.Position.Y < 0) { + setup.Position = setup.Position.WithY(0); + } + } catch (Exception e) { + Log.Error(e, $"Failed to install singer {file}"); + MessageCustomizableException mce; + if (e is MessageCustomizableException) { + mce = (MessageCustomizableException)e; + } else { + mce = new MessageCustomizableException($"Failed to install singer {file}", $": {file}", e); + } + _ = await MessageBox.ShowError(this, mce); + } + } else if (ext == Core.Vogen.VogenSingerInstaller.FileExt) { + Core.Vogen.VogenSingerInstaller.Install(file); + } else if (ext == ".dll") { + var result = await MessageBox.Show( + this, + ThemeManager.GetString("dialogs.installdll.message") + file, + ThemeManager.GetString("dialogs.installdll.caption"), + MessageBox.MessageBoxButtons.OkCancel); + if (result == MessageBox.MessageBoxResult.Ok) { + Core.Api.PhonemizerInstaller.Install(file); + } + } else if (ext == ".exe") { + var setup = new ExeSetupDialog() { + DataContext = new ExeSetupViewModel(file) + }; + _ = setup.ShowDialog(this); + if (setup.Position.Y < 0) { + setup.Position = setup.Position.WithY(0); + } + } else if (ext == DependencyInstaller.FileExt) { + var result = await MessageBox.Show( + this, + ThemeManager.GetString("dialogs.installdependency.message") + file, + ThemeManager.GetString("dialogs.installdependency.caption"), + MessageBox.MessageBoxButtons.OkCancel); + if (result == MessageBox.MessageBoxResult.Ok) { + DependencyInstaller.Install(file); + } + } else if (ext == ".mp3" || ext == ".wav" || ext == ".ogg" || ext == ".flac") { + try { + viewModel.ImportAudio(file); + } catch (Exception e) { + Log.Error(e, "Failed to import audio"); + _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import audio", "", e)); + } + } else { + _ = await MessageBox.Show( + this, + ThemeManager.GetString("dialogs.unsupportedfile.message") + ext, + ThemeManager.GetString("dialogs.unsupportedfile.caption"), + MessageBox.MessageBoxButtons.Ok); + } + } + + void OnPlayOrPause(object sender, RoutedEventArgs args) { + PlayOrPause(); + } + + void PlayOrPause() { + viewModel.PlaybackViewModel.PlayOrPause(); + } + + 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)); + } + + 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)); + } + + 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.TracksViewModel.OnXZoomed(position, 0.1 * args.Delta.Y); + } + + public void ViewScalerPointerWheelChanged(object sender, PointerWheelEventArgs args) { + viewModel.TracksViewModel.OnYZoomed(new Point(0, 0.5), 0.1 * args.Delta.Y); + } + + 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.TracksViewModel.PointToLineTick(point.Position, out int left, out int right); + viewModel.PlaybackViewModel.MovePlayPos(left); + } else if (point.Properties.IsRightButtonPressed) { + int tick = viewModel.TracksViewModel.PointToTick(point.Position); + viewModel.RefreshTimelineContextMenu(tick); + } + } + + public void TimelinePointerMoved(object sender, PointerEventArgs args) { + var control = (Control)sender; + var point = args.GetCurrentPoint(control); + if (point.Properties.IsLeftButtonPressed) { + viewModel.TracksViewModel.PointToLineTick(point.Position, out int left, out int right); + viewModel.PlaybackViewModel.MovePlayPos(left); + } + } + + public void TimelinePointerReleased(object sender, PointerReleasedEventArgs args) { + args.Pointer.Capture(null); + } + + public void PartsCanvasPointerPressed(object sender, PointerPressedEventArgs args) { + var control = (Control)sender; + var point = args.GetCurrentPoint(control); + var hitControl = control.InputHitTest(point.Position); + if (partEditState != null) { + return; + } + if (point.Properties.IsLeftButtonPressed) { + if (args.KeyModifiers == cmdKey) { + partEditState = new PartSelectionEditState(control, viewModel, SelectionBox); + Cursor = ViewConstants.cursorCross; + } else if (hitControl == control) { + viewModel.TracksViewModel.DeselectParts(); + var part = viewModel.TracksViewModel.MaybeAddPart(point.Position); + if (part != null) { + // Start moving right away + partEditState = new PartMoveEditState(control, viewModel, part); + Cursor = ViewConstants.cursorSizeAll; + } + } else if (hitControl is PartControl partControl) { + bool isVoice = partControl.part is UVoicePart; + bool isWave = partControl.part is UWavePart; + bool trim = point.Position.X > partControl.Bounds.Right - ViewConstants.ResizeMargin; + bool skip = point.Position.X < partControl.Bounds.Left + ViewConstants.ResizeMargin; + if (isVoice && trim) { + partEditState = new PartResizeEditState(control, viewModel, partControl.part); + Cursor = ViewConstants.cursorSizeWE; + } else if (isWave && skip) { + // TODO + } else if (isWave && trim) { + // TODO + } else { + partEditState = new PartMoveEditState(control, viewModel, partControl.part); + Cursor = ViewConstants.cursorSizeAll; + } + } + } else if (point.Properties.IsRightButtonPressed) { + if (hitControl is PartControl partControl) { + if (!viewModel.TracksViewModel.SelectedParts.Contains(partControl.part)) { + viewModel.TracksViewModel.DeselectParts(); + viewModel.TracksViewModel.SelectPart(partControl.part); + } + if (PartsContextMenu != null && viewModel.TracksViewModel.SelectedParts.Count > 0) { + PartsContextMenu.DataContext = new PartsContextMenuArgs { + Part = partControl.part, + PartDeleteCommand = viewModel.PartDeleteCommand, + PartGotoFileCommand = PartGotoFileCommand, + PartReplaceAudioCommand = PartReplaceAudioCommand, + PartRenameCommand = PartRenameCommand, + PartTranscribeCommand = PartTranscribeCommand, + }; + shouldOpenPartsContextMenu = true; + } + } else { + viewModel.TracksViewModel.DeselectParts(); + } + } else if (point.Properties.IsMiddleButtonPressed) { + partEditState = new PartPanningState(control, viewModel); + Cursor = ViewConstants.cursorHand; + } + if (partEditState != null) { + partEditState.Begin(point.Pointer, point.Position); + partEditState.Update(point.Pointer, point.Position); + } + } + + public void PartsCanvasPointerMoved(object sender, PointerEventArgs args) { + var control = (Control)sender; + var point = args.GetCurrentPoint(control); + if (partEditState != null) { + partEditState.Update(point.Pointer, point.Position); + return; + } + var hitControl = control.InputHitTest(point.Position); + if (hitControl is PartControl partControl) { + bool isVoice = partControl.part is UVoicePart; + bool isWave = partControl.part is UWavePart; + bool trim = point.Position.X > partControl.Bounds.Right - ViewConstants.ResizeMargin; + bool skip = point.Position.X < partControl.Bounds.Left + ViewConstants.ResizeMargin; + if (isVoice && trim) { + Cursor = ViewConstants.cursorSizeWE; + } else if (isWave && (skip || trim)) { + Cursor = null; // TODO + } else { + Cursor = null; + } + } else { + Cursor = null; + } + } + + public void PartsCanvasPointerReleased(object sender, PointerReleasedEventArgs args) { + if (partEditState != null) { + if (partEditState.MouseButton != args.InitialPressMouseButton) { + return; + } + var control = (Control)sender; + var point = args.GetCurrentPoint(control); + partEditState.Update(point.Pointer, point.Position); + partEditState.End(point.Pointer, point.Position); + partEditState = null; + Cursor = null; + } + if (openPianoRollWindow) { + pianoRollWindow?.Show(); + pianoRollWindow?.Activate(); + openPianoRollWindow = false; + } + } + + public void PartsCanvasDoubleTapped(object sender, TappedEventArgs args) { + if (!(sender is Canvas canvas)) { + return; + } + var control = canvas.InputHitTest(args.GetPosition(canvas)); + if (control is PartControl partControl && partControl.part is UVoicePart) { + if (pianoRollWindow == null) { + MessageBox.ShowLoading(this); + pianoRollWindow = new PianoRollWindow() { + MainWindow = this, + }; + pianoRollWindow.ViewModel.PlaybackViewModel = viewModel.PlaybackViewModel; + MessageBox.CloseLoading(); + } + // Workaround for new window losing focus. + openPianoRollWindow = true; + int tick = viewModel.TracksViewModel.PointToTick(args.GetPosition(canvas)); + DocManager.Inst.ExecuteCmd(new LoadPartNotification(partControl.part, DocManager.Inst.Project, tick)); + pianoRollWindow.AttachExpressions(); + } + } + + public void PartsCanvasPointerWheelChanged(object sender, PointerWheelEventArgs args) { + 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) { + ViewScalerPointerWheelChanged(VScaler, args); + } else if (args.KeyModifiers == cmdKey) { + TimelinePointerWheelChanged(TimelineCanvas, args); + } + if (partEditState != null) { + var point = args.GetCurrentPoint(partEditState.control); + partEditState.Update(point.Pointer, point.Position); + } + } + + public void PartsContextMenuOpening(object sender, CancelEventArgs args) { + if (shouldOpenPartsContextMenu) { + shouldOpenPartsContextMenu = false; + } else { + args.Cancel = true; + } + } + + public void PartsContextMenuClosing(object sender, CancelEventArgs args) { + if (PartsContextMenu != null) { + PartsContextMenu.DataContext = null; + } + } + + void RenamePart(UPart part) { + var dialog = new TypeInDialog(); + dialog.Title = ThemeManager.GetString("context.part.rename"); + dialog.SetText(part.name); + dialog.onFinish = name => { + if (!string.IsNullOrWhiteSpace(name) && name != part.name) { + if (!string.IsNullOrWhiteSpace(name) && name != part.name) { + DocManager.Inst.StartUndoGroup(); + DocManager.Inst.ExecuteCmd(new RenamePartCommand(DocManager.Inst.Project, part, name)); + DocManager.Inst.EndUndoGroup(); + } + } + }; + dialog.ShowDialog(this); + } + + void GotoFile(UPart part) { + //View the location of the audio file in explorer if the part is a wave part + if (part is UWavePart wavePart) { + try { + OS.GotoFile(wavePart.FilePath); + } catch (Exception e) { + DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); + } + } + } + + async void ReplaceAudio(UPart part) { + var file = await FilePicker.OpenFileAboutProject( + this, "context.part.replaceaudio", FilePicker.AudioFiles); + if (file == null) { + return; + } + UWavePart newPart = new UWavePart() { + FilePath = file, + trackNo = part.trackNo, + position = part.position + }; + newPart.Load(DocManager.Inst.Project); + DocManager.Inst.StartUndoGroup(); + DocManager.Inst.ExecuteCmd(new ReplacePartCommand(DocManager.Inst.Project, part, newPart)); + DocManager.Inst.EndUndoGroup(); + } + + void Transcribe(UPart part) { + //Convert audio to notes + if (part is UWavePart wavePart) { + try { + string text = ThemeManager.GetString("context.part.transcribing"); + var msgbox = MessageBox.ShowModal(this, $"{text} {part.name}", text); + //Duration of the wave file in seconds + int wavDurS = (int)(wavePart.fileDurationMs / 1000.0); + var scheduler = TaskScheduler.FromCurrentSynchronizationContext(); + var transcribeTask = Task.Run(() => { + using (var some = new Some()) { + return some.Transcribe(DocManager.Inst.Project, wavePart, wavPosS => { + //msgbox?.SetText($"{text} {part.name}\n{wavPosS}/{wavDurS}"); + msgbox.SetText(string.Format("{0} {1}\n{2}s / {3}s", text, part.name, wavPosS, wavDurS)); + }); + } + }); + transcribeTask.ContinueWith(task => { + msgbox?.Close(); + if (task.IsFaulted) { + Log.Error(task.Exception, $"Failed to transcribe part {part.name}"); + MessageBox.ShowError(this, task.Exception); + return; + } + var voicePart = task.Result; + //Add voicePart into project + if (voicePart != null) { + var project = DocManager.Inst.Project; + var track = new UTrack(project); + track.TrackNo = project.tracks.Count; + voicePart.trackNo = track.TrackNo; + DocManager.Inst.StartUndoGroup(); + DocManager.Inst.ExecuteCmd(new AddTrackCommand(project, track)); + DocManager.Inst.ExecuteCmd(new AddPartCommand(project, voicePart)); + DocManager.Inst.EndUndoGroup(); + } + }, scheduler); + } catch (Exception e) { + Log.Error(e, $"Failed to transcribe part {part.name}"); + MessageBox.ShowError(this, e); + } + } + } + + async void ValidateTracksVoiceColor() { + DocManager.Inst.StartUndoGroup(); + foreach (var track in DocManager.Inst.Project.tracks) { + if (track.ValidateVoiceColor(out var oldColors, out var newColors)) { + await VoiceColorRemappingAsync(track, oldColors, newColors); + } + } + DocManager.Inst.EndUndoGroup(); + } + async Task VoiceColorRemappingAsync(UTrack track, string[] oldColors, string[] newColors) { + var parts = DocManager.Inst.Project.parts + .Where(part => part.trackNo == track.TrackNo && part is UVoicePart) + .Cast() + .Where(vpart => vpart.notes.Count > 0); + if (parts.Any()) { + var dialog = new VoiceColorMappingDialog(); + VoiceColorMappingViewModel vm = new VoiceColorMappingViewModel(oldColors, newColors, track.TrackName); + dialog.DataContext = vm; + await dialog.ShowDialog(this); + + if (dialog.Apply) { + SetVoiceColorRemapping(track, parts, vm); + } + } + } + void VoiceColorRemapping(UTrack track, string[] oldColors, string[] newColors) { + var parts = DocManager.Inst.Project.parts + .Where(part => part.trackNo == track.TrackNo && part is UVoicePart) + .Cast() + .Where(vpart => vpart.notes.Count > 0); + if (parts.Any()) { + var dialog = new VoiceColorMappingDialog(); + VoiceColorMappingViewModel vm = new VoiceColorMappingViewModel(oldColors, newColors, track.TrackName); + dialog.DataContext = vm; + dialog.onFinish = () => { + DocManager.Inst.StartUndoGroup(); + SetVoiceColorRemapping(track, parts, vm); + DocManager.Inst.EndUndoGroup(); + }; + dialog.ShowDialog(this); + } + } + void SetVoiceColorRemapping(UTrack track, IEnumerable parts, VoiceColorMappingViewModel vm) { + foreach (var part in parts) { + foreach (var phoneme in part.phonemes) { + var tuple = phoneme.GetExpression(DocManager.Inst.Project, track, Ustx.CLR); + if (vm.ColorMappings.Any(m => m.OldIndex == tuple.Item1)) { + var mapping = vm.ColorMappings.First(m => m.OldIndex == tuple.Item1); + if (mapping.OldIndex != mapping.SelectedIndex) { + if (mapping.SelectedIndex == 0) { + DocManager.Inst.ExecuteCmd(new SetPhonemeExpressionCommand(DocManager.Inst.Project, track, part, phoneme, Ustx.CLR, null)); + } else { + DocManager.Inst.ExecuteCmd(new SetPhonemeExpressionCommand(DocManager.Inst.Project, track, part, phoneme, Ustx.CLR, mapping.SelectedIndex)); + } + } + } else { + DocManager.Inst.ExecuteCmd(new SetPhonemeExpressionCommand(DocManager.Inst.Project, track, part, phoneme, Ustx.CLR, null)); + } + } + } + } + + public async void WindowClosing(object? sender, WindowClosingEventArgs e) { + if (forceClose || DocManager.Inst.ChangesSaved) { + if (Preferences.Default.ClearCacheOnQuit) { + Log.Information("Clearing cache..."); + PathManager.Inst.ClearCache(); + Log.Information("Cache cleared."); + } + return; + } + e.Cancel = true; + if (!await AskIfSaveAndContinue()) { + return; + } + pianoRollWindow?.Close(); + forceClose = true; + Close(); + } + + private async Task AskIfSaveAndContinue() { + var result = await MessageBox.Show( + this, + ThemeManager.GetString("dialogs.exitsave.message"), + ThemeManager.GetString("dialogs.exitsave.caption"), + MessageBox.MessageBoxButtons.YesNoCancel); + switch (result) { + case MessageBox.MessageBoxResult.Yes: + await Save(); + goto case MessageBox.MessageBoxResult.No; + case MessageBox.MessageBoxResult.No: + return true; // Continue. + default: + return false; // Cancel. + } + } + + public void OnNext(UCommand cmd, bool isUndo) { + if (cmd is ErrorMessageNotification notif) { + switch (notif.e) { + case Core.Render.NoResamplerException: + case Core.Render.NoWavtoolException: + MessageBox.Show( + this, + ThemeManager.GetString("dialogs.noresampler.message"), + ThemeManager.GetString("dialogs.noresampler.caption"), + MessageBox.MessageBoxButtons.Ok); + break; + default: + MessageBox.ShowError(this, notif.e, notif.message, true); + break; + } + } else if (cmd is LoadingNotification loadingNotif && loadingNotif.window == typeof(MainWindow)) { + if (loadingNotif.startLoading) { + MessageBox.ShowLoading(this); + } else { + MessageBox.CloseLoading(); + } + } else if (cmd is VoiceColorRemappingNotification voicecolorNotif) { + if (voicecolorNotif.TrackNo < 0 || DocManager.Inst.Project.tracks.Count <= voicecolorNotif.TrackNo) { + ValidateTracksVoiceColor(); + } else { + UTrack track = DocManager.Inst.Project.tracks[voicecolorNotif.TrackNo]; + if (!voicecolorNotif.Validate) { + VoiceColorRemapping(track, track.VoiceColorNames, track.VoiceColorExp.options); + } else if (track.ValidateVoiceColor(out var oldColors, out var newColors)) { + VoiceColorRemapping(track, oldColors, newColors); + } + } + } + } + } +} diff --git a/OpenUtau/Views/PreferencesDialog.axaml b/OpenUtau/Views/PreferencesDialog.axaml index b97dd3a13..1407d3b2b 100644 --- a/OpenUtau/Views/PreferencesDialog.axaml +++ b/OpenUtau/Views/PreferencesDialog.axaml @@ -234,6 +234,10 @@ + + + + @@ -269,4 +273,4 @@ - + \ No newline at end of file From 43f978b81ff84844e654fd7c1bf153b3d7000394 Mon Sep 17 00:00:00 2001 From: RedBlackAka <140876408+RedBlackAka@users.noreply.github.com> Date: Thu, 19 Dec 2024 03:23:58 +0100 Subject: [PATCH 2/4] Fix Preferences.cs Fix for accidentally replacing it with the wrong code --- OpenUtau.Core/Util/Preferences.cs | 772 ++++++++---------------------- 1 file changed, 201 insertions(+), 571 deletions(-) diff --git a/OpenUtau.Core/Util/Preferences.cs b/OpenUtau.Core/Util/Preferences.cs index e7f705ccc..87a633892 100644 --- a/OpenUtau.Core/Util/Preferences.cs +++ b/OpenUtau.Core/Util/Preferences.cs @@ -1,571 +1,201 @@ - - - Copy note - Delete note - Select and paste parameters - Delete part - View file location - Rename part - Reselect audio file - Transcribe audio to create a note part - Transcribing - Ease in - Ease in/out - Ease out - Linear - Add point - Delete point - Snap to previous note - Add tempo change at {0} - Add time signature change - Delete tempo change at {0} - Delete time signature change - - Clear - Copy Log - Reverse Log Order - - About OpenUTAU - - OpenUtau aims to be an open source editing environment for UTAU community, with a modern user experience and intelligent phonological support. Visit us on Github. - - Exit OpenUtau - The current project contains unsaved changes. Do you want to save it? - Export - Save project file first - Import tracks - The imported project contains the following tempo markers. Do you want to use them? - Installing dependency - Installing - Installing phonemizer - Installing - Cancel - Copy error to clipboard - No - Ok - Yes - No resampler - No resampler! Put your favourite resampler exe or dll in the Resamplers folder and choose it in Preferences! - 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 - Renderer - Set As Default - Unsupported file format - Unsupported file format: - Voice color remapping - Applies to all notes in this track: - - Error - Error Details - Error loading vocoder - . Please download vocoder from - Encrypted archive file isn't supported. Please extract the archive file manually. - Abbreviation must be between 1 and 4 characters long - Abbreviation must be set - Abbreviations must be unique - Default value must be between min and max - Flags must be unique - The following expressions have been merged due to duplicate flags - Min must be smaller than max - Name must be set - Failed to export - Failed to import audio - Failed to import files - Failed to import midi - Failed to install singer - Failed to load - Failed to load prefs. Initialize it. - Failed to open - Failed to open location - Failed to render - Failed to run editing macro - Failed to save - Failed to save singer config file - Failed to search singers - Try installing the latest Visual C++ Redistributable. https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170 - Character not allowed in regular expression - - regular expression error - - - Abbreviation - Apply - Expressions - Default - Resampler Flag - Add all expressions suggested by renderers - Is Resampler Flag - Max - Min - Name - Option Values - Separate values by ',' - Type - Curve - Numerical - Options - - German - English - Spanish - French - Not translating - Italian - Japanese - Korean - Polish - Portuguese - Russian - Vietnamese - Chinese - Cantonese - Thai - - Apply - Cancel - Edit Lyrics - Live preview - Max - Reset - Select some notes first! - Separators - - After : - Before : - Remove alphabet - Remove non-hiragana - Remove phonetic hint - Remove spaces and earlier - Remove tone suffix - Presets - Preview - Regular expressions can be used - General lyrics replacement - - Edit - Copy - Cut - Delete - Lock editing of unselected notes - Expressions - Pitch Points - Vibrato - Paste - Redo - Select All - Undo - File - Exit - Export Audio - Export DiffSinger Scripts To - Export DiffSinger Scripts To (v2) - Export DiffSinger Scripts To (v2, without pitch curve) - Export Midi File - Mixdown To Wav File - Export Project - Export Ust Files - Export Ust Files To... - Export Wav Files - Export Wav Files To... - Import Audio... - Import Midi... - DryWetMidi Importer - Naudio Importer - Import Tracks... - New - New From Template - Open... - Open As - Open Export Location - Open Project Location - Open Recent - Save - Save As... - Save Template... - Help - About OpenUtau - Check Update - Open Logs Location - Report Issue - OpenUtau Documentation - Project - Expressions... - Adjust Tempo (Preserve Timing) - Tools - Clear Cache - Debug Window - Install Dependency (.oudep)... - Layout - Horizontal 1:1 - Horizontal 1:2 - Horizontal 1:3 - Vertical 1:1 - Vertical 1:2 - Vertical 1:3 - Preferences... - Install Singer... - Install Singer (Advanced)... - Singers... - View - - Lyric - Default Lyric - Portamento - Length - Start - Preset - New Preset Name - Remove - Removes last applied preset. - Save - Save current settings to a new preset. - Reset All Settings - Reset every setting to default values. -Warning: this option removes custom presets. - Vibrato - Minimum Length - Automatic Vibrato by Length - Depth - Drift - Fade In - Length - Fade Out - Period - Shift - Volume Link - - Note Properties - Apply - Basic - Cancel - Set - Set only on long notes - Tone - Enable - - Alias - Color - Consonant - Cutoff - Set Consonant - Set Cutoff - Set Offset - Set Overlap - Set Preutter - File - Offset - Overlap - Phonetic - Prefix - Preutter - Set - Suffix - - Phonetic Assistant - Copy - - Batch Edits - Running batch edit - Lyrics - Replace "-" with "+" - Edit Lyrics - Hiragana to Romaji - Insert slur lyric - Japanese VCV to CV - Move Suffix to VoiceColor - Remove Letter Suffix - Remove Phonetic Hint - Remove Tone Suffix - Romaji to Hiragana - Note Defaults - Notes - Add breath - Add tail "-" - Add tail "R" - Auto Legato - Convert PITD to pitch control points - Clear vibratos - Fix overlapping notes - Hanzi to pinyin - Lengthen crossfades - Load rendered pitch - Move an octave down - Move an octave up - Quantize to 1/128 - Quantize to 1/64 - Remove tail "-" - Remove tail "R" - Reset phoneme aliases - Reset notes to default - Reset all parameters - Reset all expressions - Reset phoneme timings - Reset pitch bends - Reset vibratos - Part - Legacy Plugin (Experimental) - Singer and Oto settings - Open folder - Reload - Reset - Search Note - Select All - Close - Next - Prev - View Final Pitch to Render (R) - View Note Parameters (\) - View Phonemes (O) - View Pitch Bend (I) - Toggle Snap (P) - Auto - Auto (triplet) - View Tips (T) - Toggle Note Tone (Y) - View Vibrato (U) - View Waveform (W) - Draw Pitch Tool (4) - Left click to draw - Right click to reset - Hold Ctrl to select - Hold Alt to smoothen - - Line Draw Pitch Tool (Shift + 4) - Left click to draw (draw straight line) - Right click to reset - Hold Ctrl to select - Hold Alt to smoothen - - Eraser Tool (3) - Knife Tool (5) - Overwrite Pitch Tool (Ctrl + 4) - Left click to draw (overwrites vibrato or mod+) - Right click to reset - Hold Ctrl to select - Hold Alt to smoothen - - Pen Plus Tool (Ctrl + 2) - Left click to draw - Right click to delete - Hold Ctrl to select - Pen Tool (2) - Left click to draw - Hold Ctrl to select - Selection Tool (1) - - Advanced - Beta - Check for update on start - When importing tracks, use the tempos of the imported project - Always - Ask me each time - Never - Lyrics Helper - Lyrics Helper Adds Brackets - Remember these file types in "Open Recent" - Resampler Logging - Stores resampler output in log files. This option slows down UI and rendering. - Stable - vLabeler Path - Appearance - Scale degree display style - Numbered (1 2 3 4 5 6 7) - Off - Solfège (do re mi fa sol la ti) - Language - Show other tracks' notes on piano roll - Show icon on piano roll - Show portrait on piano roll - Singer name display language - Theme - Dark - Light - Use track color in UI - Cache - Clear cache on quit - DiffSinger Tensor Cache - Preferences - Note: please restart OpenUtau after changing this item. - Off - On - Oto Editor - Default Oto Editor - setParam Path - Paths - Additional Singer Path - Install to Additional Singer Path - Load all depth folders - Reset - Select - Set Pen Plus Tool as Default - Playback - Auto-Scroll - Auto-Scroll Mode - Page Scroll - Stationary Cursor - Audio Backend - Automatic - MiniAudio - Auto-Scroll Margin - Playback Device - On Pausing - Do nothing - Move cursor and view position back to where you started playing - Move only cursor back to where you started playing - Test - Rendering - Default renderer (for classic voicebanks) - DiffSinger Render Depth - DiffSinger Render Steps - GPU - Machine Learning Runner - Phase Compensation - Pre-render - Resampler - - Warning: moresampler is not fully supported. It may be slow and cause high CPU usage. If you insist, please: - 1. Set "resampler-compatibility" to "on" in moreconfig.txt. - 2. Set "auto-update-llsm-mrq" to "off" in moreconfig.txt. - 3. Patiently wait for moresampler to generate missing llsm files. - - - Note: to use external resamplers, please add the resampler DLL or EXE file in the Resamplers folder in the OpenUtau install path and choose it in Preferences. - - Skip muted tracks - - Warning: too many render threads may cause OpenUtau to run slowly. - - Maximum Render Threads - Wavtool - - Loading Singers... - Project saved. {0} - Waiting Rendering - - Singers - Edit In setParam - Edit In vLabeler - Goto Source File - Regenerate FRQ - Regenerating FRQ - Reset Otos - Save Otos - Search Alias - Download setParam (v4.0b or higher) from http://nwp8861.blog92.fc2.com/ and set setParam path in Preferences first! - Download vLabeler (1.0.0-beta1 or higher) from https://github.com/sdercolin/vlabeler and set vLabeler path in Preferences first! - Generate Singer Error Report - Location - Move Left - Move Right - Select Next - Select Prev - Show All - Zoom In - Zoom Out - Play sample - Publish Singer - Create an optimized zip package of your singer for distribution. - Ignore these file types during packaging (gitignore syntax): - Publish - Use file type ignoring - Open readme.txt - readme.txt not found. - Refresh - Set Default Phonemizer - Set Encoding - Set Icon - Set Portrait - Set Singer Type - Cancel - Clear - Color - Add Color - Remove Color - Rename - Edit Subbanks - Export prefix.map - Import prefix.map - Reset - Save - Select All - Set - Tone - Tone Ranges - Use filename as alias - Visit Website - - Archive File Encoding - Choose an encoding that make file names look right. - Back - Install - Please add the following settings to this voicebank's character.yaml. - Next - Singer Type - Text File Encoding - Choose an encoding that make file contents look right. - - Override Alias - Left Button Draw: Set expressions -Right Button Draw: Reset expressions - Expression not supported by renderer - Tab: Next Note - Shift+Tab: Previous Note - Selection Tool - Box Select: Select notes - Ctrl + Box Select: Select more notes - Up/Down: Transpose selected notes - Ctrl + Up/Down: Transpose selected notes by octave -Pen Tool - Left Button Draw: Add note - Right Click: Remove note - Right Button Draw: Remove multiple notes -General - T: Toggle tips - Drag Note: Move note - Drag End of Note: Resize note - Alt + Drag End of Note: Resize neighbouring notes - Space: Play - ◀ Scroll here to zoom horizontally ▶ - Scroll here to zoom vertically ▶ - - Duplicate Track - Duplicate Track Settings - Install singer - More - Move Down - Move Up - Mute - Mute all others - Mute this only - Unmute all - Open singers location - Open additional singers location - Remove - Rename track - Select Renderer - Select Singer - Solo - Solo additionally (which not removes solo from other tracks) - Solo this only (which removes solo from other tracks) - Unsolo all - Change track color - Track Settings - - Segoe UI,San Francisco,Helvetica Neue - - Check for Update - Open singing synthesis platform - GitHub - Update v{0} available! - Checking for updates... - Up to date - Unable to check update - Update - - Warning - Your OpenUtau home path "{0}" contains non-ASCII character. Exe resamplers may not work. - No settings for this renderer. - \ No newline at end of file +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; +using Newtonsoft.Json; +using OpenUtau.Core.Render; +using Serilog; + +namespace OpenUtau.Core.Util { + + public static class Preferences { + public static SerializablePreferences Default; + + static Preferences() { + Load(); + } + + public static void Save() { + try { + File.WriteAllText(PathManager.Inst.PrefsFilePath, + JsonConvert.SerializeObject(Default, Formatting.Indented), + Encoding.UTF8); + } catch (Exception e) { + Log.Error(e, "Failed to save prefs."); + } + } + + public static void Reset() { + Default = new SerializablePreferences(); + Save(); + } + + public static List GetSingerSearchPaths() { + return new List(Default.SingerSearchPaths); + } + + public static void SetSingerSearchPaths(List paths) { + Default.SingerSearchPaths = new List(paths); + Save(); + } + + public static void AddRecentFileIfEnabled(string filePath){ + //Users can choose adding .ust, .vsqx and .mid files to recent files or not + string ext = Path.GetExtension(filePath); + switch(ext){ + case ".ustx": + AddRecentFile(filePath); + break; + case ".mid": + case ".midi": + if(Preferences.Default.RememberMid){ + AddRecentFile(filePath); + } + break; + case ".ust": + if(Preferences.Default.RememberUst){ + AddRecentFile(filePath); + } + break; + case ".vsqx": + if(Preferences.Default.RememberVsqx){ + AddRecentFile(filePath); + } + break; + default: + break; + } + } + + private static void AddRecentFile(string filePath) { + if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) { + return; + } + var recent = Default.RecentFiles; + recent.RemoveAll(f => f == filePath); + recent.Insert(0, filePath); + recent.RemoveAll(f => string.IsNullOrEmpty(f) + || !File.Exists(f) + || f.Contains(PathManager.Inst.TemplatesPath)); + if (recent.Count > 16) { + recent.RemoveRange(16, recent.Count - 16); + } + Save(); + } + + private static void Load() { + try { + if (File.Exists(PathManager.Inst.PrefsFilePath)) { + Default = JsonConvert.DeserializeObject( + File.ReadAllText(PathManager.Inst.PrefsFilePath, Encoding.UTF8)); + if(Default == null) { + Reset(); + return; + } + + if (!ValidString(new Action(() => CultureInfo.GetCultureInfo(Default.Language)))) Default.Language = string.Empty; + if (!ValidString(new Action(() => CultureInfo.GetCultureInfo(Default.SortingOrder)))) Default.SortingOrder = string.Empty; + if (!Renderers.getRendererOptions().Contains(Default.DefaultRenderer)) Default.DefaultRenderer = string.Empty; + if (!Onnx.getRunnerOptions().Contains(Default.OnnxRunner)) Default.OnnxRunner = string.Empty; + } else { + Reset(); + } + } catch (Exception e) { + Log.Error(e, "Failed to load prefs."); + Default = new SerializablePreferences(); + } + } + + private static bool ValidString(Action action) { + try { + action(); + return true; + } catch { + return false; + } + } + + [Serializable] + public class SerializablePreferences { + public const int MidiWidth = 1024; + public const int MidiHeight = 768; + public int MainWidth = 1024; + public int MainHeight = 768; + public bool MainMaximized; + public bool MidiMaximized; + public int UndoLimit = 100; + public List SingerSearchPaths = new List(); + public string PlaybackDevice = string.Empty; + public int PlaybackDeviceNumber; + public int? PlaybackDeviceIndex; + public bool ShowPrefs = true; + public bool ShowTips = true; + public int Theme; + public bool PenPlusDefault = false; + public int DegreeStyle; + public bool UseTrackColor = false; + public bool ClearCacheOnQuit = false; + public bool PreRender = true; + public int NumRenderThreads = 2; + public string DefaultRenderer = string.Empty; + public int WorldlineR = 0; + public string OnnxRunner = string.Empty; + public int OnnxGpu = 0; + public double DiffSingerDepth = 1.0; + public int DiffSingerSteps = 20; + public bool DiffSingerTensorCache = true; + public bool SkipRenderingMutedTracks = false; + public string Language = string.Empty; + public string? SortingOrder = null; + public List RecentFiles = new List(); + public string SkipUpdate = string.Empty; + public string AdditionalSingerPath = string.Empty; + public bool InstallToAdditionalSingersPath = true; + public bool LoadDeepFolderSinger = true; + public bool PreferCommaSeparator = false; + public bool ResamplerLogging = false; + public List RecentSingers = new List(); + public List FavoriteSingers = new List(); + public Dictionary SingerPhonemizers = new Dictionary(); + public List RecentPhonemizers = new List(); + public bool PreferPortAudio = false; + public double PlayPosMarkerMargin = 0.9; + public int LockStartTime = 0; + public int PlaybackAutoScroll = 2; + public bool ReverseLogOrder = true; + public bool ShowPortrait = true; + public bool ShowIcon = true; + public bool ShowGhostNotes = true; + public bool PlayTone = true; + public bool ShowVibrato = true; + public bool ShowPitch = true; + public bool ShowFinalPitch = true; + public bool ShowWaveform = true; + public bool ShowPhoneme = true; + public bool ShowNoteParams = false; + public Dictionary DefaultResamplers = new Dictionary(); + public Dictionary DefaultWavtools = new Dictionary(); + public string LyricHelper = string.Empty; + public bool LyricsHelperBrackets = false; + public int OtoEditor = 0; + public string VLabelerPath = string.Empty; + public string SetParamPath = string.Empty; + public bool CheckForUpdateOnStart = true; + public bool Beta = false; + public bool RememberMid = false; + public bool RememberUst = true; + public bool RememberVsqx = true; + public int ImportTempo = 0; + public string PhoneticAssistant = string.Empty; + public string RecentOpenSingerDirectory = string.Empty; + public string RecentOpenProjectDirectory = string.Empty; + public bool LockUnselectedNotesPitch = true; + public bool LockUnselectedNotesVibrato = true; + public bool LockUnselectedNotesExpressions = true; + + public bool VoicebankPublishUseIgnore = true; + public string VoicebankPublishIgnores = "#Adobe Audition\n*.pkf\n\n#UTAU Engines\n*.ctspec\n*.d4c\n*.dio\n*.frc\n*.frt\n#*.frq\n*.harvest\n*.lessaudio\n*.llsm\n*.mrq\n*.pitchtier\n*.pkf\n*.platinum\n*.pmk\n*.star\n*.uspec\n*.vs4ufrq\n\n#UTAU related tools\n$read\n*.setParam-Scache\n*.lbp\n*.lbp.caches/*\n\n#OpenUtau\nerrors.txt\n*.sc.npz"; + } + } +} From 69831c4c6cc8c47819c211b7f74e719dd8fbceb7 Mon Sep 17 00:00:00 2001 From: RedBlackAka <140876408+RedBlackAka@users.noreply.github.com> Date: Thu, 19 Dec 2024 03:27:49 +0100 Subject: [PATCH 3/4] Fix line ending --- OpenUtau/Views/MainWindow.axaml.cs | 2710 ++++++++++++++-------------- 1 file changed, 1355 insertions(+), 1355 deletions(-) diff --git a/OpenUtau/Views/MainWindow.axaml.cs b/OpenUtau/Views/MainWindow.axaml.cs index ca890b246..5786c044e 100644 --- a/OpenUtau/Views/MainWindow.axaml.cs +++ b/OpenUtau/Views/MainWindow.axaml.cs @@ -1,1355 +1,1355 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -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.Controls; -using OpenUtau.App.ViewModels; -using OpenUtau.Classic; -using OpenUtau.Core; -using OpenUtau.Core.Analysis.Some; -using OpenUtau.Core.DiffSinger; -using OpenUtau.Core.Format; -using OpenUtau.Core.Ustx; -using OpenUtau.Core.Util; -using ReactiveUI; -using Serilog; -using Point = Avalonia.Point; - -namespace OpenUtau.App.Views { - public partial class MainWindow : Window, ICmdSubscriber { - private readonly KeyModifiers cmdKey = - OS.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control; - private readonly MainWindowViewModel viewModel; - - private PianoRollWindow? pianoRollWindow; - private bool openPianoRollWindow; - - private PartEditState? partEditState; - private readonly DispatcherTimer timer; - private readonly DispatcherTimer autosaveTimer; - private bool forceClose; - - private bool shouldOpenPartsContextMenu; - - private readonly ReactiveCommand PartRenameCommand; - private readonly ReactiveCommand PartGotoFileCommand; - private readonly ReactiveCommand PartReplaceAudioCommand; - private readonly ReactiveCommand PartTranscribeCommand; - - public MainWindow() { - Log.Information("Creating main window."); - InitializeComponent(); - Log.Information("Initialized main window component."); - DataContext = viewModel = new MainWindowViewModel(); - - viewModel.InitProject(); - viewModel.AddTempoChangeCmd = ReactiveCommand.Create(tick => AddTempoChange(tick)); - viewModel.DelTempoChangeCmd = ReactiveCommand.Create(tick => DelTempoChange(tick)); - viewModel.AddTimeSigChangeCmd = ReactiveCommand.Create(bar => AddTimeSigChange(bar)); - viewModel.DelTimeSigChangeCmd = ReactiveCommand.Create(bar => DelTimeSigChange(bar)); - - timer = new DispatcherTimer( - TimeSpan.FromMilliseconds(15), - DispatcherPriority.Normal, - (sender, args) => PlaybackManager.Inst.UpdatePlayPos()); - timer.Start(); - - autosaveTimer = new DispatcherTimer( - TimeSpan.FromSeconds(30), - DispatcherPriority.Normal, - (sender, args) => DocManager.Inst.AutoSave()); - autosaveTimer.Start(); - - PartRenameCommand = ReactiveCommand.Create(part => RenamePart(part)); - PartGotoFileCommand = ReactiveCommand.Create(part => GotoFile(part)); - PartReplaceAudioCommand = ReactiveCommand.Create(part => ReplaceAudio(part)); - PartTranscribeCommand = ReactiveCommand.Create(part => Transcribe(part)); - - AddHandler(DragDrop.DropEvent, OnDrop); - - DocManager.Inst.AddSubscriber(this); - - if (Preferences.Default.CheckForUpdateOnStart) { - Log.Information("Main window checking Update."); - UpdaterDialog.CheckForUpdate( - dialog => dialog.Show(this), - () => (Application.Current?.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown(), - TaskScheduler.FromCurrentSynchronizationContext()); - } - Log.Information("Created main window."); - } - - void OnEditTimeSignature(object sender, PointerPressedEventArgs args) { - var project = DocManager.Inst.Project; - var timeSig = project.timeSignatures[0]; - var dialog = new TimeSignatureDialog(timeSig.beatPerBar, timeSig.beatUnit); - dialog.OnOk = (beatPerBar, beatUnit) => { - viewModel.PlaybackViewModel.SetTimeSignature(beatPerBar, beatUnit); - }; - dialog.ShowDialog(this); - // Workaround for https://github.com/AvaloniaUI/Avalonia/issues/3986 - args.Pointer.Capture(null); - } - - void OnEditBpm(object sender, PointerPressedEventArgs args) { - var project = DocManager.Inst.Project; - var dialog = new TypeInDialog(); - dialog.Title = "BPM"; - dialog.SetText(project.tempos[0].bpm.ToString()); - dialog.onFinish = s => { - if (double.TryParse(s, out double bpm)) { - viewModel.PlaybackViewModel.SetBpm(bpm); - } - }; - dialog.ShowDialog(this); - // Workaround for https://github.com/AvaloniaUI/Avalonia/issues/3986 - args.Pointer.Capture(null); - } - - private void AddTempoChange(int tick) { - var project = DocManager.Inst.Project; - var dialog = new TypeInDialog { - Title = "BPM" - }; - dialog.SetText(project.tempos[0].bpm.ToString()); - dialog.onFinish = s => { - if (double.TryParse(s, out double bpm)) { - DocManager.Inst.StartUndoGroup(); - DocManager.Inst.ExecuteCmd(new AddTempoChangeCommand( - project, tick, bpm)); - DocManager.Inst.EndUndoGroup(); - } - }; - dialog.ShowDialog(this); - } - - private void DelTempoChange(int tick) { - var project = DocManager.Inst.Project; - DocManager.Inst.StartUndoGroup(); - DocManager.Inst.ExecuteCmd(new DelTempoChangeCommand(project, tick)); - DocManager.Inst.EndUndoGroup(); - } - - - - void OnMenuRemapTimeaxis(object sender, RoutedEventArgs e) { - var project = DocManager.Inst.Project; - var dialog = new TypeInDialog { - Title = ThemeManager.GetString("menu.project.remaptimeaxis") - }; - dialog.Height = 200; - dialog.SetPrompt(ThemeManager.GetString("dialogs.remaptimeaxis.message")); - dialog.SetText(project.tempos[0].bpm.ToString()); - dialog.onFinish = s => { - try { - if (double.TryParse(s, out double bpm)) { - DocManager.Inst.StartUndoGroup(); - var oldTimeAxis = project.timeAxis.Clone(); - DocManager.Inst.ExecuteCmd(new BpmCommand( - project, bpm)); - foreach (var tempo in project.tempos.Skip(1)) { - DocManager.Inst.ExecuteCmd(new DelTempoChangeCommand( - project, tempo.position)); - } - viewModel.RemapTimeAxis(oldTimeAxis, project.timeAxis.Clone()); - DocManager.Inst.EndUndoGroup(); - } - } catch (Exception e) { - Log.Error(e, "Failed to open project location"); - MessageBox.ShowError(this, new MessageCustomizableException("Failed to open project location", ": project location", e)); - } - }; - dialog.ShowDialog(this); - } - - private void AddTimeSigChange(int bar) { - var project = DocManager.Inst.Project; - var timeSig = project.timeAxis.TimeSignatureAtBar(bar); - var dialog = new TimeSignatureDialog(timeSig.beatPerBar, timeSig.beatUnit); - dialog.OnOk = (beatPerBar, beatUnit) => { - DocManager.Inst.StartUndoGroup(); - DocManager.Inst.ExecuteCmd(new AddTimeSigCommand( - project, bar, dialog.BeatPerBar, dialog.BeatUnit)); - DocManager.Inst.EndUndoGroup(); - }; - dialog.ShowDialog(this); - } - - private void DelTimeSigChange(int bar) { - var project = DocManager.Inst.Project; - DocManager.Inst.StartUndoGroup(); - DocManager.Inst.ExecuteCmd(new DelTimeSigCommand(project, bar)); - DocManager.Inst.EndUndoGroup(); - } - - void OnMenuNew(object sender, RoutedEventArgs args) => NewProject(); - async void NewProject() { - if (!DocManager.Inst.ChangesSaved && !await AskIfSaveAndContinue()) { - return; - } - viewModel.NewProject(); - } - - void OnMenuOpen(object sender, RoutedEventArgs args) => Open(); - async void Open() { - if (!DocManager.Inst.ChangesSaved && !await AskIfSaveAndContinue()) { - return; - } - var files = await FilePicker.OpenFilesAboutProject( - this, "menu.file.open", - FilePicker.ProjectFiles, - FilePicker.USTX, - FilePicker.VSQX, - FilePicker.UST, - FilePicker.MIDI, - FilePicker.UFDATA); - if (files == null || files.Length == 0) { - return; - } - try { - viewModel.OpenProject(files); - } catch (Exception e) { - Log.Error(e, $"Failed to open files {string.Join("\n", files)}"); - _ = await MessageBox.ShowError(this, new MessageCustomizableException($"Failed to open files {string.Join("\n", files)}", $":\n{string.Join("\n", files)}", e)); - } - } - - void OnMainMenuOpened(object sender, RoutedEventArgs args) { - viewModel.RefreshOpenRecent(); - viewModel.RefreshTemplates(); - viewModel.RefreshCacheSize(); - } - - void OnMainMenuClosed(object sender, RoutedEventArgs args) { - Focus(); // Force unfocus menu for key down events. - } - - void OnMainMenuPointerLeave(object sender, PointerEventArgs args) { - Focus(); // Force unfocus menu for key down events. - } - - void OnMenuOpenProjectLocation(object sender, RoutedEventArgs args) { - var project = DocManager.Inst.Project; - if (string.IsNullOrEmpty(project.FilePath) || !project.Saved) { - MessageBox.Show( - this, - ThemeManager.GetString("dialogs.export.savefirst"), - ThemeManager.GetString("errors.caption"), - MessageBox.MessageBoxButtons.Ok); - } - try { - var dir = Path.GetDirectoryName(project.FilePath); - if (dir != null) { - OS.OpenFolder(dir); - } else { - Log.Error($"Failed to get project location from {dir}."); - } - } catch (Exception e) { - Log.Error(e, "Failed to open project location."); - MessageBox.ShowError(this, new MessageCustomizableException("Failed to open project location.", ": project location", e)); - } - } - - async void OnMenuSave(object sender, RoutedEventArgs args) => await Save(); - public async Task Save() { - if (!viewModel.ProjectSaved) { - await SaveAs(); - } else { - viewModel.SaveProject(); - string message = ThemeManager.GetString("progress.saved"); - message = string.Format(message, DateTime.Now); - DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, message)); - } - } - - async void OnMenuSaveAs(object sender, RoutedEventArgs args) => await SaveAs(); - async Task SaveAs() { - var file = await FilePicker.SaveFileAboutProject( - this, "menu.file.saveas", FilePicker.USTX); - if (!string.IsNullOrEmpty(file)) { - viewModel.SaveProject(file); - } - } - - void OnMenuSaveTemplate(object sender, RoutedEventArgs args) { - var project = DocManager.Inst.Project; - var dialog = new TypeInDialog(); - dialog.Title = ThemeManager.GetString("menu.file.savetemplate"); - dialog.SetText("default"); - dialog.onFinish = file => { - if (string.IsNullOrEmpty(file)) { - return; - } - file = Path.GetFileNameWithoutExtension(file); - file = $"{file}.ustx"; - file = Path.Combine(PathManager.Inst.TemplatesPath, file); - Ustx.Save(file, project.CloneAsTemplate()); - }; - dialog.ShowDialog(this); - } - - async void OnMenuImportTracks(object sender, RoutedEventArgs args) { - var files = await FilePicker.OpenFilesAboutProject( - this, "menu.file.importtracks", - FilePicker.ProjectFiles, - FilePicker.USTX, - FilePicker.VSQX, - FilePicker.UST, - FilePicker.MIDI, - FilePicker.UFDATA); - if (files == null || files.Length == 0) { - return; - } - try { - var loadedProjects = Formats.ReadProjects(files); - if (loadedProjects == null || loadedProjects.Length == 0) { - return; - } - bool importTempo = true; - switch (Preferences.Default.ImportTempo) { - case 1: - importTempo = false; - break; - case 2: - if (loadedProjects[0].tempos.Count == 0) { - importTempo = false; - break; - } - var tempoString = String.Join("\n", - loadedProjects[0].tempos - .Select(tempo => $"position: {tempo.position}, tempo: {tempo.bpm}") - ); - //ask the user - var result = await MessageBox.Show( - this, - ThemeManager.GetString("dialogs.importtracks.importtempo") + "\n" + tempoString, - ThemeManager.GetString("dialogs.importtracks.caption"), - MessageBox.MessageBoxButtons.YesNo); - if (result == MessageBox.MessageBoxResult.No) { - importTempo = false; - } - break; - } - viewModel.ImportTracks(loadedProjects, importTempo); - } catch (Exception e) { - Log.Error(e, $"Failed to import files"); - _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import files", "", e)); - } - ValidateTracksVoiceColor(); - } - - async void OnMenuImportAudio(object sender, RoutedEventArgs args) { - var file = await FilePicker.OpenFileAboutProject( - this, "menu.file.importaudio", FilePicker.AudioFiles); - if (file == null) { - return; - } - try { - viewModel.ImportAudio(file); - } catch (Exception e) { - Log.Error(e, "Failed to import audio"); - _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import audio", "", e)); - } - } - - async void OnMenuImportMidi(object sender, RoutedEventArgs args) { - var file = await FilePicker.OpenFileAboutProject( - this, "menu.file.importmidi", FilePicker.MIDI); - if (file == null) { - return; - } - try { - viewModel.ImportMidi(file); - } catch (Exception e) { - Log.Error(e, "Failed to import midi"); - _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import midi", "", e)); - } - } - - async void OnMenuExportMixdown(object sender, RoutedEventArgs args) { - var project = DocManager.Inst.Project; - var file = await FilePicker.SaveFileAboutProject( - this, "menu.file.exportmixdown", FilePicker.WAV); - if (!string.IsNullOrEmpty(file)) { - await PlaybackManager.Inst.RenderMixdown(project, file); - } - } - - async void OnMenuExportWav(object sender, RoutedEventArgs args) { - var project = DocManager.Inst.Project; - if (await WarnToSave(project)) { - var name = Path.GetFileNameWithoutExtension(project.FilePath); - var path = Path.GetDirectoryName(project.FilePath); - path = Path.Combine(path!, "Export", $"{name}.wav"); - await PlaybackManager.Inst.RenderToFiles(project, path); - } - } - - async void OnMenuExportWavTo(object sender, RoutedEventArgs args) { - var project = DocManager.Inst.Project; - var file = await FilePicker.SaveFileAboutProject( - this, "menu.file.exportwavto", FilePicker.WAV); - if (!string.IsNullOrEmpty(file)) { - await PlaybackManager.Inst.RenderToFiles(project, file); - } - } - - async void OnMenuExportDsTo(object sender, RoutedEventArgs e) { - var project = DocManager.Inst.Project; - var file = await FilePicker.SaveFileAboutProject( - this, "menu.file.exportds", FilePicker.DS); - if (!string.IsNullOrEmpty(file)) { - for (var i = 0; i < project.parts.Count; i++) { - var part = project.parts[i]; - if (part is UVoicePart voicePart) { - var savePath = PathManager.Inst.GetPartSavePath(file, voicePart.DisplayName, i)[..^4] + ".ds"; - DiffSingerScript.SavePart(project, voicePart, savePath); - DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); - } - } - } - } - - async void OnMenuExportDsV2To(object sender, RoutedEventArgs e) { - var project = DocManager.Inst.Project; - var file = await FilePicker.SaveFileAboutProject( - this, "menu.file.exportds.v2", FilePicker.DS); - if (!string.IsNullOrEmpty(file)) { - for (var i = 0; i < project.parts.Count; i++) { - var part = project.parts[i]; - if (part is UVoicePart voicePart) { - var savePath = PathManager.Inst.GetPartSavePath(file, voicePart.DisplayName, i)[..^4] + ".ds"; - DiffSingerScript.SavePart(project, voicePart, savePath, true); - DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); - } - } - } - } - - async void OnMenuExportDsV2WithoutPitchTo(object sender, RoutedEventArgs e) { - var project = DocManager.Inst.Project; - var file = await FilePicker.SaveFileAboutProject( - this, "menu.file.exportds.v2withoutpitch", FilePicker.DS); - if (!string.IsNullOrEmpty(file)) { - for (var i = 0; i < project.parts.Count; i++) { - var part = project.parts[i]; - if (part is UVoicePart voicePart) { - var savePath = PathManager.Inst.GetPartSavePath(file, voicePart.DisplayName, i)[..^4] + ".ds"; - DiffSingerScript.SavePart(project, voicePart, savePath, true, false); - DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); - } - } - } - } - - async void OnMenuExportUst(object sender, RoutedEventArgs e) { - var project = DocManager.Inst.Project; - if (await WarnToSave(project)) { - var name = Path.GetFileNameWithoutExtension(project.FilePath); - var path = Path.GetDirectoryName(project.FilePath); - path = Path.Combine(path!, "Export", $"{name}.ust"); - for (var i = 0; i < project.parts.Count; i++) { - var part = project.parts[i]; - if (part is UVoicePart voicePart) { - var savePath = PathManager.Inst.GetPartSavePath(path, voicePart.DisplayName, i); - Ust.SavePart(project, voicePart, savePath); - DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); - } - } - } - } - - async void OnMenuExportUstTo(object sender, RoutedEventArgs e) { - var project = DocManager.Inst.Project; - var file = await FilePicker.SaveFileAboutProject( - this, "menu.file.exportustto", FilePicker.UST); - if (!string.IsNullOrEmpty(file)) { - for (var i = 0; i < project.parts.Count; i++) { - var part = project.parts[i]; - if (part is UVoicePart voicePart) { - var savePath = PathManager.Inst.GetPartSavePath(file, voicePart.DisplayName, i); - Ust.SavePart(project, voicePart, savePath); - DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); - } - } - } - } - - async void OnMenuExportMidi(object sender, RoutedEventArgs e) { - var project = DocManager.Inst.Project; - var file = await FilePicker.SaveFileAboutProject( - this, "menu.file.exportmidi", FilePicker.MIDI); - if (!string.IsNullOrEmpty(file)) { - MidiWriter.Save(file, project); - } - } - - private async Task WarnToSave(UProject project) { - if (string.IsNullOrEmpty(project.FilePath)) { - await MessageBox.Show( - this, - ThemeManager.GetString("dialogs.export.savefirst"), - ThemeManager.GetString("dialogs.export.caption"), - MessageBox.MessageBoxButtons.Ok); - return false; - } - return true; - } - - void OnMenuUndo(object sender, RoutedEventArgs args) => viewModel.Undo(); - void OnMenuRedo(object sender, RoutedEventArgs args) => viewModel.Redo(); - - void OnMenuExpressionss(object sender, RoutedEventArgs args) { - var dialog = new ExpressionsDialog() { - DataContext = new ExpressionsViewModel(), - }; - dialog.ShowDialog(this); - if (dialog.Position.Y < 0) { - dialog.Position = dialog.Position.WithY(0); - } - } - - void OnMenuSingers(object sender, RoutedEventArgs args) { - OpenSingersWindow(); - } - - /// - /// Check if a track has a singer and if it exists. - /// If the user haven't selected a singer for the track, or the singer specified in ustx project doesn't exist, return null. - /// Otherwise, return the singer. - /// - public USinger? TrackSingerIfFound(UTrack track) { - if (track.Singer?.Found ?? false) { - return track.Singer; - } - return null; - } - - public void OpenSingersWindow() { - var lifetime = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; - if (lifetime == null) { - return; - } - - MessageBox.ShowLoading(this); - var dialog = lifetime.Windows.FirstOrDefault(w => w is SingersDialog); - try { - if (dialog == null) { - USinger? singer = null; - if (viewModel.TracksViewModel.SelectedParts.Count > 0) { - singer = TrackSingerIfFound(viewModel.TracksViewModel.Tracks[viewModel.TracksViewModel.SelectedParts.First().trackNo]); - } - if (singer == null && viewModel.TracksViewModel.Tracks.Count > 0) { - singer = TrackSingerIfFound(viewModel.TracksViewModel.Tracks.First()); - } - var vm = new SingersViewModel(); - if (singer != null) { - vm.Singer = singer; - } - dialog = new SingersDialog() { DataContext = vm }; - dialog.Show(); - } - if (dialog.Position.Y < 0) { - dialog.Position = dialog.Position.WithY(0); - } - } catch (Exception e) { - DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); - } finally { - MessageBox.CloseLoading(); - } - if (dialog != null) { - dialog.Activate(); - } - } - - async void OnMenuInstallSinger(object sender, RoutedEventArgs args) { - var file = await FilePicker.OpenFileAboutSinger( - this, "menu.tools.singer.install", FilePicker.ArchiveFiles); - if (file == null) { - return; - } - try { - if (file.EndsWith(Core.Vogen.VogenSingerInstaller.FileExt)) { - Core.Vogen.VogenSingerInstaller.Install(file); - return; - } - if (file.EndsWith(DependencyInstaller.FileExt)) { - DependencyInstaller.Install(file); - return; - } - - var setup = new SingerSetupDialog() { - DataContext = new SingerSetupViewModel() { - ArchiveFilePath = file, - }, - }; - _ = setup.ShowDialog(this); - if (setup.Position.Y < 0) { - setup.Position = setup.Position.WithY(0); - } - } catch (Exception e) { - Log.Error(e, $"Failed to install singer {file}"); - MessageCustomizableException mce; - if (e is MessageCustomizableException) { - mce = (MessageCustomizableException)e; - } else { - mce = new MessageCustomizableException($"Failed to install singer {file}", $": {file}", e); - } - _ = await MessageBox.ShowError(this, mce); - } - } - - async void OnMenuInstallDependency(object sender, RoutedEventArgs args) { - var file = await FilePicker.OpenFile( - this, "menu.tools.dependency.install", FilePicker.OUDEP); - if (file == null) { - return; - } - if (file.EndsWith(DependencyInstaller.FileExt)) { - DependencyInstaller.Install(file); - return; - } - } - - void OnMenuPreferences(object sender, RoutedEventArgs args) { - PreferencesViewModel dataContext; - try { - dataContext = new PreferencesViewModel(); - } catch (Exception e) { - Log.Error(e, "Failed to load prefs. Initialize it."); - MessageBox.ShowError(this, new MessageCustomizableException("Failed to load prefs. Initialize it.", "", e)); - Preferences.Reset(); - dataContext = new PreferencesViewModel(); - } - var dialog = new PreferencesDialog() { - DataContext = dataContext - }; - dialog.ShowDialog(this); - if (dialog.Position.Y < 0) { - dialog.Position = dialog.Position.WithY(0); - } - } - - void OnMenuClearCache(object sender, RoutedEventArgs args) { - Task.Run(() => { - DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, "Clearing cache...")); - PathManager.Inst.ClearCache(); - DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, "Cache cleared.")); - }); - } - - void OnMenuDebugWindow(object sender, RoutedEventArgs args) { - var desktop = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; - if (desktop == null) { - return; - } - var window = desktop.Windows.FirstOrDefault(w => w is DebugWindow); - if (window == null) { - window = new DebugWindow(); - } - window.Show(); - } - - void OnMenuPhoneticAssistant(object sender, RoutedEventArgs args) { - var desktop = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; - if (desktop == null) { - return; - } - var window = desktop.Windows.FirstOrDefault(w => w is PhoneticAssistant); - if (window == null) { - window = new PhoneticAssistant(); - } - window.Show(); - } - - void OnMenuCheckUpdate(object sender, RoutedEventArgs args) { - var dialog = new UpdaterDialog(); - dialog.ViewModel.CloseApplication = - () => (Application.Current?.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown(); - dialog.ShowDialog(this); - } - - void OnMenuLogsLocation(object sender, RoutedEventArgs args) { - try { - OS.OpenFolder(PathManager.Inst.LogsPath); - } catch (Exception e) { - DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); - } - } - - void OnMenuReportIssue(object sender, RoutedEventArgs args) { - try { - OS.OpenWeb("https://github.com/stakira/OpenUtau/issues"); - } catch (Exception e) { - DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); - } - } - - void OnMenuWiki(object sender, RoutedEventArgs args) { - try { - OS.OpenWeb("https://github.com/stakira/OpenUtau/wiki/Getting-Started"); - } catch (Exception e) { - DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); - } - } - - void OnMenuLayoutVSplit11(object sender, RoutedEventArgs args) => LayoutSplit(null, 1.0 / 2); - void OnMenuLayoutVSplit12(object sender, RoutedEventArgs args) => LayoutSplit(null, 1.0 / 3); - void OnMenuLayoutVSplit13(object sender, RoutedEventArgs args) => LayoutSplit(null, 1.0 / 4); - void OnMenuLayoutHSplit11(object sender, RoutedEventArgs args) => LayoutSplit(1.0 / 2, null); - void OnMenuLayoutHSplit12(object sender, RoutedEventArgs args) => LayoutSplit(1.0 / 3, null); - void OnMenuLayoutHSplit13(object sender, RoutedEventArgs args) => LayoutSplit(1.0 / 4, null); - - private void LayoutSplit(double? x, double? y) { - if (Screens.Primary == null) { - return; - } - var wa = Screens.Primary.WorkingArea; - WindowState = WindowState.Normal; - double titleBarHeight = 20; - if (FrameSize != null) { - double borderThickness = (FrameSize!.Value.Width - ClientSize.Width) / 2; - titleBarHeight = FrameSize!.Value.Height - ClientSize.Height - borderThickness; - } - Position = new PixelPoint(0, 0); - Width = x != null ? wa.Size.Width * x.Value : wa.Size.Width; - Height = (y != null ? wa.Size.Height * y.Value : wa.Size.Height) - titleBarHeight; - if (pianoRollWindow != null) { - pianoRollWindow.Position = new PixelPoint(x != null ? (int)Width : 0, y != null ? (int)(Height + (OS.IsMacOS() ? 25 : titleBarHeight)) : 0); - pianoRollWindow.Width = x != null ? wa.Size.Width - Width : wa.Size.Width; - pianoRollWindow.Height = (y != null ? wa.Size.Height - (Height + titleBarHeight) : wa.Size.Height) - titleBarHeight; - } - } - - void OnKeyDown(object sender, 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; - 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; - } - } - } - - void OnPointerPressed(object? sender, PointerPressedEventArgs args) { - if (!args.Handled && args.ClickCount == 1) { - FocusManager?.ClearFocus(); - } - } - - async void OnDrop(object? sender, DragEventArgs args) { - var storageItem = args.Data?.GetFiles()?.FirstOrDefault(); - if (storageItem == null) { - return; - } - string file = storageItem.Path.LocalPath; - var ext = Path.GetExtension(file); - if (ext == ".ustx" || ext == ".ust" || ext == ".vsqx" || ext == ".ufdata") { - if (!DocManager.Inst.ChangesSaved && !await AskIfSaveAndContinue()) { - return; - } - try { - viewModel.OpenProject(new string[] { file }); - } catch (Exception e) { - Log.Error(e, $"Failed to open file {file}"); - _ = await MessageBox.ShowError(this, new MessageCustomizableException($"Failed to open file {file}", $": {file}", e)); - } - } else if (ext == ".mid" || ext == ".midi") { - try { - viewModel.ImportMidi(file); - } catch (Exception e) { - Log.Error(e, "Failed to import midi"); - _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import midi", "", e)); - } - } else if (ext == ".zip" || ext == ".rar" || ext == ".uar") { - try { - var setup = new SingerSetupDialog() { - DataContext = new SingerSetupViewModel() { - ArchiveFilePath = file, - }, - }; - _ = setup.ShowDialog(this); - if (setup.Position.Y < 0) { - setup.Position = setup.Position.WithY(0); - } - } catch (Exception e) { - Log.Error(e, $"Failed to install singer {file}"); - MessageCustomizableException mce; - if (e is MessageCustomizableException) { - mce = (MessageCustomizableException)e; - } else { - mce = new MessageCustomizableException($"Failed to install singer {file}", $": {file}", e); - } - _ = await MessageBox.ShowError(this, mce); - } - } else if (ext == Core.Vogen.VogenSingerInstaller.FileExt) { - Core.Vogen.VogenSingerInstaller.Install(file); - } else if (ext == ".dll") { - var result = await MessageBox.Show( - this, - ThemeManager.GetString("dialogs.installdll.message") + file, - ThemeManager.GetString("dialogs.installdll.caption"), - MessageBox.MessageBoxButtons.OkCancel); - if (result == MessageBox.MessageBoxResult.Ok) { - Core.Api.PhonemizerInstaller.Install(file); - } - } else if (ext == ".exe") { - var setup = new ExeSetupDialog() { - DataContext = new ExeSetupViewModel(file) - }; - _ = setup.ShowDialog(this); - if (setup.Position.Y < 0) { - setup.Position = setup.Position.WithY(0); - } - } else if (ext == DependencyInstaller.FileExt) { - var result = await MessageBox.Show( - this, - ThemeManager.GetString("dialogs.installdependency.message") + file, - ThemeManager.GetString("dialogs.installdependency.caption"), - MessageBox.MessageBoxButtons.OkCancel); - if (result == MessageBox.MessageBoxResult.Ok) { - DependencyInstaller.Install(file); - } - } else if (ext == ".mp3" || ext == ".wav" || ext == ".ogg" || ext == ".flac") { - try { - viewModel.ImportAudio(file); - } catch (Exception e) { - Log.Error(e, "Failed to import audio"); - _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import audio", "", e)); - } - } else { - _ = await MessageBox.Show( - this, - ThemeManager.GetString("dialogs.unsupportedfile.message") + ext, - ThemeManager.GetString("dialogs.unsupportedfile.caption"), - MessageBox.MessageBoxButtons.Ok); - } - } - - void OnPlayOrPause(object sender, RoutedEventArgs args) { - PlayOrPause(); - } - - void PlayOrPause() { - viewModel.PlaybackViewModel.PlayOrPause(); - } - - 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)); - } - - 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)); - } - - 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.TracksViewModel.OnXZoomed(position, 0.1 * args.Delta.Y); - } - - public void ViewScalerPointerWheelChanged(object sender, PointerWheelEventArgs args) { - viewModel.TracksViewModel.OnYZoomed(new Point(0, 0.5), 0.1 * args.Delta.Y); - } - - 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.TracksViewModel.PointToLineTick(point.Position, out int left, out int right); - viewModel.PlaybackViewModel.MovePlayPos(left); - } else if (point.Properties.IsRightButtonPressed) { - int tick = viewModel.TracksViewModel.PointToTick(point.Position); - viewModel.RefreshTimelineContextMenu(tick); - } - } - - public void TimelinePointerMoved(object sender, PointerEventArgs args) { - var control = (Control)sender; - var point = args.GetCurrentPoint(control); - if (point.Properties.IsLeftButtonPressed) { - viewModel.TracksViewModel.PointToLineTick(point.Position, out int left, out int right); - viewModel.PlaybackViewModel.MovePlayPos(left); - } - } - - public void TimelinePointerReleased(object sender, PointerReleasedEventArgs args) { - args.Pointer.Capture(null); - } - - public void PartsCanvasPointerPressed(object sender, PointerPressedEventArgs args) { - var control = (Control)sender; - var point = args.GetCurrentPoint(control); - var hitControl = control.InputHitTest(point.Position); - if (partEditState != null) { - return; - } - if (point.Properties.IsLeftButtonPressed) { - if (args.KeyModifiers == cmdKey) { - partEditState = new PartSelectionEditState(control, viewModel, SelectionBox); - Cursor = ViewConstants.cursorCross; - } else if (hitControl == control) { - viewModel.TracksViewModel.DeselectParts(); - var part = viewModel.TracksViewModel.MaybeAddPart(point.Position); - if (part != null) { - // Start moving right away - partEditState = new PartMoveEditState(control, viewModel, part); - Cursor = ViewConstants.cursorSizeAll; - } - } else if (hitControl is PartControl partControl) { - bool isVoice = partControl.part is UVoicePart; - bool isWave = partControl.part is UWavePart; - bool trim = point.Position.X > partControl.Bounds.Right - ViewConstants.ResizeMargin; - bool skip = point.Position.X < partControl.Bounds.Left + ViewConstants.ResizeMargin; - if (isVoice && trim) { - partEditState = new PartResizeEditState(control, viewModel, partControl.part); - Cursor = ViewConstants.cursorSizeWE; - } else if (isWave && skip) { - // TODO - } else if (isWave && trim) { - // TODO - } else { - partEditState = new PartMoveEditState(control, viewModel, partControl.part); - Cursor = ViewConstants.cursorSizeAll; - } - } - } else if (point.Properties.IsRightButtonPressed) { - if (hitControl is PartControl partControl) { - if (!viewModel.TracksViewModel.SelectedParts.Contains(partControl.part)) { - viewModel.TracksViewModel.DeselectParts(); - viewModel.TracksViewModel.SelectPart(partControl.part); - } - if (PartsContextMenu != null && viewModel.TracksViewModel.SelectedParts.Count > 0) { - PartsContextMenu.DataContext = new PartsContextMenuArgs { - Part = partControl.part, - PartDeleteCommand = viewModel.PartDeleteCommand, - PartGotoFileCommand = PartGotoFileCommand, - PartReplaceAudioCommand = PartReplaceAudioCommand, - PartRenameCommand = PartRenameCommand, - PartTranscribeCommand = PartTranscribeCommand, - }; - shouldOpenPartsContextMenu = true; - } - } else { - viewModel.TracksViewModel.DeselectParts(); - } - } else if (point.Properties.IsMiddleButtonPressed) { - partEditState = new PartPanningState(control, viewModel); - Cursor = ViewConstants.cursorHand; - } - if (partEditState != null) { - partEditState.Begin(point.Pointer, point.Position); - partEditState.Update(point.Pointer, point.Position); - } - } - - public void PartsCanvasPointerMoved(object sender, PointerEventArgs args) { - var control = (Control)sender; - var point = args.GetCurrentPoint(control); - if (partEditState != null) { - partEditState.Update(point.Pointer, point.Position); - return; - } - var hitControl = control.InputHitTest(point.Position); - if (hitControl is PartControl partControl) { - bool isVoice = partControl.part is UVoicePart; - bool isWave = partControl.part is UWavePart; - bool trim = point.Position.X > partControl.Bounds.Right - ViewConstants.ResizeMargin; - bool skip = point.Position.X < partControl.Bounds.Left + ViewConstants.ResizeMargin; - if (isVoice && trim) { - Cursor = ViewConstants.cursorSizeWE; - } else if (isWave && (skip || trim)) { - Cursor = null; // TODO - } else { - Cursor = null; - } - } else { - Cursor = null; - } - } - - public void PartsCanvasPointerReleased(object sender, PointerReleasedEventArgs args) { - if (partEditState != null) { - if (partEditState.MouseButton != args.InitialPressMouseButton) { - return; - } - var control = (Control)sender; - var point = args.GetCurrentPoint(control); - partEditState.Update(point.Pointer, point.Position); - partEditState.End(point.Pointer, point.Position); - partEditState = null; - Cursor = null; - } - if (openPianoRollWindow) { - pianoRollWindow?.Show(); - pianoRollWindow?.Activate(); - openPianoRollWindow = false; - } - } - - public void PartsCanvasDoubleTapped(object sender, TappedEventArgs args) { - if (!(sender is Canvas canvas)) { - return; - } - var control = canvas.InputHitTest(args.GetPosition(canvas)); - if (control is PartControl partControl && partControl.part is UVoicePart) { - if (pianoRollWindow == null) { - MessageBox.ShowLoading(this); - pianoRollWindow = new PianoRollWindow() { - MainWindow = this, - }; - pianoRollWindow.ViewModel.PlaybackViewModel = viewModel.PlaybackViewModel; - MessageBox.CloseLoading(); - } - // Workaround for new window losing focus. - openPianoRollWindow = true; - int tick = viewModel.TracksViewModel.PointToTick(args.GetPosition(canvas)); - DocManager.Inst.ExecuteCmd(new LoadPartNotification(partControl.part, DocManager.Inst.Project, tick)); - pianoRollWindow.AttachExpressions(); - } - } - - public void PartsCanvasPointerWheelChanged(object sender, PointerWheelEventArgs args) { - 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) { - ViewScalerPointerWheelChanged(VScaler, args); - } else if (args.KeyModifiers == cmdKey) { - TimelinePointerWheelChanged(TimelineCanvas, args); - } - if (partEditState != null) { - var point = args.GetCurrentPoint(partEditState.control); - partEditState.Update(point.Pointer, point.Position); - } - } - - public void PartsContextMenuOpening(object sender, CancelEventArgs args) { - if (shouldOpenPartsContextMenu) { - shouldOpenPartsContextMenu = false; - } else { - args.Cancel = true; - } - } - - public void PartsContextMenuClosing(object sender, CancelEventArgs args) { - if (PartsContextMenu != null) { - PartsContextMenu.DataContext = null; - } - } - - void RenamePart(UPart part) { - var dialog = new TypeInDialog(); - dialog.Title = ThemeManager.GetString("context.part.rename"); - dialog.SetText(part.name); - dialog.onFinish = name => { - if (!string.IsNullOrWhiteSpace(name) && name != part.name) { - if (!string.IsNullOrWhiteSpace(name) && name != part.name) { - DocManager.Inst.StartUndoGroup(); - DocManager.Inst.ExecuteCmd(new RenamePartCommand(DocManager.Inst.Project, part, name)); - DocManager.Inst.EndUndoGroup(); - } - } - }; - dialog.ShowDialog(this); - } - - void GotoFile(UPart part) { - //View the location of the audio file in explorer if the part is a wave part - if (part is UWavePart wavePart) { - try { - OS.GotoFile(wavePart.FilePath); - } catch (Exception e) { - DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); - } - } - } - - async void ReplaceAudio(UPart part) { - var file = await FilePicker.OpenFileAboutProject( - this, "context.part.replaceaudio", FilePicker.AudioFiles); - if (file == null) { - return; - } - UWavePart newPart = new UWavePart() { - FilePath = file, - trackNo = part.trackNo, - position = part.position - }; - newPart.Load(DocManager.Inst.Project); - DocManager.Inst.StartUndoGroup(); - DocManager.Inst.ExecuteCmd(new ReplacePartCommand(DocManager.Inst.Project, part, newPart)); - DocManager.Inst.EndUndoGroup(); - } - - void Transcribe(UPart part) { - //Convert audio to notes - if (part is UWavePart wavePart) { - try { - string text = ThemeManager.GetString("context.part.transcribing"); - var msgbox = MessageBox.ShowModal(this, $"{text} {part.name}", text); - //Duration of the wave file in seconds - int wavDurS = (int)(wavePart.fileDurationMs / 1000.0); - var scheduler = TaskScheduler.FromCurrentSynchronizationContext(); - var transcribeTask = Task.Run(() => { - using (var some = new Some()) { - return some.Transcribe(DocManager.Inst.Project, wavePart, wavPosS => { - //msgbox?.SetText($"{text} {part.name}\n{wavPosS}/{wavDurS}"); - msgbox.SetText(string.Format("{0} {1}\n{2}s / {3}s", text, part.name, wavPosS, wavDurS)); - }); - } - }); - transcribeTask.ContinueWith(task => { - msgbox?.Close(); - if (task.IsFaulted) { - Log.Error(task.Exception, $"Failed to transcribe part {part.name}"); - MessageBox.ShowError(this, task.Exception); - return; - } - var voicePart = task.Result; - //Add voicePart into project - if (voicePart != null) { - var project = DocManager.Inst.Project; - var track = new UTrack(project); - track.TrackNo = project.tracks.Count; - voicePart.trackNo = track.TrackNo; - DocManager.Inst.StartUndoGroup(); - DocManager.Inst.ExecuteCmd(new AddTrackCommand(project, track)); - DocManager.Inst.ExecuteCmd(new AddPartCommand(project, voicePart)); - DocManager.Inst.EndUndoGroup(); - } - }, scheduler); - } catch (Exception e) { - Log.Error(e, $"Failed to transcribe part {part.name}"); - MessageBox.ShowError(this, e); - } - } - } - - async void ValidateTracksVoiceColor() { - DocManager.Inst.StartUndoGroup(); - foreach (var track in DocManager.Inst.Project.tracks) { - if (track.ValidateVoiceColor(out var oldColors, out var newColors)) { - await VoiceColorRemappingAsync(track, oldColors, newColors); - } - } - DocManager.Inst.EndUndoGroup(); - } - async Task VoiceColorRemappingAsync(UTrack track, string[] oldColors, string[] newColors) { - var parts = DocManager.Inst.Project.parts - .Where(part => part.trackNo == track.TrackNo && part is UVoicePart) - .Cast() - .Where(vpart => vpart.notes.Count > 0); - if (parts.Any()) { - var dialog = new VoiceColorMappingDialog(); - VoiceColorMappingViewModel vm = new VoiceColorMappingViewModel(oldColors, newColors, track.TrackName); - dialog.DataContext = vm; - await dialog.ShowDialog(this); - - if (dialog.Apply) { - SetVoiceColorRemapping(track, parts, vm); - } - } - } - void VoiceColorRemapping(UTrack track, string[] oldColors, string[] newColors) { - var parts = DocManager.Inst.Project.parts - .Where(part => part.trackNo == track.TrackNo && part is UVoicePart) - .Cast() - .Where(vpart => vpart.notes.Count > 0); - if (parts.Any()) { - var dialog = new VoiceColorMappingDialog(); - VoiceColorMappingViewModel vm = new VoiceColorMappingViewModel(oldColors, newColors, track.TrackName); - dialog.DataContext = vm; - dialog.onFinish = () => { - DocManager.Inst.StartUndoGroup(); - SetVoiceColorRemapping(track, parts, vm); - DocManager.Inst.EndUndoGroup(); - }; - dialog.ShowDialog(this); - } - } - void SetVoiceColorRemapping(UTrack track, IEnumerable parts, VoiceColorMappingViewModel vm) { - foreach (var part in parts) { - foreach (var phoneme in part.phonemes) { - var tuple = phoneme.GetExpression(DocManager.Inst.Project, track, Ustx.CLR); - if (vm.ColorMappings.Any(m => m.OldIndex == tuple.Item1)) { - var mapping = vm.ColorMappings.First(m => m.OldIndex == tuple.Item1); - if (mapping.OldIndex != mapping.SelectedIndex) { - if (mapping.SelectedIndex == 0) { - DocManager.Inst.ExecuteCmd(new SetPhonemeExpressionCommand(DocManager.Inst.Project, track, part, phoneme, Ustx.CLR, null)); - } else { - DocManager.Inst.ExecuteCmd(new SetPhonemeExpressionCommand(DocManager.Inst.Project, track, part, phoneme, Ustx.CLR, mapping.SelectedIndex)); - } - } - } else { - DocManager.Inst.ExecuteCmd(new SetPhonemeExpressionCommand(DocManager.Inst.Project, track, part, phoneme, Ustx.CLR, null)); - } - } - } - } - - public async void WindowClosing(object? sender, WindowClosingEventArgs e) { - if (forceClose || DocManager.Inst.ChangesSaved) { - if (Preferences.Default.ClearCacheOnQuit) { - Log.Information("Clearing cache..."); - PathManager.Inst.ClearCache(); - Log.Information("Cache cleared."); - } - return; - } - e.Cancel = true; - if (!await AskIfSaveAndContinue()) { - return; - } - pianoRollWindow?.Close(); - forceClose = true; - Close(); - } - - private async Task AskIfSaveAndContinue() { - var result = await MessageBox.Show( - this, - ThemeManager.GetString("dialogs.exitsave.message"), - ThemeManager.GetString("dialogs.exitsave.caption"), - MessageBox.MessageBoxButtons.YesNoCancel); - switch (result) { - case MessageBox.MessageBoxResult.Yes: - await Save(); - goto case MessageBox.MessageBoxResult.No; - case MessageBox.MessageBoxResult.No: - return true; // Continue. - default: - return false; // Cancel. - } - } - - public void OnNext(UCommand cmd, bool isUndo) { - if (cmd is ErrorMessageNotification notif) { - switch (notif.e) { - case Core.Render.NoResamplerException: - case Core.Render.NoWavtoolException: - MessageBox.Show( - this, - ThemeManager.GetString("dialogs.noresampler.message"), - ThemeManager.GetString("dialogs.noresampler.caption"), - MessageBox.MessageBoxButtons.Ok); - break; - default: - MessageBox.ShowError(this, notif.e, notif.message, true); - break; - } - } else if (cmd is LoadingNotification loadingNotif && loadingNotif.window == typeof(MainWindow)) { - if (loadingNotif.startLoading) { - MessageBox.ShowLoading(this); - } else { - MessageBox.CloseLoading(); - } - } else if (cmd is VoiceColorRemappingNotification voicecolorNotif) { - if (voicecolorNotif.TrackNo < 0 || DocManager.Inst.Project.tracks.Count <= voicecolorNotif.TrackNo) { - ValidateTracksVoiceColor(); - } else { - UTrack track = DocManager.Inst.Project.tracks[voicecolorNotif.TrackNo]; - if (!voicecolorNotif.Validate) { - VoiceColorRemapping(track, track.VoiceColorNames, track.VoiceColorExp.options); - } else if (track.ValidateVoiceColor(out var oldColors, out var newColors)) { - VoiceColorRemapping(track, oldColors, newColors); - } - } - } - } - } -} +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +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.Controls; +using OpenUtau.App.ViewModels; +using OpenUtau.Classic; +using OpenUtau.Core; +using OpenUtau.Core.Analysis.Some; +using OpenUtau.Core.DiffSinger; +using OpenUtau.Core.Format; +using OpenUtau.Core.Ustx; +using OpenUtau.Core.Util; +using ReactiveUI; +using Serilog; +using Point = Avalonia.Point; + +namespace OpenUtau.App.Views { + public partial class MainWindow : Window, ICmdSubscriber { + private readonly KeyModifiers cmdKey = + OS.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control; + private readonly MainWindowViewModel viewModel; + + private PianoRollWindow? pianoRollWindow; + private bool openPianoRollWindow; + + private PartEditState? partEditState; + private readonly DispatcherTimer timer; + private readonly DispatcherTimer autosaveTimer; + private bool forceClose; + + private bool shouldOpenPartsContextMenu; + + private readonly ReactiveCommand PartRenameCommand; + private readonly ReactiveCommand PartGotoFileCommand; + private readonly ReactiveCommand PartReplaceAudioCommand; + private readonly ReactiveCommand PartTranscribeCommand; + + public MainWindow() { + Log.Information("Creating main window."); + InitializeComponent(); + Log.Information("Initialized main window component."); + DataContext = viewModel = new MainWindowViewModel(); + + viewModel.InitProject(); + viewModel.AddTempoChangeCmd = ReactiveCommand.Create(tick => AddTempoChange(tick)); + viewModel.DelTempoChangeCmd = ReactiveCommand.Create(tick => DelTempoChange(tick)); + viewModel.AddTimeSigChangeCmd = ReactiveCommand.Create(bar => AddTimeSigChange(bar)); + viewModel.DelTimeSigChangeCmd = ReactiveCommand.Create(bar => DelTimeSigChange(bar)); + + timer = new DispatcherTimer( + TimeSpan.FromMilliseconds(15), + DispatcherPriority.Normal, + (sender, args) => PlaybackManager.Inst.UpdatePlayPos()); + timer.Start(); + + autosaveTimer = new DispatcherTimer( + TimeSpan.FromSeconds(30), + DispatcherPriority.Normal, + (sender, args) => DocManager.Inst.AutoSave()); + autosaveTimer.Start(); + + PartRenameCommand = ReactiveCommand.Create(part => RenamePart(part)); + PartGotoFileCommand = ReactiveCommand.Create(part => GotoFile(part)); + PartReplaceAudioCommand = ReactiveCommand.Create(part => ReplaceAudio(part)); + PartTranscribeCommand = ReactiveCommand.Create(part => Transcribe(part)); + + AddHandler(DragDrop.DropEvent, OnDrop); + + DocManager.Inst.AddSubscriber(this); + + if (Preferences.Default.CheckForUpdateOnStart) { + Log.Information("Main window checking Update."); + UpdaterDialog.CheckForUpdate( + dialog => dialog.Show(this), + () => (Application.Current?.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown(), + TaskScheduler.FromCurrentSynchronizationContext()); + } + Log.Information("Created main window."); + } + + void OnEditTimeSignature(object sender, PointerPressedEventArgs args) { + var project = DocManager.Inst.Project; + var timeSig = project.timeSignatures[0]; + var dialog = new TimeSignatureDialog(timeSig.beatPerBar, timeSig.beatUnit); + dialog.OnOk = (beatPerBar, beatUnit) => { + viewModel.PlaybackViewModel.SetTimeSignature(beatPerBar, beatUnit); + }; + dialog.ShowDialog(this); + // Workaround for https://github.com/AvaloniaUI/Avalonia/issues/3986 + args.Pointer.Capture(null); + } + + void OnEditBpm(object sender, PointerPressedEventArgs args) { + var project = DocManager.Inst.Project; + var dialog = new TypeInDialog(); + dialog.Title = "BPM"; + dialog.SetText(project.tempos[0].bpm.ToString()); + dialog.onFinish = s => { + if (double.TryParse(s, out double bpm)) { + viewModel.PlaybackViewModel.SetBpm(bpm); + } + }; + dialog.ShowDialog(this); + // Workaround for https://github.com/AvaloniaUI/Avalonia/issues/3986 + args.Pointer.Capture(null); + } + + private void AddTempoChange(int tick) { + var project = DocManager.Inst.Project; + var dialog = new TypeInDialog { + Title = "BPM" + }; + dialog.SetText(project.tempos[0].bpm.ToString()); + dialog.onFinish = s => { + if (double.TryParse(s, out double bpm)) { + DocManager.Inst.StartUndoGroup(); + DocManager.Inst.ExecuteCmd(new AddTempoChangeCommand( + project, tick, bpm)); + DocManager.Inst.EndUndoGroup(); + } + }; + dialog.ShowDialog(this); + } + + private void DelTempoChange(int tick) { + var project = DocManager.Inst.Project; + DocManager.Inst.StartUndoGroup(); + DocManager.Inst.ExecuteCmd(new DelTempoChangeCommand(project, tick)); + DocManager.Inst.EndUndoGroup(); + } + + + + void OnMenuRemapTimeaxis(object sender, RoutedEventArgs e) { + var project = DocManager.Inst.Project; + var dialog = new TypeInDialog { + Title = ThemeManager.GetString("menu.project.remaptimeaxis") + }; + dialog.Height = 200; + dialog.SetPrompt(ThemeManager.GetString("dialogs.remaptimeaxis.message")); + dialog.SetText(project.tempos[0].bpm.ToString()); + dialog.onFinish = s => { + try { + if (double.TryParse(s, out double bpm)) { + DocManager.Inst.StartUndoGroup(); + var oldTimeAxis = project.timeAxis.Clone(); + DocManager.Inst.ExecuteCmd(new BpmCommand( + project, bpm)); + foreach (var tempo in project.tempos.Skip(1)) { + DocManager.Inst.ExecuteCmd(new DelTempoChangeCommand( + project, tempo.position)); + } + viewModel.RemapTimeAxis(oldTimeAxis, project.timeAxis.Clone()); + DocManager.Inst.EndUndoGroup(); + } + } catch (Exception e) { + Log.Error(e, "Failed to open project location"); + MessageBox.ShowError(this, new MessageCustomizableException("Failed to open project location", ": project location", e)); + } + }; + dialog.ShowDialog(this); + } + + private void AddTimeSigChange(int bar) { + var project = DocManager.Inst.Project; + var timeSig = project.timeAxis.TimeSignatureAtBar(bar); + var dialog = new TimeSignatureDialog(timeSig.beatPerBar, timeSig.beatUnit); + dialog.OnOk = (beatPerBar, beatUnit) => { + DocManager.Inst.StartUndoGroup(); + DocManager.Inst.ExecuteCmd(new AddTimeSigCommand( + project, bar, dialog.BeatPerBar, dialog.BeatUnit)); + DocManager.Inst.EndUndoGroup(); + }; + dialog.ShowDialog(this); + } + + private void DelTimeSigChange(int bar) { + var project = DocManager.Inst.Project; + DocManager.Inst.StartUndoGroup(); + DocManager.Inst.ExecuteCmd(new DelTimeSigCommand(project, bar)); + DocManager.Inst.EndUndoGroup(); + } + + void OnMenuNew(object sender, RoutedEventArgs args) => NewProject(); + async void NewProject() { + if (!DocManager.Inst.ChangesSaved && !await AskIfSaveAndContinue()) { + return; + } + viewModel.NewProject(); + } + + void OnMenuOpen(object sender, RoutedEventArgs args) => Open(); + async void Open() { + if (!DocManager.Inst.ChangesSaved && !await AskIfSaveAndContinue()) { + return; + } + var files = await FilePicker.OpenFilesAboutProject( + this, "menu.file.open", + FilePicker.ProjectFiles, + FilePicker.USTX, + FilePicker.VSQX, + FilePicker.UST, + FilePicker.MIDI, + FilePicker.UFDATA); + if (files == null || files.Length == 0) { + return; + } + try { + viewModel.OpenProject(files); + } catch (Exception e) { + Log.Error(e, $"Failed to open files {string.Join("\n", files)}"); + _ = await MessageBox.ShowError(this, new MessageCustomizableException($"Failed to open files {string.Join("\n", files)}", $":\n{string.Join("\n", files)}", e)); + } + } + + void OnMainMenuOpened(object sender, RoutedEventArgs args) { + viewModel.RefreshOpenRecent(); + viewModel.RefreshTemplates(); + viewModel.RefreshCacheSize(); + } + + void OnMainMenuClosed(object sender, RoutedEventArgs args) { + Focus(); // Force unfocus menu for key down events. + } + + void OnMainMenuPointerLeave(object sender, PointerEventArgs args) { + Focus(); // Force unfocus menu for key down events. + } + + void OnMenuOpenProjectLocation(object sender, RoutedEventArgs args) { + var project = DocManager.Inst.Project; + if (string.IsNullOrEmpty(project.FilePath) || !project.Saved) { + MessageBox.Show( + this, + ThemeManager.GetString("dialogs.export.savefirst"), + ThemeManager.GetString("errors.caption"), + MessageBox.MessageBoxButtons.Ok); + } + try { + var dir = Path.GetDirectoryName(project.FilePath); + if (dir != null) { + OS.OpenFolder(dir); + } else { + Log.Error($"Failed to get project location from {dir}."); + } + } catch (Exception e) { + Log.Error(e, "Failed to open project location."); + MessageBox.ShowError(this, new MessageCustomizableException("Failed to open project location.", ": project location", e)); + } + } + + async void OnMenuSave(object sender, RoutedEventArgs args) => await Save(); + public async Task Save() { + if (!viewModel.ProjectSaved) { + await SaveAs(); + } else { + viewModel.SaveProject(); + string message = ThemeManager.GetString("progress.saved"); + message = string.Format(message, DateTime.Now); + DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, message)); + } + } + + async void OnMenuSaveAs(object sender, RoutedEventArgs args) => await SaveAs(); + async Task SaveAs() { + var file = await FilePicker.SaveFileAboutProject( + this, "menu.file.saveas", FilePicker.USTX); + if (!string.IsNullOrEmpty(file)) { + viewModel.SaveProject(file); + } + } + + void OnMenuSaveTemplate(object sender, RoutedEventArgs args) { + var project = DocManager.Inst.Project; + var dialog = new TypeInDialog(); + dialog.Title = ThemeManager.GetString("menu.file.savetemplate"); + dialog.SetText("default"); + dialog.onFinish = file => { + if (string.IsNullOrEmpty(file)) { + return; + } + file = Path.GetFileNameWithoutExtension(file); + file = $"{file}.ustx"; + file = Path.Combine(PathManager.Inst.TemplatesPath, file); + Ustx.Save(file, project.CloneAsTemplate()); + }; + dialog.ShowDialog(this); + } + + async void OnMenuImportTracks(object sender, RoutedEventArgs args) { + var files = await FilePicker.OpenFilesAboutProject( + this, "menu.file.importtracks", + FilePicker.ProjectFiles, + FilePicker.USTX, + FilePicker.VSQX, + FilePicker.UST, + FilePicker.MIDI, + FilePicker.UFDATA); + if (files == null || files.Length == 0) { + return; + } + try { + var loadedProjects = Formats.ReadProjects(files); + if (loadedProjects == null || loadedProjects.Length == 0) { + return; + } + bool importTempo = true; + switch (Preferences.Default.ImportTempo) { + case 1: + importTempo = false; + break; + case 2: + if (loadedProjects[0].tempos.Count == 0) { + importTempo = false; + break; + } + var tempoString = String.Join("\n", + loadedProjects[0].tempos + .Select(tempo => $"position: {tempo.position}, tempo: {tempo.bpm}") + ); + //ask the user + var result = await MessageBox.Show( + this, + ThemeManager.GetString("dialogs.importtracks.importtempo") + "\n" + tempoString, + ThemeManager.GetString("dialogs.importtracks.caption"), + MessageBox.MessageBoxButtons.YesNo); + if (result == MessageBox.MessageBoxResult.No) { + importTempo = false; + } + break; + } + viewModel.ImportTracks(loadedProjects, importTempo); + } catch (Exception e) { + Log.Error(e, $"Failed to import files"); + _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import files", "", e)); + } + ValidateTracksVoiceColor(); + } + + async void OnMenuImportAudio(object sender, RoutedEventArgs args) { + var file = await FilePicker.OpenFileAboutProject( + this, "menu.file.importaudio", FilePicker.AudioFiles); + if (file == null) { + return; + } + try { + viewModel.ImportAudio(file); + } catch (Exception e) { + Log.Error(e, "Failed to import audio"); + _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import audio", "", e)); + } + } + + async void OnMenuImportMidi(object sender, RoutedEventArgs args) { + var file = await FilePicker.OpenFileAboutProject( + this, "menu.file.importmidi", FilePicker.MIDI); + if (file == null) { + return; + } + try { + viewModel.ImportMidi(file); + } catch (Exception e) { + Log.Error(e, "Failed to import midi"); + _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import midi", "", e)); + } + } + + async void OnMenuExportMixdown(object sender, RoutedEventArgs args) { + var project = DocManager.Inst.Project; + var file = await FilePicker.SaveFileAboutProject( + this, "menu.file.exportmixdown", FilePicker.WAV); + if (!string.IsNullOrEmpty(file)) { + await PlaybackManager.Inst.RenderMixdown(project, file); + } + } + + async void OnMenuExportWav(object sender, RoutedEventArgs args) { + var project = DocManager.Inst.Project; + if (await WarnToSave(project)) { + var name = Path.GetFileNameWithoutExtension(project.FilePath); + var path = Path.GetDirectoryName(project.FilePath); + path = Path.Combine(path!, "Export", $"{name}.wav"); + await PlaybackManager.Inst.RenderToFiles(project, path); + } + } + + async void OnMenuExportWavTo(object sender, RoutedEventArgs args) { + var project = DocManager.Inst.Project; + var file = await FilePicker.SaveFileAboutProject( + this, "menu.file.exportwavto", FilePicker.WAV); + if (!string.IsNullOrEmpty(file)) { + await PlaybackManager.Inst.RenderToFiles(project, file); + } + } + + async void OnMenuExportDsTo(object sender, RoutedEventArgs e) { + var project = DocManager.Inst.Project; + var file = await FilePicker.SaveFileAboutProject( + this, "menu.file.exportds", FilePicker.DS); + if (!string.IsNullOrEmpty(file)) { + for (var i = 0; i < project.parts.Count; i++) { + var part = project.parts[i]; + if (part is UVoicePart voicePart) { + var savePath = PathManager.Inst.GetPartSavePath(file, voicePart.DisplayName, i)[..^4] + ".ds"; + DiffSingerScript.SavePart(project, voicePart, savePath); + DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); + } + } + } + } + + async void OnMenuExportDsV2To(object sender, RoutedEventArgs e) { + var project = DocManager.Inst.Project; + var file = await FilePicker.SaveFileAboutProject( + this, "menu.file.exportds.v2", FilePicker.DS); + if (!string.IsNullOrEmpty(file)) { + for (var i = 0; i < project.parts.Count; i++) { + var part = project.parts[i]; + if (part is UVoicePart voicePart) { + var savePath = PathManager.Inst.GetPartSavePath(file, voicePart.DisplayName, i)[..^4] + ".ds"; + DiffSingerScript.SavePart(project, voicePart, savePath, true); + DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); + } + } + } + } + + async void OnMenuExportDsV2WithoutPitchTo(object sender, RoutedEventArgs e) { + var project = DocManager.Inst.Project; + var file = await FilePicker.SaveFileAboutProject( + this, "menu.file.exportds.v2withoutpitch", FilePicker.DS); + if (!string.IsNullOrEmpty(file)) { + for (var i = 0; i < project.parts.Count; i++) { + var part = project.parts[i]; + if (part is UVoicePart voicePart) { + var savePath = PathManager.Inst.GetPartSavePath(file, voicePart.DisplayName, i)[..^4] + ".ds"; + DiffSingerScript.SavePart(project, voicePart, savePath, true, false); + DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); + } + } + } + } + + async void OnMenuExportUst(object sender, RoutedEventArgs e) { + var project = DocManager.Inst.Project; + if (await WarnToSave(project)) { + var name = Path.GetFileNameWithoutExtension(project.FilePath); + var path = Path.GetDirectoryName(project.FilePath); + path = Path.Combine(path!, "Export", $"{name}.ust"); + for (var i = 0; i < project.parts.Count; i++) { + var part = project.parts[i]; + if (part is UVoicePart voicePart) { + var savePath = PathManager.Inst.GetPartSavePath(path, voicePart.DisplayName, i); + Ust.SavePart(project, voicePart, savePath); + DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); + } + } + } + } + + async void OnMenuExportUstTo(object sender, RoutedEventArgs e) { + var project = DocManager.Inst.Project; + var file = await FilePicker.SaveFileAboutProject( + this, "menu.file.exportustto", FilePicker.UST); + if (!string.IsNullOrEmpty(file)) { + for (var i = 0; i < project.parts.Count; i++) { + var part = project.parts[i]; + if (part is UVoicePart voicePart) { + var savePath = PathManager.Inst.GetPartSavePath(file, voicePart.DisplayName, i); + Ust.SavePart(project, voicePart, savePath); + DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"{savePath}.")); + } + } + } + } + + async void OnMenuExportMidi(object sender, RoutedEventArgs e) { + var project = DocManager.Inst.Project; + var file = await FilePicker.SaveFileAboutProject( + this, "menu.file.exportmidi", FilePicker.MIDI); + if (!string.IsNullOrEmpty(file)) { + MidiWriter.Save(file, project); + } + } + + private async Task WarnToSave(UProject project) { + if (string.IsNullOrEmpty(project.FilePath)) { + await MessageBox.Show( + this, + ThemeManager.GetString("dialogs.export.savefirst"), + ThemeManager.GetString("dialogs.export.caption"), + MessageBox.MessageBoxButtons.Ok); + return false; + } + return true; + } + + void OnMenuUndo(object sender, RoutedEventArgs args) => viewModel.Undo(); + void OnMenuRedo(object sender, RoutedEventArgs args) => viewModel.Redo(); + + void OnMenuExpressionss(object sender, RoutedEventArgs args) { + var dialog = new ExpressionsDialog() { + DataContext = new ExpressionsViewModel(), + }; + dialog.ShowDialog(this); + if (dialog.Position.Y < 0) { + dialog.Position = dialog.Position.WithY(0); + } + } + + void OnMenuSingers(object sender, RoutedEventArgs args) { + OpenSingersWindow(); + } + + /// + /// Check if a track has a singer and if it exists. + /// If the user haven't selected a singer for the track, or the singer specified in ustx project doesn't exist, return null. + /// Otherwise, return the singer. + /// + public USinger? TrackSingerIfFound(UTrack track) { + if (track.Singer?.Found ?? false) { + return track.Singer; + } + return null; + } + + public void OpenSingersWindow() { + var lifetime = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; + if (lifetime == null) { + return; + } + + MessageBox.ShowLoading(this); + var dialog = lifetime.Windows.FirstOrDefault(w => w is SingersDialog); + try { + if (dialog == null) { + USinger? singer = null; + if (viewModel.TracksViewModel.SelectedParts.Count > 0) { + singer = TrackSingerIfFound(viewModel.TracksViewModel.Tracks[viewModel.TracksViewModel.SelectedParts.First().trackNo]); + } + if (singer == null && viewModel.TracksViewModel.Tracks.Count > 0) { + singer = TrackSingerIfFound(viewModel.TracksViewModel.Tracks.First()); + } + var vm = new SingersViewModel(); + if (singer != null) { + vm.Singer = singer; + } + dialog = new SingersDialog() { DataContext = vm }; + dialog.Show(); + } + if (dialog.Position.Y < 0) { + dialog.Position = dialog.Position.WithY(0); + } + } catch (Exception e) { + DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); + } finally { + MessageBox.CloseLoading(); + } + if (dialog != null) { + dialog.Activate(); + } + } + + async void OnMenuInstallSinger(object sender, RoutedEventArgs args) { + var file = await FilePicker.OpenFileAboutSinger( + this, "menu.tools.singer.install", FilePicker.ArchiveFiles); + if (file == null) { + return; + } + try { + if (file.EndsWith(Core.Vogen.VogenSingerInstaller.FileExt)) { + Core.Vogen.VogenSingerInstaller.Install(file); + return; + } + if (file.EndsWith(DependencyInstaller.FileExt)) { + DependencyInstaller.Install(file); + return; + } + + var setup = new SingerSetupDialog() { + DataContext = new SingerSetupViewModel() { + ArchiveFilePath = file, + }, + }; + _ = setup.ShowDialog(this); + if (setup.Position.Y < 0) { + setup.Position = setup.Position.WithY(0); + } + } catch (Exception e) { + Log.Error(e, $"Failed to install singer {file}"); + MessageCustomizableException mce; + if (e is MessageCustomizableException) { + mce = (MessageCustomizableException)e; + } else { + mce = new MessageCustomizableException($"Failed to install singer {file}", $": {file}", e); + } + _ = await MessageBox.ShowError(this, mce); + } + } + + async void OnMenuInstallDependency(object sender, RoutedEventArgs args) { + var file = await FilePicker.OpenFile( + this, "menu.tools.dependency.install", FilePicker.OUDEP); + if (file == null) { + return; + } + if (file.EndsWith(DependencyInstaller.FileExt)) { + DependencyInstaller.Install(file); + return; + } + } + + void OnMenuPreferences(object sender, RoutedEventArgs args) { + PreferencesViewModel dataContext; + try { + dataContext = new PreferencesViewModel(); + } catch (Exception e) { + Log.Error(e, "Failed to load prefs. Initialize it."); + MessageBox.ShowError(this, new MessageCustomizableException("Failed to load prefs. Initialize it.", "", e)); + Preferences.Reset(); + dataContext = new PreferencesViewModel(); + } + var dialog = new PreferencesDialog() { + DataContext = dataContext + }; + dialog.ShowDialog(this); + if (dialog.Position.Y < 0) { + dialog.Position = dialog.Position.WithY(0); + } + } + + void OnMenuClearCache(object sender, RoutedEventArgs args) { + Task.Run(() => { + DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, "Clearing cache...")); + PathManager.Inst.ClearCache(); + DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, "Cache cleared.")); + }); + } + + void OnMenuDebugWindow(object sender, RoutedEventArgs args) { + var desktop = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; + if (desktop == null) { + return; + } + var window = desktop.Windows.FirstOrDefault(w => w is DebugWindow); + if (window == null) { + window = new DebugWindow(); + } + window.Show(); + } + + void OnMenuPhoneticAssistant(object sender, RoutedEventArgs args) { + var desktop = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime; + if (desktop == null) { + return; + } + var window = desktop.Windows.FirstOrDefault(w => w is PhoneticAssistant); + if (window == null) { + window = new PhoneticAssistant(); + } + window.Show(); + } + + void OnMenuCheckUpdate(object sender, RoutedEventArgs args) { + var dialog = new UpdaterDialog(); + dialog.ViewModel.CloseApplication = + () => (Application.Current?.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown(); + dialog.ShowDialog(this); + } + + void OnMenuLogsLocation(object sender, RoutedEventArgs args) { + try { + OS.OpenFolder(PathManager.Inst.LogsPath); + } catch (Exception e) { + DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); + } + } + + void OnMenuReportIssue(object sender, RoutedEventArgs args) { + try { + OS.OpenWeb("https://github.com/stakira/OpenUtau/issues"); + } catch (Exception e) { + DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); + } + } + + void OnMenuWiki(object sender, RoutedEventArgs args) { + try { + OS.OpenWeb("https://github.com/stakira/OpenUtau/wiki/Getting-Started"); + } catch (Exception e) { + DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); + } + } + + void OnMenuLayoutVSplit11(object sender, RoutedEventArgs args) => LayoutSplit(null, 1.0 / 2); + void OnMenuLayoutVSplit12(object sender, RoutedEventArgs args) => LayoutSplit(null, 1.0 / 3); + void OnMenuLayoutVSplit13(object sender, RoutedEventArgs args) => LayoutSplit(null, 1.0 / 4); + void OnMenuLayoutHSplit11(object sender, RoutedEventArgs args) => LayoutSplit(1.0 / 2, null); + void OnMenuLayoutHSplit12(object sender, RoutedEventArgs args) => LayoutSplit(1.0 / 3, null); + void OnMenuLayoutHSplit13(object sender, RoutedEventArgs args) => LayoutSplit(1.0 / 4, null); + + private void LayoutSplit(double? x, double? y) { + if (Screens.Primary == null) { + return; + } + var wa = Screens.Primary.WorkingArea; + WindowState = WindowState.Normal; + double titleBarHeight = 20; + if (FrameSize != null) { + double borderThickness = (FrameSize!.Value.Width - ClientSize.Width) / 2; + titleBarHeight = FrameSize!.Value.Height - ClientSize.Height - borderThickness; + } + Position = new PixelPoint(0, 0); + Width = x != null ? wa.Size.Width * x.Value : wa.Size.Width; + Height = (y != null ? wa.Size.Height * y.Value : wa.Size.Height) - titleBarHeight; + if (pianoRollWindow != null) { + pianoRollWindow.Position = new PixelPoint(x != null ? (int)Width : 0, y != null ? (int)(Height + (OS.IsMacOS() ? 25 : titleBarHeight)) : 0); + pianoRollWindow.Width = x != null ? wa.Size.Width - Width : wa.Size.Width; + pianoRollWindow.Height = (y != null ? wa.Size.Height - (Height + titleBarHeight) : wa.Size.Height) - titleBarHeight; + } + } + + void OnKeyDown(object sender, 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; + 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; + } + } + } + + void OnPointerPressed(object? sender, PointerPressedEventArgs args) { + if (!args.Handled && args.ClickCount == 1) { + FocusManager?.ClearFocus(); + } + } + + async void OnDrop(object? sender, DragEventArgs args) { + var storageItem = args.Data?.GetFiles()?.FirstOrDefault(); + if (storageItem == null) { + return; + } + string file = storageItem.Path.LocalPath; + var ext = Path.GetExtension(file); + if (ext == ".ustx" || ext == ".ust" || ext == ".vsqx" || ext == ".ufdata") { + if (!DocManager.Inst.ChangesSaved && !await AskIfSaveAndContinue()) { + return; + } + try { + viewModel.OpenProject(new string[] { file }); + } catch (Exception e) { + Log.Error(e, $"Failed to open file {file}"); + _ = await MessageBox.ShowError(this, new MessageCustomizableException($"Failed to open file {file}", $": {file}", e)); + } + } else if (ext == ".mid" || ext == ".midi") { + try { + viewModel.ImportMidi(file); + } catch (Exception e) { + Log.Error(e, "Failed to import midi"); + _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import midi", "", e)); + } + } else if (ext == ".zip" || ext == ".rar" || ext == ".uar") { + try { + var setup = new SingerSetupDialog() { + DataContext = new SingerSetupViewModel() { + ArchiveFilePath = file, + }, + }; + _ = setup.ShowDialog(this); + if (setup.Position.Y < 0) { + setup.Position = setup.Position.WithY(0); + } + } catch (Exception e) { + Log.Error(e, $"Failed to install singer {file}"); + MessageCustomizableException mce; + if (e is MessageCustomizableException) { + mce = (MessageCustomizableException)e; + } else { + mce = new MessageCustomizableException($"Failed to install singer {file}", $": {file}", e); + } + _ = await MessageBox.ShowError(this, mce); + } + } else if (ext == Core.Vogen.VogenSingerInstaller.FileExt) { + Core.Vogen.VogenSingerInstaller.Install(file); + } else if (ext == ".dll") { + var result = await MessageBox.Show( + this, + ThemeManager.GetString("dialogs.installdll.message") + file, + ThemeManager.GetString("dialogs.installdll.caption"), + MessageBox.MessageBoxButtons.OkCancel); + if (result == MessageBox.MessageBoxResult.Ok) { + Core.Api.PhonemizerInstaller.Install(file); + } + } else if (ext == ".exe") { + var setup = new ExeSetupDialog() { + DataContext = new ExeSetupViewModel(file) + }; + _ = setup.ShowDialog(this); + if (setup.Position.Y < 0) { + setup.Position = setup.Position.WithY(0); + } + } else if (ext == DependencyInstaller.FileExt) { + var result = await MessageBox.Show( + this, + ThemeManager.GetString("dialogs.installdependency.message") + file, + ThemeManager.GetString("dialogs.installdependency.caption"), + MessageBox.MessageBoxButtons.OkCancel); + if (result == MessageBox.MessageBoxResult.Ok) { + DependencyInstaller.Install(file); + } + } else if (ext == ".mp3" || ext == ".wav" || ext == ".ogg" || ext == ".flac") { + try { + viewModel.ImportAudio(file); + } catch (Exception e) { + Log.Error(e, "Failed to import audio"); + _ = await MessageBox.ShowError(this, new MessageCustomizableException("Failed to import audio", "", e)); + } + } else { + _ = await MessageBox.Show( + this, + ThemeManager.GetString("dialogs.unsupportedfile.message") + ext, + ThemeManager.GetString("dialogs.unsupportedfile.caption"), + MessageBox.MessageBoxButtons.Ok); + } + } + + void OnPlayOrPause(object sender, RoutedEventArgs args) { + PlayOrPause(); + } + + void PlayOrPause() { + viewModel.PlaybackViewModel.PlayOrPause(); + } + + 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)); + } + + 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)); + } + + 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.TracksViewModel.OnXZoomed(position, 0.1 * args.Delta.Y); + } + + public void ViewScalerPointerWheelChanged(object sender, PointerWheelEventArgs args) { + viewModel.TracksViewModel.OnYZoomed(new Point(0, 0.5), 0.1 * args.Delta.Y); + } + + 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.TracksViewModel.PointToLineTick(point.Position, out int left, out int right); + viewModel.PlaybackViewModel.MovePlayPos(left); + } else if (point.Properties.IsRightButtonPressed) { + int tick = viewModel.TracksViewModel.PointToTick(point.Position); + viewModel.RefreshTimelineContextMenu(tick); + } + } + + public void TimelinePointerMoved(object sender, PointerEventArgs args) { + var control = (Control)sender; + var point = args.GetCurrentPoint(control); + if (point.Properties.IsLeftButtonPressed) { + viewModel.TracksViewModel.PointToLineTick(point.Position, out int left, out int right); + viewModel.PlaybackViewModel.MovePlayPos(left); + } + } + + public void TimelinePointerReleased(object sender, PointerReleasedEventArgs args) { + args.Pointer.Capture(null); + } + + public void PartsCanvasPointerPressed(object sender, PointerPressedEventArgs args) { + var control = (Control)sender; + var point = args.GetCurrentPoint(control); + var hitControl = control.InputHitTest(point.Position); + if (partEditState != null) { + return; + } + if (point.Properties.IsLeftButtonPressed) { + if (args.KeyModifiers == cmdKey) { + partEditState = new PartSelectionEditState(control, viewModel, SelectionBox); + Cursor = ViewConstants.cursorCross; + } else if (hitControl == control) { + viewModel.TracksViewModel.DeselectParts(); + var part = viewModel.TracksViewModel.MaybeAddPart(point.Position); + if (part != null) { + // Start moving right away + partEditState = new PartMoveEditState(control, viewModel, part); + Cursor = ViewConstants.cursorSizeAll; + } + } else if (hitControl is PartControl partControl) { + bool isVoice = partControl.part is UVoicePart; + bool isWave = partControl.part is UWavePart; + bool trim = point.Position.X > partControl.Bounds.Right - ViewConstants.ResizeMargin; + bool skip = point.Position.X < partControl.Bounds.Left + ViewConstants.ResizeMargin; + if (isVoice && trim) { + partEditState = new PartResizeEditState(control, viewModel, partControl.part); + Cursor = ViewConstants.cursorSizeWE; + } else if (isWave && skip) { + // TODO + } else if (isWave && trim) { + // TODO + } else { + partEditState = new PartMoveEditState(control, viewModel, partControl.part); + Cursor = ViewConstants.cursorSizeAll; + } + } + } else if (point.Properties.IsRightButtonPressed) { + if (hitControl is PartControl partControl) { + if (!viewModel.TracksViewModel.SelectedParts.Contains(partControl.part)) { + viewModel.TracksViewModel.DeselectParts(); + viewModel.TracksViewModel.SelectPart(partControl.part); + } + if (PartsContextMenu != null && viewModel.TracksViewModel.SelectedParts.Count > 0) { + PartsContextMenu.DataContext = new PartsContextMenuArgs { + Part = partControl.part, + PartDeleteCommand = viewModel.PartDeleteCommand, + PartGotoFileCommand = PartGotoFileCommand, + PartReplaceAudioCommand = PartReplaceAudioCommand, + PartRenameCommand = PartRenameCommand, + PartTranscribeCommand = PartTranscribeCommand, + }; + shouldOpenPartsContextMenu = true; + } + } else { + viewModel.TracksViewModel.DeselectParts(); + } + } else if (point.Properties.IsMiddleButtonPressed) { + partEditState = new PartPanningState(control, viewModel); + Cursor = ViewConstants.cursorHand; + } + if (partEditState != null) { + partEditState.Begin(point.Pointer, point.Position); + partEditState.Update(point.Pointer, point.Position); + } + } + + public void PartsCanvasPointerMoved(object sender, PointerEventArgs args) { + var control = (Control)sender; + var point = args.GetCurrentPoint(control); + if (partEditState != null) { + partEditState.Update(point.Pointer, point.Position); + return; + } + var hitControl = control.InputHitTest(point.Position); + if (hitControl is PartControl partControl) { + bool isVoice = partControl.part is UVoicePart; + bool isWave = partControl.part is UWavePart; + bool trim = point.Position.X > partControl.Bounds.Right - ViewConstants.ResizeMargin; + bool skip = point.Position.X < partControl.Bounds.Left + ViewConstants.ResizeMargin; + if (isVoice && trim) { + Cursor = ViewConstants.cursorSizeWE; + } else if (isWave && (skip || trim)) { + Cursor = null; // TODO + } else { + Cursor = null; + } + } else { + Cursor = null; + } + } + + public void PartsCanvasPointerReleased(object sender, PointerReleasedEventArgs args) { + if (partEditState != null) { + if (partEditState.MouseButton != args.InitialPressMouseButton) { + return; + } + var control = (Control)sender; + var point = args.GetCurrentPoint(control); + partEditState.Update(point.Pointer, point.Position); + partEditState.End(point.Pointer, point.Position); + partEditState = null; + Cursor = null; + } + if (openPianoRollWindow) { + pianoRollWindow?.Show(); + pianoRollWindow?.Activate(); + openPianoRollWindow = false; + } + } + + public void PartsCanvasDoubleTapped(object sender, TappedEventArgs args) { + if (!(sender is Canvas canvas)) { + return; + } + var control = canvas.InputHitTest(args.GetPosition(canvas)); + if (control is PartControl partControl && partControl.part is UVoicePart) { + if (pianoRollWindow == null) { + MessageBox.ShowLoading(this); + pianoRollWindow = new PianoRollWindow() { + MainWindow = this, + }; + pianoRollWindow.ViewModel.PlaybackViewModel = viewModel.PlaybackViewModel; + MessageBox.CloseLoading(); + } + // Workaround for new window losing focus. + openPianoRollWindow = true; + int tick = viewModel.TracksViewModel.PointToTick(args.GetPosition(canvas)); + DocManager.Inst.ExecuteCmd(new LoadPartNotification(partControl.part, DocManager.Inst.Project, tick)); + pianoRollWindow.AttachExpressions(); + } + } + + public void PartsCanvasPointerWheelChanged(object sender, PointerWheelEventArgs args) { + 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) { + ViewScalerPointerWheelChanged(VScaler, args); + } else if (args.KeyModifiers == cmdKey) { + TimelinePointerWheelChanged(TimelineCanvas, args); + } + if (partEditState != null) { + var point = args.GetCurrentPoint(partEditState.control); + partEditState.Update(point.Pointer, point.Position); + } + } + + public void PartsContextMenuOpening(object sender, CancelEventArgs args) { + if (shouldOpenPartsContextMenu) { + shouldOpenPartsContextMenu = false; + } else { + args.Cancel = true; + } + } + + public void PartsContextMenuClosing(object sender, CancelEventArgs args) { + if (PartsContextMenu != null) { + PartsContextMenu.DataContext = null; + } + } + + void RenamePart(UPart part) { + var dialog = new TypeInDialog(); + dialog.Title = ThemeManager.GetString("context.part.rename"); + dialog.SetText(part.name); + dialog.onFinish = name => { + if (!string.IsNullOrWhiteSpace(name) && name != part.name) { + if (!string.IsNullOrWhiteSpace(name) && name != part.name) { + DocManager.Inst.StartUndoGroup(); + DocManager.Inst.ExecuteCmd(new RenamePartCommand(DocManager.Inst.Project, part, name)); + DocManager.Inst.EndUndoGroup(); + } + } + }; + dialog.ShowDialog(this); + } + + void GotoFile(UPart part) { + //View the location of the audio file in explorer if the part is a wave part + if (part is UWavePart wavePart) { + try { + OS.GotoFile(wavePart.FilePath); + } catch (Exception e) { + DocManager.Inst.ExecuteCmd(new ErrorMessageNotification(e)); + } + } + } + + async void ReplaceAudio(UPart part) { + var file = await FilePicker.OpenFileAboutProject( + this, "context.part.replaceaudio", FilePicker.AudioFiles); + if (file == null) { + return; + } + UWavePart newPart = new UWavePart() { + FilePath = file, + trackNo = part.trackNo, + position = part.position + }; + newPart.Load(DocManager.Inst.Project); + DocManager.Inst.StartUndoGroup(); + DocManager.Inst.ExecuteCmd(new ReplacePartCommand(DocManager.Inst.Project, part, newPart)); + DocManager.Inst.EndUndoGroup(); + } + + void Transcribe(UPart part) { + //Convert audio to notes + if (part is UWavePart wavePart) { + try { + string text = ThemeManager.GetString("context.part.transcribing"); + var msgbox = MessageBox.ShowModal(this, $"{text} {part.name}", text); + //Duration of the wave file in seconds + int wavDurS = (int)(wavePart.fileDurationMs / 1000.0); + var scheduler = TaskScheduler.FromCurrentSynchronizationContext(); + var transcribeTask = Task.Run(() => { + using (var some = new Some()) { + return some.Transcribe(DocManager.Inst.Project, wavePart, wavPosS => { + //msgbox?.SetText($"{text} {part.name}\n{wavPosS}/{wavDurS}"); + msgbox.SetText(string.Format("{0} {1}\n{2}s / {3}s", text, part.name, wavPosS, wavDurS)); + }); + } + }); + transcribeTask.ContinueWith(task => { + msgbox?.Close(); + if (task.IsFaulted) { + Log.Error(task.Exception, $"Failed to transcribe part {part.name}"); + MessageBox.ShowError(this, task.Exception); + return; + } + var voicePart = task.Result; + //Add voicePart into project + if (voicePart != null) { + var project = DocManager.Inst.Project; + var track = new UTrack(project); + track.TrackNo = project.tracks.Count; + voicePart.trackNo = track.TrackNo; + DocManager.Inst.StartUndoGroup(); + DocManager.Inst.ExecuteCmd(new AddTrackCommand(project, track)); + DocManager.Inst.ExecuteCmd(new AddPartCommand(project, voicePart)); + DocManager.Inst.EndUndoGroup(); + } + }, scheduler); + } catch (Exception e) { + Log.Error(e, $"Failed to transcribe part {part.name}"); + MessageBox.ShowError(this, e); + } + } + } + + async void ValidateTracksVoiceColor() { + DocManager.Inst.StartUndoGroup(); + foreach (var track in DocManager.Inst.Project.tracks) { + if (track.ValidateVoiceColor(out var oldColors, out var newColors)) { + await VoiceColorRemappingAsync(track, oldColors, newColors); + } + } + DocManager.Inst.EndUndoGroup(); + } + async Task VoiceColorRemappingAsync(UTrack track, string[] oldColors, string[] newColors) { + var parts = DocManager.Inst.Project.parts + .Where(part => part.trackNo == track.TrackNo && part is UVoicePart) + .Cast() + .Where(vpart => vpart.notes.Count > 0); + if (parts.Any()) { + var dialog = new VoiceColorMappingDialog(); + VoiceColorMappingViewModel vm = new VoiceColorMappingViewModel(oldColors, newColors, track.TrackName); + dialog.DataContext = vm; + await dialog.ShowDialog(this); + + if (dialog.Apply) { + SetVoiceColorRemapping(track, parts, vm); + } + } + } + void VoiceColorRemapping(UTrack track, string[] oldColors, string[] newColors) { + var parts = DocManager.Inst.Project.parts + .Where(part => part.trackNo == track.TrackNo && part is UVoicePart) + .Cast() + .Where(vpart => vpart.notes.Count > 0); + if (parts.Any()) { + var dialog = new VoiceColorMappingDialog(); + VoiceColorMappingViewModel vm = new VoiceColorMappingViewModel(oldColors, newColors, track.TrackName); + dialog.DataContext = vm; + dialog.onFinish = () => { + DocManager.Inst.StartUndoGroup(); + SetVoiceColorRemapping(track, parts, vm); + DocManager.Inst.EndUndoGroup(); + }; + dialog.ShowDialog(this); + } + } + void SetVoiceColorRemapping(UTrack track, IEnumerable parts, VoiceColorMappingViewModel vm) { + foreach (var part in parts) { + foreach (var phoneme in part.phonemes) { + var tuple = phoneme.GetExpression(DocManager.Inst.Project, track, Ustx.CLR); + if (vm.ColorMappings.Any(m => m.OldIndex == tuple.Item1)) { + var mapping = vm.ColorMappings.First(m => m.OldIndex == tuple.Item1); + if (mapping.OldIndex != mapping.SelectedIndex) { + if (mapping.SelectedIndex == 0) { + DocManager.Inst.ExecuteCmd(new SetPhonemeExpressionCommand(DocManager.Inst.Project, track, part, phoneme, Ustx.CLR, null)); + } else { + DocManager.Inst.ExecuteCmd(new SetPhonemeExpressionCommand(DocManager.Inst.Project, track, part, phoneme, Ustx.CLR, mapping.SelectedIndex)); + } + } + } else { + DocManager.Inst.ExecuteCmd(new SetPhonemeExpressionCommand(DocManager.Inst.Project, track, part, phoneme, Ustx.CLR, null)); + } + } + } + } + + public async void WindowClosing(object? sender, WindowClosingEventArgs e) { + if (forceClose || DocManager.Inst.ChangesSaved) { + if (Preferences.Default.ClearCacheOnQuit) { + Log.Information("Clearing cache..."); + PathManager.Inst.ClearCache(); + Log.Information("Cache cleared."); + } + return; + } + e.Cancel = true; + if (!await AskIfSaveAndContinue()) { + return; + } + pianoRollWindow?.Close(); + forceClose = true; + Close(); + } + + private async Task AskIfSaveAndContinue() { + var result = await MessageBox.Show( + this, + ThemeManager.GetString("dialogs.exitsave.message"), + ThemeManager.GetString("dialogs.exitsave.caption"), + MessageBox.MessageBoxButtons.YesNoCancel); + switch (result) { + case MessageBox.MessageBoxResult.Yes: + await Save(); + goto case MessageBox.MessageBoxResult.No; + case MessageBox.MessageBoxResult.No: + return true; // Continue. + default: + return false; // Cancel. + } + } + + public void OnNext(UCommand cmd, bool isUndo) { + if (cmd is ErrorMessageNotification notif) { + switch (notif.e) { + case Core.Render.NoResamplerException: + case Core.Render.NoWavtoolException: + MessageBox.Show( + this, + ThemeManager.GetString("dialogs.noresampler.message"), + ThemeManager.GetString("dialogs.noresampler.caption"), + MessageBox.MessageBoxButtons.Ok); + break; + default: + MessageBox.ShowError(this, notif.e, notif.message, true); + break; + } + } else if (cmd is LoadingNotification loadingNotif && loadingNotif.window == typeof(MainWindow)) { + if (loadingNotif.startLoading) { + MessageBox.ShowLoading(this); + } else { + MessageBox.CloseLoading(); + } + } else if (cmd is VoiceColorRemappingNotification voicecolorNotif) { + if (voicecolorNotif.TrackNo < 0 || DocManager.Inst.Project.tracks.Count <= voicecolorNotif.TrackNo) { + ValidateTracksVoiceColor(); + } else { + UTrack track = DocManager.Inst.Project.tracks[voicecolorNotif.TrackNo]; + if (!voicecolorNotif.Validate) { + VoiceColorRemapping(track, track.VoiceColorNames, track.VoiceColorExp.options); + } else if (track.ValidateVoiceColor(out var oldColors, out var newColors)) { + VoiceColorRemapping(track, oldColors, newColors); + } + } + } + } + } +} \ No newline at end of file From 919d07b73b5d1a3b53a65a1d5d25712ef69f4eeb Mon Sep 17 00:00:00 2001 From: RedBlackAka <140876408+RedBlackAka@users.noreply.github.com> Date: Fri, 20 Dec 2024 01:45:56 +0100 Subject: [PATCH 4/4] Add strings and DE/JP translation --- OpenUtau/Strings/Strings.de-DE.axaml | 1 + OpenUtau/Strings/Strings.es-ES.axaml | 1 + OpenUtau/Strings/Strings.es-MX.axaml | 1 + OpenUtau/Strings/Strings.fi-FI.axaml | 1 + OpenUtau/Strings/Strings.fr-FR.axaml | 1 + OpenUtau/Strings/Strings.id-ID.axaml | 1 + OpenUtau/Strings/Strings.it-IT.axaml | 1 + OpenUtau/Strings/Strings.ja-JP.axaml | 1 + OpenUtau/Strings/Strings.ko-KR.axaml | 1 + OpenUtau/Strings/Strings.nl-NL.axaml | 1 + OpenUtau/Strings/Strings.pl-PL.axaml | 1 + OpenUtau/Strings/Strings.pt-BR.axaml | 1 + OpenUtau/Strings/Strings.ru-RU.axaml | 1 + OpenUtau/Strings/Strings.th-TH.axaml | 1 + OpenUtau/Strings/Strings.vi-VN.axaml | 1 + OpenUtau/Strings/Strings.zh-CN.axaml | 1 + OpenUtau/Strings/Strings.zh-TW.axaml | 1 + 17 files changed, 17 insertions(+) diff --git a/OpenUtau/Strings/Strings.de-DE.axaml b/OpenUtau/Strings/Strings.de-DE.axaml index 3368b4f22..9aefcce9a 100644 --- a/OpenUtau/Strings/Strings.de-DE.axaml +++ b/OpenUtau/Strings/Strings.de-DE.axaml @@ -333,6 +333,7 @@ Warnung: Diese Option entfernt alle benutzerdefinierten Einstellungen.Auswahlwerkzeug (1) Fortgeschritten + Beim Start nach Update suchen Beim importieren von Spuren das Tempo des importierten Projekts verwenden Immer diff --git a/OpenUtau/Strings/Strings.es-ES.axaml b/OpenUtau/Strings/Strings.es-ES.axaml index c9b0971dc..c7acda206 100644 --- a/OpenUtau/Strings/Strings.es-ES.axaml +++ b/OpenUtau/Strings/Strings.es-ES.axaml @@ -333,6 +333,7 @@ Warning: this option removes custom presets.--> + diff --git a/OpenUtau/Strings/Strings.es-MX.axaml b/OpenUtau/Strings/Strings.es-MX.axaml index 172a550a6..db20a5b76 100644 --- a/OpenUtau/Strings/Strings.es-MX.axaml +++ b/OpenUtau/Strings/Strings.es-MX.axaml @@ -329,6 +329,7 @@ Advertencia: Esta opción eliminará las bases personalizadas. Mano (1) Avanzado + diff --git a/OpenUtau/Strings/Strings.fi-FI.axaml b/OpenUtau/Strings/Strings.fi-FI.axaml index 931e8e8f0..00cc7d22d 100644 --- a/OpenUtau/Strings/Strings.fi-FI.axaml +++ b/OpenUtau/Strings/Strings.fi-FI.axaml @@ -333,6 +333,7 @@ Warning: this option removes custom presets.--> Lisäasetukset + Beta versio diff --git a/OpenUtau/Strings/Strings.fr-FR.axaml b/OpenUtau/Strings/Strings.fr-FR.axaml index 7bedc4ffa..d4c999dd0 100644 --- a/OpenUtau/Strings/Strings.fr-FR.axaml +++ b/OpenUtau/Strings/Strings.fr-FR.axaml @@ -329,6 +329,7 @@ Attention: cela va effacer le préréglage. Sélectionner (1) Paramètres avancés + diff --git a/OpenUtau/Strings/Strings.id-ID.axaml b/OpenUtau/Strings/Strings.id-ID.axaml index 8f4c7d90a..7e70dcde8 100644 --- a/OpenUtau/Strings/Strings.id-ID.axaml +++ b/OpenUtau/Strings/Strings.id-ID.axaml @@ -335,6 +335,7 @@ Peringatan: opsi ini menghapus prasetel khusus. Alat Seleksi (1) Lanjutan + diff --git a/OpenUtau/Strings/Strings.it-IT.axaml b/OpenUtau/Strings/Strings.it-IT.axaml index 8c5e8c903..749a53a41 100644 --- a/OpenUtau/Strings/Strings.it-IT.axaml +++ b/OpenUtau/Strings/Strings.it-IT.axaml @@ -333,6 +333,7 @@ Tieni premuto Ctrl per selezionare Strumento Selezione (1) Avanzato + diff --git a/OpenUtau/Strings/Strings.ja-JP.axaml b/OpenUtau/Strings/Strings.ja-JP.axaml index 005cabb61..79b75d229 100644 --- a/OpenUtau/Strings/Strings.ja-JP.axaml +++ b/OpenUtau/Strings/Strings.ja-JP.axaml @@ -353,6 +353,7 @@ 選択ツール (1) 高度な設定 + 起動時にアップデートをチェック ベータ版 トラックのインポート時、テンポもインポートする 常にインポートする diff --git a/OpenUtau/Strings/Strings.ko-KR.axaml b/OpenUtau/Strings/Strings.ko-KR.axaml index 6ba7cfb64..928daa557 100644 --- a/OpenUtau/Strings/Strings.ko-KR.axaml +++ b/OpenUtau/Strings/Strings.ko-KR.axaml @@ -333,6 +333,7 @@ 선택 도구 (1) 고급 + 베타 버전 사용 프로젝트를 불러올 때, 프로젝트에 포함된 템포 사용 항상 diff --git a/OpenUtau/Strings/Strings.nl-NL.axaml b/OpenUtau/Strings/Strings.nl-NL.axaml index 433f94d91..9d77064ce 100644 --- a/OpenUtau/Strings/Strings.nl-NL.axaml +++ b/OpenUtau/Strings/Strings.nl-NL.axaml @@ -329,6 +329,7 @@ Ctrl ingedrukt houden om te selecteren Selectiegereedschap (1) Geavanceerd + diff --git a/OpenUtau/Strings/Strings.pl-PL.axaml b/OpenUtau/Strings/Strings.pl-PL.axaml index 881e33279..b7ef5ba76 100644 --- a/OpenUtau/Strings/Strings.pl-PL.axaml +++ b/OpenUtau/Strings/Strings.pl-PL.axaml @@ -349,6 +349,7 @@ Uwaga: ta opcja usuwa presety własne. Zaznaczanie (1) Zaawansowane + Beta Przy imporcie ścieżek użyj tempa importowanego projektu Zawsze diff --git a/OpenUtau/Strings/Strings.pt-BR.axaml b/OpenUtau/Strings/Strings.pt-BR.axaml index 8ba2d0538..5477b43c9 100644 --- a/OpenUtau/Strings/Strings.pt-BR.axaml +++ b/OpenUtau/Strings/Strings.pt-BR.axaml @@ -329,6 +329,7 @@ Segure Ctrl para selecionar Ferramenta Seleção (1) Avançado + diff --git a/OpenUtau/Strings/Strings.ru-RU.axaml b/OpenUtau/Strings/Strings.ru-RU.axaml index 583fc2cd4..cb87c3c03 100644 --- a/OpenUtau/Strings/Strings.ru-RU.axaml +++ b/OpenUtau/Strings/Strings.ru-RU.axaml @@ -329,6 +329,7 @@ Инструмент выбора (1) Дополнительно + Бета Когда импортировуются треки, использовать темп импортированного проекта Всегда diff --git a/OpenUtau/Strings/Strings.th-TH.axaml b/OpenUtau/Strings/Strings.th-TH.axaml index e46175350..62246384e 100644 --- a/OpenUtau/Strings/Strings.th-TH.axaml +++ b/OpenUtau/Strings/Strings.th-TH.axaml @@ -329,6 +329,7 @@ ขั้นสูง + เบต้า diff --git a/OpenUtau/Strings/Strings.vi-VN.axaml b/OpenUtau/Strings/Strings.vi-VN.axaml index b06198ee2..4c7b1b1bf 100644 --- a/OpenUtau/Strings/Strings.vi-VN.axaml +++ b/OpenUtau/Strings/Strings.vi-VN.axaml @@ -350,6 +350,7 @@ Lưu ý: tuỳ chọn này cũng sẽ xoá các tổ hợp thiết lập của b Chọn nốt (1) Nâng cao + Tính năng thử nghiệm Khi cho vào track, sử dụng nhịp độ kèm trong tệp đó Luôn luôn diff --git a/OpenUtau/Strings/Strings.zh-CN.axaml b/OpenUtau/Strings/Strings.zh-CN.axaml index 13538e9f9..ae76c8821 100644 --- a/OpenUtau/Strings/Strings.zh-CN.axaml +++ b/OpenUtau/Strings/Strings.zh-CN.axaml @@ -330,6 +330,7 @@ 选择工具 (1) 高级 + Beta测试版 导入音轨时导入曲速 总是 diff --git a/OpenUtau/Strings/Strings.zh-TW.axaml b/OpenUtau/Strings/Strings.zh-TW.axaml index a1e89f00f..025489cb7 100644 --- a/OpenUtau/Strings/Strings.zh-TW.axaml +++ b/OpenUtau/Strings/Strings.zh-TW.axaml @@ -329,6 +329,7 @@ 進階 + 測試版