diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index 3a632ed07..a9c4706ed 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -21,10 +21,11 @@ - + + - - + + @@ -34,8 +35,8 @@ - - + + diff --git a/src/Artemis.Core/Events/PropertyGroupUpdatingEventArgs.cs b/src/Artemis.Core/Events/PropertyGroupUpdatingEventArgs.cs new file mode 100644 index 000000000..a1f4c3b6c --- /dev/null +++ b/src/Artemis.Core/Events/PropertyGroupUpdatingEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace Artemis.Core.Events +{ + public class PropertyGroupUpdatingEventArgs : EventArgs + { + public PropertyGroupUpdatingEventArgs(double deltaTime) + { + DeltaTime = deltaTime; + } + + public PropertyGroupUpdatingEventArgs(TimeSpan overrideTime) + { + OverrideTime = overrideTime; + } + + public double DeltaTime { get; } + public TimeSpan OverrideTime { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/ColorGradient.cs b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs similarity index 75% rename from src/Artemis.Core/Models/Profile/ColorGradient.cs rename to src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs index 7414f13ef..bc66fb1ee 100644 --- a/src/Artemis.Core/Models/Profile/ColorGradient.cs +++ b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs @@ -6,7 +6,7 @@ using Artemis.Core.Annotations; using SkiaSharp; using Stylet; -namespace Artemis.Core.Models.Profile +namespace Artemis.Core.Models.Profile.Colors { public class ColorGradient : INotifyPropertyChanged { @@ -55,7 +55,7 @@ namespace Artemis.Core.Models.Profile if (right == null || left == right) return left.Color; - + position = (float) Math.Round((position - left.Position) / (right.Position - left.Position), 2); var a = (byte) ((right.Color.Alpha - left.Color.Alpha) * position + left.Color.Alpha); var r = (byte) ((right.Color.Red - left.Color.Red) * position + left.Color.Red); @@ -65,15 +65,19 @@ namespace Artemis.Core.Models.Profile } /// - /// [PH] Looping through HSV, adds 8 rainbow colors + /// Gets a new ColorGradient with colors looping through the HSV-spectrum /// - public void MakeFabulous() + /// + public static ColorGradient GetUnicornBarf() { + var gradient = new ColorGradient(); for (var i = 0; i < 9; i++) { var color = i != 8 ? SKColor.FromHsv(i * 32, 100, 100) : SKColor.FromHsv(0, 100, 100); - Stops.Add(new ColorGradientStop(color, 0.125f * i)); + gradient.Stops.Add(new ColorGradientStop(color, 0.125f * i)); } + + return gradient; } #region PropertyChanged @@ -88,28 +92,4 @@ namespace Artemis.Core.Models.Profile #endregion } - - public class ColorGradientStop : INotifyPropertyChanged - { - public ColorGradientStop(SKColor color, float position) - { - Color = color; - Position = position; - } - - public SKColor Color { get; set; } - public float Position { get; set; } - - #region PropertyChanged - - public event PropertyChangedEventHandler PropertyChanged; - - [NotifyPropertyChangedInvocator] - protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - #endregion - } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs b/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs new file mode 100644 index 000000000..7a860da42 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs @@ -0,0 +1,31 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Artemis.Core.Annotations; +using SkiaSharp; + +namespace Artemis.Core.Models.Profile.Colors +{ + public class ColorGradientStop : INotifyPropertyChanged + { + public ColorGradientStop(SKColor color, float position) + { + Color = color; + Position = position; + } + + public SKColor Color { get; set; } + public float Position { get; set; } + + #region PropertyChanged + + public event PropertyChangedEventHandler PropertyChanged; + + [NotifyPropertyChangedInvocator] + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/LayerCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/LayerCondition.cs new file mode 100644 index 000000000..fb91ef3b6 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/LayerCondition.cs @@ -0,0 +1,11 @@ +using System; +using System.Linq.Expressions; +using Artemis.Core.Plugins.Abstract.DataModels; + +namespace Artemis.Core.Models.Profile.Conditions +{ + public class LayerCondition + { + public Expression> ExpressionTree { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/KeyframeEngines/FloatKeyframeEngine.cs b/src/Artemis.Core/Models/Profile/KeyframeEngines/FloatKeyframeEngine.cs deleted file mode 100644 index 88a922d39..000000000 --- a/src/Artemis.Core/Models/Profile/KeyframeEngines/FloatKeyframeEngine.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using Artemis.Core.Models.Profile.LayerProperties; - -namespace Artemis.Core.Models.Profile.KeyframeEngines -{ - /// - public class FloatKeyframeEngine : KeyframeEngine - { - public sealed override List CompatibleTypes { get; } = new List {typeof(float)}; - - protected override object GetInterpolatedValue() - { - var currentKeyframe = (Keyframe) CurrentKeyframe; - var nextKeyframe = (Keyframe) NextKeyframe; - - var diff = nextKeyframe.Value - currentKeyframe.Value; - return currentKeyframe.Value + diff * KeyframeProgressEased; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/KeyframeEngines/IntKeyframeEngine.cs b/src/Artemis.Core/Models/Profile/KeyframeEngines/IntKeyframeEngine.cs deleted file mode 100644 index 9d25f8148..000000000 --- a/src/Artemis.Core/Models/Profile/KeyframeEngines/IntKeyframeEngine.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using Artemis.Core.Models.Profile.LayerProperties; - -namespace Artemis.Core.Models.Profile.KeyframeEngines -{ - /// - public class IntKeyframeEngine : KeyframeEngine - { - public sealed override List CompatibleTypes { get; } = new List {typeof(int)}; - - protected override object GetInterpolatedValue() - { - var currentKeyframe = (Keyframe) CurrentKeyframe; - var nextKeyframe = (Keyframe) NextKeyframe; - - var diff = nextKeyframe.Value - currentKeyframe.Value; - return (int) Math.Round(currentKeyframe.Value + diff * KeyframeProgressEased, MidpointRounding.AwayFromZero); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/KeyframeEngines/KeyframeEngine.cs b/src/Artemis.Core/Models/Profile/KeyframeEngines/KeyframeEngine.cs deleted file mode 100644 index 60396b945..000000000 --- a/src/Artemis.Core/Models/Profile/KeyframeEngines/KeyframeEngine.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Artemis.Core.Exceptions; -using Artemis.Core.Models.Profile.LayerProperties; -using Artemis.Core.Utilities; - -namespace Artemis.Core.Models.Profile.KeyframeEngines -{ - public abstract class KeyframeEngine - { - /// - /// Indicates whether has been called. - /// - public bool Initialized { get; private set; } - - /// - /// The layer property this keyframe engine applies to. - /// - public BaseLayerProperty LayerProperty { get; private set; } - - /// - /// The total progress - /// - public TimeSpan Progress { get; private set; } - - /// - /// The progress from the current keyframe to the next. - /// Range 0.0 to 1.0. - /// - public float KeyframeProgress { get; private set; } - - /// - /// The progress from the current keyframe to the next with the current keyframes easing function applied. - /// Range 0.0 to 1.0 but can be higher than 1.0 depending on easing function. - /// - public float KeyframeProgressEased { get; set; } - - /// - /// The current keyframe - /// - public BaseKeyframe CurrentKeyframe { get; private set; } - - /// - /// The next keyframe - /// - public BaseKeyframe NextKeyframe { get; private set; } - - /// - /// The types this keyframe engine supports. - /// - public abstract List CompatibleTypes { get; } - - /// - /// Associates the keyframe engine with the provided layer property. - /// - /// - public void Initialize(BaseLayerProperty layerProperty) - { - if (Initialized) - throw new ArtemisCoreException("Cannot initialize the same keyframe engine twice"); - if (!CompatibleTypes.Contains(layerProperty.Type)) - throw new ArtemisCoreException($"This property engine does not support the provided type {layerProperty.Type.Name}"); - - LayerProperty = layerProperty; - LayerProperty.KeyframeEngine = this; - Initialized = true; - } - - /// - /// Updates the engine's progress - /// - /// - public void Update(double deltaTime) - { - if (!Initialized) - return; - - var keyframes = LayerProperty.UntypedKeyframes.ToList(); - Progress = Progress.Add(TimeSpan.FromSeconds(deltaTime)); - // The current keyframe is the last keyframe before the current time - CurrentKeyframe = keyframes.LastOrDefault(k => k.Position <= Progress); - // The next keyframe is the first keyframe that's after the current time - NextKeyframe = keyframes.FirstOrDefault(k => k.Position > Progress); - - if (CurrentKeyframe == null) - { - KeyframeProgress = 0; - KeyframeProgressEased = 0; - } - else if (NextKeyframe == null) - { - KeyframeProgress = 1; - KeyframeProgressEased = 1; - } - else - { - var timeDiff = NextKeyframe.Position - CurrentKeyframe.Position; - KeyframeProgress = (float) ((Progress - CurrentKeyframe.Position).TotalMilliseconds / timeDiff.TotalMilliseconds); - KeyframeProgressEased = (float) Easings.Interpolate(KeyframeProgress, CurrentKeyframe.EasingFunction); - } - - // LayerProperty determines what's next: reset, stop, continue - } - - - /// - /// Overrides the engine's progress to the provided value - /// - /// - public void OverrideProgress(TimeSpan progress) - { - Progress = TimeSpan.Zero; - Update(progress.TotalSeconds); - } - - /// - /// Gets the current value, if the progress is in between two keyframes the value will be interpolated - /// - /// - public object GetCurrentValue() - { - if (CurrentKeyframe == null && LayerProperty.UntypedKeyframes.Any()) - return LayerProperty.UntypedKeyframes.First().BaseValue; - if (CurrentKeyframe == null) - return LayerProperty.BaseValue; - if (NextKeyframe == null) - return CurrentKeyframe.BaseValue; - - return GetInterpolatedValue(); - } - - protected abstract object GetInterpolatedValue(); - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/KeyframeEngines/SKColorKeyframeEngine.cs b/src/Artemis.Core/Models/Profile/KeyframeEngines/SKColorKeyframeEngine.cs deleted file mode 100644 index e0f0246b2..000000000 --- a/src/Artemis.Core/Models/Profile/KeyframeEngines/SKColorKeyframeEngine.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using Artemis.Core.Models.Profile.LayerProperties; -using SkiaSharp; - -namespace Artemis.Core.Models.Profile.KeyframeEngines -{ - /// - public class SKColorKeyframeEngine : KeyframeEngine - { - public sealed override List CompatibleTypes { get; } = new List {typeof(SKColor)}; - - protected override object GetInterpolatedValue() - { - var currentKeyframe = (Keyframe) CurrentKeyframe; - var nextKeyframe = (Keyframe) NextKeyframe; - - var redDiff = nextKeyframe.Value.Red - currentKeyframe.Value.Red; - var greenDiff = nextKeyframe.Value.Green - currentKeyframe.Value.Green; - var blueDiff = nextKeyframe.Value.Blue - currentKeyframe.Value.Blue; - var alphaDiff = nextKeyframe.Value.Alpha - currentKeyframe.Value.Alpha; - - return new SKColor( - ClampToByte(currentKeyframe.Value.Red + redDiff * KeyframeProgressEased), - ClampToByte(currentKeyframe.Value.Green + greenDiff * KeyframeProgressEased), - ClampToByte(currentKeyframe.Value.Blue + blueDiff * KeyframeProgressEased), - ClampToByte(currentKeyframe.Value.Alpha + alphaDiff * KeyframeProgressEased) - ); - } - - private byte ClampToByte(float value) - { - return (byte) Math.Max(0, Math.Min(255, value)); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/KeyframeEngines/SKPointKeyframeEngine.cs b/src/Artemis.Core/Models/Profile/KeyframeEngines/SKPointKeyframeEngine.cs deleted file mode 100644 index b38250ca6..000000000 --- a/src/Artemis.Core/Models/Profile/KeyframeEngines/SKPointKeyframeEngine.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using Artemis.Core.Models.Profile.LayerProperties; -using SkiaSharp; - -namespace Artemis.Core.Models.Profile.KeyframeEngines -{ - /// - public class SKPointKeyframeEngine : KeyframeEngine - { - public sealed override List CompatibleTypes { get; } = new List {typeof(SKPoint)}; - - protected override object GetInterpolatedValue() - { - var currentKeyframe = (Keyframe) CurrentKeyframe; - var nextKeyframe = (Keyframe) NextKeyframe; - - var xDiff = nextKeyframe.Value.X - currentKeyframe.Value.X; - var yDiff = nextKeyframe.Value.Y - currentKeyframe.Value.Y; - return new SKPoint(currentKeyframe.Value.X + xDiff * KeyframeProgressEased, currentKeyframe.Value.Y + yDiff * KeyframeProgressEased); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/KeyframeEngines/SKSizeKeyframeEngine.cs b/src/Artemis.Core/Models/Profile/KeyframeEngines/SKSizeKeyframeEngine.cs deleted file mode 100644 index ea9e8fe45..000000000 --- a/src/Artemis.Core/Models/Profile/KeyframeEngines/SKSizeKeyframeEngine.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using Artemis.Core.Models.Profile.LayerProperties; -using SkiaSharp; - -namespace Artemis.Core.Models.Profile.KeyframeEngines -{ - /// - public class SKSizeKeyframeEngine : KeyframeEngine - { - public sealed override List CompatibleTypes { get; } = new List {typeof(SKSize)}; - - protected override object GetInterpolatedValue() - { - var currentKeyframe = (Keyframe) CurrentKeyframe; - var nextKeyframe = (Keyframe) NextKeyframe; - - var widthDiff = nextKeyframe.Value.Width - currentKeyframe.Value.Width; - var heightDiff = nextKeyframe.Value.Height - currentKeyframe.Value.Height; - return new SKSize(currentKeyframe.Value.Width + widthDiff * KeyframeProgressEased, currentKeyframe.Value.Height + heightDiff * KeyframeProgressEased); - } - } -} \ 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 1de5eb131..e9f74c2d9 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -4,21 +4,29 @@ using System.Collections.ObjectModel; using System.Linq; using Artemis.Core.Extensions; using Artemis.Core.Models.Profile.LayerProperties; +using Artemis.Core.Models.Profile.LayerProperties.Attributes; using Artemis.Core.Models.Profile.LayerShapes; using Artemis.Core.Models.Surface; using Artemis.Core.Plugins.LayerBrush; +using Artemis.Core.Services; +using Artemis.Core.Services.Interfaces; using Artemis.Storage.Entities.Profile; using SkiaSharp; namespace Artemis.Core.Models.Profile { + /// + /// Represents a layer on a profile. To create new layers use the by injecting + /// into your code + /// public sealed class Layer : ProfileElement { + private readonly List _expandedPropertyGroups; private LayerShape _layerShape; private List _leds; private SKPath _path; - public Layer(Profile profile, ProfileElement parent, string name) + internal Layer(Profile profile, ProfileElement parent, string name) { LayerEntity = new LayerEntity(); EntityId = Guid.NewGuid(); @@ -26,12 +34,13 @@ namespace Artemis.Core.Models.Profile Profile = profile; Parent = parent; Name = name; - Properties = new LayerPropertyCollection(this); + General = new LayerGeneralProperties {IsCorePropertyGroup = true}; + Transform = new LayerTransformProperties {IsCorePropertyGroup = true}; _leds = new List(); + _expandedPropertyGroups = new List(); - ApplyShapeType(); - Properties.ShapeType.ValueChanged += (sender, args) => ApplyShapeType(); + General.PropertyGroupInitialized += GeneralOnPropertyGroupInitialized; } internal Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity) @@ -43,12 +52,14 @@ namespace Artemis.Core.Models.Profile Parent = parent; Name = layerEntity.Name; Order = layerEntity.Order; - Properties = new LayerPropertyCollection(this); + General = new LayerGeneralProperties {IsCorePropertyGroup = true}; + Transform = new LayerTransformProperties {IsCorePropertyGroup = true}; _leds = new List(); + _expandedPropertyGroups = new List(); + _expandedPropertyGroups.AddRange(layerEntity.ExpandedPropertyGroups); - ApplyShapeType(); - Properties.ShapeType.ValueChanged += (sender, args) => ApplyShapeType(); + General.PropertyGroupInitialized += GeneralOnPropertyGroupInitialized; } internal LayerEntity LayerEntity { get; set; } @@ -93,21 +104,35 @@ namespace Artemis.Core.Models.Profile } } - /// - /// The properties of this layer - /// - public LayerPropertyCollection Properties { get; set; } + [PropertyGroupDescription(Name = "General", Description = "A collection of general properties")] + public LayerGeneralProperties General { get; set; } + + [PropertyGroupDescription(Name = "Transform", Description = "A collection of transformation properties")] + public LayerTransformProperties Transform { get; set; } /// /// The brush that will fill the . /// - public LayerBrush LayerBrush { get; internal set; } + public BaseLayerBrush LayerBrush { get; internal set; } public override string ToString() { return $"[Layer] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}"; } + public bool IsPropertyGroupExpanded(LayerPropertyGroup layerPropertyGroup) + { + return _expandedPropertyGroups.Contains(layerPropertyGroup.Path); + } + + public void SetPropertyGroupExpanded(LayerPropertyGroup layerPropertyGroup, bool expanded) + { + if (!expanded && IsPropertyGroupExpanded(layerPropertyGroup)) + _expandedPropertyGroups.Remove(layerPropertyGroup.Path); + else if (expanded && !IsPropertyGroupExpanded(layerPropertyGroup)) + _expandedPropertyGroups.Add(layerPropertyGroup.Path); + } + #region Storage internal override void ApplyToEntity() @@ -118,8 +143,12 @@ namespace Artemis.Core.Models.Profile LayerEntity.Order = Order; LayerEntity.Name = Name; LayerEntity.ProfileId = Profile.EntityId; - foreach (var layerProperty in Properties) - layerProperty.ApplyToEntity(); + LayerEntity.ExpandedPropertyGroups.Clear(); + LayerEntity.ExpandedPropertyGroups.AddRange(_expandedPropertyGroups); + + General.ApplyToEntity(); + Transform.ApplyToEntity(); + LayerBrush?.BaseProperties.ApplyToEntity(); // LEDs LayerEntity.Leds.Clear(); @@ -141,9 +170,21 @@ namespace Artemis.Core.Models.Profile #region Shape management + private void GeneralOnPropertyGroupInitialized(object sender, EventArgs e) + { + ApplyShapeType(); + General.ShapeType.BaseValueChanged -= ShapeTypeOnBaseValueChanged; + General.ShapeType.BaseValueChanged += ShapeTypeOnBaseValueChanged; + } + + private void ShapeTypeOnBaseValueChanged(object sender, EventArgs e) + { + ApplyShapeType(); + } + private void ApplyShapeType() { - switch (Properties.ShapeType.CurrentValue) + switch (General.ShapeType.CurrentValue) { case LayerShapeType.Ellipse: LayerShape = new Ellipse(this); @@ -163,28 +204,43 @@ namespace Artemis.Core.Models.Profile /// public override void Update(double deltaTime) { - foreach (var property in Properties) - property.KeyframeEngine?.Update(deltaTime); + if (LayerBrush == null || !LayerBrush.BaseProperties.PropertiesInitialized) + return; + + var properties = new List(General.GetAllLayerProperties().Where(p => p.BaseKeyframes.Any())); + properties.AddRange(Transform.GetAllLayerProperties().Where(p => p.BaseKeyframes.Any())); + properties.AddRange(LayerBrush.BaseProperties.GetAllLayerProperties().Where(p => p.BaseKeyframes.Any())); // For now, reset all keyframe engines after the last keyframe was hit // This is a placeholder method of repeating the animation until repeat modes are implemented - var lastKeyframe = Properties.SelectMany(p => p.UntypedKeyframes).OrderByDescending(t => t.Position).FirstOrDefault(); - if (lastKeyframe != null) + var timeLineEnd = properties.Any() ? properties.Max(p => p.BaseKeyframes.Max(k => k.Position)) : TimeSpan.MaxValue; + if (properties.Any(p => p.TimelineProgress >= timeLineEnd)) { - if (Properties.Any(p => p.KeyframeEngine?.Progress > lastKeyframe.Position)) - { - foreach (var baseLayerProperty in Properties) - baseLayerProperty.KeyframeEngine?.OverrideProgress(TimeSpan.Zero); - } + General.Override(TimeSpan.Zero); + Transform.Override(TimeSpan.Zero); + LayerBrush.BaseProperties.Override(TimeSpan.Zero); + } + else + { + General.Update(deltaTime); + Transform.Update(deltaTime); + LayerBrush.BaseProperties.Update(deltaTime); } - LayerBrush?.Update(deltaTime); + LayerBrush.Update(deltaTime); + } + + public void OverrideProgress(TimeSpan timeOverride) + { + General.Override(timeOverride); + Transform.Override(timeOverride); + LayerBrush?.BaseProperties.Override(timeOverride); } /// public override void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo) { - if (Path == null || LayerShape?.Path == null) + if (Path == null || LayerShape?.Path == null || !General.PropertiesInitialized || !Transform.PropertiesInitialized) return; canvas.Save(); @@ -192,10 +248,10 @@ namespace Artemis.Core.Models.Profile using (var paint = new SKPaint()) { - paint.BlendMode = Properties.BlendMode.CurrentValue; - paint.Color = new SKColor(0, 0, 0, (byte) (Properties.Opacity.CurrentValue * 2.55f)); + paint.BlendMode = General.BlendMode.CurrentValue; + paint.Color = new SKColor(0, 0, 0, (byte) (Transform.Opacity.CurrentValue * 2.55f)); - switch (Properties.FillType.CurrentValue) + switch (General.FillType.CurrentValue) { case LayerFillType.Stretch: StretchRender(canvas, canvasInfo, paint); @@ -214,11 +270,11 @@ namespace Artemis.Core.Models.Profile private void StretchRender(SKCanvas canvas, SKImageInfo canvasInfo, SKPaint paint) { // Apply transformations - var sizeProperty = Properties.Scale.CurrentValue; - var rotationProperty = Properties.Rotation.CurrentValue; + var sizeProperty = Transform.Scale.CurrentValue; + var rotationProperty = Transform.Rotation.CurrentValue; var anchorPosition = GetLayerAnchorPosition(); - var anchorProperty = Properties.AnchorPoint.CurrentValue; + var anchorProperty = Transform.AnchorPoint.CurrentValue; // Translation originates from the unscaled center of the shape and is tied to the anchor var x = anchorPosition.X - Bounds.MidX - anchorProperty.X * Bounds.Width; @@ -229,17 +285,18 @@ namespace Artemis.Core.Models.Profile canvas.Scale(sizeProperty.Width / 100f, sizeProperty.Height / 100f, anchorPosition.X, anchorPosition.Y); canvas.Translate(x, y); - LayerBrush?.Render(canvas, canvasInfo, new SKPath(LayerShape.Path), paint); + if (LayerBrush != null && LayerBrush.BaseProperties.PropertiesInitialized) + LayerBrush.Render(canvas, canvasInfo, new SKPath(LayerShape.Path), paint); } private void ClipRender(SKCanvas canvas, SKImageInfo canvasInfo, SKPaint paint) { // Apply transformations - var sizeProperty = Properties.Scale.CurrentValue; - var rotationProperty = Properties.Rotation.CurrentValue; + var sizeProperty = Transform.Scale.CurrentValue; + var rotationProperty = Transform.Rotation.CurrentValue; var anchorPosition = GetLayerAnchorPosition(); - var anchorProperty = Properties.AnchorPoint.CurrentValue; + var anchorProperty = Transform.AnchorPoint.CurrentValue; // Translation originates from the unscaled center of the shape and is tied to the anchor var x = anchorPosition.X - Bounds.MidX - anchorProperty.X * Bounds.Width; @@ -292,7 +349,7 @@ namespace Artemis.Core.Models.Profile internal SKPoint GetLayerAnchorPosition() { - var positionProperty = Properties.Position.CurrentValue; + var positionProperty = Transform.Position.CurrentValue; // Start at the center of the shape var position = new SKPoint(Bounds.MidX, Bounds.MidY); @@ -371,6 +428,7 @@ namespace Artemis.Core.Models.Profile public event EventHandler RenderPropertiesUpdated; public event EventHandler ShapePropertiesUpdated; + public event EventHandler LayerBrushUpdated; private void OnRenderPropertiesUpdated() { @@ -382,6 +440,11 @@ namespace Artemis.Core.Models.Profile ShapePropertiesUpdated?.Invoke(this, EventArgs.Empty); } + internal void OnLayerBrushUpdated() + { + LayerBrushUpdated?.Invoke(this, EventArgs.Empty); + } + #endregion } diff --git a/src/Artemis.Core/Models/Profile/LayerGeneralProperties.cs b/src/Artemis.Core/Models/Profile/LayerGeneralProperties.cs new file mode 100644 index 000000000..e0a2b46d4 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerGeneralProperties.cs @@ -0,0 +1,32 @@ +using Artemis.Core.Models.Profile.LayerProperties.Attributes; +using Artemis.Core.Models.Profile.LayerProperties.Types; +using SkiaSharp; + +namespace Artemis.Core.Models.Profile +{ + public class LayerGeneralProperties : LayerPropertyGroup + { + [PropertyDescription(Name = "Shape type", Description = "The type of shape to draw in this layer")] + public EnumLayerProperty ShapeType { get; set; } + + [PropertyDescription(Name = "Fill type", Description = "How to make the shape adjust to scale changes")] + public EnumLayerProperty FillType { get; set; } + + [PropertyDescription(Name = "Blend mode", Description = "How to blend this layer into the resulting image")] + public EnumLayerProperty BlendMode { get; set; } + + [PropertyDescription(Name = "Brush type", Description = "The type of brush to use for this layer")] + public LayerBrushReferenceLayerProperty BrushReference { get; set; } + + protected override void PopulateDefaults() + { + ShapeType.DefaultValue = LayerShapeType.Rectangle; + FillType.DefaultValue = LayerFillType.Stretch; + BlendMode.DefaultValue = SKBlendMode.SrcOver; + } + + protected override void OnPropertiesInitialized() + { + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs new file mode 100644 index 000000000..ad00c49a9 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs @@ -0,0 +1,42 @@ +using System; + +namespace Artemis.Core.Models.Profile.LayerProperties.Attributes +{ + public class PropertyDescriptionAttribute : Attribute + { + /// + /// The user-friendly name for this property, shown in the UI + /// + public string Name { get; set; } + + /// + /// The user-friendly description for this property, shown in the UI + /// + public string Description { get; set; } + + /// + /// Input prefix to show before input elements in the UI + /// + public string InputPrefix { get; set; } + + /// + /// Input affix to show behind input elements in the UI + /// + public string InputAffix { get; set; } + + /// + /// The input drag step size, used in the UI + /// + public float InputStepSize { get; set; } + + /// + /// Minimum input value, only enforced in the UI + /// + public object MinInputValue { get; set; } + + /// + /// Maximum input value, only enforced in the UI + /// + public object MaxInputValue { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs new file mode 100644 index 000000000..ba725a66a --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs @@ -0,0 +1,22 @@ +using System; + +namespace Artemis.Core.Models.Profile.LayerProperties.Attributes +{ + public class PropertyGroupDescriptionAttribute : Attribute + { + /// + /// The user-friendly name for this property, shown in the UI. + /// + public string Name { get; set; } + + /// + /// The user-friendly description for this property, shown in the UI. + /// + public string Description { get; set; } + + /// + /// Whether to expand this property by default, this is useful for important parent properties. + /// + public bool ExpandByDefault { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/BaseKeyframe.cs b/src/Artemis.Core/Models/Profile/LayerProperties/BaseKeyframe.cs deleted file mode 100644 index 2f68a2a71..000000000 --- a/src/Artemis.Core/Models/Profile/LayerProperties/BaseKeyframe.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using Artemis.Core.Utilities; - -namespace Artemis.Core.Models.Profile.LayerProperties -{ - public class BaseKeyframe - { - private TimeSpan _position; - - protected BaseKeyframe(Layer layer, BaseLayerProperty property) - { - Layer = layer; - BaseProperty = property; - } - - public Layer Layer { get; set; } - - public TimeSpan Position - { - get => _position; - set - { - if (value == _position) return; - _position = value; - BaseProperty.SortKeyframes(); - } - } - - protected BaseLayerProperty BaseProperty { get; } - public object BaseValue { get; internal set; } - public Easings.Functions EasingFunction { get; set; } - } -} \ 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 184415ff9..bed89fd32 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs @@ -1,127 +1,53 @@ using System; using System.Collections.Generic; -using System.Linq; -using Artemis.Core.Exceptions; -using Artemis.Core.Models.Profile.KeyframeEngines; -using Artemis.Core.Plugins.Models; -using Artemis.Core.Utilities; using Artemis.Storage.Entities.Profile; -using Newtonsoft.Json; -using Stylet; namespace Artemis.Core.Models.Profile.LayerProperties { - public abstract class BaseLayerProperty : PropertyChangedBase + /// + /// For internal use only, to implement your own layer property type, extend instead. + /// + public abstract class BaseLayerProperty { - private object _baseValue; + private bool _keyframesEnabled; private bool _isHidden; - protected BaseLayerProperty(Layer layer, PluginInfo pluginInfo, BaseLayerProperty parent, string id, string name, string description, Type type) + internal BaseLayerProperty() { - Layer = layer; - PluginInfo = pluginInfo; - Parent = parent; - Id = id; - Name = name; - Description = description; - Type = type; - CanUseKeyframes = true; - InputStepSize = 1; - - // This can only be null if accessed internally - if (PluginInfo == null) - PluginInfo = Constants.CorePluginInfo; - - Children = new List(); - BaseKeyframes = new List(); - - - parent?.Children.Add(this); } /// - /// Gets the layer this property applies to + /// The layer this property applies to /// - public Layer Layer { get; } + public Layer Layer { get; internal set; } /// - /// Info of the plugin associated with this property + /// The parent group of this layer property, set after construction /// - public PluginInfo PluginInfo { get; } + public LayerPropertyGroup Parent { get; internal set; } /// - /// Gets the parent property of this property. + /// Gets whether keyframes are supported on this property /// - public BaseLayerProperty Parent { get; } + public bool KeyframesSupported { get; protected set; } = true; /// - /// Gets or sets the child properties of this property. - /// If the layer has children it cannot contain a value or keyframes. + /// Gets or sets whether keyframes are enabled on this property, has no effect if is + /// False /// - public List Children { get; set; } + public bool KeyframesEnabled + { + get => _keyframesEnabled; + set + { + if (_keyframesEnabled == value) return; + _keyframesEnabled = value; + OnKeyframesToggled(); + } + } /// - /// Gets or sets a unique identifier for this property, a layer may not contain two properties with the same ID. - /// - public string Id { get; set; } - - /// - /// Gets or sets the user-friendly name for this property, shown in the UI. - /// - public string Name { get; set; } - - /// - /// Gets or sets the user-friendly description for this property, shown in the UI. - /// - public string Description { get; set; } - - /// - /// Gets or sets whether to expand this property by default, this is useful for important parent properties. - /// - public bool ExpandByDefault { get; set; } - - /// - /// Gets or sets the an optional input prefix to show before input elements in the UI. - /// - public string InputPrefix { get; set; } - - /// - /// Gets or sets an optional input affix to show behind input elements in the UI. - /// - public string InputAffix { get; set; } - - /// - /// Gets or sets an optional maximum input value, only enforced in the UI. - /// - public object MaxInputValue { get; set; } - - /// - /// Gets or sets the input drag step size, used in the UI. - /// - public float InputStepSize { get; set; } - - /// - /// Gets or sets an optional minimum input value, only enforced in the UI. - /// - public object MinInputValue { get; set; } - - /// - /// Gets or sets whether this property can use keyframes, True by default. - /// - public bool CanUseKeyframes { get; set; } - - /// - /// Gets or sets whether this property is using keyframes. - /// - public bool IsUsingKeyframes { get; set; } - - /// - /// Gets the type of value this layer property contains. - /// - public Type Type { get; protected set; } - - /// - /// Gets or sets whether this property is hidden in the UI. + /// Gets or sets whether the property is hidden in the UI /// public bool IsHidden { @@ -134,241 +60,109 @@ namespace Artemis.Core.Models.Profile.LayerProperties } /// - /// Gets a list of keyframes defining different values of the property in time, this list contains the untyped - /// . + /// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied /// - public IReadOnlyCollection UntypedKeyframes => BaseKeyframes.AsReadOnly(); + public bool IsLoadedFromStorage { get; internal set; } /// - /// Gets the keyframe engine instance of this property + /// Gets the total progress on the timeline /// - public KeyframeEngine KeyframeEngine { get; internal set; } - - protected List BaseKeyframes { get; set; } - - public object BaseValue - { - get => _baseValue; - internal set - { - if (value != null && value.GetType() != Type) - throw new ArtemisCoreException($"Cannot set value of type {value.GetType()} on property {this}, expected type is {Type}."); - if (!Equals(_baseValue, value)) - { - _baseValue = value; - OnValueChanged(); - } - } - } + public TimeSpan TimelineProgress { get; internal set; } /// - /// Creates a new keyframe for this base property without knowing the type + /// Used to declare that this property doesn't belong to a plugin and should use the core plugin GUID /// - /// - public BaseKeyframe CreateNewKeyframe(TimeSpan position, object value) - { - // Create a strongly typed keyframe or else it cannot be cast later on - var keyframeType = typeof(Keyframe<>); - var keyframe = (BaseKeyframe) Activator.CreateInstance(keyframeType.MakeGenericType(Type), Layer, this); - keyframe.Position = position; - keyframe.BaseValue = value; - BaseKeyframes.Add(keyframe); - SortKeyframes(); - - return keyframe; - } + public bool IsCoreProperty { get; internal set; } /// - /// Removes all keyframes from the property and sets the base value to the current value. + /// Gets a list of all the keyframes in their non-generic base form, without their values being available /// - public void ClearKeyframes() - { - if (KeyframeEngine != null) - BaseValue = KeyframeEngine.GetCurrentValue(); + public abstract IReadOnlyList BaseKeyframes { get; } + + internal PropertyEntity PropertyEntity { get; set; } + internal LayerPropertyGroup LayerPropertyGroup { get; set; } - BaseKeyframes.Clear(); - } /// - /// Gets the current value using the regular value or if present, keyframes + /// Applies the provided property entity to the layer property by deserializing the JSON base value and keyframe values /// - public object GetCurrentValue() - { - if (KeyframeEngine == null || !UntypedKeyframes.Any()) - return BaseValue; - - return KeyframeEngine.GetCurrentValue(); - } + /// + /// + /// + internal abstract void ApplyToLayerProperty(PropertyEntity entity, LayerPropertyGroup layerPropertyGroup, bool fromStorage); /// - /// Gets the current value using the regular value or keyframes. + /// Saves the property to the underlying property entity that was configured when calling + /// /// - /// The value to set. - /// - /// An optional time to set the value add, if provided and property is using keyframes the value will be set to an new - /// or existing keyframe. - /// - public void SetCurrentValue(object value, TimeSpan? time) - { - if (value != null && value.GetType() != Type) - throw new ArtemisCoreException($"Cannot set value of type {value.GetType()} on property {this}, expected type is {Type}."); - - if (time == null || !CanUseKeyframes || !IsUsingKeyframes) - BaseValue = value; - else - { - // If on a keyframe, update the keyframe - var currentKeyframe = UntypedKeyframes.FirstOrDefault(k => k.Position == time.Value); - // Create a new keyframe if none found - if (currentKeyframe == null) - currentKeyframe = CreateNewKeyframe(time.Value, value); - - currentKeyframe.BaseValue = value; - } - - OnValueChanged(); - } - - /// - /// Adds a keyframe to the property. - /// - /// The keyframe to remove - public void AddKeyframe(BaseKeyframe keyframe) - { - BaseKeyframes.Add(keyframe); - SortKeyframes(); - } - - /// - /// Removes a keyframe from the property. - /// - /// The keyframe to remove - public void RemoveKeyframe(BaseKeyframe keyframe) - { - BaseKeyframes.Remove(keyframe); - SortKeyframes(); - } - - /// - /// Returns the flattened index of this property on the layer - /// - /// - public int GetFlattenedIndex() - { - if (Parent == null) - return Layer.Properties.ToList().IndexOf(this); - - // Create a flattened list of all properties in their order as defined by the parent/child hierarchy - var properties = new List(); - // Iterate root properties (those with children) - foreach (var baseLayerProperty in Layer.Properties) - { - // First add self, then add all children - if (baseLayerProperty.Children.Any()) - { - properties.Add(baseLayerProperty); - properties.AddRange(baseLayerProperty.GetAllChildren()); - } - } - - return properties.IndexOf(this); - } - - public override string ToString() - { - return $"{nameof(Id)}: {Id}, {nameof(Name)}: {Name}, {nameof(Description)}: {Description}"; - } - - internal void ApplyToEntity() - { - var propertyEntity = Layer.LayerEntity.PropertyEntities.FirstOrDefault(p => p.Id == Id); - if (propertyEntity == null) - { - propertyEntity = new PropertyEntity {Id = Id}; - Layer.LayerEntity.PropertyEntities.Add(propertyEntity); - } - - propertyEntity.ValueType = Type.Name; - propertyEntity.Value = JsonConvert.SerializeObject(BaseValue); - propertyEntity.IsUsingKeyframes = IsUsingKeyframes; - - propertyEntity.KeyframeEntities.Clear(); - foreach (var baseKeyframe in BaseKeyframes) - { - propertyEntity.KeyframeEntities.Add(new KeyframeEntity - { - Position = baseKeyframe.Position, - Value = JsonConvert.SerializeObject(baseKeyframe.BaseValue), - EasingFunction = (int) baseKeyframe.EasingFunction - }); - } - } - - internal void ApplyToProperty(PropertyEntity propertyEntity) - { - BaseValue = DeserializePropertyValue(propertyEntity.Value); - IsUsingKeyframes = propertyEntity.IsUsingKeyframes; - - BaseKeyframes.Clear(); - foreach (var keyframeEntity in propertyEntity.KeyframeEntities.OrderBy(e => e.Position)) - { - // Create a strongly typed keyframe or else it cannot be cast later on - var keyframeType = typeof(Keyframe<>); - var keyframe = (BaseKeyframe) Activator.CreateInstance(keyframeType.MakeGenericType(Type), Layer, this); - keyframe.Position = keyframeEntity.Position; - keyframe.BaseValue = DeserializePropertyValue(keyframeEntity.Value); - keyframe.EasingFunction = (Easings.Functions) keyframeEntity.EasingFunction; - - BaseKeyframes.Add(keyframe); - } - } - - internal void SortKeyframes() - { - BaseKeyframes = BaseKeyframes.OrderBy(k => k.Position).ToList(); - } - - internal IEnumerable GetAllChildren() - { - var children = new List(); - children.AddRange(Children); - foreach (var layerPropertyViewModel in Children) - children.AddRange(layerPropertyViewModel.GetAllChildren()); - - return children; - } - - private object DeserializePropertyValue(string value) - { - if (value == "null") - return Type.IsValueType ? Activator.CreateInstance(Type) : null; - return JsonConvert.DeserializeObject(value, Type); - } + internal abstract void ApplyToEntity(); #region Events /// - /// Occurs when this property's value was changed outside regular keyframe updates + /// Occurs once every frame when the layer property is updated /// - public event EventHandler ValueChanged; + public event EventHandler Updated; /// - /// Occurs when this property or any of it's ancestors visibility is changed + /// Occurs when the base value of the layer property was updated /// - public event EventHandler VisibilityChanged; + public event EventHandler BaseValueChanged; - protected virtual void OnValueChanged() + /// + /// Occurs when the value of the layer property was updated + /// + public event EventHandler VisibilityChanged; + + /// + /// Occurs when keyframes are enabled/disabled + /// + public event EventHandler KeyframesToggled; + + /// + /// Occurs when a new keyframe was added to the layer property + /// + public event EventHandler KeyframeAdded; + + /// + /// Occurs when a keyframe was removed from the layer property + /// + public event EventHandler KeyframeRemoved; + + protected virtual void OnUpdated() { - ValueChanged?.Invoke(this, EventArgs.Empty); + Updated?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnBaseValueChanged() + { + BaseValueChanged?.Invoke(this, EventArgs.Empty); } protected virtual void OnVisibilityChanged() { VisibilityChanged?.Invoke(this, EventArgs.Empty); - foreach (var baseLayerProperty in Children) - baseLayerProperty.OnVisibilityChanged(); } - + + protected virtual void OnKeyframesToggled() + { + KeyframesToggled?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnKeyframeAdded() + { + KeyframeAdded?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnKeyframeRemoved() + { + KeyframeRemoved?.Invoke(this, EventArgs.Empty); + } + #endregion + + public abstract void ApplyDefaultValue(); + + } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerPropertyKeyframe.cs b/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerPropertyKeyframe.cs new file mode 100644 index 000000000..d0537d8b1 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerPropertyKeyframe.cs @@ -0,0 +1,31 @@ +using System; +using Artemis.Core.Utilities; + +namespace Artemis.Core.Models.Profile.LayerProperties +{ + /// + /// For internal use only, use instead. + /// + public abstract class BaseLayerPropertyKeyframe + { + internal BaseLayerPropertyKeyframe(BaseLayerProperty baseLayerProperty) + { + BaseLayerProperty = baseLayerProperty; + } + + /// + /// The base class of the layer property this keyframe is applied to + /// + public BaseLayerProperty BaseLayerProperty { get; internal set; } + + /// + /// The position of this keyframe in the timeline + /// + public abstract TimeSpan Position { get; set; } + + /// + /// The easing function applied on the value of the keyframe + /// + public Easings.Functions EasingFunction { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Keyframe.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Keyframe.cs deleted file mode 100644 index 5cd31a61e..000000000 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Keyframe.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Artemis.Core.Models.Profile.LayerProperties -{ - /// - public class Keyframe : BaseKeyframe - { - public Keyframe(Layer layer, LayerProperty propertyBase) : base(layer, propertyBase) - { - } - - public LayerProperty Property => (LayerProperty) BaseProperty; - - public T Value - { - get => BaseValue != null ? (T) BaseValue : default; - set => BaseValue = value; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 644957862..edf61b8d5 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -1,76 +1,291 @@ -using System.Collections.ObjectModel; +using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Linq; -using Artemis.Core.Plugins.LayerBrush; -using Artemis.Core.Plugins.Models; +using Artemis.Core.Exceptions; +using Artemis.Core.Utilities; +using Artemis.Storage.Entities.Profile; +using Newtonsoft.Json; namespace Artemis.Core.Models.Profile.LayerProperties { /// - /// Represents a property on the layer. This property is visible in the profile editor and can be key-framed (unless - /// opted out). - /// To create and register a new LayerProperty use + /// Represents a property on a layer. Properties are saved in storage and can optionally be modified from the UI. + /// + /// Note: You cannot initialize layer properties yourself. If properly placed and annotated, the Artemis core will + /// initialize + /// these for you. + /// /// - /// - public class LayerProperty : BaseLayerProperty + /// The type of property encapsulated in this layer property + public abstract class LayerProperty : BaseLayerProperty { - internal LayerProperty(Layer layer, BaseLayerProperty parent, string id, string name, string description) - : base(layer, null, parent, id, name, description, typeof(T)) - { - } + private T _baseValue; + private T _currentValue; + private bool _isInitialized; + private List> _keyframes; - internal LayerProperty(Layer layer, string id, string name, string description) - : base(layer, null, null, id, name, description, typeof(T)) - { - } - - internal LayerProperty(Layer layer, PluginInfo pluginInfo, BaseLayerProperty parent, string id, string name, string description) - : base(layer, pluginInfo, parent, id, name, description, typeof(T)) + protected LayerProperty() { + _keyframes = new List>(); } /// - /// Gets or sets the value of the property without any keyframes applied + /// Gets or sets the base value of this layer property without any keyframes applied /// - public T Value + public T BaseValue { - get => BaseValue != null ? (T) BaseValue : default; - set => BaseValue = value; - } - - /// - /// Gets the value of the property with keyframes applied - /// - public T CurrentValue - { - get + get => _baseValue; + set { - var currentValue = GetCurrentValue(); - return currentValue == null ? default : (T) currentValue; + if (_baseValue != null && !_baseValue.Equals(value) || _baseValue == null && value != null) + { + _baseValue = value; + OnBaseValueChanged(); + } } } /// - /// Gets a list of keyframes defining different values of the property in time, this list contains the strongly typed - /// + /// Gets the current value of this property as it is affected by it's keyframes, updated once every frame /// - public ReadOnlyCollection> Keyframes => BaseKeyframes.Cast>().ToList().AsReadOnly(); - - /// - /// Adds a keyframe to the property. - /// - /// The keyframe to remove - public void AddKeyframe(Keyframe keyframe) + public T CurrentValue { - base.AddKeyframe(keyframe); + get => !KeyframesEnabled || !KeyframesSupported ? BaseValue : _currentValue; + internal set => _currentValue = value; } /// - /// Removes a keyframe from the property. + /// Gets or sets the default value of this layer property. If set, this value is automatically applied if the property has no + /// value in storage + /// + public T DefaultValue { get; set; } + + /// + /// Gets a read-only list of all the keyframes on this layer property + /// + public IReadOnlyList> Keyframes => _keyframes.AsReadOnly(); + + /// + /// Gets the current keyframe in the timeline according to the current progress + /// + public LayerPropertyKeyframe CurrentKeyframe { get; protected set; } + + /// + /// Gets the next keyframe in the timeline according to the current progress + /// + public LayerPropertyKeyframe NextKeyframe { get; protected set; } + + public override IReadOnlyList BaseKeyframes => _keyframes.Cast().ToList().AsReadOnly(); + + /// + /// Sets the current value, using either keyframes if enabled or the base value. + /// + /// The value to set. + /// + /// An optional time to set the value add, if provided and property is using keyframes the value will be set to an new + /// or existing keyframe. + /// + public void SetCurrentValue(T value, TimeSpan? time) + { + if (time == null || !KeyframesEnabled || !KeyframesSupported) + BaseValue = value; + else + { + // If on a keyframe, update the keyframe + var currentKeyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value); + // Create a new keyframe if none found + if (currentKeyframe == null) + AddKeyframe(new LayerPropertyKeyframe(value, time.Value, Easings.Functions.Linear, this)); + else + currentKeyframe.Value = value; + + // Update the property so that the new keyframe is reflected on the current value + Update(0); + } + } + + /// + /// Adds a keyframe to the layer property + /// + /// The keyframe to add + public void AddKeyframe(LayerPropertyKeyframe keyframe) + { + if (_keyframes.Contains(keyframe)) + return; + + keyframe.LayerProperty?.RemoveKeyframe(keyframe); + + keyframe.LayerProperty = this; + keyframe.BaseLayerProperty = this; + _keyframes.Add(keyframe); + SortKeyframes(); + OnKeyframeAdded(); + } + + /// + /// Removes a keyframe from the layer property /// /// The keyframe to remove - public void RemoveKeyframe(Keyframe keyframe) + public LayerPropertyKeyframe CopyKeyframe(LayerPropertyKeyframe keyframe) { - base.RemoveKeyframe(keyframe); + var newKeyframe = new LayerPropertyKeyframe( + keyframe.Value, + keyframe.Position, + keyframe.EasingFunction, + keyframe.LayerProperty + ); + AddKeyframe(newKeyframe); + + return newKeyframe; + } + + /// + /// Removes a keyframe from the layer property + /// + /// The keyframe to remove + public void RemoveKeyframe(LayerPropertyKeyframe keyframe) + { + if (!_keyframes.Contains(keyframe)) + return; + + _keyframes.Remove(keyframe); + keyframe.LayerProperty = null; + keyframe.BaseLayerProperty = null; + SortKeyframes(); + OnKeyframeRemoved(); + } + + /// + /// Removes all keyframes from the layer property + /// + public void ClearKeyframes() + { + var keyframes = new List>(_keyframes); + foreach (var layerPropertyKeyframe in keyframes) + RemoveKeyframe(layerPropertyKeyframe); + } + + /// + /// Called every update (if keyframes are both supported and enabled) to determine the new + /// based on the provided progress + /// + /// The linear current keyframe progress + /// The current keyframe progress, eased with the current easing function + protected abstract void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased); + + /// + /// Updates the property, moving the timeline forwards by the provided + /// + /// The amount of time to move the timeline forwards + internal void Update(double deltaTime) + { + TimelineProgress = TimelineProgress.Add(TimeSpan.FromSeconds(deltaTime)); + if (!KeyframesSupported || !KeyframesEnabled) + return; + + // The current keyframe is the last keyframe before the current time + CurrentKeyframe = _keyframes.LastOrDefault(k => k.Position <= TimelineProgress); + // 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) ((TimelineProgress - CurrentKeyframe.Position).TotalMilliseconds / timeDiff.TotalMilliseconds); + var keyframeProgressEased = (float) Easings.Interpolate(keyframeProgress, CurrentKeyframe.EasingFunction); + UpdateCurrentValue(keyframeProgress, keyframeProgressEased); + } + + OnUpdated(); + } + + /// + /// Overrides the timeline progress to match the provided + /// + /// The new progress to set the layer property timeline to. + internal void OverrideProgress(TimeSpan overrideTime) + { + TimelineProgress = TimeSpan.Zero; + Update(overrideTime.TotalSeconds); + } + + /// + /// Sorts the keyframes in ascending order by position + /// + internal void SortKeyframes() + { + _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 + if (_isInitialized) + throw new ArtemisCoreException("Layer property already initialized, wut"); + + PropertyEntity = entity; + LayerPropertyGroup = layerPropertyGroup; + LayerPropertyGroup.PropertyGroupUpdating += (sender, args) => Update(args.DeltaTime); + LayerPropertyGroup.PropertyGroupOverriding += (sender, args) => OverrideProgress(args.OverrideTime); + + try + { + if (entity.Value != null) + BaseValue = JsonConvert.DeserializeObject(entity.Value); + + IsLoadedFromStorage = fromStorage; + CurrentValue = BaseValue; + KeyframesEnabled = entity.KeyframesEnabled; + + _keyframes.Clear(); + _keyframes.AddRange(entity.KeyframeEntities.Select(k => new LayerPropertyKeyframe( + JsonConvert.DeserializeObject(k.Value), + k.Position, + (Easings.Functions) k.EasingFunction, + this + ))); + } + catch (JsonException e) + { + // TODO: Properly log the JSON exception + Debug.WriteLine($"JSON exception while deserializing: {e}"); + IsLoadedFromStorage = false; + } + finally + { + SortKeyframes(); + _isInitialized = true; + } + } + + internal override void ApplyToEntity() + { + if (!_isInitialized) + throw new ArtemisCoreException("Layer property is not yet initialized"); + + PropertyEntity.Value = JsonConvert.SerializeObject(BaseValue); + PropertyEntity.KeyframesEnabled = KeyframesEnabled; + PropertyEntity.KeyframeEntities.Clear(); + PropertyEntity.KeyframeEntities.AddRange(Keyframes.Select(k => new KeyframeEntity + { + Value = JsonConvert.SerializeObject(k.Value), + Position = k.Position, + EasingFunction = (int) k.EasingFunction + })); + } + + public override void ApplyDefaultValue() + { + BaseValue = DefaultValue; + CurrentValue = DefaultValue; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyCollection.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyCollection.cs deleted file mode 100644 index 80b5a65e6..000000000 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyCollection.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using Artemis.Core.Events; -using Artemis.Core.Exceptions; -using Artemis.Core.Plugins.Models; -using SkiaSharp; - -namespace Artemis.Core.Models.Profile.LayerProperties -{ - /// - /// Contains all the properties of the layer and provides easy access to the default properties. - /// - public class LayerPropertyCollection : IEnumerable - { - private readonly Dictionary<(Guid, string), BaseLayerProperty> _properties; - - internal LayerPropertyCollection(Layer layer) - { - _properties = new Dictionary<(Guid, string), BaseLayerProperty>(); - - Layer = layer; - CreateDefaultProperties(); - } - - /// - /// Gets the layer these properties are applied on - /// - public Layer Layer { get; } - - /// - public IEnumerator GetEnumerator() - { - return _properties.Values.GetEnumerator(); - } - - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - /// - /// If found, returns the matching the provided ID - /// - /// The type of the layer property - /// The plugin this property belongs to - /// - /// - public LayerProperty GetLayerPropertyById(PluginInfo pluginInfo, string id) - { - if (!_properties.ContainsKey((pluginInfo.Guid, id))) - return null; - - var property = _properties[(pluginInfo.Guid, id)]; - if (property.Type != typeof(T)) - throw new ArtemisCoreException($"Property type mismatch. Expected property {property} to have type {typeof(T)} but it has {property.Type} instead."); - return (LayerProperty)_properties[(pluginInfo.Guid, id)]; - } - - /// - /// Removes the provided layer property from the layer. - /// - /// The type of value of the layer property - /// The property to remove from the layer - internal void RemoveLayerProperty(LayerProperty layerProperty) - { - RemoveLayerProperty((BaseLayerProperty) layerProperty); - } - - /// - /// Removes the provided layer property from the layer. - /// - /// The property to remove from the layer - internal void RemoveLayerProperty(BaseLayerProperty layerProperty) - { - if (!_properties.ContainsKey((layerProperty.PluginInfo.Guid, layerProperty.Id))) - throw new ArtemisCoreException($"Could not find a property with ID {layerProperty.Id}."); - - var property = _properties[(layerProperty.PluginInfo.Guid, layerProperty.Id)]; - property.Parent?.Children.Remove(property); - _properties.Remove((layerProperty.PluginInfo.Guid, layerProperty.Id)); - - OnLayerPropertyRemoved(new LayerPropertyEventArgs(property)); - } - - /// - /// Adds the provided layer property and its children to the layer. - /// If found, the last stored base value and keyframes will be applied to the provided property. - /// - /// The type of value of the layer property - /// The property to apply to the layer - /// True if an existing value was found and applied, otherwise false. - internal bool RegisterLayerProperty(LayerProperty layerProperty) - { - return RegisterLayerProperty((BaseLayerProperty) layerProperty); - } - - /// - /// Adds the provided layer property to the layer. - /// If found, the last stored base value and keyframes will be applied to the provided property. - /// - /// The property to apply to the layer - /// True if an existing value was found and applied, otherwise false. - internal bool RegisterLayerProperty(BaseLayerProperty layerProperty) - { - if (_properties.ContainsKey((layerProperty.PluginInfo.Guid, layerProperty.Id))) - throw new ArtemisCoreException($"Duplicate property ID detected. Layer already contains a property with ID {layerProperty.Id}."); - - var entity = Layer.LayerEntity.PropertyEntities.FirstOrDefault(p => p.Id == layerProperty.Id && p.ValueType == layerProperty.Type.Name); - // TODO: Catch serialization exceptions and log them - if (entity != null) - layerProperty.ApplyToProperty(entity); - - _properties.Add((layerProperty.PluginInfo.Guid, layerProperty.Id), layerProperty); - OnLayerPropertyRegistered(new LayerPropertyEventArgs(layerProperty)); - return entity != null; - } - - #region Default properties - - /// - /// Gets the shape type property of the layer - /// - public LayerProperty ShapeType { get; private set; } - - /// - /// Gets the fill type property of the layer - /// - public LayerProperty FillType { get; private set; } - - /// - /// Gets the blend mode property of the layer - /// - public LayerProperty BlendMode { get; private set; } - - /// - /// Gets the brush reference property of the layer - /// - public LayerProperty BrushReference { get; private set; } - - /// - /// Gets the anchor point property of the layer - /// - public LayerProperty AnchorPoint { get; private set; } - - /// - /// Gets the position of the layer - /// - public LayerProperty Position { get; private set; } - - /// - /// Gets the size property of the layer - /// - public LayerProperty Scale { get; private set; } - - /// - /// Gets the rotation property of the layer range 0 - 360 - /// - public LayerProperty Rotation { get; private set; } - - /// - /// Gets the opacity property of the layer range 0 - 100 - /// - public LayerProperty Opacity { get; private set; } - - private void CreateDefaultProperties() - { - // Shape - var shape = new LayerProperty(Layer, "Core.Shape", "Shape", "A collection of basic shape properties"); - ShapeType = new LayerProperty(Layer, shape, "Core.ShapeType", "Shape type", "The type of shape to draw in this layer") {CanUseKeyframes = false}; - FillType = new LayerProperty(Layer, shape, "Core.FillType", "Fill type", "How to make the shape adjust to scale changes") {CanUseKeyframes = false}; - BlendMode = new LayerProperty(Layer, shape, "Core.BlendMode", "Blend mode", "How to blend this layer into the resulting image") {CanUseKeyframes = false}; - ShapeType.Value = LayerShapeType.Rectangle; - FillType.Value = LayerFillType.Stretch; - BlendMode.Value = SKBlendMode.SrcOver; - - RegisterLayerProperty(shape); - foreach (var shapeProperty in shape.Children) - RegisterLayerProperty(shapeProperty); - - // Brush - var brush = new LayerProperty(Layer, "Core.Brush", "Brush", "A collection of properties that configure the selected brush"); - BrushReference = new LayerProperty(Layer, brush, "Core.BrushReference", "Brush type", "The type of brush to use for this layer") {CanUseKeyframes = false}; - - RegisterLayerProperty(brush); - foreach (var brushProperty in brush.Children) - RegisterLayerProperty(brushProperty); - - // Transform - var transform = new LayerProperty(Layer, "Core.Transform", "Transform", "A collection of transformation properties") {ExpandByDefault = true}; - AnchorPoint = new LayerProperty(Layer, transform, "Core.AnchorPoint", "Anchor Point", "The point at which the shape is attached to its position") {InputStepSize = 0.001f}; - Position = new LayerProperty(Layer, transform, "Core.Position", "Position", "The position of the shape") {InputStepSize = 0.001f}; - Scale = new LayerProperty(Layer, transform, "Core.Scale", "Scale", "The scale of the shape") {InputAffix = "%", MinInputValue = 0f}; - Rotation = new LayerProperty(Layer, transform, "Core.Rotation", "Rotation", "The rotation of the shape in degrees") {InputAffix = "°"}; - Opacity = new LayerProperty(Layer, transform, "Core.Opacity", "Opacity", "The opacity of the shape") {InputAffix = "%", MinInputValue = 0f, MaxInputValue = 100f}; - Scale.Value = new SKSize(100, 100); - Opacity.Value = 100; - - RegisterLayerProperty(transform); - foreach (var transformProperty in transform.Children) - RegisterLayerProperty(transformProperty); - } - - #endregion - - #region Events - - public event EventHandler LayerPropertyRegistered; - public event EventHandler LayerPropertyRemoved; - - private void OnLayerPropertyRegistered(LayerPropertyEventArgs e) - { - LayerPropertyRegistered?.Invoke(this, e); - } - - private void OnLayerPropertyRemoved(LayerPropertyEventArgs e) - { - LayerPropertyRemoved?.Invoke(this, e); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs new file mode 100644 index 000000000..af37209aa --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs @@ -0,0 +1,39 @@ +using System; +using Artemis.Core.Utilities; + +namespace Artemis.Core.Models.Profile.LayerProperties +{ + public class LayerPropertyKeyframe : BaseLayerPropertyKeyframe + { + private TimeSpan _position; + + public LayerPropertyKeyframe(T value, TimeSpan position, Easings.Functions easingFunction, LayerProperty layerProperty) : base(layerProperty) + { + _position = position; + Value = value; + LayerProperty = layerProperty; + EasingFunction = easingFunction; + } + + /// + /// The layer property this keyframe is applied to + /// + public LayerProperty LayerProperty { get; internal set; } + + /// + /// The value of this keyframe + /// + public T Value { get; set; } + + /// + public override TimeSpan Position + { + get => _position; + set + { + _position = value; + LayerProperty.SortKeyframes(); + } + } + } +} \ 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 new file mode 100644 index 000000000..11cf760d3 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/ColorGradientLayerProperty.cs @@ -0,0 +1,28 @@ +using Artemis.Core.Exceptions; +using Artemis.Core.Models.Profile.Colors; +using Artemis.Storage.Entities.Profile; + +namespace Artemis.Core.Models.Profile.LayerProperties.Types +{ + /// + public class ColorGradientLayerProperty : LayerProperty + { + internal ColorGradientLayerProperty() + { + KeyframesSupported = false; + } + + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) + { + throw new ArtemisCoreException("Color Gradients do not support keyframes."); + } + + internal override void ApplyToLayerProperty(PropertyEntity entity, LayerPropertyGroup layerPropertyGroup, bool fromStorage) + { + base.ApplyToLayerProperty(entity, layerPropertyGroup, fromStorage); + + // Don't allow color gradients to be null + BaseValue ??= DefaultValue ?? new ColorGradient(); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Types/EnumLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/EnumLayerProperty.cs new file mode 100644 index 000000000..63cfbbfd7 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/EnumLayerProperty.cs @@ -0,0 +1,19 @@ +using System; +using Artemis.Core.Exceptions; + +namespace Artemis.Core.Models.Profile.LayerProperties.Types +{ + /// + public class EnumLayerProperty : LayerProperty where T : Enum + { + public EnumLayerProperty() + { + KeyframesSupported = false; + } + + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) + { + throw new ArtemisCoreException("Enum properties do not support keyframes."); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Types/FloatLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/FloatLayerProperty.cs new file mode 100644 index 000000000..bc6e2ed01 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/FloatLayerProperty.cs @@ -0,0 +1,16 @@ +namespace Artemis.Core.Models.Profile.LayerProperties.Types +{ + /// + public class FloatLayerProperty : LayerProperty + { + internal FloatLayerProperty() + { + } + + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) + { + var diff = NextKeyframe.Value - CurrentKeyframe.Value; + CurrentValue = CurrentKeyframe.Value + diff * keyframeProgressEased; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Types/IntLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/IntLayerProperty.cs new file mode 100644 index 000000000..96eab86f2 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/IntLayerProperty.cs @@ -0,0 +1,18 @@ +using System; + +namespace Artemis.Core.Models.Profile.LayerProperties.Types +{ + /// + public class IntLayerProperty : LayerProperty + { + internal IntLayerProperty() + { + } + + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) + { + var diff = NextKeyframe.Value - CurrentKeyframe.Value; + CurrentValue = (int) Math.Round(CurrentKeyframe.Value + diff * keyframeProgressEased, MidpointRounding.AwayFromZero); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Types/LayerBrushReferenceLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/LayerBrushReferenceLayerProperty.cs new file mode 100644 index 000000000..f1d99e642 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/LayerBrushReferenceLayerProperty.cs @@ -0,0 +1,20 @@ +using Artemis.Core.Exceptions; + +namespace Artemis.Core.Models.Profile.LayerProperties.Types +{ + /// + /// A special layer property used to configure the selected layer brush + /// + public class LayerBrushReferenceLayerProperty : LayerProperty + { + internal LayerBrushReferenceLayerProperty() + { + KeyframesSupported = false; + } + + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) + { + throw new ArtemisCoreException("Layer brush references do not support keyframes."); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKColorLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKColorLayerProperty.cs new file mode 100644 index 000000000..b478cbd77 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKColorLayerProperty.cs @@ -0,0 +1,33 @@ +using System; +using SkiaSharp; + +namespace Artemis.Core.Models.Profile.LayerProperties.Types +{ + /// + public class SKColorLayerProperty : LayerProperty + { + internal SKColorLayerProperty() + { + } + + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) + { + var redDiff = NextKeyframe.Value.Red - CurrentKeyframe.Value.Red; + var greenDiff = NextKeyframe.Value.Green - CurrentKeyframe.Value.Green; + var blueDiff = NextKeyframe.Value.Blue - CurrentKeyframe.Value.Blue; + var alphaDiff = NextKeyframe.Value.Alpha - CurrentKeyframe.Value.Alpha; + + CurrentValue = new SKColor( + ClampToByte(CurrentKeyframe.Value.Red + redDiff * keyframeProgressEased), + ClampToByte(CurrentKeyframe.Value.Green + greenDiff * keyframeProgressEased), + ClampToByte(CurrentKeyframe.Value.Blue + blueDiff * keyframeProgressEased), + ClampToByte(CurrentKeyframe.Value.Alpha + alphaDiff * keyframeProgressEased) + ); + } + + private static byte ClampToByte(float value) + { + return (byte) Math.Max(0, Math.Min(255, value)); + } + } +} \ 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 new file mode 100644 index 000000000..7595d2d62 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKPointLayerProperty.cs @@ -0,0 +1,19 @@ +using SkiaSharp; + +namespace Artemis.Core.Models.Profile.LayerProperties.Types +{ + /// + public class SKPointLayerProperty : LayerProperty + { + internal SKPointLayerProperty() + { + } + + 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); + } + } +} \ 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 new file mode 100644 index 000000000..f89229005 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKSizeLayerProperty.cs @@ -0,0 +1,19 @@ +using SkiaSharp; + +namespace Artemis.Core.Models.Profile.LayerProperties.Types +{ + /// + public class SKSizeLayerProperty : LayerProperty + { + internal SKSizeLayerProperty() + { + } + + 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); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs new file mode 100644 index 000000000..5b0dc8a60 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Artemis.Core.Annotations; +using Artemis.Core.Events; +using Artemis.Core.Exceptions; +using Artemis.Core.Models.Profile.LayerProperties; +using Artemis.Core.Models.Profile.LayerProperties.Attributes; +using Artemis.Core.Plugins.Exceptions; +using Artemis.Core.Services.Interfaces; +using Artemis.Storage.Entities.Profile; + +namespace Artemis.Core.Models.Profile +{ + public abstract class LayerPropertyGroup + { + private readonly List _layerProperties; + private readonly List _layerPropertyGroups; + private ReadOnlyCollection _allLayerProperties; + private bool _isHidden; + + protected LayerPropertyGroup() + { + _layerProperties = new List(); + _layerPropertyGroups = new List(); + } + + /// + /// The layer this property group applies to + /// + public Layer Layer { get; internal set; } + + /// + /// The path of this property group + /// + public string Path { get; internal set; } + + /// + /// The parent group of this layer property group, set after construction + /// + public LayerPropertyGroup Parent { get; internal set; } + + /// + /// Gets whether this property group's properties are all initialized + /// + public bool PropertiesInitialized { get; private set; } + + /// + /// Used to declare that this property group doesn't belong to a plugin and should use the core plugin GUID + /// + public bool IsCorePropertyGroup { get; internal set; } + + /// + /// Gets or sets whether the property is hidden in the UI + /// + public bool IsHidden + { + get => _isHidden; + set + { + _isHidden = value; + OnVisibilityChanged(); + } + } + + /// + /// A list of all layer properties in this group + /// + public ReadOnlyCollection LayerProperties => _layerProperties.AsReadOnly(); + + /// + /// A list of al child groups in this group + /// + public ReadOnlyCollection LayerPropertyGroups => _layerPropertyGroups.AsReadOnly(); + + /// + /// Recursively gets all layer properties on this group and any subgroups + /// + /// + public IReadOnlyCollection GetAllLayerProperties() + { + if (!PropertiesInitialized) + return new List(); + if (_allLayerProperties != null) + return _allLayerProperties; + + var result = new List(LayerProperties); + foreach (var layerPropertyGroup in LayerPropertyGroups) + result.AddRange(layerPropertyGroup.GetAllLayerProperties()); + + _allLayerProperties = result.AsReadOnly(); + return _allLayerProperties; + } + + /// + /// Called before properties are fully initialized to allow you to populate + /// on the properties you want + /// + protected abstract void PopulateDefaults(); + + /// + /// Called when all layer properties in this property group have been initialized, you may access all properties on the + /// group here + /// + protected abstract void OnPropertiesInitialized(); + + protected virtual void OnPropertyGroupInitialized() + { + PropertyGroupInitialized?.Invoke(this, EventArgs.Empty); + } + + internal void InitializeProperties(ILayerService layerService, Layer layer, [NotNull] string path) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + // Doubt this will happen but let's make sure + if (PropertiesInitialized) + throw new ArtemisCoreException("Layer property group already initialized, wut"); + + Layer = layer; + Path = path.TrimEnd('.'); + + // Get all properties with a PropertyDescriptionAttribute + foreach (var propertyInfo in GetType().GetProperties()) + { + var propertyDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyDescriptionAttribute)); + if (propertyDescription != null) + { + if (!typeof(BaseLayerProperty).IsAssignableFrom(propertyInfo.PropertyType)) + throw new ArtemisPluginException("Layer property with PropertyDescription attribute must be of type LayerProperty"); + + var instance = (BaseLayerProperty) Activator.CreateInstance(propertyInfo.PropertyType, true); + instance.Parent = this; + instance.Layer = layer; + InitializeProperty(layer, path + propertyInfo.Name, instance); + propertyInfo.SetValue(this, instance); + _layerProperties.Add(instance); + } + else + { + var propertyGroupDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyGroupDescriptionAttribute)); + if (propertyGroupDescription != null) + { + if (!typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType)) + throw new ArtemisPluginException("Layer property with PropertyGroupDescription attribute must be of type LayerPropertyGroup"); + + var instance = (LayerPropertyGroup) Activator.CreateInstance(propertyInfo.PropertyType); + instance.Parent = this; + instance.InitializeProperties(layerService, layer, $"{path}{propertyInfo.Name}."); + propertyInfo.SetValue(this, instance); + _layerPropertyGroups.Add(instance); + } + } + } + + PopulateDefaults(); + foreach (var layerProperty in _layerProperties.Where(p => !p.IsLoadedFromStorage)) + layerProperty.ApplyDefaultValue(); + + OnPropertiesInitialized(); + PropertiesInitialized = true; + OnPropertyGroupInitialized(); + } + + internal void ApplyToEntity() + { + // Get all properties with a PropertyDescriptionAttribute + foreach (var propertyInfo in GetType().GetProperties()) + { + var propertyDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyDescriptionAttribute)); + if (propertyDescription != null) + { + var layerProperty = (BaseLayerProperty) propertyInfo.GetValue(this); + layerProperty.ApplyToEntity(); + } + else + { + var propertyGroupDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyGroupDescriptionAttribute)); + if (propertyGroupDescription != null) + { + var layerPropertyGroup = (LayerPropertyGroup) propertyInfo.GetValue(this); + layerPropertyGroup.ApplyToEntity(); + } + } + } + } + + internal void Update(double deltaTime) + { + // Since at this point we don't know what properties the group has without using reflection, + // let properties subscribe to the update event and update themselves + OnPropertyGroupUpdating(new PropertyGroupUpdatingEventArgs(deltaTime)); + } + + internal void Override(TimeSpan overrideTime) + { + // Same as above, but now the progress is overridden + OnPropertyGroupOverriding(new PropertyGroupUpdatingEventArgs(overrideTime)); + } + + private void InitializeProperty(Layer layer, string path, BaseLayerProperty instance) + { + var pluginGuid = IsCorePropertyGroup || instance.IsCoreProperty ? Constants.CorePluginInfo.Guid : layer.LayerBrush.PluginInfo.Guid; + var entity = layer.LayerEntity.PropertyEntities.FirstOrDefault(p => p.PluginGuid == pluginGuid && p.Path == path); + var fromStorage = true; + if (entity == null) + { + fromStorage = false; + entity = new PropertyEntity {PluginGuid = pluginGuid, Path = path}; + layer.LayerEntity.PropertyEntities.Add(entity); + } + + instance.ApplyToLayerProperty(entity, this, fromStorage); + } + + #region Events + + internal event EventHandler PropertyGroupUpdating; + internal event EventHandler PropertyGroupOverriding; + public event EventHandler PropertyGroupInitialized; + + /// + /// Occurs when the value of the layer property was updated + /// + public event EventHandler VisibilityChanged; + + internal virtual void OnPropertyGroupUpdating(PropertyGroupUpdatingEventArgs e) + { + PropertyGroupUpdating?.Invoke(this, e); + } + + protected virtual void OnPropertyGroupOverriding(PropertyGroupUpdatingEventArgs e) + { + PropertyGroupOverriding?.Invoke(this, e); + } + + protected virtual void OnVisibilityChanged() + { + VisibilityChanged?.Invoke(this, EventArgs.Empty); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs b/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs new file mode 100644 index 000000000..81b35a838 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs @@ -0,0 +1,34 @@ +using Artemis.Core.Models.Profile.LayerProperties.Attributes; +using Artemis.Core.Models.Profile.LayerProperties.Types; +using SkiaSharp; + +namespace Artemis.Core.Models.Profile +{ + public class LayerTransformProperties : LayerPropertyGroup + { + [PropertyDescription(Description = "The point at which the shape is attached to its position", InputStepSize = 0.001f)] + public SKPointLayerProperty AnchorPoint { get; set; } + + [PropertyDescription(Description = "The position of the shape", InputStepSize = 0.001f)] + public SKPointLayerProperty Position { get; set; } + + [PropertyDescription(Description = "The scale of the shape", InputAffix = "%", MinInputValue = 0f)] + public SKSizeLayerProperty Scale { get; set; } + + [PropertyDescription(Description = "The rotation of the shape in degrees", InputAffix = "°")] + public FloatLayerProperty Rotation { get; set; } + + [PropertyDescription(Description = "The opacity of the shape", InputAffix = "%", MinInputValue = 0f, MaxInputValue = 100f)] + public FloatLayerProperty Opacity { get; set; } + + protected override void PopulateDefaults() + { + Scale.DefaultValue = new SKSize(100, 100); + Opacity.DefaultValue = 100; + } + + protected override void OnPropertiesInitialized() + { + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/ProfileElement.cs b/src/Artemis.Core/Models/Profile/ProfileElement.cs index 89d74aa99..6657ade42 100644 --- a/src/Artemis.Core/Models/Profile/ProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/ProfileElement.cs @@ -131,5 +131,10 @@ namespace Artemis.Core.Models.Profile /// Applies the profile element's properties to the underlying storage entity /// internal abstract void ApplyToEntity(); + + public override string ToString() + { + return $"{nameof(EntityId)}: {EntityId}, {nameof(Order)}: {Order}, {nameof(Name)}: {Name}"; + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Ninject/CoreModule.cs b/src/Artemis.Core/Ninject/CoreModule.cs index 123f2076e..b61a0f0c5 100644 --- a/src/Artemis.Core/Ninject/CoreModule.cs +++ b/src/Artemis.Core/Ninject/CoreModule.cs @@ -1,9 +1,9 @@ -using System; -using System.IO; +using System.IO; using Artemis.Core.Exceptions; -using Artemis.Core.Models.Profile.KeyframeEngines; using Artemis.Core.Plugins.Models; using Artemis.Core.Services.Interfaces; +using Artemis.Storage; +using Artemis.Storage.Migrations.Interfaces; using Artemis.Storage.Repositories.Interfaces; using LiteDB; using Ninject.Activation; @@ -53,16 +53,27 @@ namespace Artemis.Core.Ninject catch (LiteException e) { // I don't like this way of error reporting, now I need to use reflection if I want a meaningful error code - if (e.ErrorCode != LiteException.INVALID_DATABASE) + if (e.ErrorCode != LiteException.INVALID_DATABASE) throw new ArtemisCoreException($"LiteDB threw error code {e.ErrorCode}. See inner exception for more details", e); // If the DB is invalid it's probably LiteDB v4 (TODO: we'll have to do something better later) File.Delete($"{Constants.DataFolder}\\database.db"); return new LiteRepository(Constants.ConnectionString); } - }).InSingletonScope(); + Kernel.Bind().ToSelf().InSingletonScope(); + + // Bind all migrations as singletons + Kernel.Bind(x => + { + x.FromAssemblyContaining() + .SelectAllClasses() + .InheritedFrom() + .BindAllInterfaces() + .Configure(c => c.InSingletonScope()); + }); + // Bind all repositories as singletons Kernel.Bind(x => { @@ -73,15 +84,6 @@ namespace Artemis.Core.Ninject .Configure(c => c.InSingletonScope()); }); - // Bind all keyframe engines - Kernel.Bind(x => - { - x.FromAssemblyContaining() - .SelectAllClasses() - .InheritedFrom() - .BindAllBaseClasses(); - }); - Kernel.Bind().ToProvider(); Kernel.Bind().ToProvider(); } diff --git a/src/Artemis.Core/Plugins/Abstract/DataModels/Attributes/DataModelProperty.cs b/src/Artemis.Core/Plugins/Abstract/DataModels/Attributes/DataModelProperty.cs new file mode 100644 index 000000000..030f951c2 --- /dev/null +++ b/src/Artemis.Core/Plugins/Abstract/DataModels/Attributes/DataModelProperty.cs @@ -0,0 +1,43 @@ +using System; + +namespace Artemis.Core.Plugins.Abstract.DataModels.Attributes +{ + [AttributeUsage(System.AttributeTargets.Property)] + public class DataModelPropertyAttribute : Attribute + { + /// + /// Gets or sets the user-friendly name for this property, shown in the UI. + /// + public string Name { get; set; } + + /// + /// Gets or sets the user-friendly description for this property, shown in the UI. + /// + public string Description { get; set; } + + /// + /// Gets or sets the an optional input prefix to show before input elements in the UI. + /// + public string InputPrefix { get; set; } + + /// + /// Gets or sets an optional input affix to show behind input elements in the UI. + /// + public string InputAffix { get; set; } + + /// + /// Gets or sets an optional maximum input value, only enforced in the UI. + /// + public object MaxInputValue { get; set; } + + /// + /// Gets or sets the input drag step size, used in the UI. + /// + public float InputStepSize { get; set; } + + /// + /// Gets or sets an optional minimum input value, only enforced in the UI. + /// + public object MinInputValue { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Abstract/DataModels/DataModel.cs b/src/Artemis.Core/Plugins/Abstract/DataModels/DataModel.cs new file mode 100644 index 000000000..372e4191b --- /dev/null +++ b/src/Artemis.Core/Plugins/Abstract/DataModels/DataModel.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Artemis.Core.Plugins.Abstract.DataModels.Attributes; +using Artemis.Core.Plugins.Exceptions; +using SkiaSharp; + +namespace Artemis.Core.Plugins.Abstract.DataModels +{ + public abstract class DataModel + { + private static readonly List SupportedTypes = new List + { + typeof(bool), + typeof(byte), + typeof(decimal), + typeof(double), + typeof(float), + typeof(int), + typeof(long), + typeof(string), + typeof(SKColor), + typeof(SKPoint) + }; + + protected DataModel(Module module) + { + Module = module; + Validate(); + } + + public Module Module { get; } + + /// + /// Recursively validates the current datamodel, ensuring all properties annotated with + /// are of supported types. + /// + /// + public bool Validate() + { + return ValidateType(GetType()); + } + + private bool ValidateType(Type type) + { + foreach (var propertyInfo in type.GetProperties()) + { + var dataModelPropertyAttribute = (DataModelPropertyAttribute) Attribute.GetCustomAttribute(propertyInfo, typeof(DataModelPropertyAttribute)); + if (dataModelPropertyAttribute == null) + continue; + + // If the a nested datamodel, ensure the properties on there are valid + if (typeof(DataModel).IsAssignableFrom(propertyInfo.PropertyType)) + ValidateType(propertyInfo.PropertyType); + else if (!SupportedTypes.Contains(propertyInfo.PropertyType)) + { + // Show a useful error for plugin devs + throw new ArtemisPluginException(Module.PluginInfo, + $"Plugin datamodel contains property of unsupported type {propertyInfo.PropertyType.Name}. \r\n\r\n" + + $"Property name: {propertyInfo.Name}\r\n" + + $"Property declared on: {propertyInfo.DeclaringType?.Name ?? "-"} \r\n\r\n" + + $"Supported properties:\r\n{string.Join("\r\n", SupportedTypes.Select(t => $" - {t.Name}"))}"); + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Abstract/Module.cs b/src/Artemis.Core/Plugins/Abstract/Module.cs index 2c69f9d4e..1e0b3951a 100644 --- a/src/Artemis.Core/Plugins/Abstract/Module.cs +++ b/src/Artemis.Core/Plugins/Abstract/Module.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Artemis.Core.Models.Surface; +using Artemis.Core.Plugins.Abstract.DataModels; using Artemis.Core.Plugins.Abstract.ViewModels; using Artemis.Core.Plugins.Models; using SkiaSharp; @@ -27,6 +28,11 @@ namespace Artemis.Core.Plugins.Abstract /// public string DisplayIcon { get; set; } + /// + /// The optional datamodel driving this module + /// + public DataModel DataModel { get; set; } + /// /// Whether or not this module expands upon the main data model. If set to true any data in main data model can be /// accessed by profiles in this module diff --git a/src/Artemis.Core/Plugins/Abstract/ModuleDataModel.cs b/src/Artemis.Core/Plugins/Abstract/ModuleDataModel.cs deleted file mode 100644 index aef8fe778..000000000 --- a/src/Artemis.Core/Plugins/Abstract/ModuleDataModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Artemis.Core.Plugins.Abstract -{ - public abstract class ModuleDataModel - { - protected ModuleDataModel(Module module) - { - Module = module; - } - - public Module Module { get; } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrush/BaseLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrush/BaseLayerBrush.cs new file mode 100644 index 000000000..5a06297f8 --- /dev/null +++ b/src/Artemis.Core/Plugins/LayerBrush/BaseLayerBrush.cs @@ -0,0 +1,64 @@ +using System; +using Artemis.Core.Models.Profile; +using Artemis.Core.Plugins.Models; +using Artemis.Core.Services.Interfaces; +using SkiaSharp; + +namespace Artemis.Core.Plugins.LayerBrush +{ + /// + /// A basic layer brush that does not implement any layer property, to use properties with persistent storage, + /// implement instead + /// + public abstract class BaseLayerBrush : IDisposable + { + /// + /// Gets the layer this brush is applied to + /// + public Layer Layer { get; internal set; } + + /// + /// Gets the descriptor of this brush + /// + public LayerBrushDescriptor Descriptor { get; internal set; } + + /// + /// Gets the plugin info that defined this brush + /// + public PluginInfo PluginInfo => Descriptor.LayerBrushProvider.PluginInfo; + + public virtual LayerPropertyGroup BaseProperties => null; + + /// + /// Called when the brush is being removed from the layer + /// + public virtual void Dispose() + { + } + + /// + /// Called before rendering every frame, write your update logic here + /// + /// + public virtual void Update(double deltaTime) + { + } + + /// + /// The main method of rendering anything to the layer. The provided is specific to the layer + /// and matches it's width and height. + /// Called during rendering or layer preview, in the order configured on the layer + /// + /// The layer canvas + /// + /// The path to be filled, represents the shape + /// The paint to be used to fill the shape + public virtual void Render(SKCanvas canvas, SKImageInfo canvasInfo, SKPath path, SKPaint paint) + { + } + + internal virtual void InitializeProperties(ILayerService layerService, string path) + { + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrush/LayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrush/LayerBrush.cs index bd83f5f8e..6c518d3c4 100644 --- a/src/Artemis.Core/Plugins/LayerBrush/LayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrush/LayerBrush.cs @@ -1,16 +1,15 @@ using System; -using System.Linq; +using System.Collections.Generic; using Artemis.Core.Models.Profile; using Artemis.Core.Models.Profile.LayerProperties; using Artemis.Core.Plugins.Exceptions; using Artemis.Core.Services.Interfaces; -using SkiaSharp; namespace Artemis.Core.Plugins.LayerBrush { - public abstract class LayerBrush : IDisposable + public abstract class LayerBrush : BaseLayerBrush where T : LayerPropertyGroup { - private ILayerService _layerService; + private T _properties; protected LayerBrush(Layer layer, LayerBrushDescriptor descriptor) { @@ -18,112 +17,61 @@ namespace Artemis.Core.Plugins.LayerBrush Descriptor = descriptor; } - public Layer Layer { get; } - public LayerBrushDescriptor Descriptor { get; } - - public virtual void Dispose() - { - } + #region Properties /// - /// Called before rendering every frame, write your update logic here + /// Gets the properties of this brush. /// - /// - public virtual void Update(double deltaTime) + public T Properties { - } - - /// - /// The main method of rendering anything to the layer. The provided is specific to the layer - /// and matches it's width and height. - /// Called during rendering or layer preview, in the order configured on the layer - /// - /// The layer canvas - /// - /// The path to be filled, represents the shape - /// The paint to be used to fill the shape - public virtual void Render(SKCanvas canvas, SKImageInfo canvasInfo, SKPath path, SKPaint paint) - { - } - - /// - /// Provides an easy way to add your own properties to the layer, for more info see . - /// Note: If found, the last value and keyframes are loaded and set when calling this method. - /// - /// - /// The parent of this property, use this to create a tree-hierarchy in the editor - /// A and ID identifying your property, must be unique within your plugin - /// A name for your property, this is visible in the editor - /// A description for your property, this is visible in the editor - /// The default value of the property, if not configured by the user - /// The layer property - protected LayerProperty RegisterLayerProperty(BaseLayerProperty parent, string id, string name, string description, T defaultValue = default) - { - var property = new LayerProperty(Layer, Descriptor.LayerBrushProvider.PluginInfo, parent, id, name, description) {Value = defaultValue}; - Layer.Properties.RegisterLayerProperty(property); - // It's fine if this is null, it'll be picked up by SetLayerService later - _layerService?.InstantiateKeyframeEngine(property); - return property; - } - - /// - /// Provides an easy way to add your own properties to the layer, for more info see . - /// Note: If found, the last value and keyframes are loaded and set when calling this method. - /// - /// - /// A and ID identifying your property, must be unique within your plugin - /// A name for your property, this is visible in the editor - /// A description for your property, this is visible in the editor - /// The default value of the property, if not configured by the user - /// The layer property - protected LayerProperty RegisterLayerProperty(string id, string name, string description, T defaultValue = default) - { - // Check if the property already exists - var existing = Layer.Properties.FirstOrDefault(p => - p.PluginInfo.Guid == Descriptor.LayerBrushProvider.PluginInfo.Guid && - p.Id == id && - p.Name == name && - p.Description == description); - - if (existing != null) + get { - // If it exists and the types match, return the existing property - if (existing.Type == typeof(T)) - return (LayerProperty) existing; - // If it exists and the types are different, something is wrong - throw new ArtemisPluginException($"Cannot register the property {id} with different types twice."); + // I imagine a null reference here can be confusing, so lets throw an exception explaining what to do + if (_properties == null) + throw new ArtemisPluginException("Cannot access brush properties until OnPropertiesInitialized has been called"); + return _properties; } - - var property = new LayerProperty(Layer, Descriptor.LayerBrushProvider.PluginInfo, Layer.Properties.BrushReference.Parent, id, name, description) - { - Value = defaultValue - }; - - Layer.Properties.RegisterLayerProperty(property); - // It's fine if this is null, it'll be picked up by SetLayerService later - _layerService?.InstantiateKeyframeEngine(property); - return property; + internal set => _properties = value; } /// - /// Allows you to remove layer properties previously added by using . + /// Gets whether all properties on this brush are initialized /// - /// - /// - protected void UnRegisterLayerProperty(LayerProperty layerProperty) - { - if (layerProperty == null) - return; + public bool PropertiesInitialized { get; private set; } - if (Layer.Properties.Any(p => p == layerProperty)) - Layer.Properties.RemoveLayerProperty(layerProperty); + /// + /// Called when all layer properties in this brush have been initialized + /// + protected virtual void OnPropertiesInitialized() + { } - internal void SetLayerService(ILayerService layerService) + /// + public override LayerPropertyGroup BaseProperties => Properties; + + internal override void InitializeProperties(ILayerService layerService, string path) { - _layerService = layerService; - foreach (var baseLayerProperty in Layer.Properties) - _layerService.InstantiateKeyframeEngine(baseLayerProperty); + Properties = Activator.CreateInstance(); + Properties.InitializeProperties(layerService, Layer, path); + OnPropertiesInitialized(); + PropertiesInitialized = true; } + + internal virtual void ApplyToEntity() + { + Properties.ApplyToEntity(); + } + + internal virtual void OverrideProperties(TimeSpan overrideTime) + { + Properties.Override(overrideTime); + } + + internal virtual IReadOnlyCollection GetAllLayerProperties() + { + return Properties.GetAllLayerProperties(); + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrush/LayerBrushProvider.cs b/src/Artemis.Core/Plugins/LayerBrush/LayerBrushProvider.cs index 65d73d17d..53f505f7a 100644 --- a/src/Artemis.Core/Plugins/LayerBrush/LayerBrushProvider.cs +++ b/src/Artemis.Core/Plugins/LayerBrush/LayerBrushProvider.cs @@ -20,7 +20,7 @@ namespace Artemis.Core.Plugins.LayerBrush public ReadOnlyCollection LayerBrushDescriptors => _layerBrushDescriptors.AsReadOnly(); - protected void AddLayerBrushDescriptor(string displayName, string description, string icon) where T : LayerBrush + protected void AddLayerBrushDescriptor(string displayName, string description, string icon) where T : BaseLayerBrush { _layerBrushDescriptors.Add(new LayerBrushDescriptor(displayName, description, icon, typeof(T), this)); } diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index 896186074..6a07ae5b1 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -9,6 +9,8 @@ using Artemis.Core.Plugins.Abstract; using Artemis.Core.Plugins.Models; using Artemis.Core.Services.Interfaces; using Artemis.Core.Services.Storage.Interfaces; +using Artemis.Storage; +using Artemis.Storage.Migrations.Interfaces; using Newtonsoft.Json; using RGB.NET.Core; using Serilog; @@ -30,8 +32,9 @@ namespace Artemis.Core.Services private List _modules; private PluginSetting _loggingLevel; - internal CoreService(ILogger logger, ISettingsService settingsService, IPluginService pluginService, IRgbService rgbService, - ISurfaceService surfaceService, IProfileService profileService) + // ReSharper disable once UnusedParameter.Local - Storage migration service is injected early to ensure it runs before anything else + internal CoreService(ILogger logger, StorageMigrationService _, ISettingsService settingsService, IPluginService pluginService, + IRgbService rgbService, ISurfaceService surfaceService, IProfileService profileService) { _logger = logger; _pluginService = pluginService; @@ -48,6 +51,7 @@ namespace Artemis.Core.Services _pluginService.PluginEnabled += (sender, args) => _modules = _pluginService.GetPluginsOfType(); _pluginService.PluginDisabled += (sender, args) => _modules = _pluginService.GetPluginsOfType(); + ConfigureJsonConvert(); } @@ -99,7 +103,7 @@ namespace Artemis.Core.Services _logger.Information("Initialized without an active surface entity"); _profileService.ActivateDefaultProfiles(); - + OnInitialized(); } diff --git a/src/Artemis.Core/Services/Interfaces/ILayerService.cs b/src/Artemis.Core/Services/Interfaces/ILayerService.cs index 95b098f1e..c59245532 100644 --- a/src/Artemis.Core/Services/Interfaces/ILayerService.cs +++ b/src/Artemis.Core/Services/Interfaces/ILayerService.cs @@ -1,6 +1,4 @@ using Artemis.Core.Models.Profile; -using Artemis.Core.Models.Profile.KeyframeEngines; -using Artemis.Core.Models.Profile.LayerProperties; using Artemis.Core.Plugins.LayerBrush; namespace Artemis.Core.Services.Interfaces @@ -8,27 +6,27 @@ namespace Artemis.Core.Services.Interfaces public interface ILayerService : IArtemisService { /// - /// Instantiates and adds the described by the provided + /// Creates a new layer + /// + /// + /// + /// + /// + Layer CreateLayer(Profile profile, ProfileElement parent, string name); + + /// + /// Instantiates and adds the described by the provided + /// /// to the . /// /// The layer to instantiate the brush for /// - LayerBrush InstantiateLayerBrush(Layer layer); + BaseLayerBrush InstantiateLayerBrush(Layer layer); /// - /// Instantiates and adds a compatible to the provided . - /// If the property already has a compatible keyframe engine, nothing happens. + /// Removes the layer brush from the provided layer and disposes it /// - /// The layer property to apply the keyframe engine to. - /// The resulting keyframe engine, if a compatible engine was found. - KeyframeEngine InstantiateKeyframeEngine(LayerProperty layerProperty); - - /// - /// Instantiates and adds a compatible to the provided . - /// If the property already has a compatible keyframe engine, nothing happens. - /// - /// The layer property to apply the keyframe engine to. - /// The resulting keyframe engine, if a compatible engine was found. - KeyframeEngine InstantiateKeyframeEngine(BaseLayerProperty layerProperty); + /// + void RemoveLayerBrush(Layer layer); } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/LayerService.cs b/src/Artemis.Core/Services/LayerService.cs index e1a4f03ea..b080b8b5a 100644 --- a/src/Artemis.Core/Services/LayerService.cs +++ b/src/Artemis.Core/Services/LayerService.cs @@ -1,8 +1,6 @@ using System.Collections.Generic; using System.Linq; using Artemis.Core.Models.Profile; -using Artemis.Core.Models.Profile.KeyframeEngines; -using Artemis.Core.Models.Profile.LayerProperties; using Artemis.Core.Plugins.LayerBrush; using Artemis.Core.Services.Interfaces; using Ninject; @@ -24,11 +22,25 @@ namespace Artemis.Core.Services _pluginService = pluginService; } - public LayerBrush InstantiateLayerBrush(Layer layer) + public Layer CreateLayer(Profile profile, ProfileElement parent, string name) + { + var layer = new Layer(profile, parent, name); + + // Layers have two hardcoded property groups, instantiate them + layer.General.InitializeProperties(this, layer, "General."); + layer.Transform.InitializeProperties(this, layer, "Transform."); + + // With the properties loaded, the layer brush can be instantiated + InstantiateLayerBrush(layer); + + return layer; + } + + public BaseLayerBrush InstantiateLayerBrush(Layer layer) { RemoveLayerBrush(layer); - var descriptorReference = layer.Properties.BrushReference.CurrentValue; + var descriptorReference = layer.General.BrushReference?.CurrentValue; if (descriptorReference == null) return null; @@ -46,34 +58,10 @@ namespace Artemis.Core.Services new ConstructorArgument("layer", layer), new ConstructorArgument("descriptor", descriptor) }; - var layerBrush = (LayerBrush) _kernel.Get(descriptor.LayerBrushType, arguments); - // Set the layer service after the brush was created to avoid constructor clutter, SetLayerService will play catch-up for us. - // If layer brush implementations need the LayerService they can inject it themselves, but don't require it by default - layerBrush.SetLayerService(this); - layer.LayerBrush = layerBrush; - - return layerBrush; - } - - public KeyframeEngine InstantiateKeyframeEngine(LayerProperty layerProperty) - { - return InstantiateKeyframeEngine((BaseLayerProperty) layerProperty); - } - - public KeyframeEngine InstantiateKeyframeEngine(BaseLayerProperty layerProperty) - { - if (layerProperty.KeyframeEngine != null && layerProperty.KeyframeEngine.CompatibleTypes.Contains(layerProperty.Type)) - return layerProperty.KeyframeEngine; - - // This creates an instance of each keyframe engine, which is pretty cheap since all the expensive stuff is done during - // Initialize() call but it's not ideal - var keyframeEngines = _kernel.Get>(); - var keyframeEngine = keyframeEngines.FirstOrDefault(k => k.CompatibleTypes.Contains(layerProperty.Type)); - if (keyframeEngine == null) - return null; - - keyframeEngine.Initialize(layerProperty); - return keyframeEngine; + layer.LayerBrush = (BaseLayerBrush)_kernel.Get(descriptor.LayerBrushType, arguments); ; + layer.LayerBrush.InitializeProperties(this, "LayerBrush."); + layer.OnLayerBrushUpdated(); + return layer.LayerBrush; } public void RemoveLayerBrush(Layer layer) @@ -83,11 +71,9 @@ namespace Artemis.Core.Services var brush = layer.LayerBrush; layer.LayerBrush = null; - - var propertiesToRemove = layer.Properties.Where(l => l.PluginInfo == brush.Descriptor.LayerBrushProvider.PluginInfo).ToList(); - foreach (var layerProperty in propertiesToRemove) - layer.Properties.RemoveLayerProperty(layerProperty); brush.Dispose(); + + layer.LayerEntity.PropertyEntities.RemoveAll(p => p.PluginGuid == brush.PluginInfo.Guid); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs index 30b2b12bf..9fe885d6b 100644 --- a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs +++ b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs @@ -26,13 +26,13 @@ namespace Artemis.Core.Services.Storage.Interfaces /// /// /// - void UndoUpdateProfile(Profile selectedProfile, ProfileModule module); + bool UndoUpdateProfile(Profile selectedProfile, ProfileModule module); /// /// Attempts to restore the profile to the state it had before the last call. /// /// /// - void RedoUpdateProfile(Profile selectedProfile, ProfileModule module); + bool RedoUpdateProfile(Profile selectedProfile, ProfileModule module); } } \ 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 8434ea02b..733201487 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -93,18 +93,20 @@ namespace Artemis.Core.Services.Storage module.ChangeActiveProfile(profile, _surfaceService.ActiveSurface); if (profile != null) { - InstantiateProfileLayerBrushes(profile); - InstantiateProfileKeyframeEngines(profile); + InitializeLayerProperties(profile); + InstantiateLayerBrushes(profile); } } public void DeleteProfile(Profile profile) { + _logger.Debug("Removing profile " + profile); _profileRepository.Remove(profile.ProfileEntity); } public void UpdateProfile(Profile profile, bool includeChildren) { + _logger.Debug("Updating profile " + profile); var memento = JsonConvert.SerializeObject(profile.ProfileEntity); profile.RedoStack.Clear(); profile.UndoStack.Push(memento); @@ -121,12 +123,12 @@ namespace Artemis.Core.Services.Storage _profileRepository.Save(profile.ProfileEntity); } - public void UndoUpdateProfile(Profile profile, ProfileModule module) + public bool UndoUpdateProfile(Profile profile, ProfileModule module) { if (!profile.UndoStack.Any()) { _logger.Debug("Undo profile update - Failed, undo stack empty"); - return; + return false; } ActivateProfile(module, null); @@ -138,14 +140,15 @@ namespace Artemis.Core.Services.Storage ActivateProfile(module, profile); _logger.Debug("Undo profile update - Success"); + return true; } - public void RedoUpdateProfile(Profile profile, ProfileModule module) + public bool RedoUpdateProfile(Profile profile, ProfileModule module) { if (!profile.RedoStack.Any()) { _logger.Debug("Redo profile update - Failed, redo empty"); - return; + return false; } ActivateProfile(module, null); @@ -157,22 +160,29 @@ namespace Artemis.Core.Services.Storage ActivateProfile(module, profile); _logger.Debug("Redo profile update - Success"); + return true; } - private void InstantiateProfileLayerBrushes(Profile profile) + private void InitializeLayerProperties(Profile profile) + { + foreach (var layer in profile.GetAllLayers().Where(l => l.LayerBrush == null)) + { + if (!layer.General.PropertiesInitialized) + layer.General.InitializeProperties(_layerService, layer, "General."); + if (!layer.Transform.PropertiesInitialized) + layer.Transform.InitializeProperties(_layerService, layer, "Transform."); + } + + ; + } + + private void InstantiateLayerBrushes(Profile profile) { // Only instantiate brushes for layers without an existing brush instance foreach (var layer in profile.GetAllLayers().Where(l => l.LayerBrush == null)) _layerService.InstantiateLayerBrush(layer); } - private void InstantiateProfileKeyframeEngines(Profile profile) - { - // Only instantiate engines for properties without an existing engine instance - foreach (var layerProperty in profile.GetAllLayers().SelectMany(l => l.Properties).Where(p => p.KeyframeEngine == null)) - _layerService.InstantiateKeyframeEngine(layerProperty); - } - private void ActiveProfilesPopulateLeds(ArtemisSurface surface) { var profileModules = _pluginService.GetPluginsOfType(); @@ -184,14 +194,7 @@ namespace Artemis.Core.Services.Storage { var profileModules = _pluginService.GetPluginsOfType(); foreach (var profileModule in profileModules.Where(p => p.ActiveProfile != null).ToList()) - InstantiateProfileLayerBrushes(profileModule.ActiveProfile); - } - - private void ActiveProfilesInstantiateKeyframeEngines() - { - var profileModules = _pluginService.GetPluginsOfType(); - foreach (var profileModule in profileModules.Where(p => p.ActiveProfile != null).ToList()) - InstantiateProfileKeyframeEngines(profileModule.ActiveProfile); + InstantiateLayerBrushes(profileModule.ActiveProfile); } #region Event handlers @@ -212,7 +215,6 @@ namespace Artemis.Core.Services.Storage if (e.PluginInfo.Instance is LayerBrushProvider) { ActiveProfilesInstantiateProfileLayerBrushes(); - ActiveProfilesInstantiateKeyframeEngines(); } else if (e.PluginInfo.Instance is ProfileModule profileModule) { diff --git a/src/Artemis.Storage/Artemis.Storage.csproj b/src/Artemis.Storage/Artemis.Storage.csproj index 5efccadd8..0ae5807f4 100644 --- a/src/Artemis.Storage/Artemis.Storage.csproj +++ b/src/Artemis.Storage/Artemis.Storage.csproj @@ -5,6 +5,7 @@ 7 - + + \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/LayerEntity.cs b/src/Artemis.Storage/Entities/Profile/LayerEntity.cs index 614e575c3..d3ab661bd 100644 --- a/src/Artemis.Storage/Entities/Profile/LayerEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/LayerEntity.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; using LiteDB; namespace Artemis.Storage.Entities.Profile @@ -11,6 +12,7 @@ namespace Artemis.Storage.Entities.Profile Leds = new List(); PropertyEntities = new List(); Condition = new List(); + ExpandedPropertyGroups = new List(); } public Guid Id { get; set; } @@ -22,6 +24,7 @@ namespace Artemis.Storage.Entities.Profile public List Leds { get; set; } public List PropertyEntities { get; set; } public List Condition { get; set; } + public List ExpandedPropertyGroups { get; set; } [BsonRef("ProfileEntity")] public ProfileEntity Profile { get; set; } diff --git a/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs b/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs index 2f450633d..de95b56b8 100644 --- a/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs @@ -10,10 +10,11 @@ namespace Artemis.Storage.Entities.Profile KeyframeEntities = new List(); } - public string Id { get; set; } - public string ValueType { get; set; } + public Guid PluginGuid { get; set; } + public string Path { get; set; } + public string Value { get; set; } - public bool IsUsingKeyframes { get; set; } + public bool KeyframesEnabled { get; set; } public List KeyframeEntities { get; set; } } diff --git a/src/Artemis.Storage/Migrations/AttributeBasedPropertiesMigration.cs b/src/Artemis.Storage/Migrations/AttributeBasedPropertiesMigration.cs new file mode 100644 index 000000000..6fa31eb10 --- /dev/null +++ b/src/Artemis.Storage/Migrations/AttributeBasedPropertiesMigration.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Artemis.Storage.Entities.Profile; +using Artemis.Storage.Migrations.Interfaces; +using LiteDB; + +namespace Artemis.Storage.Migrations +{ + public class AttributeBasedPropertiesMigration : IStorageMigration + { + public int UserVersion => 1; + public void Apply(LiteRepository repository) + { + if (repository.Database.CollectionExists("ProfileEntity")) + repository.Database.DropCollection("ProfileEntity"); + } + } +} diff --git a/src/Artemis.Storage/Migrations/Interfaces/IStorageMigration.cs b/src/Artemis.Storage/Migrations/Interfaces/IStorageMigration.cs new file mode 100644 index 000000000..a72a3c92b --- /dev/null +++ b/src/Artemis.Storage/Migrations/Interfaces/IStorageMigration.cs @@ -0,0 +1,10 @@ +using LiteDB; + +namespace Artemis.Storage.Migrations.Interfaces +{ + public interface IStorageMigration + { + int UserVersion { get; } + void Apply(LiteRepository repository); + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/StorageMigrationService.cs b/src/Artemis.Storage/StorageMigrationService.cs new file mode 100644 index 000000000..35d642e09 --- /dev/null +++ b/src/Artemis.Storage/StorageMigrationService.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using Artemis.Storage.Migrations.Interfaces; +using LiteDB; +using Serilog; + +namespace Artemis.Storage +{ + public class StorageMigrationService + { + private readonly ILogger _logger; + private readonly LiteRepository _repository; + private readonly List _migrations; + + public StorageMigrationService(ILogger logger, LiteRepository repository, List migrations) + { + _logger = logger; + _repository = repository; + _migrations = migrations; + + ApplyPendingMigrations(); + } + + public void ApplyPendingMigrations() + { + foreach (var storageMigration in _migrations.OrderBy(m => m.UserVersion)) + { + if (_repository.Database.UserVersion >= storageMigration.UserVersion) + continue; + + _logger.Information("Applying storage migration {storageMigration} to update DB from v{oldVersion} to v{newVersion}", + storageMigration.GetType().Name, _repository.Database.UserVersion, storageMigration.UserVersion); + storageMigration.Apply(_repository); + _repository.Database.UserVersion = storageMigration.UserVersion; + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj index f25f3f433..4714f8226 100644 --- a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -20,15 +20,15 @@ - - - + + + - - - + + + diff --git a/src/Artemis.UI.Shared/Controls/ColorPicker.xaml b/src/Artemis.UI.Shared/Controls/ColorPicker.xaml index 4d6db87f3..faba740fa 100644 --- a/src/Artemis.UI.Shared/Controls/ColorPicker.xaml +++ b/src/Artemis.UI.Shared/Controls/ColorPicker.xaml @@ -9,10 +9,6 @@ d:DesignHeight="101.848" d:DesignWidth="242.956"> - - - - @@ -62,14 +58,14 @@ Grid.Row="1" OpacityMask="{x:Null}"> - + - + + Template="{DynamicResource MaterialDesignColorSliderThumb}"> @@ -124,7 +120,7 @@ diff --git a/src/Artemis.UI.Shared/Controls/DraggableFloat.xaml b/src/Artemis.UI.Shared/Controls/DraggableFloat.xaml index 1da088f62..352ba72b4 100644 --- a/src/Artemis.UI.Shared/Controls/DraggableFloat.xaml +++ b/src/Artemis.UI.Shared/Controls/DraggableFloat.xaml @@ -4,17 +4,25 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" + xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> + + + - + Height="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}}, Path=ActualHeight}"> + @@ -27,15 +35,14 @@ Foreground="{DynamicResource SecondaryAccentBrush}" MouseDown="InputMouseDown" MouseUp="InputMouseUp" - MouseMove="InputMouseMove" - RequestBringIntoView="Input_OnRequestBringIntoView"/> + MouseMove="InputMouseMove" + RequestBringIntoView="Input_OnRequestBringIntoView" /> - - - - diff --git a/src/Artemis.UI.Shared/Controls/GradientPicker.xaml.cs b/src/Artemis.UI.Shared/Controls/GradientPicker.xaml.cs index 3a3a59c97..a001c5c9d 100644 --- a/src/Artemis.UI.Shared/Controls/GradientPicker.xaml.cs +++ b/src/Artemis.UI.Shared/Controls/GradientPicker.xaml.cs @@ -5,6 +5,7 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Input; using Artemis.Core.Models.Profile; +using Artemis.Core.Models.Profile.Colors; using Artemis.UI.Shared.Annotations; using Artemis.UI.Shared.Services.Interfaces; diff --git a/src/Artemis.UI.Shared/Converters/ColorGradientToGradientStopsConverter.cs b/src/Artemis.UI.Shared/Converters/ColorGradientToGradientStopsConverter.cs index a5c82a4cb..67618268a 100644 --- a/src/Artemis.UI.Shared/Converters/ColorGradientToGradientStopsConverter.cs +++ b/src/Artemis.UI.Shared/Converters/ColorGradientToGradientStopsConverter.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Windows.Data; using System.Windows.Media; using Artemis.Core.Models.Profile; +using Artemis.Core.Models.Profile.Colors; using SkiaSharp; using Stylet; diff --git a/src/Artemis.UI.Shared/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI.Shared/Ninject/Factories/IVMFactory.cs index 026791701..44ba4b651 100644 --- a/src/Artemis.UI.Shared/Ninject/Factories/IVMFactory.cs +++ b/src/Artemis.UI.Shared/Ninject/Factories/IVMFactory.cs @@ -1,4 +1,5 @@ using Artemis.Core.Models.Profile; +using Artemis.Core.Models.Profile.Colors; using Artemis.UI.Shared.Screens.GradientEditor; namespace Artemis.UI.Shared.Ninject.Factories diff --git a/src/Artemis.UI.Shared/Screens/Dialogs/ExceptionDialogView.xaml b/src/Artemis.UI.Shared/Screens/Dialogs/ExceptionDialogView.xaml index 09e40501e..4a4ad281d 100644 --- a/src/Artemis.UI.Shared/Screens/Dialogs/ExceptionDialogView.xaml +++ b/src/Artemis.UI.Shared/Screens/Dialogs/ExceptionDialogView.xaml @@ -10,29 +10,38 @@ mc:Ignorable="d" d:DesignHeight="163.274" d:DesignWidth="254.425" d:DataContext="{d:DesignInstance dialogs:ExceptionDialogViewModel}"> - - - + + - Exception message - - - + + + + + + + - + - - -