Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

動的なenumを追加 #763

Merged
merged 1 commit into from
Sep 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/Beutl.Core/IDynamicEnum.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Beutl.Collections;

namespace Beutl;

public interface IDynamicEnum : IJsonSerializable, IEquatable<IDynamicEnum?>
{
IDynamicEnumValue? SelectedValue { get; }

ICoreReadOnlyList<IDynamicEnumValue> Values { get; }

IDynamicEnum WithNewValue(IDynamicEnumValue? value);

bool IEquatable<IDynamicEnum?>.Equals(IDynamicEnum? other)
{
return (SelectedValue?.Equals(other?.SelectedValue) == true
|| (other?.SelectedValue == null && SelectedValue == null))
&& other?.GetType() == GetType();
}
}

public interface IDynamicEnumValue : IEquatable<IDynamicEnumValue?>
{
string DisplayName { get; }
}
97 changes: 97 additions & 0 deletions src/Beutl.Engine/Graphics/FilterEffects/DynamicEnumTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#if DEBUG

using System.Text.Json.Nodes;

using Beutl.Collections;

namespace Beutl.Graphics.Effects;

public sealed class DynamicEnumTest : FilterEffect
{
public static readonly CoreProperty<MyDynamicEnum> DynamicEnumProperty;
private MyDynamicEnum _dynamicEnum = new();

static DynamicEnumTest()
{
DynamicEnumProperty = ConfigureProperty<MyDynamicEnum, DynamicEnumTest>(nameof(DynamicEnum))
.Accessor(o => o.DynamicEnum, (o, v) => o.DynamicEnum = v)
.Register();

AffectsRender<DynamicEnumTest>(DynamicEnumProperty);
}

public MyDynamicEnum DynamicEnum
{
get => _dynamicEnum;
set => SetAndRaise(DynamicEnumProperty, ref _dynamicEnum, value);
}

public override void ApplyTo(FilterEffectContext context)
{
}

public sealed class MyDynamicEnumValue : IDynamicEnumValue
{
// metadataは例えばEnumに関連づけられている、Idなど
public MyDynamicEnumValue(string display/*, object? metadata*/)
{
DisplayName = display;
}

public string DisplayName { get; }

public bool Equals(IDynamicEnumValue? other)
{
return other is MyDynamicEnumValue @enum && @enum.DisplayName == DisplayName;
}
}

public sealed class MyDynamicEnum : IDynamicEnum
{
private static readonly CoreList<IDynamicEnumValue> s_enumValues = new()
{
new MyDynamicEnumValue("Enum 1"),
new MyDynamicEnumValue("Enum 2"),
new MyDynamicEnumValue("Enum 3"),
new MyDynamicEnumValue("Enum 4"),
};

public MyDynamicEnum()
{
SelectedValue = s_enumValues[0];
}

public MyDynamicEnum(IDynamicEnumValue? selectedValue)
{
SelectedValue = selectedValue;
}

// 選択されている値
public IDynamicEnumValue? SelectedValue { get; private set; }

// 選択する候補を返す
public ICoreReadOnlyList<IDynamicEnumValue> Values => s_enumValues;

// 新しい値で初期化
public IDynamicEnum WithNewValue(IDynamicEnumValue? value)
{
return new MyDynamicEnum(value);
}

// 通常はEnumに関連付けられた情報を復元する
public void ReadFromJson(JsonObject json)
{
if (json[nameof(SelectedValue)] is JsonValue val
&& val.TryGetValue(out string? str))
{
SelectedValue = s_enumValues.FirstOrDefault(v => v.DisplayName == str);
}
}

public void WriteToJson(JsonObject json)
{
json[nameof(SelectedValue)] = SelectedValue?.DisplayName;
}
}
}
#endif
10 changes: 10 additions & 0 deletions src/Beutl/Services/PropertyEditorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ public static (IAbstractProperty[] Properties, PropertyEditorExtension Extension
return interfaceType?.GenericTypeArguments?.FirstOrDefault();
}

private static BaseEditorViewModel? CreateDynamicEnumViewModel(IAbstractProperty s)
{
if (s.PropertyType.IsAbstract)
return null;

Type type = typeof(DynamicEnumEditorViewModel<>).MakeGenericType(s.PropertyType);
return Activator.CreateInstance(type, s) as BaseEditorViewModel;
}

internal sealed class PropertyEditorExtensionImpl : IPropertyEditorExtensionImpl
{
private record struct Editor(Func<IAbstractProperty, Control?> CreateEditor, Func<IAbstractProperty, BaseEditorViewModel?> CreateViewModel);
Expand Down Expand Up @@ -157,6 +166,7 @@ private record struct ListItemEditor(Func<IAbstractProperty, IListItemEditor?> C
{ typeof(AlignmentX), new(_ => new AlignmentXEditor(), s => new AlignmentXEditorViewModel(s.ToTyped<AlignmentX>())) },
{ typeof(AlignmentY), new(_ => new AlignmentYEditor(), s => new AlignmentYEditorViewModel(s.ToTyped<AlignmentY>())) },
{ typeof(Enum), new(CreateEnumEditor, CreateEnumViewModel) },
{ typeof(IDynamicEnum), new(_ => new EnumEditor(), CreateDynamicEnumViewModel) },
{ typeof(FontFamily), new(_ => new FontFamilyEditor(), s => new FontFamilyEditorViewModel(s.ToTyped<FontFamily?>())) },
{ typeof(FileInfo), new(_ => new StorageFileEditor(), s => new StorageFileEditorViewModel(s.ToTyped<FileInfo>())) },

Expand Down
124 changes: 124 additions & 0 deletions src/Beutl/ViewModels/Editors/DynamicEnumEditorViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using System.Collections;
using System.Collections.Specialized;

using Avalonia;

using Beutl.Controls.PropertyEditors;

using DynamicData;

using Reactive.Bindings;

namespace Beutl.ViewModels.Editors;

public sealed class DynamicEnumEditorViewModel<T> : ValueEditorViewModel<T?>
where T : IDynamicEnum
{
private IDisposable? _valuesSubscription;

public DynamicEnumEditorViewModel(IAbstractProperty<T?> property)
: base(property)
{
Value.Subscribe(value =>
{
_valuesSubscription?.Dispose();
_valuesSubscription = null;

Items.Replace(value?.Values?.Select(v => v.DisplayName)?.ToArray() ?? Array.Empty<string>());
if (value?.Values != null)
{
value.Values.CollectionChanged += OnValuesCollectionChanged;
_valuesSubscription = Disposable.Create(value.Values, v => v.CollectionChanged -= OnValuesCollectionChanged);
}
})
.DisposeWith(Disposables);

SelectedValue = Value.Select(v => v?.SelectedValue)
.ToReadOnlyReactivePropertySlim()
.DisposeWith(Disposables);

SelectedIndex = Value.Select(v => v?.Values.IndexOf(v.SelectedValue) ?? -1)
.ToReactiveProperty()
.DisposeWith(Disposables);
}

private void OnValuesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
void Add(int index, IList items)
{
foreach (IDynamicEnumValue item in items)
{
Items.Insert(index++, item.DisplayName);
}
}

void Remove(int index, IList items)
{
for (int i = items.Count - 1; i >= 0; --i)
{
Items.RemoveAt(index + i);
}
}

switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Add(e.NewStartingIndex, e.NewItems!);
break;

case NotifyCollectionChangedAction.Remove:
Remove(e.OldStartingIndex, e.OldItems!);
break;

case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Replace:
Remove(e.OldStartingIndex, e.OldItems!);
Add(e.NewStartingIndex, e.NewItems!);
break;

case NotifyCollectionChangedAction.Reset:
Items.Clear();
break;
}

SelectedIndex.Value = Value.Value?.Values.IndexOf(Value.Value.SelectedValue) ?? -1;
}

public CoreList<string> Items { get; } = new();

public ReadOnlyReactivePropertySlim<IDynamicEnumValue?> SelectedValue { get; }

public ReactiveProperty<int> SelectedIndex { get; }

public override void Accept(IPropertyEditorContextVisitor visitor)
{
base.Accept(visitor);
if (visitor is EnumEditor editor)
{
editor.Items = Items;
editor[!EnumEditor.SelectedIndexProperty] = SelectedIndex.ToBinding();
editor.ValueChanged += OnValueChanged;
}
}

private void OnValueChanged(object? sender, PropertyEditorValueChangedEventArgs e)
{
if (e is PropertyEditorValueChangedEventArgs<int> args)
{
int newIndex = args.NewValue;
IDynamicEnumValue? newValue = 0 <= newIndex && newIndex < Items.Count
? Value.Value?.Values[newIndex]
: null;

SetValue(Value.Value, (T?)(Value.Value?.WithNewValue(newValue)));
}
}

protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

_valuesSubscription?.Dispose();
_valuesSubscription = null;
}
}