diff --git a/OpenUtau.Core/Commands/TrackCommands.cs b/OpenUtau.Core/Commands/TrackCommands.cs index 7d2eb1718..031b42b82 100644 --- a/OpenUtau.Core/Commands/TrackCommands.cs +++ b/OpenUtau.Core/Commands/TrackCommands.cs @@ -78,10 +78,16 @@ public MoveTrackCommand(UProject project, UTrack track, bool up) { } public override string ToString() => "Move track"; public override void Execute() { + if (index < 0 || index + 1 >= project.tracks.Count) { + return; + } project.tracks.Reverse(index, 2); UpdateTrackNo(); } public override void Unexecute() { + if (index < 0 || index + 1 >= project.tracks.Count) { + return; + } project.tracks.Reverse(index, 2); UpdateTrackNo(); } @@ -113,6 +119,36 @@ public ChangeTrackColorCommand(UProject project, UTrack track, string colorName) public override void Unexecute() => track.TrackColor = oldName; } + public class TrackChangeSettingsCommand : TrackCommand { + readonly bool newMute; + readonly bool oldMute; + readonly double newVolume; + readonly double oldVolume; + readonly double newPan; + readonly double oldPan; + public TrackChangeSettingsCommand(UProject project, UTrack track, bool mute, double volume, double pan) { + this.project = project; + this.track = track; + newMute = mute; + newVolume = volume; + newPan = pan; + oldMute = track.Mute; + oldVolume = track.Volume; + oldPan = track.Pan; + } + public override string ToString() => "Change track settings"; + public override void Execute() { + track.Mute = newMute; + track.Volume = newVolume; + track.Pan = newPan; + } + public override void Unexecute() { + track.Mute = oldMute; + track.Volume = oldVolume; + track.Pan = oldPan; + } + } + public class TrackChangeSingerCommand : TrackCommand { readonly USinger newSinger, oldSinger; public TrackChangeSingerCommand(UProject project, UTrack track, USinger newSinger) { diff --git a/OpenUtau/Controls/ApplyToAllTracksButton.cs b/OpenUtau/Controls/ApplyToAllTracksButton.cs new file mode 100644 index 000000000..fd22f6d49 --- /dev/null +++ b/OpenUtau/Controls/ApplyToAllTracksButton.cs @@ -0,0 +1,32 @@ +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Input; +using Avalonia.Media; + +namespace OpenUtau.App.Controls { + + public class ApplyToAllTracksButton : Button { + public ApplyToAllTracksButton() { + Padding = new Avalonia.Thickness(0); + BorderThickness = new Avalonia.Thickness(0); + Background = Brushes.Transparent; + Focusable = false; + Content = new Path { + Stroke = ThemeManager.AccentBrush2, + StrokeThickness = 1.75, + Data = Geometry.Parse("M3,4 H11 M3,8 H11 M3,12 H11 M10,2 L13,4 L10,6 M10,6 L13,4 L10,2"), + }; + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) { + base.OnPointerPressed(e); + e.Handled = true; + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) { + base.OnPointerReleased(e); + e.Handled = true; + } + } + +} diff --git a/OpenUtau/Controls/FavouriteToggleButton.cs b/OpenUtau/Controls/FavouriteToggleButton.cs index e1ffcc931..93e93374d 100644 --- a/OpenUtau/Controls/FavouriteToggleButton.cs +++ b/OpenUtau/Controls/FavouriteToggleButton.cs @@ -1,44 +1,54 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; +using Avalonia.Input; using Avalonia.Media; using OpenUtau.App; -public class FavouriteToggleButton : ToggleButton -{ - private readonly Path _iconPath; - - public FavouriteToggleButton() - { - //this.Height = 20; - //this.Width = 20; - // Create icon Path. - _iconPath = new Path - { - Fill = SolidColorBrush.Parse("#00000000"), - Stroke = ThemeManager.AccentBrush3, - StrokeThickness = 2, - Data = Geometry.Parse("M12,21.35L10.55,20.03C5.4,15.36,2,12.28,2,8.5C2,5.42,4.42,3,7.5,3C9.24,3,10.91,3.81,12,5.09C13.09,3.81,14.76,3,16.5,3C19.58,3,22,5.42,22,8.5C22,12.28,18.6,15.36,13.45,20.04L12,21.35Z"), - RenderTransform = new ScaleTransform { ScaleX = 0.6,ScaleY = 0.6} - }; - - this.Content = _iconPath; - - // Change icon on click. - this.PropertyChanged += (sender, e) => - { - if (e.Property == IsCheckedProperty) - { - UpdateIcon(IsChecked ?? false); +namespace OpenUtau.App.Controls { + + public class FavouriteToggleButton : ToggleButton { + private readonly Path _iconPath; + + public FavouriteToggleButton() { + Padding = new Avalonia.Thickness(0); + //this.Height = 20; + //this.Width = 20; + // Create icon Path. + _iconPath = new Path { + Fill = SolidColorBrush.Parse("#00000000"), + Stroke = ThemeManager.AccentBrush3, + StrokeThickness = 2, + Data = Geometry.Parse("M12,21.35L10.55,20.03C5.4,15.36,2,12.28,2,8.5C2,5.42,4.42,3,7.5,3C9.24,3,10.91,3.81,12,5.09C13.09,3.81,14.76,3,16.5,3C19.58,3,22,5.42,22,8.5C22,12.28,18.6,15.36,13.45,20.04L12,21.35Z"), + RenderTransform = new ScaleTransform { ScaleX = 0.5, ScaleY = 0.5 } + }; + + this.Content = _iconPath; + + // Change icon on click. + this.PropertyChanged += (sender, e) => { + if (e.Property == IsCheckedProperty) { + UpdateIcon(IsChecked ?? false); + } + }; + } + + private void UpdateIcon(bool isChecked) { + if (isChecked) { + _iconPath.Fill = ThemeManager.AccentBrush3; + } else { + _iconPath.Fill = SolidColorBrush.Parse("#00000000"); } - }; - } + } - private void UpdateIcon(bool isChecked) - { - if (isChecked) { - _iconPath.Fill = ThemeManager.AccentBrush3; - } else { - _iconPath.Fill = SolidColorBrush.Parse("#00000000"); + protected override void OnPointerPressed(PointerPressedEventArgs e) { + base.OnPointerPressed(e); + e.Handled = true; + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) { + base.OnPointerReleased(e); + e.Handled = true; } } + } diff --git a/OpenUtau/Controls/TrackHeader.axaml b/OpenUtau/Controls/TrackHeader.axaml index b724ea1d2..2b30222ef 100644 --- a/OpenUtau/Controls/TrackHeader.axaml +++ b/OpenUtau/Controls/TrackHeader.axaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:OpenUtau.App.Controls" xmlns:vm="using:OpenUtau.App.ViewModels" mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="104" x:Class="OpenUtau.App.Controls.TrackHeader" Width="300" Height="104" TrackNo="{Binding TrackNo}"> @@ -53,18 +54,42 @@ HorizontalOffset="-3" ItemsSource="{Binding PhonemizerMenuItems}"> + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + diff --git a/OpenUtau/Controls/TrackHeader.axaml.cs b/OpenUtau/Controls/TrackHeader.axaml.cs index 13a8ec35c..5c7559326 100644 --- a/OpenUtau/Controls/TrackHeader.axaml.cs +++ b/OpenUtau/Controls/TrackHeader.axaml.cs @@ -44,12 +44,15 @@ public int TrackNo { private double trackHeight; private Point offset; private int trackNo; + private readonly KeyModifiers cmdKey = + OS.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control; public TrackHeaderViewModel? ViewModel; private List unbinds = new List(); private UTrack? track; + private TrackHeaderCanvas? canvas; public TrackHeader() { InitializeComponent(); @@ -66,6 +69,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang internal void Bind(UTrack track, TrackHeaderCanvas canvas) { this.track = track; + this.canvas = canvas; unbinds.Add(this.Bind(TrackHeightProperty, canvas.GetObservable(TrackHeaderCanvas.TrackHeightProperty))); unbinds.Add(this.Bind(HeightProperty, canvas.GetObservable(TrackHeaderCanvas.TrackHeightProperty))); unbinds.Add(this.Bind(OffsetProperty, canvas.WhenAnyValue(x => x.TrackOffset, trackOffset => new Point(0, -trackOffset * TrackHeight)))); @@ -82,6 +86,21 @@ private void SetPosition() { } } + void HeaderPointerPressed(object? sender, PointerPressedEventArgs args) { + if (!args.GetCurrentPoint(this).Properties.IsLeftButtonPressed || + track == null || + canvas?.DataContext is not TracksViewModel tracksViewModel) { + return; + } + if (args.KeyModifiers == KeyModifiers.Shift) { + tracksViewModel.SelectTracksUntil(track); + } else if (args.KeyModifiers == cmdKey) { + tracksViewModel.ToggleSelectTrack(track); + } else { + tracksViewModel.SelectTrack(track); + } + } + void TrackNameButtonClicked(object sender, RoutedEventArgs args) { ViewModel?.Rename(); args.Handled = true; diff --git a/OpenUtau/Controls/TrackHeaderCanvas.cs b/OpenUtau/Controls/TrackHeaderCanvas.cs index ca716e8fd..a945f71ea 100644 --- a/OpenUtau/Controls/TrackHeaderCanvas.cs +++ b/OpenUtau/Controls/TrackHeaderCanvas.cs @@ -2,11 +2,11 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.Linq; using System.Reactive.Linq; using Avalonia; using Avalonia.Controls; using OpenUtau.App.ViewModels; -using OpenUtau.Core; using OpenUtau.Core.Ustx; using ReactiveUI; @@ -98,6 +98,21 @@ public TrackHeaderCanvas() { } } }); + MessageBus.Current.Listen() + .Subscribe(e => { + var selectedTracks = new HashSet(e.selectedTracks); + foreach (var (track, header) in trackHeaders) { + if (header.ViewModel != null) { + header.ViewModel.IsSelected = selectedTracks.Contains(track); + } + } + }); + MessageBus.Current.Listen() + .Subscribe(_ => { + foreach (var (_, header) in trackHeaders) { + header.ViewModel?.RefreshSelectionStyle(); + } + }); } protected override void OnInitialized() { @@ -153,6 +168,9 @@ private void Items_CollectionChanged(object? sender, NotifyCollectionChangedEven void Add(UTrack track) { var vm = new TrackHeaderViewModel(track); + if (DataContext is TracksViewModel tracksViewModel) { + vm.IsSelected = tracksViewModel.SelectedTracks.Contains(track); + } var header = new TrackHeader() { DataContext = vm, ViewModel = vm, diff --git a/OpenUtau/Strings/Strings.axaml b/OpenUtau/Strings/Strings.axaml index d6787a479..50edeacc2 100644 --- a/OpenUtau/Strings/Strings.axaml +++ b/OpenUtau/Strings/Strings.axaml @@ -772,7 +772,7 @@ General Space: Play ◀ Scroll here to zoom horizontally ▶ Scroll here to zoom vertically ▶ - + Duplicate Track Duplicate Track Settings Favorites @@ -789,6 +789,7 @@ General Open singers location Remove Rename track + Rotate singers across selected tracks Select Renderer Select Singer (Singer default) @@ -796,6 +797,7 @@ General Solo additionally (which not removes solo from other tracks) Solo this only (which removes solo from other tracks) Unsolo all + Standardize Track Settings Change track color Track Settings diff --git a/OpenUtau/ViewModels/MenuItemViewModel.cs b/OpenUtau/ViewModels/MenuItemViewModel.cs index 701d3e05d..ca847037c 100644 --- a/OpenUtau/ViewModels/MenuItemViewModel.cs +++ b/OpenUtau/ViewModels/MenuItemViewModel.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Windows.Input; using Avalonia.Controls.Shapes; -using Avalonia.Data; using Avalonia.Input; using Avalonia.Threading; using OpenUtau.Core.Ustx; @@ -10,6 +9,7 @@ namespace OpenUtau.App.ViewModels { public class MenuItemViewModel { public string? Header { get; set; } public ICommand? Command { get; set; } + public ICommand? SecondaryCommand { get; set; } public object? CommandParameter { get; set; } public IList? Items { get; set; } public double Height { get; set; } = 24; @@ -17,6 +17,7 @@ public class MenuItemViewModel { public KeyGesture? InputGesture { get; set; } public bool IsEnabled { get; set; } = true; public object? Icon { get; set; } + public virtual object HeaderViewModel => this; public MenuItemViewModel() { } public MenuItemViewModel(bool isChecked) { @@ -44,19 +45,7 @@ public bool IsFavourite { } } } - private object? _icon; - public new object? Icon { - get { - if(_icon == null) { - if (CommandParameter is USinger) { - _icon = new FavouriteToggleButton() { - [!FavouriteToggleButton.IsCheckedProperty] = new Binding("IsFavourite") - }; - } - } - return _icon; - } - } + public string? Location { get { if (CommandParameter is USinger singer) { @@ -66,4 +55,7 @@ public string? Location { } } } + + public class PhonemizerMenuItemViewModel : MenuItemViewModel { + } } diff --git a/OpenUtau/ViewModels/TrackHeaderViewModel.cs b/OpenUtau/ViewModels/TrackHeaderViewModel.cs index 1c62570aa..a7d4a4c0b 100644 --- a/OpenUtau/ViewModels/TrackHeaderViewModel.cs +++ b/OpenUtau/ViewModels/TrackHeaderViewModel.cs @@ -3,7 +3,6 @@ using System.IO; using System.Linq; using System.Reactive; -using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; @@ -27,9 +26,11 @@ public class TrackHeaderViewModel : ViewModelBase, IActivatableViewModel { public string PhonemizerTag => track.Phonemizer.Tag; public Core.Render.IRenderer Renderer => track.RendererSettings.Renderer; public IReadOnlyList? SingerMenuItems { get; set; } - public ReactiveCommand SelectSingerCommand { get; } + public ReactiveCommand SelectSingerCommand { get; } + public ReactiveCommand AllSetSingerCommand { get; } public IReadOnlyList? PhonemizerMenuItems { get; set; } public ReactiveCommand SelectPhonemizerCommand { get; } + public ReactiveCommand AllSetPhonemizerCommand { get; } public IReadOnlyList? RenderersMenuItems { get; set; } public ReactiveCommand SelectRendererCommand { get; } [Reactive] public string TrackName { get; set; } = string.Empty; @@ -40,10 +41,12 @@ public class TrackHeaderViewModel : ViewModelBase, IActivatableViewModel { [Reactive] public bool Mute { get; set; } [Reactive] public bool Muted { get; set; } [Reactive] public bool Solo { get; set; } + [Reactive] public bool IsSelected { get; set; } [Reactive] public Bitmap? Avatar { get; set; } - [Reactive] public bool IsSingerVisible { get; set; } - [Reactive] public bool IsPhonemizerVisible { get; set; } - [Reactive] public bool IsRendererVisible { get; set; } + [Reactive] public bool IsSingerVisible { get; set; } + [Reactive] public bool IsPhonemizerVisible { get; set; } + [Reactive] public bool IsRendererVisible { get; set; } + [Reactive] public IBrush HeaderBorderBrush { get; set; } = ThemeManager.NeutralAccentBrushSemi; public ViewModelActivator Activator { get; } @@ -51,45 +54,27 @@ public class TrackHeaderViewModel : ViewModelBase, IActivatableViewModel { // Parameterless constructor for Avalonia preview only. public TrackHeaderViewModel() { - SelectSingerCommand = ReactiveCommand.Create(_ => { }); + SelectSingerCommand = ReactiveCommand.Create(_ => { }); + AllSetSingerCommand = ReactiveCommand.Create(_ => { }); SelectPhonemizerCommand = ReactiveCommand.Create(_ => { }); + AllSetPhonemizerCommand = ReactiveCommand.Create(_ => { }); SelectRendererCommand = ReactiveCommand.Create(_ => { }); Activator = new ViewModelActivator(); track = new UTrack(DocManager.Inst.Project); + this.WhenAnyValue(x => x.IsSelected) + .Subscribe(_ => RefreshSelectionStyle()); + RefreshSelectionStyle(); } public TrackHeaderViewModel(UTrack track) { this.track = track; - SelectSingerCommand = ReactiveCommand.Create(singer => { + SelectSingerCommand = ReactiveCommand.Create(singer => { if (track.Singer != singer) { DocManager.Inst.StartUndoGroup("command.track.singer"); - Log.Information($"Loading Singer: {singer.Name}"); - DocManager.Inst.ExecuteCmd(new TrackChangeSingerCommand(DocManager.Inst.Project, track, singer)); - if (!string.IsNullOrEmpty(singer?.Id) && - Preferences.Default.SingerPhonemizers.TryGetValue(Singer.Id, out var phonemizerName) && - TryChangePhonemizer(phonemizerName)) { - } else if (!string.IsNullOrEmpty(singer?.DefaultPhonemizer)) { - TryChangePhonemizer(singer.DefaultPhonemizer); - } - if (singer == null || !singer.Found) { - var settings = new URenderSettings(); - DocManager.Inst.ExecuteCmd(new TrackChangeRenderSettingCommand(DocManager.Inst.Project, track, settings)); - } else if (singer.SingerType != track.RendererSettings.Renderer?.SingerType) { - var settings = new URenderSettings { - renderer = Core.Render.Renderers.GetDefaultRenderer(singer.SingerType), - }; - DocManager.Inst.ExecuteCmd(new TrackChangeRenderSettingCommand(DocManager.Inst.Project, track, settings)); - } + ApplySingerToTrack(track, singer); DocManager.Inst.ExecuteCmd(new VoiceColorRemappingNotification(track.TrackNo, true)); DocManager.Inst.EndUndoGroup(); - if (!string.IsNullOrEmpty(singer?.Id) && singer.Found) { - Preferences.Default.RecentSingers.Remove(singer.Id); - Preferences.Default.RecentSingers.Insert(0, singer.Id); - if (Preferences.Default.RecentSingers.Count > 16) { - Preferences.Default.RecentSingers.RemoveRange( - 16, Preferences.Default.RecentSingers.Count - 16); - } - } + UpdateRecentSingers(singer); Preferences.Save(); MessageBus.Current.SendMessage(new PianorollRefreshEvent("Part")); } @@ -97,6 +82,28 @@ public TrackHeaderViewModel(UTrack track) { this.RaisePropertyChanged(nameof(Renderer)); RefreshAvatar(); }); + AllSetSingerCommand = ReactiveCommand.Create(singer => { + var targetTracks = DocManager.Inst.Project.tracks + .Where(projectTrack => projectTrack != null) + .Where(projectTrack => IsTrackSelected(projectTrack)) + .ToList(); + if (targetTracks.Count <= 1) { + targetTracks = DocManager.Inst.Project.tracks; + } + DocManager.Inst.StartUndoGroup("command.track.singer"); + foreach (var targetTrack in targetTracks) { + ApplySingerToTrack(targetTrack, singer); + } + DocManager.Inst.ExecuteCmd(new VoiceColorRemappingNotification(-1, true)); + DocManager.Inst.EndUndoGroup(); + UpdateRecentSingers(singer); + Preferences.Save(); + MessageBus.Current.SendMessage(new PianorollRefreshEvent("Part")); + MessageBus.Current.SendMessage(new TracksRefreshEvent()); + this.RaisePropertyChanged(nameof(Singer)); + this.RaisePropertyChanged(nameof(Renderer)); + RefreshAvatar(); + }); SelectPhonemizerCommand = ReactiveCommand.Create(factory => { if (track.Phonemizer.GetType() != factory.type) { DocManager.Inst.StartUndoGroup("command.track.setting"); @@ -119,6 +126,39 @@ public TrackHeaderViewModel(UTrack track) { this.RaisePropertyChanged(nameof(Phonemizer)); this.RaisePropertyChanged(nameof(PhonemizerTag)); }); + AllSetPhonemizerCommand = ReactiveCommand.Create(factory => { + if (factory == null) { + return; + } + var name = factory.type.FullName!; + Log.Information($"Loading Phonemizer: {factory}"); + var targetTracks = DocManager.Inst.Project.tracks + .Where(projectTrack => projectTrack != null) + .Where(projectTrack => IsTrackSelected(projectTrack)) + .ToList(); + if (targetTracks.Count <= 1) { + targetTracks = DocManager.Inst.Project.tracks; + } + DocManager.Inst.StartUndoGroup("command.track.setting"); + foreach (var targetTrack in targetTracks) { + var phonemizer = factory.Create(); + if (phonemizer != null) { + DocManager.Inst.ExecuteCmd(new TrackChangePhonemizerCommand(DocManager.Inst.Project, targetTrack, phonemizer)); + } + var targetSingerId = targetTrack.Singer?.Id; + if (!string.IsNullOrEmpty(targetSingerId) && targetTrack.Singer?.Found == true && phonemizer != null) { + Preferences.Default.SingerPhonemizers[targetSingerId] = name; + } + } + Preferences.Default.RecentPhonemizers.Remove(name); + Preferences.Default.RecentPhonemizers.Insert(0, name); + DocManager.Inst.EndUndoGroup(); + Preferences.Save(); + MessageBus.Current.SendMessage(new PianorollRefreshEvent("Part")); + MessageBus.Current.SendMessage(new TracksRefreshEvent()); + this.RaisePropertyChanged(nameof(Phonemizer)); + this.RaisePropertyChanged(nameof(PhonemizerTag)); + }); SelectRendererCommand = ReactiveCommand.Create(name => { var settings = new URenderSettings { renderer = name, @@ -130,14 +170,6 @@ public TrackHeaderViewModel(UTrack track) { }); Activator = new ViewModelActivator(); - this.WhenActivated((CompositeDisposable disposables) => { - Disposable.Create(() => { - MessageBus.Current.Listen() - .Subscribe(_ => { - ManuallyRaise(); - }).DisposeWith(disposables); - }); - }); TrackName = track.TrackName; TrackAccentColor = ThemeManager.GetTrackColor(track.TrackColor).AccentColor; @@ -172,8 +204,17 @@ public TrackHeaderViewModel(UTrack track) { .Subscribe(solo => { track.Solo = solo; }); + this.WhenAnyValue(x => x.IsSelected) + .Subscribe(_ => RefreshSelectionStyle()); RefreshAvatar(); + RefreshSelectionStyle(); + } + + public void RefreshSelectionStyle() { + HeaderBorderBrush = IsSelected + ? TrackAccentColor + : ThemeManager.NeutralAccentBrushSemi; } public void ToggleSolo() { @@ -234,12 +275,64 @@ public void JudgeMuted() { this.RaisePropertyChanged(nameof(Muted)); } - private bool TryChangePhonemizer(string phonemizerName) { + private static bool IsTrackSelected(UTrack projectTrack) { + var tracksViewModel = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime) + ?.MainWindow?.DataContext as MainWindowViewModel; + return tracksViewModel?.TracksViewModel.SelectedTracks.Contains(projectTrack) == true; + } + + private void ApplySingerToTrack(UTrack targetTrack, USinger? singer) { + if (singer is USinger selectedSinger) { + Log.Information($"Loading Singer: {selectedSinger.Name}"); + DocManager.Inst.ExecuteCmd(new TrackChangeSingerCommand(DocManager.Inst.Project, targetTrack, selectedSinger)); + if (!string.IsNullOrEmpty(selectedSinger.Id) && + Preferences.Default.SingerPhonemizers.TryGetValue(selectedSinger.Id, out var phonemizerName) && + TryChangePhonemizer(targetTrack, phonemizerName)) { + } else if (!string.IsNullOrEmpty(selectedSinger.DefaultPhonemizer)) { + TryChangePhonemizer(targetTrack, selectedSinger.DefaultPhonemizer); + } + if (!selectedSinger.Found || selectedSinger.SingerType != targetTrack.RendererSettings.Renderer?.SingerType) { + var settings = new URenderSettings(); + if (selectedSinger.Found) { + settings = new URenderSettings { + renderer = Core.Render.Renderers.GetDefaultRenderer(selectedSinger.SingerType), + }; + } + DocManager.Inst.ExecuteCmd(new TrackChangeRenderSettingCommand(DocManager.Inst.Project, targetTrack, settings)); + } + } else { + DocManager.Inst.ExecuteCmd(new TrackChangeSingerCommand(DocManager.Inst.Project, targetTrack, USinger.CreateMissing(string.Empty))); + var settings = new URenderSettings(); + DocManager.Inst.ExecuteCmd(new TrackChangeRenderSettingCommand(DocManager.Inst.Project, targetTrack, settings)); + } + } + + private void UpdateRecentSingers(USinger? singer) { + if (!string.IsNullOrEmpty(singer?.Id) && singer.Found) { + Preferences.Default.RecentSingers.Remove(singer.Id); + Preferences.Default.RecentSingers.Insert(0, singer.Id); + if (Preferences.Default.RecentSingers.Count > 16) { + Preferences.Default.RecentSingers.RemoveRange( + 16, Preferences.Default.RecentSingers.Count - 16); + } + } + } + + private SingerMenuItemViewModel CreateSingerMenuItem(USinger singer) { + return new SingerMenuItemViewModel() { + Header = singer.LocalizedName, + Command = SelectSingerCommand, + SecondaryCommand = AllSetSingerCommand, + CommandParameter = singer, + }; + } + + private bool TryChangePhonemizer(UTrack targetTrack, string phonemizerName) { try { var factory = PhonemizerFactory.Get(phonemizerName); var phonemizer = factory?.Create(); if (phonemizer != null) { - DocManager.Inst.ExecuteCmd(new TrackChangePhonemizerCommand(DocManager.Inst.Project, track, phonemizer)); + DocManager.Inst.ExecuteCmd(new TrackChangePhonemizerCommand(DocManager.Inst.Project, targetTrack, phonemizer)); return true; } } catch (Exception e) { @@ -254,33 +347,21 @@ public void RefreshSingers() { items.AddRange(Preferences.Default.RecentSingers .Select(id => SingerManager.Inst.Singers.Values.FirstOrDefault(singer => singer.Id == id)) .OfType() - .Select(singer => new SingerMenuItemViewModel() { - Header = singer.LocalizedName, - Command = SelectSingerCommand, - CommandParameter = singer, - })); - items.Add(new SingerMenuItemViewModel() { + .Select(CreateSingerMenuItem)); + items.Add(new MenuItemViewModel() { Header = ThemeManager.GetString("tracks.favorite") + " ...", Items = Preferences.Default.FavoriteSingers .Select(id => SingerManager.Inst.Singers.Values.FirstOrDefault(singer => singer.Id == id)) .OfType() .LocalizedOrderBy(singer => singer.LocalizedName) - .Select(singer => new SingerMenuItemViewModel() { - Header = singer.LocalizedName, - Command = SelectSingerCommand, - CommandParameter = singer, - }).ToArray(), + .Select(CreateSingerMenuItem).ToArray(), }); var keys = SingerManager.Inst.SingerGroups.Keys.OrderBy(k => k); foreach (var key in keys) { - items.Add(new SingerMenuItemViewModel() { + items.Add(new MenuItemViewModel() { Header = $"{key} ...", Items = SingerManager.Inst.SingerGroups[key] - .Select(singer => new SingerMenuItemViewModel() { - Header = singer.LocalizedName, - Command = SelectSingerCommand, - CommandParameter = singer, - }).ToArray(), + .Select(CreateSingerMenuItem).ToArray(), }); } } else { @@ -388,7 +469,7 @@ public void RefreshPhonemizers() { if (track != null && track.Singer != null && track.Singer.Found) { var factory = FindPhonemizerByName(track.Singer.DefaultPhonemizer); if (factory != null) { - items.Add(new MenuItemViewModel() { + items.Add(new PhonemizerMenuItemViewModel() { Header = ThemeManager.GetString("tracks.singerdefault") + factory.ToString(), Command = SelectPhonemizerCommand, CommandParameter = factory, @@ -400,9 +481,10 @@ public void RefreshPhonemizers() { .Select(name => FindPhonemizerByName(name)) .OfType() .OrderBy(factory => factory.tag) - .Select(factory => new MenuItemViewModel() { + .Select(factory => new PhonemizerMenuItemViewModel() { Header = factory.ToString(), Command = SelectPhonemizerCommand, + SecondaryCommand = AllSetPhonemizerCommand, CommandParameter = factory, })); //more phonemizers grouped by singing language @@ -412,9 +494,10 @@ public void RefreshPhonemizers() { .OrderBy(group => group.Key) .Select(group => new MenuItemViewModel() { Header = GetPhonemizerGroupHeader(group.Key), - Items = group.Select(factory => new MenuItemViewModel() { + Items = group.Select(factory => new PhonemizerMenuItemViewModel() { Header = factory.ToString(), Command = SelectPhonemizerCommand, + SecondaryCommand = AllSetPhonemizerCommand, CommandParameter = factory, }).ToArray(), }).ToArray() @@ -453,8 +536,17 @@ public void RefreshAvatar() { } public void ManuallyRaise() { + TrackName = track.TrackName; + TrackAccentColor = ThemeManager.GetTrackColor(track.TrackColor).AccentColor; + TrackColor = Preferences.Default.UseTrackColor + ? ThemeManager.GetTrackColor(track.TrackColor) + : ThemeManager.GetTrackColor("Blue"); + RefreshSelectionStyle(); this.RaisePropertyChanged(nameof(Singer)); this.RaisePropertyChanged(nameof(TrackNo)); + this.RaisePropertyChanged(nameof(TrackName)); + this.RaisePropertyChanged(nameof(TrackAccentColor)); + this.RaisePropertyChanged(nameof(TrackColor)); this.RaisePropertyChanged(nameof(Phonemizer)); this.RaisePropertyChanged(nameof(PhonemizerTag)); this.RaisePropertyChanged(nameof(Renderer)); @@ -468,7 +560,16 @@ public void ManuallyRaise() { public void Remove() { DocManager.Inst.StartUndoGroup("command.track.delete"); - DocManager.Inst.ExecuteCmd(new RemoveTrackCommand(DocManager.Inst.Project, track)); + var targetTracks = DocManager.Inst.Project.tracks + .Where(projectTrack => projectTrack != null) + .Where(projectTrack => IsTrackSelected(projectTrack)) + .ToList(); + if (targetTracks.Count < 2) { + targetTracks = new List() { this.track }; + } + foreach (var track in targetTracks) { + DocManager.Inst.ExecuteCmd(new RemoveTrackCommand(DocManager.Inst.Project, track)); + } DocManager.Inst.EndUndoGroup(); } @@ -477,7 +578,21 @@ public void MoveUp() { return; } DocManager.Inst.StartUndoGroup("command.track.order"); - DocManager.Inst.ExecuteCmd(new MoveTrackCommand(DocManager.Inst.Project, track, true)); + var targetTracks = DocManager.Inst.Project.tracks + .Where(projectTrack => projectTrack != null) + .Where(projectTrack => IsTrackSelected(projectTrack)) + .ToList(); + if (targetTracks.Count < 2) { + targetTracks = new List() { this.track }; + } else { + targetTracks = targetTracks + .Where(targetTrack => targetTrack != DocManager.Inst.Project.tracks.First()) + .OrderBy(targetTrack => targetTrack.TrackNo) + .ToList(); + } + foreach (var track in targetTracks) { + DocManager.Inst.ExecuteCmd(new MoveTrackCommand(DocManager.Inst.Project, track, true)); + } DocManager.Inst.EndUndoGroup(); } @@ -486,7 +601,21 @@ public void MoveDown() { return; } DocManager.Inst.StartUndoGroup("command.track.order"); - DocManager.Inst.ExecuteCmd(new MoveTrackCommand(DocManager.Inst.Project, track, false)); + var targetTracks = DocManager.Inst.Project.tracks + .Where(projectTrack => projectTrack != null) + .Where(projectTrack => IsTrackSelected(projectTrack)) + .ToList(); + if (targetTracks.Count < 2) { + targetTracks = new List() { this.track }; + } else { + targetTracks = targetTracks + .Where(targetTrack => targetTrack != DocManager.Inst.Project.tracks.Last()) + .OrderByDescending(targetTrack => targetTrack.TrackNo) + .ToList(); + } + foreach (var track in targetTracks) { + DocManager.Inst.ExecuteCmd(new MoveTrackCommand(DocManager.Inst.Project, track, false)); + } DocManager.Inst.EndUndoGroup(); } @@ -497,8 +626,19 @@ public void Rename() { dialog.onFinish = name => { if (!string.IsNullOrWhiteSpace(name) && name != track.TrackName) { DocManager.Inst.StartUndoGroup("command.track.setting"); - this.TrackName = name; - DocManager.Inst.ExecuteCmd(new RenameTrackCommand(DocManager.Inst.Project, track, name)); + var targetTracks = DocManager.Inst.Project.tracks + .Where(projectTrack => projectTrack != null) + .Where(projectTrack => IsTrackSelected(projectTrack)) + .ToList(); + if (targetTracks.Count < 2) { + this.TrackName = name; + DocManager.Inst.ExecuteCmd(new RenameTrackCommand(DocManager.Inst.Project, track, name)); + } else { + for (int i = 0; i < targetTracks.Count; i++) { + string name_ = $"{name}_{i:000}"; + DocManager.Inst.ExecuteCmd(new RenameTrackCommand(DocManager.Inst.Project, targetTracks[i], name_)); + } + } DocManager.Inst.EndUndoGroup(); } }; @@ -508,65 +648,188 @@ public void Rename() { } public async void SelectTrackColor() { - var dialog = new TrackColorDialog(); - dialog.DataContext = new TrackColorViewModel(track); - - if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow != null) { - await dialog.ShowDialog(desktop.MainWindow); - TrackAccentColor = ThemeManager.GetTrackColor(track.TrackColor).AccentColor; - TrackColor = Preferences.Default.UseTrackColor - ? ThemeManager.GetTrackColor(track.TrackColor) - : ThemeManager.GetTrackColor("Blue"); + var targetTracks = DocManager.Inst.Project.tracks + .Where(projectTrack => projectTrack != null) + .Where(projectTrack => IsTrackSelected(projectTrack)) + .ToList(); + var dialogs = new List(); + if (targetTracks.Count < 2) { + var dialog = new TrackColorDialog(); + dialog.DataContext = new TrackColorViewModel(track); + dialogs.Add(dialog); + } else { + foreach (var track in targetTracks) { + var dialog = new TrackColorDialog(); + dialog.DataContext = new TrackColorViewModel(track); + dialogs.Add(dialog); + } + } + foreach (var dialog in dialogs) { + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow != null) { + await dialog.ShowDialog(desktop.MainWindow); + TrackAccentColor = ThemeManager.GetTrackColor(track.TrackColor).AccentColor; + TrackColor = Preferences.Default.UseTrackColor + ? ThemeManager.GetTrackColor(track.TrackColor) + : ThemeManager.GetTrackColor("Blue"); + RefreshSelectionStyle(); + } } } public void Duplicate() { + var targetTracks = DocManager.Inst.Project.tracks + .Where(projectTrack => projectTrack != null) + .Where(projectTrack => IsTrackSelected(projectTrack)) + .ToList(); + if (targetTracks.Count < 2) { + targetTracks = new List() { this.track }; + } else { + targetTracks = targetTracks + .OrderByDescending(targetTrack => targetTrack.TrackNo) + .ToList(); + } DocManager.Inst.StartUndoGroup("command.track.duplicate"); - var newTrack = new UTrack(track.TrackName + "_copy") { - TrackNo = track.TrackNo + 1, - Singer = track.Singer, - Phonemizer = track.Phonemizer, - RendererSettings = track.RendererSettings, - Mute = track.Mute, - Muted = track.Muted, - Solo = false, - Volume = track.Volume, - Pan = track.Pan, - TrackColor = track.TrackColor, - TrackExpressions = track.TrackExpressions.Select(exp => exp.Clone()).ToList() - }; - DocManager.Inst.ExecuteCmd(new AddTrackCommand(DocManager.Inst.Project, newTrack)); - var parts = DocManager.Inst.Project.parts - .Where(part => part.trackNo == track.TrackNo) - .Select(part => part.Clone()).ToList(); - foreach (var part in parts) { - part.trackNo = newTrack.TrackNo; - DocManager.Inst.ExecuteCmd(new AddPartCommand(DocManager.Inst.Project, part)); + foreach (var track in targetTracks) { + var sourceTrackNo = track.TrackNo; + var newTrack = new UTrack(track.TrackName + "_copy") { + TrackNo = sourceTrackNo + 1, + Singer = track.Singer, + Phonemizer = track.Phonemizer, + RendererSettings = track.RendererSettings, + Mute = track.Mute, + Muted = track.Muted, + Solo = false, + Volume = track.Volume, + Pan = track.Pan, + TrackColor = track.TrackColor, + TrackExpressions = track.TrackExpressions.Select(exp => exp.Clone()).ToList() + }; + DocManager.Inst.ExecuteCmd(new AddTrackCommand(DocManager.Inst.Project, newTrack)); + var parts = DocManager.Inst.Project.parts + .Where(part => part.trackNo == sourceTrackNo) + .Select(part => part.Clone()).ToList(); + foreach (var part in parts) { + part.trackNo = newTrack.TrackNo; + DocManager.Inst.ExecuteCmd(new AddPartCommand(DocManager.Inst.Project, part)); + } } DocManager.Inst.EndUndoGroup(); } public void DuplicateSettings() { + var targetTracks = DocManager.Inst.Project.tracks + .Where(projectTrack => projectTrack != null) + .Where(projectTrack => IsTrackSelected(projectTrack)) + .ToList(); + if (targetTracks.Count < 2) { + targetTracks = new List() { this.track }; + } else { + targetTracks = targetTracks + .OrderByDescending(targetTrack => targetTrack.TrackNo) + .ToList(); + } DocManager.Inst.StartUndoGroup("command.track.duplicate"); - DocManager.Inst.ExecuteCmd(new AddTrackCommand(DocManager.Inst.Project, new UTrack(track.TrackName + "_copy") { - TrackNo = track.TrackNo + 1, - Singer = track.Singer, - Phonemizer = track.Phonemizer, - RendererSettings = track.RendererSettings, - Mute = track.Mute, - Muted = track.Muted, - Solo = false, - Volume = track.Volume, - Pan = track.Pan, - TrackColor = track.TrackColor, - TrackExpressions = track.TrackExpressions.Select(exp => exp.Clone()).ToList() - })); + foreach (var track in targetTracks) { + DocManager.Inst.ExecuteCmd(new AddTrackCommand(DocManager.Inst.Project, new UTrack(track.TrackName + "_copy") { + TrackNo = track.TrackNo + 1, + Singer = track.Singer, + Phonemizer = track.Phonemizer, + RendererSettings = track.RendererSettings, + Mute = track.Mute, + Muted = track.Muted, + Solo = false, + Volume = track.Volume, + Pan = track.Pan, + TrackColor = track.TrackColor, + TrackExpressions = track.TrackExpressions.Select(exp => exp.Clone()).ToList() + })); + } + DocManager.Inst.EndUndoGroup(); + } + + public void StandardizeSettings() { + var targetTracks = DocManager.Inst.Project.tracks + .Where(projectTrack => projectTrack != null) + .Where(projectTrack => IsTrackSelected(projectTrack)) + .ToList(); + if (targetTracks.Count <= 1) { + targetTracks = DocManager.Inst.Project.tracks; + } + var phonemizerFactory = PhonemizerFactory.Get(track.Phonemizer.GetType()); + DocManager.Inst.StartUndoGroup("command.track.setting"); + foreach (var targetTrack in targetTracks) { + if (targetTrack == track) { + continue; + } + if (track.Singer != targetTrack.Singer) { + ApplySingerToTrack(targetTrack, track.Singer); + } + if (targetTrack.Phonemizer.GetType() != track.Phonemizer.GetType()) { + var phonemizer = phonemizerFactory?.Create(); + if (phonemizer != null) { + DocManager.Inst.ExecuteCmd(new TrackChangePhonemizerCommand(DocManager.Inst.Project, targetTrack, phonemizer)); + } + } + DocManager.Inst.ExecuteCmd(new TrackChangeRenderSettingCommand( + DocManager.Inst.Project, + targetTrack, + track.RendererSettings.Clone())); + DocManager.Inst.ExecuteCmd(new TrackChangeSettingsCommand( + DocManager.Inst.Project, + targetTrack, + track.Mute, + track.Volume, + track.Pan)); + DocManager.Inst.ExecuteCmd(new ChangeTrackColorCommand( + DocManager.Inst.Project, + targetTrack, + track.TrackColor)); + DocManager.Inst.ExecuteCmd(new ConfigureExpressionsCommand( + DocManager.Inst.Project, + DocManager.Inst.Project.expressions.Values.ToArray(), + targetTrack, + track.TrackExpressions.Select(exp => exp.Clone()).ToArray())); + } + DocManager.Inst.EndUndoGroup(); + MessageBus.Current.SendMessage(new TracksRefreshEvent()); + MessageBus.Current.SendMessage(new PianorollRefreshEvent("TrackColor")); + } + + public void RotateSelectedTrackSingers() { + var targetTracks = DocManager.Inst.Project.tracks + .Where(projectTrack => projectTrack != null) + .Where(projectTrack => IsTrackSelected(projectTrack)) + .OrderBy(targetTrack => targetTrack.TrackNo) + .ToList(); + if (targetTracks.Count <= 1) { + targetTracks = DocManager.Inst.Project.tracks; + } + var singers = targetTracks + .Select(targetTrack => targetTrack.Singer) + .ToList(); + DocManager.Inst.StartUndoGroup("command.track.singer"); + for (int i = 0; i < targetTracks.Count; i++) { + var singer = singers[(i + 1) % singers.Count]; + ApplySingerToTrack(targetTracks[i], singer); + } DocManager.Inst.EndUndoGroup(); + DocManager.Inst.ExecuteCmd(new VoiceColorRemappingNotification(-1, true)); + MessageBus.Current.SendMessage(new TracksRefreshEvent()); + MessageBus.Current.SendMessage(new PianorollRefreshEvent("Part")); } public void VoiceColorRemapping() { - if (track.Singer != null && track.Singer.Found && track.VoiceColorExp != null) { - DocManager.Inst.ExecuteCmd(new VoiceColorRemappingNotification(track.TrackNo, false)); + var targetTracks = DocManager.Inst.Project.tracks + .Where(projectTrack => projectTrack != null) + .Where(projectTrack => IsTrackSelected(projectTrack)) + .ToList(); + if (targetTracks.Count < 2) { + targetTracks = new List() { this.track }; + } + foreach (var track in targetTracks) { + if (track.Singer != null && track.Singer.Found && track.VoiceColorExp != null) { + DocManager.Inst.ExecuteCmd(new VoiceColorRemappingNotification(track.TrackNo, false)); + } } } } diff --git a/OpenUtau/ViewModels/TracksViewModel.cs b/OpenUtau/ViewModels/TracksViewModel.cs index 7ecc60458..bfd26869b 100644 --- a/OpenUtau/ViewModels/TracksViewModel.cs +++ b/OpenUtau/ViewModels/TracksViewModel.cs @@ -31,6 +31,12 @@ public TracksMuteEvent(int trackNo, bool allmute) { this.allmute = allmute; } } + public class TrackSelectionEvent { + public readonly UTrack[] selectedTracks; + public TrackSelectionEvent(UTrack[] selectedTracks) { + this.selectedTracks = selectedTracks; + } + } public class PartsSelectionEvent { public readonly UPart[] selectedParts; public readonly UPart[] tempSelectedParts; @@ -94,6 +100,7 @@ public class TracksViewModel : ViewModelBase, ICmdSubscriber { private readonly ObservableAsPropertyHelper smallChangeY; public readonly List SelectedParts = new List(); + public readonly List SelectedTracks = new List(); private readonly HashSet TempSelectedParts = new HashSet(); public TracksViewModel() { @@ -265,6 +272,44 @@ public void DeselectParts() { SelectedParts.ToArray(), TempSelectedParts.ToArray())); } + public void DeselectTracks() { + SelectedTracks.Clear(); + MessageBus.Current.SendMessage(new TrackSelectionEvent(SelectedTracks.ToArray())); + } + + public void SelectTrack(UTrack track) { + if (SelectedTracks.Count == 1 && SelectedTracks[0] == track) { + return; + } + SelectedTracks.Clear(); + SelectedTracks.Add(track); + MessageBus.Current.SendMessage(new TrackSelectionEvent(SelectedTracks.ToArray())); + } + + public void ToggleSelectTrack(UTrack track) { + if (SelectedTracks.Contains(track)) { + SelectedTracks.Remove(track); + } else { + SelectedTracks.Add(track); + } + MessageBus.Current.SendMessage(new TrackSelectionEvent(SelectedTracks.ToArray())); + } + + public void SelectTracksUntil(UTrack track) { + if (SelectedTracks.Count == 0) { + SelectTrack(track); + return; + } + int start = SelectedTracks.Min(selected => selected.TrackNo); + int end = track.TrackNo; + if (start > end) { + (start, end) = (end, start); + } + SelectedTracks.Clear(); + SelectedTracks.AddRange(Project.tracks.Where(t => start <= t.TrackNo && t.TrackNo <= end)); + MessageBus.Current.SendMessage(new TrackSelectionEvent(SelectedTracks.ToArray())); + } + public void SelectPart(UPart part) { TempSelectedParts.Clear(); SelectedParts.Add(part); @@ -424,19 +469,23 @@ public void OnNext(UCommand cmd, bool isUndo) { } else if (cmd is RemoveTrackCommand removeTrack) { if (!isUndo) { Tracks.Remove(removeTrack.track); + SelectedTracks.Remove(removeTrack.track); } else { Tracks.Add(removeTrack.track); } } Notify(); MessageBus.Current.SendMessage(new TracksRefreshEvent()); + MessageBus.Current.SendMessage(new TrackSelectionEvent(SelectedTracks.ToArray())); } else if (cmd is UNotification) { if (cmd is LoadProjectNotification loadProjectNotif) { Parts.Clear(); Parts.AddRange(loadProjectNotif.project.parts); Tracks.Clear(); Tracks.AddRange(loadProjectNotif.project.tracks); + SelectedTracks.Clear(); MessageBus.Current.SendMessage(new TracksRefreshEvent()); + MessageBus.Current.SendMessage(new TrackSelectionEvent(SelectedTracks.ToArray())); } else if (cmd is SetPlayPosTickNotification setPlayPosTick) { SetPlayPos(setPlayPosTick.playPosTick, setPlayPosTick.waitingRendering); if (!setPlayPosTick.pause || Preferences.Default.LockStartTime == 1) { @@ -447,6 +496,9 @@ public void OnNext(UCommand cmd, bool isUndo) { DeselectParts(); SelectPart(loadPartNotif.part); } + if (0 <= loadPartNotif.part.trackNo && loadPartNotif.part.trackNo < Project.tracks.Count) { + SelectTrack(Project.tracks[loadPartNotif.part.trackNo]); + } } Notify(); }