From af6921253684c7a8a58774a93c4ca047287241b5 Mon Sep 17 00:00:00 2001 From: "Yuto Terada (indigo-san)" Date: Wed, 10 Apr 2024 14:19:29 +0900 Subject: [PATCH 1/3] =?UTF-8?q?FilterEffect=E3=82=92=E9=81=B8=E6=8A=9E?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=83=80=E3=82=A4=E3=82=A2=E3=83=AD=E3=82=B0?= =?UTF-8?q?=E3=82=92=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FilterEffectPickerFlyoutPresenter.cs | 95 ++++++++++++ src/Beutl.Controls/Styles.axaml | 1 + .../FilterEffectPickerFlyoutPresenter.axaml | 142 ++++++++++++++++++ .../SelectFilterEffectTypeViewModel.cs | 2 +- .../SelectLibraryItemDialogViewModel.cs | 102 ++++++++----- .../Dialogs/SelectSoundEffectTypeViewModel.cs | 2 +- .../Dialogs/SelectFilterEffectType.axaml | 44 ------ .../Dialogs/SelectFilterEffectType.axaml.cs | 50 ------ .../Views/Editors/FilterEffectEditor.axaml.cs | 35 ++--- .../Views/Editors/FilterEffectPickerFlyout.cs | 74 +++++++++ .../Views/Editors/SoundEffectEditor.axaml.cs | 33 ++-- 11 files changed, 411 insertions(+), 169 deletions(-) create mode 100644 src/Beutl.Controls/PropertyEditors/FilterEffectPickerFlyoutPresenter.cs create mode 100644 src/Beutl.Controls/Styling/PropertyEditors/FilterEffectPickerFlyoutPresenter.axaml delete mode 100644 src/Beutl/Views/Dialogs/SelectFilterEffectType.axaml delete mode 100644 src/Beutl/Views/Dialogs/SelectFilterEffectType.axaml.cs create mode 100644 src/Beutl/Views/Editors/FilterEffectPickerFlyout.cs diff --git a/src/Beutl.Controls/PropertyEditors/FilterEffectPickerFlyoutPresenter.cs b/src/Beutl.Controls/PropertyEditors/FilterEffectPickerFlyoutPresenter.cs new file mode 100644 index 000000000..4b04b6be6 --- /dev/null +++ b/src/Beutl.Controls/PropertyEditors/FilterEffectPickerFlyoutPresenter.cs @@ -0,0 +1,95 @@ +#nullable enable + +using System.Diagnostics; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Beutl.Reactive; +using Beutl.Services; +using FluentAvalonia.UI.Media; +using Reactive.Bindings; + +namespace Beutl.Controls.PropertyEditors; + +public class FilterEffectPickerFlyoutPresenter : DraggablePickerFlyoutPresenter +{ + public static readonly StyledProperty SelectedItemProperty = + AvaloniaProperty.Register(nameof(SelectedItem)); + + public static readonly StyledProperty?> ItemsProperty = + AvaloniaProperty.Register?>(nameof(Items)); + + public static readonly StyledProperty IsBusyProperty = + AvaloniaProperty.Register(nameof(IsBusy)); + + public static readonly StyledProperty ShowAllProperty = + AvaloniaProperty.Register(nameof(ShowAll)); + + public static readonly StyledProperty ShowSearchBoxProperty = + AvaloniaProperty.Register(nameof(ShowSearchBox)); + + public static readonly StyledProperty SearchTextProperty = + AvaloniaProperty.Register(nameof(SearchText)); + + private readonly CompositeDisposable _disposables = []; + private const string SearchBoxPseudoClass = ":search-box"; + private const string IsBusyPseudoClass = ":busy"; + + public LibraryItem? SelectedItem + { + get => GetValue(SelectedItemProperty); + set => SetValue(SelectedItemProperty, value); + } + + public ReactiveCollection? Items + { + get => GetValue(ItemsProperty); + set => SetValue(ItemsProperty, value); + } + + public bool IsBusy + { + get => GetValue(IsBusyProperty); + set => SetValue(IsBusyProperty, value); + } + + public bool ShowAll + { + get => GetValue(ShowAllProperty); + set => SetValue(ShowAllProperty, value); + } + + public bool ShowSearchBox + { + get => GetValue(ShowSearchBoxProperty); + set => SetValue(ShowSearchBoxProperty, value); + } + + public string? SearchText + { + get => GetValue(SearchTextProperty); + set => SetValue(SearchTextProperty, value); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + _disposables.Clear(); + base.OnApplyTemplate(e); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == ShowSearchBoxProperty) + { + PseudoClasses.Set(SearchBoxPseudoClass, ShowSearchBox); + } + } +} diff --git a/src/Beutl.Controls/Styles.axaml b/src/Beutl.Controls/Styles.axaml index 95e445e7b..c9354aa67 100644 --- a/src/Beutl.Controls/Styles.axaml +++ b/src/Beutl.Controls/Styles.axaml @@ -49,6 +49,7 @@ + diff --git a/src/Beutl.Controls/Styling/PropertyEditors/FilterEffectPickerFlyoutPresenter.axaml b/src/Beutl.Controls/Styling/PropertyEditors/FilterEffectPickerFlyoutPresenter.axaml new file mode 100644 index 000000000..4037303ad --- /dev/null +++ b/src/Beutl.Controls/Styling/PropertyEditors/FilterEffectPickerFlyoutPresenter.axaml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Beutl/ViewModels/Dialogs/SelectFilterEffectTypeViewModel.cs b/src/Beutl/ViewModels/Dialogs/SelectFilterEffectTypeViewModel.cs index 6d540799f..61ab3dfd5 100644 --- a/src/Beutl/ViewModels/Dialogs/SelectFilterEffectTypeViewModel.cs +++ b/src/Beutl/ViewModels/Dialogs/SelectFilterEffectTypeViewModel.cs @@ -6,7 +6,7 @@ namespace Beutl.ViewModels.Dialogs; public sealed class SelectFilterEffectTypeViewModel : SelectLibraryItemDialogViewModel { public SelectFilterEffectTypeViewModel() - : base(KnownLibraryItemFormats.FilterEffect, typeof(FilterEffect), Strings.SelectFilterEffect) + : base(KnownLibraryItemFormats.FilterEffect, typeof(FilterEffect)) { } } diff --git a/src/Beutl/ViewModels/Dialogs/SelectLibraryItemDialogViewModel.cs b/src/Beutl/ViewModels/Dialogs/SelectLibraryItemDialogViewModel.cs index 210d5a1d6..79f5501b1 100644 --- a/src/Beutl/ViewModels/Dialogs/SelectLibraryItemDialogViewModel.cs +++ b/src/Beutl/ViewModels/Dialogs/SelectLibraryItemDialogViewModel.cs @@ -1,6 +1,9 @@ -using Beutl.Services; - +using System.Text.RegularExpressions; +using Beutl.Services; +using DynamicData; +using NuGet.Packaging; using Reactive.Bindings; +using Reactive.Bindings.Extensions; namespace Beutl.ViewModels.Dialogs; @@ -8,51 +11,51 @@ public class SelectLibraryItemDialogViewModel { private readonly string _format; private readonly Type _baseType; - private bool _allLoaded; + private readonly Task _itemsTask; + private Task? _allItemsTask; - public SelectLibraryItemDialogViewModel(string format, Type baseType, string title) + public SelectLibraryItemDialogViewModel(string format, Type baseType) { _format = format; _baseType = baseType; - Title = title; IReadOnlySet items = LibraryService.Current.GetTypesFromFormat(_format); - Task.Run(() => + + _itemsTask = Task.Run(() => { try { IsBusy.Value = true; - foreach (Type type in items) - { - LibraryItem? item = LibraryService.Current.FindItem(type); - if (item != null) - { - Items.Add(item); - } - } + return items.Select(i => LibraryService.Current.FindItem(i)) + .Where(i => i != null) + .ToArray(); } finally { IsBusy.Value = false; } - }); - } + })!; + ShowAll.Subscribe(_ => ProcessSearchText()); - public string Title { get; } + // SearchBoxが並列で変更された場合、最後の一つを処理する + SearchText + .Throttle(TimeSpan.FromMilliseconds(100)) + .ObserveOnUIDispatcher() + .Subscribe(_ => ProcessSearchText()); + } public ReactiveCollection Items { get; } = []; - public ReactiveCollection AllItems { get; } = []; + public ReactiveProperty ShowAll { get; } = new(); public ReactiveProperty IsBusy { get; } = new(); + public ReactiveProperty SearchText { get; } = new(); + public ReactiveProperty SelectedItem { get; } = new(); - public void LoadAllItems() + public Task LoadAllItems() { - if (_allLoaded) - return; - - Task.Run(() => + return _allItemsTask ??= Task.Run(() => { try { @@ -61,34 +64,51 @@ public void LoadAllItems() Type itemType = _baseType; Type[] availableTypes = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(x => x.GetTypes()) - .Where(x => !x.IsAbstract - && x.IsPublic - && x.IsAssignableTo(itemType) - && (itemType.GetConstructor([]) != null - || itemType.GetConstructors().Length == 0)) + .Where(x => x is { IsAbstract: false, IsPublic: true } + && x.IsAssignableTo(itemType) + && (itemType.GetConstructor([]) != null + || itemType.GetConstructors().Length == 0)) .ToArray(); - foreach (Type type in availableTypes) - { - LibraryItem? item = LibraryService.Current.FindItem(type); - if (item != null) + return availableTypes + .Select(type => { - AllItems.Add(item); - } - else - { - AllItems.Add(new SingleTypeLibraryItem( + LibraryItem? item = LibraryService.Current.FindItem(type); + return item ?? new SingleTypeLibraryItem( _format, type, - type.FullName ?? type.Name)); - } - } + type.FullName ?? type.Name); + }) + .ToArray(); } finally { IsBusy.Value = false; } }); + } + + private async void ProcessSearchText() + { + Items.ClearOnScheduler(); + if (ShowAll.Value) + { + Items.AddRange(await LoadAllItems()); + } + else + { + Items.AddRange(await _itemsTask); + } + + if (string.IsNullOrWhiteSpace(SearchText.Value)) return; + Regex[] regexes = RegexHelper.CreateRegexes(SearchText.Value); - _allLoaded = true; + var newItems = Items.Select(v => LibraryItemViewModel.CreateFromOperatorRegistryItem(v)) + .Select(v => (score: v.Match(regexes), item: v)) + .Where(v => v.score > 0) + .OrderByDescending(v => v.score) + .Select(v => (LibraryItem)v.item.Data!) + .ToArray(); + Items.Clear(); + Items.AddRange(newItems); } } diff --git a/src/Beutl/ViewModels/Dialogs/SelectSoundEffectTypeViewModel.cs b/src/Beutl/ViewModels/Dialogs/SelectSoundEffectTypeViewModel.cs index 7cc1a53cb..257d69a98 100644 --- a/src/Beutl/ViewModels/Dialogs/SelectSoundEffectTypeViewModel.cs +++ b/src/Beutl/ViewModels/Dialogs/SelectSoundEffectTypeViewModel.cs @@ -6,7 +6,7 @@ namespace Beutl.ViewModels.Dialogs; public sealed class SelectSoundEffectTypeViewModel : SelectLibraryItemDialogViewModel { public SelectSoundEffectTypeViewModel() - : base(KnownLibraryItemFormats.SoundEffect, typeof(ISoundEffect), Strings.SelectFilterEffect) + : base(KnownLibraryItemFormats.SoundEffect, typeof(ISoundEffect)) { } } diff --git a/src/Beutl/Views/Dialogs/SelectFilterEffectType.axaml b/src/Beutl/Views/Dialogs/SelectFilterEffectType.axaml deleted file mode 100644 index bc68f78ec..000000000 --- a/src/Beutl/Views/Dialogs/SelectFilterEffectType.axaml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Beutl/Views/Dialogs/SelectFilterEffectType.axaml.cs b/src/Beutl/Views/Dialogs/SelectFilterEffectType.axaml.cs deleted file mode 100644 index 42d06e941..000000000 --- a/src/Beutl/Views/Dialogs/SelectFilterEffectType.axaml.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Beutl.Services; -using Beutl.ViewModels.Dialogs; - -using FluentAvalonia.UI.Controls; - -namespace Beutl.Views.Dialogs; - -public sealed partial class SelectFilterEffectType : ContentDialog -{ - public SelectFilterEffectType() - { - InitializeComponent(); - } - - protected override Type StyleKeyOverride => typeof(ContentDialog); - - protected override void OnPrimaryButtonClick(ContentDialogButtonClickEventArgs args) - { - base.OnPrimaryButtonClick(args); - if (DataContext is not SelectLibraryItemDialogViewModel vm) return; - - if (carousel.SelectedIndex == 0) - { - vm.SelectedItem.Value = listbox1.SelectedItem as LibraryItem; - } - else - { - vm.SelectedItem.Value = listbox2.SelectedItem as LibraryItem; - } - } - - protected override void OnSecondaryButtonClick(ContentDialogButtonClickEventArgs args) - { - base.OnSecondaryButtonClick(args); - if (DataContext is not SelectLibraryItemDialogViewModel vm) return; - args.Cancel = true; - - if (carousel.SelectedIndex == 1) - { - SecondaryButtonText = Strings.ShowMore; - carousel.Previous(); - } - else - { - SecondaryButtonText = Strings.Back; - vm.LoadAllItems(); - carousel.Next(); - } - } -} diff --git a/src/Beutl/Views/Editors/FilterEffectEditor.axaml.cs b/src/Beutl/Views/Editors/FilterEffectEditor.axaml.cs index 11599ee4f..373b1853b 100644 --- a/src/Beutl/Views/Editors/FilterEffectEditor.axaml.cs +++ b/src/Beutl/Views/Editors/FilterEffectEditor.axaml.cs @@ -4,12 +4,10 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; - using Beutl.Services; using Beutl.ViewModels.Dialogs; using Beutl.ViewModels.Editors; using Beutl.Views.Dialogs; - using FluentAvalonia.UI.Controls; namespace Beutl.Views.Editors; @@ -116,27 +114,30 @@ private async void Tag_Click(object? sender, RoutedEventArgs e) } } - private static async Task SelectType() + private Task SelectType() { var viewModel = new SelectFilterEffectTypeViewModel(); - var dialog = new SelectFilterEffectType - { - DataContext = viewModel - }; - - if (await dialog.ShowAsync() == ContentDialogResult.Primary) + var dialog = new FilterEffectPickerFlyout(viewModel); + dialog.ShowAt(this); + var tcs = new TaskCompletionSource(); + dialog.Dismissed += (_, _) => tcs.SetResult(null); + dialog.Confirmed += (_, _) => { - if (viewModel.SelectedItem.Value is SingleTypeLibraryItem single) + switch (viewModel.SelectedItem.Value) { - return single.ImplementationType; + case SingleTypeLibraryItem single: + tcs.SetResult(single.ImplementationType); + break; + case MultipleTypeLibraryItem multi: + tcs.SetResult(multi.Types.GetValueOrDefault(KnownLibraryItemFormats.FilterEffect)); + break; + default: + tcs.SetResult(null); + break; } - else if (viewModel.SelectedItem.Value is MultipleTypeLibraryItem multi) - { - return multi.Types.GetValueOrDefault(KnownLibraryItemFormats.FilterEffect); - } - } + }; - return null; + return tcs.Task; } private async void ChangeFilterTypeClick(object? sender, RoutedEventArgs e) diff --git a/src/Beutl/Views/Editors/FilterEffectPickerFlyout.cs b/src/Beutl/Views/Editors/FilterEffectPickerFlyout.cs new file mode 100644 index 000000000..31f81ad14 --- /dev/null +++ b/src/Beutl/Views/Editors/FilterEffectPickerFlyout.cs @@ -0,0 +1,74 @@ +using System; +using System.Reactive.Linq; +using System.ComponentModel; +using Avalonia; +using Avalonia.Input; +using Avalonia.Controls; +using Beutl.Controls.PropertyEditors; +using Beutl.ViewModels.Dialogs; +using FluentAvalonia.Core; +using FluentAvalonia.UI.Controls.Primitives; + +namespace Beutl.Views.Editors; + +public sealed class FilterEffectPickerFlyout(SelectLibraryItemDialogViewModel viewModel) : PickerFlyoutBase +{ + public event TypedEventHandler Confirmed; + + public event TypedEventHandler Dismissed; + + protected override Control CreatePresenter() + { + var pfp = new FilterEffectPickerFlyoutPresenter(); + pfp.CloseClicked += (_, _) => Hide(); + pfp.Confirmed += OnFlyoutConfirmed; + pfp.Dismissed += OnFlyoutDismissed; + pfp.Items = viewModel.Items; + pfp.GetObservable(FilterEffectPickerFlyoutPresenter.SelectedItemProperty) + .Subscribe(v => viewModel.SelectedItem.Value = v); + pfp.GetObservable(FilterEffectPickerFlyoutPresenter.ShowAllProperty) + .Subscribe(v => viewModel.ShowAll.Value = v); + pfp.GetObservable(FilterEffectPickerFlyoutPresenter.SearchTextProperty) + .Subscribe(v => viewModel.SearchText.Value = v); + pfp.KeyDown += (_, e) => + { + switch (e.Key) + { + case Key.Enter: + OnConfirmed(); + break; + case Key.Escape: + Hide(); + break; + } + }; + + return pfp; + } + + private void OnFlyoutDismissed(DraggablePickerFlyoutPresenter sender, object args) + { + Dismissed?.Invoke(this, EventArgs.Empty); + Hide(); + } + + private void OnFlyoutConfirmed(DraggablePickerFlyoutPresenter sender, object args) + { + OnConfirmed(); + } + + protected override void OnConfirmed() + { + Confirmed?.Invoke(this, EventArgs.Empty); + Hide(); + } + + protected override void OnOpening(CancelEventArgs args) + { + base.OnOpening(args); + + Popup.IsLightDismissEnabled = false; + } + + protected override bool ShouldShowConfirmationButtons() => true; +} diff --git a/src/Beutl/Views/Editors/SoundEffectEditor.axaml.cs b/src/Beutl/Views/Editors/SoundEffectEditor.axaml.cs index 3e0895722..2659f0d88 100644 --- a/src/Beutl/Views/Editors/SoundEffectEditor.axaml.cs +++ b/src/Beutl/Views/Editors/SoundEffectEditor.axaml.cs @@ -103,27 +103,30 @@ private async void Tag_Click(object? sender, RoutedEventArgs e) } } - private static async Task SelectType() + private Task SelectType() { var viewModel = new SelectSoundEffectTypeViewModel(); - var dialog = new SelectFilterEffectType + var dialog = new FilterEffectPickerFlyout(viewModel); + dialog.ShowAt(this); + var tcs = new TaskCompletionSource(); + dialog.Dismissed += (_, _) => tcs.SetResult(null); + dialog.Confirmed += (_, _) => { - DataContext = viewModel - }; - - if (await dialog.ShowAsync() == ContentDialogResult.Primary) - { - if (viewModel.SelectedItem.Value is SingleTypeLibraryItem single) + switch (viewModel.SelectedItem.Value) { - return single.ImplementationType; + case SingleTypeLibraryItem single: + tcs.SetResult(single.ImplementationType); + break; + case MultipleTypeLibraryItem multi: + tcs.SetResult(multi.Types.GetValueOrDefault(KnownLibraryItemFormats.SoundEffect)); + break; + default: + tcs.SetResult(null); + break; } - else if (viewModel.SelectedItem.Value is MultipleTypeLibraryItem multi) - { - return multi.Types.GetValueOrDefault(KnownLibraryItemFormats.SoundEffect); - } - } + }; - return null; + return tcs.Task; } private async void ChangeEffectTypeClick(object? sender, RoutedEventArgs e) From d7f34047fe7082715fc8753517848618656c9c91 Mon Sep 17 00:00:00 2001 From: "Yuto Terada (indigo-san)" Date: Wed, 10 Apr 2024 20:23:19 +0900 Subject: [PATCH 2/3] =?UTF-8?q?FilterEffectPicker=E3=81=A7=E3=83=97?= =?UTF-8?q?=E3=83=AD=E3=82=B0=E3=83=AC=E3=82=B9=E3=83=AA=E3=83=B3=E3=82=B0?= =?UTF-8?q?=E3=82=92=E8=A1=A8=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FilterEffectPickerFlyoutPresenter.cs | 4 ++++ .../FilterEffectPickerFlyoutPresenter.axaml | 15 ++++++++++++++- .../Views/Editors/FilterEffectPickerFlyout.cs | 3 +++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Beutl.Controls/PropertyEditors/FilterEffectPickerFlyoutPresenter.cs b/src/Beutl.Controls/PropertyEditors/FilterEffectPickerFlyoutPresenter.cs index 4b04b6be6..5449c94b2 100644 --- a/src/Beutl.Controls/PropertyEditors/FilterEffectPickerFlyoutPresenter.cs +++ b/src/Beutl.Controls/PropertyEditors/FilterEffectPickerFlyoutPresenter.cs @@ -91,5 +91,9 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang { PseudoClasses.Set(SearchBoxPseudoClass, ShowSearchBox); } + else if (change.Property == IsBusyProperty) + { + PseudoClasses.Set(IsBusyPseudoClass, IsBusy); + } } } diff --git a/src/Beutl.Controls/Styling/PropertyEditors/FilterEffectPickerFlyoutPresenter.axaml b/src/Beutl.Controls/Styling/PropertyEditors/FilterEffectPickerFlyoutPresenter.axaml index 4037303ad..f43bc5561 100644 --- a/src/Beutl.Controls/Styling/PropertyEditors/FilterEffectPickerFlyoutPresenter.axaml +++ b/src/Beutl.Controls/Styling/PropertyEditors/FilterEffectPickerFlyoutPresenter.axaml @@ -43,7 +43,8 @@ Background="Transparent" ColumnDefinitions="*,Auto"> - + @@ -110,6 +111,13 @@ + + @@ -126,6 +134,11 @@ + +