diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs index 1c1121b16..47b5a76c8 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs @@ -1,4 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; using Artemis.Core.DataModelExpansions; namespace Artemis.Core @@ -11,9 +15,14 @@ namespace Artemis.Core private readonly List _modifiers = new List(); /// - /// The that the data binding targets + /// Gets the layer property this data binding targets /// - public BaseLayerProperty Target { get; set; } // BIG FAT TODO: Take into account X and Y of SkPosition etc., forgot about it again :> + public BaseLayerProperty LayerProperty { get; private set; } + + /// + /// Gets the inner property this data binding targets + /// + public PropertyInfo TargetProperty { get; private set; } /// /// Gets the currently used instance of the data model that contains the source of the data binding @@ -30,6 +39,8 @@ namespace Artemis.Core /// public IReadOnlyList Modifiers => _modifiers.AsReadOnly(); + public Func CompiledTargetAccessor { get; private set; } + /// /// Adds a modifier to the data binding's collection /// @@ -55,5 +66,44 @@ namespace Artemis.Core _modifiers.Remove(modifier); } } + + /// + /// Gets the current value of the data binding + /// + /// The base value of the property the data binding is applied to + /// + public object GetValue(object baseValue) + { + if (baseValue.GetType() != TargetProperty.PropertyType) + { + throw new ArtemisCoreException($"The provided current value type ({baseValue.GetType().Name}) not match the " + + $"target property type ({TargetProperty.PropertyType.Name})"); + } + + if (CompiledTargetAccessor == null) + return baseValue; + + var dataBindingValue = CompiledTargetAccessor(SourceDataModel); + foreach (var dataBindingModifier in Modifiers) + dataBindingValue = dataBindingModifier.Apply(dataBindingValue); + + return dataBindingValue; + } + + public void Update() + { + var listType = SourceDataModel.GetListTypeInPath(SourcePropertyPath); + if (listType != null) + throw new ArtemisCoreException($"Cannot create a regular accessor at path {SourcePropertyPath} because the path contains a list"); + + var parameter = Expression.Parameter(typeof(object), "targetDataModel"); + var accessor = SourcePropertyPath.Split('.').Aggregate( + Expression.Convert(parameter, SourceDataModel.GetType()), // Cast to the appropriate type + Expression.Property + ); + + var lambda = Expression.Lambda>(accessor); + CompiledTargetAccessor = 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 8e3ba1ac4..58e241581 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingModifier.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingModifier.cs @@ -89,7 +89,7 @@ namespace Artemis.Core /// The modified value public object Apply(object currentValue) { - var targetType = DataBinding.Target.GetPropertyType(); + var targetType = DataBinding.LayerProperty.GetPropertyType(); if (currentValue.GetType() != targetType) { throw new ArtemisCoreException("The current value of the data binding does not match the type of the target property." + @@ -118,7 +118,7 @@ namespace Artemis.Core return; } - var targetType = DataBinding.Target.GetPropertyType(); + var targetType = DataBinding.LayerProperty.GetPropertyType(); if (!modifierType.SupportsType(targetType)) { throw new ArtemisCoreException($"Cannot apply modifier type {modifierType.GetType().Name} to this modifier because " + @@ -164,7 +164,7 @@ namespace Artemis.Core ParameterDataModel = null; ParameterPropertyPath = null; - var targetType = DataBinding.Target.GetPropertyType(); + var targetType = DataBinding.LayerProperty.GetPropertyType(); // If not null ensure the types match and if not, convert it if (staticValue != null && staticValue.GetType() == targetType) @@ -200,7 +200,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.Target.GetPropertyType(); + var targetType = DataBinding.LayerProperty.GetPropertyType(); object staticValue; try @@ -240,15 +240,15 @@ namespace Artemis.Core if (ParameterDataModel == null) return; - var currentValueParameter = Expression.Parameter(DataBinding.Target.GetPropertyType()); + var currentValueParameter = Expression.Parameter(DataBinding.LayerProperty.GetPropertyType()); // 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.Target.GetPropertyType()) - rightSideAccessor = Expression.Convert(rightSideAccessor, DataBinding.Target.GetPropertyType()); + if (rightSideAccessor.Type != DataBinding.LayerProperty.GetPropertyType()) + rightSideAccessor = Expression.Convert(rightSideAccessor, DataBinding.LayerProperty.GetPropertyType()); var modifierExpression = ModifierType.CreateExpression(currentValueParameter, rightSideAccessor); var lambda = Expression.Lambda>(modifierExpression, currentValueParameter, rightSideParameter); @@ -257,12 +257,12 @@ namespace Artemis.Core private void CreateStaticExpression() { - var currentValueParameter = Expression.Parameter(DataBinding.Target.GetPropertyType()); + var currentValueParameter = Expression.Parameter(DataBinding.LayerProperty.GetPropertyType()); // 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.Target.GetPropertyType()); + : Expression.Constant(null, DataBinding.LayerProperty.GetPropertyType()); var modifierExpression = ModifierType.CreateExpression(currentValueParameter, rightSideConstant); var lambda = Expression.Lambda>(modifierExpression, currentValueParameter); diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs index f616f352b..f807ea4a6 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs @@ -10,6 +10,7 @@ namespace Artemis.Core /// public abstract class BaseLayerProperty { + private readonly List _dataBindings = new List(); private bool _isHidden; private bool _keyframesEnabled; @@ -30,7 +31,17 @@ namespace Artemis.Core /// /// Gets whether keyframes are supported on this type of property /// - public bool KeyframesSupported { get; protected internal set; } = true; + public bool KeyframesSupported { get; protected internal set; } = true; + + /// + /// Gets whether data bindings are supported on this type of property + /// + 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 @@ -99,6 +110,12 @@ namespace Artemis.Core /// 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 /// @@ -113,6 +130,19 @@ namespace Artemis.Core /// internal abstract void ApplyToEntity(); + #region Data bindings + + /// + /// Applies the current to the layer property + /// + public void ApplyDataBindings() + { + foreach (var dataBinding in DataBindings) + ApplyDataBinding(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 9f47f97d4..7ecaaf718 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -178,12 +178,6 @@ namespace Artemis.Core return typeof(T); } - /// - public override List GetDataBindingProperties() - { - return new List {GetType().GetProperty(nameof(CurrentValue))}; - } - /// /// Called every update (if keyframes are both supported and enabled) to determine the new /// based on the provided progress @@ -234,6 +228,7 @@ namespace Artemis.Core _keyframes = _keyframes.OrderBy(k => k.Position).ToList(); } + internal override void ApplyToLayerProperty(PropertyEntity entity, LayerPropertyGroup layerPropertyGroup, bool fromStorage) { // Doubt this will happen but let's make sure @@ -289,5 +284,23 @@ namespace Artemis.Core EasingFunction = (int) k.EasingFunction })); } + + #region Data bindings + + /// + public override List GetDataBindingProperties() + { + return new List {GetType().GetProperty(nameof(CurrentValue))}; + } + + /// + protected override void ApplyDataBinding(DataBinding dataBinding) + { + // The default implementation only supports simple types + if (dataBinding.TargetProperty.DeclaringType == GetType()) + CurrentValue = (T) dataBinding.GetValue(CurrentValue); + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Types/ColorGradientLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/ColorGradientLayerProperty.cs index a0598537a..073f17462 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Types/ColorGradientLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/ColorGradientLayerProperty.cs @@ -8,6 +8,7 @@ namespace Artemis.Core internal ColorGradientLayerProperty() { KeyframesSupported = false; + DataBindingsSupported = false; } public static implicit operator ColorGradient(ColorGradientLayerProperty p) diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Types/EnumLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/EnumLayerProperty.cs index 0f0abd777..97b8ff3d5 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Types/EnumLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/EnumLayerProperty.cs @@ -8,6 +8,7 @@ namespace Artemis.Core public EnumLayerProperty() { KeyframesSupported = false; + DataBindingsSupported = false; } public static implicit operator T(EnumLayerProperty p) diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Types/LayerBrushReferenceLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/LayerBrushReferenceLayerProperty.cs index fcab68db6..cebc0b1d7 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Types/LayerBrushReferenceLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/LayerBrushReferenceLayerProperty.cs @@ -8,6 +8,7 @@ internal LayerBrushReferenceLayerProperty() { KeyframesSupported = false; + DataBindingsSupported = false; } public static implicit operator LayerBrushReference(LayerBrushReferenceLayerProperty p) diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKColorLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKColorLayerProperty.cs index 3895fd3a5..fbe8968ac 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKColorLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKColorLayerProperty.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using SkiaSharp; namespace Artemis.Core @@ -10,19 +13,44 @@ namespace Artemis.Core { } + /// + /// Implicitly converts an to an + /// + /// + /// public static implicit operator SKColor(SKColorLayerProperty p) { return p.CurrentValue; } + /// protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) { CurrentValue = CurrentKeyframe.Value.Interpolate(NextKeyframe.Value, keyframeProgressEased); } - private static byte ClampToByte(float value) + /// + public override List GetDataBindingProperties() { - return (byte) Math.Max(0, Math.Min(255, value)); + return typeof(SKColor).GetProperties().ToList(); + } + + /// + protected override void ApplyDataBinding(DataBinding dataBinding) + { + 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)) + { + CurrentValue.ToHsv(out var h, out var s, out var v); + CurrentValue = SKColor.FromHsv((float) dataBinding.GetValue(h), s, v); + } } } } \ 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 b7c608f54..e69eac535 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKPointLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKPointLayerProperty.cs @@ -1,4 +1,7 @@ -using SkiaSharp; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using SkiaSharp; namespace Artemis.Core { @@ -8,17 +11,36 @@ namespace Artemis.Core internal SKPointLayerProperty() { } - + + /// + /// Implicitly converts an to an + /// public static implicit operator SKPoint(SKPointLayerProperty p) { return p.CurrentValue; } + /// protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) { var xDiff = NextKeyframe.Value.X - CurrentKeyframe.Value.X; 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 bf3a0a00c..326f74527 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKSizeLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKSizeLayerProperty.cs @@ -12,21 +12,35 @@ namespace Artemis.Core { } + /// + /// Implicitly converts an to an + /// public static implicit operator SKSize(SKSizeLayerProperty p) { return p.CurrentValue; } - public override List GetDataBindingProperties() - { - return typeof(SKSize).GetProperties().Where(p => p.CanWrite).ToList(); - } - + /// protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) { var widthDiff = NextKeyframe.Value.Width - CurrentKeyframe.Value.Width; 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/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs index 684c99493..df10d1a78 100644 --- a/src/Artemis.Core/Services/RgbService.cs +++ b/src/Artemis.Core/Services/RgbService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using RGB.NET.Core; using RGB.NET.Groups; using Serilog; @@ -57,7 +58,7 @@ namespace Artemis.Core.Services throw e; } - if (deviceProvider.Devices == null) + if (deviceProvider.Devices == null || !deviceProvider.Devices.Any()) { _logger.Warning("Device provider {deviceProvider} has no devices", deviceProvider.GetType().Name); return; diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreePropertyView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreePropertyView.xaml index 957c4bf9f..a06db4ac0 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreePropertyView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreePropertyView.xaml @@ -51,6 +51,7 @@ Width="20" Height="20" VerticalAlignment="Center" + IsEnabled="{Binding LayerPropertyViewModel.LayerProperty.DataBindingsSupported}" IsChecked="{Binding DataBindingsOpen}">