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

Merge branch 'properties-refactor'

This commit is contained in:
SpoinkyNL 2020-05-29 17:21:52 +02:00
commit 5978dfcc94
166 changed files with 3442 additions and 2897 deletions

View File

@ -21,10 +21,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Ben.Demystifier" Version="0.1.6" />
<PackageReference Include="Castle.Core" Version="4.4.0" />
<PackageReference Include="Castle.Core" Version="4.4.1" />
<PackageReference Include="FastMember" Version="1.5.0" />
<PackageReference Include="HidSharp" Version="2.1.0" />
<PackageReference Include="LiteDB" Version="5.0.7" />
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.2.0" />
<PackageReference Include="LiteDB" Version="5.0.8" />
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.3.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Ninject" Version="3.3.4" />
<PackageReference Include="Ninject.Extensions.ChildKernel" Version="3.3.0" />
@ -34,8 +35,8 @@
<PackageReference Include="Serilog.Enrichers.Demystify" Version="1.0.0-dev-00019" />
<PackageReference Include="Serilog.Sinks.Debug" Version="1.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="SkiaSharp" Version="1.68.2-preview.29" />
<PackageReference Include="Stylet" Version="1.3.1" />
<PackageReference Include="SkiaSharp" Version="1.68.3" />
<PackageReference Include="Stylet" Version="1.3.2" />
<PackageReference Include="System.Buffers" Version="4.5.0" />
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
<PackageReference Include="System.Reflection.Metadata" Version="1.8.0" />

View File

@ -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; }
}
}

View File

@ -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
}
/// <summary>
/// [PH] Looping through HSV, adds 8 rainbow colors
/// Gets a new ColorGradient with colors looping through the HSV-spectrum
/// </summary>
public void MakeFabulous()
/// <returns></returns>
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
}
}

View File

@ -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
}
}

View File

@ -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<Func<DataModel, bool>> ExpressionTree { get; set; }
}
}

View File

@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
using Artemis.Core.Models.Profile.LayerProperties;
namespace Artemis.Core.Models.Profile.KeyframeEngines
{
/// <inheritdoc />
public class FloatKeyframeEngine : KeyframeEngine
{
public sealed override List<Type> CompatibleTypes { get; } = new List<Type> {typeof(float)};
protected override object GetInterpolatedValue()
{
var currentKeyframe = (Keyframe<float>) CurrentKeyframe;
var nextKeyframe = (Keyframe<float>) NextKeyframe;
var diff = nextKeyframe.Value - currentKeyframe.Value;
return currentKeyframe.Value + diff * KeyframeProgressEased;
}
}
}

View File

@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
using Artemis.Core.Models.Profile.LayerProperties;
namespace Artemis.Core.Models.Profile.KeyframeEngines
{
/// <inheritdoc />
public class IntKeyframeEngine : KeyframeEngine
{
public sealed override List<Type> CompatibleTypes { get; } = new List<Type> {typeof(int)};
protected override object GetInterpolatedValue()
{
var currentKeyframe = (Keyframe<int>) CurrentKeyframe;
var nextKeyframe = (Keyframe<int>) NextKeyframe;
var diff = nextKeyframe.Value - currentKeyframe.Value;
return (int) Math.Round(currentKeyframe.Value + diff * KeyframeProgressEased, MidpointRounding.AwayFromZero);
}
}
}

View File

@ -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
{
/// <summary>
/// Indicates whether <see cref="Initialize" /> has been called.
/// </summary>
public bool Initialized { get; private set; }
/// <summary>
/// The layer property this keyframe engine applies to.
/// </summary>
public BaseLayerProperty LayerProperty { get; private set; }
/// <summary>
/// The total progress
/// </summary>
public TimeSpan Progress { get; private set; }
/// <summary>
/// The progress from the current keyframe to the next.
/// <para>Range 0.0 to 1.0.</para>
/// </summary>
public float KeyframeProgress { get; private set; }
/// <summary>
/// The progress from the current keyframe to the next with the current keyframes easing function applied.
/// <para>Range 0.0 to 1.0 but can be higher than 1.0 depending on easing function.</para>
/// </summary>
public float KeyframeProgressEased { get; set; }
/// <summary>
/// The current keyframe
/// </summary>
public BaseKeyframe CurrentKeyframe { get; private set; }
/// <summary>
/// The next keyframe
/// </summary>
public BaseKeyframe NextKeyframe { get; private set; }
/// <summary>
/// The types this keyframe engine supports.
/// </summary>
public abstract List<Type> CompatibleTypes { get; }
/// <summary>
/// Associates the keyframe engine with the provided layer property.
/// </summary>
/// <param name="layerProperty"></param>
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;
}
/// <summary>
/// Updates the engine's progress
/// </summary>
/// <param name="deltaTime"></param>
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
}
/// <summary>
/// Overrides the engine's progress to the provided value
/// </summary>
/// <param name="progress"></param>
public void OverrideProgress(TimeSpan progress)
{
Progress = TimeSpan.Zero;
Update(progress.TotalSeconds);
}
/// <summary>
/// Gets the current value, if the progress is in between two keyframes the value will be interpolated
/// </summary>
/// <returns></returns>
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();
}
}

View File

@ -1,36 +0,0 @@
using System;
using System.Collections.Generic;
using Artemis.Core.Models.Profile.LayerProperties;
using SkiaSharp;
namespace Artemis.Core.Models.Profile.KeyframeEngines
{
/// <inheritdoc />
public class SKColorKeyframeEngine : KeyframeEngine
{
public sealed override List<Type> CompatibleTypes { get; } = new List<Type> {typeof(SKColor)};
protected override object GetInterpolatedValue()
{
var currentKeyframe = (Keyframe<SKColor>) CurrentKeyframe;
var nextKeyframe = (Keyframe<SKColor>) 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));
}
}
}

View File

@ -1,23 +0,0 @@
using System;
using System.Collections.Generic;
using Artemis.Core.Models.Profile.LayerProperties;
using SkiaSharp;
namespace Artemis.Core.Models.Profile.KeyframeEngines
{
/// <inheritdoc />
public class SKPointKeyframeEngine : KeyframeEngine
{
public sealed override List<Type> CompatibleTypes { get; } = new List<Type> {typeof(SKPoint)};
protected override object GetInterpolatedValue()
{
var currentKeyframe = (Keyframe<SKPoint>) CurrentKeyframe;
var nextKeyframe = (Keyframe<SKPoint>) 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);
}
}
}

View File

@ -1,23 +0,0 @@
using System;
using System.Collections.Generic;
using Artemis.Core.Models.Profile.LayerProperties;
using SkiaSharp;
namespace Artemis.Core.Models.Profile.KeyframeEngines
{
/// <inheritdoc />
public class SKSizeKeyframeEngine : KeyframeEngine
{
public sealed override List<Type> CompatibleTypes { get; } = new List<Type> {typeof(SKSize)};
protected override object GetInterpolatedValue()
{
var currentKeyframe = (Keyframe<SKSize>) CurrentKeyframe;
var nextKeyframe = (Keyframe<SKSize>) 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);
}
}
}

View File

@ -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
{
/// <summary>
/// Represents a layer on a profile. To create new layers use the <see cref="LayerService" /> by injecting
/// <see cref="ILayerService" /> into your code
/// </summary>
public sealed class Layer : ProfileElement
{
private readonly List<string> _expandedPropertyGroups;
private LayerShape _layerShape;
private List<ArtemisLed> _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<ArtemisLed>();
_expandedPropertyGroups = new List<string>();
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<ArtemisLed>();
_expandedPropertyGroups = new List<string>();
_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
}
}
/// <summary>
/// The properties of this layer
/// </summary>
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; }
/// <summary>
/// The brush that will fill the <see cref="LayerShape" />.
/// </summary>
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
/// <inheritdoc />
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<BaseLayerProperty>(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);
}
/// <inheritdoc />
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
}

View File

@ -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<LayerShapeType> ShapeType { get; set; }
[PropertyDescription(Name = "Fill type", Description = "How to make the shape adjust to scale changes")]
public EnumLayerProperty<LayerFillType> FillType { get; set; }
[PropertyDescription(Name = "Blend mode", Description = "How to blend this layer into the resulting image")]
public EnumLayerProperty<SKBlendMode> 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()
{
}
}
}

View File

@ -0,0 +1,42 @@
using System;
namespace Artemis.Core.Models.Profile.LayerProperties.Attributes
{
public class PropertyDescriptionAttribute : Attribute
{
/// <summary>
/// The user-friendly name for this property, shown in the UI
/// </summary>
public string Name { get; set; }
/// <summary>
/// The user-friendly description for this property, shown in the UI
/// </summary>
public string Description { get; set; }
/// <summary>
/// Input prefix to show before input elements in the UI
/// </summary>
public string InputPrefix { get; set; }
/// <summary>
/// Input affix to show behind input elements in the UI
/// </summary>
public string InputAffix { get; set; }
/// <summary>
/// The input drag step size, used in the UI
/// </summary>
public float InputStepSize { get; set; }
/// <summary>
/// Minimum input value, only enforced in the UI
/// </summary>
public object MinInputValue { get; set; }
/// <summary>
/// Maximum input value, only enforced in the UI
/// </summary>
public object MaxInputValue { get; set; }
}
}

View File

@ -0,0 +1,22 @@
using System;
namespace Artemis.Core.Models.Profile.LayerProperties.Attributes
{
public class PropertyGroupDescriptionAttribute : Attribute
{
/// <summary>
/// The user-friendly name for this property, shown in the UI.
/// </summary>
public string Name { get; set; }
/// <summary>
/// The user-friendly description for this property, shown in the UI.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Whether to expand this property by default, this is useful for important parent properties.
/// </summary>
public bool ExpandByDefault { get; set; }
}
}

View File

@ -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; }
}
}

View File

@ -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
/// <summary>
/// For internal use only, to implement your own layer property type, extend <see cref="LayerProperty{T}" /> instead.
/// </summary>
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<BaseLayerProperty>();
BaseKeyframes = new List<BaseKeyframe>();
parent?.Children.Add(this);
}
/// <summary>
/// Gets the layer this property applies to
/// The layer this property applies to
/// </summary>
public Layer Layer { get; }
public Layer Layer { get; internal set; }
/// <summary>
/// Info of the plugin associated with this property
/// The parent group of this layer property, set after construction
/// </summary>
public PluginInfo PluginInfo { get; }
public LayerPropertyGroup Parent { get; internal set; }
/// <summary>
/// Gets the parent property of this property.
/// Gets whether keyframes are supported on this property
/// </summary>
public BaseLayerProperty Parent { get; }
public bool KeyframesSupported { get; protected set; } = true;
/// <summary>
/// Gets or sets the child properties of this property.
/// <remarks>If the layer has children it cannot contain a value or keyframes.</remarks>
/// Gets or sets whether keyframes are enabled on this property, has no effect if <see cref="KeyframesSupported" /> is
/// False
/// </summary>
public List<BaseLayerProperty> Children { get; set; }
public bool KeyframesEnabled
{
get => _keyframesEnabled;
set
{
if (_keyframesEnabled == value) return;
_keyframesEnabled = value;
OnKeyframesToggled();
}
}
/// <summary>
/// Gets or sets a unique identifier for this property, a layer may not contain two properties with the same ID.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Gets or sets the user-friendly name for this property, shown in the UI.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the user-friendly description for this property, shown in the UI.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Gets or sets whether to expand this property by default, this is useful for important parent properties.
/// </summary>
public bool ExpandByDefault { get; set; }
/// <summary>
/// Gets or sets the an optional input prefix to show before input elements in the UI.
/// </summary>
public string InputPrefix { get; set; }
/// <summary>
/// Gets or sets an optional input affix to show behind input elements in the UI.
/// </summary>
public string InputAffix { get; set; }
/// <summary>
/// Gets or sets an optional maximum input value, only enforced in the UI.
/// </summary>
public object MaxInputValue { get; set; }
/// <summary>
/// Gets or sets the input drag step size, used in the UI.
/// </summary>
public float InputStepSize { get; set; }
/// <summary>
/// Gets or sets an optional minimum input value, only enforced in the UI.
/// </summary>
public object MinInputValue { get; set; }
/// <summary>
/// Gets or sets whether this property can use keyframes, True by default.
/// </summary>
public bool CanUseKeyframes { get; set; }
/// <summary>
/// Gets or sets whether this property is using keyframes.
/// </summary>
public bool IsUsingKeyframes { get; set; }
/// <summary>
/// Gets the type of value this layer property contains.
/// </summary>
public Type Type { get; protected set; }
/// <summary>
/// Gets or sets whether this property is hidden in the UI.
/// Gets or sets whether the property is hidden in the UI
/// </summary>
public bool IsHidden
{
@ -134,241 +60,109 @@ namespace Artemis.Core.Models.Profile.LayerProperties
}
/// <summary>
/// Gets a list of keyframes defining different values of the property in time, this list contains the untyped
/// <see cref="BaseKeyframe" />.
/// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied
/// </summary>
public IReadOnlyCollection<BaseKeyframe> UntypedKeyframes => BaseKeyframes.AsReadOnly();
public bool IsLoadedFromStorage { get; internal set; }
/// <summary>
/// Gets the keyframe engine instance of this property
/// Gets the total progress on the timeline
/// </summary>
public KeyframeEngine KeyframeEngine { get; internal set; }
protected List<BaseKeyframe> 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; }
/// <summary>
/// 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
/// </summary>
/// <returns></returns>
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; }
/// <summary>
/// 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
/// </summary>
public void ClearKeyframes()
{
if (KeyframeEngine != null)
BaseValue = KeyframeEngine.GetCurrentValue();
public abstract IReadOnlyList<BaseLayerPropertyKeyframe> BaseKeyframes { get; }
internal PropertyEntity PropertyEntity { get; set; }
internal LayerPropertyGroup LayerPropertyGroup { get; set; }
BaseKeyframes.Clear();
}
/// <summary>
/// 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
/// </summary>
public object GetCurrentValue()
{
if (KeyframeEngine == null || !UntypedKeyframes.Any())
return BaseValue;
return KeyframeEngine.GetCurrentValue();
}
/// <param name="entity"></param>
/// <param name="layerPropertyGroup"></param>
/// <param name="fromStorage"></param>
internal abstract void ApplyToLayerProperty(PropertyEntity entity, LayerPropertyGroup layerPropertyGroup, bool fromStorage);
/// <summary>
/// Gets the current value using the regular value or keyframes.
/// Saves the property to the underlying property entity that was configured when calling
/// <see cref="ApplyToLayerProperty" />
/// </summary>
/// <param name="value">The value to set.</param>
/// <param name="time">
/// 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.
/// </param>
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();
}
/// <summary>
/// Adds a keyframe to the property.
/// </summary>
/// <param name="keyframe">The keyframe to remove</param>
public void AddKeyframe(BaseKeyframe keyframe)
{
BaseKeyframes.Add(keyframe);
SortKeyframes();
}
/// <summary>
/// Removes a keyframe from the property.
/// </summary>
/// <param name="keyframe">The keyframe to remove</param>
public void RemoveKeyframe(BaseKeyframe keyframe)
{
BaseKeyframes.Remove(keyframe);
SortKeyframes();
}
/// <summary>
/// Returns the flattened index of this property on the layer
/// </summary>
/// <returns></returns>
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<BaseLayerProperty>();
// 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<BaseLayerProperty> GetAllChildren()
{
var children = new List<BaseLayerProperty>();
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
/// <summary>
/// Occurs when this property's value was changed outside regular keyframe updates
/// Occurs once every frame when the layer property is updated
/// </summary>
public event EventHandler<EventArgs> ValueChanged;
public event EventHandler Updated;
/// <summary>
/// Occurs when this property or any of it's ancestors visibility is changed
/// Occurs when the base value of the layer property was updated
/// </summary>
public event EventHandler<EventArgs> VisibilityChanged;
public event EventHandler BaseValueChanged;
protected virtual void OnValueChanged()
/// <summary>
/// Occurs when the <see cref="IsHidden"/> value of the layer property was updated
/// </summary>
public event EventHandler VisibilityChanged;
/// <summary>
/// Occurs when keyframes are enabled/disabled
/// </summary>
public event EventHandler KeyframesToggled;
/// <summary>
/// Occurs when a new keyframe was added to the layer property
/// </summary>
public event EventHandler KeyframeAdded;
/// <summary>
/// Occurs when a keyframe was removed from the layer property
/// </summary>
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();
}
}

View File

@ -0,0 +1,31 @@
using System;
using Artemis.Core.Utilities;
namespace Artemis.Core.Models.Profile.LayerProperties
{
/// <summary>
/// For internal use only, use <see cref="LayerPropertyKeyframe{T}" /> instead.
/// </summary>
public abstract class BaseLayerPropertyKeyframe
{
internal BaseLayerPropertyKeyframe(BaseLayerProperty baseLayerProperty)
{
BaseLayerProperty = baseLayerProperty;
}
/// <summary>
/// The base class of the layer property this keyframe is applied to
/// </summary>
public BaseLayerProperty BaseLayerProperty { get; internal set; }
/// <summary>
/// The position of this keyframe in the timeline
/// </summary>
public abstract TimeSpan Position { get; set; }
/// <summary>
/// The easing function applied on the value of the keyframe
/// </summary>
public Easings.Functions EasingFunction { get; set; }
}
}

View File

@ -1,18 +0,0 @@
namespace Artemis.Core.Models.Profile.LayerProperties
{
/// <inheritdoc />
public class Keyframe<T> : BaseKeyframe
{
public Keyframe(Layer layer, LayerProperty<T> propertyBase) : base(layer, propertyBase)
{
}
public LayerProperty<T> Property => (LayerProperty<T>) BaseProperty;
public T Value
{
get => BaseValue != null ? (T) BaseValue : default;
set => BaseValue = value;
}
}
}

View File

@ -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
{
/// <summary>
/// Represents a property on the layer. This property is visible in the profile editor and can be key-framed (unless
/// opted out).
/// <para>To create and register a new LayerProperty use <see cref="LayerBrush.RegisterLayerProperty" /></para>
/// Represents a property on a layer. Properties are saved in storage and can optionally be modified from the UI.
/// <para>
/// Note: You cannot initialize layer properties yourself. If properly placed and annotated, the Artemis core will
/// initialize
/// these for you.
/// </para>
/// </summary>
/// <typeparam name="T"></typeparam>
public class LayerProperty<T> : BaseLayerProperty
/// <typeparam name="T">The type of property encapsulated in this layer property</typeparam>
public abstract class LayerProperty<T> : 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<LayerPropertyKeyframe<T>> _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<LayerPropertyKeyframe<T>>();
}
/// <summary>
/// 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
/// </summary>
public T Value
public T BaseValue
{
get => BaseValue != null ? (T) BaseValue : default;
set => BaseValue = value;
}
/// <summary>
/// Gets the value of the property with keyframes applied
/// </summary>
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();
}
}
}
/// <summary>
/// Gets a list of keyframes defining different values of the property in time, this list contains the strongly typed
/// <see cref="Keyframe{T}" />
/// Gets the current value of this property as it is affected by it's keyframes, updated once every frame
/// </summary>
public ReadOnlyCollection<Keyframe<T>> Keyframes => BaseKeyframes.Cast<Keyframe<T>>().ToList().AsReadOnly();
/// <summary>
/// Adds a keyframe to the property.
/// </summary>
/// <param name="keyframe">The keyframe to remove</param>
public void AddKeyframe(Keyframe<T> keyframe)
public T CurrentValue
{
base.AddKeyframe(keyframe);
get => !KeyframesEnabled || !KeyframesSupported ? BaseValue : _currentValue;
internal set => _currentValue = value;
}
/// <summary>
/// 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
/// </summary>
public T DefaultValue { get; set; }
/// <summary>
/// Gets a read-only list of all the keyframes on this layer property
/// </summary>
public IReadOnlyList<LayerPropertyKeyframe<T>> Keyframes => _keyframes.AsReadOnly();
/// <summary>
/// Gets the current keyframe in the timeline according to the current progress
/// </summary>
public LayerPropertyKeyframe<T> CurrentKeyframe { get; protected set; }
/// <summary>
/// Gets the next keyframe in the timeline according to the current progress
/// </summary>
public LayerPropertyKeyframe<T> NextKeyframe { get; protected set; }
public override IReadOnlyList<BaseLayerPropertyKeyframe> BaseKeyframes => _keyframes.Cast<BaseLayerPropertyKeyframe>().ToList().AsReadOnly();
/// <summary>
/// Sets the current value, using either keyframes if enabled or the base value.
/// </summary>
/// <param name="value">The value to set.</param>
/// <param name="time">
/// 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.
/// </param>
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<T>(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);
}
}
/// <summary>
/// Adds a keyframe to the layer property
/// </summary>
/// <param name="keyframe">The keyframe to add</param>
public void AddKeyframe(LayerPropertyKeyframe<T> keyframe)
{
if (_keyframes.Contains(keyframe))
return;
keyframe.LayerProperty?.RemoveKeyframe(keyframe);
keyframe.LayerProperty = this;
keyframe.BaseLayerProperty = this;
_keyframes.Add(keyframe);
SortKeyframes();
OnKeyframeAdded();
}
/// <summary>
/// Removes a keyframe from the layer property
/// </summary>
/// <param name="keyframe">The keyframe to remove</param>
public void RemoveKeyframe(Keyframe<T> keyframe)
public LayerPropertyKeyframe<T> CopyKeyframe(LayerPropertyKeyframe<T> keyframe)
{
base.RemoveKeyframe(keyframe);
var newKeyframe = new LayerPropertyKeyframe<T>(
keyframe.Value,
keyframe.Position,
keyframe.EasingFunction,
keyframe.LayerProperty
);
AddKeyframe(newKeyframe);
return newKeyframe;
}
/// <summary>
/// Removes a keyframe from the layer property
/// </summary>
/// <param name="keyframe">The keyframe to remove</param>
public void RemoveKeyframe(LayerPropertyKeyframe<T> keyframe)
{
if (!_keyframes.Contains(keyframe))
return;
_keyframes.Remove(keyframe);
keyframe.LayerProperty = null;
keyframe.BaseLayerProperty = null;
SortKeyframes();
OnKeyframeRemoved();
}
/// <summary>
/// Removes all keyframes from the layer property
/// </summary>
public void ClearKeyframes()
{
var keyframes = new List<LayerPropertyKeyframe<T>>(_keyframes);
foreach (var layerPropertyKeyframe in keyframes)
RemoveKeyframe(layerPropertyKeyframe);
}
/// <summary>
/// Called every update (if keyframes are both supported and enabled) to determine the new <see cref="CurrentValue" />
/// based on the provided progress
/// </summary>
/// <param name="keyframeProgress">The linear current keyframe progress</param>
/// <param name="keyframeProgressEased">The current keyframe progress, eased with the current easing function</param>
protected abstract void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased);
/// <summary>
/// Updates the property, moving the timeline forwards by the provided <paramref name="deltaTime" />
/// </summary>
/// <param name="deltaTime">The amount of time to move the timeline forwards</param>
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();
}
/// <summary>
/// Overrides the timeline progress to match the provided <paramref name="overrideTime" />
/// </summary>
/// <param name="overrideTime">The new progress to set the layer property timeline to.</param>
internal void OverrideProgress(TimeSpan overrideTime)
{
TimelineProgress = TimeSpan.Zero;
Update(overrideTime.TotalSeconds);
}
/// <summary>
/// Sorts the keyframes in ascending order by position
/// </summary>
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<T>(entity.Value);
IsLoadedFromStorage = fromStorage;
CurrentValue = BaseValue;
KeyframesEnabled = entity.KeyframesEnabled;
_keyframes.Clear();
_keyframes.AddRange(entity.KeyframeEntities.Select(k => new LayerPropertyKeyframe<T>(
JsonConvert.DeserializeObject<T>(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;
}
}
}

View File

@ -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
{
/// <summary>
/// Contains all the properties of the layer and provides easy access to the default properties.
/// </summary>
public class LayerPropertyCollection : IEnumerable<BaseLayerProperty>
{
private readonly Dictionary<(Guid, string), BaseLayerProperty> _properties;
internal LayerPropertyCollection(Layer layer)
{
_properties = new Dictionary<(Guid, string), BaseLayerProperty>();
Layer = layer;
CreateDefaultProperties();
}
/// <summary>
/// Gets the layer these properties are applied on
/// </summary>
public Layer Layer { get; }
/// <inheritdoc />
public IEnumerator<BaseLayerProperty> GetEnumerator()
{
return _properties.Values.GetEnumerator();
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <summary>
/// If found, returns the <see cref="LayerProperty{T}" /> matching the provided ID
/// </summary>
/// <typeparam name="T">The type of the layer property</typeparam>
/// <param name="pluginInfo">The plugin this property belongs to</param>
/// <param name="id"></param>
/// <returns></returns>
public LayerProperty<T> GetLayerPropertyById<T>(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<T>)_properties[(pluginInfo.Guid, id)];
}
/// <summary>
/// Removes the provided layer property from the layer.
/// </summary>
/// <typeparam name="T">The type of value of the layer property</typeparam>
/// <param name="layerProperty">The property to remove from the layer</param>
internal void RemoveLayerProperty<T>(LayerProperty<T> layerProperty)
{
RemoveLayerProperty((BaseLayerProperty) layerProperty);
}
/// <summary>
/// Removes the provided layer property from the layer.
/// </summary>
/// <param name="layerProperty">The property to remove from the layer</param>
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));
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">The type of value of the layer property</typeparam>
/// <param name="layerProperty">The property to apply to the layer</param>
/// <returns>True if an existing value was found and applied, otherwise false.</returns>
internal bool RegisterLayerProperty<T>(LayerProperty<T> layerProperty)
{
return RegisterLayerProperty((BaseLayerProperty) layerProperty);
}
/// <summary>
/// Adds the provided layer property to the layer.
/// If found, the last stored base value and keyframes will be applied to the provided property.
/// </summary>
/// <param name="layerProperty">The property to apply to the layer</param>
/// <returns>True if an existing value was found and applied, otherwise false.</returns>
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
/// <summary>
/// Gets the shape type property of the layer
/// </summary>
public LayerProperty<LayerShapeType> ShapeType { get; private set; }
/// <summary>
/// Gets the fill type property of the layer
/// </summary>
public LayerProperty<LayerFillType> FillType { get; private set; }
/// <summary>
/// Gets the blend mode property of the layer
/// </summary>
public LayerProperty<SKBlendMode> BlendMode { get; private set; }
/// <summary>
/// Gets the brush reference property of the layer
/// </summary>
public LayerProperty<LayerBrushReference> BrushReference { get; private set; }
/// <summary>
/// Gets the anchor point property of the layer
/// </summary>
public LayerProperty<SKPoint> AnchorPoint { get; private set; }
/// <summary>
/// Gets the position of the layer
/// </summary>
public LayerProperty<SKPoint> Position { get; private set; }
/// <summary>
/// Gets the size property of the layer
/// </summary>
public LayerProperty<SKSize> Scale { get; private set; }
/// <summary>
/// Gets the rotation property of the layer range 0 - 360
/// </summary>
public LayerProperty<float> Rotation { get; private set; }
/// <summary>
/// Gets the opacity property of the layer range 0 - 100
/// </summary>
public LayerProperty<float> Opacity { get; private set; }
private void CreateDefaultProperties()
{
// Shape
var shape = new LayerProperty<object>(Layer, "Core.Shape", "Shape", "A collection of basic shape properties");
ShapeType = new LayerProperty<LayerShapeType>(Layer, shape, "Core.ShapeType", "Shape type", "The type of shape to draw in this layer") {CanUseKeyframes = false};
FillType = new LayerProperty<LayerFillType>(Layer, shape, "Core.FillType", "Fill type", "How to make the shape adjust to scale changes") {CanUseKeyframes = false};
BlendMode = new LayerProperty<SKBlendMode>(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<object>(Layer, "Core.Brush", "Brush", "A collection of properties that configure the selected brush");
BrushReference = new LayerProperty<LayerBrushReference>(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<object>(Layer, "Core.Transform", "Transform", "A collection of transformation properties") {ExpandByDefault = true};
AnchorPoint = new LayerProperty<SKPoint>(Layer, transform, "Core.AnchorPoint", "Anchor Point", "The point at which the shape is attached to its position") {InputStepSize = 0.001f};
Position = new LayerProperty<SKPoint>(Layer, transform, "Core.Position", "Position", "The position of the shape") {InputStepSize = 0.001f};
Scale = new LayerProperty<SKSize>(Layer, transform, "Core.Scale", "Scale", "The scale of the shape") {InputAffix = "%", MinInputValue = 0f};
Rotation = new LayerProperty<float>(Layer, transform, "Core.Rotation", "Rotation", "The rotation of the shape in degrees") {InputAffix = "°"};
Opacity = new LayerProperty<float>(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<LayerPropertyEventArgs> LayerPropertyRegistered;
public event EventHandler<LayerPropertyEventArgs> LayerPropertyRemoved;
private void OnLayerPropertyRegistered(LayerPropertyEventArgs e)
{
LayerPropertyRegistered?.Invoke(this, e);
}
private void OnLayerPropertyRemoved(LayerPropertyEventArgs e)
{
LayerPropertyRemoved?.Invoke(this, e);
}
#endregion
}
}

View File

@ -0,0 +1,39 @@
using System;
using Artemis.Core.Utilities;
namespace Artemis.Core.Models.Profile.LayerProperties
{
public class LayerPropertyKeyframe<T> : BaseLayerPropertyKeyframe
{
private TimeSpan _position;
public LayerPropertyKeyframe(T value, TimeSpan position, Easings.Functions easingFunction, LayerProperty<T> layerProperty) : base(layerProperty)
{
_position = position;
Value = value;
LayerProperty = layerProperty;
EasingFunction = easingFunction;
}
/// <summary>
/// The layer property this keyframe is applied to
/// </summary>
public LayerProperty<T> LayerProperty { get; internal set; }
/// <summary>
/// The value of this keyframe
/// </summary>
public T Value { get; set; }
/// <inheritdoc />
public override TimeSpan Position
{
get => _position;
set
{
_position = value;
LayerProperty.SortKeyframes();
}
}
}
}

View File

@ -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
{
/// <inheritdoc />
public class ColorGradientLayerProperty : LayerProperty<ColorGradient>
{
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();
}
}
}

View File

@ -0,0 +1,19 @@
using System;
using Artemis.Core.Exceptions;
namespace Artemis.Core.Models.Profile.LayerProperties.Types
{
/// <inheritdoc />
public class EnumLayerProperty<T> : LayerProperty<T> where T : Enum
{
public EnumLayerProperty()
{
KeyframesSupported = false;
}
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
throw new ArtemisCoreException("Enum properties do not support keyframes.");
}
}
}

View File

@ -0,0 +1,16 @@
namespace Artemis.Core.Models.Profile.LayerProperties.Types
{
/// <inheritdoc/>
public class FloatLayerProperty : LayerProperty<float>
{
internal FloatLayerProperty()
{
}
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
var diff = NextKeyframe.Value - CurrentKeyframe.Value;
CurrentValue = CurrentKeyframe.Value + diff * keyframeProgressEased;
}
}
}

View File

@ -0,0 +1,18 @@
using System;
namespace Artemis.Core.Models.Profile.LayerProperties.Types
{
/// <inheritdoc/>
public class IntLayerProperty : LayerProperty<int>
{
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);
}
}
}

View File

@ -0,0 +1,20 @@
using Artemis.Core.Exceptions;
namespace Artemis.Core.Models.Profile.LayerProperties.Types
{
/// <summary>
/// A special layer property used to configure the selected layer brush
/// </summary>
public class LayerBrushReferenceLayerProperty : LayerProperty<LayerBrushReference>
{
internal LayerBrushReferenceLayerProperty()
{
KeyframesSupported = false;
}
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
throw new ArtemisCoreException("Layer brush references do not support keyframes.");
}
}
}

View File

@ -0,0 +1,33 @@
using System;
using SkiaSharp;
namespace Artemis.Core.Models.Profile.LayerProperties.Types
{
/// <inheritdoc/>
public class SKColorLayerProperty : LayerProperty<SKColor>
{
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));
}
}
}

View File

@ -0,0 +1,19 @@
using SkiaSharp;
namespace Artemis.Core.Models.Profile.LayerProperties.Types
{
/// <inheritdoc/>
public class SKPointLayerProperty : LayerProperty<SKPoint>
{
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);
}
}
}

View File

@ -0,0 +1,19 @@
using SkiaSharp;
namespace Artemis.Core.Models.Profile.LayerProperties.Types
{
/// <inheritdoc/>
public class SKSizeLayerProperty : LayerProperty<SKSize>
{
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);
}
}
}

View File

@ -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<BaseLayerProperty> _layerProperties;
private readonly List<LayerPropertyGroup> _layerPropertyGroups;
private ReadOnlyCollection<BaseLayerProperty> _allLayerProperties;
private bool _isHidden;
protected LayerPropertyGroup()
{
_layerProperties = new List<BaseLayerProperty>();
_layerPropertyGroups = new List<LayerPropertyGroup>();
}
/// <summary>
/// The layer this property group applies to
/// </summary>
public Layer Layer { get; internal set; }
/// <summary>
/// The path of this property group
/// </summary>
public string Path { get; internal set; }
/// <summary>
/// The parent group of this layer property group, set after construction
/// </summary>
public LayerPropertyGroup Parent { get; internal set; }
/// <summary>
/// Gets whether this property group's properties are all initialized
/// </summary>
public bool PropertiesInitialized { get; private set; }
/// <summary>
/// Used to declare that this property group doesn't belong to a plugin and should use the core plugin GUID
/// </summary>
public bool IsCorePropertyGroup { get; internal set; }
/// <summary>
/// Gets or sets whether the property is hidden in the UI
/// </summary>
public bool IsHidden
{
get => _isHidden;
set
{
_isHidden = value;
OnVisibilityChanged();
}
}
/// <summary>
/// A list of all layer properties in this group
/// </summary>
public ReadOnlyCollection<BaseLayerProperty> LayerProperties => _layerProperties.AsReadOnly();
/// <summary>
/// A list of al child groups in this group
/// </summary>
public ReadOnlyCollection<LayerPropertyGroup> LayerPropertyGroups => _layerPropertyGroups.AsReadOnly();
/// <summary>
/// Recursively gets all layer properties on this group and any subgroups
/// </summary>
/// <returns></returns>
public IReadOnlyCollection<BaseLayerProperty> GetAllLayerProperties()
{
if (!PropertiesInitialized)
return new List<BaseLayerProperty>();
if (_allLayerProperties != null)
return _allLayerProperties;
var result = new List<BaseLayerProperty>(LayerProperties);
foreach (var layerPropertyGroup in LayerPropertyGroups)
result.AddRange(layerPropertyGroup.GetAllLayerProperties());
_allLayerProperties = result.AsReadOnly();
return _allLayerProperties;
}
/// <summary>
/// Called before properties are fully initialized to allow you to populate
/// <see cref="LayerProperty{T}.DefaultValue" /> on the properties you want
/// </summary>
protected abstract void PopulateDefaults();
/// <summary>
/// Called when all layer properties in this property group have been initialized, you may access all properties on the
/// group here
/// </summary>
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<PropertyGroupUpdatingEventArgs> PropertyGroupUpdating;
internal event EventHandler<PropertyGroupUpdatingEventArgs> PropertyGroupOverriding;
public event EventHandler PropertyGroupInitialized;
/// <summary>
/// Occurs when the <see cref="IsHidden" /> value of the layer property was updated
/// </summary>
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
}
}

View File

@ -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()
{
}
}
}

View File

@ -131,5 +131,10 @@ namespace Artemis.Core.Models.Profile
/// Applies the profile element's properties to the underlying storage entity
/// </summary>
internal abstract void ApplyToEntity();
public override string ToString()
{
return $"{nameof(EntityId)}: {EntityId}, {nameof(Order)}: {Order}, {nameof(Name)}: {Name}";
}
}
}

View File

@ -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<StorageMigrationService>().ToSelf().InSingletonScope();
// Bind all migrations as singletons
Kernel.Bind(x =>
{
x.FromAssemblyContaining<IStorageMigration>()
.SelectAllClasses()
.InheritedFrom<IStorageMigration>()
.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<KeyframeEngine>()
.SelectAllClasses()
.InheritedFrom<KeyframeEngine>()
.BindAllBaseClasses();
});
Kernel.Bind<PluginSettings>().ToProvider<PluginSettingsProvider>();
Kernel.Bind<ILogger>().ToProvider<LoggerProvider>();
}

View File

@ -0,0 +1,43 @@
using System;
namespace Artemis.Core.Plugins.Abstract.DataModels.Attributes
{
[AttributeUsage(System.AttributeTargets.Property)]
public class DataModelPropertyAttribute : Attribute
{
/// <summary>
/// Gets or sets the user-friendly name for this property, shown in the UI.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the user-friendly description for this property, shown in the UI.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Gets or sets the an optional input prefix to show before input elements in the UI.
/// </summary>
public string InputPrefix { get; set; }
/// <summary>
/// Gets or sets an optional input affix to show behind input elements in the UI.
/// </summary>
public string InputAffix { get; set; }
/// <summary>
/// Gets or sets an optional maximum input value, only enforced in the UI.
/// </summary>
public object MaxInputValue { get; set; }
/// <summary>
/// Gets or sets the input drag step size, used in the UI.
/// </summary>
public float InputStepSize { get; set; }
/// <summary>
/// Gets or sets an optional minimum input value, only enforced in the UI.
/// </summary>
public object MinInputValue { get; set; }
}
}

View File

@ -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<Type> SupportedTypes = new List<Type>
{
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; }
/// <summary>
/// Recursively validates the current datamodel, ensuring all properties annotated with
/// <see cref="DataModelPropertyAttribute" /> are of supported types.
/// </summary>
/// <returns></returns>
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;
}
}
}

View File

@ -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
/// </summary>
public string DisplayIcon { get; set; }
/// <summary>
/// The optional datamodel driving this module
/// </summary>
public DataModel DataModel { get; set; }
/// <summary>
/// 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

View File

@ -1,12 +0,0 @@
namespace Artemis.Core.Plugins.Abstract
{
public abstract class ModuleDataModel
{
protected ModuleDataModel(Module module)
{
Module = module;
}
public Module Module { get; }
}
}

View File

@ -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
{
/// <summary>
/// A basic layer brush that does not implement any layer property, to use properties with persistent storage,
/// implement <see cref="LayerBrush{T}" /> instead
/// </summary>
public abstract class BaseLayerBrush : IDisposable
{
/// <summary>
/// Gets the layer this brush is applied to
/// </summary>
public Layer Layer { get; internal set; }
/// <summary>
/// Gets the descriptor of this brush
/// </summary>
public LayerBrushDescriptor Descriptor { get; internal set; }
/// <summary>
/// Gets the plugin info that defined this brush
/// </summary>
public PluginInfo PluginInfo => Descriptor.LayerBrushProvider.PluginInfo;
public virtual LayerPropertyGroup BaseProperties => null;
/// <summary>
/// Called when the brush is being removed from the layer
/// </summary>
public virtual void Dispose()
{
}
/// <summary>
/// Called before rendering every frame, write your update logic here
/// </summary>
/// <param name="deltaTime"></param>
public virtual void Update(double deltaTime)
{
}
/// <summary>
/// The main method of rendering anything to the layer. The provided <see cref="SKCanvas" /> is specific to the layer
/// and matches it's width and height.
/// <para>Called during rendering or layer preview, in the order configured on the layer</para>
/// </summary>
/// <param name="canvas">The layer canvas</param>
/// <param name="canvasInfo"></param>
/// <param name="path">The path to be filled, represents the shape</param>
/// <param name="paint">The paint to be used to fill the shape</param>
public virtual void Render(SKCanvas canvas, SKImageInfo canvasInfo, SKPath path, SKPaint paint)
{
}
internal virtual void InitializeProperties(ILayerService layerService, string path)
{
}
}
}

View File

@ -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<T> : 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
/// <summary>
/// Called before rendering every frame, write your update logic here
/// Gets the properties of this brush.
/// </summary>
/// <param name="deltaTime"></param>
public virtual void Update(double deltaTime)
public T Properties
{
}
/// <summary>
/// The main method of rendering anything to the layer. The provided <see cref="SKCanvas" /> is specific to the layer
/// and matches it's width and height.
/// <para>Called during rendering or layer preview, in the order configured on the layer</para>
/// </summary>
/// <param name="canvas">The layer canvas</param>
/// <param name="canvasInfo"></param>
/// <param name="path">The path to be filled, represents the shape</param>
/// <param name="paint">The paint to be used to fill the shape</param>
public virtual void Render(SKCanvas canvas, SKImageInfo canvasInfo, SKPath path, SKPaint paint)
{
}
/// <summary>
/// Provides an easy way to add your own properties to the layer, for more info see <see cref="LayerProperty{T}" />.
/// <para>Note: If found, the last value and keyframes are loaded and set when calling this method.</para>
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="parent">The parent of this property, use this to create a tree-hierarchy in the editor</param>
/// <param name="id">A and ID identifying your property, must be unique within your plugin</param>
/// <param name="name">A name for your property, this is visible in the editor</param>
/// <param name="description">A description for your property, this is visible in the editor</param>
/// <param name="defaultValue">The default value of the property, if not configured by the user</param>
/// <returns>The layer property</returns>
protected LayerProperty<T> RegisterLayerProperty<T>(BaseLayerProperty parent, string id, string name, string description, T defaultValue = default)
{
var property = new LayerProperty<T>(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;
}
/// <summary>
/// Provides an easy way to add your own properties to the layer, for more info see <see cref="LayerProperty{T}" />.
/// <para>Note: If found, the last value and keyframes are loaded and set when calling this method.</para>
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="id">A and ID identifying your property, must be unique within your plugin</param>
/// <param name="name">A name for your property, this is visible in the editor</param>
/// <param name="description">A description for your property, this is visible in the editor</param>
/// <param name="defaultValue">The default value of the property, if not configured by the user</param>
/// <returns>The layer property</returns>
protected LayerProperty<T> RegisterLayerProperty<T>(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<T>) 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<T>(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;
}
/// <summary>
/// Allows you to remove layer properties previously added by using <see cref="RegisterLayerProperty{T}(BaseLayerProperty,string,string,string,T)" />.
/// Gets whether all properties on this brush are initialized
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="layerProperty"></param>
protected void UnRegisterLayerProperty<T>(LayerProperty<T> layerProperty)
{
if (layerProperty == null)
return;
public bool PropertiesInitialized { get; private set; }
if (Layer.Properties.Any(p => p == layerProperty))
Layer.Properties.RemoveLayerProperty(layerProperty);
/// <summary>
/// Called when all layer properties in this brush have been initialized
/// </summary>
protected virtual void OnPropertiesInitialized()
{
}
internal void SetLayerService(ILayerService layerService)
/// <inheritdoc/>
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<T>();
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<BaseLayerProperty> GetAllLayerProperties()
{
return Properties.GetAllLayerProperties();
}
#endregion
}
}

View File

@ -20,7 +20,7 @@ namespace Artemis.Core.Plugins.LayerBrush
public ReadOnlyCollection<LayerBrushDescriptor> LayerBrushDescriptors => _layerBrushDescriptors.AsReadOnly();
protected void AddLayerBrushDescriptor<T>(string displayName, string description, string icon) where T : LayerBrush
protected void AddLayerBrushDescriptor<T>(string displayName, string description, string icon) where T : BaseLayerBrush
{
_layerBrushDescriptors.Add(new LayerBrushDescriptor(displayName, description, icon, typeof(T), this));
}

View File

@ -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<Module> _modules;
private PluginSetting<LogEventLevel> _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<Module>();
_pluginService.PluginDisabled += (sender, args) => _modules = _pluginService.GetPluginsOfType<Module>();
ConfigureJsonConvert();
}
@ -99,7 +103,7 @@ namespace Artemis.Core.Services
_logger.Information("Initialized without an active surface entity");
_profileService.ActivateDefaultProfiles();
OnInitialized();
}

View File

@ -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
{
/// <summary>
/// Instantiates and adds the <see cref="LayerBrush" /> described by the provided <see cref="LayerBrushDescriptor" />
/// Creates a new layer
/// </summary>
/// <param name="profile"></param>
/// <param name="parent"></param>
/// <param name="name"></param>
/// <returns></returns>
Layer CreateLayer(Profile profile, ProfileElement parent, string name);
/// <summary>
/// Instantiates and adds the <see cref="BaseLayerBrush" /> described by the provided
/// <see cref="LayerBrushDescriptor" />
/// to the <see cref="Layer" />.
/// </summary>
/// <param name="layer">The layer to instantiate the brush for</param>
/// <returns></returns>
LayerBrush InstantiateLayerBrush(Layer layer);
BaseLayerBrush InstantiateLayerBrush(Layer layer);
/// <summary>
/// Instantiates and adds a compatible <see cref="KeyframeEngine" /> to the provided <see cref="LayerProperty{T}" />.
/// If the property already has a compatible keyframe engine, nothing happens.
/// Removes the layer brush from the provided layer and disposes it
/// </summary>
/// <param name="layerProperty">The layer property to apply the keyframe engine to.</param>
/// <returns>The resulting keyframe engine, if a compatible engine was found.</returns>
KeyframeEngine InstantiateKeyframeEngine<T>(LayerProperty<T> layerProperty);
/// <summary>
/// Instantiates and adds a compatible <see cref="KeyframeEngine" /> to the provided <see cref="BaseLayerProperty" />.
/// If the property already has a compatible keyframe engine, nothing happens.
/// </summary>
/// <param name="layerProperty">The layer property to apply the keyframe engine to.</param>
/// <returns>The resulting keyframe engine, if a compatible engine was found.</returns>
KeyframeEngine InstantiateKeyframeEngine(BaseLayerProperty layerProperty);
/// <param name="layer"></param>
void RemoveLayerBrush(Layer layer);
}
}

View File

@ -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<T>(LayerProperty<T> 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<List<KeyframeEngine>>();
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);
}
}
}

View File

@ -26,13 +26,13 @@ namespace Artemis.Core.Services.Storage.Interfaces
/// </summary>
/// <param name="selectedProfile"></param>
/// <param name="module"></param>
void UndoUpdateProfile(Profile selectedProfile, ProfileModule module);
bool UndoUpdateProfile(Profile selectedProfile, ProfileModule module);
/// <summary>
/// Attempts to restore the profile to the state it had before the last <see cref="UndoUpdateProfile" /> call.
/// </summary>
/// <param name="selectedProfile"></param>
/// <param name="module"></param>
void RedoUpdateProfile(Profile selectedProfile, ProfileModule module);
bool RedoUpdateProfile(Profile selectedProfile, ProfileModule module);
}
}

View File

@ -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<ProfileModule>();
@ -184,14 +194,7 @@ namespace Artemis.Core.Services.Storage
{
var profileModules = _pluginService.GetPluginsOfType<ProfileModule>();
foreach (var profileModule in profileModules.Where(p => p.ActiveProfile != null).ToList())
InstantiateProfileLayerBrushes(profileModule.ActiveProfile);
}
private void ActiveProfilesInstantiateKeyframeEngines()
{
var profileModules = _pluginService.GetPluginsOfType<ProfileModule>();
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)
{

View File

@ -5,6 +5,7 @@
<LangVersion>7</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LiteDB" Version="5.0.7" />
<PackageReference Include="LiteDB" Version="5.0.8" />
<PackageReference Include="Serilog" Version="2.9.0" />
</ItemGroup>
</Project>

View File

@ -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<LedEntity>();
PropertyEntities = new List<PropertyEntity>();
Condition = new List<ProfileConditionEntity>();
ExpandedPropertyGroups = new List<string>();
}
public Guid Id { get; set; }
@ -22,6 +24,7 @@ namespace Artemis.Storage.Entities.Profile
public List<LedEntity> Leds { get; set; }
public List<PropertyEntity> PropertyEntities { get; set; }
public List<ProfileConditionEntity> Condition { get; set; }
public List<string> ExpandedPropertyGroups { get; set; }
[BsonRef("ProfileEntity")]
public ProfileEntity Profile { get; set; }

View File

@ -10,10 +10,11 @@ namespace Artemis.Storage.Entities.Profile
KeyframeEntities = new List<KeyframeEntity>();
}
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<KeyframeEntity> KeyframeEntities { get; set; }
}

View File

@ -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");
}
}
}

View File

@ -0,0 +1,10 @@
using LiteDB;
namespace Artemis.Storage.Migrations.Interfaces
{
public interface IStorageMigration
{
int UserVersion { get; }
void Apply(LiteRepository repository);
}
}

View File

@ -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<IStorageMigration> _migrations;
public StorageMigrationService(ILogger logger, LiteRepository repository, List<IStorageMigration> 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;
}
}
}
}

View File

@ -20,15 +20,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AvalonEdit" Version="6.0.1" />
<PackageReference Include="Humanizer.Core" Version="2.7.9" />
<PackageReference Include="MaterialDesignExtensions" Version="3.0.0" />
<PackageReference Include="MaterialDesignThemes" Version="3.1.0" />
<PackageReference Include="Humanizer.Core" Version="2.8.11" />
<PackageReference Include="MaterialDesignExtensions" Version="3.1.0" />
<PackageReference Include="MaterialDesignThemes" Version="3.1.3" />
<PackageReference Include="Ninject" Version="3.3.4" />
<PackageReference Include="Ninject.Extensions.Conventions" Version="3.3.0" />
<PackageReference Include="PropertyChanged.Fody" Version="3.2.8" />
<PackageReference Include="SkiaSharp" Version="1.68.2-preview.29" />
<PackageReference Include="SkiaSharp.Views.WPF" Version="1.68.2-preview.29" />
<PackageReference Include="Stylet" Version="1.3.1" />
<PackageReference Include="SkiaSharp" Version="1.68.3" />
<PackageReference Include="SkiaSharp.Views.WPF" Version="1.68.3" />
<PackageReference Include="Stylet" Version="1.3.2" />
<PackageReference Include="System.Buffers" Version="4.5.0" />
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.7.0" />

View File

@ -9,10 +9,6 @@
d:DesignHeight="101.848" d:DesignWidth="242.956">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ColorPicker.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" />
</ResourceDictionary.MergedDictionaries>
<converters:ColorToStringConverter x:Key="ColorToStringConverter" />
<converters:ColorToSolidColorConverter x:Key="ColorToSolidColorConverter" />
<VisualBrush x:Key="Checkerboard" TileMode="Tile" Stretch="Uniform" ViewportUnits="Absolute" Viewport="0,0,10,10">
@ -62,14 +58,14 @@
Grid.Row="1"
OpacityMask="{x:Null}">
<Track.DecreaseRepeatButton>
<RepeatButton Command="{x:Static Slider.DecreaseLarge}" Style="{StaticResource MaterialDesignHorizontalColorSliderTrackRepeatButton}" />
<RepeatButton Command="{x:Static Slider.DecreaseLarge}" Style="{DynamicResource MaterialDesignHorizontalColorSliderTrackRepeatButton}" />
</Track.DecreaseRepeatButton>
<Track.IncreaseRepeatButton>
<RepeatButton Command="{x:Static Slider.IncreaseLarge}" Style="{StaticResource MaterialDesignHorizontalColorSliderTrackRepeatButton}" />
<RepeatButton Command="{x:Static Slider.IncreaseLarge}" Style="{DynamicResource MaterialDesignHorizontalColorSliderTrackRepeatButton}" />
</Track.IncreaseRepeatButton>
<Track.Thumb>
<Thumb x:Name="Thumb" Width="20" Height="20" VerticalAlignment="Center" Focusable="False" OverridesDefaultStyle="True"
Template="{StaticResource MaterialDesignColorSliderThumb}">
Template="{DynamicResource MaterialDesignColorSliderThumb}">
<Thumb.Foreground>
<SolidColorBrush
Color="{Binding Color, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Mode=OneWay, Converter={StaticResource ColorToSolidColorConverter}}" />
@ -124,7 +120,7 @@
<Slider Grid.Row="1" Margin="8"
IsMoveToPointEnabled="True"
Orientation="Horizontal"
Style="{StaticResource MaterialDesignColorSlider}"
Style="{DynamicResource MaterialDesignColorSlider}"
Template="{StaticResource MaterialDesignOpacitySlider}"
Value="{Binding ColorOpacity, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Maximum="255" />

View File

@ -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">
<UserControl.Style>
<Style>
<Setter Property="Validation.ErrorTemplate" Value="{StaticResource MaterialDesignValidationErrorTemplate}" />
</Style>
</UserControl.Style>
<StackPanel>
<!-- Drag handle -->
<Border x:Name="DragHandle" BorderThickness="0,0,0,1" Height="19">
<Border.BorderBrush>
<VisualBrush>
<VisualBrush.Visual>
<Rectangle StrokeDashArray="2 2" Stroke="{DynamicResource SecondaryAccentBrush}" StrokeThickness="1"
<Rectangle x:Name="BorderVisual"
StrokeDashArray="2 2" Stroke="{DynamicResource SecondaryAccentBrush}" StrokeThickness="1"
Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}}, Path=ActualWidth}"
Height="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}}, Path=ActualHeight}" />
Height="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}}, Path=ActualHeight}">
</Rectangle>
</VisualBrush.Visual>
</VisualBrush>
</Border.BorderBrush>
@ -27,15 +35,14 @@
Foreground="{DynamicResource SecondaryAccentBrush}"
MouseDown="InputMouseDown"
MouseUp="InputMouseUp"
MouseMove="InputMouseMove"
RequestBringIntoView="Input_OnRequestBringIntoView"/>
MouseMove="InputMouseMove"
RequestBringIntoView="Input_OnRequestBringIntoView" />
</Border>
<!-- Input -->
<TextBox x:Name="Input"
Width="60"
Height="20"
materialDesign:ValidationAssist.UsePopup="True"
HorizontalAlignment="Left"
Text="{Binding Value, StringFormat=N3, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
LostFocus="InputLostFocus"

View File

@ -104,6 +104,8 @@ namespace Artemis.UI.Shared.Controls
var startX = new decimal(_mouseDragStartPoint.X);
var x = new decimal(e.GetPosition((IInputElement) sender).X);
var stepSize = new decimal(StepSize);
if (stepSize == 0)
stepSize = 0.1m;
Value = (float) UltimateRoundingFunction(startValue + stepSize * (x - startX), stepSize, 0.5m);
}

View File

@ -8,10 +8,6 @@
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ColorPicker.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" />
</ResourceDictionary.MergedDictionaries>
<converters:ColorGradientToGradientStopsConverter x:Key="ColorGradientToGradientStopsConverter" />
<VisualBrush x:Key="Checkerboard" TileMode="Tile" Stretch="Uniform" ViewportUnits="Absolute" Viewport="0,0,10,10">
<VisualBrush.Visual>

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -10,29 +10,38 @@
mc:Ignorable="d"
d:DesignHeight="163.274" d:DesignWidth="254.425"
d:DataContext="{d:DesignInstance dialogs:ExceptionDialogViewModel}">
<StackPanel Margin="16">
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Text="{Binding Header}" TextWrapping="Wrap" />
<StackPanel Orientation="Vertical" HorizontalAlignment="Right" Margin="16">
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Text="{Binding Header}"
TextWrapping="Wrap" />
<Separator Margin="0 15" />
<TextBlock Style="{StaticResource MaterialDesignBody1TextBlock}" FontWeight="Bold" Margin="22 0">Exception message</TextBlock>
<TextBlock Style="{StaticResource MaterialDesignBody1TextBlock}" HorizontalAlignment="Left" Text="{Binding Exception.Message}" TextWrapping="Wrap" Margin="22 5" MaxWidth="1000" />
<Separator Margin="0 15" />
<TextBlock Style="{StaticResource MaterialDesignBody1TextBlock}" Text="Stack trace" TextWrapping="Wrap" FontWeight="Bold" Margin="22 0" />
<ScrollViewer MaxHeight="500">
<StackPanel>
<ItemsControl ItemsSource="{Binding Exceptions}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Style="{StaticResource MaterialDesignBody1TextBlock}" Text="Stack trace"
TextWrapping="Wrap" FontWeight="Bold"/>
<avalonedit:TextEditor SyntaxHighlighting="C#"
FontFamily="pack://application:,,,/Resources/Fonts/#Roboto Mono"
FontSize="10pt"
IsReadOnly="True"
Document="{Binding Document}"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
MaxWidth="1000"
Margin="0 10" />
<avalonedit:TextEditor SyntaxHighlighting="C#"
FontFamily="pack://application:,,,/Resources/Fonts/#Roboto Mono"
FontSize="10pt"
IsReadOnly="True"
Document="{Binding Document}"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
MaxWidth="1000"
Margin="0 10 10 0"
Padding="10"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Style="{StaticResource MaterialDesignFlatButton}" IsDefault="True" Margin="0 8 0 0"
Command="{s:Action Close}" Content="Close" />
</StackPanel>
<Separator Margin="0 15" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
<Button Style="{StaticResource MaterialDesignFlatButton}" IsDefault="True" Margin="0 8 0 0"
Command="{s:Action Close}" Content="Close" HorizontalAlignment="Right" />
</StackPanel>
</UserControl>

View File

@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using Artemis.UI.Shared.Services.Dialog;
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.Document;
namespace Artemis.UI.Shared.Screens.Dialogs
@ -10,18 +10,35 @@ namespace Artemis.UI.Shared.Screens.Dialogs
public ExceptionDialogViewModel(string message, Exception exception)
{
Header = message;
Exception = exception;
Document = new TextDocument(new StringTextSource(exception.StackTrace));
Exceptions = new List<DialogException>();
var currentException = exception;
while (currentException != null)
{
Exceptions.Add(new DialogException(currentException));
currentException = currentException.InnerException;
}
}
public string Header { get; }
public Exception Exception { get; }
public List<DialogException> Exceptions { get; set; }
public IDocument Document { get; set; }
public void Close()
{
Session.Close();
}
}
public class DialogException
{
public Exception Exception { get; }
public IDocument Document { get; set; }
public DialogException(Exception exception)
{
Exception = exception;
Document = new TextDocument(new StringTextSource($"{exception.Message}\r\n\r\n{exception.StackTrace}"));
}
}
}

View File

@ -6,6 +6,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.Utilities;
using Stylet;

View File

@ -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.Services.Dialog;
using Artemis.UI.Shared.Utilities;
using Stylet;

View File

@ -1,5 +1,6 @@
using System.Collections.Generic;
using Artemis.Core.Models.Profile;
using Artemis.Core.Models.Profile.Colors;
using Artemis.UI.Shared.Screens.GradientEditor;
using Artemis.UI.Shared.Services.Interfaces;

View File

@ -1,4 +1,5 @@
using Artemis.Core.Models.Profile;
using Artemis.Core.Models.Profile.Colors;
namespace Artemis.UI.Shared.Services.Interfaces
{

View File

@ -20,8 +20,8 @@ namespace Artemis.UI.Shared.Utilities
var resultCallback = new HitTestResultCallback(r => HitTestResultBehavior.Continue);
var filterCallback = new HitTestFilterCallback(e =>
{
if (e is FrameworkElement fe && fe.DataContext.GetType() == typeof(T) && !result.Contains((T) fe.DataContext))
result.Add((T) fe.DataContext);
if (e is FrameworkElement fe && fe.DataContext is T context && !result.Contains(context))
result.Add(context);
return HitTestFilterBehavior.Continue;
});
VisualTreeHelper.HitTest(container, filterCallback, resultCallback, hitTestParams);

View File

@ -115,21 +115,21 @@
<Resource Include="Resources\Cursors\aero_rotate.cur" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Castle.Core" Version="4.4.0" />
<PackageReference Include="Castle.Core" Version="4.4.1" />
<PackageReference Include="FluentValidation" Version="8.6.2" />
<PackageReference Include="gong-wpf-dragdrop" Version="2.2.0" />
<PackageReference Include="Hardcodet.NotifyIcon.Wpf.NetCore" Version="1.0.10" />
<PackageReference Include="Humanizer.Core" Version="2.7.9" />
<PackageReference Include="MaterialDesignExtensions" Version="3.0.0" />
<PackageReference Include="MaterialDesignThemes" Version="3.1.0" />
<PackageReference Include="Humanizer.Core" Version="2.8.11" />
<PackageReference Include="MaterialDesignExtensions" Version="3.1.0" />
<PackageReference Include="MaterialDesignThemes" Version="3.1.3" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.7.0" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.19" />
<PackageReference Include="Ninject" Version="3.3.4" />
<PackageReference Include="Ninject.Extensions.Conventions" Version="3.3.0" />
<PackageReference Include="PropertyChanged.Fody" Version="3.2.8" />
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="SkiaSharp.Views.WPF" Version="1.68.2-preview.29" />
<PackageReference Include="Stylet" Version="1.3.1" />
<PackageReference Include="SkiaSharp.Views.WPF" Version="1.68.3" />
<PackageReference Include="Stylet" Version="1.3.2" />
<PackageReference Include="System.Buffers" Version="4.5.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" />
<PackageReference Include="System.Drawing.Common" Version="4.7.0" />

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using Artemis.Core.Models.Profile.Conditions;
using Artemis.Core.Ninject;
using Artemis.Core.Services.Interfaces;
using Artemis.UI.Ninject;
@ -33,6 +34,8 @@ namespace Artemis.UI
protected override void Launch()
{
var test = new LayerCondition();
StartupArguments = Args.ToList();
var logger = Kernel.Get<ILogger>();

View File

@ -1,12 +1,8 @@
using Artemis.Core.Models.Profile;
using Artemis.Core.Models.Profile.LayerProperties;
using Artemis.Core.Models.Surface;
using Artemis.Core.Plugins.Abstract;
using Artemis.UI.Screens.Module;
using Artemis.UI.Screens.Module.ProfileEditor;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline;
using Artemis.UI.Screens.Module.ProfileEditor.ProfileTree.TreeItem;
using Artemis.UI.Screens.Module.ProfileEditor.Visualization;
using Artemis.UI.Screens.Settings.Tabs.Devices;
@ -47,29 +43,4 @@ namespace Artemis.UI.Ninject.Factories
{
ProfileLayerViewModel Create(Layer layer);
}
public interface ILayerPropertyVmFactory : IVmFactory
{
LayerPropertyViewModel Create(BaseLayerProperty layerProperty, LayerPropertyViewModel parent);
}
public interface IPropertyTreeVmFactory : IVmFactory
{
PropertyTreeViewModel Create(LayerPropertiesViewModel layerPropertiesViewModel);
}
public interface IPropertyTimelineVmFactory : IVmFactory
{
PropertyTimelineViewModel Create(LayerPropertiesViewModel layerPropertiesViewModel);
}
public interface IPropertyTrackVmFactory : IVmFactory
{
PropertyTrackViewModel Create(PropertyTimelineViewModel propertyTimelineViewModel, LayerPropertyViewModel layerPropertyViewModel);
}
public interface IPropertyTrackKeyframeVmFactory : IVmFactory
{
PropertyTrackKeyframeViewModel Create(PropertyTrackViewModel propertyTrackViewModel, BaseKeyframe keyframe);
}
}

View File

@ -2,7 +2,7 @@
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens;
using Artemis.UI.Screens.Module.ProfileEditor;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree.PropertyInput.Abstract;
using Artemis.UI.Services.Interfaces;
using Artemis.UI.Shared.Services.Dialog;
using Artemis.UI.Stylet;
@ -58,15 +58,6 @@ namespace Artemis.UI.Ninject
.BindAllBaseClasses();
});
// Bind property input VMs
Kernel.Bind(x =>
{
x.FromThisAssembly()
.SelectAllClasses()
.InheritedFrom<PropertyInputViewModel>()
.BindAllBaseClasses();
});
// Bind all UI services as singletons
Kernel.Bind(x =>
{
@ -86,6 +77,14 @@ namespace Artemis.UI.Ninject
.InheritedFrom<IValidator>()
.BindAllInterfaces();
});
Kernel.Bind(x =>
{
x.FromThisAssembly()
.SelectAllClasses()
.InheritedFrom(typeof(PropertyInputViewModel<>))
.BindToSelf();
});
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using Artemis.Core.Models.Profile.LayerProperties;
using Stylet;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Abstract
{
public abstract class LayerPropertyBaseViewModel : PropertyChangedBase, IDisposable
{
protected LayerPropertyBaseViewModel()
{
Children = new List<LayerPropertyBaseViewModel>();
}
public virtual bool IsExpanded { get; set; }
public abstract bool IsVisible { get; }
public List<LayerPropertyBaseViewModel> Children { get; set; }
public abstract List<BaseLayerPropertyKeyframe> GetKeyframes(bool visibleOnly);
public abstract void Dispose();
}
}

View File

@ -18,8 +18,7 @@
<KeyBinding Command="{s:Action PlayFromStart}" Modifiers="Shift" Key="Space" />
</UserControl.InputBindings>
<UserControl.Resources>
<s:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<Style x:Key="SVStyle" TargetType="{x:Type ScrollViewer}">
<Style x:Key="SvStyle" TargetType="{x:Type ScrollViewer}">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template">
<Setter.Value>
@ -37,6 +36,7 @@
<ScrollBar Name="PART_VerticalScrollBar"
HorizontalAlignment="Right"
Opacity="0.5"
Grid.Row="0"
Grid.Column="1"
Value="{TemplateBinding VerticalOffset}"
Maximum="{TemplateBinding ScrollableHeight}"
@ -46,7 +46,8 @@
VerticalAlignment="Bottom"
Orientation="Horizontal"
Opacity="0.5"
Grid.Row="1"
Grid.Row="1"
Grid.Column="0"
Value="{TemplateBinding HorizontalOffset}"
Maximum="{TemplateBinding ScrollableWidth}"
ViewportSize="{TemplateBinding ViewportWidth}"
@ -60,7 +61,7 @@
<Grid x:Name="ContainerGrid">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="28" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
@ -127,14 +128,14 @@
VerticalScrollBarVisibility="Hidden"
ScrollChanged="TimelineScrollChanged">
<Border BorderThickness="0,0,1,0" BorderBrush="{DynamicResource MaterialDesignDivider}">
<ContentControl s:View.Model="{Binding PropertyTree}" />
<ContentControl s:View.Model="{Binding TreeViewModel}" />
</Border>
</ScrollViewer>
</materialDesign:DialogHost>
</Grid>
<!-- Right side -->
<Grid Grid.Column="1">
<Grid Grid.Row="0" Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="56" />
<RowDefinition Height="*" />
@ -159,7 +160,7 @@
<!-- Time -->
<timeline:PropertyTimelineHeader Margin="0 25 0 0"
Fill="{DynamicResource MaterialDesignBody}"
PixelsPerSecond="{Binding PixelsPerSecond}"
PixelsPerSecond="{Binding ProfileEditorService.PixelsPerSecond}"
HorizontalOffset="{Binding ContentHorizontalOffset, ElementName=TimelineHeaderScrollViewer}"
VisibleWidth="{Binding ActualWidth, ElementName=TimelineHeaderScrollViewer}"
Width="{Binding ActualWidth, ElementName=PropertyTimeLine}" />
@ -169,7 +170,7 @@
<!-- Timeline rails -->
<ScrollViewer x:Name="TimelineRailsScrollViewer"
Grid.Row="1"
Style="{StaticResource SVStyle}"
Style="{StaticResource SvStyle}"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
ScrollChanged="TimelineScrollChanged">
@ -183,7 +184,7 @@
<Line X1="0" X2="0" Y1="0" Y2="{Binding ActualHeight, ElementName=ContainerGrid}"
StrokeThickness="2" Stroke="{StaticResource SecondaryAccentBrush}" />
</Canvas>
<ContentControl x:Name="PropertyTimeLine" s:View.Model="{Binding PropertyTimeline}" />
<ContentControl x:Name="PropertyTimeLine" s:View.Model="{Binding TimelineViewModel}" />
</Grid>
</ScrollViewer>
</Grid>
@ -195,17 +196,15 @@
HorizontalAlignment="Stretch"
ZIndex="2"
Background="{DynamicResource MaterialDesignCardBackground}">
<!-- Zoom control -->
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<TextBlock Text="{Binding PixelsPerSecond}" VerticalAlignment="Center" />
<Slider Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="10"
Minimum="31"
Maximum="350"
Value="{Binding PixelsPerSecond}"
Width="319" />
</StackPanel>
<Slider Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="10 5"
Minimum="31"
Maximum="350"
Value="{Binding ProfileEditorService.PixelsPerSecond}"
Width="319" />
</StackPanel>
</Grid>
</UserControl>

View File

@ -7,12 +7,12 @@ using System.Windows.Media;
using Artemis.Core.Events;
using Artemis.Core.Models.Profile;
using Artemis.Core.Models.Profile.LayerProperties;
using Artemis.Core.Models.Profile.LayerProperties.Attributes;
using Artemis.Core.Services;
using Artemis.Core.Services.Interfaces;
using Artemis.UI.Events;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree;
using Artemis.UI.Services.Interfaces;
using Stylet;
@ -20,84 +20,51 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
{
public class LayerPropertiesViewModel : ProfileEditorPanelViewModel
{
private readonly ICoreService _coreService;
private readonly List<LayerPropertyViewModel> _layerPropertyViewModels;
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
private readonly IPropertyTreeVmFactory _propertyTreeVmFactory;
private readonly IPropertyTimelineVmFactory _propertyTimelineVmFactory;
private readonly IProfileEditorService _profileEditorService;
private readonly ISettingsService _settingsService;
private Layer _lastSelectedLayer;
public LayerPropertiesViewModel(IProfileEditorService profileEditorService,
ICoreService coreService,
ISettingsService settingsService,
ILayerPropertyVmFactory layerPropertyVmFactory,
IPropertyTreeVmFactory propertyTreeVmFactory,
IPropertyTimelineVmFactory propertyTimelineVmFactory)
public LayerPropertiesViewModel(IProfileEditorService profileEditorService, ICoreService coreService, ISettingsService settingsService)
{
_profileEditorService = profileEditorService;
_coreService = coreService;
_settingsService = settingsService;
_layerPropertyVmFactory = layerPropertyVmFactory;
_propertyTreeVmFactory = propertyTreeVmFactory;
_propertyTimelineVmFactory = propertyTimelineVmFactory;
_layerPropertyViewModels = new List<LayerPropertyViewModel>();
ProfileEditorService = profileEditorService;
CoreService = coreService;
SettingsService = settingsService;
PixelsPerSecond = 31;
LayerPropertyGroups = new BindableCollection<LayerPropertyGroupViewModel>();
}
public IProfileEditorService ProfileEditorService { get; }
public ICoreService CoreService { get; }
public ISettingsService SettingsService { get; }
public bool Playing { get; set; }
public bool RepeatAfterLastKeyframe { get; set; }
public string FormattedCurrentTime => $"{Math.Floor(_profileEditorService.CurrentTime.TotalSeconds):00}.{_profileEditorService.CurrentTime.Milliseconds:000}";
public int PixelsPerSecond
{
get => _pixelsPerSecond;
set
{
_pixelsPerSecond = value;
OnPixelsPerSecondChanged();
}
}
public string FormattedCurrentTime => $"{Math.Floor(ProfileEditorService.CurrentTime.TotalSeconds):00}.{ProfileEditorService.CurrentTime.Milliseconds:000}";
public Thickness TimeCaretPosition
{
get => new Thickness(_profileEditorService.CurrentTime.TotalSeconds * PixelsPerSecond, 0, 0, 0);
set => _profileEditorService.CurrentTime = TimeSpan.FromSeconds(value.Left / PixelsPerSecond);
get => new Thickness(ProfileEditorService.CurrentTime.TotalSeconds * ProfileEditorService.PixelsPerSecond, 0, 0, 0);
set => ProfileEditorService.CurrentTime = TimeSpan.FromSeconds(value.Left / ProfileEditorService.PixelsPerSecond);
}
public PropertyTreeViewModel PropertyTree { get; set; }
public PropertyTimelineViewModel PropertyTimeline { get; set; }
public Layer SelectedLayer { get; set; }
public BindableCollection<LayerPropertyGroupViewModel> LayerPropertyGroups { get; set; }
public TreeViewModel TreeViewModel { get; set; }
public TimelineViewModel TimelineViewModel { get; set; }
protected override void OnInitialActivate()
{
PropertyTree = _propertyTreeVmFactory.Create(this);
PropertyTimeline = _propertyTimelineVmFactory.Create(this);
PopulateProperties(ProfileEditorService.SelectedProfileElement);
PopulateProperties(_profileEditorService.SelectedProfileElement);
_profileEditorService.ProfileElementSelected += ProfileEditorServiceOnProfileElementSelected;
_profileEditorService.CurrentTimeChanged += ProfileEditorServiceOnCurrentTimeChanged;
ProfileEditorService.ProfileElementSelected += ProfileEditorServiceOnProfileElementSelected;
ProfileEditorService.CurrentTimeChanged += ProfileEditorServiceOnCurrentTimeChanged;
ProfileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
base.OnInitialActivate();
}
protected override void OnClose()
{
_profileEditorService.ProfileElementSelected -= ProfileEditorServiceOnProfileElementSelected;
_profileEditorService.CurrentTimeChanged -= ProfileEditorServiceOnCurrentTimeChanged;
ProfileEditorService.ProfileElementSelected -= ProfileEditorServiceOnProfileElementSelected;
ProfileEditorService.CurrentTimeChanged -= ProfileEditorServiceOnCurrentTimeChanged;
ProfileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
if (_lastSelectedLayer != null)
{
_lastSelectedLayer.Properties.LayerPropertyRegistered -= LayerOnPropertyRegistered;
_lastSelectedLayer.Properties.LayerPropertyRemoved -= LayerOnPropertyRemoved;
}
PropertyTree?.Dispose();
PropertyTimeline?.Dispose();
PropertyTree = null;
PropertyTimeline = null;
base.OnClose();
}
@ -118,82 +85,88 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
NotifyOfPropertyChange(() => TimeCaretPosition);
}
private void ProfileEditorServiceOnPixelsPerSecondChanged(object? sender, EventArgs e)
{
NotifyOfPropertyChange(nameof(TimeCaretPosition));
}
#region View model managament
public List<LayerPropertyGroupViewModel> GetAllLayerPropertyGroupViewModels()
{
var groups = LayerPropertyGroups.ToList();
groups.AddRange(groups.SelectMany(g => g.Children).Where(g => g is LayerPropertyGroupViewModel).Cast<LayerPropertyGroupViewModel>());
return groups;
}
private void PopulateProperties(ProfileElement profileElement)
{
if (_lastSelectedLayer != null)
if (SelectedLayer != null)
{
_lastSelectedLayer.Properties.LayerPropertyRegistered -= LayerOnPropertyRegistered;
_lastSelectedLayer.Properties.LayerPropertyRemoved -= LayerOnPropertyRemoved;
SelectedLayer.LayerBrushUpdated -= SelectedLayerOnLayerBrushUpdated;
SelectedLayer = null;
}
foreach (var layerPropertyGroupViewModel in LayerPropertyGroups)
layerPropertyGroupViewModel.Dispose();
LayerPropertyGroups.Clear();
if (profileElement is Layer layer)
{
// Create VMs for missing properties
foreach (var baseLayerProperty in layer.Properties)
{
if (_layerPropertyViewModels.All(vm => vm.LayerProperty != baseLayerProperty))
CreatePropertyViewModel(baseLayerProperty);
}
SelectedLayer = layer;
SelectedLayer.LayerBrushUpdated += SelectedLayerOnLayerBrushUpdated;
// Remove VMs for extra properties
foreach (var layerPropertyViewModel in _layerPropertyViewModels.ToList())
{
if (layer.Properties.All(p => p != layerPropertyViewModel.LayerProperty))
RemovePropertyViewModel(layerPropertyViewModel);
}
// Add the built-in root groups of the layer
var generalAttribute = Attribute.GetCustomAttribute(
layer.GetType().GetProperty(nameof(layer.General)),
typeof(PropertyGroupDescriptionAttribute)
);
var transformAttribute = Attribute.GetCustomAttribute(
layer.GetType().GetProperty(nameof(layer.Transform)),
typeof(PropertyGroupDescriptionAttribute)
);
LayerPropertyGroups.Add(new LayerPropertyGroupViewModel(ProfileEditorService, layer.General, (PropertyGroupDescriptionAttribute) generalAttribute));
LayerPropertyGroups.Add(new LayerPropertyGroupViewModel(ProfileEditorService, layer.Transform, (PropertyGroupDescriptionAttribute) transformAttribute));
_lastSelectedLayer = layer;
layer.Properties.LayerPropertyRegistered += LayerOnPropertyRegistered;
layer.Properties.LayerPropertyRemoved += LayerOnPropertyRemoved;
if (layer.LayerBrush != null)
{
// Add the rout group of the brush
// The root group of the brush has no attribute so let's pull one out of our sleeve
var brushDescription = new PropertyGroupDescriptionAttribute
{
Name = layer.LayerBrush.Descriptor.DisplayName,
Description = layer.LayerBrush.Descriptor.Description
};
LayerPropertyGroups.Add(new LayerPropertyGroupViewModel(ProfileEditorService, layer.LayerBrush.BaseProperties, brushDescription));
}
}
else
SelectedLayer = null;
TreeViewModel = new TreeViewModel(this, LayerPropertyGroups);
TimelineViewModel = new TimelineViewModel(this, LayerPropertyGroups);
}
private void SelectedLayerOnLayerBrushUpdated(object sender, EventArgs e)
{
// Get rid of the old layer properties group
if (LayerPropertyGroups.Count == 3)
{
foreach (var layerPropertyViewModel in _layerPropertyViewModels.ToList())
RemovePropertyViewModel(layerPropertyViewModel);
_lastSelectedLayer = null;
}
}
private void LayerOnPropertyRegistered(object sender, LayerPropertyEventArgs e)
{
Console.WriteLine("LayerOnPropertyRegistered");
PopulateProperties(e.LayerProperty.Layer);
}
private void LayerOnPropertyRemoved(object sender, LayerPropertyEventArgs e)
{
Console.WriteLine("LayerOnPropertyRemoved");
PopulateProperties(e.LayerProperty.Layer);
}
private LayerPropertyViewModel CreatePropertyViewModel(BaseLayerProperty layerProperty)
{
LayerPropertyViewModel parent = null;
// If the property has a parent, find it's VM
if (layerProperty.Parent != null)
{
parent = _layerPropertyViewModels.FirstOrDefault(vm => vm.LayerProperty == layerProperty.Parent);
// If no VM is found, create it
if (parent == null)
parent = CreatePropertyViewModel(layerProperty.Parent);
LayerPropertyGroups[2].Dispose();
LayerPropertyGroups.RemoveAt(2);
}
var createdViewModel = _layerPropertyVmFactory.Create(layerProperty, parent);
_layerPropertyViewModels.Add(createdViewModel);
PropertyTree.AddLayerProperty(createdViewModel);
PropertyTimeline.AddLayerProperty(createdViewModel);
return createdViewModel;
}
private void RemovePropertyViewModel(LayerPropertyViewModel layerPropertyViewModel)
{
PropertyTree.RemoveLayerProperty(layerPropertyViewModel);
PropertyTimeline.RemoveLayerProperty(layerPropertyViewModel);
_layerPropertyViewModels.Remove(layerPropertyViewModel);
if (SelectedLayer.LayerBrush != null)
{
// Add the rout group of the brush
// The root group of the brush has no attribute so let's pull one out of our sleeve
var brushDescription = new PropertyGroupDescriptionAttribute
{
Name = SelectedLayer.LayerBrush.Descriptor.DisplayName,
Description = SelectedLayer.LayerBrush.Descriptor.Description
};
LayerPropertyGroups.Add(new LayerPropertyGroupViewModel(ProfileEditorService, SelectedLayer.LayerBrush.BaseProperties, brushDescription));
}
}
#endregion
@ -203,7 +176,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
public void PlayFromStart()
{
if (!Playing)
_profileEditorService.CurrentTime = TimeSpan.Zero;
ProfileEditorService.CurrentTime = TimeSpan.Zero;
Play();
}
@ -218,7 +191,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
return;
}
_coreService.FrameRendering += CoreServiceOnFrameRendering;
CoreService.FrameRendering += CoreServiceOnFrameRendering;
Playing = true;
}
@ -227,48 +200,55 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
if (!Playing)
return;
_coreService.FrameRendering -= CoreServiceOnFrameRendering;
CoreService.FrameRendering -= CoreServiceOnFrameRendering;
Playing = false;
}
public void GoToStart()
{
_profileEditorService.CurrentTime = TimeSpan.Zero;
ProfileEditorService.CurrentTime = TimeSpan.Zero;
}
public void GoToEnd()
{
_profileEditorService.CurrentTime = CalculateEndTime();
ProfileEditorService.CurrentTime = CalculateEndTime();
}
public void GoToPreviousFrame()
{
var frameTime = 1000.0 / _settingsService.GetSetting("Core.TargetFrameRate", 25).Value;
var newTime = Math.Max(0, Math.Round((_profileEditorService.CurrentTime.TotalMilliseconds - frameTime) / frameTime) * frameTime);
_profileEditorService.CurrentTime = TimeSpan.FromMilliseconds(newTime);
var frameTime = 1000.0 / SettingsService.GetSetting("Core.TargetFrameRate", 25).Value;
var newTime = Math.Max(0, Math.Round((ProfileEditorService.CurrentTime.TotalMilliseconds - frameTime) / frameTime) * frameTime);
ProfileEditorService.CurrentTime = TimeSpan.FromMilliseconds(newTime);
}
public void GoToNextFrame()
{
var frameTime = 1000.0 / _settingsService.GetSetting("Core.TargetFrameRate", 25).Value;
var newTime = Math.Round((_profileEditorService.CurrentTime.TotalMilliseconds + frameTime) / frameTime) * frameTime;
var frameTime = 1000.0 / SettingsService.GetSetting("Core.TargetFrameRate", 25).Value;
var newTime = Math.Round((ProfileEditorService.CurrentTime.TotalMilliseconds + frameTime) / frameTime) * frameTime;
newTime = Math.Min(newTime, CalculateEndTime().TotalMilliseconds);
_profileEditorService.CurrentTime = TimeSpan.FromMilliseconds(newTime);
ProfileEditorService.CurrentTime = TimeSpan.FromMilliseconds(newTime);
}
private TimeSpan CalculateEndTime()
{
// End time is the last keyframe + 10 sec
var lastKeyFrame = PropertyTimeline.PropertyTrackViewModels.SelectMany(r => r.KeyframeViewModels).OrderByDescending(t => t.Keyframe.Position).FirstOrDefault();
return lastKeyFrame?.Keyframe.Position.Add(new TimeSpan(0, 0, 0, 10)) ?? TimeSpan.MaxValue;
if (!(ProfileEditorService.SelectedProfileElement is Layer layer))
return TimeSpan.MaxValue;
var keyframes = GetKeyframes(false);
// If there are no keyframes, don't stop at all
if (!keyframes.Any())
return TimeSpan.MaxValue;
// If there are keyframes, stop after the last keyframe + 10 sec
return keyframes.Max(k => k.Position).Add(TimeSpan.FromSeconds(10));
}
private void CoreServiceOnFrameRendering(object sender, FrameRenderingEventArgs e)
{
Execute.PostToUIThread(() =>
{
var newTime = _profileEditorService.CurrentTime.Add(TimeSpan.FromSeconds(e.DeltaTime));
var newTime = ProfileEditorService.CurrentTime.Add(TimeSpan.FromSeconds(e.DeltaTime));
if (RepeatAfterLastKeyframe)
{
if (newTime > CalculateEndTime().Subtract(TimeSpan.FromSeconds(10)))
@ -280,7 +260,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
Pause();
}
_profileEditorService.CurrentTime = newTime;
ProfileEditorService.CurrentTime = newTime;
});
}
@ -288,8 +268,6 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
#region Caret movement
private int _pixelsPerSecond;
public void TimelineMouseDown(object sender, MouseButtonEventArgs e)
{
((IInputElement) sender).CaptureMouse();
@ -307,44 +285,38 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
// Get the parent grid, need that for our position
var parent = (IInputElement) VisualTreeHelper.GetParent((DependencyObject) sender);
var x = Math.Max(0, e.GetPosition(parent).X);
var newTime = TimeSpan.FromSeconds(x / PixelsPerSecond);
var newTime = TimeSpan.FromSeconds(x / ProfileEditorService.PixelsPerSecond);
// Round the time to something that fits the current zoom level
if (PixelsPerSecond < 200)
if (ProfileEditorService.PixelsPerSecond < 200)
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 5.0) * 5.0);
else if (PixelsPerSecond < 500)
else if (ProfileEditorService.PixelsPerSecond < 500)
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 2.0) * 2.0);
else
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds));
if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift))
{
_profileEditorService.CurrentTime = newTime;
ProfileEditorService.CurrentTime = newTime;
return;
}
// If shift is held, snap to closest keyframe
var visibleKeyframes = PropertyTimeline.PropertyTrackViewModels
.Where(t => t.LayerPropertyViewModel.Parent != null && t.LayerPropertyViewModel.Parent.IsExpanded)
.SelectMany(t => t.KeyframeViewModels);
var visibleKeyframes = GetKeyframes(true);
// Take a tolerance of 5 pixels (half a keyframe width)
var tolerance = 1000f / PixelsPerSecond * 5;
var closeKeyframe = visibleKeyframes.FirstOrDefault(
kf => Math.Abs(kf.Keyframe.Position.TotalMilliseconds - newTime.TotalMilliseconds) < tolerance
);
_profileEditorService.CurrentTime = closeKeyframe?.Keyframe.Position ?? newTime;
var tolerance = 1000f / ProfileEditorService.PixelsPerSecond * 5;
var closeKeyframe = visibleKeyframes.FirstOrDefault(k => Math.Abs(k.Position.TotalMilliseconds - newTime.TotalMilliseconds) < tolerance);
ProfileEditorService.CurrentTime = closeKeyframe?.Position ?? newTime;
}
}
#endregion
#region Events
public event EventHandler PixelsPerSecondChanged;
protected virtual void OnPixelsPerSecondChanged()
private List<BaseLayerPropertyKeyframe> GetKeyframes(bool visibleOnly)
{
PixelsPerSecondChanged?.Invoke(this, EventArgs.Empty);
var result = new List<BaseLayerPropertyKeyframe>();
foreach (var layerPropertyGroupViewModel in LayerPropertyGroups)
result.AddRange(layerPropertyGroupViewModel.GetKeyframes(visibleOnly));
return result;
}
#endregion

View File

@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using Artemis.Core.Models.Profile;
using Artemis.Core.Models.Profile.LayerProperties;
using Artemis.Core.Models.Profile.LayerProperties.Attributes;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Abstract;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree;
using Artemis.UI.Services.Interfaces;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
{
public class LayerPropertyGroupViewModel : LayerPropertyBaseViewModel
{
public LayerPropertyGroupViewModel(IProfileEditorService profileEditorService, LayerPropertyGroup layerPropertyGroup, PropertyGroupDescriptionAttribute propertyGroupDescription)
{
ProfileEditorService = profileEditorService;
LayerPropertyGroup = layerPropertyGroup;
PropertyGroupDescription = propertyGroupDescription;
TreePropertyGroupViewModel = new TreePropertyGroupViewModel(this);
TimelinePropertyGroupViewModel = new TimelinePropertyGroupViewModel(this);
PopulateChildren();
LayerPropertyGroup.VisibilityChanged += LayerPropertyGroupOnVisibilityChanged;
}
public override bool IsExpanded
{
get => LayerPropertyGroup.Layer.IsPropertyGroupExpanded(LayerPropertyGroup);
set => LayerPropertyGroup.Layer.SetPropertyGroupExpanded(LayerPropertyGroup, value);
}
public override bool IsVisible => !LayerPropertyGroup.IsHidden;
public IProfileEditorService ProfileEditorService { get; }
public LayerPropertyGroup LayerPropertyGroup { get; }
public PropertyGroupDescriptionAttribute PropertyGroupDescription { get; }
public TreePropertyGroupViewModel TreePropertyGroupViewModel { get; set; }
public TimelinePropertyGroupViewModel TimelinePropertyGroupViewModel { get; set; }
private void PopulateChildren()
{
// Get all properties and property groups and create VMs for them
foreach (var propertyInfo in LayerPropertyGroup.GetType().GetProperties())
{
var propertyAttribute = (PropertyDescriptionAttribute) Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyDescriptionAttribute));
var groupAttribute = (PropertyGroupDescriptionAttribute) Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyGroupDescriptionAttribute));
var value = propertyInfo.GetValue(LayerPropertyGroup);
// Create VMs for properties on the group
if (propertyAttribute != null && value is BaseLayerProperty baseLayerProperty)
{
var viewModel = ProfileEditorService.CreateLayerPropertyViewModel(baseLayerProperty, propertyAttribute);
if (viewModel != null)
Children.Add(viewModel);
}
// Create VMs for child groups on this group, resulting in a nested structure
else if (groupAttribute != null && value is LayerPropertyGroup layerPropertyGroup)
{
Children.Add(new LayerPropertyGroupViewModel(ProfileEditorService, layerPropertyGroup, groupAttribute));
}
}
}
public override List<BaseLayerPropertyKeyframe> GetKeyframes(bool visibleOnly)
{
var result = new List<BaseLayerPropertyKeyframe>();
if (visibleOnly && !IsExpanded)
return result;
foreach (var layerPropertyBaseViewModel in Children)
result.AddRange(layerPropertyBaseViewModel.GetKeyframes(visibleOnly));
return result;
}
public override void Dispose()
{
foreach (var layerPropertyBaseViewModel in Children)
layerPropertyBaseViewModel.Dispose();
LayerPropertyGroup.VisibilityChanged -= LayerPropertyGroupOnVisibilityChanged;
TimelinePropertyGroupViewModel.Dispose();
}
public List<LayerPropertyBaseViewModel> GetAllChildren()
{
var result = new List<LayerPropertyBaseViewModel>();
foreach (var layerPropertyBaseViewModel in Children)
{
result.Add(layerPropertyBaseViewModel);
if (layerPropertyBaseViewModel is LayerPropertyGroupViewModel layerPropertyGroupViewModel)
result.AddRange(layerPropertyGroupViewModel.GetAllChildren());
}
return result;
}
private void LayerPropertyGroupOnVisibilityChanged(object? sender, EventArgs e)
{
NotifyOfPropertyChange(nameof(IsVisible));
}
}
}

View File

@ -2,99 +2,90 @@
using System.Collections.Generic;
using System.Linq;
using Artemis.Core.Models.Profile.LayerProperties;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput;
using Artemis.Core.Models.Profile.LayerProperties.Attributes;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Abstract;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree;
using Artemis.UI.Services.Interfaces;
using Ninject;
using Stylet;
using Humanizer;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
{
public class LayerPropertyViewModel : PropertyChangedBase
public class LayerPropertyViewModel<T> : LayerPropertyViewModel
{
private readonly IKernel _kernel;
private readonly IProfileEditorService _profileEditorService;
private bool _keyframesEnabled;
private bool _isExpanded;
public LayerPropertyViewModel(BaseLayerProperty layerProperty, LayerPropertyViewModel parent, IKernel kernel, IProfileEditorService profileEditorService)
public LayerPropertyViewModel(IProfileEditorService profileEditorService, LayerProperty<T> layerProperty, PropertyDescriptionAttribute propertyDescription)
: base(profileEditorService, layerProperty)
{
_kernel = kernel;
_profileEditorService = profileEditorService;
_keyframesEnabled = layerProperty.IsUsingKeyframes;
LayerProperty = layerProperty;
Parent = parent;
Children = new List<LayerPropertyViewModel>();
IsExpanded = layerProperty.ExpandByDefault;
PropertyDescription = propertyDescription;
Parent?.Children.Add(this);
}
TreePropertyViewModel = ProfileEditorService.CreateTreePropertyViewModel(this);
TimelinePropertyViewModel = new TimelinePropertyViewModel<T>(this, profileEditorService);
public BaseLayerProperty LayerProperty { get; }
TreePropertyBaseViewModel = TreePropertyViewModel;
TimelinePropertyBaseViewModel = TimelinePropertyViewModel;
public LayerPropertyViewModel Parent { get; }
public List<LayerPropertyViewModel> Children { get; }
public bool IsExpanded
{
get => _isExpanded;
set
// Generate a fallback name if the description does not contain one
if (PropertyDescription.Name == null)
{
_isExpanded = value;
OnExpandedStateChanged();
var propertyInfo = LayerProperty.Parent?.GetType().GetProperties().FirstOrDefault(p => ReferenceEquals(p.GetValue(LayerProperty.Parent), LayerProperty));
if (propertyInfo != null)
PropertyDescription.Name = propertyInfo.Name.Humanize();
else
PropertyDescription.Name = $"Unknown {typeof(T).Name} property";
}
LayerProperty.VisibilityChanged += LayerPropertyOnVisibilityChanged;
}
public bool KeyframesEnabled
public override bool IsVisible => !LayerProperty.IsHidden;
public LayerProperty<T> LayerProperty { get; }
public TreePropertyViewModel<T> TreePropertyViewModel { get; set; }
public TimelinePropertyViewModel<T> TimelinePropertyViewModel { get; set; }
public override List<BaseLayerPropertyKeyframe> GetKeyframes(bool visibleOnly)
{
get => _keyframesEnabled;
set
{
_keyframesEnabled = value;
UpdateKeyframes();
}
return LayerProperty.BaseKeyframes.ToList();
}
public PropertyInputViewModel GetPropertyInputViewModel()
public override void Dispose()
{
// If the type is an enum type, search for Enum instead.
var type = LayerProperty.Type;
if (type.IsEnum)
type = typeof(Enum);
TreePropertyViewModel.Dispose();
TimelinePropertyViewModel.Dispose();
var match = _kernel.Get<List<PropertyInputViewModel>>().FirstOrDefault(p => p.CompatibleTypes.Contains(type));
if (match == null)
return null;
match.Initialize(this);
return match;
LayerProperty.VisibilityChanged -= LayerPropertyOnVisibilityChanged;
}
private void UpdateKeyframes()
public void SetCurrentValue(T value, bool saveChanges)
{
// Either create a new first keyframe or clear all the keyframes
if (_keyframesEnabled)
LayerProperty.CreateNewKeyframe(_profileEditorService.CurrentTime, LayerProperty.GetCurrentValue());
LayerProperty.SetCurrentValue(value, ProfileEditorService.CurrentTime);
if (saveChanges)
ProfileEditorService.UpdateSelectedProfileElement();
else
LayerProperty.ClearKeyframes();
// Force the keyframe engine to update, the new keyframe is the current keyframe
LayerProperty.IsUsingKeyframes = _keyframesEnabled;
LayerProperty.KeyframeEngine?.Update(0);
_profileEditorService.UpdateSelectedProfileElement();
ProfileEditorService.UpdateProfilePreview();
}
#region Events
public event EventHandler<EventArgs> ExpandedStateChanged;
protected virtual void OnExpandedStateChanged()
private void LayerPropertyOnVisibilityChanged(object? sender, EventArgs e)
{
ExpandedStateChanged?.Invoke(this, EventArgs.Empty);
foreach (var layerPropertyViewModel in Children)
layerPropertyViewModel.OnExpandedStateChanged();
NotifyOfPropertyChange(nameof(IsVisible));
}
}
public abstract class LayerPropertyViewModel : LayerPropertyBaseViewModel
{
protected LayerPropertyViewModel(IProfileEditorService profileEditorService, BaseLayerProperty baseLayerProperty)
{
ProfileEditorService = profileEditorService;
BaseLayerProperty = baseLayerProperty;
}
#endregion
public IProfileEditorService ProfileEditorService { get; }
public BaseLayerProperty BaseLayerProperty { get; }
public PropertyDescriptionAttribute PropertyDescription { get; protected set; }
public TreePropertyViewModel TreePropertyBaseViewModel { get; set; }
public TimelinePropertyViewModel TimelinePropertyBaseViewModel { get; set; }
}
}

View File

@ -1,27 +0,0 @@
using System;
using System.Collections.Generic;
using Artemis.Core.Models.Profile;
using Artemis.UI.Services.Interfaces;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput
{
public class ColorGradientPropertyInputViewModel : PropertyInputViewModel
{
public ColorGradientPropertyInputViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
{
}
public sealed override List<Type> CompatibleTypes { get; } = new List<Type> {typeof(ColorGradient)};
public ColorGradient ColorGradientInputValue
{
get => (ColorGradient) InputValue ?? new ColorGradient();
set => InputValue = value;
}
public override void Update()
{
NotifyOfPropertyChange(() => ColorGradientInputValue);
}
}
}

View File

@ -1,35 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Artemis.UI.Services.Interfaces;
using Artemis.UI.Shared.Utilities;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput
{
public class EnumPropertyInputViewModel : PropertyInputViewModel
{
public EnumPropertyInputViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
{
}
public IEnumerable<ValueDescription> EnumValues { get; private set; }
public sealed override List<Type> CompatibleTypes { get; } = new List<Type> {typeof(Enum)};
public object EnumInputValue
{
get => InputValue ?? Enum.GetValues(LayerPropertyViewModel.LayerProperty.Type).Cast<object>().First();
set => InputValue = value;
}
public override void Update()
{
NotifyOfPropertyChange(() => EnumInputValue);
}
protected override void OnInitialized()
{
EnumValues = EnumUtilities.GetAllValuesAndDescriptions(LayerPropertyViewModel.LayerProperty.Type);
}
}
}

View File

@ -1,21 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput.FloatPropertyInputView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:FloatPropertyInputViewModel}">
<StackPanel Orientation="Horizontal">
<TextBlock Margin="0 0 5 4" Width="10" VerticalAlignment="Bottom" Text="{Binding LayerPropertyViewModel.LayerProperty.InputPrefix}" />
<controls:DraggableFloat Value="{Binding FloatInputValue}"
StepSize="{Binding LayerPropertyViewModel.LayerProperty.InputStepSize}"
DragStarted="{s:Action InputDragStarted}"
DragEnded="{s:Action InputDragEnded}" />
<TextBlock Margin="5 0 0 4" Width="10" VerticalAlignment="Bottom" Text="{Binding LayerPropertyViewModel.LayerProperty.InputAffix}" />
</StackPanel>
</UserControl>

View File

@ -1,39 +0,0 @@
using System;
using System.Collections.Generic;
using Artemis.UI.Services.Interfaces;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput
{
public class FloatPropertyInputViewModel : PropertyInputViewModel
{
public FloatPropertyInputViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
{
}
public sealed override List<Type> CompatibleTypes { get; } = new List<Type> {typeof(float)};
public float FloatInputValue
{
get => (float?) InputValue ?? 0f;
set => InputValue = ApplyInputValue(value);
}
public override void Update()
{
NotifyOfPropertyChange(() => FloatInputValue);
}
private float ApplyInputValue(float value)
{
if (LayerPropertyViewModel.LayerProperty.MaxInputValue != null &&
LayerPropertyViewModel.LayerProperty.MaxInputValue is float maxFloat)
value = Math.Min(value, maxFloat);
if (LayerPropertyViewModel.LayerProperty.MinInputValue != null &&
LayerPropertyViewModel.LayerProperty.MinInputValue is float minFloat)
value = Math.Max(value, minFloat);
return value;
}
}
}

View File

@ -1,21 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput.IntPropertyInputView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:IntPropertyInputViewModel}">
<StackPanel Orientation="Horizontal">
<TextBlock Margin="0 0 5 4" Width="10" VerticalAlignment="Bottom" Text="{Binding LayerPropertyViewModel.LayerProperty.InputPrefix}" />
<controls:DraggableFloat Value="{Binding IntInputValue}"
StepSize="{Binding LayerPropertyViewModel.LayerProperty.InputStepSize}"
DragStarted="{s:Action InputDragStarted}"
DragEnded="{s:Action InputDragEnded}" />
<TextBlock Margin="5 0 0 4" Width="10" VerticalAlignment="Bottom" Text="{Binding LayerPropertyViewModel.LayerProperty.InputAffix}" />
</StackPanel>
</UserControl>

View File

@ -1,38 +0,0 @@
using System;
using System.Collections.Generic;
using Artemis.UI.Services.Interfaces;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput
{
public class IntPropertyInputViewModel : PropertyInputViewModel
{
public IntPropertyInputViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
{
}
public sealed override List<Type> CompatibleTypes { get; } = new List<Type> {typeof(int)};
public int IntInputValue
{
get => (int?) InputValue ?? 0;
set => InputValue = ApplyInputValue(value);
}
public override void Update()
{
NotifyOfPropertyChange(() => IntInputValue);
}
private int ApplyInputValue(int value)
{
if (LayerPropertyViewModel.LayerProperty.MaxInputValue != null &&
LayerPropertyViewModel.LayerProperty.MaxInputValue is int maxInt)
value = Math.Min(value, maxInt);
if (LayerPropertyViewModel.LayerProperty.MinInputValue != null &&
LayerPropertyViewModel.LayerProperty.MinInputValue is int minInt)
value = Math.Max(value, minInt);
return value;
}
}
}

View File

@ -1,92 +0,0 @@
using System;
using System.Collections.Generic;
using Artemis.UI.Exceptions;
using Artemis.UI.Services.Interfaces;
using Stylet;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput
{
public abstract class PropertyInputViewModel : PropertyChangedBase, IDisposable
{
protected PropertyInputViewModel(IProfileEditorService profileEditorService)
{
ProfileEditorService = profileEditorService;
}
protected IProfileEditorService ProfileEditorService { get; }
public abstract List<Type> CompatibleTypes { get; }
public bool Initialized { get; private set; }
public bool InputDragging { get; private set; }
public LayerPropertyViewModel LayerPropertyViewModel { get; private set; }
protected object InputValue
{
get => LayerPropertyViewModel.LayerProperty.GetCurrentValue();
set => UpdateInputValue(value);
}
public void Initialize(LayerPropertyViewModel layerPropertyViewModel)
{
var type = layerPropertyViewModel.LayerProperty.Type;
if (type.IsEnum)
type = typeof(Enum);
if (Initialized)
throw new ArtemisUIException("Cannot initialize the same property input VM twice");
if (!CompatibleTypes.Contains(type))
throw new ArtemisUIException($"This input VM does not support the provided type {type.Name}");
LayerPropertyViewModel = layerPropertyViewModel;
LayerPropertyViewModel.LayerProperty.ValueChanged += LayerPropertyOnValueChanged;
Update();
Initialized = true;
OnInitialized();
}
public abstract void Update();
protected virtual void OnInitialized()
{
}
private void LayerPropertyOnValueChanged(object sender, EventArgs e)
{
Update();
}
private void UpdateInputValue(object value)
{
LayerPropertyViewModel.LayerProperty.SetCurrentValue(value, ProfileEditorService.CurrentTime);
// Force the keyframe engine to update, the edited keyframe might affect the current keyframe progress
LayerPropertyViewModel.LayerProperty.KeyframeEngine?.Update(0);
if (!InputDragging)
ProfileEditorService.UpdateSelectedProfileElement();
else
ProfileEditorService.UpdateProfilePreview();
}
#region Event handlers
public void InputDragStarted(object sender, EventArgs e)
{
InputDragging = true;
}
public void InputDragEnded(object sender, EventArgs e)
{
InputDragging = false;
ProfileEditorService.UpdateSelectedProfileElement();
}
#endregion
public virtual void Dispose()
{
if (LayerPropertyViewModel != null)
LayerPropertyViewModel.LayerProperty.ValueChanged -= LayerPropertyOnValueChanged;
}
}
}

View File

@ -1,27 +0,0 @@
using System;
using System.Collections.Generic;
using Artemis.UI.Services.Interfaces;
using SkiaSharp;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput
{
public class SKColorPropertyInputViewModel : PropertyInputViewModel
{
public SKColorPropertyInputViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
{
}
public sealed override List<Type> CompatibleTypes { get; } = new List<Type> {typeof(SKColor)};
public SKColor SKColorInputValue
{
get => (SKColor?) InputValue ?? new SKColor();
set => InputValue = value;
}
public override void Update()
{
NotifyOfPropertyChange(() => SKColorInputValue);
}
}
}

View File

@ -1,50 +0,0 @@
using System;
using System.Collections.Generic;
using Artemis.UI.Services.Interfaces;
using PropertyChanged;
using SkiaSharp;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput
{
public class SKPointPropertyInputViewModel : PropertyInputViewModel
{
public SKPointPropertyInputViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
{
}
public sealed override List<Type> CompatibleTypes { get; } = new List<Type> {typeof(SKPoint)};
// Since SKPoint is immutable we need to create properties that replace the SKPoint entirely
[DependsOn(nameof(InputValue))]
public float X
{
get => ((SKPoint?) InputValue)?.X ?? 0;
set => InputValue = new SKPoint(ApplyInputValue(value), Y);
}
[DependsOn(nameof(InputValue))]
public float Y
{
get => ((SKPoint?) InputValue)?.Y ?? 0;
set => InputValue = new SKPoint(X, ApplyInputValue(value));
}
public override void Update()
{
NotifyOfPropertyChange(() => X);
NotifyOfPropertyChange(() => Y);
}
private float ApplyInputValue(float value)
{
if (LayerPropertyViewModel.LayerProperty.MaxInputValue != null &&
LayerPropertyViewModel.LayerProperty.MaxInputValue is float maxFloat)
value = Math.Min(value, maxFloat);
if (LayerPropertyViewModel.LayerProperty.MinInputValue != null &&
LayerPropertyViewModel.LayerProperty.MinInputValue is float minFloat)
value = Math.Max(value, minFloat);
return value;
}
}
}

View File

@ -1,50 +0,0 @@
using System;
using System.Collections.Generic;
using Artemis.UI.Services.Interfaces;
using PropertyChanged;
using SkiaSharp;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput
{
public class SKSizePropertyInputViewModel : PropertyInputViewModel
{
public SKSizePropertyInputViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
{
}
public sealed override List<Type> CompatibleTypes { get; } = new List<Type> {typeof(SKSize)};
// Since SKSize is immutable we need to create properties that replace the SKPoint entirely
[DependsOn(nameof(InputValue))]
public float Width
{
get => ((SKSize?) InputValue)?.Width ?? 0;
set => InputValue = new SKSize(ApplyInputValue(value), Height);
}
[DependsOn(nameof(InputValue))]
public float Height
{
get => ((SKSize?) InputValue)?.Height ?? 0;
set => InputValue = new SKSize(Width, ApplyInputValue(value));
}
public override void Update()
{
NotifyOfPropertyChange(() => Width);
NotifyOfPropertyChange(() => Height);
}
private float ApplyInputValue(float value)
{
if (LayerPropertyViewModel.LayerProperty.MaxInputValue != null &&
LayerPropertyViewModel.LayerProperty.MaxInputValue is float maxFloat)
value = Math.Min(value, maxFloat);
if (LayerPropertyViewModel.LayerProperty.MinInputValue != null &&
LayerPropertyViewModel.LayerProperty.MinInputValue is float minFloat)
value = Math.Max(value, minFloat);
return value;
}
}
}

View File

@ -1,42 +0,0 @@
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree.PropertyInput;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree
{
public class PropertyTreeChildViewModel : PropertyTreeItemViewModel
{
public PropertyTreeChildViewModel(LayerPropertyViewModel layerPropertyViewModel) : base(layerPropertyViewModel)
{
PropertyInputViewModel = layerPropertyViewModel.GetPropertyInputViewModel();
}
public PropertyInputViewModel PropertyInputViewModel { get; set; }
public override void Update(bool forceUpdate)
{
if (forceUpdate)
PropertyInputViewModel?.Update();
else
{
// Only update if visible and if keyframes are enabled
if (LayerPropertyViewModel.Parent.IsExpanded && LayerPropertyViewModel.KeyframesEnabled)
PropertyInputViewModel?.Update();
}
}
public override void RemoveLayerProperty(LayerPropertyViewModel layerPropertyViewModel)
{
}
public override void AddLayerProperty(LayerPropertyViewModel layerPropertyViewModel)
{
}
public override void Dispose()
{
PropertyInputViewModel?.Dispose();
PropertyInputViewModel = null;
base.Dispose();
}
}
}

View File

@ -1,37 +0,0 @@
using System;
using Stylet;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree
{
public abstract class PropertyTreeItemViewModel : PropertyChangedBase, IDisposable
{
protected PropertyTreeItemViewModel(LayerPropertyViewModel layerPropertyViewModel)
{
LayerPropertyViewModel = layerPropertyViewModel;
}
public LayerPropertyViewModel LayerPropertyViewModel { get; }
/// <summary>
/// Updates the tree item's input if it is visible and has keyframes enabled
/// </summary>
/// <param name="forceUpdate">Force update regardless of visibility and keyframes</param>
public abstract void Update(bool forceUpdate);
/// <summary>
/// Removes the layer property recursively
/// </summary>
/// <param name="layerPropertyViewModel"></param>
public abstract void RemoveLayerProperty(LayerPropertyViewModel layerPropertyViewModel);
/// <summary>
/// Adds the layer property recursively
/// </summary>
/// <param name="layerPropertyViewModel"></param>
public abstract void AddLayerProperty(LayerPropertyViewModel layerPropertyViewModel);
public virtual void Dispose()
{
}
}
}

View File

@ -1,65 +0,0 @@
using System.Linq;
using Stylet;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree
{
public class PropertyTreeParentViewModel : PropertyTreeItemViewModel
{
public PropertyTreeParentViewModel(LayerPropertyViewModel layerPropertyViewModel) : base(layerPropertyViewModel)
{
Children = new BindableCollection<PropertyTreeItemViewModel>();
}
public BindableCollection<PropertyTreeItemViewModel> Children { get; set; }
public override void Update(bool forceUpdate)
{
foreach (var child in Children)
child.Update(forceUpdate);
}
// TODO: Change this to not add one by one, this raises far too many events
public override void AddLayerProperty(LayerPropertyViewModel layerPropertyViewModel)
{
if (layerPropertyViewModel.Parent == LayerPropertyViewModel)
{
lock (Children)
{
var index = layerPropertyViewModel.LayerProperty.Parent.Children.IndexOf(layerPropertyViewModel.LayerProperty);
if (index > Children.Count)
index = Children.Count;
if (layerPropertyViewModel.Children.Any())
Children.Insert(index, new PropertyTreeParentViewModel(layerPropertyViewModel));
else
Children.Insert(index, new PropertyTreeChildViewModel(layerPropertyViewModel));
}
}
else
{
foreach (var propertyTreeItemViewModel in Children)
propertyTreeItemViewModel.AddLayerProperty(layerPropertyViewModel);
}
}
// TODO: Change this to not remove one by one, this raises far too many events
public override void RemoveLayerProperty(LayerPropertyViewModel layerPropertyViewModel)
{
foreach (var child in Children.ToList())
{
if (child.LayerPropertyViewModel == layerPropertyViewModel)
{
Children.Remove(child);
child.Dispose();
}
else
child.RemoveLayerProperty(layerPropertyViewModel);
}
}
public override void Dispose()
{
foreach (var child in Children.ToList())
child.Dispose();
}
}
}

View File

@ -1,103 +0,0 @@
using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Artemis.UI.Events;
using Artemis.UI.Services.Interfaces;
using Stylet;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree
{
public class PropertyTreeViewModel : PropertyChangedBase, IDisposable
{
private readonly IProfileEditorService _profileEditorService;
public PropertyTreeViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
LayerPropertiesViewModel = layerPropertiesViewModel;
PropertyTreeItemViewModels = new BindableCollection<PropertyTreeItemViewModel>();
_profileEditorService.CurrentTimeChanged += OnCurrentTimeChanged;
_profileEditorService.SelectedProfileElementUpdated += OnSelectedProfileElementUpdated;
}
public LayerPropertiesViewModel LayerPropertiesViewModel { get; }
public BindableCollection<PropertyTreeItemViewModel> PropertyTreeItemViewModels { get; set; }
public void Dispose()
{
_profileEditorService.CurrentTimeChanged -= OnCurrentTimeChanged;
_profileEditorService.SelectedProfileElementUpdated -= OnSelectedProfileElementUpdated;
foreach (var propertyTreeItemViewModel in PropertyTreeItemViewModels)
propertyTreeItemViewModel.Dispose();
}
// TODO: Change this to not add one by one, this raises far too many events
public void AddLayerProperty(LayerPropertyViewModel layerPropertyViewModel)
{
// Add as a root VM
if (layerPropertyViewModel.Parent == null)
PropertyTreeItemViewModels.Add(new PropertyTreeParentViewModel(layerPropertyViewModel));
// Add recursively to one of the child VMs
else
{
foreach (var propertyTreeItemViewModel in PropertyTreeItemViewModels)
propertyTreeItemViewModel.AddLayerProperty(layerPropertyViewModel);
}
}
// TODO: Change this to not remove one by one, this raises far too many events
public void RemoveLayerProperty(LayerPropertyViewModel layerPropertyViewModel)
{
// Remove a root VM
var rootVm = PropertyTreeItemViewModels.FirstOrDefault(vm => vm.LayerPropertyViewModel == layerPropertyViewModel);
if (rootVm != null)
PropertyTreeItemViewModels.Remove(rootVm);
// Remove recursively from one of the child VMs
else
{
foreach (var propertyTreeItemViewModel in PropertyTreeItemViewModels)
propertyTreeItemViewModel.RemoveLayerProperty(layerPropertyViewModel);
}
}
/// <summary>
/// Updates the tree item's input if it is visible and has keyframes enabled
/// </summary>
/// <param name="forceUpdate">Force update regardless of visibility and keyframes</param>
public void Update(bool forceUpdate)
{
foreach (var viewModel in PropertyTreeItemViewModels)
viewModel.Update(forceUpdate);
}
public void PropertyTreePreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (e.Handled || !(sender is TreeView))
return;
e.Handled = true;
var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
{
RoutedEvent = UIElement.MouseWheelEvent,
Source = sender
};
var parent = ((Control) sender).Parent as UIElement;
parent?.RaiseEvent(eventArg);
}
private void OnCurrentTimeChanged(object sender, EventArgs e)
{
Update(false);
}
private void OnSelectedProfileElementUpdated(object sender, ProfileElementEventArgs e)
{
Update(true);
}
}
}

View File

@ -1,245 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Artemis.UI.Events;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Services.Interfaces;
using Artemis.UI.Shared.Utilities;
using Artemis.UI.Utilities;
using Stylet;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
{
public class PropertyTimelineViewModel : PropertyChangedBase, IDisposable
{
private readonly IProfileEditorService _profileEditorService;
private readonly IPropertyTrackVmFactory _propertyTrackVmFactory;
public PropertyTimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel,
IProfileEditorService profileEditorService,
IPropertyTrackVmFactory propertyTrackVmFactory)
{
_profileEditorService = profileEditorService;
_propertyTrackVmFactory = propertyTrackVmFactory;
LayerPropertiesViewModel = layerPropertiesViewModel;
PropertyTrackViewModels = new BindableCollection<PropertyTrackViewModel>();
_profileEditorService.SelectedProfileElementUpdated += OnSelectedProfileElementUpdated;
LayerPropertiesViewModel.PixelsPerSecondChanged += OnPixelsPerSecondChanged;
Execute.PostToUIThread(() => SelectionRectangle = new RectangleGeometry());
}
public LayerPropertiesViewModel LayerPropertiesViewModel { get; }
public double Width { get; set; }
public BindableCollection<PropertyTrackViewModel> PropertyTrackViewModels { get; set; }
public RectangleGeometry SelectionRectangle { get; set; }
public void Dispose()
{
_profileEditorService.SelectedProfileElementUpdated -= OnSelectedProfileElementUpdated;
LayerPropertiesViewModel.PixelsPerSecondChanged -= OnPixelsPerSecondChanged;
}
public void UpdateEndTime()
{
// End time is the last keyframe + 10 sec
var lastKeyFrame = PropertyTrackViewModels.SelectMany(r => r.KeyframeViewModels).OrderByDescending(t => t.Keyframe.Position).FirstOrDefault();
var endTime = lastKeyFrame?.Keyframe.Position.Add(new TimeSpan(0, 0, 0, 10)) ?? TimeSpan.FromSeconds(10);
Width = endTime.TotalSeconds * LayerPropertiesViewModel.PixelsPerSecond;
// Ensure the caret isn't outside the end time
if (_profileEditorService.CurrentTime > endTime)
_profileEditorService.CurrentTime = endTime;
}
public void PopulateProperties(List<LayerPropertyViewModel> properties)
{
var newViewModels = new List<PropertyTrackViewModel>();
foreach (var property in properties)
newViewModels.AddRange(CreateViewModels(property));
PropertyTrackViewModels.Clear();
PropertyTrackViewModels.AddRange(newViewModels);
UpdateEndTime();
}
public void AddLayerProperty(LayerPropertyViewModel layerPropertyViewModel)
{
// Determine the index by flattening all the layer's properties
var index = layerPropertyViewModel.LayerProperty.GetFlattenedIndex();
if (index > PropertyTrackViewModels.Count)
index = PropertyTrackViewModels.Count;
PropertyTrackViewModels.Insert(index, _propertyTrackVmFactory.Create(this, layerPropertyViewModel));
}
public void RemoveLayerProperty(LayerPropertyViewModel layerPropertyViewModel)
{
var vm = PropertyTrackViewModels.FirstOrDefault(v => v.LayerPropertyViewModel == layerPropertyViewModel);
if (vm != null)
PropertyTrackViewModels.Remove(vm);
}
public void UpdateKeyframePositions()
{
foreach (var viewModel in PropertyTrackViewModels)
viewModel.UpdateKeyframes(LayerPropertiesViewModel.PixelsPerSecond);
UpdateEndTime();
}
/// <summary>
/// Updates the time line's keyframes
/// </summary>
public void Update()
{
foreach (var viewModel in PropertyTrackViewModels)
viewModel.PopulateKeyframes();
UpdateEndTime();
}
private void OnSelectedProfileElementUpdated(object sender, ProfileElementEventArgs e)
{
Update();
}
private void OnPixelsPerSecondChanged(object sender, EventArgs e)
{
UpdateKeyframePositions();
}
private List<PropertyTrackViewModel> CreateViewModels(LayerPropertyViewModel property)
{
var result = new List<PropertyTrackViewModel> {_propertyTrackVmFactory.Create(this, property)};
foreach (var child in property.Children)
result.AddRange(CreateViewModels(child));
return result;
}
#region Keyframe movement
public void MoveSelectedKeyframes(TimeSpan cursorTime)
{
// Ensure the selection rectangle doesn't show, the view isn't aware of different types of dragging
SelectionRectangle.Rect = new Rect();
var keyframeViewModels = PropertyTrackViewModels.SelectMany(t => t.KeyframeViewModels.OrderBy(k => k.Keyframe.Position)).ToList();
foreach (var keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected))
keyframeViewModel.ApplyMovement(cursorTime);
_profileEditorService.UpdateProfilePreview();
}
public void ReleaseSelectedKeyframes()
{
var keyframeViewModels = PropertyTrackViewModels.SelectMany(t => t.KeyframeViewModels.OrderBy(k => k.Keyframe.Position)).ToList();
foreach (var keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected))
keyframeViewModel.ReleaseMovement();
}
#endregion
#region Keyframe selection
private Point _mouseDragStartPoint;
private bool _mouseDragging;
// ReSharper disable once UnusedMember.Global - Called from view
public void TimelineCanvasMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton == MouseButtonState.Released)
return;
((IInputElement) sender).CaptureMouse();
SelectionRectangle.Rect = new Rect();
_mouseDragStartPoint = e.GetPosition((IInputElement) sender);
_mouseDragging = true;
e.Handled = true;
}
// ReSharper disable once UnusedMember.Global - Called from view
public void TimelineCanvasMouseUp(object sender, MouseEventArgs e)
{
if (!_mouseDragging)
return;
var position = e.GetPosition((IInputElement) sender);
var selectedRect = new Rect(_mouseDragStartPoint, position);
SelectionRectangle.Rect = selectedRect;
var selectedKeyframes = HitTestUtilities.GetHitViewModels<PropertyTrackKeyframeViewModel>((Visual) sender, SelectionRectangle);
var keyframeViewModels = PropertyTrackViewModels.SelectMany(t => t.KeyframeViewModels.OrderBy(k => k.Keyframe.Position)).ToList();
foreach (var keyframeViewModel in keyframeViewModels)
keyframeViewModel.IsSelected = selectedKeyframes.Contains(keyframeViewModel);
_mouseDragging = false;
e.Handled = true;
((IInputElement) sender).ReleaseMouseCapture();
}
public void TimelineCanvasMouseMove(object sender, MouseEventArgs e)
{
if (_mouseDragging && e.LeftButton == MouseButtonState.Pressed)
{
var position = e.GetPosition((IInputElement) sender);
var selectedRect = new Rect(_mouseDragStartPoint, position);
SelectionRectangle.Rect = selectedRect;
e.Handled = true;
}
}
public void SelectKeyframe(PropertyTrackKeyframeViewModel clicked, bool selectBetween, bool toggle)
{
var keyframeViewModels = PropertyTrackViewModels.SelectMany(t => t.KeyframeViewModels.OrderBy(k => k.Keyframe.Position)).ToList();
if (selectBetween)
{
var selectedIndex = keyframeViewModels.FindIndex(k => k.IsSelected);
// If nothing is selected, select only the clicked
if (selectedIndex == -1)
{
clicked.IsSelected = true;
return;
}
foreach (var keyframeViewModel in keyframeViewModels)
keyframeViewModel.IsSelected = false;
var clickedIndex = keyframeViewModels.IndexOf(clicked);
if (clickedIndex < selectedIndex)
{
foreach (var keyframeViewModel in keyframeViewModels.Skip(clickedIndex).Take(selectedIndex - clickedIndex + 1))
keyframeViewModel.IsSelected = true;
}
else
{
foreach (var keyframeViewModel in keyframeViewModels.Skip(selectedIndex).Take(clickedIndex - selectedIndex + 1))
keyframeViewModel.IsSelected = true;
}
}
else if (toggle)
{
// Toggle only the clicked keyframe, leave others alone
clicked.IsSelected = !clicked.IsSelected;
}
else
{
// Only select the clicked keyframe
foreach (var keyframeViewModel in keyframeViewModels)
keyframeViewModel.IsSelected = false;
clicked.IsSelected = true;
}
}
#endregion
}
}

View File

@ -1,91 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Artemis.UI.Ninject.Factories;
using Stylet;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
{
public class PropertyTrackViewModel : Screen
{
private readonly IPropertyTrackKeyframeVmFactory _propertyTrackKeyframeVmFactory;
public PropertyTrackViewModel(PropertyTimelineViewModel propertyTimelineViewModel, LayerPropertyViewModel layerPropertyViewModel,
IPropertyTrackKeyframeVmFactory propertyTrackKeyframeVmFactory)
{
_propertyTrackKeyframeVmFactory = propertyTrackKeyframeVmFactory;
PropertyTimelineViewModel = propertyTimelineViewModel;
LayerPropertyViewModel = layerPropertyViewModel;
KeyframeViewModels = new BindableCollection<PropertyTrackKeyframeViewModel>();
PopulateKeyframes();
UpdateKeyframes(PropertyTimelineViewModel.LayerPropertiesViewModel.PixelsPerSecond);
LayerPropertyViewModel.ExpandedStateChanged += (sender, args) => UpdateMustDisplay();
LayerPropertyViewModel.LayerProperty.VisibilityChanged += (sender, args) => UpdateMustDisplay();
UpdateMustDisplay();
}
public PropertyTimelineViewModel PropertyTimelineViewModel { get; }
public LayerPropertyViewModel LayerPropertyViewModel { get; }
public BindableCollection<PropertyTrackKeyframeViewModel> KeyframeViewModels { get; set; }
public bool MustDisplay { get; set; }
private void UpdateMustDisplay()
{
var expandedTest = LayerPropertyViewModel.Parent;
while (expandedTest != null)
{
if (!expandedTest.IsExpanded)
{
MustDisplay = false;
return;
}
expandedTest = expandedTest.Parent;
}
var visibilityTest = LayerPropertyViewModel.LayerProperty;
while (visibilityTest != null)
{
if (visibilityTest.IsHidden)
{
MustDisplay = false;
return;
}
visibilityTest = visibilityTest.Parent;
}
MustDisplay = true;
}
public void PopulateKeyframes()
{
// Remove old keyframes
KeyframeViewModels.RemoveRange(KeyframeViewModels.ToList().Where(vm => !LayerPropertyViewModel.LayerProperty.UntypedKeyframes.Contains(vm.Keyframe)));
// Add new keyframes
KeyframeViewModels.AddRange(
LayerPropertyViewModel.LayerProperty.UntypedKeyframes
.Where(k => KeyframeViewModels.All(vm => vm.Keyframe != k))
.Select(k => _propertyTrackKeyframeVmFactory.Create(this, k))
);
UpdateKeyframes(PropertyTimelineViewModel.LayerPropertiesViewModel.PixelsPerSecond);
}
public void UpdateKeyframes(int pixelsPerSecond)
{
foreach (var keyframeViewModel in KeyframeViewModels)
{
keyframeViewModel.ParentView = View;
keyframeViewModel.Update(pixelsPerSecond);
}
}
protected override void OnViewLoaded()
{
foreach (var keyframeViewModel in KeyframeViewModels)
keyframeViewModel.ParentView = View;
base.OnViewLoaded();
}
}
}

View File

@ -2,24 +2,23 @@
using System.Windows.Media;
using Artemis.Core.Utilities;
using Humanizer;
using Stylet;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
{
public class PropertyTrackEasingViewModel : PropertyChangedBase
public class TimelineEasingViewModel
{
private readonly PropertyTrackKeyframeViewModel _keyframeViewModel;
private readonly TimelineKeyframeViewModel _keyframeViewModel;
private bool _isEasingModeSelected;
public PropertyTrackEasingViewModel(PropertyTrackKeyframeViewModel keyframeViewModel, Easings.Functions easingFunction)
public TimelineEasingViewModel(TimelineKeyframeViewModel keyframeViewModel, Easings.Functions easingFunction)
{
_keyframeViewModel = keyframeViewModel;
_isEasingModeSelected = keyframeViewModel.Keyframe.EasingFunction == easingFunction;
_isEasingModeSelected = keyframeViewModel.BaseLayerPropertyKeyframe.EasingFunction == easingFunction;
EasingFunction = easingFunction;
Description = easingFunction.Humanize();
CreateGeometry();
CreateEasingPoints();
}
public Easings.Functions EasingFunction { get; }
@ -37,7 +36,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
}
}
private void CreateGeometry()
private void CreateEasingPoints()
{
EasingPoints = new PointCollection();
for (var i = 1; i <= 10; i++)

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Input;
@ -10,37 +9,71 @@ using Stylet;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
{
public class PropertyTrackKeyframeViewModel : PropertyChangedBase
public class TimelineKeyframeViewModel<T> : TimelineKeyframeViewModel
{
private readonly IProfileEditorService _profileEditorService;
private int _pixelsPerSecond;
public PropertyTrackKeyframeViewModel(PropertyTrackViewModel propertyTrackViewModel, BaseKeyframe keyframe, IProfileEditorService profileEditorService)
public TimelineKeyframeViewModel(IProfileEditorService profileEditorService, TimelineViewModel timelineViewModel, LayerPropertyKeyframe<T> layerPropertyKeyframe)
: base(profileEditorService, timelineViewModel, layerPropertyKeyframe)
{
_profileEditorService = profileEditorService;
PropertyTrackViewModel = propertyTrackViewModel;
Keyframe = keyframe;
EasingViewModels = new BindableCollection<PropertyTrackEasingViewModel>();
CreateEasingViewModels();
LayerPropertyKeyframe = layerPropertyKeyframe;
}
public LayerPropertyKeyframe<T> LayerPropertyKeyframe { get; }
#region Context menu actions
public void Copy()
{
var newKeyframe = new LayerPropertyKeyframe<T>(
LayerPropertyKeyframe.Value,
LayerPropertyKeyframe.Position,
LayerPropertyKeyframe.EasingFunction,
LayerPropertyKeyframe.LayerProperty
);
LayerPropertyKeyframe.LayerProperty.AddKeyframe(newKeyframe);
_profileEditorService.UpdateSelectedProfileElement();
}
public void Delete()
{
LayerPropertyKeyframe.LayerProperty.RemoveKeyframe(LayerPropertyKeyframe);
_profileEditorService.UpdateSelectedProfileElement();
}
#endregion
}
public abstract class TimelineKeyframeViewModel : PropertyChangedBase
{
private readonly IProfileEditorService _profileEditorService;
private readonly TimelineViewModel _timelineViewModel;
private int _pixelsPerSecond;
protected TimelineKeyframeViewModel(IProfileEditorService profileEditorService, TimelineViewModel timelineViewModel, BaseLayerPropertyKeyframe baseLayerPropertyKeyframe)
{
_profileEditorService = profileEditorService;
_timelineViewModel = timelineViewModel;
BaseLayerPropertyKeyframe = baseLayerPropertyKeyframe;
EasingViewModels = new BindableCollection<TimelineEasingViewModel>();
}
public BaseLayerPropertyKeyframe BaseLayerPropertyKeyframe { get; }
public BindableCollection<TimelineEasingViewModel> EasingViewModels { get; set; }
public bool IsSelected { get; set; }
public PropertyTrackViewModel PropertyTrackViewModel { get; }
public BaseKeyframe Keyframe { get; }
public BindableCollection<PropertyTrackEasingViewModel> EasingViewModels { get; set; }
public double X { get; set; }
public string Timestamp { get; set; }
public UIElement ParentView { get; set; }
public void Update(int pixelsPerSecond)
{
_pixelsPerSecond = pixelsPerSecond;
X = pixelsPerSecond * Keyframe.Position.TotalSeconds;
Timestamp = $"{Math.Floor(Keyframe.Position.TotalSeconds):00}.{Keyframe.Position.Milliseconds:000}";
X = pixelsPerSecond * BaseLayerPropertyKeyframe.Position.TotalSeconds;
Timestamp = $"{Math.Floor(BaseLayerPropertyKeyframe.Position.TotalSeconds):00}.{BaseLayerPropertyKeyframe.Position.Milliseconds:000}";
}
#region Keyframe movement
@ -52,11 +85,11 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
((IInputElement) sender).CaptureMouse();
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift) && !IsSelected)
PropertyTrackViewModel.PropertyTimelineViewModel.SelectKeyframe(this, true, false);
_timelineViewModel.SelectKeyframe(this, true, false);
else if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
PropertyTrackViewModel.PropertyTimelineViewModel.SelectKeyframe(this, false, true);
_timelineViewModel.SelectKeyframe(this, false, true);
else if (!IsSelected)
PropertyTrackViewModel.PropertyTimelineViewModel.SelectKeyframe(this, false, false);
_timelineViewModel.SelectKeyframe(this, false, false);
e.Handled = true;
}
@ -64,7 +97,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
public void KeyframeMouseUp(object sender, MouseButtonEventArgs e)
{
_profileEditorService.UpdateSelectedProfileElement();
PropertyTrackViewModel.PropertyTimelineViewModel.ReleaseSelectedKeyframes();
_timelineViewModel.ReleaseSelectedKeyframes();
((IInputElement) sender).ReleaseMouseCapture();
}
@ -72,7 +105,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
public void KeyframeMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
PropertyTrackViewModel.PropertyTimelineViewModel.MoveSelectedKeyframes(GetCursorTime(e.GetPosition(ParentView)));
_timelineViewModel.MoveSelectedKeyframes(GetCursorTime(e.GetPosition(ParentView)));
e.Handled = true;
}
@ -105,37 +138,31 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
#endregion
#region Context menu actions
public void Copy()
{
var keyframe = PropertyTrackViewModel.LayerPropertyViewModel.LayerProperty.CreateNewKeyframe(Keyframe.Position, Keyframe.BaseValue);
keyframe.EasingFunction = Keyframe.EasingFunction;
_profileEditorService.UpdateSelectedProfileElement();
}
public void Delete()
{
PropertyTrackViewModel.LayerPropertyViewModel.LayerProperty.RemoveKeyframe(Keyframe);
_profileEditorService.UpdateSelectedProfileElement();
}
#endregion
#region Easing
public void ContextMenuOpening()
{
CreateEasingViewModels();
}
public void ContextMenuClosing()
{
EasingViewModels.Clear();
}
private void CreateEasingViewModels()
{
EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)).Cast<Easings.Functions>().Select(v => new PropertyTrackEasingViewModel(this, v)));
EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)).Cast<Easings.Functions>().Select(v => new TimelineEasingViewModel(this, v)));
}
public void SelectEasingMode(PropertyTrackEasingViewModel easingViewModel)
public void SelectEasingMode(TimelineEasingViewModel easingViewModel)
{
Keyframe.EasingFunction = easingViewModel.EasingFunction;
BaseLayerPropertyKeyframe.EasingFunction = easingViewModel.EasingFunction;
// Set every selection to false except on the VM that made the change
foreach (var propertyTrackEasingViewModel in EasingViewModels.Where(vm => vm != easingViewModel))
propertyTrackEasingViewModel.IsEasingModeSelected = false;
_profileEditorService.UpdateSelectedProfileElement();
}
@ -151,13 +178,13 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
if (_movementReleased)
{
_movementReleased = false;
_startOffset = cursorTime - Keyframe.Position;
_startOffset = cursorTime - BaseLayerPropertyKeyframe.Position;
}
else
{
Keyframe.Position = cursorTime - _startOffset;
if (Keyframe.Position < TimeSpan.Zero)
Keyframe.Position = TimeSpan.Zero;
BaseLayerPropertyKeyframe.Position = cursorTime - _startOffset;
if (BaseLayerPropertyKeyframe.Position < TimeSpan.Zero)
BaseLayerPropertyKeyframe.Position = TimeSpan.Zero;
Update(_pixelsPerSecond);
}

View File

@ -0,0 +1,71 @@
<UserControl x:Class="Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline.TimelinePropertyGroupView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:layerProperties="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:TimelinePropertyGroupViewModel}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="24" />
<RowDefinition Height="1" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ItemsControl Grid.Row="0"
Height="24"
Visibility="{Binding LayerPropertyGroupViewModel.IsExpanded, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}"
ItemsSource="{Binding TimelineKeyframeViewModels}"
Background="{DynamicResource MaterialDesignToolBarBackground}"
HorizontalAlignment="Left">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type ContentPresenter}">
<Setter Property="Canvas.Left" Value="{Binding}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Fill="{StaticResource MaterialDesignCheckBoxDisabled}"
Stroke="White"
StrokeThickness="0"
Width="10"
Height="10"
Margin="-5,6,0,0">
</Ellipse>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Rectangle Grid.Row="1" HorizontalAlignment="Stretch" Fill="{DynamicResource MaterialDesignDivider}" Height="1" />
<ItemsControl Grid.Row="2"
Visibility="{Binding LayerPropertyGroupViewModel.IsExpanded, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"
ItemsSource="{Binding LayerPropertyGroupViewModel.Children}">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type layerProperties:LayerPropertyGroupViewModel}">
<ContentControl s:View.Model="{Binding TimelinePropertyGroupViewModel}"
VerticalContentAlignment="Stretch"
HorizontalContentAlignment="Stretch"
IsTabStop="False" />
</DataTemplate>
<DataTemplate DataType="{x:Type layerProperties:LayerPropertyViewModel}">
<ContentControl s:View.Model="{Binding TimelinePropertyBaseViewModel}"
VerticalContentAlignment="Stretch"
HorizontalContentAlignment="Stretch"
IsTabStop="False" />
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
</Grid>
</UserControl>

View File

@ -0,0 +1,50 @@
using System;
using System.ComponentModel;
using System.Linq;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Abstract;
using Stylet;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
{
public class TimelinePropertyGroupViewModel
{
public TimelinePropertyGroupViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel)
{
LayerPropertyGroupViewModel = (LayerPropertyGroupViewModel) layerPropertyBaseViewModel;
TimelineKeyframeViewModels = new BindableCollection<double>();
LayerPropertyGroupViewModel.ProfileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
LayerPropertyGroupViewModel.PropertyChanged += LayerPropertyGroupViewModelOnPropertyChanged;
UpdateKeyframes();
}
public LayerPropertyGroupViewModel LayerPropertyGroupViewModel { get; }
public BindableCollection<double> TimelineKeyframeViewModels { get; set; }
public TimelineViewModel TimelineViewModel { get; set; }
public void UpdateKeyframes()
{
TimelineKeyframeViewModels.Clear();
TimelineKeyframeViewModels.AddRange(LayerPropertyGroupViewModel.GetKeyframes(false)
.Select(k => LayerPropertyGroupViewModel.ProfileEditorService.PixelsPerSecond * k.Position.TotalSeconds));
}
public void Dispose()
{
LayerPropertyGroupViewModel.ProfileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
LayerPropertyGroupViewModel.PropertyChanged -= LayerPropertyGroupViewModelOnPropertyChanged;
}
private void LayerPropertyGroupViewModelOnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(LayerPropertyGroupViewModel.IsExpanded))
UpdateKeyframes();
}
private void ProfileEditorServiceOnPixelsPerSecondChanged(object? sender, EventArgs e)
{
UpdateKeyframes();
}
}
}

View File

@ -1,4 +1,4 @@
<UserControl x:Class="Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline.PropertyTrackView"
<UserControl x:Class="Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline.TimelinePropertyView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@ -7,13 +7,10 @@
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
mc:Ignorable="d"
d:DesignHeight="20" d:DesignWidth="600"
d:DataContext="{d:DesignInstance local:PropertyTrackViewModel}">
<Border Height="25"
BorderThickness="0,0,0,1"
BorderBrush="{DynamicResource MaterialDesignDivider}"
Visibility="{Binding MustDisplay, Converter={StaticResource BoolToVisibilityConverter}}">
<ItemsControl ItemsSource="{Binding KeyframeViewModels}"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:TimelinePropertyViewModel}">
<Border Height="25" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource MaterialDesignDivider}">
<ItemsControl ItemsSource="{Binding TimelineKeyframeViewModels}"
Background="{DynamicResource MaterialDesignToolBarBackground}"
HorizontalAlignment="Left">
<ItemsControl.ItemsPanel>
@ -38,7 +35,9 @@
s:View.ActionTarget="{Binding}"
MouseDown="{s:Action KeyframeMouseDown}"
MouseUp="{s:Action KeyframeMouseUp}"
MouseMove="{s:Action KeyframeMouseMove}">
MouseMove="{s:Action KeyframeMouseMove}"
ContextMenuOpening="{s:Action ContextMenuOpening}"
ContextMenuClosing="{s:Action ContextMenuClosing}">
<Ellipse.Style>
<Style TargetType="{x:Type Ellipse}">
<Style.Triggers>
@ -75,6 +74,9 @@
</MenuItem>
<Separator />
<MenuItem Header="Easing" ItemsSource="{Binding EasingViewModels}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Creation" />
</MenuItem.Icon>
<MenuItem.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource MaterialDesignMenuItem}">
<Setter Property="IsCheckable" Value="True" />

View File

@ -0,0 +1,88 @@
using System;
using System.Linq;
using Artemis.UI.Exceptions;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Abstract;
using Artemis.UI.Services.Interfaces;
using Stylet;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
{
public class TimelinePropertyViewModel<T> : TimelinePropertyViewModel
{
private readonly IProfileEditorService _profileEditorService;
public TimelinePropertyViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel, IProfileEditorService profileEditorService) : base(layerPropertyBaseViewModel)
{
_profileEditorService = profileEditorService;
LayerPropertyViewModel = (LayerPropertyViewModel<T>) layerPropertyBaseViewModel;
LayerPropertyViewModel.LayerProperty.KeyframeAdded += LayerPropertyOnKeyframeModified;
LayerPropertyViewModel.LayerProperty.KeyframeRemoved += LayerPropertyOnKeyframeModified;
LayerPropertyViewModel.LayerProperty.KeyframesToggled += LayerPropertyOnKeyframeModified;
_profileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
}
private void LayerPropertyOnKeyframeModified(object sender, EventArgs e)
{
UpdateKeyframes();
}
private void ProfileEditorServiceOnPixelsPerSecondChanged(object? sender, EventArgs e)
{
foreach (var timelineKeyframeViewModel in TimelineKeyframeViewModels)
timelineKeyframeViewModel.Update(_profileEditorService.PixelsPerSecond);
}
public LayerPropertyViewModel<T> LayerPropertyViewModel { get; }
public override void UpdateKeyframes()
{
if (TimelineViewModel == null)
throw new ArtemisUIException("Timeline view model must be set before keyframes can be updated");
// Only show keyframes if they are enabled
if (LayerPropertyViewModel.LayerProperty.KeyframesEnabled)
{
var keyframes = LayerPropertyViewModel.LayerProperty.Keyframes.ToList();
var toRemove = TimelineKeyframeViewModels.Where(t => !keyframes.Contains(t.BaseLayerPropertyKeyframe)).ToList();
TimelineKeyframeViewModels.RemoveRange(toRemove);
TimelineKeyframeViewModels.AddRange(
keyframes.Where(k => TimelineKeyframeViewModels.All(t => t.BaseLayerPropertyKeyframe != k))
.Select(k => new TimelineKeyframeViewModel<T>(_profileEditorService, TimelineViewModel, k))
);
}
else
{
TimelineKeyframeViewModels.Clear();
}
foreach (var timelineKeyframeViewModel in TimelineKeyframeViewModels)
timelineKeyframeViewModel.Update(_profileEditorService.PixelsPerSecond);
}
public override void Dispose()
{
_profileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
LayerPropertyViewModel.LayerProperty.KeyframeAdded -= LayerPropertyOnKeyframeModified;
LayerPropertyViewModel.LayerProperty.KeyframeRemoved -= LayerPropertyOnKeyframeModified;
LayerPropertyViewModel.LayerProperty.KeyframesToggled -= LayerPropertyOnKeyframeModified;
}
}
public abstract class TimelinePropertyViewModel : IDisposable
{
protected TimelinePropertyViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel)
{
LayerPropertyBaseViewModel = layerPropertyBaseViewModel;
TimelineKeyframeViewModels = new BindableCollection<TimelineKeyframeViewModel>();
}
public LayerPropertyBaseViewModel LayerPropertyBaseViewModel { get; }
public TimelineViewModel TimelineViewModel { get; set; }
public BindableCollection<TimelineKeyframeViewModel> TimelineKeyframeViewModels { get; set; }
public abstract void UpdateKeyframes();
public abstract void Dispose();
}
}

View File

@ -1,4 +1,4 @@
<UserControl x:Class="Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline.PropertyTimelineView"
<UserControl x:Class="Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline.TimelineView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@ -8,11 +8,11 @@
mc:Ignorable="d"
d:DesignHeight="25"
d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:PropertyTimelineViewModel}">
d:DataContext="{d:DesignInstance local:TimelineViewModel}">
<Grid Background="{DynamicResource MaterialDesignToolBarBackground}"
MouseDown="{s:Action TimelineCanvasMouseDown}"
MouseUp="{s:Action TimelineCanvasMouseUp}"
MouseMove="{s:Action TimelineCanvasMouseMove}">
MouseDown="{s:Action TimelineCanvasMouseDown}"
MouseUp="{s:Action TimelineCanvasMouseUp}"
MouseMove="{s:Action TimelineCanvasMouseMove}">
<Grid.Triggers>
<EventTrigger RoutedEvent="UIElement.MouseLeftButtonDown">
<BeginStoryboard>
@ -30,22 +30,23 @@
</EventTrigger>
</Grid.Triggers>
<ItemsControl ItemsSource="{Binding PropertyTrackViewModels}"
<ItemsControl ItemsSource="{Binding LayerPropertyGroups}"
Width="{Binding Width}"
MinWidth="{Binding ActualWidth, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ScrollViewer}}"
HorizontalAlignment="Left">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl s:View.Model="{Binding}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" />
<ContentControl s:View.Model="{Binding TimelinePropertyGroupViewModel}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Multi-selection rectangle -->
<Path Data="{Binding SelectionRectangle}" Opacity="0"
<Path x:Name="MultiSelectionPath"
Data="{Binding SelectionRectangle}"
Opacity="0"
Stroke="{DynamicResource PrimaryHueLightBrush}"
StrokeThickness="1"
x:Name="MultiSelectionPath"
IsHitTestVisible="False">
<Path.Fill>
<SolidColorBrush Color="{DynamicResource Primary400}" Opacity="0.25" />

Some files were not shown because too many files have changed in this diff Show More