1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Profile editor - Ported most VMs of the properties tree and timeline

This commit is contained in:
Robert 2022-01-11 00:25:12 +01:00
parent bf1aad1549
commit c04bff1f48
19 changed files with 657 additions and 102 deletions

View File

@ -249,6 +249,7 @@ namespace Artemis.Core
if (_keyframesEnabled == value) return; if (_keyframesEnabled == value) return;
_keyframesEnabled = value; _keyframesEnabled = value;
OnKeyframesToggled(); OnKeyframesToggled();
OnPropertyChanged(nameof(KeyframesEnabled));
} }
} }

View File

@ -115,8 +115,8 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
public interface ITimelinePropertyViewModel : IScreen public interface ITimelinePropertyViewModel : IScreen
{ {
List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels(); List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels();
void WipeKeyframes(TimeSpan? start, TimeSpan? end); void WipeKeyframes(TimeSpan? start, TimeSpan? end);
void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount); void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount);
} }
} }

View File

@ -11,6 +11,7 @@ namespace Artemis.UI.Shared.Services.ProfileEditor
IObservable<RenderProfileElement?> ProfileElement { get; } IObservable<RenderProfileElement?> ProfileElement { get; }
IObservable<ProfileEditorHistory?> History { get; } IObservable<ProfileEditorHistory?> History { get; }
IObservable<TimeSpan> Time { get; } IObservable<TimeSpan> Time { get; }
IObservable<double> PixelsPerSecond { get; }
void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration); void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration);
void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement); void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement);

View File

@ -15,6 +15,7 @@ internal class ProfileEditorService : IProfileEditorService
private readonly Dictionary<ProfileConfiguration, ProfileEditorHistory> _profileEditorHistories = new(); private readonly Dictionary<ProfileConfiguration, ProfileEditorHistory> _profileEditorHistories = new();
private readonly BehaviorSubject<RenderProfileElement?> _profileElementSubject = new(null); private readonly BehaviorSubject<RenderProfileElement?> _profileElementSubject = new(null);
private readonly BehaviorSubject<TimeSpan> _timeSubject = new(TimeSpan.Zero); private readonly BehaviorSubject<TimeSpan> _timeSubject = new(TimeSpan.Zero);
private readonly BehaviorSubject<double> _pixelsPerSecondSubject = new(300);
private readonly IProfileService _profileService; private readonly IProfileService _profileService;
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
@ -22,9 +23,12 @@ internal class ProfileEditorService : IProfileEditorService
{ {
_profileService = profileService; _profileService = profileService;
_windowService = windowService; _windowService = windowService;
ProfileConfiguration = _profileConfigurationSubject.AsObservable(); ProfileConfiguration = _profileConfigurationSubject.AsObservable();
ProfileElement = _profileElementSubject.AsObservable(); ProfileElement = _profileElementSubject.AsObservable();
History = Observable.Defer(() => Observable.Return(GetHistory(_profileConfigurationSubject.Value))).Concat(ProfileConfiguration.Select(GetHistory)); History = Observable.Defer(() => Observable.Return(GetHistory(_profileConfigurationSubject.Value))).Concat(ProfileConfiguration.Select(GetHistory));
Time = _timeSubject.AsObservable();
PixelsPerSecond = _pixelsPerSecondSubject.AsObservable();
} }
private ProfileEditorHistory? GetHistory(ProfileConfiguration? profileConfiguration) private ProfileEditorHistory? GetHistory(ProfileConfiguration? profileConfiguration)
@ -43,6 +47,7 @@ internal class ProfileEditorService : IProfileEditorService
public IObservable<RenderProfileElement?> ProfileElement { get; } public IObservable<RenderProfileElement?> ProfileElement { get; }
public IObservable<ProfileEditorHistory?> History { get; } public IObservable<ProfileEditorHistory?> History { get; }
public IObservable<TimeSpan> Time { get; } public IObservable<TimeSpan> Time { get; }
public IObservable<double> PixelsPerSecond { get; }
public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration) public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration)
{ {
@ -59,6 +64,11 @@ internal class ProfileEditorService : IProfileEditorService
_timeSubject.OnNext(time); _timeSubject.OnNext(time);
} }
public void ChangePixelsPerSecond(double pixelsPerSecond)
{
_pixelsPerSecondSubject.OnNext(pixelsPerSecond);
}
public void ExecuteCommand(IProfileEditorCommand command) public void ExecuteCommand(IProfileEditorCommand command)
{ {
try try

View File

@ -0,0 +1,51 @@
using System;
using System.Collections.ObjectModel;
using Artemis.Core;
using Artemis.UI.Shared.Services.Interfaces;
namespace Artemis.UI.Shared.Services.PropertyInput;
public interface IPropertyInputService : IArtemisSharedUIService
{
/// <summary>
/// Gets a read-only collection of all registered property editors
/// </summary>
ReadOnlyCollection<PropertyInputRegistration> RegisteredPropertyEditors { get; }
/// <summary>
/// Registers a new property input view model used in the profile editor for the generic type defined in
/// <see cref="PropertyInputViewModel{T}" />
/// <para>Note: DataBindingProperty will remove itself on plugin disable so you don't have to</para>
/// </summary>
/// <param name="plugin"></param>
/// <returns></returns>
PropertyInputRegistration RegisterPropertyInput<T>(Plugin plugin) where T : PropertyInputViewModel;
/// <summary>
/// Registers a new property input view model used in the profile editor for the generic type defined in
/// <see cref="PropertyInputViewModel{T}" />
/// <para>Note: DataBindingProperty will remove itself on plugin disable so you don't have to</para>
/// </summary>
/// <param name="viewModelType"></param>
/// <param name="plugin"></param>
/// <returns></returns>
PropertyInputRegistration RegisterPropertyInput(Type viewModelType, Plugin plugin);
/// <summary>
/// Removes the property input view model
/// </summary>
/// <param name="registration"></param>
void RemovePropertyInput(PropertyInputRegistration registration);
/// <summary>
/// Determines if there is a matching registration for the provided layer property
/// </summary>
/// <param name="layerProperty">The layer property to try to find a view model for</param>
bool CanCreatePropertyInputViewModel(ILayerProperty layerProperty);
/// <summary>
/// If a matching registration is found, creates a new <see cref="PropertyInputViewModel{T}" /> supporting
/// <typeparamref name="T" />
/// </summary>
PropertyInputViewModel<T>? CreatePropertyInputViewModel<T>(LayerProperty<T> layerProperty);
}

View File

@ -2,99 +2,118 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Shared.Services.Interfaces; using Ninject;
using Ninject.Parameters;
namespace Artemis.UI.Shared.Services.PropertyInput namespace Artemis.UI.Shared.Services.PropertyInput;
internal class PropertyInputService : IPropertyInputService
{ {
internal class PropertyInputService : IPropertyInputService private readonly IKernel _kernel;
private readonly List<PropertyInputRegistration> _registeredPropertyEditors;
public PropertyInputService(IKernel kernel)
{ {
private readonly List<PropertyInputRegistration> _registeredPropertyEditors; _kernel = kernel;
_registeredPropertyEditors = new List<PropertyInputRegistration>();
RegisteredPropertyEditors = new ReadOnlyCollection<PropertyInputRegistration>(_registeredPropertyEditors);
}
public PropertyInputService() /// <inheritdoc />
public ReadOnlyCollection<PropertyInputRegistration> RegisteredPropertyEditors { get; }
public PropertyInputRegistration RegisterPropertyInput<T>(Plugin plugin) where T : PropertyInputViewModel
{
return RegisterPropertyInput(typeof(T), plugin);
}
public PropertyInputRegistration RegisterPropertyInput(Type viewModelType, Plugin plugin)
{
if (!typeof(PropertyInputViewModel).IsAssignableFrom(viewModelType))
throw new ArtemisSharedUIException($"Property input VM type must implement {nameof(PropertyInputViewModel)}");
lock (_registeredPropertyEditors)
{ {
_registeredPropertyEditors = new List<PropertyInputRegistration>(); // Indirectly checked if there's a BaseType above
RegisteredPropertyEditors = new ReadOnlyCollection<PropertyInputRegistration>(_registeredPropertyEditors); Type supportedType = viewModelType.BaseType!.GetGenericArguments()[0];
} // If the supported type is a generic, assume there is a base type
if (supportedType.IsGenericParameter)
{
if (supportedType.BaseType == null)
throw new ArtemisSharedUIException("Generic property input VM type must have a type constraint");
supportedType = supportedType.BaseType;
}
/// <inheritdoc /> PropertyInputRegistration? existing = _registeredPropertyEditors.FirstOrDefault(r => r.SupportedType == supportedType);
public ReadOnlyCollection<PropertyInputRegistration> RegisteredPropertyEditors { get; } if (existing != null)
{
if (existing.Plugin != plugin)
throw new ArtemisSharedUIException($"Cannot register property editor for type {supportedType.Name} because an editor was already " +
$"registered by {existing.Plugin}");
/// <inheritdoc /> return existing;
public PropertyInputRegistration RegisterPropertyInput<T>(Plugin plugin) where T : PropertyInputViewModel }
{
throw new NotImplementedException();
}
/// <inheritdoc /> _kernel.Bind(viewModelType).ToSelf();
public PropertyInputRegistration RegisterPropertyInput(Type viewModelType, Plugin plugin) PropertyInputRegistration registration = new(this, plugin, supportedType, viewModelType);
{ _registeredPropertyEditors.Add(registration);
throw new NotImplementedException(); return registration;
}
/// <inheritdoc />
public void RemovePropertyInput(PropertyInputRegistration registration)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public bool CanCreatePropertyInputViewModel(ILayerProperty layerProperty)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public PropertyInputViewModel<T>? CreatePropertyInputViewModel<T>(LayerProperty<T> layerProperty)
{
throw new NotImplementedException();
} }
} }
public interface IPropertyInputService : IArtemisSharedUIService public void RemovePropertyInput(PropertyInputRegistration registration)
{ {
/// <summary> lock (_registeredPropertyEditors)
/// Gets a read-only collection of all registered property editors {
/// </summary> if (_registeredPropertyEditors.Contains(registration))
ReadOnlyCollection<PropertyInputRegistration> RegisteredPropertyEditors { get; } {
registration.Unsubscribe();
_registeredPropertyEditors.Remove(registration);
/// <summary> _kernel.Unbind(registration.ViewModelType);
/// Registers a new property input view model used in the profile editor for the generic type defined in }
/// <see cref="PropertyInputViewModel{T}" /> }
/// <para>Note: DataBindingProperty will remove itself on plugin disable so you don't have to</para> }
/// </summary>
/// <param name="plugin"></param>
/// <returns></returns>
PropertyInputRegistration RegisterPropertyInput<T>(Plugin plugin) where T : PropertyInputViewModel;
/// <summary> public bool CanCreatePropertyInputViewModel(ILayerProperty layerProperty)
/// Registers a new property input view model used in the profile editor for the generic type defined in {
/// <see cref="PropertyInputViewModel{T}" /> PropertyInputRegistration? registration = RegisteredPropertyEditors.FirstOrDefault(r => r.SupportedType == layerProperty.PropertyType);
/// <para>Note: DataBindingProperty will remove itself on plugin disable so you don't have to</para> if (registration == null && layerProperty.PropertyType.IsEnum)
/// </summary> registration = RegisteredPropertyEditors.FirstOrDefault(r => r.SupportedType == typeof(Enum));
/// <param name="viewModelType"></param>
/// <param name="plugin"></param>
/// <returns></returns>
PropertyInputRegistration RegisterPropertyInput(Type viewModelType, Plugin plugin);
/// <summary> return registration != null;
/// Removes the property input view model }
/// </summary>
/// <param name="registration"></param>
void RemovePropertyInput(PropertyInputRegistration registration);
/// <summary> public PropertyInputViewModel<T>? CreatePropertyInputViewModel<T>(LayerProperty<T> layerProperty)
/// Determines if there is a matching registration for the provided layer property {
/// </summary> Type? viewModelType = null;
/// <param name="layerProperty">The layer property to try to find a view model for</param> PropertyInputRegistration? registration = RegisteredPropertyEditors.FirstOrDefault(r => r.SupportedType == typeof(T));
bool CanCreatePropertyInputViewModel(ILayerProperty layerProperty);
/// <summary> // Check for enums if no supported type was found
/// If a matching registration is found, creates a new <see cref="PropertyInputViewModel{T}" /> supporting if (registration == null && typeof(T).IsEnum)
/// <typeparamref name="T" /> {
/// </summary> // The enum VM will likely be a generic, that requires creating a generic type matching the layer property
PropertyInputViewModel<T>? CreatePropertyInputViewModel<T>(LayerProperty<T> layerProperty); registration = RegisteredPropertyEditors.FirstOrDefault(r => r.SupportedType == typeof(Enum));
if (registration != null && registration.ViewModelType.IsGenericType)
viewModelType = registration.ViewModelType.MakeGenericType(layerProperty.GetType().GenericTypeArguments);
}
else if (registration != null)
{
viewModelType = registration.ViewModelType;
}
else
{
return null;
}
if (viewModelType == null)
return null;
ConstructorArgument parameter = new("layerProperty", layerProperty);
// ReSharper disable once InconsistentlySynchronizedField
// When you've just spent the last 2 hours trying to figure out a deadlock and reach this line, I'm so, so sorry. I thought this would be fine.
IKernel kernel = registration?.Plugin.Kernel ?? _kernel;
return (PropertyInputViewModel<T>) kernel.Get(viewModelType, parameter);
} }
} }

View File

@ -4,6 +4,7 @@ using Artemis.UI.Screens.Device;
using Artemis.UI.Screens.Plugins; using Artemis.UI.Screens.Plugins;
using Artemis.UI.Screens.ProfileEditor; using Artemis.UI.Screens.ProfileEditor;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
using Artemis.UI.Screens.ProfileEditor.ProfileTree; using Artemis.UI.Screens.ProfileEditor.ProfileTree;
using Artemis.UI.Screens.Settings; using Artemis.UI.Screens.Settings;
@ -75,4 +76,10 @@ namespace Artemis.UI.Ninject.Factories
// TimelineViewModel TimelineViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups); // TimelineViewModel TimelineViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
// TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups); // TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
} }
public interface IPropertyVmFactory
{
ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, ProfileElementPropertyViewModel profileElementPropertyViewModel);
ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, ProfileElementPropertyViewModel profileElementPropertyViewModel);
}
} }

View File

@ -0,0 +1,32 @@
using System;
using System.Reflection;
using Artemis.Core;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
using Ninject.Extensions.Factory;
namespace Artemis.UI.Ninject.InstanceProviders
{
public class LayerPropertyViewModelInstanceProvider : StandardInstanceProvider
{
protected override Type GetType(MethodInfo methodInfo, object[] arguments)
{
if (methodInfo.ReturnType != typeof(ITreePropertyViewModel) && methodInfo.ReturnType != typeof(ITimelinePropertyViewModel))
return base.GetType(methodInfo, arguments);
// Find LayerProperty type
Type? layerPropertyType = arguments[0].GetType();
while (layerPropertyType != null && (!layerPropertyType.IsGenericType || layerPropertyType.GetGenericTypeDefinition() != typeof(LayerProperty<>)))
layerPropertyType = layerPropertyType.BaseType;
if (layerPropertyType == null)
return base.GetType(methodInfo, arguments);
if (methodInfo.ReturnType == typeof(ITreePropertyViewModel))
return typeof(TreePropertyViewModel<>).MakeGenericType(layerPropertyType.GetGenericArguments());
if (methodInfo.ReturnType == typeof(ITimelinePropertyViewModel))
return typeof(TimelinePropertyViewModel<>).MakeGenericType(layerPropertyType.GetGenericArguments());
return base.GetType(methodInfo, arguments);
}
}
}

View File

@ -1,11 +1,13 @@
using System; using System;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Ninject.InstanceProviders;
using Artemis.UI.Screens; using Artemis.UI.Screens;
using Artemis.UI.Services.Interfaces; using Artemis.UI.Services.Interfaces;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Shared.PlatformSupport; using Avalonia.Shared.PlatformSupport;
using Ninject.Extensions.Conventions; using Ninject.Extensions.Conventions;
using Ninject.Extensions.Factory;
using Ninject.Modules; using Ninject.Modules;
using Ninject.Planning.Bindings.Resolvers; using Ninject.Planning.Bindings.Resolvers;
@ -46,6 +48,8 @@ namespace Artemis.UI.Ninject
.BindToFactory(); .BindToFactory();
}); });
Kernel.Bind<IPropertyVmFactory>().ToFactory(() => new LayerPropertyViewModelInstanceProvider());
// Bind all UI services as singletons // Bind all UI services as singletons
Kernel.Bind(x => Kernel.Bind(x =>
{ {

View File

@ -1,28 +1,63 @@
using System.Collections.ObjectModel; using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.PropertyInput;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties;
public class ProfileElementPropertyGroupViewModel : ViewModelBase public class ProfileElementPropertyGroupViewModel : ViewModelBase
{ {
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
private readonly IPropertyInputService _propertyInputService;
private bool _isVisible; private bool _isVisible;
private bool _isExpanded; private bool _isExpanded;
private bool _hasChildren;
public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory) public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService)
{ {
Children = new ObservableCollection<ActivatableViewModelBase>(); _layerPropertyVmFactory = layerPropertyVmFactory;
_propertyInputService = propertyInputService;
Children = new ObservableCollection<ViewModelBase>();
LayerPropertyGroup = layerPropertyGroup; LayerPropertyGroup = layerPropertyGroup;
TreeGroupViewModel = layerPropertyVmFactory.TreeGroupViewModel(this); TreeGroupViewModel = layerPropertyVmFactory.TreeGroupViewModel(this);
IsVisible = !LayerPropertyGroup.IsHidden; PopulateChildren();
// TODO: Update visiblity on change, can't do it atm because not sure how to unsubscribe from the event
} }
public ObservableCollection<ActivatableViewModelBase> Children { get; } private void PopulateChildren()
{
// Get all properties and property groups and create VMs for them
// The group has methods for getting this without reflection but then we lose the order of the properties as they are defined on the group
foreach (PropertyInfo propertyInfo in LayerPropertyGroup.GetType().GetProperties())
{
if (Attribute.IsDefined(propertyInfo, typeof(LayerPropertyIgnoreAttribute)))
continue;
if (typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType))
{
ILayerProperty? value = (ILayerProperty?) propertyInfo.GetValue(LayerPropertyGroup);
// Ensure a supported input VM was found, otherwise don't add it
if (value != null && _propertyInputService.CanCreatePropertyInputViewModel(value))
Children.Add(_layerPropertyVmFactory.ProfileElementPropertyViewModel(value));
}
else if (typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType))
{
LayerPropertyGroup? value = (LayerPropertyGroup?) propertyInfo.GetValue(LayerPropertyGroup);
if (value != null)
Children.Add(_layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(value));
}
}
HasChildren = Children.Any(i => i is ProfileElementPropertyViewModel {IsVisible: true} || i is ProfileElementPropertyGroupViewModel {IsVisible: true});
}
public ObservableCollection<ViewModelBase> Children { get; }
public LayerPropertyGroup LayerPropertyGroup { get; } public LayerPropertyGroup LayerPropertyGroup { get; }
public TreeGroupViewModel TreeGroupViewModel { get; } public TreeGroupViewModel TreeGroupViewModel { get; }
@ -37,4 +72,10 @@ public class ProfileElementPropertyGroupViewModel : ViewModelBase
get => _isExpanded; get => _isExpanded;
set => this.RaiseAndSetIfChanged(ref _isExpanded, value); set => this.RaiseAndSetIfChanged(ref _isExpanded, value);
} }
public bool HasChildren
{
get => _hasChildren;
set => this.RaiseAndSetIfChanged(ref _hasChildren, value);
}
} }

View File

@ -1,20 +1,44 @@
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
using Artemis.UI.Shared;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties;
public class ProfileElementPropertyViewModel public class ProfileElementPropertyViewModel : ViewModelBase
{ {
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory; private bool _isExpanded;
private readonly IProfileEditorService _profileEditorService; private bool _isHighlighted;
private bool _isVisible;
public ProfileElementPropertyViewModel(ILayerProperty layerProperty, IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory) public ProfileElementPropertyViewModel(ILayerProperty layerProperty, IPropertyVmFactory propertyVmFactory)
{ {
LayerProperty = layerProperty; LayerProperty = layerProperty;
_profileEditorService = profileEditorService; TreePropertyViewModel = propertyVmFactory.TreePropertyViewModel(LayerProperty, this);
_layerPropertyVmFactory = layerPropertyVmFactory; TimelinePropertyViewModel = propertyVmFactory.TimelinePropertyViewModel(LayerProperty, this);
} }
public ILayerProperty LayerProperty { get; } public ILayerProperty LayerProperty { get; }
public ITreePropertyViewModel TreePropertyViewModel { get; }
public ITimelinePropertyViewModel TimelinePropertyViewModel { get; }
public bool IsVisible
{
get => _isVisible;
set => this.RaiseAndSetIfChanged(ref _isVisible, value);
}
public bool IsHighlighted
{
get => _isHighlighted;
set => this.RaiseAndSetIfChanged(ref _isHighlighted, value);
}
public bool IsExpanded
{
get => _isExpanded;
set => this.RaiseAndSetIfChanged(ref _isExpanded, value);
}
} }

View File

@ -0,0 +1,28 @@
using System;
using Artemis.Core;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline;
public interface ITimelineKeyframeViewModel
{
bool IsSelected { get; set; }
TimeSpan Position { get; }
ILayerPropertyKeyframe Keyframe { get; }
#region Movement
void SaveOffsetToKeyframe(ITimelineKeyframeViewModel source);
void ApplyOffsetToKeyframe(ITimelineKeyframeViewModel source);
void UpdatePosition(TimeSpan position);
void ReleaseMovement();
#endregion
#region Context menu actions
void PopulateEasingViewModels();
void ClearEasingViewModels();
void Delete(bool save = true);
#endregion
}

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline;
public interface ITimelinePropertyViewModel : IReactiveObject
{
List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels();
void WipeKeyframes(TimeSpan? start, TimeSpan? end);
void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount);
}

View File

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using Artemis.Core;
using Artemis.UI.Shared;
using Avalonia;
using Humanizer;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline;
public class TimelineEasingViewModel : ViewModelBase
{
private bool _isEasingModeSelected;
public TimelineEasingViewModel(Easings.Functions easingFunction, bool isSelected)
{
_isEasingModeSelected = isSelected;
EasingFunction = easingFunction;
Description = easingFunction.Humanize();
EasingPoints = new List<Point>();
for (int i = 1; i <= 10; i++)
{
int x = i;
double y = Easings.Interpolate(i / 10.0, EasingFunction) * 10;
EasingPoints.Add(new Point(x, y));
}
}
public Easings.Functions EasingFunction { get; }
public List<Point> EasingPoints { get; }
public string Description { get; }
public bool IsEasingModeSelected
{
get => _isEasingModeSelected;
set
{
_isEasingModeSelected = value;
if (value) OnEasingModeSelected();
}
}
public event EventHandler EasingModeSelected;
protected virtual void OnEasingModeSelected()
{
EasingModeSelected?.Invoke(this, EventArgs.Empty);
}
}

View File

@ -0,0 +1,169 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia.Controls.Mixins;
using DynamicData;
using ReactiveUI;
using Disposable = System.Reactive.Disposables.Disposable;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline
{
public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineKeyframeViewModel
{
private bool _isSelected;
private string _timestamp;
private double _x;
private readonly IProfileEditorService _profileEditorService;
public TimelineKeyframeViewModel(LayerPropertyKeyframe<T> layerPropertyKeyframe, IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
LayerPropertyKeyframe = layerPropertyKeyframe;
EasingViewModels = new ObservableCollection<TimelineEasingViewModel>();
this.WhenActivated(d =>
{
_profileEditorService.PixelsPerSecond.Subscribe(p =>
{
_pixelsPerSecond = p;
_profileEditorService.PixelsPerSecond.Subscribe(_ => Update()).DisposeWith(d);
Disposable.Create(() =>
{
foreach (TimelineEasingViewModel timelineEasingViewModel in EasingViewModels)
timelineEasingViewModel.EasingModeSelected -= TimelineEasingViewModelOnEasingModeSelected;
}).DisposeWith(d);
}).DisposeWith(d);
});
}
public LayerPropertyKeyframe<T> LayerPropertyKeyframe { get; }
public ObservableCollection<TimelineEasingViewModel> EasingViewModels { get; }
public double X
{
get => _x;
set => this.RaiseAndSetIfChanged(ref _x, value);
}
public string Timestamp
{
get => _timestamp;
set => this.RaiseAndSetIfChanged(ref _timestamp, value);
}
public bool IsSelected
{
get => _isSelected;
set => this.RaiseAndSetIfChanged(ref _isSelected, value);
}
public TimeSpan Position => LayerPropertyKeyframe.Position;
public ILayerPropertyKeyframe Keyframe => LayerPropertyKeyframe;
public void Update()
{
X = _pixelsPerSecond * LayerPropertyKeyframe.Position.TotalSeconds;
Timestamp = $"{Math.Floor(LayerPropertyKeyframe.Position.TotalSeconds):00}.{LayerPropertyKeyframe.Position.Milliseconds:000}";
}
#region Movement
private TimeSpan? _offset;
private double _pixelsPerSecond;
public void ReleaseMovement()
{
_offset = null;
}
public void SaveOffsetToKeyframe(ITimelineKeyframeViewModel source)
{
if (source == this)
{
_offset = null;
return;
}
if (_offset != null)
return;
_offset = LayerPropertyKeyframe.Position - source.Position;
}
public void ApplyOffsetToKeyframe(ITimelineKeyframeViewModel source)
{
if (source == this || _offset == null)
return;
UpdatePosition(source.Position + _offset.Value);
}
public void UpdatePosition(TimeSpan position)
{
throw new NotImplementedException();
// if (position < TimeSpan.Zero)
// LayerPropertyKeyframe.Position = TimeSpan.Zero;
// else if (position > _profileEditorService.SelectedProfileElement.Timeline.Length)
// LayerPropertyKeyframe.Position = _profileEditorService.SelectedProfileElement.Timeline.Length;
// else
// LayerPropertyKeyframe.Position = position;
Update();
}
#endregion
#region Easing
public void PopulateEasingViewModels()
{
if (EasingViewModels.Any())
return;
EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions))
.Cast<Easings.Functions>()
.Select(e => new TimelineEasingViewModel(e, e == LayerPropertyKeyframe.EasingFunction)));
foreach (TimelineEasingViewModel timelineEasingViewModel in EasingViewModels)
timelineEasingViewModel.EasingModeSelected += TimelineEasingViewModelOnEasingModeSelected;
}
public void ClearEasingViewModels()
{
EasingViewModels.Clear();
}
private void TimelineEasingViewModelOnEasingModeSelected(object? sender, EventArgs e)
{
if (sender is TimelineEasingViewModel timelineEasingViewModel)
SelectEasingMode(timelineEasingViewModel);
}
public void SelectEasingMode(TimelineEasingViewModel easingViewModel)
{
throw new NotImplementedException();
LayerPropertyKeyframe.EasingFunction = easingViewModel.EasingFunction;
// Set every selection to false except on the VM that made the change
foreach (TimelineEasingViewModel propertyTrackEasingViewModel in EasingViewModels.Where(vm => vm != easingViewModel))
propertyTrackEasingViewModel.IsEasingModeSelected = false;
}
#endregion
#region Context menu actions
public void Delete(bool save = true)
{
throw new NotImplementedException();
}
#endregion
}
}

View File

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline
{
public class TimelinePropertyViewModel<T> : ActivatableViewModelBase, ITimelinePropertyViewModel
{
private readonly IProfileEditorService _profileEditorService;
public LayerProperty<T> LayerProperty { get; }
public ProfileElementPropertyViewModel ProfileElementPropertyViewModel { get; }
public ObservableCollection<TimelineKeyframeViewModel<T>> KeyframeViewModels { get; }
public TimelinePropertyViewModel(LayerProperty<T> layerProperty, ProfileElementPropertyViewModel profileElementPropertyViewModel, IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
LayerProperty = layerProperty;
ProfileElementPropertyViewModel = profileElementPropertyViewModel;
KeyframeViewModels = new ObservableCollection<TimelineKeyframeViewModel<T>>();
}
#region Implementation of ITimelinePropertyViewModel
public List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels()
{
return KeyframeViewModels.Cast<ITimelineKeyframeViewModel>().ToList();
}
public void WipeKeyframes(TimeSpan? start, TimeSpan? end)
{
start ??= TimeSpan.Zero;
end ??= TimeSpan.MaxValue;
List<LayerPropertyKeyframe<T>> toShift = LayerProperty.Keyframes.Where(k => k.Position >= start && k.Position < end).ToList();
foreach (LayerPropertyKeyframe<T> keyframe in toShift)
LayerProperty.RemoveKeyframe(keyframe);
UpdateKeyframes();
}
public void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount)
{
start ??= TimeSpan.Zero;
end ??= TimeSpan.MaxValue;
List<LayerPropertyKeyframe<T>> toShift = LayerProperty.Keyframes.Where(k => k.Position > start && k.Position < end).ToList();
foreach (LayerPropertyKeyframe<T> keyframe in toShift)
keyframe.Position += amount;
UpdateKeyframes();
}
#endregion
private void UpdateKeyframes()
{
// Only show keyframes if they are enabled
if (LayerProperty.KeyframesEnabled)
{
List<LayerPropertyKeyframe<T>> keyframes = LayerProperty.Keyframes.ToList();
List<TimelineKeyframeViewModel<T>> toRemove = KeyframeViewModels.Where(t => !keyframes.Contains(t.LayerPropertyKeyframe)).ToList();
foreach (TimelineKeyframeViewModel<T> timelineKeyframeViewModel in toRemove)
KeyframeViewModels.Remove(timelineKeyframeViewModel);
List<TimelineKeyframeViewModel<T>> toAdd = keyframes.Where(k => KeyframeViewModels.All(t => t.LayerPropertyKeyframe != k)).Select(k => new TimelineKeyframeViewModel<T>(k, _profileEditorService)).ToList();
foreach (TimelineKeyframeViewModel<T> timelineKeyframeViewModel in toAdd)
KeyframeViewModels.Add(timelineKeyframeViewModel);
}
else
KeyframeViewModels.Clear();
foreach (TimelineKeyframeViewModel<T> timelineKeyframeViewModel in KeyframeViewModels)
timelineKeyframeViewModel.Update();
}
}
}

View File

@ -0,0 +1,9 @@
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
public interface ITreePropertyViewModel : IReactiveObject
{
bool HasDataBinding { get; }
double GetDepth();
}

View File

@ -1,9 +1,7 @@
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
@ -24,8 +22,8 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
public class TreeGroupViewModel : ActivatableViewModelBase public class TreeGroupViewModel : ActivatableViewModelBase
{ {
private readonly IWindowService _windowService;
private readonly IProfileEditorService _profileEditorService; private readonly IProfileEditorService _profileEditorService;
private readonly IWindowService _windowService;
private BrushConfigurationWindowViewModel? _brushConfigurationWindowViewModel; private BrushConfigurationWindowViewModel? _brushConfigurationWindowViewModel;
private EffectConfigurationWindowViewModel? _effectConfigurationWindowViewModel; private EffectConfigurationWindowViewModel? _effectConfigurationWindowViewModel;
@ -41,12 +39,14 @@ public class TreeGroupViewModel : ActivatableViewModelBase
ProfileElementPropertyGroupViewModel.WhenAnyValue(vm => vm.IsExpanded).Subscribe(_ => this.RaisePropertyChanged(nameof(Children))).DisposeWith(d); ProfileElementPropertyGroupViewModel.WhenAnyValue(vm => vm.IsExpanded).Subscribe(_ => this.RaisePropertyChanged(nameof(Children))).DisposeWith(d);
Disposable.Create(CloseViewModels).DisposeWith(d); Disposable.Create(CloseViewModels).DisposeWith(d);
}); });
// TODO: Update ProfileElementPropertyGroupViewModel visibility on change (can remove the sub on line 41 as well then)
} }
public ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel { get; } public ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel { get; }
public LayerPropertyGroup LayerPropertyGroup => ProfileElementPropertyGroupViewModel.LayerPropertyGroup; public LayerPropertyGroup LayerPropertyGroup => ProfileElementPropertyGroupViewModel.LayerPropertyGroup;
public ObservableCollection<ActivatableViewModelBase>? Children => ProfileElementPropertyGroupViewModel.IsExpanded ? ProfileElementPropertyGroupViewModel.Children : null; public ObservableCollection<ViewModelBase>? Children => ProfileElementPropertyGroupViewModel.IsExpanded ? ProfileElementPropertyGroupViewModel.Children : null;
public LayerPropertyGroupType GroupType { get; private set; } public LayerPropertyGroupType GroupType { get; private set; }
@ -96,7 +96,7 @@ public class TreeGroupViewModel : ActivatableViewModelBase
// Find the BaseLayerEffect parameter, it is required by the base constructor so its there for sure // Find the BaseLayerEffect parameter, it is required by the base constructor so its there for sure
ParameterInfo effectParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerEffect).IsAssignableFrom(p.ParameterType)); ParameterInfo effectParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerEffect).IsAssignableFrom(p.ParameterType));
ConstructorArgument argument = new(effectParameter.Name!, layerEffect); ConstructorArgument argument = new(effectParameter.Name!, layerEffect);
EffectConfigurationViewModel viewModel = (EffectConfigurationViewModel)layerEffect.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument); EffectConfigurationViewModel viewModel = (EffectConfigurationViewModel) layerEffect.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument);
_effectConfigurationWindowViewModel = new EffectConfigurationWindowViewModel(viewModel, configurationViewModel); _effectConfigurationWindowViewModel = new EffectConfigurationWindowViewModel(viewModel, configurationViewModel);
await _windowService.ShowDialogAsync(_effectConfigurationWindowViewModel); await _windowService.ShowDialogAsync(_effectConfigurationWindowViewModel);
@ -138,7 +138,7 @@ public class TreeGroupViewModel : ActivatableViewModelBase
_effectConfigurationWindowViewModel?.Close(null); _effectConfigurationWindowViewModel?.Close(null);
_brushConfigurationWindowViewModel?.Close(null); _brushConfigurationWindowViewModel?.Close(null);
} }
private void DetermineGroupType() private void DetermineGroupType()
{ {
if (LayerPropertyGroup is LayerGeneralProperties) if (LayerPropertyGroup is LayerGeneralProperties)

View File

@ -4,16 +4,32 @@ using Artemis.UI.Shared.Services.PropertyInput;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
internal class TreePropertyViewModel<T> : ActivatableViewModelBase internal class TreePropertyViewModel<T> : ActivatableViewModelBase, ITreePropertyViewModel
{ {
public TreePropertyViewModel(LayerProperty<T> layerProperty, ProfileElementPropertyViewModel layerPropertyViewModel, IPropertyInputService propertyInputService) public TreePropertyViewModel(LayerProperty<T> layerProperty, ProfileElementPropertyViewModel profileElementPropertyViewModel, IPropertyInputService propertyInputService)
{ {
LayerProperty = layerProperty; LayerProperty = layerProperty;
LayerPropertyViewModel = layerPropertyViewModel; ProfileElementPropertyViewModel = profileElementPropertyViewModel;
PropertyInputViewModel = propertyInputService.CreatePropertyInputViewModel(LayerProperty); PropertyInputViewModel = propertyInputService.CreatePropertyInputViewModel(LayerProperty);
// TODO: Update ProfileElementPropertyViewModel visibility on change
} }
public LayerProperty<T> LayerProperty { get; } public LayerProperty<T> LayerProperty { get; }
public ProfileElementPropertyViewModel LayerPropertyViewModel { get; } public ProfileElementPropertyViewModel ProfileElementPropertyViewModel { get; }
public PropertyInputViewModel<T>? PropertyInputViewModel { get; } public PropertyInputViewModel<T>? PropertyInputViewModel { get; }
public bool HasDataBinding => LayerProperty.HasDataBinding;
public double GetDepth()
{
int depth = 0;
LayerPropertyGroup? current = LayerProperty.LayerPropertyGroup;
while (current != null)
{
depth++;
current = current.Parent;
}
return depth;
}
} }