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:
commit
5978dfcc94
@ -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" />
|
||||
|
||||
20
src/Artemis.Core/Events/PropertyGroupUpdatingEventArgs.cs
Normal file
20
src/Artemis.Core/Events/PropertyGroupUpdatingEventArgs.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
31
src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs
Normal file
31
src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs
Normal 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
|
||||
}
|
||||
}
|
||||
11
src/Artemis.Core/Models/Profile/Conditions/LayerCondition.cs
Normal file
11
src/Artemis.Core/Models/Profile/Conditions/LayerCondition.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
32
src/Artemis.Core/Models/Profile/LayerGeneralProperties.cs
Normal file
32
src/Artemis.Core/Models/Profile/LayerGeneralProperties.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
245
src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs
Normal file
245
src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs
Normal 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
|
||||
}
|
||||
}
|
||||
34
src/Artemis.Core/Models/Profile/LayerTransformProperties.cs
Normal file
34
src/Artemis.Core/Models/Profile/LayerTransformProperties.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>();
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
69
src/Artemis.Core/Plugins/Abstract/DataModels/DataModel.cs
Normal file
69
src/Artemis.Core/Plugins/Abstract/DataModels/DataModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
namespace Artemis.Core.Plugins.Abstract
|
||||
{
|
||||
public abstract class ModuleDataModel
|
||||
{
|
||||
protected ModuleDataModel(Module module)
|
||||
{
|
||||
Module = module;
|
||||
}
|
||||
|
||||
public Module Module { get; }
|
||||
}
|
||||
}
|
||||
64
src/Artemis.Core/Plugins/LayerBrush/BaseLayerBrush.cs
Normal file
64
src/Artemis.Core/Plugins/LayerBrush/BaseLayerBrush.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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>
|
||||
@ -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; }
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
using LiteDB;
|
||||
|
||||
namespace Artemis.Storage.Migrations.Interfaces
|
||||
{
|
||||
public interface IStorageMigration
|
||||
{
|
||||
int UserVersion { get; }
|
||||
void Apply(LiteRepository repository);
|
||||
}
|
||||
}
|
||||
38
src/Artemis.Storage/StorageMigrationService.cs
Normal file
38
src/Artemis.Storage/StorageMigrationService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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" />
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
@ -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}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.Core.Models.Profile.Colors;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.Interfaces
|
||||
{
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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>();
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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++)
|
||||
@ -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);
|
||||
}
|
||||
@ -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>
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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" />
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user