diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings index babfa2469..44ebfc334 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -9,7 +9,11 @@ True True True + True + True + True True + True True True True diff --git a/src/Artemis.Core/Events/DataBindingPropertyUpdatedEvent.cs b/src/Artemis.Core/Events/DataBindingPropertyUpdatedEvent.cs new file mode 100644 index 000000000..52c8a99fa --- /dev/null +++ b/src/Artemis.Core/Events/DataBindingPropertyUpdatedEvent.cs @@ -0,0 +1,17 @@ +using System; + +namespace Artemis.Core +{ + public class DataBindingPropertyUpdatedEvent : EventArgs + { + public DataBindingPropertyUpdatedEvent(T value) + { + Value = value; + } + + /// + /// The updated value that should be applied to the layer property + /// + public T Value { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Extensions/TypeExtensions.cs b/src/Artemis.Core/Extensions/TypeExtensions.cs index 027610874..9b82bf445 100644 --- a/src/Artemis.Core/Extensions/TypeExtensions.cs +++ b/src/Artemis.Core/Extensions/TypeExtensions.cs @@ -86,5 +86,13 @@ namespace Artemis.Core ); return castable; } + + /// + /// Returns the default value of the given type + /// + public static object GetDefault(this Type type) + { + return type.IsValueType ? Activator.CreateInstance(type) : null; + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Converters/FloatDataBindingConverter.cs b/src/Artemis.Core/Models/Profile/DataBindings/Converters/FloatDataBindingConverter.cs new file mode 100644 index 000000000..c0d8a49be --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataBindings/Converters/FloatDataBindingConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Artemis.Core.Models.Profile.DataBindings.Converters +{ + public class FloatDataBindingConverter : IDataBindingConverter + { + public BaseLayerProperty BaseLayerProperty { get; set; } + public object Sum(object a, object b) + { + return (float) a + (float) b; + } + + public object Interpolate(object a, object b, float progress) + { + throw new NotImplementedException(); + } + + public void ApplyValue(object value) + { + throw new NotImplementedException(); + } + + public object GetValue() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs index 9442c07b9..9e34edc09 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs @@ -17,15 +17,23 @@ namespace Artemis.Core private readonly List _modifiers = new List(); private bool _isInitialized; - internal DataBinding(BaseLayerProperty layerProperty, PropertyInfo targetProperty) + private object _currentValue; + private object _previousValue; + private float _easingProgress; + + internal DataBinding(DataBindingRegistration dataBindingRegistration) { - LayerProperty = layerProperty; - TargetProperty = targetProperty; + LayerProperty = dataBindingRegistration.LayerProperty; + TargetProperty = dataBindingRegistration.Property; + Registration = dataBindingRegistration; + Entity = new DataBindingEntity(); ApplyToEntity(); } + public DataBindingRegistration Registration { get; private set; } + internal DataBinding(BaseLayerProperty layerProperty, DataBindingEntity entity) { LayerProperty = layerProperty; @@ -87,6 +95,8 @@ namespace Artemis.Core { modifier.DataBinding = this; _modifiers.Add(modifier); + + OnModifiersUpdated(); } } @@ -99,6 +109,8 @@ namespace Artemis.Core { modifier.DataBinding = null; _modifiers.Remove(modifier); + + OnModifiersUpdated(); } } @@ -132,14 +144,6 @@ namespace Artemis.Core /// public object GetValue(object baseValue) { - // Validating this is kinda expensive, it'll fail on ChangeType later anyway ^^ - // var targetType = TargetProperty.PropertyType; - // if (!targetType.IsCastableFrom(baseValue.GetType())) - // { - // throw new ArtemisCoreException($"The provided current value type ({baseValue.GetType().Name}) not match the " + - // $"target property type ({targetType.Name})"); - // } - if (CompiledTargetAccessor == null) return baseValue; @@ -147,11 +151,54 @@ namespace Artemis.Core foreach (var dataBindingModifier in Modifiers) dataBindingValue = dataBindingModifier.Apply(dataBindingValue); - return Convert.ChangeType(dataBindingValue, TargetProperty.PropertyType); + 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) + 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; + } + + // Apply interpolation between the previous and current value + return GetInterpolatedValue(); } - internal void Update(double deltaTime) + /// + /// Returns the type of the source property of this data binding + /// + public Type GetSourceType() { + return SourceDataModel?.GetTypeAtPath(SourcePropertyPath); + } + + /// + /// Updates the smoothing progress of the data binding + /// + /// + public void Update(double deltaTime) + { + } + + /// + /// Applies the data binding to the + /// + public void ApplyToProperty() + { + + } + + private object GetInterpolatedValue() + { + if (_easingProgress >= 1.0f) + return _currentValue; + + return Registration.Converter.Interpolate(_previousValue, _currentValue, _easingProgress); } internal void ApplyToEntity() @@ -227,6 +274,20 @@ namespace Artemis.Core var lambda = Expression.Lambda>(returnValue, parameter); CompiledTargetAccessor = lambda.Compile(); } + + #region Events + + /// + /// Occurs when a modifier is added or removed + /// + public event EventHandler ModifiersUpdated; + + protected virtual void OnModifiersUpdated() + { + ModifiersUpdated?.Invoke(this, EventArgs.Empty); + } + + #endregion } /// diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingModifier.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingModifier.cs index 24ee17a39..cff4ea1c7 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingModifier.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingModifier.cs @@ -4,6 +4,7 @@ using System.Linq.Expressions; using Artemis.Core.DataModelExpansions; using Artemis.Core.Services; using Artemis.Storage.Entities.Profile.DataBindings; +using LiteDB.Engine; using Microsoft.VisualBasic; using Newtonsoft.Json; @@ -82,14 +83,9 @@ namespace Artemis.Core public object ParameterStaticValue { get; private set; } /// - /// Gets the compiled function that evaluates this predicate if it of a dynamic + /// A compiled expression tree that when given a matching data model returns the value of the modifiers parameter /// - public Func CompiledDynamicPredicate { get; private set; } - - /// - /// Gets the compiled function that evaluates this predicate if it is of a static - /// - public Func CompiledStaticPredicate { get; private set; } + public Func CompiledParameterAccessor { get; set; } internal DataBindingModifierEntity Entity { get; set; } @@ -100,10 +96,14 @@ namespace Artemis.Core /// The modified value public object Apply(object currentValue) { - if (CompiledDynamicPredicate != null) - return CompiledDynamicPredicate(currentValue, ParameterDataModel); - if (CompiledStaticPredicate != null) - return CompiledStaticPredicate(currentValue); + if (ParameterType == ProfileRightSideType.Dynamic && CompiledParameterAccessor != null) + { + var value = CompiledParameterAccessor(ParameterDataModel); + return ModifierType.Apply(currentValue, value); + } + + if (ParameterType == ProfileRightSideType.Static) + return ModifierType.Apply(currentValue, ParameterStaticValue); return currentValue; } @@ -122,7 +122,7 @@ namespace Artemis.Core return; } - var targetType = DataBinding.TargetProperty.GetType(); + var targetType = DataBinding.TargetProperty.PropertyType; if (!modifierType.SupportsType(targetType)) { throw new ArtemisCoreException($"Cannot apply modifier type {modifierType.GetType().Name} to this modifier because " + @@ -168,7 +168,7 @@ namespace Artemis.Core ParameterDataModel = null; ParameterPropertyPath = null; - var targetType = DataBinding.TargetProperty.GetType(); + var targetType = DataBinding.TargetProperty.PropertyType; // If not null ensure the types match and if not, convert it if (staticValue != null && staticValue.GetType() == targetType) @@ -208,7 +208,7 @@ namespace Artemis.Core else if (ParameterType == ProfileRightSideType.Static && Entity.ParameterStaticValue != null) { // Use the target type so JSON.NET has a better idea what to do - var targetType = DataBinding.TargetProperty.GetType(); + var targetType = DataBinding.TargetProperty.PropertyType; object staticValue; try @@ -231,52 +231,23 @@ namespace Artemis.Core private void CreateExpression() { - CompiledDynamicPredicate = null; - CompiledStaticPredicate = null; + CompiledParameterAccessor = null; if (ModifierType == null || DataBinding == null) return; if (ParameterType == ProfileRightSideType.Dynamic && ModifierType.SupportsParameter) - CreateDynamicExpression(); - else - CreateStaticExpression(); + { + if (ParameterDataModel == null) + return; + + // If the right side value is null, the constant type cannot be inferred and must be provided based on the data binding target + var parameterAccessor = CreateAccessor(ParameterDataModel, ParameterPropertyPath, "parameter", out var rightSideParameter); + var lambda = Expression.Lambda>(Expression.Convert(parameterAccessor, typeof(object)), rightSideParameter); + CompiledParameterAccessor = lambda.Compile(); + } } - - private void CreateDynamicExpression() - { - if (ParameterDataModel == null) - return; - - var currentValueParameter = Expression.Parameter(DataBinding.TargetProperty.GetType()); - - // If the right side value is null, the constant type cannot be inferred and must be provided based on the data binding target - var rightSideAccessor = CreateAccessor(ParameterDataModel, ParameterPropertyPath, "right", out var rightSideParameter); - - // A conversion may be required if the types differ - // This can cause issues if the DisplayConditionOperator wasn't accurate in it's supported types but that is not a concern here - if (rightSideAccessor.Type != DataBinding.TargetProperty.GetType()) - rightSideAccessor = Expression.Convert(rightSideAccessor, DataBinding.TargetProperty.GetType()); - - var modifierExpression = ModifierType.CreateExpression(currentValueParameter, rightSideAccessor); - var lambda = Expression.Lambda>(modifierExpression, currentValueParameter, rightSideParameter); - CompiledDynamicPredicate = lambda.Compile(); - } - - private void CreateStaticExpression() - { - var currentValueParameter = Expression.Parameter(DataBinding.TargetProperty.GetType()); - - // If the right side value is null, the constant type cannot be inferred and must be provided based on the data binding target - var rightSideConstant = ParameterStaticValue != null - ? Expression.Constant(ParameterStaticValue) - : Expression.Constant(null, DataBinding.TargetProperty.GetType()); - - var modifierExpression = ModifierType.CreateExpression(currentValueParameter, rightSideConstant); - var lambda = Expression.Lambda>(modifierExpression, currentValueParameter); - CompiledStaticPredicate = lambda.Compile(); - } - + private Expression CreateAccessor(DataModel dataModel, string path, string parameterName, out ParameterExpression parameter) { var listType = dataModel.GetListTypeInPath(path); diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs new file mode 100644 index 000000000..9064bc32b --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs @@ -0,0 +1,19 @@ +using System; +using System.Reflection; + +namespace Artemis.Core +{ + public class DataBindingRegistration + { + internal DataBindingRegistration(BaseLayerProperty layerProperty, PropertyInfo property, IDataBindingConverter converter) + { + LayerProperty = layerProperty ?? throw new ArgumentNullException(nameof(layerProperty)); + Property = property ?? throw new ArgumentNullException(nameof(property)); + Converter = converter ?? throw new ArgumentNullException(nameof(converter)); + } + + public BaseLayerProperty LayerProperty { get; set; } + public PropertyInfo Property { get; set; } + public IDataBindingConverter Converter { get; set; } + } +} \ 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 new file mode 100644 index 000000000..ffdf55b12 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingConverter.cs @@ -0,0 +1,38 @@ +namespace Artemis.Core +{ + /// + /// A data binding converter that acts as the bridge between a and a + /// + /// + public interface IDataBindingConverter + { + public BaseLayerProperty BaseLayerProperty { get; set; } + + /// + /// Returns the sum of and + /// + 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 + /// + object Interpolate(object a, object b, float progress); + + /// + /// Applies the to the layer property + /// + /// + void ApplyValue(object value); + + /// + /// Returns the current base value of the data binding + /// + object GetValue(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/DataBindingModifierType.cs b/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/Abstract/DataBindingModifierType.cs similarity index 77% rename from src/Artemis.Core/Models/Profile/DataBindings/Modifiers/DataBindingModifierType.cs rename to src/Artemis.Core/Models/Profile/DataBindings/Modifiers/Abstract/DataBindingModifierType.cs index 59ca364ac..2e01954bf 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/DataBindingModifierType.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/Abstract/DataBindingModifierType.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using Artemis.Core.Services; namespace Artemis.Core @@ -51,12 +50,19 @@ namespace Artemis.Core } /// - /// Creates a binary expression comparing two types + /// Called whenever the modifier must apply to a specific value, must be a value of a type contained in + /// /// - /// The current value of the data binding - /// An argument passed to the modifier, either static of dynamic - /// - public abstract Expression CreateExpression(ParameterExpression currentValue, Expression modifierArgument); + /// + /// The current value before modification, is always of a type contained in + /// + /// + /// + /// The parameter to use for the modification, is always of a type contained in + /// + /// + /// The modified value, must be a value of a type contained in + public abstract object Apply(object currentValue, object parameterValue); internal void Register(PluginInfo pluginInfo, IDataBindingService dataBindingService) { diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/DivideModifierType.cs b/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/DivideModifierType.cs new file mode 100644 index 000000000..7f153d14b --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/DivideModifierType.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace Artemis.Core +{ + internal class DivideModifierType : DataBindingModifierType + { + public override IReadOnlyCollection CompatibleTypes => Constants.NumberTypes; + + public override string Description => "Divide by"; + public override string Icon => "Divide"; + + public override object Apply(object currentValue, object parameterValue) + { + var parameter = Convert.ToSingle(parameterValue); + // Ye ye none of that + if (parameter == 0) + return 0; + + return Convert.ToSingle(currentValue) / parameter; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/MultiplicationModifier.cs b/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/MultiplicationModifier.cs new file mode 100644 index 000000000..bf400778d --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/MultiplicationModifier.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +namespace Artemis.Core +{ + internal class MultiplicationModifierType : DataBindingModifierType + { + public override IReadOnlyCollection CompatibleTypes => Constants.NumberTypes; + + public override string Description => "Multiply by"; + public override string Icon => "Close"; + + public override object Apply(object currentValue, object parameterValue) + { + return Convert.ToSingle(currentValue) * Convert.ToSingle(parameterValue); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 6830bf239..cad95b8d0 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -186,9 +186,6 @@ namespace Artemis.Core Transform.ApplyToEntity(); LayerBrush?.BaseProperties.ApplyToEntity(); - // Effects - ApplyRenderElementToEntity(); - // LEDs LayerEntity.Leds.Clear(); foreach (var artemisLed in Leds) @@ -204,6 +201,8 @@ namespace Artemis.Core // Conditions RenderElementEntity.RootDisplayCondition = DisplayConditionGroup?.Entity; DisplayConditionGroup?.ApplyToEntity(); + + ApplyRenderElementToEntity(); } #endregion @@ -313,6 +312,18 @@ namespace Artemis.Core } } + /// + public override List GetAllLayerProperties() + { + var result = base.GetAllLayerProperties(); + result.AddRange(General.GetAllLayerProperties()); + result.AddRange(Transform.GetAllLayerProperties()); + if (LayerBrush?.BaseProperties != null) + result.AddRange(LayerBrush.BaseProperties.GetAllLayerProperties()); + + return result; + } + /// public override void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo) { diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs index 5d3836415..3d5e96a0a 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using Artemis.Core.Services; using Artemis.Storage.Entities.Profile; namespace Artemis.Core @@ -11,6 +12,7 @@ namespace Artemis.Core public abstract class BaseLayerProperty { protected readonly List _dataBindings = new List(); + protected readonly List _dataBindingRegistrations = new List(); private bool _isHidden; private bool _keyframesEnabled; @@ -18,6 +20,11 @@ namespace Artemis.Core { } + /// + /// Gets a list containing the active data bindings + /// + public IReadOnlyList DataBindings => _dataBindings.AsReadOnly(); + /// /// Gets the profile element (such as layer or folder) this effect is applied to /// @@ -38,11 +45,6 @@ namespace Artemis.Core /// public bool DataBindingsSupported { get; protected internal set; } = true; - /// - /// Gets a read-only collection of the currently applied data bindings - /// - public IReadOnlyCollection DataBindings => _dataBindings.AsReadOnly(); - /// /// Gets or sets whether keyframes are enabled on this property, has no effect if is /// False @@ -104,18 +106,6 @@ namespace Artemis.Core /// public abstract Type GetPropertyType(); - /// - /// Returns a list of properties to which data bindings can be applied - /// - /// - public abstract List GetDataBindingProperties(); - - /// - /// Called when the provided data binding must be applied to a property - /// - /// - protected abstract void ApplyDataBinding(DataBinding dataBinding); - /// /// Applies the provided property entity to the layer property by deserializing the JSON base value and keyframe values /// @@ -132,23 +122,26 @@ namespace Artemis.Core #region Data bindings - /// - /// Applies the current to the layer property - /// - public void ApplyDataBindings() + internal DataBindingRegistration RegisterDataBindingProperty(PropertyInfo property, IDataBindingConverter converter) + { + var registration = new DataBindingRegistration(this, property, converter); + _dataBindingRegistrations.Add(registration); + return registration; + } + + internal void InitializeDataBindings(IDataModelService dataModelService, IDataBindingService dataBindingService) { foreach (var dataBinding in DataBindings) - ApplyDataBinding(dataBinding); + dataBinding.Initialize(dataModelService, dataBindingService); } /// /// Adds a new data binding targeting the given property to the collection /// - /// The property the new data binding should target /// The newly created data binding - public DataBinding AddDataBinding(PropertyInfo targetProperty) + public DataBinding EnableDataBinding(DataBindingRegistration dataBindingRegistration) { - var dataBinding = new DataBinding(this, targetProperty); + var dataBinding = new DataBinding(this, dataBindingProperty); _dataBindings.Add(dataBinding); return dataBinding; @@ -158,13 +151,11 @@ namespace Artemis.Core /// Removes the provided data binding from the collection /// /// The data binding to remove - public void RemoveDataBinding(DataBinding dataBinding) + public void DisableDataBinding(DataBinding dataBinding) { _dataBindings.Remove(dataBinding); } - - #endregion #region Events diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 59a2af329..3edb15b0f 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Linq.Expressions; using System.Reflection; +using Artemis.Core.DataModelExpansions; using Artemis.Storage.Entities.Profile; using Newtonsoft.Json; @@ -16,7 +18,7 @@ namespace Artemis.Core /// /// /// The type of property encapsulated in this layer property - public class LayerProperty : BaseLayerProperty + public abstract class LayerProperty : BaseLayerProperty { private T _baseValue; private bool _isInitialized; @@ -198,41 +200,6 @@ namespace Artemis.Core OnUpdated(); } - private void UpdateKeyframes() - { - if (!KeyframesSupported || !KeyframesEnabled) - return; - - // The current keyframe is the last keyframe before the current time - CurrentKeyframe = _keyframes.LastOrDefault(k => k.Position <= ProfileElement.TimelinePosition); - // Keyframes are sorted by position so we can safely assume the next keyframe's position is after the current - var nextIndex = _keyframes.IndexOf(CurrentKeyframe) + 1; - NextKeyframe = _keyframes.Count > nextIndex ? _keyframes[nextIndex] : null; - - // No need to update the current value if either of the keyframes are null - if (CurrentKeyframe == null) - CurrentValue = _keyframes.Any() ? _keyframes[0].Value : BaseValue; - else if (NextKeyframe == null) - CurrentValue = CurrentKeyframe.Value; - // Only determine progress and current value if both keyframes are present - else - { - var timeDiff = NextKeyframe.Position - CurrentKeyframe.Position; - var keyframeProgress = (float) ((ProfileElement.TimelinePosition - CurrentKeyframe.Position).TotalMilliseconds / timeDiff.TotalMilliseconds); - var keyframeProgressEased = (float) Easings.Interpolate(keyframeProgress, CurrentKeyframe.EasingFunction); - UpdateCurrentValue(keyframeProgress, keyframeProgressEased); - } - } - - private void UpdateDataBindings(double deltaTime) - { - foreach (var dataBinding in DataBindings) - { - dataBinding.Update(deltaTime); - ApplyDataBinding(dataBinding); - } - } - /// /// Sorts the keyframes in ascending order by position /// @@ -303,20 +270,88 @@ namespace Artemis.Core Position = k.Position, EasingFunction = (int) k.EasingFunction })); + + PropertyEntity.DataBindingEntities.Clear(); + foreach (var dataBinding in DataBindings) + { + dataBinding.ApplyToEntity(); + PropertyEntity.DataBindingEntities.Add(dataBinding.Entity); + } + } + + private void UpdateKeyframes() + { + if (!KeyframesSupported || !KeyframesEnabled) + return; + + // The current keyframe is the last keyframe before the current time + CurrentKeyframe = _keyframes.LastOrDefault(k => k.Position <= ProfileElement.TimelinePosition); + // Keyframes are sorted by position so we can safely assume the next keyframe's position is after the current + var nextIndex = _keyframes.IndexOf(CurrentKeyframe) + 1; + NextKeyframe = _keyframes.Count > nextIndex ? _keyframes[nextIndex] : null; + + // No need to update the current value if either of the keyframes are null + if (CurrentKeyframe == null) + CurrentValue = _keyframes.Any() ? _keyframes[0].Value : BaseValue; + else if (NextKeyframe == null) + CurrentValue = CurrentKeyframe.Value; + // Only determine progress and current value if both keyframes are present + else + { + var timeDiff = NextKeyframe.Position - CurrentKeyframe.Position; + var keyframeProgress = (float) ((ProfileElement.TimelinePosition - CurrentKeyframe.Position).TotalMilliseconds / timeDiff.TotalMilliseconds); + var keyframeProgressEased = (float) Easings.Interpolate(keyframeProgress, CurrentKeyframe.EasingFunction); + UpdateCurrentValue(keyframeProgress, keyframeProgressEased); + } } #region Data bindings - /// - public override List GetDataBindingProperties() + public void RegisterDataBindingProperty(Expression> propertyLambda, IDataBindingConverter converter) { - return new List {GetType().GetProperty(nameof(CurrentValue))}; + + var propertyInfo = ReflectionUtilities.GetPropertyInfo(CurrentValue, propertyLambda); + } - /// - protected override void ApplyDataBinding(DataBinding dataBinding) + /// + /// Registers the provided property to be available for data binding using a data binding property of type + /// + /// + /// The type of data binding property to use + /// The name of the property + /// + public TD RegisterDataBindingProperty(string propertyName, IDataBindingConverter converter) where TD : BaseDataBindingProperty { - CurrentValue = (T) dataBinding.GetValue(CurrentValue); + var property = typeof(T).GetProperty(propertyName); + if (property == null) + { + throw new ArtemisCoreException($"Cannot register data binding property for property {propertyName} " + + $"because it does not exist on type {typeof(T).Name}"); + } + + // Create an instance of the converter + var dataBindingOperator = (TD) Activator.CreateInstance(typeof(TD), property); + if (dataBindingOperator == null) + throw new ArtemisCoreException($"Cannot register data binding, failed to create an instance of {typeof(TD).Name}"); + if (dataBindingOperator.PropertyType != property.PropertyType) + { + throw new ArtemisCoreException($"Cannot register data binding property for property {propertyName} using a {typeof(TD).Name} " + + $"because it does not support type {typeof(T).Name}"); + } + + AddDataBindingConverter(dataBindingOperator); + + return dataBindingOperator; + } + + private void UpdateDataBindings(double deltaTime) + { + foreach (var dataBinding in DataBindings) + { + dataBinding.Update(deltaTime); + dataBinding.ApplyToProperty(); + } } #endregion diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKColorLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKColorLayerProperty.cs index fbe8968ac..e9c289e75 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKColorLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKColorLayerProperty.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; using SkiaSharp; namespace Artemis.Core @@ -11,10 +8,15 @@ namespace Artemis.Core { internal SKColorLayerProperty() { + RegisterDataBindingProperty(color => color.Alpha, new SKColorDataBindingConverter(SKColorDataBindingConverter.Channel.Alpha)); + RegisterDataBindingProperty(color => color.Red, new SKColorDataBindingConverter(SKColorDataBindingConverter.Channel.Red)); + RegisterDataBindingProperty(color => color.Green, new SKColorDataBindingConverter(SKColorDataBindingConverter.Channel.Green)); + RegisterDataBindingProperty(color => color.Blue, new SKColorDataBindingConverter(SKColorDataBindingConverter.Channel.Blue)); + RegisterDataBindingProperty(color => color.Hue, new SKColorDataBindingConverter(SKColorDataBindingConverter.Channel.Hue)); } /// - /// Implicitly converts an to an + /// Implicitly converts an to an ¶ /// /// /// @@ -28,29 +30,84 @@ namespace Artemis.Core { CurrentValue = CurrentKeyframe.Value.Interpolate(NextKeyframe.Value, keyframeProgressEased); } + } - /// - public override List GetDataBindingProperties() + internal class SKColorDataBindingConverter : IDataBindingConverter + { + private readonly Channel _channel; + + public SKColorDataBindingConverter(Channel channel) { - return typeof(SKColor).GetProperties().ToList(); + _channel = channel; } - /// - protected override void ApplyDataBinding(DataBinding dataBinding) + public BaseLayerProperty BaseLayerProperty { get; set; } + + public object Sum(object a, object b) { - if (dataBinding.TargetProperty.Name == nameof(CurrentValue.Alpha)) - CurrentValue = CurrentValue.WithAlpha((byte) dataBinding.GetValue(BaseValue.Alpha)); - else if (dataBinding.TargetProperty.Name == nameof(CurrentValue.Red)) - CurrentValue = CurrentValue.WithRed((byte) dataBinding.GetValue(BaseValue.Red)); - else if (dataBinding.TargetProperty.Name == nameof(CurrentValue.Green)) - CurrentValue = CurrentValue.WithGreen((byte) dataBinding.GetValue(BaseValue.Green)); - else if (dataBinding.TargetProperty.Name == nameof(CurrentValue.Blue)) - CurrentValue = CurrentValue.WithBlue((byte) dataBinding.GetValue(BaseValue.Blue)); - else if (dataBinding.TargetProperty.Name == nameof(CurrentValue.Hue)) + return (float) a + (float) b; + } + + public object Interpolate(object a, object b, float progress) + { + var diff = (float) b - (float) a; + return diff * progress; + } + + public void ApplyValue(object value) + { + var property = (SKColorLayerProperty) BaseLayerProperty; + switch (_channel) { - CurrentValue.ToHsv(out var h, out var s, out var v); - CurrentValue = SKColor.FromHsv((float) dataBinding.GetValue(h), s, v); + case Channel.Alpha: + property.CurrentValue = property.CurrentValue.WithAlpha((byte) value); + break; + case Channel.Red: + property.CurrentValue = property.CurrentValue.WithRed((byte) value); + break; + case Channel.Green: + property.CurrentValue = property.CurrentValue.WithGreen((byte) value); + break; + case Channel.Blue: + property.CurrentValue = property.CurrentValue.WithBlue((byte) value); + break; + case Channel.Hue: + property.CurrentValue.ToHsv(out var h, out var s, out var v); + property.CurrentValue = SKColor.FromHsv((float) value, s, v); + break; + default: + throw new ArgumentOutOfRangeException(); } } + + public object GetValue() + { + var property = (SKColorLayerProperty) BaseLayerProperty; + switch (_channel) + { + case Channel.Alpha: + return property.CurrentValue.Alpha; + case Channel.Red: + return property.CurrentValue.Red; + case Channel.Green: + return property.CurrentValue.Green; + case Channel.Blue: + return property.CurrentValue.Blue; + case Channel.Hue: + property.CurrentValue.ToHsv(out var h, out _, out _); + return h; + default: + throw new ArgumentOutOfRangeException(); + } + } + + public enum Channel + { + Alpha, + Red, + Green, + Blue, + Hue + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKPointLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKPointLayerProperty.cs index e69eac535..75c0a934c 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKPointLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKPointLayerProperty.cs @@ -27,20 +27,5 @@ namespace Artemis.Core var yDiff = NextKeyframe.Value.Y - CurrentKeyframe.Value.Y; CurrentValue = new SKPoint(CurrentKeyframe.Value.X + xDiff * keyframeProgressEased, CurrentKeyframe.Value.Y + yDiff * keyframeProgressEased); } - - /// - public override List GetDataBindingProperties() - { - return typeof(SKPoint).GetProperties().Where(p => p.CanWrite).ToList(); - } - - /// - protected override void ApplyDataBinding(DataBinding dataBinding) - { - if (dataBinding.TargetProperty.Name == nameof(CurrentValue.X)) - CurrentValue = new SKPoint((float) dataBinding.GetValue(BaseValue), CurrentValue.Y); - else if (dataBinding.TargetProperty.Name == nameof(CurrentValue.Y)) - CurrentValue = new SKPoint(CurrentValue.X, (float) dataBinding.GetValue(BaseValue)); - } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKSizeLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKSizeLayerProperty.cs index 326f74527..5db8b2a35 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKSizeLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKSizeLayerProperty.cs @@ -27,20 +27,5 @@ namespace Artemis.Core var heightDiff = NextKeyframe.Value.Height - CurrentKeyframe.Value.Height; CurrentValue = new SKSize(CurrentKeyframe.Value.Width + widthDiff * keyframeProgressEased, CurrentKeyframe.Value.Height + heightDiff * keyframeProgressEased); } - - /// - public override List GetDataBindingProperties() - { - return typeof(SKSize).GetProperties().Where(p => p.CanWrite).ToList(); - } - - /// - protected override void ApplyDataBinding(DataBinding dataBinding) - { - if (dataBinding.TargetProperty.Name == nameof(CurrentValue.Height)) - CurrentValue = new SKSize(CurrentValue.Width, (float) dataBinding.GetValue(BaseValue)); - else if (dataBinding.TargetProperty.Name == nameof(CurrentValue.Width)) - CurrentValue = new SKSize((float) dataBinding.GetValue(BaseValue), CurrentValue.Width); - } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index c3d5b01f3..34aac49d9 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -311,5 +311,17 @@ namespace Artemis.Core } #endregion + + /// + /// Returns all the layer properties of this profile element + /// + public virtual List GetAllLayerProperties() + { + var result = new List(); + foreach (var baseLayerEffect in LayerEffects) + result.AddRange(baseLayerEffect.BaseProperties.GetAllLayerProperties()); + + return result; + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/DataBindingService.cs b/src/Artemis.Core/Services/DataBindingService.cs index 8a82ae864..4a215c211 100644 --- a/src/Artemis.Core/Services/DataBindingService.cs +++ b/src/Artemis.Core/Services/DataBindingService.cs @@ -15,6 +15,8 @@ namespace Artemis.Core.Services { _logger = logger; _registeredDataBindingModifierTypes = new List(); + + RegisterBuiltInModifiers(); } public IReadOnlyCollection RegisteredDataBindingModifierTypes @@ -98,5 +100,10 @@ namespace Artemis.Core.Services dataBindingModifier.Entity.ModifierType ); } + + private void RegisterBuiltInModifiers() + { + RegisterModifierType(Constants.CorePluginInfo, new MultiplicationModifierType()); + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Interfaces/IRenderElementService.cs b/src/Artemis.Core/Services/Interfaces/IRenderElementService.cs index bbb760dcf..79319f578 100644 --- a/src/Artemis.Core/Services/Interfaces/IRenderElementService.cs +++ b/src/Artemis.Core/Services/Interfaces/IRenderElementService.cs @@ -54,5 +54,6 @@ namespace Artemis.Core.Services void RemoveLayerEffect(BaseLayerEffect layerEffect); void InstantiateDisplayConditions(RenderProfileElement renderElement); + void InstantiateDataBindings(RenderProfileElement renderElement); } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/RenderElementService.cs b/src/Artemis.Core/Services/RenderElementService.cs index ec6901764..6b26c94de 100644 --- a/src/Artemis.Core/Services/RenderElementService.cs +++ b/src/Artemis.Core/Services/RenderElementService.cs @@ -10,16 +10,18 @@ namespace Artemis.Core.Services internal class RenderElementService : IRenderElementService { private readonly IDataModelService _dataModelService; + private readonly IDataBindingService _dataBindingService; private readonly IKernel _kernel; private readonly ILogger _logger; private readonly IPluginService _pluginService; - public RenderElementService(IKernel kernel, ILogger logger, IPluginService pluginService, IDataModelService dataModelService) + public RenderElementService(IKernel kernel, ILogger logger, IPluginService pluginService, IDataModelService dataModelService, IDataBindingService dataBindingService) { _kernel = kernel; _logger = logger; _pluginService = pluginService; _dataModelService = dataModelService; + _dataBindingService = dataBindingService; } public Layer CreateLayer(Profile profile, ProfileElement parent, string name) @@ -35,6 +37,7 @@ namespace Artemis.Core.Services InstantiateLayerBrush(layer); InstantiateLayerEffects(layer); InstantiateDisplayConditions(layer); + InstantiateDataBindings(layer); return layer; } @@ -156,5 +159,11 @@ namespace Artemis.Core.Services _logger.Warning(e, $"Failed to init display conditions for {renderElement}"); } } + + public void InstantiateDataBindings(RenderProfileElement renderElement) + { + foreach (var baseLayerProperty in renderElement.GetAllLayerProperties()) + baseLayerProperty.InitializeDataBindings(_dataModelService, _dataBindingService); + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index 78a9e7155..f66031fca 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -299,6 +299,7 @@ namespace Artemis.Core.Services _renderElementService.RemoveLayerEffect(layerLayerEffect); _renderElementService.InstantiateDisplayConditions(folder); + _renderElementService.InstantiateDataBindings(folder); } } @@ -324,6 +325,7 @@ namespace Artemis.Core.Services _renderElementService.RemoveLayerEffect(layerLayerEffect); _renderElementService.InstantiateDisplayConditions(layer); + _renderElementService.InstantiateDataBindings(layer); } } diff --git a/src/Artemis.UI.Shared/DataModelVisualization/DataModelInputViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/DataModelInputViewModel.cs index b25dd3614..f0b2b2a02 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/DataModelInputViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/DataModelInputViewModel.cs @@ -14,9 +14,9 @@ namespace Artemis.UI.Shared private bool _closed; private T _inputValue; - protected DataModelInputViewModel(DataModelPropertyAttribute description, T initialValue) + protected DataModelInputViewModel(DataModelPropertyAttribute targetDescription, T initialValue) { - Description = description; + TargetDescription = targetDescription; InputValue = initialValue; } @@ -26,7 +26,7 @@ namespace Artemis.UI.Shared set => SetAndNotify(ref _inputValue, value); } - public DataModelPropertyAttribute Description { get; } + public DataModelPropertyAttribute TargetDescription { get; } internal override object InternalGuard { get; } = null; /// diff --git a/src/Artemis.UI.Shared/DataModelVisualization/DataModelSelectionView.xaml b/src/Artemis.UI.Shared/DataModelVisualization/DataModelSelectionView.xaml deleted file mode 100644 index f74af0f0b..000000000 --- a/src/Artemis.UI.Shared/DataModelVisualization/DataModelSelectionView.xaml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml new file mode 100644 index 000000000..ca1dbb905 --- /dev/null +++ b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Shared/DataModelVisualization/DataModelSelectionView.xaml.cs b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml.cs similarity index 75% rename from src/Artemis.UI.Shared/DataModelVisualization/DataModelSelectionView.xaml.cs rename to src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml.cs index fc0034294..8a45bde5d 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/DataModelSelectionView.xaml.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml.cs @@ -1,14 +1,14 @@ using System.Windows; using System.Windows.Controls; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.Input { /// - /// Interaction logic for DataModelSelectionView.xaml + /// Interaction logic for DataModelDynamicView.xaml /// - public partial class DataModelSelectionView : UserControl + public partial class DataModelDynamicView : UserControl { - public DataModelSelectionView() + public DataModelDynamicView() { InitializeComponent(); } diff --git a/src/Artemis.UI.Shared/DataModelVisualization/DataModelSelectionViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs similarity index 89% rename from src/Artemis.UI.Shared/DataModelVisualization/DataModelSelectionViewModel.cs rename to src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs index 029290827..bc718153d 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/DataModelSelectionViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs @@ -11,22 +11,21 @@ using Stylet; // Remove, annoying while working on it #pragma warning disable 1591 -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.Input { - public class DataModelSelectionViewModel : PropertyChangedBase + public class DataModelDynamicViewModel : PropertyChangedBase { private readonly IDataModelUIService _dataModelUIService; private readonly Module _module; private readonly Timer _updateTimer; private Brush _buttonBrush = new SolidColorBrush(Color.FromRgb(171, 71, 188)); - private DataModelPropertiesViewModel _dataModelViewModel; private bool _isDataModelViewModelOpen; - private bool _isEnabled; + private bool _isEnabled = true; private string _placeholder = "Select a property"; private DataModelVisualizationViewModel _selectedPropertyViewModel; - public DataModelSelectionViewModel(Module module, ISettingsService settingsService, IDataModelUIService dataModelUIService) + internal DataModelDynamicViewModel(Module module, ISettingsService settingsService, IDataModelUIService dataModelUIService) { _module = module; _dataModelUIService = dataModelUIService; @@ -38,22 +37,18 @@ namespace Artemis.UI.Shared Initialize(); } - public string Placeholder - { - get => _placeholder; - set => SetAndNotify(ref _placeholder, value); - } - public Brush ButtonBrush { get => _buttonBrush; set => SetAndNotify(ref _buttonBrush, value); } - public DelegateCommand SelectPropertyCommand { get; } - public Type[] FilterTypes { get; set; } - public PluginSetting ShowDataModelValues { get; } + public string Placeholder + { + get => _placeholder; + set => SetAndNotify(ref _placeholder, value); + } public bool IsEnabled { @@ -61,6 +56,10 @@ namespace Artemis.UI.Shared set => SetAndNotify(ref _isEnabled, value); } + public Type[] FilterTypes { get; set; } + public DelegateCommand SelectPropertyCommand { get; } + public PluginSetting ShowDataModelValues { get; } + public DataModelPropertiesViewModel DataModelViewModel { get => _dataModelViewModel; @@ -117,14 +116,15 @@ namespace Artemis.UI.Shared return; SelectedPropertyViewModel = selected; - OnPropertySelected(new DataModelPropertySelectedEventArgs(selected)); + OnPropertySelected(new DataModelInputDynamicEventArgs(selected)); } + #region Events - public event EventHandler PropertySelected; + public event EventHandler PropertySelected; - protected virtual void OnPropertySelected(DataModelPropertySelectedEventArgs e) + protected virtual void OnPropertySelected(DataModelInputDynamicEventArgs e) { PropertySelected?.Invoke(this, e); } diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelStaticView.xaml b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelStaticView.xaml new file mode 100644 index 000000000..6afe0edba --- /dev/null +++ b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelStaticView.xaml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelStaticView.xaml.cs b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelStaticView.xaml.cs new file mode 100644 index 000000000..5aacbb841 --- /dev/null +++ b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelStaticView.xaml.cs @@ -0,0 +1,15 @@ +using System.Windows.Controls; + +namespace Artemis.UI.Shared.Input +{ + /// + /// Interaction logic for DataModelStaticView.xaml + /// + public partial class DataModelStaticView : UserControl + { + public DataModelStaticView() + { + InitializeComponent(); + } + } +} diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelStaticViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelStaticViewModel.cs new file mode 100644 index 000000000..7d4e23214 --- /dev/null +++ b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelStaticViewModel.cs @@ -0,0 +1,116 @@ +using System; +using System.Windows.Media; +using Artemis.Core; +using Artemis.Core.DataModelExpansions; +using Artemis.UI.Shared.Services; +using Stylet; + +// Remove, annoying while working on it +#pragma warning disable 1591 + +namespace Artemis.UI.Shared.Input +{ + public class DataModelStaticViewModel : PropertyChangedBase + { + private readonly IDataModelUIService _dataModelUIService; + private Brush _buttonBrush = new SolidColorBrush(Color.FromRgb(171, 71, 188)); + private DataModelInputViewModel _inputViewModel; + private string _placeholder = "Enter a value"; + private DataModelPropertyAttribute _targetDescription; + private Type _targetType; + private int _transitionIndex; + private object _value; + + internal DataModelStaticViewModel(Type targetType, IDataModelUIService dataModelUIService) + { + TargetType = targetType; + _dataModelUIService = dataModelUIService; + } + + public Brush ButtonBrush + { + get => _buttonBrush; + set => SetAndNotify(ref _buttonBrush, value); + } + + public int TransitionIndex + { + get => _transitionIndex; + set => SetAndNotify(ref _transitionIndex, value); + } + + public DataModelInputViewModel InputViewModel + { + get => _inputViewModel; + private set => SetAndNotify(ref _inputViewModel, value); + } + + public Type TargetType + { + get => _targetType; + set => SetAndNotify(ref _targetType, value); + } + + public DataModelPropertyAttribute TargetDescription + { + get => _targetDescription; + set => SetAndNotify(ref _targetDescription, value); + } + + public object Value + { + get => _value; + set => SetAndNotify(ref _value, value); + } + + public string Placeholder + { + get => _placeholder; + set => SetAndNotify(ref _placeholder, value); + } + + public void ActivateInputViewModel() + { + TransitionIndex = 1; + InputViewModel = _dataModelUIService.GetDataModelInputViewModel( + TargetType, + TargetDescription, + Value, + ApplyFreeInput + ); + } + + public void UpdateTargetType(Type target) + { + TargetType = target ?? throw new ArgumentNullException(nameof(target)); + + // If the type changed, reset to the default type + if (!target.IsCastableFrom(Value.GetType())) + { + // Force the VM to close if it was open and apply the new value + ApplyFreeInput(target.GetDefault(), true); + } + } + + private void ApplyFreeInput(object value, bool submitted) + { + if (submitted) + OnValueUpdated(new DataModelInputStaticEventArgs(value)); + + TransitionIndex = 0; + InputViewModel = null; + Value = value; + } + + #region Events + + public event EventHandler ValueUpdated; + + protected virtual void OnValueUpdated(DataModelInputStaticEventArgs e) + { + ValueUpdated?.Invoke(this, e); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Events/DataModelPropertySelectedEventArgs.cs b/src/Artemis.UI.Shared/Events/DataModelInputDynamicEventArgs.cs similarity index 58% rename from src/Artemis.UI.Shared/Events/DataModelPropertySelectedEventArgs.cs rename to src/Artemis.UI.Shared/Events/DataModelInputDynamicEventArgs.cs index 81bb38629..9f7649dbb 100644 --- a/src/Artemis.UI.Shared/Events/DataModelPropertySelectedEventArgs.cs +++ b/src/Artemis.UI.Shared/Events/DataModelInputDynamicEventArgs.cs @@ -2,11 +2,11 @@ namespace Artemis.UI.Shared { - public class DataModelPropertySelectedEventArgs : EventArgs + public class DataModelInputDynamicEventArgs : EventArgs { public DataModelVisualizationViewModel DataModelVisualizationViewModel { get; } - public DataModelPropertySelectedEventArgs(DataModelVisualizationViewModel dataModelVisualizationViewModel) + public DataModelInputDynamicEventArgs(DataModelVisualizationViewModel dataModelVisualizationViewModel) { DataModelVisualizationViewModel = dataModelVisualizationViewModel; } diff --git a/src/Artemis.UI.Shared/Events/DataModelInputStaticEventArgs.cs b/src/Artemis.UI.Shared/Events/DataModelInputStaticEventArgs.cs new file mode 100644 index 000000000..278681dd6 --- /dev/null +++ b/src/Artemis.UI.Shared/Events/DataModelInputStaticEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Artemis.UI.Shared +{ + public class DataModelInputStaticEventArgs : EventArgs + { + public object Value { get; } + + public DataModelInputStaticEventArgs(object value) + { + Value = value; + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/DataModelUIService.cs b/src/Artemis.UI.Shared/Services/DataModelUIService.cs index b8ead4820..73e34e9e6 100644 --- a/src/Artemis.UI.Shared/Services/DataModelUIService.cs +++ b/src/Artemis.UI.Shared/Services/DataModelUIService.cs @@ -5,6 +5,7 @@ using Artemis.Core; using Artemis.Core.DataModelExpansions; using Artemis.Core.Modules; using Artemis.Core.Services; +using Artemis.UI.Shared.Input; using Ninject; using Ninject.Parameters; @@ -179,9 +180,15 @@ namespace Artemis.UI.Shared.Services } } - public DataModelSelectionViewModel GetDataModelSelectionViewModel(Module module) + public DataModelDynamicViewModel GetDynamicSelectionViewModel(Module module) { - return _kernel.Get(new ConstructorArgument("module", module)); + return _kernel.Get(new ConstructorArgument("module", module)); + } + + public DataModelStaticViewModel GetStaticInputViewModel(Type targetType) + { + if (targetType == null) throw new ArgumentNullException(nameof(targetType)); + return _kernel.Get(new ConstructorArgument("targetType", targetType)); } private DataModelInputViewModel InstantiateDataModelInputViewModel(DataModelVisualizationRegistration registration, DataModelPropertyAttribute description, object initialValue) diff --git a/src/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs b/src/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs index f4ee8cc13..049a1bf11 100644 --- a/src/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs +++ b/src/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Artemis.Core; using Artemis.Core.DataModelExpansions; using Artemis.Core.Modules; +using Artemis.UI.Shared.Input; namespace Artemis.UI.Shared.Services { @@ -28,6 +29,7 @@ namespace Artemis.UI.Shared.Services DataModelDisplayViewModel GetDataModelDisplayViewModel(Type propertyType); DataModelInputViewModel GetDataModelInputViewModel(Type propertyType, DataModelPropertyAttribute description, object initialValue, Action updateCallback); - DataModelSelectionViewModel GetDataModelSelectionViewModel(Module module); + DataModelDynamicViewModel GetDynamicSelectionViewModel(Module module); + DataModelStaticViewModel GetStaticInputViewModel(Type targetType); } } \ No newline at end of file diff --git a/src/Artemis.UI/DataModelVisualization/Input/DoubleDataModelInputViewModel.cs b/src/Artemis.UI/DataModelVisualization/Input/DoubleDataModelInputViewModel.cs index f4953e318..9099ea2c6 100644 --- a/src/Artemis.UI/DataModelVisualization/Input/DoubleDataModelInputViewModel.cs +++ b/src/Artemis.UI/DataModelVisualization/Input/DoubleDataModelInputViewModel.cs @@ -8,7 +8,7 @@ namespace Artemis.UI.DataModelVisualization.Input { public class DoubleDataModelInputViewModel : DataModelInputViewModel { - public DoubleDataModelInputViewModel(DataModelPropertyAttribute description, double initialValue) : base(description, initialValue) + public DoubleDataModelInputViewModel(DataModelPropertyAttribute targetDescription, double initialValue) : base(targetDescription, initialValue) { } diff --git a/src/Artemis.UI/DataModelVisualization/Input/IntDataModelInputViewModel.cs b/src/Artemis.UI/DataModelVisualization/Input/IntDataModelInputViewModel.cs index 9317f38e3..f4fce549f 100644 --- a/src/Artemis.UI/DataModelVisualization/Input/IntDataModelInputViewModel.cs +++ b/src/Artemis.UI/DataModelVisualization/Input/IntDataModelInputViewModel.cs @@ -7,7 +7,7 @@ namespace Artemis.UI.DataModelVisualization.Input { public class IntDataModelInputViewModel : DataModelInputViewModel { - public IntDataModelInputViewModel(DataModelPropertyAttribute description, int initialValue) : base(description, initialValue) + public IntDataModelInputViewModel(DataModelPropertyAttribute targetDescription, int initialValue) : base(targetDescription, initialValue) { } diff --git a/src/Artemis.UI/DataModelVisualization/Input/StringDataModelInputViewModel.cs b/src/Artemis.UI/DataModelVisualization/Input/StringDataModelInputViewModel.cs index f7a42f01a..4f8818eca 100644 --- a/src/Artemis.UI/DataModelVisualization/Input/StringDataModelInputViewModel.cs +++ b/src/Artemis.UI/DataModelVisualization/Input/StringDataModelInputViewModel.cs @@ -5,7 +5,7 @@ namespace Artemis.UI.DataModelVisualization.Input { public class StringDataModelInputViewModel : DataModelInputViewModel { - public StringDataModelInputViewModel(DataModelPropertyAttribute description, string initialValue) : base(description, initialValue) + public StringDataModelInputViewModel(DataModelPropertyAttribute targetDescription, string initialValue) : base(targetDescription, initialValue) { } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierView.xaml index f8edd20cf..a3aaf5c56 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierView.xaml @@ -62,8 +62,8 @@ Style="{StaticResource DisplayConditionButtonLeftClickMenu}" Background="#7B7B7B" BorderBrush="#7B7B7B" - Content="{Binding SelectedModifierType.Description}"> - + Content="{Binding SelectedModifierType.Description}" + Click="PropertyButton_OnClick"> @@ -85,34 +85,7 @@ - + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierView.xaml.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierView.xaml.cs index ab48662ff..53ba23175 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierView.xaml.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierView.xaml.cs @@ -22,5 +22,15 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings { InitializeComponent(); } + + private void PropertyButton_OnClick(object sender, RoutedEventArgs e) + { + // DataContext is not set when using left button, I don't know why but there it is + if (sender is Button button && button.ContextMenu != null) + { + button.ContextMenu.DataContext = button.DataContext; + button.ContextMenu.IsOpen = true; + } + } } } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierViewModel.cs index 3b76ea7e4..af6bf2689 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierViewModel.cs @@ -1,7 +1,10 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; +using Artemis.UI.Exceptions; using Artemis.UI.Shared; +using Artemis.UI.Shared.Input; using Artemis.UI.Shared.Services; using Stylet; @@ -13,6 +16,8 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings private readonly IDataModelUIService _dataModelUIService; private readonly IProfileEditorService _profileEditorService; private DataBindingModifierType _selectedModifierType; + private DataModelDynamicViewModel _dynamicSelectionViewModel; + private DataModelStaticViewModel _staticInputViewModel; public DataBindingModifierViewModel(DataBindingModifier modifier, IDataBindingService dataBindingService, @@ -32,11 +37,10 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings SelectModifierTypeCommand = new DelegateCommand(ExecuteSelectModifierTypeCommand); // Initialize async, no need to wait for it - Task.Run(Initialize); + Execute.PostToUIThread(Initialize); } - public DelegateCommand SelectModifierTypeCommand { get; set; } - + public DelegateCommand SelectModifierTypeCommand { get; } public PluginSetting ShowDataModelValues { get; } public DataBindingModifier Modifier { get; } public BindableCollection ModifierTypes { get; } @@ -47,30 +51,65 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings set => SetAndNotify(ref _selectedModifierType, value); } - public DataModelSelectionViewModel ParameterSelectionViewModel { get; private set; } + public DataModelDynamicViewModel DynamicSelectionViewModel + { + get => _dynamicSelectionViewModel; + private set => SetAndNotify(ref _dynamicSelectionViewModel, value); + } + + public DataModelStaticViewModel StaticInputViewModel + { + get => _staticInputViewModel; + private set => SetAndNotify(ref _staticInputViewModel, value); + } private void Initialize() { - ParameterSelectionViewModel = _dataModelUIService.GetDataModelSelectionViewModel(_profileEditorService.GetCurrentModule()); - ParameterSelectionViewModel.PropertySelected += ParameterSelectionViewModelOnPropertySelected; - ParameterSelectionViewModel.FilterTypes = new[] {Modifier.DataBinding.TargetProperty.PropertyType}; + var sourceType = Modifier.DataBinding.GetSourceType(); + if (sourceType == null) + throw new ArtemisUIException("Cannot initialize a data binding modifier VM for a data binding without a source"); + + if (Modifier.ParameterType == ProfileRightSideType.Dynamic) + { + StaticInputViewModel = null; + DynamicSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule()); + DynamicSelectionViewModel.PropertySelected += ParameterSelectionViewModelOnPropertySelected; + DynamicSelectionViewModel.FilterTypes = new[] {sourceType}; + } + else + { + DynamicSelectionViewModel = null; + StaticInputViewModel = _dataModelUIService.GetStaticInputViewModel(sourceType); + StaticInputViewModel.ValueUpdated += StaticInputViewModelOnValueUpdated; + } Update(); } - private void ParameterSelectionViewModelOnPropertySelected(object sender, DataModelPropertySelectedEventArgs e) + private void ParameterSelectionViewModelOnPropertySelected(object sender, DataModelInputDynamicEventArgs e) { Modifier.UpdateParameter(e.DataModelVisualizationViewModel.DataModel, e.DataModelVisualizationViewModel.PropertyPath); } + private void StaticInputViewModelOnValueUpdated(object sender, DataModelInputStaticEventArgs e) + { + Modifier.UpdateParameter(e.Value); + } + private void Update() { + var sourceType = Modifier.DataBinding.GetSourceType(); + // Modifier type ModifierTypes.Clear(); - ModifierTypes.AddRange(_dataBindingService.GetCompatibleModifierTypes(Modifier.DataBinding.TargetProperty.PropertyType)); + ModifierTypes.AddRange(_dataBindingService.GetCompatibleModifierTypes(sourceType)); SelectedModifierType = Modifier.ModifierType; - ParameterSelectionViewModel.PopulateSelectedPropertyViewModel(Modifier.ParameterDataModel, Modifier.ParameterPropertyPath); + // Parameter + if (DynamicSelectionViewModel != null) + DynamicSelectionViewModel?.PopulateSelectedPropertyViewModel(Modifier.ParameterDataModel, Modifier.ParameterPropertyPath); + else if (StaticInputViewModel != null) + StaticInputViewModel.Value = Modifier.ParameterStaticValue; } private void ExecuteSelectModifierTypeCommand(object context) @@ -83,5 +122,20 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings Update(); } + + public void Delete() + { + Modifier.DataBinding.RemoveModifier(Modifier); + } + + public void SwapType() + { + if (Modifier.ParameterType == ProfileRightSideType.Dynamic) + Modifier.UpdateParameter(Modifier.DataBinding.GetSourceType().GetDefault()); + else + Modifier.UpdateParameter(null, null); + + Initialize(); + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml.cs index 24e8655ff..61f02ab77 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml.cs @@ -12,15 +12,5 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings { InitializeComponent(); } - - private void PropertyButton_OnClick(object sender, RoutedEventArgs e) - { - // DataContext is not set when using left button, I don't know why but there it is - if (sender is Button button && button.ContextMenu != null) - { - button.ContextMenu.DataContext = button.DataContext; - button.ContextMenu.IsOpen = true; - } - } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs index f3e99961d..6c65e55bf 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs @@ -1,33 +1,27 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Threading.Tasks; using System.Timers; using Artemis.Core; -using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared; +using Artemis.UI.Shared.Input; using Artemis.UI.Shared.Services; -using Artemis.UI.Utilities; using Stylet; namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings { public class DataBindingViewModel : PropertyChangedBase { - private readonly IDataModelUIService _dataModelUIService; private readonly IDataBindingsVmFactory _dataBindingsVmFactory; + private readonly IDataModelUIService _dataModelUIService; private readonly IProfileEditorService _profileEditorService; private readonly Timer _updateTimer; private DataBinding _dataBinding; private bool _isDataBindingEnabled; - private DataModelPropertiesViewModel _sourceDataModel; - private DataModelVisualizationViewModel _selectedSourceProperty; - private bool _sourceDataModelOpen; + private DataModelDynamicViewModel _targetSelectionViewModel; private object _testInputValue; private object _testResultValue; - private DataModelSelectionViewModel _targetSelectionViewModel; public DataBindingViewModel(BaseLayerProperty layerProperty, PropertyInfo targetProperty, @@ -41,12 +35,14 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings _updateTimer = new Timer(500); DisplayName = targetProperty.Name.ToUpper(); + ModifierViewModels = new BindableCollection(); LayerProperty = layerProperty; TargetProperty = targetProperty; DataBinding = layerProperty.DataBindings.FirstOrDefault(d => d.TargetProperty == targetProperty); - ModifierViewModels = new BindableCollection(); + if (DataBinding != null) + DataBinding.ModifiersUpdated += DataBindingOnModifiersUpdated; _isDataBindingEnabled = DataBinding != null; @@ -59,7 +55,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings public string DisplayName { get; } public BindableCollection ModifierViewModels { get; } - public DataModelSelectionViewModel TargetSelectionViewModel + public DataModelDynamicViewModel TargetSelectionViewModel { get => _targetSelectionViewModel; private set => SetAndNotify(ref _targetSelectionViewModel, value); @@ -106,9 +102,13 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings return; DataBinding = LayerProperty.AddDataBinding(TargetProperty); + DataBinding.ModifiersUpdated += DataBindingOnModifiersUpdated; Update(); + + _profileEditorService.UpdateSelectedProfileElement(); } + public void RemoveDataBinding() { if (DataBinding == null) @@ -117,7 +117,10 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings var toRemove = DataBinding; DataBinding = null; LayerProperty.RemoveDataBinding(toRemove); + toRemove.ModifiersUpdated -= DataBindingOnModifiersUpdated; Update(); + + _profileEditorService.UpdateSelectedProfileElement(); } public void AddModifier() @@ -128,12 +131,12 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings var modifier = new DataBindingModifier(ProfileRightSideType.Dynamic); DataBinding.AddModifier(modifier); - ModifierViewModels.Add(_dataBindingsVmFactory.DataBindingModifierViewModel(modifier)); + _profileEditorService.UpdateSelectedProfileElement(); } private void Initialize() { - TargetSelectionViewModel = _dataModelUIService.GetDataModelSelectionViewModel(_profileEditorService.GetCurrentModule()); + TargetSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule()); TargetSelectionViewModel.PropertySelected += TargetSelectionViewModelOnPropertySelected; Update(); @@ -153,11 +156,19 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings TargetSelectionViewModel.IsEnabled = true; TargetSelectionViewModel.PopulateSelectedPropertyViewModel(DataBinding.SourceDataModel, DataBinding.SourcePropertyPath); TargetSelectionViewModel.FilterTypes = new[] {DataBinding.TargetProperty.PropertyType}; + + UpdateModifierViewModels(); } - private void TargetSelectionViewModelOnPropertySelected(object sender, DataModelPropertySelectedEventArgs e) + private void DataBindingOnModifiersUpdated(object sender, EventArgs e) + { + UpdateModifierViewModels(); + } + + private void TargetSelectionViewModelOnPropertySelected(object sender, DataModelInputDynamicEventArgs e) { DataBinding.UpdateSource(e.DataModelVisualizationViewModel.DataModel, e.DataModelVisualizationViewModel.PropertyPath); + _profileEditorService.UpdateSelectedProfileElement(); } private void OnUpdateTimerOnElapsed(object sender, ElapsedEventArgs e) @@ -184,6 +195,5 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings foreach (var dataBindingModifier in DataBinding.Modifiers) ModifierViewModels.Add(_dataBindingsVmFactory.DataBindingModifierViewModel(dataBindingModifier)); } - } } \ No newline at end of file