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>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Ben.Demystifier" Version="0.1.6" />
|
<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="HidSharp" Version="2.1.0" />
|
||||||
<PackageReference Include="LiteDB" Version="5.0.7" />
|
<PackageReference Include="LiteDB" Version="5.0.8" />
|
||||||
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.2.0" />
|
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.3.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
<PackageReference Include="Ninject" Version="3.3.4" />
|
<PackageReference Include="Ninject" Version="3.3.4" />
|
||||||
<PackageReference Include="Ninject.Extensions.ChildKernel" Version="3.3.0" />
|
<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.Enrichers.Demystify" Version="1.0.0-dev-00019" />
|
||||||
<PackageReference Include="Serilog.Sinks.Debug" Version="1.0.1" />
|
<PackageReference Include="Serilog.Sinks.Debug" Version="1.0.1" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
|
||||||
<PackageReference Include="SkiaSharp" Version="1.68.2-preview.29" />
|
<PackageReference Include="SkiaSharp" Version="1.68.3" />
|
||||||
<PackageReference Include="Stylet" Version="1.3.1" />
|
<PackageReference Include="Stylet" Version="1.3.2" />
|
||||||
<PackageReference Include="System.Buffers" Version="4.5.0" />
|
<PackageReference Include="System.Buffers" Version="4.5.0" />
|
||||||
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
|
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
|
||||||
<PackageReference Include="System.Reflection.Metadata" Version="1.8.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 SkiaSharp;
|
||||||
using Stylet;
|
using Stylet;
|
||||||
|
|
||||||
namespace Artemis.Core.Models.Profile
|
namespace Artemis.Core.Models.Profile.Colors
|
||||||
{
|
{
|
||||||
public class ColorGradient : INotifyPropertyChanged
|
public class ColorGradient : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
@ -55,7 +55,7 @@ namespace Artemis.Core.Models.Profile
|
|||||||
|
|
||||||
if (right == null || left == right)
|
if (right == null || left == right)
|
||||||
return left.Color;
|
return left.Color;
|
||||||
|
|
||||||
position = (float) Math.Round((position - left.Position) / (right.Position - left.Position), 2);
|
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 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);
|
var r = (byte) ((right.Color.Red - left.Color.Red) * position + left.Color.Red);
|
||||||
@ -65,15 +65,19 @@ namespace Artemis.Core.Models.Profile
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// [PH] Looping through HSV, adds 8 rainbow colors
|
/// Gets a new ColorGradient with colors looping through the HSV-spectrum
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void MakeFabulous()
|
/// <returns></returns>
|
||||||
|
public static ColorGradient GetUnicornBarf()
|
||||||
{
|
{
|
||||||
|
var gradient = new ColorGradient();
|
||||||
for (var i = 0; i < 9; i++)
|
for (var i = 0; i < 9; i++)
|
||||||
{
|
{
|
||||||
var color = i != 8 ? SKColor.FromHsv(i * 32, 100, 100) : SKColor.FromHsv(0, 100, 100);
|
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
|
#region PropertyChanged
|
||||||
@ -88,28 +92,4 @@ namespace Artemis.Core.Models.Profile
|
|||||||
|
|
||||||
#endregion
|
#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 System.Linq;
|
||||||
using Artemis.Core.Extensions;
|
using Artemis.Core.Extensions;
|
||||||
using Artemis.Core.Models.Profile.LayerProperties;
|
using Artemis.Core.Models.Profile.LayerProperties;
|
||||||
|
using Artemis.Core.Models.Profile.LayerProperties.Attributes;
|
||||||
using Artemis.Core.Models.Profile.LayerShapes;
|
using Artemis.Core.Models.Profile.LayerShapes;
|
||||||
using Artemis.Core.Models.Surface;
|
using Artemis.Core.Models.Surface;
|
||||||
using Artemis.Core.Plugins.LayerBrush;
|
using Artemis.Core.Plugins.LayerBrush;
|
||||||
|
using Artemis.Core.Services;
|
||||||
|
using Artemis.Core.Services.Interfaces;
|
||||||
using Artemis.Storage.Entities.Profile;
|
using Artemis.Storage.Entities.Profile;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace Artemis.Core.Models.Profile
|
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
|
public sealed class Layer : ProfileElement
|
||||||
{
|
{
|
||||||
|
private readonly List<string> _expandedPropertyGroups;
|
||||||
private LayerShape _layerShape;
|
private LayerShape _layerShape;
|
||||||
private List<ArtemisLed> _leds;
|
private List<ArtemisLed> _leds;
|
||||||
private SKPath _path;
|
private SKPath _path;
|
||||||
|
|
||||||
public Layer(Profile profile, ProfileElement parent, string name)
|
internal Layer(Profile profile, ProfileElement parent, string name)
|
||||||
{
|
{
|
||||||
LayerEntity = new LayerEntity();
|
LayerEntity = new LayerEntity();
|
||||||
EntityId = Guid.NewGuid();
|
EntityId = Guid.NewGuid();
|
||||||
@ -26,12 +34,13 @@ namespace Artemis.Core.Models.Profile
|
|||||||
Profile = profile;
|
Profile = profile;
|
||||||
Parent = parent;
|
Parent = parent;
|
||||||
Name = name;
|
Name = name;
|
||||||
Properties = new LayerPropertyCollection(this);
|
General = new LayerGeneralProperties {IsCorePropertyGroup = true};
|
||||||
|
Transform = new LayerTransformProperties {IsCorePropertyGroup = true};
|
||||||
|
|
||||||
_leds = new List<ArtemisLed>();
|
_leds = new List<ArtemisLed>();
|
||||||
|
_expandedPropertyGroups = new List<string>();
|
||||||
|
|
||||||
ApplyShapeType();
|
General.PropertyGroupInitialized += GeneralOnPropertyGroupInitialized;
|
||||||
Properties.ShapeType.ValueChanged += (sender, args) => ApplyShapeType();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity)
|
internal Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity)
|
||||||
@ -43,12 +52,14 @@ namespace Artemis.Core.Models.Profile
|
|||||||
Parent = parent;
|
Parent = parent;
|
||||||
Name = layerEntity.Name;
|
Name = layerEntity.Name;
|
||||||
Order = layerEntity.Order;
|
Order = layerEntity.Order;
|
||||||
Properties = new LayerPropertyCollection(this);
|
General = new LayerGeneralProperties {IsCorePropertyGroup = true};
|
||||||
|
Transform = new LayerTransformProperties {IsCorePropertyGroup = true};
|
||||||
|
|
||||||
_leds = new List<ArtemisLed>();
|
_leds = new List<ArtemisLed>();
|
||||||
|
_expandedPropertyGroups = new List<string>();
|
||||||
|
_expandedPropertyGroups.AddRange(layerEntity.ExpandedPropertyGroups);
|
||||||
|
|
||||||
ApplyShapeType();
|
General.PropertyGroupInitialized += GeneralOnPropertyGroupInitialized;
|
||||||
Properties.ShapeType.ValueChanged += (sender, args) => ApplyShapeType();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal LayerEntity LayerEntity { get; set; }
|
internal LayerEntity LayerEntity { get; set; }
|
||||||
@ -93,21 +104,35 @@ namespace Artemis.Core.Models.Profile
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
[PropertyGroupDescription(Name = "General", Description = "A collection of general properties")]
|
||||||
/// The properties of this layer
|
public LayerGeneralProperties General { get; set; }
|
||||||
/// </summary>
|
|
||||||
public LayerPropertyCollection Properties { get; set; }
|
[PropertyGroupDescription(Name = "Transform", Description = "A collection of transformation properties")]
|
||||||
|
public LayerTransformProperties Transform { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The brush that will fill the <see cref="LayerShape" />.
|
/// The brush that will fill the <see cref="LayerShape" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public LayerBrush LayerBrush { get; internal set; }
|
public BaseLayerBrush LayerBrush { get; internal set; }
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"[Layer] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}";
|
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
|
#region Storage
|
||||||
|
|
||||||
internal override void ApplyToEntity()
|
internal override void ApplyToEntity()
|
||||||
@ -118,8 +143,12 @@ namespace Artemis.Core.Models.Profile
|
|||||||
LayerEntity.Order = Order;
|
LayerEntity.Order = Order;
|
||||||
LayerEntity.Name = Name;
|
LayerEntity.Name = Name;
|
||||||
LayerEntity.ProfileId = Profile.EntityId;
|
LayerEntity.ProfileId = Profile.EntityId;
|
||||||
foreach (var layerProperty in Properties)
|
LayerEntity.ExpandedPropertyGroups.Clear();
|
||||||
layerProperty.ApplyToEntity();
|
LayerEntity.ExpandedPropertyGroups.AddRange(_expandedPropertyGroups);
|
||||||
|
|
||||||
|
General.ApplyToEntity();
|
||||||
|
Transform.ApplyToEntity();
|
||||||
|
LayerBrush?.BaseProperties.ApplyToEntity();
|
||||||
|
|
||||||
// LEDs
|
// LEDs
|
||||||
LayerEntity.Leds.Clear();
|
LayerEntity.Leds.Clear();
|
||||||
@ -141,9 +170,21 @@ namespace Artemis.Core.Models.Profile
|
|||||||
|
|
||||||
#region Shape management
|
#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()
|
private void ApplyShapeType()
|
||||||
{
|
{
|
||||||
switch (Properties.ShapeType.CurrentValue)
|
switch (General.ShapeType.CurrentValue)
|
||||||
{
|
{
|
||||||
case LayerShapeType.Ellipse:
|
case LayerShapeType.Ellipse:
|
||||||
LayerShape = new Ellipse(this);
|
LayerShape = new Ellipse(this);
|
||||||
@ -163,28 +204,43 @@ namespace Artemis.Core.Models.Profile
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Update(double deltaTime)
|
public override void Update(double deltaTime)
|
||||||
{
|
{
|
||||||
foreach (var property in Properties)
|
if (LayerBrush == null || !LayerBrush.BaseProperties.PropertiesInitialized)
|
||||||
property.KeyframeEngine?.Update(deltaTime);
|
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
|
// 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
|
// 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();
|
var timeLineEnd = properties.Any() ? properties.Max(p => p.BaseKeyframes.Max(k => k.Position)) : TimeSpan.MaxValue;
|
||||||
if (lastKeyframe != null)
|
if (properties.Any(p => p.TimelineProgress >= timeLineEnd))
|
||||||
{
|
{
|
||||||
if (Properties.Any(p => p.KeyframeEngine?.Progress > lastKeyframe.Position))
|
General.Override(TimeSpan.Zero);
|
||||||
{
|
Transform.Override(TimeSpan.Zero);
|
||||||
foreach (var baseLayerProperty in Properties)
|
LayerBrush.BaseProperties.Override(TimeSpan.Zero);
|
||||||
baseLayerProperty.KeyframeEngine?.OverrideProgress(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 />
|
/// <inheritdoc />
|
||||||
public override void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
|
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;
|
return;
|
||||||
|
|
||||||
canvas.Save();
|
canvas.Save();
|
||||||
@ -192,10 +248,10 @@ namespace Artemis.Core.Models.Profile
|
|||||||
|
|
||||||
using (var paint = new SKPaint())
|
using (var paint = new SKPaint())
|
||||||
{
|
{
|
||||||
paint.BlendMode = Properties.BlendMode.CurrentValue;
|
paint.BlendMode = General.BlendMode.CurrentValue;
|
||||||
paint.Color = new SKColor(0, 0, 0, (byte) (Properties.Opacity.CurrentValue * 2.55f));
|
paint.Color = new SKColor(0, 0, 0, (byte) (Transform.Opacity.CurrentValue * 2.55f));
|
||||||
|
|
||||||
switch (Properties.FillType.CurrentValue)
|
switch (General.FillType.CurrentValue)
|
||||||
{
|
{
|
||||||
case LayerFillType.Stretch:
|
case LayerFillType.Stretch:
|
||||||
StretchRender(canvas, canvasInfo, paint);
|
StretchRender(canvas, canvasInfo, paint);
|
||||||
@ -214,11 +270,11 @@ namespace Artemis.Core.Models.Profile
|
|||||||
private void StretchRender(SKCanvas canvas, SKImageInfo canvasInfo, SKPaint paint)
|
private void StretchRender(SKCanvas canvas, SKImageInfo canvasInfo, SKPaint paint)
|
||||||
{
|
{
|
||||||
// Apply transformations
|
// Apply transformations
|
||||||
var sizeProperty = Properties.Scale.CurrentValue;
|
var sizeProperty = Transform.Scale.CurrentValue;
|
||||||
var rotationProperty = Properties.Rotation.CurrentValue;
|
var rotationProperty = Transform.Rotation.CurrentValue;
|
||||||
|
|
||||||
var anchorPosition = GetLayerAnchorPosition();
|
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
|
// 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;
|
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.Scale(sizeProperty.Width / 100f, sizeProperty.Height / 100f, anchorPosition.X, anchorPosition.Y);
|
||||||
canvas.Translate(x, 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)
|
private void ClipRender(SKCanvas canvas, SKImageInfo canvasInfo, SKPaint paint)
|
||||||
{
|
{
|
||||||
// Apply transformations
|
// Apply transformations
|
||||||
var sizeProperty = Properties.Scale.CurrentValue;
|
var sizeProperty = Transform.Scale.CurrentValue;
|
||||||
var rotationProperty = Properties.Rotation.CurrentValue;
|
var rotationProperty = Transform.Rotation.CurrentValue;
|
||||||
|
|
||||||
var anchorPosition = GetLayerAnchorPosition();
|
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
|
// 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;
|
var x = anchorPosition.X - Bounds.MidX - anchorProperty.X * Bounds.Width;
|
||||||
@ -292,7 +349,7 @@ namespace Artemis.Core.Models.Profile
|
|||||||
|
|
||||||
internal SKPoint GetLayerAnchorPosition()
|
internal SKPoint GetLayerAnchorPosition()
|
||||||
{
|
{
|
||||||
var positionProperty = Properties.Position.CurrentValue;
|
var positionProperty = Transform.Position.CurrentValue;
|
||||||
|
|
||||||
// Start at the center of the shape
|
// Start at the center of the shape
|
||||||
var position = new SKPoint(Bounds.MidX, Bounds.MidY);
|
var position = new SKPoint(Bounds.MidX, Bounds.MidY);
|
||||||
@ -371,6 +428,7 @@ namespace Artemis.Core.Models.Profile
|
|||||||
|
|
||||||
public event EventHandler RenderPropertiesUpdated;
|
public event EventHandler RenderPropertiesUpdated;
|
||||||
public event EventHandler ShapePropertiesUpdated;
|
public event EventHandler ShapePropertiesUpdated;
|
||||||
|
public event EventHandler LayerBrushUpdated;
|
||||||
|
|
||||||
private void OnRenderPropertiesUpdated()
|
private void OnRenderPropertiesUpdated()
|
||||||
{
|
{
|
||||||
@ -382,6 +440,11 @@ namespace Artemis.Core.Models.Profile
|
|||||||
ShapePropertiesUpdated?.Invoke(this, EventArgs.Empty);
|
ShapePropertiesUpdated?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void OnLayerBrushUpdated()
|
||||||
|
{
|
||||||
|
LayerBrushUpdated?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#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;
|
||||||
using System.Collections.Generic;
|
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 Artemis.Storage.Entities.Profile;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Stylet;
|
|
||||||
|
|
||||||
namespace Artemis.Core.Models.Profile.LayerProperties
|
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;
|
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>
|
/// <summary>
|
||||||
/// Gets the layer this property applies to
|
/// The layer this property applies to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Layer Layer { get; }
|
public Layer Layer { get; internal set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Info of the plugin associated with this property
|
/// The parent group of this layer property, set after construction
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public PluginInfo PluginInfo { get; }
|
public LayerPropertyGroup Parent { get; internal set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the parent property of this property.
|
/// Gets whether keyframes are supported on this property
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public BaseLayerProperty Parent { get; }
|
public bool KeyframesSupported { get; protected set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the child properties of this property.
|
/// Gets or sets whether keyframes are enabled on this property, has no effect if <see cref="KeyframesSupported" /> is
|
||||||
/// <remarks>If the layer has children it cannot contain a value or keyframes.</remarks>
|
/// False
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<BaseLayerProperty> Children { get; set; }
|
public bool KeyframesEnabled
|
||||||
|
{
|
||||||
|
get => _keyframesEnabled;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_keyframesEnabled == value) return;
|
||||||
|
_keyframesEnabled = value;
|
||||||
|
OnKeyframesToggled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a unique identifier for this property, a layer may not contain two properties with the same ID.
|
/// Gets or sets whether the property is hidden in the UI
|
||||||
/// </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.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsHidden
|
public bool IsHidden
|
||||||
{
|
{
|
||||||
@ -134,241 +60,109 @@ namespace Artemis.Core.Models.Profile.LayerProperties
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a list of keyframes defining different values of the property in time, this list contains the untyped
|
/// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied
|
||||||
/// <see cref="BaseKeyframe" />.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyCollection<BaseKeyframe> UntypedKeyframes => BaseKeyframes.AsReadOnly();
|
public bool IsLoadedFromStorage { get; internal set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the keyframe engine instance of this property
|
/// Gets the total progress on the timeline
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public KeyframeEngine KeyframeEngine { get; internal set; }
|
public TimeSpan TimelineProgress { 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <returns></returns>
|
public bool IsCoreProperty { get; internal set; }
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public void ClearKeyframes()
|
public abstract IReadOnlyList<BaseLayerPropertyKeyframe> BaseKeyframes { get; }
|
||||||
{
|
|
||||||
if (KeyframeEngine != null)
|
internal PropertyEntity PropertyEntity { get; set; }
|
||||||
BaseValue = KeyframeEngine.GetCurrentValue();
|
internal LayerPropertyGroup LayerPropertyGroup { get; set; }
|
||||||
|
|
||||||
BaseKeyframes.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public object GetCurrentValue()
|
/// <param name="entity"></param>
|
||||||
{
|
/// <param name="layerPropertyGroup"></param>
|
||||||
if (KeyframeEngine == null || !UntypedKeyframes.Any())
|
/// <param name="fromStorage"></param>
|
||||||
return BaseValue;
|
internal abstract void ApplyToLayerProperty(PropertyEntity entity, LayerPropertyGroup layerPropertyGroup, bool fromStorage);
|
||||||
|
|
||||||
return KeyframeEngine.GetCurrentValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="value">The value to set.</param>
|
internal abstract void ApplyToEntity();
|
||||||
/// <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);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Events
|
#region Events
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when this property's value was changed outside regular keyframe updates
|
/// Occurs once every frame when the layer property is updated
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event EventHandler<EventArgs> ValueChanged;
|
public event EventHandler Updated;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </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()
|
protected virtual void OnVisibilityChanged()
|
||||||
{
|
{
|
||||||
VisibilityChanged?.Invoke(this, EventArgs.Empty);
|
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
|
#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 System.Linq;
|
||||||
using Artemis.Core.Plugins.LayerBrush;
|
using Artemis.Core.Exceptions;
|
||||||
using Artemis.Core.Plugins.Models;
|
using Artemis.Core.Utilities;
|
||||||
|
using Artemis.Storage.Entities.Profile;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Artemis.Core.Models.Profile.LayerProperties
|
namespace Artemis.Core.Models.Profile.LayerProperties
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a property on the layer. This property is visible in the profile editor and can be key-framed (unless
|
/// Represents a property on a layer. Properties are saved in storage and can optionally be modified from the UI.
|
||||||
/// opted out).
|
/// <para>
|
||||||
/// <para>To create and register a new LayerProperty use <see cref="LayerBrush.RegisterLayerProperty" /></para>
|
/// Note: You cannot initialize layer properties yourself. If properly placed and annotated, the Artemis core will
|
||||||
|
/// initialize
|
||||||
|
/// these for you.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T"></typeparam>
|
/// <typeparam name="T">The type of property encapsulated in this layer property</typeparam>
|
||||||
public class LayerProperty<T> : BaseLayerProperty
|
public abstract class LayerProperty<T> : BaseLayerProperty
|
||||||
{
|
{
|
||||||
internal LayerProperty(Layer layer, BaseLayerProperty parent, string id, string name, string description)
|
private T _baseValue;
|
||||||
: base(layer, null, parent, id, name, description, typeof(T))
|
private T _currentValue;
|
||||||
{
|
private bool _isInitialized;
|
||||||
}
|
private List<LayerPropertyKeyframe<T>> _keyframes;
|
||||||
|
|
||||||
internal LayerProperty(Layer layer, string id, string name, string description)
|
protected LayerProperty()
|
||||||
: 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))
|
|
||||||
{
|
{
|
||||||
|
_keyframes = new List<LayerPropertyKeyframe<T>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public T Value
|
public T BaseValue
|
||||||
{
|
{
|
||||||
get => BaseValue != null ? (T) BaseValue : default;
|
get => _baseValue;
|
||||||
set => BaseValue = value;
|
set
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the value of the property with keyframes applied
|
|
||||||
/// </summary>
|
|
||||||
public T CurrentValue
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
{
|
||||||
var currentValue = GetCurrentValue();
|
if (_baseValue != null && !_baseValue.Equals(value) || _baseValue == null && value != null)
|
||||||
return currentValue == null ? default : (T) currentValue;
|
{
|
||||||
|
_baseValue = value;
|
||||||
|
OnBaseValueChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a list of keyframes defining different values of the property in time, this list contains the strongly typed
|
/// Gets the current value of this property as it is affected by it's keyframes, updated once every frame
|
||||||
/// <see cref="Keyframe{T}" />
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ReadOnlyCollection<Keyframe<T>> Keyframes => BaseKeyframes.Cast<Keyframe<T>>().ToList().AsReadOnly();
|
public T CurrentValue
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds a keyframe to the property.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="keyframe">The keyframe to remove</param>
|
|
||||||
public void AddKeyframe(Keyframe<T> keyframe)
|
|
||||||
{
|
{
|
||||||
base.AddKeyframe(keyframe);
|
get => !KeyframesEnabled || !KeyframesSupported ? BaseValue : _currentValue;
|
||||||
|
internal set => _currentValue = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="keyframe">The keyframe to remove</param>
|
/// <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
|
/// Applies the profile element's properties to the underlying storage entity
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal abstract void ApplyToEntity();
|
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.Exceptions;
|
||||||
using Artemis.Core.Models.Profile.KeyframeEngines;
|
|
||||||
using Artemis.Core.Plugins.Models;
|
using Artemis.Core.Plugins.Models;
|
||||||
using Artemis.Core.Services.Interfaces;
|
using Artemis.Core.Services.Interfaces;
|
||||||
|
using Artemis.Storage;
|
||||||
|
using Artemis.Storage.Migrations.Interfaces;
|
||||||
using Artemis.Storage.Repositories.Interfaces;
|
using Artemis.Storage.Repositories.Interfaces;
|
||||||
using LiteDB;
|
using LiteDB;
|
||||||
using Ninject.Activation;
|
using Ninject.Activation;
|
||||||
@ -53,16 +53,27 @@ namespace Artemis.Core.Ninject
|
|||||||
catch (LiteException e)
|
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
|
// 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);
|
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)
|
// 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");
|
File.Delete($"{Constants.DataFolder}\\database.db");
|
||||||
return new LiteRepository(Constants.ConnectionString);
|
return new LiteRepository(Constants.ConnectionString);
|
||||||
}
|
}
|
||||||
|
|
||||||
}).InSingletonScope();
|
}).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
|
// Bind all repositories as singletons
|
||||||
Kernel.Bind(x =>
|
Kernel.Bind(x =>
|
||||||
{
|
{
|
||||||
@ -73,15 +84,6 @@ namespace Artemis.Core.Ninject
|
|||||||
.Configure(c => c.InSingletonScope());
|
.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<PluginSettings>().ToProvider<PluginSettingsProvider>();
|
||||||
Kernel.Bind<ILogger>().ToProvider<LoggerProvider>();
|
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 System.Collections.Generic;
|
||||||
using Artemis.Core.Models.Surface;
|
using Artemis.Core.Models.Surface;
|
||||||
|
using Artemis.Core.Plugins.Abstract.DataModels;
|
||||||
using Artemis.Core.Plugins.Abstract.ViewModels;
|
using Artemis.Core.Plugins.Abstract.ViewModels;
|
||||||
using Artemis.Core.Plugins.Models;
|
using Artemis.Core.Plugins.Models;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
@ -27,6 +28,11 @@ namespace Artemis.Core.Plugins.Abstract
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string DisplayIcon { get; set; }
|
public string DisplayIcon { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The optional datamodel driving this module
|
||||||
|
/// </summary>
|
||||||
|
public DataModel DataModel { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not this module expands upon the main data model. If set to true any data in main data model can be
|
/// 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
|
/// 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;
|
||||||
using System.Linq;
|
using System.Collections.Generic;
|
||||||
using Artemis.Core.Models.Profile;
|
using Artemis.Core.Models.Profile;
|
||||||
using Artemis.Core.Models.Profile.LayerProperties;
|
using Artemis.Core.Models.Profile.LayerProperties;
|
||||||
using Artemis.Core.Plugins.Exceptions;
|
using Artemis.Core.Plugins.Exceptions;
|
||||||
using Artemis.Core.Services.Interfaces;
|
using Artemis.Core.Services.Interfaces;
|
||||||
using SkiaSharp;
|
|
||||||
|
|
||||||
namespace Artemis.Core.Plugins.LayerBrush
|
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)
|
protected LayerBrush(Layer layer, LayerBrushDescriptor descriptor)
|
||||||
{
|
{
|
||||||
@ -18,112 +17,61 @@ namespace Artemis.Core.Plugins.LayerBrush
|
|||||||
Descriptor = descriptor;
|
Descriptor = descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Layer Layer { get; }
|
#region Properties
|
||||||
public LayerBrushDescriptor Descriptor { get; }
|
|
||||||
|
|
||||||
public virtual void Dispose()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called before rendering every frame, write your update logic here
|
/// Gets the properties of this brush.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="deltaTime"></param>
|
public T Properties
|
||||||
public virtual void Update(double deltaTime)
|
|
||||||
{
|
{
|
||||||
}
|
get
|
||||||
|
|
||||||
/// <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)
|
|
||||||
{
|
{
|
||||||
// If it exists and the types match, return the existing property
|
// I imagine a null reference here can be confusing, so lets throw an exception explaining what to do
|
||||||
if (existing.Type == typeof(T))
|
if (_properties == null)
|
||||||
return (LayerProperty<T>) existing;
|
throw new ArtemisPluginException("Cannot access brush properties until OnPropertiesInitialized has been called");
|
||||||
// If it exists and the types are different, something is wrong
|
return _properties;
|
||||||
throw new ArtemisPluginException($"Cannot register the property {id} with different types twice.");
|
|
||||||
}
|
}
|
||||||
|
internal set => _properties = value;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <typeparam name="T"></typeparam>
|
public bool PropertiesInitialized { get; private set; }
|
||||||
/// <param name="layerProperty"></param>
|
|
||||||
protected void UnRegisterLayerProperty<T>(LayerProperty<T> layerProperty)
|
|
||||||
{
|
|
||||||
if (layerProperty == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (Layer.Properties.Any(p => p == layerProperty))
|
/// <summary>
|
||||||
Layer.Properties.RemoveLayerProperty(layerProperty);
|
/// 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;
|
Properties = Activator.CreateInstance<T>();
|
||||||
foreach (var baseLayerProperty in Layer.Properties)
|
Properties.InitializeProperties(layerService, Layer, path);
|
||||||
_layerService.InstantiateKeyframeEngine(baseLayerProperty);
|
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();
|
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));
|
_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.Plugins.Models;
|
||||||
using Artemis.Core.Services.Interfaces;
|
using Artemis.Core.Services.Interfaces;
|
||||||
using Artemis.Core.Services.Storage.Interfaces;
|
using Artemis.Core.Services.Storage.Interfaces;
|
||||||
|
using Artemis.Storage;
|
||||||
|
using Artemis.Storage.Migrations.Interfaces;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using RGB.NET.Core;
|
using RGB.NET.Core;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
@ -30,8 +32,9 @@ namespace Artemis.Core.Services
|
|||||||
private List<Module> _modules;
|
private List<Module> _modules;
|
||||||
private PluginSetting<LogEventLevel> _loggingLevel;
|
private PluginSetting<LogEventLevel> _loggingLevel;
|
||||||
|
|
||||||
internal CoreService(ILogger logger, ISettingsService settingsService, IPluginService pluginService, IRgbService rgbService,
|
// ReSharper disable once UnusedParameter.Local - Storage migration service is injected early to ensure it runs before anything else
|
||||||
ISurfaceService surfaceService, IProfileService profileService)
|
internal CoreService(ILogger logger, StorageMigrationService _, ISettingsService settingsService, IPluginService pluginService,
|
||||||
|
IRgbService rgbService, ISurfaceService surfaceService, IProfileService profileService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_pluginService = pluginService;
|
_pluginService = pluginService;
|
||||||
@ -48,6 +51,7 @@ namespace Artemis.Core.Services
|
|||||||
_pluginService.PluginEnabled += (sender, args) => _modules = _pluginService.GetPluginsOfType<Module>();
|
_pluginService.PluginEnabled += (sender, args) => _modules = _pluginService.GetPluginsOfType<Module>();
|
||||||
_pluginService.PluginDisabled += (sender, args) => _modules = _pluginService.GetPluginsOfType<Module>();
|
_pluginService.PluginDisabled += (sender, args) => _modules = _pluginService.GetPluginsOfType<Module>();
|
||||||
|
|
||||||
|
|
||||||
ConfigureJsonConvert();
|
ConfigureJsonConvert();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +103,7 @@ namespace Artemis.Core.Services
|
|||||||
_logger.Information("Initialized without an active surface entity");
|
_logger.Information("Initialized without an active surface entity");
|
||||||
|
|
||||||
_profileService.ActivateDefaultProfiles();
|
_profileService.ActivateDefaultProfiles();
|
||||||
|
|
||||||
OnInitialized();
|
OnInitialized();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
using Artemis.Core.Models.Profile;
|
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.Plugins.LayerBrush;
|
||||||
|
|
||||||
namespace Artemis.Core.Services.Interfaces
|
namespace Artemis.Core.Services.Interfaces
|
||||||
@ -8,27 +6,27 @@ namespace Artemis.Core.Services.Interfaces
|
|||||||
public interface ILayerService : IArtemisService
|
public interface ILayerService : IArtemisService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <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" />.
|
/// to the <see cref="Layer" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="layer">The layer to instantiate the brush for</param>
|
/// <param name="layer">The layer to instantiate the brush for</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
LayerBrush InstantiateLayerBrush(Layer layer);
|
BaseLayerBrush InstantiateLayerBrush(Layer layer);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Instantiates and adds a compatible <see cref="KeyframeEngine" /> to the provided <see cref="LayerProperty{T}" />.
|
/// Removes the layer brush from the provided layer and disposes it
|
||||||
/// If the property already has a compatible keyframe engine, nothing happens.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="layerProperty">The layer property to apply the keyframe engine to.</param>
|
/// <param name="layer"></param>
|
||||||
/// <returns>The resulting keyframe engine, if a compatible engine was found.</returns>
|
void RemoveLayerBrush(Layer layer);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,8 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Artemis.Core.Models.Profile;
|
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.Plugins.LayerBrush;
|
||||||
using Artemis.Core.Services.Interfaces;
|
using Artemis.Core.Services.Interfaces;
|
||||||
using Ninject;
|
using Ninject;
|
||||||
@ -24,11 +22,25 @@ namespace Artemis.Core.Services
|
|||||||
_pluginService = pluginService;
|
_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);
|
RemoveLayerBrush(layer);
|
||||||
|
|
||||||
var descriptorReference = layer.Properties.BrushReference.CurrentValue;
|
var descriptorReference = layer.General.BrushReference?.CurrentValue;
|
||||||
if (descriptorReference == null)
|
if (descriptorReference == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@ -46,34 +58,10 @@ namespace Artemis.Core.Services
|
|||||||
new ConstructorArgument("layer", layer),
|
new ConstructorArgument("layer", layer),
|
||||||
new ConstructorArgument("descriptor", descriptor)
|
new ConstructorArgument("descriptor", descriptor)
|
||||||
};
|
};
|
||||||
var layerBrush = (LayerBrush) _kernel.Get(descriptor.LayerBrushType, arguments);
|
layer.LayerBrush = (BaseLayerBrush)_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.
|
layer.LayerBrush.InitializeProperties(this, "LayerBrush.");
|
||||||
// If layer brush implementations need the LayerService they can inject it themselves, but don't require it by default
|
layer.OnLayerBrushUpdated();
|
||||||
layerBrush.SetLayerService(this);
|
return layer.LayerBrush;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveLayerBrush(Layer layer)
|
public void RemoveLayerBrush(Layer layer)
|
||||||
@ -83,11 +71,9 @@ namespace Artemis.Core.Services
|
|||||||
|
|
||||||
var brush = layer.LayerBrush;
|
var brush = layer.LayerBrush;
|
||||||
layer.LayerBrush = null;
|
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();
|
brush.Dispose();
|
||||||
|
|
||||||
|
layer.LayerEntity.PropertyEntities.RemoveAll(p => p.PluginGuid == brush.PluginInfo.Guid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -26,13 +26,13 @@ namespace Artemis.Core.Services.Storage.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="selectedProfile"></param>
|
/// <param name="selectedProfile"></param>
|
||||||
/// <param name="module"></param>
|
/// <param name="module"></param>
|
||||||
void UndoUpdateProfile(Profile selectedProfile, ProfileModule module);
|
bool UndoUpdateProfile(Profile selectedProfile, ProfileModule module);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to restore the profile to the state it had before the last <see cref="UndoUpdateProfile" /> call.
|
/// Attempts to restore the profile to the state it had before the last <see cref="UndoUpdateProfile" /> call.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="selectedProfile"></param>
|
/// <param name="selectedProfile"></param>
|
||||||
/// <param name="module"></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);
|
module.ChangeActiveProfile(profile, _surfaceService.ActiveSurface);
|
||||||
if (profile != null)
|
if (profile != null)
|
||||||
{
|
{
|
||||||
InstantiateProfileLayerBrushes(profile);
|
InitializeLayerProperties(profile);
|
||||||
InstantiateProfileKeyframeEngines(profile);
|
InstantiateLayerBrushes(profile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteProfile(Profile profile)
|
public void DeleteProfile(Profile profile)
|
||||||
{
|
{
|
||||||
|
_logger.Debug("Removing profile " + profile);
|
||||||
_profileRepository.Remove(profile.ProfileEntity);
|
_profileRepository.Remove(profile.ProfileEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateProfile(Profile profile, bool includeChildren)
|
public void UpdateProfile(Profile profile, bool includeChildren)
|
||||||
{
|
{
|
||||||
|
_logger.Debug("Updating profile " + profile);
|
||||||
var memento = JsonConvert.SerializeObject(profile.ProfileEntity);
|
var memento = JsonConvert.SerializeObject(profile.ProfileEntity);
|
||||||
profile.RedoStack.Clear();
|
profile.RedoStack.Clear();
|
||||||
profile.UndoStack.Push(memento);
|
profile.UndoStack.Push(memento);
|
||||||
@ -121,12 +123,12 @@ namespace Artemis.Core.Services.Storage
|
|||||||
_profileRepository.Save(profile.ProfileEntity);
|
_profileRepository.Save(profile.ProfileEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UndoUpdateProfile(Profile profile, ProfileModule module)
|
public bool UndoUpdateProfile(Profile profile, ProfileModule module)
|
||||||
{
|
{
|
||||||
if (!profile.UndoStack.Any())
|
if (!profile.UndoStack.Any())
|
||||||
{
|
{
|
||||||
_logger.Debug("Undo profile update - Failed, undo stack empty");
|
_logger.Debug("Undo profile update - Failed, undo stack empty");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ActivateProfile(module, null);
|
ActivateProfile(module, null);
|
||||||
@ -138,14 +140,15 @@ namespace Artemis.Core.Services.Storage
|
|||||||
ActivateProfile(module, profile);
|
ActivateProfile(module, profile);
|
||||||
|
|
||||||
_logger.Debug("Undo profile update - Success");
|
_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())
|
if (!profile.RedoStack.Any())
|
||||||
{
|
{
|
||||||
_logger.Debug("Redo profile update - Failed, redo empty");
|
_logger.Debug("Redo profile update - Failed, redo empty");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ActivateProfile(module, null);
|
ActivateProfile(module, null);
|
||||||
@ -157,22 +160,29 @@ namespace Artemis.Core.Services.Storage
|
|||||||
ActivateProfile(module, profile);
|
ActivateProfile(module, profile);
|
||||||
|
|
||||||
_logger.Debug("Redo profile update - Success");
|
_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
|
// Only instantiate brushes for layers without an existing brush instance
|
||||||
foreach (var layer in profile.GetAllLayers().Where(l => l.LayerBrush == null))
|
foreach (var layer in profile.GetAllLayers().Where(l => l.LayerBrush == null))
|
||||||
_layerService.InstantiateLayerBrush(layer);
|
_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)
|
private void ActiveProfilesPopulateLeds(ArtemisSurface surface)
|
||||||
{
|
{
|
||||||
var profileModules = _pluginService.GetPluginsOfType<ProfileModule>();
|
var profileModules = _pluginService.GetPluginsOfType<ProfileModule>();
|
||||||
@ -184,14 +194,7 @@ namespace Artemis.Core.Services.Storage
|
|||||||
{
|
{
|
||||||
var profileModules = _pluginService.GetPluginsOfType<ProfileModule>();
|
var profileModules = _pluginService.GetPluginsOfType<ProfileModule>();
|
||||||
foreach (var profileModule in profileModules.Where(p => p.ActiveProfile != null).ToList())
|
foreach (var profileModule in profileModules.Where(p => p.ActiveProfile != null).ToList())
|
||||||
InstantiateProfileLayerBrushes(profileModule.ActiveProfile);
|
InstantiateLayerBrushes(profileModule.ActiveProfile);
|
||||||
}
|
|
||||||
|
|
||||||
private void ActiveProfilesInstantiateKeyframeEngines()
|
|
||||||
{
|
|
||||||
var profileModules = _pluginService.GetPluginsOfType<ProfileModule>();
|
|
||||||
foreach (var profileModule in profileModules.Where(p => p.ActiveProfile != null).ToList())
|
|
||||||
InstantiateProfileKeyframeEngines(profileModule.ActiveProfile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Event handlers
|
#region Event handlers
|
||||||
@ -212,7 +215,6 @@ namespace Artemis.Core.Services.Storage
|
|||||||
if (e.PluginInfo.Instance is LayerBrushProvider)
|
if (e.PluginInfo.Instance is LayerBrushProvider)
|
||||||
{
|
{
|
||||||
ActiveProfilesInstantiateProfileLayerBrushes();
|
ActiveProfilesInstantiateProfileLayerBrushes();
|
||||||
ActiveProfilesInstantiateKeyframeEngines();
|
|
||||||
}
|
}
|
||||||
else if (e.PluginInfo.Instance is ProfileModule profileModule)
|
else if (e.PluginInfo.Instance is ProfileModule profileModule)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
<LangVersion>7</LangVersion>
|
<LangVersion>7</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="LiteDB" Version="5.0.7" />
|
<PackageReference Include="LiteDB" Version="5.0.8" />
|
||||||
|
<PackageReference Include="Serilog" Version="2.9.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using LiteDB;
|
using LiteDB;
|
||||||
|
|
||||||
namespace Artemis.Storage.Entities.Profile
|
namespace Artemis.Storage.Entities.Profile
|
||||||
@ -11,6 +12,7 @@ namespace Artemis.Storage.Entities.Profile
|
|||||||
Leds = new List<LedEntity>();
|
Leds = new List<LedEntity>();
|
||||||
PropertyEntities = new List<PropertyEntity>();
|
PropertyEntities = new List<PropertyEntity>();
|
||||||
Condition = new List<ProfileConditionEntity>();
|
Condition = new List<ProfileConditionEntity>();
|
||||||
|
ExpandedPropertyGroups = new List<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
@ -22,6 +24,7 @@ namespace Artemis.Storage.Entities.Profile
|
|||||||
public List<LedEntity> Leds { get; set; }
|
public List<LedEntity> Leds { get; set; }
|
||||||
public List<PropertyEntity> PropertyEntities { get; set; }
|
public List<PropertyEntity> PropertyEntities { get; set; }
|
||||||
public List<ProfileConditionEntity> Condition { get; set; }
|
public List<ProfileConditionEntity> Condition { get; set; }
|
||||||
|
public List<string> ExpandedPropertyGroups { get; set; }
|
||||||
|
|
||||||
[BsonRef("ProfileEntity")]
|
[BsonRef("ProfileEntity")]
|
||||||
public ProfileEntity Profile { get; set; }
|
public ProfileEntity Profile { get; set; }
|
||||||
|
|||||||
@ -10,10 +10,11 @@ namespace Artemis.Storage.Entities.Profile
|
|||||||
KeyframeEntities = new List<KeyframeEntity>();
|
KeyframeEntities = new List<KeyframeEntity>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Id { get; set; }
|
public Guid PluginGuid { get; set; }
|
||||||
public string ValueType { get; set; }
|
public string Path { get; set; }
|
||||||
|
|
||||||
public string Value { get; set; }
|
public string Value { get; set; }
|
||||||
public bool IsUsingKeyframes { get; set; }
|
public bool KeyframesEnabled { get; set; }
|
||||||
|
|
||||||
public List<KeyframeEntity> KeyframeEntities { 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>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AvalonEdit" Version="6.0.1" />
|
<PackageReference Include="AvalonEdit" Version="6.0.1" />
|
||||||
<PackageReference Include="Humanizer.Core" Version="2.7.9" />
|
<PackageReference Include="Humanizer.Core" Version="2.8.11" />
|
||||||
<PackageReference Include="MaterialDesignExtensions" Version="3.0.0" />
|
<PackageReference Include="MaterialDesignExtensions" Version="3.1.0" />
|
||||||
<PackageReference Include="MaterialDesignThemes" Version="3.1.0" />
|
<PackageReference Include="MaterialDesignThemes" Version="3.1.3" />
|
||||||
<PackageReference Include="Ninject" Version="3.3.4" />
|
<PackageReference Include="Ninject" Version="3.3.4" />
|
||||||
<PackageReference Include="Ninject.Extensions.Conventions" Version="3.3.0" />
|
<PackageReference Include="Ninject.Extensions.Conventions" Version="3.3.0" />
|
||||||
<PackageReference Include="PropertyChanged.Fody" Version="3.2.8" />
|
<PackageReference Include="PropertyChanged.Fody" Version="3.2.8" />
|
||||||
<PackageReference Include="SkiaSharp" Version="1.68.2-preview.29" />
|
<PackageReference Include="SkiaSharp" Version="1.68.3" />
|
||||||
<PackageReference Include="SkiaSharp.Views.WPF" Version="1.68.2-preview.29" />
|
<PackageReference Include="SkiaSharp.Views.WPF" Version="1.68.3" />
|
||||||
<PackageReference Include="Stylet" Version="1.3.1" />
|
<PackageReference Include="Stylet" Version="1.3.2" />
|
||||||
<PackageReference Include="System.Buffers" Version="4.5.0" />
|
<PackageReference Include="System.Buffers" Version="4.5.0" />
|
||||||
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
|
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
|
||||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.7.0" />
|
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.7.0" />
|
||||||
|
|||||||
@ -9,10 +9,6 @@
|
|||||||
d:DesignHeight="101.848" d:DesignWidth="242.956">
|
d:DesignHeight="101.848" d:DesignWidth="242.956">
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<ResourceDictionary>
|
<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:ColorToStringConverter x:Key="ColorToStringConverter" />
|
||||||
<converters:ColorToSolidColorConverter x:Key="ColorToSolidColorConverter" />
|
<converters:ColorToSolidColorConverter x:Key="ColorToSolidColorConverter" />
|
||||||
<VisualBrush x:Key="Checkerboard" TileMode="Tile" Stretch="Uniform" ViewportUnits="Absolute" Viewport="0,0,10,10">
|
<VisualBrush x:Key="Checkerboard" TileMode="Tile" Stretch="Uniform" ViewportUnits="Absolute" Viewport="0,0,10,10">
|
||||||
@ -62,14 +58,14 @@
|
|||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
OpacityMask="{x:Null}">
|
OpacityMask="{x:Null}">
|
||||||
<Track.DecreaseRepeatButton>
|
<Track.DecreaseRepeatButton>
|
||||||
<RepeatButton Command="{x:Static Slider.DecreaseLarge}" Style="{StaticResource MaterialDesignHorizontalColorSliderTrackRepeatButton}" />
|
<RepeatButton Command="{x:Static Slider.DecreaseLarge}" Style="{DynamicResource MaterialDesignHorizontalColorSliderTrackRepeatButton}" />
|
||||||
</Track.DecreaseRepeatButton>
|
</Track.DecreaseRepeatButton>
|
||||||
<Track.IncreaseRepeatButton>
|
<Track.IncreaseRepeatButton>
|
||||||
<RepeatButton Command="{x:Static Slider.IncreaseLarge}" Style="{StaticResource MaterialDesignHorizontalColorSliderTrackRepeatButton}" />
|
<RepeatButton Command="{x:Static Slider.IncreaseLarge}" Style="{DynamicResource MaterialDesignHorizontalColorSliderTrackRepeatButton}" />
|
||||||
</Track.IncreaseRepeatButton>
|
</Track.IncreaseRepeatButton>
|
||||||
<Track.Thumb>
|
<Track.Thumb>
|
||||||
<Thumb x:Name="Thumb" Width="20" Height="20" VerticalAlignment="Center" Focusable="False" OverridesDefaultStyle="True"
|
<Thumb x:Name="Thumb" Width="20" Height="20" VerticalAlignment="Center" Focusable="False" OverridesDefaultStyle="True"
|
||||||
Template="{StaticResource MaterialDesignColorSliderThumb}">
|
Template="{DynamicResource MaterialDesignColorSliderThumb}">
|
||||||
<Thumb.Foreground>
|
<Thumb.Foreground>
|
||||||
<SolidColorBrush
|
<SolidColorBrush
|
||||||
Color="{Binding Color, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Mode=OneWay, Converter={StaticResource ColorToSolidColorConverter}}" />
|
Color="{Binding Color, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Mode=OneWay, Converter={StaticResource ColorToSolidColorConverter}}" />
|
||||||
@ -124,7 +120,7 @@
|
|||||||
<Slider Grid.Row="1" Margin="8"
|
<Slider Grid.Row="1" Margin="8"
|
||||||
IsMoveToPointEnabled="True"
|
IsMoveToPointEnabled="True"
|
||||||
Orientation="Horizontal"
|
Orientation="Horizontal"
|
||||||
Style="{StaticResource MaterialDesignColorSlider}"
|
Style="{DynamicResource MaterialDesignColorSlider}"
|
||||||
Template="{StaticResource MaterialDesignOpacitySlider}"
|
Template="{StaticResource MaterialDesignOpacitySlider}"
|
||||||
Value="{Binding ColorOpacity, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
|
Value="{Binding ColorOpacity, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
|
||||||
Maximum="255" />
|
Maximum="255" />
|
||||||
|
|||||||
@ -4,17 +4,25 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
|
xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DesignHeight="450" d:DesignWidth="800">
|
d:DesignHeight="450" d:DesignWidth="800">
|
||||||
|
<UserControl.Style>
|
||||||
|
<Style>
|
||||||
|
<Setter Property="Validation.ErrorTemplate" Value="{StaticResource MaterialDesignValidationErrorTemplate}" />
|
||||||
|
</Style>
|
||||||
|
</UserControl.Style>
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<!-- Drag handle -->
|
<!-- Drag handle -->
|
||||||
<Border x:Name="DragHandle" BorderThickness="0,0,0,1" Height="19">
|
<Border x:Name="DragHandle" BorderThickness="0,0,0,1" Height="19">
|
||||||
<Border.BorderBrush>
|
<Border.BorderBrush>
|
||||||
<VisualBrush>
|
<VisualBrush>
|
||||||
<VisualBrush.Visual>
|
<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}"
|
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.Visual>
|
||||||
</VisualBrush>
|
</VisualBrush>
|
||||||
</Border.BorderBrush>
|
</Border.BorderBrush>
|
||||||
@ -27,15 +35,14 @@
|
|||||||
Foreground="{DynamicResource SecondaryAccentBrush}"
|
Foreground="{DynamicResource SecondaryAccentBrush}"
|
||||||
MouseDown="InputMouseDown"
|
MouseDown="InputMouseDown"
|
||||||
MouseUp="InputMouseUp"
|
MouseUp="InputMouseUp"
|
||||||
MouseMove="InputMouseMove"
|
MouseMove="InputMouseMove"
|
||||||
RequestBringIntoView="Input_OnRequestBringIntoView"/>
|
RequestBringIntoView="Input_OnRequestBringIntoView" />
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Input -->
|
<!-- Input -->
|
||||||
<TextBox x:Name="Input"
|
<TextBox x:Name="Input"
|
||||||
Width="60"
|
Width="60"
|
||||||
Height="20"
|
Height="20"
|
||||||
materialDesign:ValidationAssist.UsePopup="True"
|
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
Text="{Binding Value, StringFormat=N3, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
|
Text="{Binding Value, StringFormat=N3, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
|
||||||
LostFocus="InputLostFocus"
|
LostFocus="InputLostFocus"
|
||||||
|
|||||||
@ -104,6 +104,8 @@ namespace Artemis.UI.Shared.Controls
|
|||||||
var startX = new decimal(_mouseDragStartPoint.X);
|
var startX = new decimal(_mouseDragStartPoint.X);
|
||||||
var x = new decimal(e.GetPosition((IInputElement) sender).X);
|
var x = new decimal(e.GetPosition((IInputElement) sender).X);
|
||||||
var stepSize = new decimal(StepSize);
|
var stepSize = new decimal(StepSize);
|
||||||
|
if (stepSize == 0)
|
||||||
|
stepSize = 0.1m;
|
||||||
|
|
||||||
Value = (float) UltimateRoundingFunction(startValue + stepSize * (x - startX), stepSize, 0.5m);
|
Value = (float) UltimateRoundingFunction(startValue + stepSize * (x - startX), stepSize, 0.5m);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,10 +8,6 @@
|
|||||||
d:DesignHeight="450" d:DesignWidth="800">
|
d:DesignHeight="450" d:DesignWidth="800">
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<ResourceDictionary>
|
<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" />
|
<converters:ColorGradientToGradientStopsConverter x:Key="ColorGradientToGradientStopsConverter" />
|
||||||
<VisualBrush x:Key="Checkerboard" TileMode="Tile" Stretch="Uniform" ViewportUnits="Absolute" Viewport="0,0,10,10">
|
<VisualBrush x:Key="Checkerboard" TileMode="Tile" Stretch="Uniform" ViewportUnits="Absolute" Viewport="0,0,10,10">
|
||||||
<VisualBrush.Visual>
|
<VisualBrush.Visual>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ using System.Windows;
|
|||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using Artemis.Core.Models.Profile;
|
using Artemis.Core.Models.Profile;
|
||||||
|
using Artemis.Core.Models.Profile.Colors;
|
||||||
using Artemis.UI.Shared.Annotations;
|
using Artemis.UI.Shared.Annotations;
|
||||||
using Artemis.UI.Shared.Services.Interfaces;
|
using Artemis.UI.Shared.Services.Interfaces;
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using System.Windows.Data;
|
using System.Windows.Data;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using Artemis.Core.Models.Profile;
|
using Artemis.Core.Models.Profile;
|
||||||
|
using Artemis.Core.Models.Profile.Colors;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
using Stylet;
|
using Stylet;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using Artemis.Core.Models.Profile;
|
using Artemis.Core.Models.Profile;
|
||||||
|
using Artemis.Core.Models.Profile.Colors;
|
||||||
using Artemis.UI.Shared.Screens.GradientEditor;
|
using Artemis.UI.Shared.Screens.GradientEditor;
|
||||||
|
|
||||||
namespace Artemis.UI.Shared.Ninject.Factories
|
namespace Artemis.UI.Shared.Ninject.Factories
|
||||||
|
|||||||
@ -10,29 +10,38 @@
|
|||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DesignHeight="163.274" d:DesignWidth="254.425"
|
d:DesignHeight="163.274" d:DesignWidth="254.425"
|
||||||
d:DataContext="{d:DesignInstance dialogs:ExceptionDialogViewModel}">
|
d:DataContext="{d:DesignInstance dialogs:ExceptionDialogViewModel}">
|
||||||
<StackPanel Margin="16">
|
<StackPanel Orientation="Vertical" HorizontalAlignment="Right" Margin="16">
|
||||||
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Text="{Binding Header}" TextWrapping="Wrap" />
|
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Text="{Binding Header}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
<Separator Margin="0 15" />
|
<Separator Margin="0 15" />
|
||||||
<TextBlock Style="{StaticResource MaterialDesignBody1TextBlock}" FontWeight="Bold" Margin="22 0">Exception message</TextBlock>
|
<ScrollViewer MaxHeight="500">
|
||||||
<TextBlock Style="{StaticResource MaterialDesignBody1TextBlock}" HorizontalAlignment="Left" Text="{Binding Exception.Message}" TextWrapping="Wrap" Margin="22 5" MaxWidth="1000" />
|
<StackPanel>
|
||||||
<Separator Margin="0 15" />
|
<ItemsControl ItemsSource="{Binding Exceptions}">
|
||||||
<TextBlock Style="{StaticResource MaterialDesignBody1TextBlock}" Text="Stack trace" TextWrapping="Wrap" FontWeight="Bold" Margin="22 0" />
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Style="{StaticResource MaterialDesignBody1TextBlock}" Text="Stack trace"
|
||||||
|
TextWrapping="Wrap" FontWeight="Bold"/>
|
||||||
|
|
||||||
<avalonedit:TextEditor SyntaxHighlighting="C#"
|
<avalonedit:TextEditor SyntaxHighlighting="C#"
|
||||||
FontFamily="pack://application:,,,/Resources/Fonts/#Roboto Mono"
|
FontFamily="pack://application:,,,/Resources/Fonts/#Roboto Mono"
|
||||||
FontSize="10pt"
|
FontSize="10pt"
|
||||||
IsReadOnly="True"
|
IsReadOnly="True"
|
||||||
Document="{Binding Document}"
|
Document="{Binding Document}"
|
||||||
HorizontalScrollBarVisibility="Auto"
|
HorizontalScrollBarVisibility="Auto"
|
||||||
VerticalScrollBarVisibility="Auto"
|
VerticalScrollBarVisibility="Auto"
|
||||||
MaxWidth="1000"
|
MaxWidth="1000"
|
||||||
Margin="0 10" />
|
Margin="0 10 10 0"
|
||||||
|
Padding="10"/>
|
||||||
|
|
||||||
|
<Separator Margin="0 15" />
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
</StackPanel>
|
||||||
<Button Style="{StaticResource MaterialDesignFlatButton}" IsDefault="True" Margin="0 8 0 0"
|
</DataTemplate>
|
||||||
Command="{s:Action Close}" Content="Close" />
|
</ItemsControl.ItemTemplate>
|
||||||
</StackPanel>
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
<Button Style="{StaticResource MaterialDesignFlatButton}" IsDefault="True" Margin="0 8 0 0"
|
||||||
|
Command="{s:Action Close}" Content="Close" HorizontalAlignment="Right" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using Artemis.UI.Shared.Services.Dialog;
|
using Artemis.UI.Shared.Services.Dialog;
|
||||||
using ICSharpCode.AvalonEdit;
|
|
||||||
using ICSharpCode.AvalonEdit.Document;
|
using ICSharpCode.AvalonEdit.Document;
|
||||||
|
|
||||||
namespace Artemis.UI.Shared.Screens.Dialogs
|
namespace Artemis.UI.Shared.Screens.Dialogs
|
||||||
@ -10,18 +10,35 @@ namespace Artemis.UI.Shared.Screens.Dialogs
|
|||||||
public ExceptionDialogViewModel(string message, Exception exception)
|
public ExceptionDialogViewModel(string message, Exception exception)
|
||||||
{
|
{
|
||||||
Header = message;
|
Header = message;
|
||||||
Exception = exception;
|
Exceptions = new List<DialogException>();
|
||||||
Document = new TextDocument(new StringTextSource(exception.StackTrace));
|
|
||||||
|
var currentException = exception;
|
||||||
|
while (currentException != null)
|
||||||
|
{
|
||||||
|
Exceptions.Add(new DialogException(currentException));
|
||||||
|
currentException = currentException.InnerException;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Header { get; }
|
public string Header { get; }
|
||||||
public Exception Exception { get; }
|
public List<DialogException> Exceptions { get; set; }
|
||||||
|
|
||||||
public IDocument Document { get; set; }
|
|
||||||
|
|
||||||
public void Close()
|
public void Close()
|
||||||
{
|
{
|
||||||
Session.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.Controls;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using Artemis.Core.Models.Profile;
|
using Artemis.Core.Models.Profile;
|
||||||
|
using Artemis.Core.Models.Profile.Colors;
|
||||||
using Artemis.UI.Shared.Utilities;
|
using Artemis.UI.Shared.Utilities;
|
||||||
using Stylet;
|
using Stylet;
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ using System.Windows;
|
|||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using Artemis.Core.Models.Profile;
|
using Artemis.Core.Models.Profile;
|
||||||
|
using Artemis.Core.Models.Profile.Colors;
|
||||||
using Artemis.UI.Shared.Services.Dialog;
|
using Artemis.UI.Shared.Services.Dialog;
|
||||||
using Artemis.UI.Shared.Utilities;
|
using Artemis.UI.Shared.Utilities;
|
||||||
using Stylet;
|
using Stylet;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Artemis.Core.Models.Profile;
|
using Artemis.Core.Models.Profile;
|
||||||
|
using Artemis.Core.Models.Profile.Colors;
|
||||||
using Artemis.UI.Shared.Screens.GradientEditor;
|
using Artemis.UI.Shared.Screens.GradientEditor;
|
||||||
using Artemis.UI.Shared.Services.Interfaces;
|
using Artemis.UI.Shared.Services.Interfaces;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using Artemis.Core.Models.Profile;
|
using Artemis.Core.Models.Profile;
|
||||||
|
using Artemis.Core.Models.Profile.Colors;
|
||||||
|
|
||||||
namespace Artemis.UI.Shared.Services.Interfaces
|
namespace Artemis.UI.Shared.Services.Interfaces
|
||||||
{
|
{
|
||||||
|
|||||||
@ -20,8 +20,8 @@ namespace Artemis.UI.Shared.Utilities
|
|||||||
var resultCallback = new HitTestResultCallback(r => HitTestResultBehavior.Continue);
|
var resultCallback = new HitTestResultCallback(r => HitTestResultBehavior.Continue);
|
||||||
var filterCallback = new HitTestFilterCallback(e =>
|
var filterCallback = new HitTestFilterCallback(e =>
|
||||||
{
|
{
|
||||||
if (e is FrameworkElement fe && fe.DataContext.GetType() == typeof(T) && !result.Contains((T) fe.DataContext))
|
if (e is FrameworkElement fe && fe.DataContext is T context && !result.Contains(context))
|
||||||
result.Add((T) fe.DataContext);
|
result.Add(context);
|
||||||
return HitTestFilterBehavior.Continue;
|
return HitTestFilterBehavior.Continue;
|
||||||
});
|
});
|
||||||
VisualTreeHelper.HitTest(container, filterCallback, resultCallback, hitTestParams);
|
VisualTreeHelper.HitTest(container, filterCallback, resultCallback, hitTestParams);
|
||||||
|
|||||||
@ -115,21 +115,21 @@
|
|||||||
<Resource Include="Resources\Cursors\aero_rotate.cur" />
|
<Resource Include="Resources\Cursors\aero_rotate.cur" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<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="FluentValidation" Version="8.6.2" />
|
||||||
<PackageReference Include="gong-wpf-dragdrop" Version="2.2.0" />
|
<PackageReference Include="gong-wpf-dragdrop" Version="2.2.0" />
|
||||||
<PackageReference Include="Hardcodet.NotifyIcon.Wpf.NetCore" Version="1.0.10" />
|
<PackageReference Include="Hardcodet.NotifyIcon.Wpf.NetCore" Version="1.0.10" />
|
||||||
<PackageReference Include="Humanizer.Core" Version="2.7.9" />
|
<PackageReference Include="Humanizer.Core" Version="2.8.11" />
|
||||||
<PackageReference Include="MaterialDesignExtensions" Version="3.0.0" />
|
<PackageReference Include="MaterialDesignExtensions" Version="3.1.0" />
|
||||||
<PackageReference Include="MaterialDesignThemes" Version="3.1.0" />
|
<PackageReference Include="MaterialDesignThemes" Version="3.1.3" />
|
||||||
<PackageReference Include="Microsoft.Win32.Registry" Version="4.7.0" />
|
<PackageReference Include="Microsoft.Win32.Registry" Version="4.7.0" />
|
||||||
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.19" />
|
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.19" />
|
||||||
<PackageReference Include="Ninject" Version="3.3.4" />
|
<PackageReference Include="Ninject" Version="3.3.4" />
|
||||||
<PackageReference Include="Ninject.Extensions.Conventions" Version="3.3.0" />
|
<PackageReference Include="Ninject.Extensions.Conventions" Version="3.3.0" />
|
||||||
<PackageReference Include="PropertyChanged.Fody" Version="3.2.8" />
|
<PackageReference Include="PropertyChanged.Fody" Version="3.2.8" />
|
||||||
<PackageReference Include="Serilog" Version="2.9.0" />
|
<PackageReference Include="Serilog" Version="2.9.0" />
|
||||||
<PackageReference Include="SkiaSharp.Views.WPF" Version="1.68.2-preview.29" />
|
<PackageReference Include="SkiaSharp.Views.WPF" Version="1.68.3" />
|
||||||
<PackageReference Include="Stylet" Version="1.3.1" />
|
<PackageReference Include="Stylet" Version="1.3.2" />
|
||||||
<PackageReference Include="System.Buffers" Version="4.5.0" />
|
<PackageReference Include="System.Buffers" Version="4.5.0" />
|
||||||
<PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" />
|
<PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" />
|
||||||
<PackageReference Include="System.Drawing.Common" 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.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
|
using Artemis.Core.Models.Profile.Conditions;
|
||||||
using Artemis.Core.Ninject;
|
using Artemis.Core.Ninject;
|
||||||
using Artemis.Core.Services.Interfaces;
|
using Artemis.Core.Services.Interfaces;
|
||||||
using Artemis.UI.Ninject;
|
using Artemis.UI.Ninject;
|
||||||
@ -33,6 +34,8 @@ namespace Artemis.UI
|
|||||||
|
|
||||||
protected override void Launch()
|
protected override void Launch()
|
||||||
{
|
{
|
||||||
|
var test = new LayerCondition();
|
||||||
|
|
||||||
StartupArguments = Args.ToList();
|
StartupArguments = Args.ToList();
|
||||||
|
|
||||||
var logger = Kernel.Get<ILogger>();
|
var logger = Kernel.Get<ILogger>();
|
||||||
|
|||||||
@ -1,12 +1,8 @@
|
|||||||
using Artemis.Core.Models.Profile;
|
using Artemis.Core.Models.Profile;
|
||||||
using Artemis.Core.Models.Profile.LayerProperties;
|
|
||||||
using Artemis.Core.Models.Surface;
|
using Artemis.Core.Models.Surface;
|
||||||
using Artemis.Core.Plugins.Abstract;
|
using Artemis.Core.Plugins.Abstract;
|
||||||
using Artemis.UI.Screens.Module;
|
using Artemis.UI.Screens.Module;
|
||||||
using Artemis.UI.Screens.Module.ProfileEditor;
|
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.ProfileTree.TreeItem;
|
||||||
using Artemis.UI.Screens.Module.ProfileEditor.Visualization;
|
using Artemis.UI.Screens.Module.ProfileEditor.Visualization;
|
||||||
using Artemis.UI.Screens.Settings.Tabs.Devices;
|
using Artemis.UI.Screens.Settings.Tabs.Devices;
|
||||||
@ -47,29 +43,4 @@ namespace Artemis.UI.Ninject.Factories
|
|||||||
{
|
{
|
||||||
ProfileLayerViewModel Create(Layer layer);
|
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.Ninject.Factories;
|
||||||
using Artemis.UI.Screens;
|
using Artemis.UI.Screens;
|
||||||
using Artemis.UI.Screens.Module.ProfileEditor;
|
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.Services.Interfaces;
|
||||||
using Artemis.UI.Shared.Services.Dialog;
|
using Artemis.UI.Shared.Services.Dialog;
|
||||||
using Artemis.UI.Stylet;
|
using Artemis.UI.Stylet;
|
||||||
@ -58,15 +58,6 @@ namespace Artemis.UI.Ninject
|
|||||||
.BindAllBaseClasses();
|
.BindAllBaseClasses();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bind property input VMs
|
|
||||||
Kernel.Bind(x =>
|
|
||||||
{
|
|
||||||
x.FromThisAssembly()
|
|
||||||
.SelectAllClasses()
|
|
||||||
.InheritedFrom<PropertyInputViewModel>()
|
|
||||||
.BindAllBaseClasses();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bind all UI services as singletons
|
// Bind all UI services as singletons
|
||||||
Kernel.Bind(x =>
|
Kernel.Bind(x =>
|
||||||
{
|
{
|
||||||
@ -86,6 +77,14 @@ namespace Artemis.UI.Ninject
|
|||||||
.InheritedFrom<IValidator>()
|
.InheritedFrom<IValidator>()
|
||||||
.BindAllInterfaces();
|
.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" />
|
<KeyBinding Command="{s:Action PlayFromStart}" Modifiers="Shift" Key="Space" />
|
||||||
</UserControl.InputBindings>
|
</UserControl.InputBindings>
|
||||||
<UserControl.Resources>
|
<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="OverridesDefaultStyle" Value="True" />
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
@ -37,6 +36,7 @@
|
|||||||
<ScrollBar Name="PART_VerticalScrollBar"
|
<ScrollBar Name="PART_VerticalScrollBar"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
Opacity="0.5"
|
Opacity="0.5"
|
||||||
|
Grid.Row="0"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Value="{TemplateBinding VerticalOffset}"
|
Value="{TemplateBinding VerticalOffset}"
|
||||||
Maximum="{TemplateBinding ScrollableHeight}"
|
Maximum="{TemplateBinding ScrollableHeight}"
|
||||||
@ -46,7 +46,8 @@
|
|||||||
VerticalAlignment="Bottom"
|
VerticalAlignment="Bottom"
|
||||||
Orientation="Horizontal"
|
Orientation="Horizontal"
|
||||||
Opacity="0.5"
|
Opacity="0.5"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
Value="{TemplateBinding HorizontalOffset}"
|
Value="{TemplateBinding HorizontalOffset}"
|
||||||
Maximum="{TemplateBinding ScrollableWidth}"
|
Maximum="{TemplateBinding ScrollableWidth}"
|
||||||
ViewportSize="{TemplateBinding ViewportWidth}"
|
ViewportSize="{TemplateBinding ViewportWidth}"
|
||||||
@ -60,7 +61,7 @@
|
|||||||
<Grid x:Name="ContainerGrid">
|
<Grid x:Name="ContainerGrid">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="28" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition />
|
<ColumnDefinition />
|
||||||
@ -127,14 +128,14 @@
|
|||||||
VerticalScrollBarVisibility="Hidden"
|
VerticalScrollBarVisibility="Hidden"
|
||||||
ScrollChanged="TimelineScrollChanged">
|
ScrollChanged="TimelineScrollChanged">
|
||||||
<Border BorderThickness="0,0,1,0" BorderBrush="{DynamicResource MaterialDesignDivider}">
|
<Border BorderThickness="0,0,1,0" BorderBrush="{DynamicResource MaterialDesignDivider}">
|
||||||
<ContentControl s:View.Model="{Binding PropertyTree}" />
|
<ContentControl s:View.Model="{Binding TreeViewModel}" />
|
||||||
</Border>
|
</Border>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</materialDesign:DialogHost>
|
</materialDesign:DialogHost>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Right side -->
|
<!-- Right side -->
|
||||||
<Grid Grid.Column="1">
|
<Grid Grid.Row="0" Grid.Column="1">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="56" />
|
<RowDefinition Height="56" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
@ -159,7 +160,7 @@
|
|||||||
<!-- Time -->
|
<!-- Time -->
|
||||||
<timeline:PropertyTimelineHeader Margin="0 25 0 0"
|
<timeline:PropertyTimelineHeader Margin="0 25 0 0"
|
||||||
Fill="{DynamicResource MaterialDesignBody}"
|
Fill="{DynamicResource MaterialDesignBody}"
|
||||||
PixelsPerSecond="{Binding PixelsPerSecond}"
|
PixelsPerSecond="{Binding ProfileEditorService.PixelsPerSecond}"
|
||||||
HorizontalOffset="{Binding ContentHorizontalOffset, ElementName=TimelineHeaderScrollViewer}"
|
HorizontalOffset="{Binding ContentHorizontalOffset, ElementName=TimelineHeaderScrollViewer}"
|
||||||
VisibleWidth="{Binding ActualWidth, ElementName=TimelineHeaderScrollViewer}"
|
VisibleWidth="{Binding ActualWidth, ElementName=TimelineHeaderScrollViewer}"
|
||||||
Width="{Binding ActualWidth, ElementName=PropertyTimeLine}" />
|
Width="{Binding ActualWidth, ElementName=PropertyTimeLine}" />
|
||||||
@ -169,7 +170,7 @@
|
|||||||
<!-- Timeline rails -->
|
<!-- Timeline rails -->
|
||||||
<ScrollViewer x:Name="TimelineRailsScrollViewer"
|
<ScrollViewer x:Name="TimelineRailsScrollViewer"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Style="{StaticResource SVStyle}"
|
Style="{StaticResource SvStyle}"
|
||||||
HorizontalScrollBarVisibility="Auto"
|
HorizontalScrollBarVisibility="Auto"
|
||||||
VerticalScrollBarVisibility="Auto"
|
VerticalScrollBarVisibility="Auto"
|
||||||
ScrollChanged="TimelineScrollChanged">
|
ScrollChanged="TimelineScrollChanged">
|
||||||
@ -183,7 +184,7 @@
|
|||||||
<Line X1="0" X2="0" Y1="0" Y2="{Binding ActualHeight, ElementName=ContainerGrid}"
|
<Line X1="0" X2="0" Y1="0" Y2="{Binding ActualHeight, ElementName=ContainerGrid}"
|
||||||
StrokeThickness="2" Stroke="{StaticResource SecondaryAccentBrush}" />
|
StrokeThickness="2" Stroke="{StaticResource SecondaryAccentBrush}" />
|
||||||
</Canvas>
|
</Canvas>
|
||||||
<ContentControl x:Name="PropertyTimeLine" s:View.Model="{Binding PropertyTimeline}" />
|
<ContentControl x:Name="PropertyTimeLine" s:View.Model="{Binding TimelineViewModel}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -195,17 +196,15 @@
|
|||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
ZIndex="2"
|
ZIndex="2"
|
||||||
Background="{DynamicResource MaterialDesignCardBackground}">
|
Background="{DynamicResource MaterialDesignCardBackground}">
|
||||||
|
|
||||||
<!-- Zoom control -->
|
<!-- Zoom control -->
|
||||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
<Slider Orientation="Horizontal"
|
||||||
<TextBlock Text="{Binding PixelsPerSecond}" VerticalAlignment="Center" />
|
HorizontalAlignment="Right"
|
||||||
<Slider Orientation="Horizontal"
|
Margin="10 5"
|
||||||
HorizontalAlignment="Right"
|
Minimum="31"
|
||||||
Margin="10"
|
Maximum="350"
|
||||||
Minimum="31"
|
Value="{Binding ProfileEditorService.PixelsPerSecond}"
|
||||||
Maximum="350"
|
Width="319" />
|
||||||
Value="{Binding PixelsPerSecond}"
|
|
||||||
Width="319" />
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -7,12 +7,12 @@ using System.Windows.Media;
|
|||||||
using Artemis.Core.Events;
|
using Artemis.Core.Events;
|
||||||
using Artemis.Core.Models.Profile;
|
using Artemis.Core.Models.Profile;
|
||||||
using Artemis.Core.Models.Profile.LayerProperties;
|
using Artemis.Core.Models.Profile.LayerProperties;
|
||||||
|
using Artemis.Core.Models.Profile.LayerProperties.Attributes;
|
||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
using Artemis.Core.Services.Interfaces;
|
using Artemis.Core.Services.Interfaces;
|
||||||
using Artemis.UI.Events;
|
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.Timeline;
|
||||||
|
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree;
|
||||||
using Artemis.UI.Services.Interfaces;
|
using Artemis.UI.Services.Interfaces;
|
||||||
using Stylet;
|
using Stylet;
|
||||||
|
|
||||||
@ -20,84 +20,51 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
|
|||||||
{
|
{
|
||||||
public class LayerPropertiesViewModel : ProfileEditorPanelViewModel
|
public class LayerPropertiesViewModel : ProfileEditorPanelViewModel
|
||||||
{
|
{
|
||||||
private readonly ICoreService _coreService;
|
public LayerPropertiesViewModel(IProfileEditorService profileEditorService, ICoreService coreService, ISettingsService settingsService)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
_profileEditorService = profileEditorService;
|
ProfileEditorService = profileEditorService;
|
||||||
_coreService = coreService;
|
CoreService = coreService;
|
||||||
_settingsService = settingsService;
|
SettingsService = settingsService;
|
||||||
_layerPropertyVmFactory = layerPropertyVmFactory;
|
|
||||||
_propertyTreeVmFactory = propertyTreeVmFactory;
|
|
||||||
_propertyTimelineVmFactory = propertyTimelineVmFactory;
|
|
||||||
_layerPropertyViewModels = new List<LayerPropertyViewModel>();
|
|
||||||
|
|
||||||
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 Playing { get; set; }
|
||||||
public bool RepeatAfterLastKeyframe { get; set; }
|
public bool RepeatAfterLastKeyframe { get; set; }
|
||||||
public string FormattedCurrentTime => $"{Math.Floor(_profileEditorService.CurrentTime.TotalSeconds):00}.{_profileEditorService.CurrentTime.Milliseconds:000}";
|
public string FormattedCurrentTime => $"{Math.Floor(ProfileEditorService.CurrentTime.TotalSeconds):00}.{ProfileEditorService.CurrentTime.Milliseconds:000}";
|
||||||
|
|
||||||
public int PixelsPerSecond
|
|
||||||
{
|
|
||||||
get => _pixelsPerSecond;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_pixelsPerSecond = value;
|
|
||||||
OnPixelsPerSecondChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Thickness TimeCaretPosition
|
public Thickness TimeCaretPosition
|
||||||
{
|
{
|
||||||
get => new Thickness(_profileEditorService.CurrentTime.TotalSeconds * PixelsPerSecond, 0, 0, 0);
|
get => new Thickness(ProfileEditorService.CurrentTime.TotalSeconds * ProfileEditorService.PixelsPerSecond, 0, 0, 0);
|
||||||
set => _profileEditorService.CurrentTime = TimeSpan.FromSeconds(value.Left / PixelsPerSecond);
|
set => ProfileEditorService.CurrentTime = TimeSpan.FromSeconds(value.Left / ProfileEditorService.PixelsPerSecond);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PropertyTreeViewModel PropertyTree { get; set; }
|
public Layer SelectedLayer { get; set; }
|
||||||
public PropertyTimelineViewModel PropertyTimeline { get; set; }
|
public BindableCollection<LayerPropertyGroupViewModel> LayerPropertyGroups { get; set; }
|
||||||
|
public TreeViewModel TreeViewModel { get; set; }
|
||||||
|
public TimelineViewModel TimelineViewModel { get; set; }
|
||||||
|
|
||||||
protected override void OnInitialActivate()
|
protected override void OnInitialActivate()
|
||||||
{
|
{
|
||||||
PropertyTree = _propertyTreeVmFactory.Create(this);
|
PopulateProperties(ProfileEditorService.SelectedProfileElement);
|
||||||
PropertyTimeline = _propertyTimelineVmFactory.Create(this);
|
|
||||||
|
|
||||||
PopulateProperties(_profileEditorService.SelectedProfileElement);
|
ProfileEditorService.ProfileElementSelected += ProfileEditorServiceOnProfileElementSelected;
|
||||||
|
ProfileEditorService.CurrentTimeChanged += ProfileEditorServiceOnCurrentTimeChanged;
|
||||||
_profileEditorService.ProfileElementSelected += ProfileEditorServiceOnProfileElementSelected;
|
ProfileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||||
_profileEditorService.CurrentTimeChanged += ProfileEditorServiceOnCurrentTimeChanged;
|
|
||||||
|
|
||||||
base.OnInitialActivate();
|
base.OnInitialActivate();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnClose()
|
protected override void OnClose()
|
||||||
{
|
{
|
||||||
_profileEditorService.ProfileElementSelected -= ProfileEditorServiceOnProfileElementSelected;
|
ProfileEditorService.ProfileElementSelected -= ProfileEditorServiceOnProfileElementSelected;
|
||||||
_profileEditorService.CurrentTimeChanged -= ProfileEditorServiceOnCurrentTimeChanged;
|
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();
|
base.OnClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,82 +85,88 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
|
|||||||
NotifyOfPropertyChange(() => TimeCaretPosition);
|
NotifyOfPropertyChange(() => TimeCaretPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ProfileEditorServiceOnPixelsPerSecondChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
NotifyOfPropertyChange(nameof(TimeCaretPosition));
|
||||||
|
}
|
||||||
|
|
||||||
#region View model managament
|
#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)
|
private void PopulateProperties(ProfileElement profileElement)
|
||||||
{
|
{
|
||||||
if (_lastSelectedLayer != null)
|
if (SelectedLayer != null)
|
||||||
{
|
{
|
||||||
_lastSelectedLayer.Properties.LayerPropertyRegistered -= LayerOnPropertyRegistered;
|
SelectedLayer.LayerBrushUpdated -= SelectedLayerOnLayerBrushUpdated;
|
||||||
_lastSelectedLayer.Properties.LayerPropertyRemoved -= LayerOnPropertyRemoved;
|
SelectedLayer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var layerPropertyGroupViewModel in LayerPropertyGroups)
|
||||||
|
layerPropertyGroupViewModel.Dispose();
|
||||||
|
LayerPropertyGroups.Clear();
|
||||||
|
|
||||||
if (profileElement is Layer layer)
|
if (profileElement is Layer layer)
|
||||||
{
|
{
|
||||||
// Create VMs for missing properties
|
SelectedLayer = layer;
|
||||||
foreach (var baseLayerProperty in layer.Properties)
|
SelectedLayer.LayerBrushUpdated += SelectedLayerOnLayerBrushUpdated;
|
||||||
{
|
|
||||||
if (_layerPropertyViewModels.All(vm => vm.LayerProperty != baseLayerProperty))
|
|
||||||
CreatePropertyViewModel(baseLayerProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove VMs for extra properties
|
// Add the built-in root groups of the layer
|
||||||
foreach (var layerPropertyViewModel in _layerPropertyViewModels.ToList())
|
var generalAttribute = Attribute.GetCustomAttribute(
|
||||||
{
|
layer.GetType().GetProperty(nameof(layer.General)),
|
||||||
if (layer.Properties.All(p => p != layerPropertyViewModel.LayerProperty))
|
typeof(PropertyGroupDescriptionAttribute)
|
||||||
RemovePropertyViewModel(layerPropertyViewModel);
|
);
|
||||||
}
|
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;
|
if (layer.LayerBrush != null)
|
||||||
layer.Properties.LayerPropertyRegistered += LayerOnPropertyRegistered;
|
{
|
||||||
layer.Properties.LayerPropertyRemoved += LayerOnPropertyRemoved;
|
// 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
|
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())
|
LayerPropertyGroups[2].Dispose();
|
||||||
RemovePropertyViewModel(layerPropertyViewModel);
|
LayerPropertyGroups.RemoveAt(2);
|
||||||
|
|
||||||
_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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var createdViewModel = _layerPropertyVmFactory.Create(layerProperty, parent);
|
if (SelectedLayer.LayerBrush != null)
|
||||||
_layerPropertyViewModels.Add(createdViewModel);
|
{
|
||||||
PropertyTree.AddLayerProperty(createdViewModel);
|
// Add the rout group of the brush
|
||||||
PropertyTimeline.AddLayerProperty(createdViewModel);
|
// The root group of the brush has no attribute so let's pull one out of our sleeve
|
||||||
|
var brushDescription = new PropertyGroupDescriptionAttribute
|
||||||
return createdViewModel;
|
{
|
||||||
}
|
Name = SelectedLayer.LayerBrush.Descriptor.DisplayName,
|
||||||
|
Description = SelectedLayer.LayerBrush.Descriptor.Description
|
||||||
private void RemovePropertyViewModel(LayerPropertyViewModel layerPropertyViewModel)
|
};
|
||||||
{
|
LayerPropertyGroups.Add(new LayerPropertyGroupViewModel(ProfileEditorService, SelectedLayer.LayerBrush.BaseProperties, brushDescription));
|
||||||
PropertyTree.RemoveLayerProperty(layerPropertyViewModel);
|
}
|
||||||
PropertyTimeline.RemoveLayerProperty(layerPropertyViewModel);
|
|
||||||
_layerPropertyViewModels.Remove(layerPropertyViewModel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -203,7 +176,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
|
|||||||
public void PlayFromStart()
|
public void PlayFromStart()
|
||||||
{
|
{
|
||||||
if (!Playing)
|
if (!Playing)
|
||||||
_profileEditorService.CurrentTime = TimeSpan.Zero;
|
ProfileEditorService.CurrentTime = TimeSpan.Zero;
|
||||||
|
|
||||||
Play();
|
Play();
|
||||||
}
|
}
|
||||||
@ -218,7 +191,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_coreService.FrameRendering += CoreServiceOnFrameRendering;
|
CoreService.FrameRendering += CoreServiceOnFrameRendering;
|
||||||
Playing = true;
|
Playing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,48 +200,55 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
|
|||||||
if (!Playing)
|
if (!Playing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_coreService.FrameRendering -= CoreServiceOnFrameRendering;
|
CoreService.FrameRendering -= CoreServiceOnFrameRendering;
|
||||||
Playing = false;
|
Playing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void GoToStart()
|
public void GoToStart()
|
||||||
{
|
{
|
||||||
_profileEditorService.CurrentTime = TimeSpan.Zero;
|
ProfileEditorService.CurrentTime = TimeSpan.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void GoToEnd()
|
public void GoToEnd()
|
||||||
{
|
{
|
||||||
_profileEditorService.CurrentTime = CalculateEndTime();
|
ProfileEditorService.CurrentTime = CalculateEndTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void GoToPreviousFrame()
|
public void GoToPreviousFrame()
|
||||||
{
|
{
|
||||||
var frameTime = 1000.0 / _settingsService.GetSetting("Core.TargetFrameRate", 25).Value;
|
var frameTime = 1000.0 / SettingsService.GetSetting("Core.TargetFrameRate", 25).Value;
|
||||||
var newTime = Math.Max(0, Math.Round((_profileEditorService.CurrentTime.TotalMilliseconds - frameTime) / frameTime) * frameTime);
|
var newTime = Math.Max(0, Math.Round((ProfileEditorService.CurrentTime.TotalMilliseconds - frameTime) / frameTime) * frameTime);
|
||||||
_profileEditorService.CurrentTime = TimeSpan.FromMilliseconds(newTime);
|
ProfileEditorService.CurrentTime = TimeSpan.FromMilliseconds(newTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void GoToNextFrame()
|
public void GoToNextFrame()
|
||||||
{
|
{
|
||||||
var frameTime = 1000.0 / _settingsService.GetSetting("Core.TargetFrameRate", 25).Value;
|
var frameTime = 1000.0 / SettingsService.GetSetting("Core.TargetFrameRate", 25).Value;
|
||||||
var newTime = Math.Round((_profileEditorService.CurrentTime.TotalMilliseconds + frameTime) / frameTime) * frameTime;
|
var newTime = Math.Round((ProfileEditorService.CurrentTime.TotalMilliseconds + frameTime) / frameTime) * frameTime;
|
||||||
newTime = Math.Min(newTime, CalculateEndTime().TotalMilliseconds);
|
newTime = Math.Min(newTime, CalculateEndTime().TotalMilliseconds);
|
||||||
_profileEditorService.CurrentTime = TimeSpan.FromMilliseconds(newTime);
|
ProfileEditorService.CurrentTime = TimeSpan.FromMilliseconds(newTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
private TimeSpan CalculateEndTime()
|
private TimeSpan CalculateEndTime()
|
||||||
{
|
{
|
||||||
// End time is the last keyframe + 10 sec
|
if (!(ProfileEditorService.SelectedProfileElement is Layer layer))
|
||||||
var lastKeyFrame = PropertyTimeline.PropertyTrackViewModels.SelectMany(r => r.KeyframeViewModels).OrderByDescending(t => t.Keyframe.Position).FirstOrDefault();
|
return TimeSpan.MaxValue;
|
||||||
return lastKeyFrame?.Keyframe.Position.Add(new TimeSpan(0, 0, 0, 10)) ?? 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)
|
private void CoreServiceOnFrameRendering(object sender, FrameRenderingEventArgs e)
|
||||||
{
|
{
|
||||||
Execute.PostToUIThread(() =>
|
Execute.PostToUIThread(() =>
|
||||||
{
|
{
|
||||||
var newTime = _profileEditorService.CurrentTime.Add(TimeSpan.FromSeconds(e.DeltaTime));
|
var newTime = ProfileEditorService.CurrentTime.Add(TimeSpan.FromSeconds(e.DeltaTime));
|
||||||
if (RepeatAfterLastKeyframe)
|
if (RepeatAfterLastKeyframe)
|
||||||
{
|
{
|
||||||
if (newTime > CalculateEndTime().Subtract(TimeSpan.FromSeconds(10)))
|
if (newTime > CalculateEndTime().Subtract(TimeSpan.FromSeconds(10)))
|
||||||
@ -280,7 +260,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
|
|||||||
Pause();
|
Pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
_profileEditorService.CurrentTime = newTime;
|
ProfileEditorService.CurrentTime = newTime;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,8 +268,6 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
|
|||||||
|
|
||||||
#region Caret movement
|
#region Caret movement
|
||||||
|
|
||||||
private int _pixelsPerSecond;
|
|
||||||
|
|
||||||
public void TimelineMouseDown(object sender, MouseButtonEventArgs e)
|
public void TimelineMouseDown(object sender, MouseButtonEventArgs e)
|
||||||
{
|
{
|
||||||
((IInputElement) sender).CaptureMouse();
|
((IInputElement) sender).CaptureMouse();
|
||||||
@ -307,44 +285,38 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
|
|||||||
// Get the parent grid, need that for our position
|
// Get the parent grid, need that for our position
|
||||||
var parent = (IInputElement) VisualTreeHelper.GetParent((DependencyObject) sender);
|
var parent = (IInputElement) VisualTreeHelper.GetParent((DependencyObject) sender);
|
||||||
var x = Math.Max(0, e.GetPosition(parent).X);
|
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
|
// 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);
|
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);
|
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 2.0) * 2.0);
|
||||||
else
|
else
|
||||||
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds));
|
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds));
|
||||||
|
|
||||||
if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift))
|
if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift))
|
||||||
{
|
{
|
||||||
_profileEditorService.CurrentTime = newTime;
|
ProfileEditorService.CurrentTime = newTime;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If shift is held, snap to closest keyframe
|
var visibleKeyframes = GetKeyframes(true);
|
||||||
var visibleKeyframes = PropertyTimeline.PropertyTrackViewModels
|
|
||||||
.Where(t => t.LayerPropertyViewModel.Parent != null && t.LayerPropertyViewModel.Parent.IsExpanded)
|
|
||||||
.SelectMany(t => t.KeyframeViewModels);
|
|
||||||
// Take a tolerance of 5 pixels (half a keyframe width)
|
// Take a tolerance of 5 pixels (half a keyframe width)
|
||||||
var tolerance = 1000f / PixelsPerSecond * 5;
|
var tolerance = 1000f / ProfileEditorService.PixelsPerSecond * 5;
|
||||||
var closeKeyframe = visibleKeyframes.FirstOrDefault(
|
var closeKeyframe = visibleKeyframes.FirstOrDefault(k => Math.Abs(k.Position.TotalMilliseconds - newTime.TotalMilliseconds) < tolerance);
|
||||||
kf => Math.Abs(kf.Keyframe.Position.TotalMilliseconds - newTime.TotalMilliseconds) < tolerance
|
ProfileEditorService.CurrentTime = closeKeyframe?.Position ?? newTime;
|
||||||
);
|
|
||||||
_profileEditorService.CurrentTime = closeKeyframe?.Keyframe.Position ?? newTime;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
private List<BaseLayerPropertyKeyframe> GetKeyframes(bool visibleOnly)
|
||||||
|
|
||||||
#region Events
|
|
||||||
|
|
||||||
public event EventHandler PixelsPerSecondChanged;
|
|
||||||
|
|
||||||
protected virtual void OnPixelsPerSecondChanged()
|
|
||||||
{
|
{
|
||||||
PixelsPerSecondChanged?.Invoke(this, EventArgs.Empty);
|
var result = new List<BaseLayerPropertyKeyframe>();
|
||||||
|
foreach (var layerPropertyGroupViewModel in LayerPropertyGroups)
|
||||||
|
result.AddRange(layerPropertyGroupViewModel.GetKeyframes(visibleOnly));
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#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.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Artemis.Core.Models.Profile.LayerProperties;
|
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 Artemis.UI.Services.Interfaces;
|
||||||
using Ninject;
|
using Humanizer;
|
||||||
using Stylet;
|
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
|
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
|
||||||
{
|
{
|
||||||
public class LayerPropertyViewModel : PropertyChangedBase
|
public class LayerPropertyViewModel<T> : LayerPropertyViewModel
|
||||||
{
|
{
|
||||||
private readonly IKernel _kernel;
|
public LayerPropertyViewModel(IProfileEditorService profileEditorService, LayerProperty<T> layerProperty, PropertyDescriptionAttribute propertyDescription)
|
||||||
private readonly IProfileEditorService _profileEditorService;
|
: base(profileEditorService, layerProperty)
|
||||||
private bool _keyframesEnabled;
|
|
||||||
private bool _isExpanded;
|
|
||||||
|
|
||||||
public LayerPropertyViewModel(BaseLayerProperty layerProperty, LayerPropertyViewModel parent, IKernel kernel, IProfileEditorService profileEditorService)
|
|
||||||
{
|
{
|
||||||
_kernel = kernel;
|
|
||||||
_profileEditorService = profileEditorService;
|
|
||||||
_keyframesEnabled = layerProperty.IsUsingKeyframes;
|
|
||||||
|
|
||||||
LayerProperty = layerProperty;
|
LayerProperty = layerProperty;
|
||||||
Parent = parent;
|
PropertyDescription = propertyDescription;
|
||||||
Children = new List<LayerPropertyViewModel>();
|
|
||||||
IsExpanded = layerProperty.ExpandByDefault;
|
|
||||||
|
|
||||||
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; }
|
// Generate a fallback name if the description does not contain one
|
||||||
public List<LayerPropertyViewModel> Children { get; }
|
if (PropertyDescription.Name == null)
|
||||||
|
|
||||||
public bool IsExpanded
|
|
||||||
{
|
|
||||||
get => _isExpanded;
|
|
||||||
set
|
|
||||||
{
|
{
|
||||||
_isExpanded = value;
|
var propertyInfo = LayerProperty.Parent?.GetType().GetProperties().FirstOrDefault(p => ReferenceEquals(p.GetValue(LayerProperty.Parent), LayerProperty));
|
||||||
OnExpandedStateChanged();
|
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;
|
return LayerProperty.BaseKeyframes.ToList();
|
||||||
set
|
|
||||||
{
|
|
||||||
_keyframesEnabled = value;
|
|
||||||
UpdateKeyframes();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public PropertyInputViewModel GetPropertyInputViewModel()
|
public override void Dispose()
|
||||||
{
|
{
|
||||||
// If the type is an enum type, search for Enum instead.
|
TreePropertyViewModel.Dispose();
|
||||||
var type = LayerProperty.Type;
|
TimelinePropertyViewModel.Dispose();
|
||||||
if (type.IsEnum)
|
|
||||||
type = typeof(Enum);
|
|
||||||
|
|
||||||
var match = _kernel.Get<List<PropertyInputViewModel>>().FirstOrDefault(p => p.CompatibleTypes.Contains(type));
|
LayerProperty.VisibilityChanged -= LayerPropertyOnVisibilityChanged;
|
||||||
if (match == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
match.Initialize(this);
|
|
||||||
return match;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateKeyframes()
|
public void SetCurrentValue(T value, bool saveChanges)
|
||||||
{
|
{
|
||||||
// Either create a new first keyframe or clear all the keyframes
|
LayerProperty.SetCurrentValue(value, ProfileEditorService.CurrentTime);
|
||||||
if (_keyframesEnabled)
|
if (saveChanges)
|
||||||
LayerProperty.CreateNewKeyframe(_profileEditorService.CurrentTime, LayerProperty.GetCurrentValue());
|
ProfileEditorService.UpdateSelectedProfileElement();
|
||||||
else
|
else
|
||||||
LayerProperty.ClearKeyframes();
|
ProfileEditorService.UpdateProfilePreview();
|
||||||
|
|
||||||
// Force the keyframe engine to update, the new keyframe is the current keyframe
|
|
||||||
LayerProperty.IsUsingKeyframes = _keyframesEnabled;
|
|
||||||
LayerProperty.KeyframeEngine?.Update(0);
|
|
||||||
|
|
||||||
_profileEditorService.UpdateSelectedProfileElement();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Events
|
private void LayerPropertyOnVisibilityChanged(object? sender, EventArgs e)
|
||||||
|
|
||||||
public event EventHandler<EventArgs> ExpandedStateChanged;
|
|
||||||
protected virtual void OnExpandedStateChanged()
|
|
||||||
{
|
{
|
||||||
ExpandedStateChanged?.Invoke(this, EventArgs.Empty);
|
NotifyOfPropertyChange(nameof(IsVisible));
|
||||||
foreach (var layerPropertyViewModel in Children)
|
}
|
||||||
layerPropertyViewModel.OnExpandedStateChanged();
|
}
|
||||||
|
|
||||||
|
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 System.Windows.Media;
|
||||||
using Artemis.Core.Utilities;
|
using Artemis.Core.Utilities;
|
||||||
using Humanizer;
|
using Humanizer;
|
||||||
using Stylet;
|
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
|
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;
|
private bool _isEasingModeSelected;
|
||||||
|
|
||||||
public PropertyTrackEasingViewModel(PropertyTrackKeyframeViewModel keyframeViewModel, Easings.Functions easingFunction)
|
public TimelineEasingViewModel(TimelineKeyframeViewModel keyframeViewModel, Easings.Functions easingFunction)
|
||||||
{
|
{
|
||||||
_keyframeViewModel = keyframeViewModel;
|
_keyframeViewModel = keyframeViewModel;
|
||||||
_isEasingModeSelected = keyframeViewModel.Keyframe.EasingFunction == easingFunction;
|
_isEasingModeSelected = keyframeViewModel.BaseLayerPropertyKeyframe.EasingFunction == easingFunction;
|
||||||
|
|
||||||
EasingFunction = easingFunction;
|
EasingFunction = easingFunction;
|
||||||
Description = easingFunction.Humanize();
|
Description = easingFunction.Humanize();
|
||||||
|
|
||||||
CreateGeometry();
|
CreateEasingPoints();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Easings.Functions EasingFunction { get; }
|
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();
|
EasingPoints = new PointCollection();
|
||||||
for (var i = 1; i <= 10; i++)
|
for (var i = 1; i <= 10; i++)
|
||||||
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
@ -10,37 +9,71 @@ using Stylet;
|
|||||||
|
|
||||||
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
|
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
|
||||||
{
|
{
|
||||||
public class PropertyTrackKeyframeViewModel : PropertyChangedBase
|
public class TimelineKeyframeViewModel<T> : TimelineKeyframeViewModel
|
||||||
{
|
{
|
||||||
private readonly IProfileEditorService _profileEditorService;
|
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;
|
_profileEditorService = profileEditorService;
|
||||||
|
LayerPropertyKeyframe = layerPropertyKeyframe;
|
||||||
PropertyTrackViewModel = propertyTrackViewModel;
|
|
||||||
Keyframe = keyframe;
|
|
||||||
EasingViewModels = new BindableCollection<PropertyTrackEasingViewModel>();
|
|
||||||
CreateEasingViewModels();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 bool IsSelected { get; set; }
|
||||||
public PropertyTrackViewModel PropertyTrackViewModel { get; }
|
|
||||||
public BaseKeyframe Keyframe { get; }
|
|
||||||
public BindableCollection<PropertyTrackEasingViewModel> EasingViewModels { get; set; }
|
|
||||||
public double X { get; set; }
|
public double X { get; set; }
|
||||||
public string Timestamp { get; set; }
|
public string Timestamp { get; set; }
|
||||||
|
|
||||||
public UIElement ParentView { get; set; }
|
public UIElement ParentView { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public void Update(int pixelsPerSecond)
|
public void Update(int pixelsPerSecond)
|
||||||
{
|
{
|
||||||
_pixelsPerSecond = pixelsPerSecond;
|
_pixelsPerSecond = pixelsPerSecond;
|
||||||
|
|
||||||
X = pixelsPerSecond * Keyframe.Position.TotalSeconds;
|
X = pixelsPerSecond * BaseLayerPropertyKeyframe.Position.TotalSeconds;
|
||||||
Timestamp = $"{Math.Floor(Keyframe.Position.TotalSeconds):00}.{Keyframe.Position.Milliseconds:000}";
|
Timestamp = $"{Math.Floor(BaseLayerPropertyKeyframe.Position.TotalSeconds):00}.{BaseLayerPropertyKeyframe.Position.Milliseconds:000}";
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Keyframe movement
|
#region Keyframe movement
|
||||||
@ -52,11 +85,11 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
|
|||||||
|
|
||||||
((IInputElement) sender).CaptureMouse();
|
((IInputElement) sender).CaptureMouse();
|
||||||
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift) && !IsSelected)
|
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))
|
else if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
|
||||||
PropertyTrackViewModel.PropertyTimelineViewModel.SelectKeyframe(this, false, true);
|
_timelineViewModel.SelectKeyframe(this, false, true);
|
||||||
else if (!IsSelected)
|
else if (!IsSelected)
|
||||||
PropertyTrackViewModel.PropertyTimelineViewModel.SelectKeyframe(this, false, false);
|
_timelineViewModel.SelectKeyframe(this, false, false);
|
||||||
|
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
@ -64,7 +97,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
|
|||||||
public void KeyframeMouseUp(object sender, MouseButtonEventArgs e)
|
public void KeyframeMouseUp(object sender, MouseButtonEventArgs e)
|
||||||
{
|
{
|
||||||
_profileEditorService.UpdateSelectedProfileElement();
|
_profileEditorService.UpdateSelectedProfileElement();
|
||||||
PropertyTrackViewModel.PropertyTimelineViewModel.ReleaseSelectedKeyframes();
|
_timelineViewModel.ReleaseSelectedKeyframes();
|
||||||
|
|
||||||
((IInputElement) sender).ReleaseMouseCapture();
|
((IInputElement) sender).ReleaseMouseCapture();
|
||||||
}
|
}
|
||||||
@ -72,7 +105,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
|
|||||||
public void KeyframeMouseMove(object sender, MouseEventArgs e)
|
public void KeyframeMouseMove(object sender, MouseEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.LeftButton == MouseButtonState.Pressed)
|
if (e.LeftButton == MouseButtonState.Pressed)
|
||||||
PropertyTrackViewModel.PropertyTimelineViewModel.MoveSelectedKeyframes(GetCursorTime(e.GetPosition(ParentView)));
|
_timelineViewModel.MoveSelectedKeyframes(GetCursorTime(e.GetPosition(ParentView)));
|
||||||
|
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
@ -105,37 +138,31 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
|
|||||||
|
|
||||||
#endregion
|
#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
|
#region Easing
|
||||||
|
|
||||||
|
public void ContextMenuOpening()
|
||||||
|
{
|
||||||
|
CreateEasingViewModels();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ContextMenuClosing()
|
||||||
|
{
|
||||||
|
EasingViewModels.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
private void CreateEasingViewModels()
|
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
|
// Set every selection to false except on the VM that made the change
|
||||||
foreach (var propertyTrackEasingViewModel in EasingViewModels.Where(vm => vm != easingViewModel))
|
foreach (var propertyTrackEasingViewModel in EasingViewModels.Where(vm => vm != easingViewModel))
|
||||||
propertyTrackEasingViewModel.IsEasingModeSelected = false;
|
propertyTrackEasingViewModel.IsEasingModeSelected = false;
|
||||||
|
|
||||||
|
|
||||||
_profileEditorService.UpdateSelectedProfileElement();
|
_profileEditorService.UpdateSelectedProfileElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,13 +178,13 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
|
|||||||
if (_movementReleased)
|
if (_movementReleased)
|
||||||
{
|
{
|
||||||
_movementReleased = false;
|
_movementReleased = false;
|
||||||
_startOffset = cursorTime - Keyframe.Position;
|
_startOffset = cursorTime - BaseLayerPropertyKeyframe.Position;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Keyframe.Position = cursorTime - _startOffset;
|
BaseLayerPropertyKeyframe.Position = cursorTime - _startOffset;
|
||||||
if (Keyframe.Position < TimeSpan.Zero)
|
if (BaseLayerPropertyKeyframe.Position < TimeSpan.Zero)
|
||||||
Keyframe.Position = TimeSpan.Zero;
|
BaseLayerPropertyKeyframe.Position = TimeSpan.Zero;
|
||||||
|
|
||||||
Update(_pixelsPerSecond);
|
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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
@ -7,13 +7,10 @@
|
|||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
xmlns:s="https://github.com/canton7/Stylet"
|
xmlns:s="https://github.com/canton7/Stylet"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DesignHeight="20" d:DesignWidth="600"
|
d:DesignHeight="450" d:DesignWidth="800"
|
||||||
d:DataContext="{d:DesignInstance local:PropertyTrackViewModel}">
|
d:DataContext="{d:DesignInstance local:TimelinePropertyViewModel}">
|
||||||
<Border Height="25"
|
<Border Height="25" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource MaterialDesignDivider}">
|
||||||
BorderThickness="0,0,0,1"
|
<ItemsControl ItemsSource="{Binding TimelineKeyframeViewModels}"
|
||||||
BorderBrush="{DynamicResource MaterialDesignDivider}"
|
|
||||||
Visibility="{Binding MustDisplay, Converter={StaticResource BoolToVisibilityConverter}}">
|
|
||||||
<ItemsControl ItemsSource="{Binding KeyframeViewModels}"
|
|
||||||
Background="{DynamicResource MaterialDesignToolBarBackground}"
|
Background="{DynamicResource MaterialDesignToolBarBackground}"
|
||||||
HorizontalAlignment="Left">
|
HorizontalAlignment="Left">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
@ -38,7 +35,9 @@
|
|||||||
s:View.ActionTarget="{Binding}"
|
s:View.ActionTarget="{Binding}"
|
||||||
MouseDown="{s:Action KeyframeMouseDown}"
|
MouseDown="{s:Action KeyframeMouseDown}"
|
||||||
MouseUp="{s:Action KeyframeMouseUp}"
|
MouseUp="{s:Action KeyframeMouseUp}"
|
||||||
MouseMove="{s:Action KeyframeMouseMove}">
|
MouseMove="{s:Action KeyframeMouseMove}"
|
||||||
|
ContextMenuOpening="{s:Action ContextMenuOpening}"
|
||||||
|
ContextMenuClosing="{s:Action ContextMenuClosing}">
|
||||||
<Ellipse.Style>
|
<Ellipse.Style>
|
||||||
<Style TargetType="{x:Type Ellipse}">
|
<Style TargetType="{x:Type Ellipse}">
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
@ -75,6 +74,9 @@
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem Header="Easing" ItemsSource="{Binding EasingViewModels}">
|
<MenuItem Header="Easing" ItemsSource="{Binding EasingViewModels}">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<materialDesign:PackIcon Kind="Creation" />
|
||||||
|
</MenuItem.Icon>
|
||||||
<MenuItem.ItemContainerStyle>
|
<MenuItem.ItemContainerStyle>
|
||||||
<Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource MaterialDesignMenuItem}">
|
<Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource MaterialDesignMenuItem}">
|
||||||
<Setter Property="IsCheckable" Value="True" />
|
<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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
@ -8,11 +8,11 @@
|
|||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DesignHeight="25"
|
d:DesignHeight="25"
|
||||||
d:DesignWidth="800"
|
d:DesignWidth="800"
|
||||||
d:DataContext="{d:DesignInstance local:PropertyTimelineViewModel}">
|
d:DataContext="{d:DesignInstance local:TimelineViewModel}">
|
||||||
<Grid Background="{DynamicResource MaterialDesignToolBarBackground}"
|
<Grid Background="{DynamicResource MaterialDesignToolBarBackground}"
|
||||||
MouseDown="{s:Action TimelineCanvasMouseDown}"
|
MouseDown="{s:Action TimelineCanvasMouseDown}"
|
||||||
MouseUp="{s:Action TimelineCanvasMouseUp}"
|
MouseUp="{s:Action TimelineCanvasMouseUp}"
|
||||||
MouseMove="{s:Action TimelineCanvasMouseMove}">
|
MouseMove="{s:Action TimelineCanvasMouseMove}">
|
||||||
<Grid.Triggers>
|
<Grid.Triggers>
|
||||||
<EventTrigger RoutedEvent="UIElement.MouseLeftButtonDown">
|
<EventTrigger RoutedEvent="UIElement.MouseLeftButtonDown">
|
||||||
<BeginStoryboard>
|
<BeginStoryboard>
|
||||||
@ -30,22 +30,23 @@
|
|||||||
</EventTrigger>
|
</EventTrigger>
|
||||||
</Grid.Triggers>
|
</Grid.Triggers>
|
||||||
|
|
||||||
<ItemsControl ItemsSource="{Binding PropertyTrackViewModels}"
|
<ItemsControl ItemsSource="{Binding LayerPropertyGroups}"
|
||||||
Width="{Binding Width}"
|
Width="{Binding Width}"
|
||||||
MinWidth="{Binding ActualWidth, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ScrollViewer}}"
|
MinWidth="{Binding ActualWidth, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ScrollViewer}}"
|
||||||
HorizontalAlignment="Left">
|
HorizontalAlignment="Left">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<ContentControl s:View.Model="{Binding}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" />
|
<ContentControl s:View.Model="{Binding TimelinePropertyGroupViewModel}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
|
||||||
<!-- Multi-selection rectangle -->
|
<!-- Multi-selection rectangle -->
|
||||||
<Path Data="{Binding SelectionRectangle}" Opacity="0"
|
<Path x:Name="MultiSelectionPath"
|
||||||
|
Data="{Binding SelectionRectangle}"
|
||||||
|
Opacity="0"
|
||||||
Stroke="{DynamicResource PrimaryHueLightBrush}"
|
Stroke="{DynamicResource PrimaryHueLightBrush}"
|
||||||
StrokeThickness="1"
|
StrokeThickness="1"
|
||||||
x:Name="MultiSelectionPath"
|
|
||||||
IsHitTestVisible="False">
|
IsHitTestVisible="False">
|
||||||
<Path.Fill>
|
<Path.Fill>
|
||||||
<SolidColorBrush Color="{DynamicResource Primary400}" Opacity="0.25" />
|
<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