From 6b309cb1f309dbbe81368653e99d2e9e7d2cad63 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 7 Sep 2020 23:43:43 +0200 Subject: [PATCH] Data bindings - Further implemented default converters --- .../Converters/FloatDataBindingConverter.cs | 46 ++++--- .../Converters/GeneralDataBindingConverter.cs | 28 ++-- .../Converters/IntDataBindingConverter.cs | 40 +++--- .../SKColorPartDataBindingConverter.cs | 45 +++---- .../Profile/DataBindings/DataBinding.cs | 89 ++++++++----- .../DataBindings/DataBindingConverter.cs | 121 ++++++++++++++++++ .../DataBindings/DataBindingModifier.cs | 2 +- .../DataBindings/DataBindingRegistration.cs | 11 +- .../DataBindings/IDataBindingConverter.cs | 57 --------- .../LayerProperties/BaseLayerProperty.cs | 9 +- .../Profile/LayerProperties/LayerProperty.cs | 34 ++++- .../Ninject/Factories/IVMFactory.cs | 2 +- .../DataBindings/DataBindingView.xaml | 55 ++++---- .../DataBindings/DataBindingViewModel.cs | 105 ++++++++++++--- .../DataBindings/DataBindingsViewModel.cs | 12 +- .../Timeline/TimelineEasingViewModel.cs | 8 +- .../Timeline/TimelinePropertyView.xaml | 5 - .../DataModels/GeneralDataModel.cs | 5 +- .../GeneralModule.cs | 14 +- .../ViewModels/GeneralViewModel.cs | 8 +- .../Views/GeneralView.xaml | 8 +- 21 files changed, 447 insertions(+), 257 deletions(-) create mode 100644 src/Artemis.Core/Models/Profile/DataBindings/DataBindingConverter.cs delete mode 100644 src/Artemis.Core/Models/Profile/DataBindings/IDataBindingConverter.cs diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Converters/FloatDataBindingConverter.cs b/src/Artemis.Core/Models/Profile/DataBindings/Converters/FloatDataBindingConverter.cs index 32879ff29..eb337ba54 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/Converters/FloatDataBindingConverter.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/Converters/FloatDataBindingConverter.cs @@ -3,40 +3,46 @@ namespace Artemis.Core { /// - public class FloatDataBindingConverter : IDataBindingConverter + public class FloatDataBindingConverter : DataBindingConverter { - /// - public Type SupportedType => typeof(float); - - /// - public bool SupportsSum => true; - - /// - public bool SupportsInterpolate => true; - - /// - public object Sum(BaseLayerProperty layerProperty, object a, object b) + public FloatDataBindingConverter() { - return (float) a + (float) b; + SupportedType = typeof(float); + SupportsSum = true; + SupportsInterpolate = true; } /// - public object Interpolate(BaseLayerProperty layerProperty, object a, object b, float progress) + public override object Sum(object a, object b) { - var diff = (float) b - (float) a; - return diff * progress; + return Convert.ToSingle(a) + Convert.ToSingle(b); } /// - public void ApplyValue(BaseLayerProperty layerProperty, object value) + public override object Interpolate(object a, object b, double progress) { - layerProperty.CurrentValue = value; + var floatA = Convert.ToSingle(a); + var floatB = Convert.ToSingle(b); + var diff = floatB - floatA; + return floatA + diff * progress; } /// - public object GetValue(BaseLayerProperty layerProperty) + public override void ApplyValue(object value) { - return layerProperty.CurrentValue; + var floatValue = Convert.ToSingle(value); + if (DataBinding.LayerProperty.PropertyDescription.MaxInputValue is float max) + floatValue = Math.Min(floatValue, max); + if (DataBinding.LayerProperty.PropertyDescription.MinInputValue is float min) + floatValue = Math.Max(floatValue, min); + + ValueSetter?.Invoke(floatValue); + } + + /// + public override object GetValue() + { + return ValueGetter?.Invoke(); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Converters/GeneralDataBindingConverter.cs b/src/Artemis.Core/Models/Profile/DataBindings/Converters/GeneralDataBindingConverter.cs index 637d5f437..561ef8784 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/Converters/GeneralDataBindingConverter.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/Converters/GeneralDataBindingConverter.cs @@ -3,39 +3,37 @@ namespace Artemis.Core { /// - public class GeneralDataBindingConverter : IDataBindingConverter + public class GeneralDataBindingConverter : DataBindingConverter { - /// - public Type SupportedType => typeof(object); + public GeneralDataBindingConverter() + { + SupportedType = typeof(object); + SupportsSum = false; + SupportsInterpolate = false; + } /// - public bool SupportsSum => false; - - /// - public bool SupportsInterpolate => false; - - /// - public object Sum(BaseLayerProperty layerProperty, object a, object b) + public override object Sum(object a, object b) { throw new NotSupportedException(); } /// - public object Interpolate(BaseLayerProperty layerProperty, object a, object b, float progress) + public override object Interpolate(object a, object b, double progress) { throw new NotSupportedException(); } /// - public void ApplyValue(BaseLayerProperty layerProperty, object value) + public override void ApplyValue(object value) { - layerProperty.CurrentValue = value; + ValueSetter?.Invoke(value); } /// - public object GetValue(BaseLayerProperty layerProperty) + public override object GetValue() { - return layerProperty.CurrentValue; + return ValueGetter?.Invoke(); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Converters/IntDataBindingConverter.cs b/src/Artemis.Core/Models/Profile/DataBindings/Converters/IntDataBindingConverter.cs index c34eaf779..c3466a43f 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/Converters/IntDataBindingConverter.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/Converters/IntDataBindingConverter.cs @@ -3,40 +3,46 @@ namespace Artemis.Core { /// - public class IntDataBindingConverter : IDataBindingConverter + public class IntDataBindingConverter : DataBindingConverter { - /// - public Type SupportedType => typeof(int); + public IntDataBindingConverter() + { + SupportedType = typeof(int); + SupportsSum = true; + SupportsInterpolate = true; + } + + /// + /// Gets or sets the mode used for rounding during interpolation. Defaults to + /// + /// + public MidpointRounding InterpolationRoundingMode { get; set; } = MidpointRounding.AwayFromZero; /// - public bool SupportsSum => true; - - /// - public bool SupportsInterpolate => true; - - /// - public object Sum(BaseLayerProperty layerProperty, object a, object b) + public override object Sum(object a, object b) { return (int) a + (int) b; } /// - public object Interpolate(BaseLayerProperty layerProperty, object a, object b, float progress) + public override object Interpolate(object a, object b, double progress) { - var diff = (int) b - (int) a; - return diff * progress; + var intA = (int) a; + var intB = (int) b; + var diff = intB - intA; + return (int) Math.Round(intA + diff * progress, InterpolationRoundingMode); } /// - public void ApplyValue(BaseLayerProperty layerProperty, object value) + public override void ApplyValue(object value) { - layerProperty.CurrentValue = value; + ValueSetter?.Invoke(value); } /// - public object GetValue(BaseLayerProperty layerProperty) + public override object GetValue() { - return layerProperty.CurrentValue; + return ValueGetter?.Invoke(); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Converters/Internal/SKColorPartDataBindingConverter.cs b/src/Artemis.Core/Models/Profile/DataBindings/Converters/Internal/SKColorPartDataBindingConverter.cs index 8176e9efc..3073a9721 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/Converters/Internal/SKColorPartDataBindingConverter.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/Converters/Internal/SKColorPartDataBindingConverter.cs @@ -4,52 +4,41 @@ using SkiaSharp; namespace Artemis.Core { // This is internal because it's mainly a proof-of-concept - internal class SKColorPartDataBindingConverter : IDataBindingConverter + internal class SKColorPartDataBindingConverter : DataBindingConverter { private readonly Channel _channel; public SKColorPartDataBindingConverter(Channel channel) { _channel = channel; - } - // This depends on what channel was passed - public Type SupportedType - { - get + SupportsSum = true; + SupportsInterpolate = true; + SupportedType = _channel switch { - switch (_channel) - { - case Channel.Alpha: - case Channel.Red: - case Channel.Green: - case Channel.Blue: - return typeof(byte); - case Channel.Hue: - return typeof(float); - default: - throw new ArgumentOutOfRangeException(); - } - } + Channel.Alpha => typeof(byte), + Channel.Red => typeof(byte), + Channel.Green => typeof(byte), + Channel.Blue => typeof(byte), + Channel.Hue => typeof(float), + _ => throw new ArgumentOutOfRangeException() + }; } - public bool SupportsSum => true; - public bool SupportsInterpolate => true; - - public object Sum(BaseLayerProperty layerProperty, object a, object b) + public override object Sum(object a, object b) { return (float) a + (float) b; } - public object Interpolate(BaseLayerProperty layerProperty, object a, object b, float progress) + public override object Interpolate(object a, object b, double progress) { var diff = (float) b - (float) a; return diff * progress; } - public void ApplyValue(BaseLayerProperty layerProperty, object value) + public override void ApplyValue(object value) { - var property = (SKColorLayerProperty) layerProperty; + var property = (SKColorLayerProperty) DataBinding.LayerProperty; switch (_channel) { case Channel.Alpha: @@ -73,9 +62,9 @@ namespace Artemis.Core } } - public object GetValue(BaseLayerProperty layerProperty) + public override object GetValue() { - var property = (SKColorLayerProperty) layerProperty; + var property = (SKColorLayerProperty) DataBinding.LayerProperty; switch (_channel) { case Channel.Alpha: diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs index 2d926fea3..0bb00863f 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs @@ -15,11 +15,11 @@ namespace Artemis.Core public class DataBinding { private readonly List _modifiers = new List(); - private bool _isInitialized; private object _currentValue; + private bool _isInitialized; private object _previousValue; - private float _easingProgress; + private TimeSpan _easingProgress; internal DataBinding(DataBindingRegistration dataBindingRegistration) { @@ -38,30 +38,23 @@ namespace Artemis.Core ApplyToDataBinding(); } - private void ApplyRegistration(DataBindingRegistration dataBindingRegistration) - { - Converter = dataBindingRegistration.Converter; - Registration = dataBindingRegistration; - TargetProperty = dataBindingRegistration.Property; - } - /// /// Gets the layer property this data binding targets /// public BaseLayerProperty LayerProperty { get; } /// - /// Gets the data binding registration this data binding is based upon + /// Gets the data binding registration this data binding is based upon /// public DataBindingRegistration Registration { get; private set; } /// - /// Gets the converter used to apply this data binding to the + /// Gets the converter used to apply this data binding to the /// - public IDataBindingConverter Converter { get; private set; } + public DataBindingConverter Converter { get; private set; } /// - /// Gets the property on the this data binding targets + /// Gets the property on the this data binding targets /// public PropertyInfo TargetProperty { get; private set; } @@ -167,21 +160,32 @@ namespace Artemis.Core var value = Convert.ChangeType(dataBindingValue, TargetProperty.PropertyType); // If no easing is to be applied simple return whatever the current value is - if (EasingTime == TimeSpan.Zero || Converter.SupportsInterpolate) + if (EasingTime == TimeSpan.Zero || !Converter.SupportsInterpolate) return value; // If the value changed, update the current and previous values used for easing if (!Equals(value, _currentValue)) - { - _previousValue = GetInterpolatedValue(); - _currentValue = value; - _easingProgress = 0f; - } + ResetEasing(value); // Apply interpolation between the previous and current value return GetInterpolatedValue(); } + private void ResetEasing(object value) + { + _previousValue = GetInterpolatedValue(); + _currentValue = value; + _easingProgress = TimeSpan.Zero; + } + + /// + /// Returns the type of the target property of this data binding + /// + public Type GetTargetType() + { + return TargetProperty.PropertyType; + } + /// /// Returns the type of the source property of this data binding /// @@ -191,21 +195,26 @@ namespace Artemis.Core } /// - /// Updates the smoothing progress of the data binding + /// Updates the smoothing progress of the data binding /// - /// + /// The time in seconds that passed since the last update public void Update(double deltaTime) { - + _easingProgress = _easingProgress.Add(TimeSpan.FromSeconds(deltaTime)); + if (_easingProgress > EasingTime) + _easingProgress = EasingTime; } /// - /// Updates the value on the according to the data binding + /// Updates the value on the according to the data binding /// public void ApplyToProperty() { - var value = GetValue(Converter.GetValue(LayerProperty)); - Converter.ApplyValue(LayerProperty, GetValue(value)); + if (Converter == null) + return; + + var value = GetValue(Converter.GetValue()); + Converter.ApplyValue(GetValue(value)); } internal void ApplyToEntity() @@ -232,8 +241,7 @@ namespace Artemis.Core internal void ApplyToDataBinding() { // General - Registration = LayerProperty.DataBindingRegistrations.FirstOrDefault(p => p.Property.Name == Entity.TargetProperty); - TargetProperty = Registration?.Property; + ApplyRegistration(LayerProperty.DataBindingRegistrations.FirstOrDefault(p => p.Property.Name == Entity.TargetProperty)); Mode = (DataBindingMode) Entity.DataBindingMode; EasingTime = Entity.EasingTime; @@ -266,14 +274,33 @@ namespace Artemis.Core _isInitialized = true; } + private void ApplyRegistration(DataBindingRegistration dataBindingRegistration) + { + if (dataBindingRegistration != null) + dataBindingRegistration.DataBinding = this; + + Converter = dataBindingRegistration?.Converter; + Registration = dataBindingRegistration; + TargetProperty = dataBindingRegistration?.Property; + + if (GetTargetType().IsValueType) + { + if (_currentValue == null) + _currentValue = GetTargetType().GetDefault(); + if (_previousValue == null) + _previousValue = _currentValue; + } + + Converter?.Initialize(this); + } + private object GetInterpolatedValue() { - if (_easingProgress == 0f) - return _previousValue; - if (_easingProgress == 1f || !Converter.SupportsInterpolate) + if (_easingProgress == EasingTime || !Converter.SupportsInterpolate) return _currentValue; - return Converter.Interpolate(LayerProperty, _previousValue, _currentValue, _easingProgress); + var easingAmount = _easingProgress.TotalSeconds / EasingTime.TotalSeconds; + return Converter.Interpolate(_previousValue, _currentValue, Easings.Interpolate(easingAmount, EasingFunction)); } private void CreateExpression() diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingConverter.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingConverter.cs new file mode 100644 index 000000000..3ee8756d2 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingConverter.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace Artemis.Core +{ + /// + /// A data binding converter that acts as the bridge between a and a + /// + /// + public abstract class DataBindingConverter + { + internal Func ValueGetter { get; set; } + internal Action ValueSetter { get; set; } + + /// + /// Gets the data binding this converter is applied to + /// + public DataBinding DataBinding { get; private set; } + + /// + /// Gets the type this converter supports + /// + public Type SupportedType { get; protected set; } + + /// + /// Gets whether or not this data binding converter supports the method + /// + public bool SupportsSum { get; protected set; } + + /// + /// Gets whether or not this data binding converter supports the method + /// + public bool SupportsInterpolate { get; protected set; } + + /// + /// Called when the data binding converter has been initialized and the is available + /// + protected virtual void OnInitialized() + { + } + + /// + /// Returns the sum of and + /// + public abstract object Sum(object a, object b); + + /// + /// Returns the the interpolated value between and on a scale (generally) + /// between 0.0 and 1.0 defined by the + /// Note: The progress may go be negative or go beyond 1.0 depending on the easing method used + /// + /// The value to interpolate away from + /// The value to interpolate towards + /// The progress of the interpolation between 0.0 and 1.0 + /// + public abstract object Interpolate(object a, object b, double progress); + + /// + /// Applies the to the layer property + /// + /// + public abstract void ApplyValue(object value); + + /// + /// Returns the current base value of the data binding + /// + public abstract object GetValue(); + + internal void Initialize(DataBinding dataBinding) + { + DataBinding = dataBinding; + ValueGetter = CreateValueGetter(); + ValueSetter = CreateValueSetter(); + + OnInitialized(); + } + + private Func CreateValueGetter() + { + if (DataBinding.TargetProperty?.DeclaringType == null) + return null; + + var getterMethod = DataBinding.TargetProperty.GetGetMethod(); + if (getterMethod == null) + return null; + + var constant = Expression.Constant(DataBinding.LayerProperty); + // The path is null if the registration is applied to the root (LayerProperty.CurrentValue) + var property = DataBinding.Registration.Path == null + ? Expression.Property(constant, DataBinding.TargetProperty) + : (MemberExpression) DataBinding.Registration.Path.Split('.').Aggregate(constant, Expression.Property); + + // The get method should cast to the object since it receives whatever type the property is + var body = Expression.Convert(property, typeof(object)); + var lambda = Expression.Lambda>(body); + + return lambda.Compile(); + } + + private Action CreateValueSetter() + { + if (DataBinding.TargetProperty?.DeclaringType == null) + return null; + + var setterMethod = DataBinding.TargetProperty.GetSetMethod(); + if (setterMethod == null) + return null; + + var constant = Expression.Constant(DataBinding.LayerProperty); + var propertyValue = Expression.Parameter(typeof(object), "propertyValue"); + + // The assign method should cast to the proper type since it receives an object + var body = Expression.Call(constant, setterMethod, Expression.Convert(propertyValue, DataBinding.TargetProperty.PropertyType)); + var lambda = Expression.Lambda>(body, propertyValue); + return lambda.Compile(); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingModifier.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingModifier.cs index cff4ea1c7..6eac1806d 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingModifier.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingModifier.cs @@ -102,7 +102,7 @@ namespace Artemis.Core return ModifierType.Apply(currentValue, value); } - if (ParameterType == ProfileRightSideType.Static) + if (ParameterType == ProfileRightSideType.Static && ModifierType != null) return ModifierType.Apply(currentValue, ParameterStaticValue); return currentValue; diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs index 9064bc32b..f8d860bf2 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs @@ -5,15 +5,18 @@ namespace Artemis.Core { public class DataBindingRegistration { - internal DataBindingRegistration(BaseLayerProperty layerProperty, PropertyInfo property, IDataBindingConverter converter) + internal DataBindingRegistration(BaseLayerProperty layerProperty, PropertyInfo property, DataBindingConverter converter, string path) { LayerProperty = layerProperty ?? throw new ArgumentNullException(nameof(layerProperty)); Property = property ?? throw new ArgumentNullException(nameof(property)); Converter = converter ?? throw new ArgumentNullException(nameof(converter)); + Path = path; } - public BaseLayerProperty LayerProperty { get; set; } - public PropertyInfo Property { get; set; } - public IDataBindingConverter Converter { get; set; } + public DataBinding DataBinding { get; internal set; } + public BaseLayerProperty LayerProperty { get; } + public PropertyInfo Property { get; } + public DataBindingConverter Converter { get; } + public string Path { get; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingConverter.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingConverter.cs deleted file mode 100644 index 39f147436..000000000 --- a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingConverter.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; - -namespace Artemis.Core -{ - /// - /// A data binding converter that acts as the bridge between a and a - /// - /// - public interface IDataBindingConverter - { - /// - /// Gets the type this converter supports - /// - Type SupportedType { get; } - - /// - /// Gets whether or not this data binding converter supports the method - /// - bool SupportsSum { get; } - - /// - /// Gets whether or not this data binding converter supports the method - /// - bool SupportsInterpolate { get; } - - /// - /// Returns the sum of and - /// - /// The layer property this sum is done for - object Sum(BaseLayerProperty layerProperty, object a, object b); - - /// - /// Returns the the interpolated value between and on a scale (generally) - /// between 0.0 and 1.0 defined by the - /// Note: The progress may go be negative or go beyond 1.0 depending on the easing method used - /// - /// The layer property this interpolation is done for - /// The value to interpolate away from - /// The value to interpolate towards - /// The progress of the interpolation between 0.0 and 1.0 - /// - object Interpolate(BaseLayerProperty layerProperty, object a, object b, float progress); - - /// - /// Applies the to the layer property - /// - /// The layer property this value is to be applied to - /// - void ApplyValue(BaseLayerProperty layerProperty, object value); - - /// - /// Returns the current base value of the data binding - /// - /// The layer property this value must be retrieved from - object GetValue(BaseLayerProperty layerProperty); - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs index 478be6e88..10f0a53f6 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs @@ -32,7 +32,7 @@ namespace Artemis.Core get => _baseValue; set { - if (value.GetType() != GetPropertyType()) + if (value != null && value.GetType() != GetPropertyType()) throw new ArtemisCoreException("Cannot update base value because of a type mismatch"); _baseValue = value; } @@ -46,7 +46,7 @@ namespace Artemis.Core get => _currentValue; set { - if (value.GetType() != GetPropertyType()) + if (value != null && value.GetType() != GetPropertyType()) throw new ArtemisCoreException("Cannot update current value because of a type mismatch"); _currentValue = value; } @@ -61,7 +61,7 @@ namespace Artemis.Core get => _defaultValue; set { - if (value.GetType() != GetPropertyType()) + if (value != null && value.GetType() != GetPropertyType()) throw new ArtemisCoreException("Cannot update default value because of a type mismatch"); _defaultValue = value; } @@ -186,6 +186,9 @@ namespace Artemis.Core /// The newly created data binding public DataBinding EnableDataBinding(DataBindingRegistration dataBindingRegistration) { + if (dataBindingRegistration.LayerProperty != this) + throw new ArtemisCoreException("Cannot enable a data binding using a data binding registration of a different layer property"); + var dataBinding = new DataBinding(dataBindingRegistration); _dataBindings.Add(dataBinding); diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 22546ecd3..d4a762a64 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using Artemis.Storage.Entities.Profile; using Newtonsoft.Json; @@ -31,7 +32,7 @@ namespace Artemis.Core /// public new T BaseValue { - get => (T) base.BaseValue; + get => base.BaseValue != null ? (T) base.BaseValue : default; set { if (Equals(base.BaseValue, value)) @@ -47,7 +48,7 @@ namespace Artemis.Core /// public new T CurrentValue { - get => (T) base.CurrentValue; + get => base.CurrentValue != null ? (T) base.CurrentValue : default; set => base.CurrentValue = value; } @@ -57,7 +58,7 @@ namespace Artemis.Core /// public new T DefaultValue { - get => (T) base.DefaultValue; + get => base.DefaultValue != null ? (T) base.DefaultValue : default; set => base.DefaultValue = value; } @@ -311,14 +312,35 @@ namespace Artemis.Core #region Data bindings - public void RegisterDataBindingProperty(Expression> propertyLambda, IDataBindingConverter converter) + public void RegisterDataBindingProperty(Expression> propertyLambda, DataBindingConverter converter) { - var propertyInfo = ReflectionUtilities.GetPropertyInfo(CurrentValue, propertyLambda); + // If the lambda references to itself, use the property info of public new T CurrentValue + PropertyInfo propertyInfo; + string path = null; + if (propertyLambda.Parameters[0] == propertyLambda.Body) + { + propertyInfo = GetType().GetProperties().FirstOrDefault(p => p.Name == nameof(CurrentValue) && p.PropertyType == typeof(T)); + } + else + { + propertyInfo = ReflectionUtilities.GetPropertyInfo(CurrentValue, propertyLambda); + // Deconstruct the lambda + var current = (MemberExpression) propertyLambda.Body; + path = current.Member.Name; + while (current.Expression is MemberExpression memberExpression) + { + path = current.Member.Name + "." + path; + current = memberExpression; + } + } + if (converter.SupportedType != propertyInfo.PropertyType) + { throw new ArtemisCoreException($"Cannot register data binding property for property {propertyInfo.Name} " + "because the provided converter does not support the property's type"); + } - _dataBindingRegistrations.Add(new DataBindingRegistration(this, propertyInfo, converter)); + _dataBindingRegistrations.Add(new DataBindingRegistration(this, propertyInfo, converter, path)); } private void UpdateDataBindings(double deltaTime) diff --git a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs index 137a6d77d..68df7dd44 100644 --- a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -81,7 +81,7 @@ namespace Artemis.UI.Ninject.Factories public interface IDataBindingsVmFactory : IVmFactory { DataBindingsViewModel DataBindingsViewModel(BaseLayerProperty layerProperty); - DataBindingViewModel DataBindingViewModel(BaseLayerProperty layerProperty, PropertyInfo targetProperty); + DataBindingViewModel DataBindingViewModel(DataBindingRegistration registration); DataBindingModifierViewModel DataBindingModifierViewModel(DataBindingModifier modifier); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml index 38e7f3806..0afdbd491 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml @@ -42,43 +42,46 @@ - - Replace value - - - Add to value - - - Subtract from value - + SelectedValue="{Binding SelectedDataBindingMode}" ItemsSource="{Binding DataBindingModes}" SelectedValuePath="Value" DisplayMemberPath="Description" > - - Ease in - - - Ease out - - - Ease in and out - + IsEnabled="{Binding IsEasingTimeEnabled}" + SelectedItem="{Binding SelectedEasingViewModel}" + ItemsSource="{Binding EasingViewModels}"> + + + + + + + + + + + + + @@ -102,10 +105,10 @@ Input - + Output - + diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs index 6c65e55bf..f5d110c1e 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs @@ -1,9 +1,9 @@ using System; using System.Linq; -using System.Reflection; using System.Timers; using Artemis.Core; using Artemis.UI.Ninject.Factories; +using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline; using Artemis.UI.Shared; using Artemis.UI.Shared.Input; using Artemis.UI.Shared.Services; @@ -22,25 +22,30 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings private DataModelDynamicViewModel _targetSelectionViewModel; private object _testInputValue; private object _testResultValue; + private TimelineEasingViewModel _selectedEasingViewModel; + private DataBindingMode _selectedDataBindingMode; + private int _easingTime; + private bool _isEasingTimeEnabled; + private bool _updating; - public DataBindingViewModel(BaseLayerProperty layerProperty, - PropertyInfo targetProperty, + public DataBindingViewModel(DataBindingRegistration registration, IProfileEditorService profileEditorService, IDataModelUIService dataModelUIService, IDataBindingsVmFactory dataBindingsVmFactory) { + Registration = registration; _profileEditorService = profileEditorService; _dataModelUIService = dataModelUIService; _dataBindingsVmFactory = dataBindingsVmFactory; _updateTimer = new Timer(500); - DisplayName = targetProperty.Name.ToUpper(); + DisplayName = Registration.Property.Name.ToUpper(); + + DataBindingModes = new BindableCollection(EnumUtilities.GetAllValuesAndDescriptions(typeof(DataBindingMode))); + EasingViewModels = new BindableCollection(); ModifierViewModels = new BindableCollection(); - LayerProperty = layerProperty; - TargetProperty = targetProperty; - DataBinding = layerProperty.DataBindings.FirstOrDefault(d => d.TargetProperty == targetProperty); - + DataBinding = Registration.DataBinding; if (DataBinding != null) DataBinding.ModifiersUpdated += DataBindingOnModifiersUpdated; @@ -50,11 +55,49 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings Execute.PostToUIThread(Initialize); } - public BaseLayerProperty LayerProperty { get; } - public PropertyInfo TargetProperty { get; } + public DataBindingRegistration Registration { get; } public string DisplayName { get; } + + public BindableCollection DataBindingModes { get; } + public BindableCollection EasingViewModels { get; } public BindableCollection ModifierViewModels { get; } + public DataBindingMode SelectedDataBindingMode + { + get => _selectedDataBindingMode; + set => SetAndNotify(ref _selectedDataBindingMode, value); + } + + public TimelineEasingViewModel SelectedEasingViewModel + { + get => _selectedEasingViewModel; + set + { + if (!SetAndNotify(ref _selectedEasingViewModel, value)) return; + ApplyChanges(); + } + } + + public int EasingTime + { + get => _easingTime; + set + { + if (!SetAndNotify(ref _easingTime, value)) return; + ApplyChanges(); + } + } + + public bool IsEasingTimeEnabled + { + get => _isEasingTimeEnabled; + set + { + if (!SetAndNotify(ref _isEasingTimeEnabled, value)) return; + ApplyChanges(); + } + } + public DataModelDynamicViewModel TargetSelectionViewModel { get => _targetSelectionViewModel; @@ -101,23 +144,22 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings if (DataBinding != null) return; - DataBinding = LayerProperty.AddDataBinding(TargetProperty); + DataBinding = Registration.LayerProperty.EnableDataBinding(Registration); DataBinding.ModifiersUpdated += DataBindingOnModifiersUpdated; Update(); _profileEditorService.UpdateSelectedProfileElement(); } - public void RemoveDataBinding() { if (DataBinding == null) return; - var toRemove = DataBinding; + var toDisable = DataBinding; DataBinding = null; - LayerProperty.RemoveDataBinding(toRemove); - toRemove.ModifiersUpdated -= DataBindingOnModifiersUpdated; + Registration.LayerProperty.DisableDataBinding(toDisable); + toDisable.ModifiersUpdated -= DataBindingOnModifiersUpdated; Update(); _profileEditorService.UpdateSelectedProfileElement(); @@ -136,6 +178,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings private void Initialize() { + EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)).Cast().Select(v => new TimelineEasingViewModel(null, v))); TargetSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule()); TargetSelectionViewModel.PropertySelected += TargetSelectionViewModelOnPropertySelected; @@ -147,17 +190,43 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings private void Update() { + if (_updating) + return; + if (DataBinding == null) { TargetSelectionViewModel.IsEnabled = false; + IsEasingTimeEnabled = false; return; } + _updating = true; + + SelectedDataBindingMode = DataBinding.Mode; + EasingTime = (int) DataBinding.EasingTime.TotalMilliseconds; + SelectedEasingViewModel = EasingViewModels.First(vm => vm.EasingFunction == DataBinding.EasingFunction); + IsEasingTimeEnabled = EasingTime > 0; + TargetSelectionViewModel.IsEnabled = true; TargetSelectionViewModel.PopulateSelectedPropertyViewModel(DataBinding.SourceDataModel, DataBinding.SourcePropertyPath); TargetSelectionViewModel.FilterTypes = new[] {DataBinding.TargetProperty.PropertyType}; UpdateModifierViewModels(); + + _updating = false; + } + + private void ApplyChanges() + { + if (_updating) + return; + + DataBinding.Mode = SelectedDataBindingMode; + DataBinding.EasingTime = TimeSpan.FromMilliseconds(EasingTime); + DataBinding.EasingFunction = SelectedEasingViewModel?.EasingFunction ?? Easings.Functions.Linear; + + _profileEditorService.UpdateSelectedProfileElement(); + Update(); } private void DataBindingOnModifiersUpdated(object sender, EventArgs e) @@ -179,10 +248,10 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings private void UpdateTestResult() { var currentValue = TargetSelectionViewModel.SelectedPropertyViewModel?.GetCurrentValue(); - if (currentValue == null && TargetProperty.PropertyType.IsValueType) - currentValue = Activator.CreateInstance(TargetProperty.PropertyType); + if (currentValue == null && Registration.Property.PropertyType.IsValueType) + currentValue = Activator.CreateInstance(Registration.Property.PropertyType); - TestInputValue = Convert.ChangeType(currentValue, TargetProperty.PropertyType); + TestInputValue = Convert.ChangeType(currentValue, Registration.Property.PropertyType); TestResultValue = DataBinding?.GetValue(TestInputValue); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs index 0f58e2313..85680795b 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs @@ -37,19 +37,19 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings DataBindingViewModel = null; DataBindingsTabsViewModel = null; - var properties = LayerProperty.GetDataBindingProperties(); - if (properties == null || properties.Count == 0) + var registrations = LayerProperty.DataBindingRegistrations; + if (registrations == null || registrations.Count == 0) return; // Create a data binding VM for each data bindable property. These VMs will be responsible for retrieving // and creating the actual data bindings - if (properties.Count == 1) - DataBindingViewModel = _dataBindingsVmFactory.DataBindingViewModel(LayerProperty, properties.First()); + if (registrations.Count == 1) + DataBindingViewModel = _dataBindingsVmFactory.DataBindingViewModel(registrations.First()); else { DataBindingsTabsViewModel = new DataBindingsTabsViewModel(); - foreach (var dataBindingProperty in properties) - DataBindingsTabsViewModel.Tabs.Add(_dataBindingsVmFactory.DataBindingViewModel(LayerProperty, dataBindingProperty)); + foreach (var registration in registrations) + DataBindingsTabsViewModel.Tabs.Add(_dataBindingsVmFactory.DataBindingViewModel(registration)); } } } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineEasingViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineEasingViewModel.cs index 4459ac59b..a89e161cb 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineEasingViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineEasingViewModel.cs @@ -12,8 +12,12 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline public TimelineEasingViewModel(TimelineKeyframeViewModel keyframeViewModel, Easings.Functions easingFunction) { - _keyframeViewModel = keyframeViewModel; - _isEasingModeSelected = keyframeViewModel.BaseLayerPropertyKeyframe.EasingFunction == easingFunction; + // Can be null if used by DataBindingViewModel because I'm lazy + if (keyframeViewModel != null) + { + _keyframeViewModel = keyframeViewModel; + _isEasingModeSelected = keyframeViewModel.BaseLayerPropertyKeyframe.EasingFunction == easingFunction; + } EasingFunction = easingFunction; Description = easingFunction.Humanize(); diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyView.xaml index 93b1a3a3d..5fcb44d45 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyView.xaml @@ -102,11 +102,6 @@ - - - - - diff --git a/src/Plugins/Artemis.Plugins.Modules.General/DataModels/GeneralDataModel.cs b/src/Plugins/Artemis.Plugins.Modules.General/DataModels/GeneralDataModel.cs index 3c678439a..030909ace 100644 --- a/src/Plugins/Artemis.Plugins.Modules.General/DataModels/GeneralDataModel.cs +++ b/src/Plugins/Artemis.Plugins.Modules.General/DataModels/GeneralDataModel.cs @@ -28,7 +28,8 @@ namespace Artemis.Plugins.Modules.General.DataModels public class TimeDataModel : DataModel { - public DateTime CurrentTime { get; set; } - public DateTime CurrentTimeUTC { get; set; } + public DateTimeOffset CurrentTime { get; set; } + public long SecondsSinceUnixEpoch { get; set; } + public TimeSpan TimeSinceMidnight { get; set; } } } \ No newline at end of file diff --git a/src/Plugins/Artemis.Plugins.Modules.General/GeneralModule.cs b/src/Plugins/Artemis.Plugins.Modules.General/GeneralModule.cs index 92b7ae2f8..ba21a0597 100644 --- a/src/Plugins/Artemis.Plugins.Modules.General/GeneralModule.cs +++ b/src/Plugins/Artemis.Plugins.Modules.General/GeneralModule.cs @@ -20,10 +20,10 @@ namespace Artemis.Plugins.Modules.General ExpandsDataModel = true; ModuleTabs = new List {new ModuleTab("General")}; - DataModel.TestTimeList.Add(new TimeDataModel {CurrentTime = DateTime.Now.AddDays(1), CurrentTimeUTC = DateTime.UtcNow.AddDays(1)}); - DataModel.TestTimeList.Add(new TimeDataModel {CurrentTime = DateTime.Now.AddDays(2), CurrentTimeUTC = DateTime.UtcNow.AddDays(2)}); - DataModel.TestTimeList.Add(new TimeDataModel {CurrentTime = DateTime.Now.AddDays(3), CurrentTimeUTC = DateTime.UtcNow.AddDays(3)}); - DataModel.TestTimeList.Add(new TimeDataModel {CurrentTime = DateTime.Now.AddDays(4), CurrentTimeUTC = DateTime.UtcNow.AddDays(4)}); + DataModel.TestTimeList.Add(new TimeDataModel {CurrentTime = DateTimeOffset.Now.AddDays(1)}); + DataModel.TestTimeList.Add(new TimeDataModel {CurrentTime = DateTimeOffset.Now.AddDays(2)}); + DataModel.TestTimeList.Add(new TimeDataModel {CurrentTime = DateTimeOffset.Now.AddDays(3)}); + DataModel.TestTimeList.Add(new TimeDataModel {CurrentTime = DateTimeOffset.Now.AddDays(4)}); } public override void DisablePlugin() @@ -40,9 +40,9 @@ namespace Artemis.Plugins.Modules.General public override void Update(double deltaTime) { - DataModel.TimeDataModel.CurrentTime = DateTime.Now; - DataModel.TimeDataModel.CurrentTimeUTC = DateTime.UtcNow; - + DataModel.TimeDataModel.CurrentTime = DateTimeOffset.Now; + DataModel.TimeDataModel.SecondsSinceUnixEpoch = DateTimeOffset.Now.ToUnixTimeSeconds(); + DataModel.TimeDataModel.TimeSinceMidnight = DateTimeOffset.Now - DateTimeOffset.Now.Date; UpdateCurrentWindow(); } diff --git a/src/Plugins/Artemis.Plugins.Modules.General/ViewModels/GeneralViewModel.cs b/src/Plugins/Artemis.Plugins.Modules.General/ViewModels/GeneralViewModel.cs index 5f03501c0..6c8788095 100644 --- a/src/Plugins/Artemis.Plugins.Modules.General/ViewModels/GeneralViewModel.cs +++ b/src/Plugins/Artemis.Plugins.Modules.General/ViewModels/GeneralViewModel.cs @@ -11,14 +11,14 @@ namespace Artemis.Plugins.Modules.General.ViewModels public GeneralModule GeneralModule { get; } - public void ShowUTCTimeInDataModel() + public void ShowTimeInDataModel() { - GeneralModule.ShowProperty(model => model.TimeDataModel.CurrentTimeUTC); + GeneralModule.ShowProperty(model => model.TimeDataModel.CurrentTime); } - public void HideUTCTimeInDataModel() + public void HideTimeInDataModel() { - GeneralModule.HideProperty(model => model.TimeDataModel.CurrentTimeUTC); + GeneralModule.HideProperty(model => model.TimeDataModel.CurrentTime); } } } \ No newline at end of file diff --git a/src/Plugins/Artemis.Plugins.Modules.General/Views/GeneralView.xaml b/src/Plugins/Artemis.Plugins.Modules.General/Views/GeneralView.xaml index 414fe8ccc..228bbe4c3 100644 --- a/src/Plugins/Artemis.Plugins.Modules.General/Views/GeneralView.xaml +++ b/src/Plugins/Artemis.Plugins.Modules.General/Views/GeneralView.xaml @@ -11,12 +11,12 @@ - - \ No newline at end of file