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

Code style - Use file scoped namespaces

Code style - Ran code cleanup
This commit is contained in:
Robert 2022-08-21 11:36:15 +02:00
parent e68e16df4d
commit f6090dc296
691 changed files with 37308 additions and 38126 deletions

View File

@ -8,149 +8,148 @@ using Artemis.Core.Services.Core;
using Artemis.Core.SkiaSharp;
using Newtonsoft.Json;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// A few useful constant values
/// </summary>
public static class Constants
{
/// <summary>
/// A few useful constant values
/// The Artemis.Core assembly
/// </summary>
public static class Constants
public static readonly Assembly CoreAssembly = typeof(Constants).Assembly;
/// <summary>
/// The full path to the Artemis application folder
/// </summary>
public static readonly string ApplicationFolder = Path.GetDirectoryName(typeof(Constants).Assembly.Location)!;
/// <summary>
/// The full path to the Artemis executable
/// </summary>
public static readonly string ExecutablePath = Utilities.GetCurrentLocation();
/// <summary>
/// The base path for Artemis application data folder
/// </summary>
public static readonly string BaseFolder = Environment.GetFolderPath(OperatingSystem.IsWindows()
? Environment.SpecialFolder.CommonApplicationData
: Environment.SpecialFolder.LocalApplicationData);
/// <summary>
/// The full path to the Artemis data folder
/// </summary>
public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis");
/// <summary>
/// The full path to the Artemis logs folder
/// </summary>
public static readonly string LogsFolder = Path.Combine(DataFolder, "Logs");
/// <summary>
/// The full path to the Artemis plugins folder
/// </summary>
public static readonly string PluginsFolder = Path.Combine(DataFolder, "Plugins");
/// <summary>
/// The full path to the Artemis user layouts folder
/// </summary>
public static readonly string LayoutsFolder = Path.Combine(DataFolder, "User Layouts");
/// <summary>
/// The current API version for plugins
/// </summary>
public static readonly Version PluginApi = new(1, 0);
/// <summary>
/// The plugin info used by core components of Artemis
/// </summary>
public static readonly PluginInfo CorePluginInfo = new()
{
/// <summary>
/// The Artemis.Core assembly
/// </summary>
public static readonly Assembly CoreAssembly = typeof(Constants).Assembly;
Guid = Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff"), Name = "Artemis Core", Version = new Version(2, 0)
};
/// <summary>
/// The full path to the Artemis application folder
/// </summary>
public static readonly string ApplicationFolder = Path.GetDirectoryName(typeof(Constants).Assembly.Location)!;
/// <summary>
/// The full path to the Artemis executable
/// </summary>
public static readonly string ExecutablePath = Utilities.GetCurrentLocation();
/// <summary>
/// The base path for Artemis application data folder
/// </summary>
public static readonly string BaseFolder = Environment.GetFolderPath(OperatingSystem.IsWindows()
? Environment.SpecialFolder.CommonApplicationData
: Environment.SpecialFolder.LocalApplicationData);
/// <summary>
/// The full path to the Artemis data folder
/// </summary>
public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis");
/// <summary>
/// The full path to the Artemis logs folder
/// </summary>
public static readonly string LogsFolder = Path.Combine(DataFolder, "Logs");
/// <summary>
/// The full path to the Artemis plugins folder
/// </summary>
public static readonly string PluginsFolder = Path.Combine(DataFolder, "Plugins");
/// <summary>
/// The full path to the Artemis user layouts folder
/// </summary>
public static readonly string LayoutsFolder = Path.Combine(DataFolder, "User Layouts");
/// <summary>
/// The current API version for plugins
/// </summary>
public static readonly Version PluginApi = new Version(1, 0);
/// <summary>
/// The plugin info used by core components of Artemis
/// </summary>
public static readonly PluginInfo CorePluginInfo = new()
/// <summary>
/// The build information related to the currently running Artemis build
/// <para>Information is retrieved from <c>buildinfo.json</c></para>
/// </summary>
public static readonly BuildInfo BuildInfo = File.Exists(Path.Combine(ApplicationFolder, "buildinfo.json"))
? JsonConvert.DeserializeObject<BuildInfo>(File.ReadAllText(Path.Combine(ApplicationFolder, "buildinfo.json")))!
: new BuildInfo
{
Guid = Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff"), Name = "Artemis Core", Version = new Version(2, 0)
IsLocalBuild = true,
BuildId = 1337,
BuildNumber = 1337,
SourceBranch = "local",
SourceVersion = "local"
};
/// <summary>
/// The build information related to the currently running Artemis build
/// <para>Information is retrieved from <c>buildinfo.json</c></para>
/// </summary>
public static readonly BuildInfo BuildInfo = File.Exists(Path.Combine(ApplicationFolder, "buildinfo.json"))
? JsonConvert.DeserializeObject<BuildInfo>(File.ReadAllText(Path.Combine(ApplicationFolder, "buildinfo.json")))!
: new BuildInfo
{
IsLocalBuild = true,
BuildId = 1337,
BuildNumber = 1337,
SourceBranch = "local",
SourceVersion = "local"
};
/// <summary>
/// The plugin used by core components of Artemis
/// </summary>
public static readonly Plugin CorePlugin = new(CorePluginInfo, new DirectoryInfo(ApplicationFolder), null);
/// <summary>
/// The plugin used by core components of Artemis
/// </summary>
public static readonly Plugin CorePlugin = new(CorePluginInfo, new DirectoryInfo(ApplicationFolder), null);
internal static readonly CorePluginFeature CorePluginFeature = new() {Plugin = CorePlugin, Profiler = CorePlugin.GetProfiler("Feature - Core")};
internal static readonly EffectPlaceholderPlugin EffectPlaceholderPlugin = new() {Plugin = CorePlugin, Profiler = CorePlugin.GetProfiler("Feature - Effect Placeholder")};
internal static readonly CorePluginFeature CorePluginFeature = new() {Plugin = CorePlugin, Profiler = CorePlugin.GetProfiler("Feature - Core")};
internal static readonly EffectPlaceholderPlugin EffectPlaceholderPlugin = new() {Plugin = CorePlugin, Profiler = CorePlugin.GetProfiler("Feature - Effect Placeholder")};
internal static JsonSerializerSettings JsonConvertSettings = new()
{
Converters = new List<JsonConverter> {new SKColorConverter(), new NumericJsonConverter(), new ForgivingIntConverter()}
};
internal static JsonSerializerSettings JsonConvertSettings = new()
{
Converters = new List<JsonConverter> {new SKColorConverter(), new NumericJsonConverter(), new ForgivingIntConverter()}
};
internal static JsonSerializerSettings JsonConvertTypedSettings = new()
{
TypeNameHandling = TypeNameHandling.All,
Converters = new List<JsonConverter> {new SKColorConverter(), new NumericJsonConverter(), new ForgivingIntConverter()}
};
internal static JsonSerializerSettings JsonConvertTypedSettings = new()
{
TypeNameHandling = TypeNameHandling.All,
Converters = new List<JsonConverter> {new SKColorConverter(), new NumericJsonConverter(), new ForgivingIntConverter()}
};
/// <summary>
/// A read-only collection containing all primitive numeric types
/// </summary>
public static IReadOnlyCollection<Type> NumberTypes = new List<Type>
{
typeof(sbyte),
typeof(byte),
typeof(short),
typeof(ushort),
typeof(int),
typeof(uint),
typeof(long),
typeof(ulong),
typeof(float),
typeof(double),
typeof(decimal)
};
/// <summary>
/// A read-only collection containing all primitive numeric types
/// </summary>
public static IReadOnlyCollection<Type> NumberTypes = new List<Type>
{
typeof(sbyte),
typeof(byte),
typeof(short),
typeof(ushort),
typeof(int),
typeof(uint),
typeof(long),
typeof(ulong),
typeof(float),
typeof(double),
typeof(decimal)
};
/// <summary>
/// A read-only collection containing all primitive integral numeric types
/// </summary>
public static IReadOnlyCollection<Type> IntegralNumberTypes = new List<Type>
{
typeof(sbyte),
typeof(byte),
typeof(short),
typeof(ushort),
typeof(int),
typeof(uint),
typeof(long),
typeof(ulong)
};
/// <summary>
/// A read-only collection containing all primitive integral numeric types
/// </summary>
public static IReadOnlyCollection<Type> IntegralNumberTypes = new List<Type>
{
typeof(sbyte),
typeof(byte),
typeof(short),
typeof(ushort),
typeof(int),
typeof(uint),
typeof(long),
typeof(ulong)
};
/// <summary>
/// A read-only collection containing all primitive floating-point numeric types
/// </summary>
public static IReadOnlyCollection<Type> FloatNumberTypes = new List<Type>
{
typeof(float),
typeof(double),
typeof(decimal)
};
/// <summary>
/// A read-only collection containing all primitive floating-point numeric types
/// </summary>
public static IReadOnlyCollection<Type> FloatNumberTypes = new List<Type>
{
typeof(float),
typeof(double),
typeof(decimal)
};
/// <summary>
/// Gets the graphics context to be used for rendering by SkiaSharp. Can be set via
/// <see cref="IRgbService.UpdateGraphicsContext" />.
/// </summary>
public static IManagedGraphicsContext? ManagedGraphicsContext { get; internal set; }
}
/// <summary>
/// Gets the graphics context to be used for rendering by SkiaSharp. Can be set via
/// <see cref="IRgbService.UpdateGraphicsContext" />.
/// </summary>
public static IManagedGraphicsContext? ManagedGraphicsContext { get; internal set; }
}

View File

@ -1,31 +1,30 @@
namespace Artemis.Core
namespace Artemis.Core;
/// <inheritdoc />
public class BoolLayerProperty : LayerProperty<bool>
{
/// <inheritdoc />
public class BoolLayerProperty : LayerProperty<bool>
internal BoolLayerProperty()
{
internal BoolLayerProperty()
{
}
}
/// <inheritdoc />
protected override void OnInitialize()
{
KeyframesSupported = false;
DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value");
}
/// <summary>
/// Implicitly converts an <see cref="BoolLayerProperty" /> to a <see cref="bool" />
/// </summary>
public static implicit operator bool(BoolLayerProperty p)
{
return p.CurrentValue;
}
/// <summary>
/// Implicitly converts an <see cref="BoolLayerProperty" /> to a <see cref="bool" />
/// </summary>
public static implicit operator bool(BoolLayerProperty p)
{
return p.CurrentValue;
}
/// <inheritdoc />
protected override void OnInitialize()
{
KeyframesSupported = false;
DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value");
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
throw new ArtemisCoreException("Boolean properties do not support keyframes.");
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
throw new ArtemisCoreException("Boolean properties do not support keyframes.");
}
}

View File

@ -1,35 +1,34 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <inheritdoc />
public class EnumLayerProperty<T> : LayerProperty<T> where T : Enum
{
/// <inheritdoc />
public class EnumLayerProperty<T> : LayerProperty<T> where T : Enum
internal EnumLayerProperty()
{
internal EnumLayerProperty()
{
KeyframesSupported = false;
}
KeyframesSupported = false;
}
/// <summary>
/// Implicitly converts an <see cref="EnumLayerProperty{T}" /> to a <typeparamref name="T"/>
/// </summary>
public static implicit operator T(EnumLayerProperty<T> p)
{
return p.CurrentValue;
}
/// <summary>
/// Implicitly converts an <see cref="EnumLayerProperty{T}" /> to a <typeparamref name="T" />
/// </summary>
public static implicit operator T(EnumLayerProperty<T> p)
{
return p.CurrentValue;
}
/// <summary>
/// Implicitly converts an <see cref="EnumLayerProperty{T}" /> to an <see cref="int" />
/// </summary>
public static implicit operator int(EnumLayerProperty<T> p)
{
return Convert.ToInt32(p.CurrentValue);
}
/// <summary>
/// Implicitly converts an <see cref="EnumLayerProperty{T}" /> to an <see cref="int" />
/// </summary>
public static implicit operator int(EnumLayerProperty<T> p)
{
return Convert.ToInt32(p.CurrentValue);
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
throw new ArtemisCoreException("Enum properties do not support keyframes.");
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
throw new ArtemisCoreException("Enum properties do not support keyframes.");
}
}

View File

@ -1,39 +1,38 @@
namespace Artemis.Core
namespace Artemis.Core;
/// <inheritdoc />
public class FloatLayerProperty : LayerProperty<float>
{
/// <inheritdoc />
public class FloatLayerProperty : LayerProperty<float>
internal FloatLayerProperty()
{
internal FloatLayerProperty()
{
}
}
/// <inheritdoc />
protected override void OnInitialize()
{
DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value");
}
/// <summary>
/// Implicitly converts an <see cref="FloatLayerProperty" /> to a <see cref="float" />
/// </summary>
public static implicit operator float(FloatLayerProperty p)
{
return p.CurrentValue;
}
/// <summary>
/// Implicitly converts an <see cref="FloatLayerProperty" /> to a <see cref="float" />
/// </summary>
public static implicit operator float(FloatLayerProperty p)
{
return p.CurrentValue;
}
/// <summary>
/// Implicitly converts an <see cref="FloatLayerProperty" /> to a <see cref="double" />
/// </summary>
public static implicit operator double(FloatLayerProperty p)
{
return p.CurrentValue;
}
/// <summary>
/// Implicitly converts an <see cref="FloatLayerProperty" /> to a <see cref="double" />
/// </summary>
public static implicit operator double(FloatLayerProperty p)
{
return p.CurrentValue;
}
/// <inheritdoc />
protected override void OnInitialize()
{
DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value");
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
float diff = NextKeyframe!.Value - CurrentKeyframe!.Value;
CurrentValue = CurrentKeyframe!.Value + diff * keyframeProgressEased;
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
float diff = NextKeyframe!.Value - CurrentKeyframe!.Value;
CurrentValue = CurrentKeyframe!.Value + diff * keyframeProgressEased;
}
}

View File

@ -1,24 +1,23 @@
namespace Artemis.Core
namespace Artemis.Core;
/// <inheritdoc />
public class FloatRangeLayerProperty : LayerProperty<FloatRange>
{
/// <inheritdoc />
public class FloatRangeLayerProperty : LayerProperty<FloatRange>
protected override void OnInitialize()
{
/// <inheritdoc />
protected override void OnInitialize()
{
DataBinding.RegisterDataBindingProperty(() => CurrentValue.Start, value => CurrentValue = new FloatRange(value, CurrentValue.End), "Start");
DataBinding.RegisterDataBindingProperty(() => CurrentValue.End, value => CurrentValue = new FloatRange(CurrentValue.Start, value), "End");
}
DataBinding.RegisterDataBindingProperty(() => CurrentValue.Start, value => CurrentValue = new FloatRange(value, CurrentValue.End), "Start");
DataBinding.RegisterDataBindingProperty(() => CurrentValue.End, value => CurrentValue = new FloatRange(CurrentValue.Start, value), "End");
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
float startDiff = NextKeyframe!.Value.Start - CurrentKeyframe!.Value.Start;
float endDiff = NextKeyframe!.Value.End - CurrentKeyframe!.Value.End;
CurrentValue = new FloatRange(
(float) (CurrentKeyframe!.Value.Start + startDiff * keyframeProgressEased),
(float) (CurrentKeyframe!.Value.End + endDiff * keyframeProgressEased)
);
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
float startDiff = NextKeyframe!.Value.Start - CurrentKeyframe!.Value.Start;
float endDiff = NextKeyframe!.Value.End - CurrentKeyframe!.Value.End;
CurrentValue = new FloatRange(
CurrentKeyframe!.Value.Start + startDiff * keyframeProgressEased,
CurrentKeyframe!.Value.End + endDiff * keyframeProgressEased
);
}
}

View File

@ -1,49 +1,48 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <inheritdoc />
public class IntLayerProperty : LayerProperty<int>
{
/// <inheritdoc />
public class IntLayerProperty : LayerProperty<int>
internal IntLayerProperty()
{
internal IntLayerProperty()
{
}
}
/// <inheritdoc />
protected override void OnInitialize()
{
DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value");
}
/// <summary>
/// Implicitly converts an <see cref="IntLayerProperty" /> to an <see cref="int" />
/// </summary>
public static implicit operator int(IntLayerProperty p)
{
return p.CurrentValue;
}
/// <summary>
/// Implicitly converts an <see cref="IntLayerProperty" /> to an <see cref="int" />
/// </summary>
public static implicit operator int(IntLayerProperty p)
{
return p.CurrentValue;
}
/// <summary>
/// Implicitly converts an <see cref="IntLayerProperty" /> to a <see cref="float" />
/// </summary>
public static implicit operator float(IntLayerProperty p)
{
return p.CurrentValue;
}
/// <summary>
/// Implicitly converts an <see cref="IntLayerProperty" /> to a <see cref="float" />
/// </summary>
public static implicit operator float(IntLayerProperty p)
{
return p.CurrentValue;
}
/// <summary>
/// Implicitly converts an <see cref="IntLayerProperty" /> to a <see cref="double" />
/// </summary>
public static implicit operator double(IntLayerProperty p)
{
return p.CurrentValue;
}
/// <summary>
/// Implicitly converts an <see cref="IntLayerProperty" /> to a <see cref="double" />
/// </summary>
public static implicit operator double(IntLayerProperty p)
{
return p.CurrentValue;
}
/// <inheritdoc />
protected override void OnInitialize()
{
DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value");
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
int diff = NextKeyframe!.Value - CurrentKeyframe!.Value;
CurrentValue = (int) Math.Round(CurrentKeyframe!.Value + diff * keyframeProgressEased, MidpointRounding.AwayFromZero);
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
int diff = NextKeyframe!.Value - CurrentKeyframe!.Value;
CurrentValue = (int) Math.Round(CurrentKeyframe!.Value + diff * keyframeProgressEased, MidpointRounding.AwayFromZero);
}
}

View File

@ -1,24 +1,23 @@
namespace Artemis.Core
namespace Artemis.Core;
/// <inheritdoc />
public class IntRangeLayerProperty : LayerProperty<IntRange>
{
/// <inheritdoc />
public class IntRangeLayerProperty : LayerProperty<IntRange>
protected override void OnInitialize()
{
/// <inheritdoc />
protected override void OnInitialize()
{
DataBinding.RegisterDataBindingProperty(() => CurrentValue.Start, value => CurrentValue = new IntRange(value, CurrentValue.End), "Start");
DataBinding.RegisterDataBindingProperty(() => CurrentValue.End, value => CurrentValue = new IntRange(CurrentValue.Start, value), "End");
}
DataBinding.RegisterDataBindingProperty(() => CurrentValue.Start, value => CurrentValue = new IntRange(value, CurrentValue.End), "Start");
DataBinding.RegisterDataBindingProperty(() => CurrentValue.End, value => CurrentValue = new IntRange(CurrentValue.Start, value), "End");
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
float startDiff = NextKeyframe!.Value.Start - CurrentKeyframe!.Value.Start;
float endDiff = NextKeyframe!.Value.End - CurrentKeyframe!.Value.End;
CurrentValue = new IntRange(
(int) (CurrentKeyframe!.Value.Start + startDiff * keyframeProgressEased),
(int) (CurrentKeyframe!.Value.End + endDiff * keyframeProgressEased)
);
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
float startDiff = NextKeyframe!.Value.Start - CurrentKeyframe!.Value.Start;
float endDiff = NextKeyframe!.Value.End - CurrentKeyframe!.Value.End;
CurrentValue = new IntRange(
(int) (CurrentKeyframe!.Value.Start + startDiff * keyframeProgressEased),
(int) (CurrentKeyframe!.Value.End + endDiff * keyframeProgressEased)
);
}
}

View File

@ -1,27 +1,26 @@
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// A special layer property used to configure the selected layer brush
/// </summary>
public class LayerBrushReferenceLayerProperty : LayerProperty<LayerBrushReference?>
{
/// <summary>
/// A special layer property used to configure the selected layer brush
/// </summary>
public class LayerBrushReferenceLayerProperty : LayerProperty<LayerBrushReference?>
internal LayerBrushReferenceLayerProperty()
{
internal LayerBrushReferenceLayerProperty()
{
KeyframesSupported = false;
}
KeyframesSupported = false;
}
/// <summary>
/// Implicitly converts an <see cref="LayerBrushReferenceLayerProperty" /> to an <see cref="LayerBrushReference" />
/// </summary>
public static implicit operator LayerBrushReference?(LayerBrushReferenceLayerProperty p)
{
return p.CurrentValue;
}
/// <summary>
/// Implicitly converts an <see cref="LayerBrushReferenceLayerProperty" /> to an <see cref="LayerBrushReference" />
/// </summary>
public static implicit operator LayerBrushReference?(LayerBrushReferenceLayerProperty p)
{
return p.CurrentValue;
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
throw new ArtemisCoreException("Layer brush references do not support keyframes.");
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
throw new ArtemisCoreException("Layer brush references do not support keyframes.");
}
}

View File

@ -1,34 +1,33 @@
using SkiaSharp;
namespace Artemis.Core
namespace Artemis.Core;
/// <inheritdoc />
public class SKColorLayerProperty : LayerProperty<SKColor>
{
/// <inheritdoc />
public class SKColorLayerProperty : LayerProperty<SKColor>
internal SKColorLayerProperty()
{
internal SKColorLayerProperty()
{
}
}
/// <inheritdoc />
protected override void OnInitialize()
{
DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value");
}
/// <summary>
/// Implicitly converts an <see cref="SKColorLayerProperty" /> to an <see cref="SKColor" />¶
/// </summary>
/// <param name="p"></param>
/// <returns></returns>
public static implicit operator SKColor(SKColorLayerProperty p)
{
return p.CurrentValue;
}
/// <summary>
/// Implicitly converts an <see cref="SKColorLayerProperty" /> to an <see cref="SKColor" />¶
/// </summary>
/// <param name="p"></param>
/// <returns></returns>
public static implicit operator SKColor(SKColorLayerProperty p)
{
return p.CurrentValue;
}
/// <inheritdoc />
protected override void OnInitialize()
{
DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value");
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
CurrentValue = CurrentKeyframe!.Value.Interpolate(NextKeyframe!.Value, keyframeProgressEased);
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
CurrentValue = CurrentKeyframe!.Value.Interpolate(NextKeyframe!.Value, keyframeProgressEased);
}
}

View File

@ -1,35 +1,34 @@
using SkiaSharp;
namespace Artemis.Core
namespace Artemis.Core;
/// <inheritdoc />
public class SKPointLayerProperty : LayerProperty<SKPoint>
{
/// <inheritdoc />
public class SKPointLayerProperty : LayerProperty<SKPoint>
internal SKPointLayerProperty()
{
internal SKPointLayerProperty()
{
}
}
/// <inheritdoc />
protected override void OnInitialize()
{
DataBinding.RegisterDataBindingProperty(() => CurrentValue.X, value => CurrentValue = new SKPoint(value, CurrentValue.Y), "X");
DataBinding.RegisterDataBindingProperty(() => CurrentValue.Y, value => CurrentValue = new SKPoint(CurrentValue.X, value), "Y");
}
/// <summary>
/// Implicitly converts an <see cref="SKPointLayerProperty" /> to an <see cref="SKPoint" />
/// </summary>
public static implicit operator SKPoint(SKPointLayerProperty p)
{
return p.CurrentValue;
}
/// <summary>
/// Implicitly converts an <see cref="SKPointLayerProperty" /> to an <see cref="SKPoint" />
/// </summary>
public static implicit operator SKPoint(SKPointLayerProperty p)
{
return p.CurrentValue;
}
/// <inheritdoc />
protected override void OnInitialize()
{
DataBinding.RegisterDataBindingProperty(() => CurrentValue.X, value => CurrentValue = new SKPoint(value, CurrentValue.Y), "X");
DataBinding.RegisterDataBindingProperty(() => CurrentValue.Y, value => CurrentValue = new SKPoint(CurrentValue.X, value), "Y");
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
float xDiff = NextKeyframe!.Value.X - CurrentKeyframe!.Value.X;
float yDiff = NextKeyframe!.Value.Y - CurrentKeyframe!.Value.Y;
CurrentValue = new SKPoint(CurrentKeyframe!.Value.X + xDiff * keyframeProgressEased, CurrentKeyframe!.Value.Y + yDiff * keyframeProgressEased);
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
float xDiff = NextKeyframe!.Value.X - CurrentKeyframe!.Value.X;
float yDiff = NextKeyframe!.Value.Y - CurrentKeyframe!.Value.Y;
CurrentValue = new SKPoint(CurrentKeyframe!.Value.X + xDiff * keyframeProgressEased, CurrentKeyframe!.Value.Y + yDiff * keyframeProgressEased);
}
}

View File

@ -1,35 +1,34 @@
using SkiaSharp;
namespace Artemis.Core
namespace Artemis.Core;
/// <inheritdoc />
public class SKSizeLayerProperty : LayerProperty<SKSize>
{
/// <inheritdoc />
public class SKSizeLayerProperty : LayerProperty<SKSize>
internal SKSizeLayerProperty()
{
internal SKSizeLayerProperty()
{
}
}
/// <inheritdoc />
protected override void OnInitialize()
{
DataBinding.RegisterDataBindingProperty(() => CurrentValue.Width, (value) => CurrentValue = new SKSize(value, CurrentValue.Height), "Width");
DataBinding.RegisterDataBindingProperty(() => CurrentValue.Height, (value) => CurrentValue = new SKSize(CurrentValue.Width, value), "Height");
}
/// <summary>
/// Implicitly converts an <see cref="SKSizeLayerProperty" /> to an <see cref="SKSize" />
/// </summary>
public static implicit operator SKSize(SKSizeLayerProperty p)
{
return p.CurrentValue;
}
/// <summary>
/// Implicitly converts an <see cref="SKSizeLayerProperty" /> to an <see cref="SKSize" />
/// </summary>
public static implicit operator SKSize(SKSizeLayerProperty p)
{
return p.CurrentValue;
}
/// <inheritdoc />
protected override void OnInitialize()
{
DataBinding.RegisterDataBindingProperty(() => CurrentValue.Width, value => CurrentValue = new SKSize(value, CurrentValue.Height), "Width");
DataBinding.RegisterDataBindingProperty(() => CurrentValue.Height, value => CurrentValue = new SKSize(CurrentValue.Width, value), "Height");
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
float widthDiff = NextKeyframe!.Value.Width - CurrentKeyframe!.Value.Width;
float heightDiff = NextKeyframe!.Value.Height - CurrentKeyframe!.Value.Height;
CurrentValue = new SKSize(CurrentKeyframe!.Value.Width + widthDiff * keyframeProgressEased, CurrentKeyframe!.Value.Height + heightDiff * keyframeProgressEased);
}
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
float widthDiff = NextKeyframe!.Value.Width - CurrentKeyframe!.Value.Width;
float heightDiff = NextKeyframe!.Value.Height - CurrentKeyframe!.Value.Height;
CurrentValue = new SKSize(CurrentKeyframe!.Value.Width + widthDiff * keyframeProgressEased, CurrentKeyframe!.Value.Height + heightDiff * keyframeProgressEased);
}
}

View File

@ -1,20 +1,19 @@
using System;
namespace Artemis.Core
{
/// <summary>
/// Provides data about data model path related events
/// </summary>
public class DataModelPathEventArgs : EventArgs
{
internal DataModelPathEventArgs(DataModelPath dataModelPath)
{
DataModelPath = dataModelPath;
}
namespace Artemis.Core;
/// <summary>
/// Gets the data model path this event is related to
/// </summary>
public DataModelPath DataModelPath { get; }
/// <summary>
/// Provides data about data model path related events
/// </summary>
public class DataModelPathEventArgs : EventArgs
{
internal DataModelPathEventArgs(DataModelPath dataModelPath)
{
DataModelPath = dataModelPath;
}
/// <summary>
/// Gets the data model path this event is related to
/// </summary>
public DataModelPath DataModelPath { get; }
}

View File

@ -1,20 +1,19 @@
using System;
namespace Artemis.Core
{
/// <summary>
/// Provides data about device related events
/// </summary>
public class DeviceEventArgs : EventArgs
{
internal DeviceEventArgs(ArtemisDevice device)
{
Device = device;
}
namespace Artemis.Core;
/// <summary>
/// Gets the device this event is related to
/// </summary>
public ArtemisDevice Device { get; }
/// <summary>
/// Provides data about device related events
/// </summary>
public class DeviceEventArgs : EventArgs
{
internal DeviceEventArgs(ArtemisDevice device)
{
Device = device;
}
/// <summary>
/// Gets the device this event is related to
/// </summary>
public ArtemisDevice Device { get; }
}

View File

@ -1,27 +1,26 @@
using System;
using Artemis.Core.Modules;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Provides data about dynamic data model child related events
/// </summary>
public class DynamicDataModelChildEventArgs : EventArgs
{
/// <summary>
/// Provides data about dynamic data model child related events
/// </summary>
public class DynamicDataModelChildEventArgs : EventArgs
internal DynamicDataModelChildEventArgs(DynamicChild dynamicChild, string key)
{
internal DynamicDataModelChildEventArgs(DynamicChild dynamicChild, string key)
{
DynamicChild = dynamicChild;
Key = key;
}
/// <summary>
/// Gets the dynamic data model child
/// </summary>
public DynamicChild DynamicChild { get; }
/// <summary>
/// Gets the key of the dynamic data model on the parent <see cref="DataModel" />
/// </summary>
public string Key { get; }
DynamicChild = dynamicChild;
Key = key;
}
/// <summary>
/// Gets the dynamic data model child
/// </summary>
public DynamicChild DynamicChild { get; }
/// <summary>
/// Gets the key of the dynamic data model on the parent <see cref="DataModel" />
/// </summary>
public string Key { get; }
}

View File

@ -1,27 +1,26 @@
using System;
using RGB.NET.Core;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Provides data about frame rendering related events
/// </summary>
public class FrameRenderedEventArgs : EventArgs
{
/// <summary>
/// Provides data about frame rendering related events
/// </summary>
public class FrameRenderedEventArgs : EventArgs
internal FrameRenderedEventArgs(SKTexture texture, RGBSurface rgbSurface)
{
internal FrameRenderedEventArgs(SKTexture texture, RGBSurface rgbSurface)
{
Texture = texture;
RgbSurface = rgbSurface;
}
/// <summary>
/// Gets the texture used to render this frame
/// </summary>
public SKTexture Texture { get; }
/// <summary>
/// Gets the RGB surface used to render this frame
/// </summary>
public RGBSurface RgbSurface { get; }
Texture = texture;
RgbSurface = rgbSurface;
}
/// <summary>
/// Gets the texture used to render this frame
/// </summary>
public SKTexture Texture { get; }
/// <summary>
/// Gets the RGB surface used to render this frame
/// </summary>
public RGBSurface RgbSurface { get; }
}

View File

@ -2,33 +2,32 @@
using RGB.NET.Core;
using SkiaSharp;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Provides data about frame rendered related events
/// </summary>
public class FrameRenderingEventArgs : EventArgs
{
/// <summary>
/// Provides data about frame rendered related events
/// </summary>
public class FrameRenderingEventArgs : EventArgs
internal FrameRenderingEventArgs(SKCanvas canvas, double deltaTime, RGBSurface rgbSurface)
{
internal FrameRenderingEventArgs(SKCanvas canvas, double deltaTime, RGBSurface rgbSurface)
{
Canvas = canvas;
DeltaTime = deltaTime;
RgbSurface = rgbSurface;
}
/// <summary>
/// Gets the canvas this frame is rendering on
/// </summary>
public SKCanvas Canvas { get; }
/// <summary>
/// Gets the delta time since the last frame was rendered
/// </summary>
public double DeltaTime { get; }
/// <summary>
/// Gets the RGB surface used to render this frame
/// </summary>
public RGBSurface RgbSurface { get; }
Canvas = canvas;
DeltaTime = deltaTime;
RgbSurface = rgbSurface;
}
/// <summary>
/// Gets the canvas this frame is rendering on
/// </summary>
public SKCanvas Canvas { get; }
/// <summary>
/// Gets the delta time since the last frame was rendered
/// </summary>
public double DeltaTime { get; }
/// <summary>
/// Gets the RGB surface used to render this frame
/// </summary>
public RGBSurface RgbSurface { get; }
}

View File

@ -1,21 +1,20 @@
using System;
using Artemis.Core.Modules;
namespace Artemis.Core
{
/// <summary>
/// Provides data about module events
/// </summary>
public class ModuleEventArgs : EventArgs
{
internal ModuleEventArgs(Module module)
{
Module = module;
}
namespace Artemis.Core;
/// <summary>
/// Gets the module this event is related to
/// </summary>
public Module Module { get; }
/// <summary>
/// Provides data about module events
/// </summary>
public class ModuleEventArgs : EventArgs
{
internal ModuleEventArgs(Module module)
{
Module = module;
}
/// <summary>
/// Gets the module this event is related to
/// </summary>
public Module Module { get; }
}

View File

@ -1,20 +1,19 @@
using System;
namespace Artemis.Core
{
/// <summary>
/// Provides data about plugin related events
/// </summary>
public class PluginEventArgs : EventArgs
{
internal PluginEventArgs(Plugin plugin)
{
Plugin = plugin;
}
namespace Artemis.Core;
/// <summary>
/// Gets the plugin this event is related to
/// </summary>
public Plugin Plugin { get; }
/// <summary>
/// Provides data about plugin related events
/// </summary>
public class PluginEventArgs : EventArgs
{
internal PluginEventArgs(Plugin plugin)
{
Plugin = plugin;
}
/// <summary>
/// Gets the plugin this event is related to
/// </summary>
public Plugin Plugin { get; }
}

View File

@ -1,36 +1,35 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Provides data about plugin feature related events
/// </summary>
public class PluginFeatureEventArgs : EventArgs
{
/// <summary>
/// Provides data about plugin feature related events
/// </summary>
public class PluginFeatureEventArgs : EventArgs
internal PluginFeatureEventArgs(PluginFeature pluginFeature)
{
internal PluginFeatureEventArgs(PluginFeature pluginFeature)
{
PluginFeature = pluginFeature;
}
/// <summary>
/// Gets the plugin feature this event is related to
/// </summary>
public PluginFeature PluginFeature { get; }
PluginFeature = pluginFeature;
}
/// <summary>
/// Provides data about plugin feature info related events
/// Gets the plugin feature this event is related to
/// </summary>
public class PluginFeatureInfoEventArgs : EventArgs
{
internal PluginFeatureInfoEventArgs(PluginFeatureInfo pluginFeatureInfo)
{
PluginFeatureInfo = pluginFeatureInfo;
}
public PluginFeature PluginFeature { get; }
}
/// <summary>
/// Gets the plugin feature this event is related to
/// </summary>
public PluginFeatureInfo PluginFeatureInfo { get; }
/// <summary>
/// Provides data about plugin feature info related events
/// </summary>
public class PluginFeatureInfoEventArgs : EventArgs
{
internal PluginFeatureInfoEventArgs(PluginFeatureInfo pluginFeatureInfo)
{
PluginFeatureInfo = pluginFeatureInfo;
}
/// <summary>
/// Gets the plugin feature this event is related to
/// </summary>
public PluginFeatureInfo PluginFeatureInfo { get; }
}

View File

@ -1,20 +1,19 @@
using System;
namespace Artemis.Core
{
/// <summary>
/// Provides data for data binding events.
/// </summary>
public class DataBindingEventArgs : EventArgs
{
internal DataBindingEventArgs(IDataBinding dataBinding)
{
DataBinding = dataBinding;
}
namespace Artemis.Core;
/// <summary>
/// Gets the data binding this event is related to
/// </summary>
public IDataBinding DataBinding { get; }
/// <summary>
/// Provides data for data binding events.
/// </summary>
public class DataBindingEventArgs : EventArgs
{
internal DataBindingEventArgs(IDataBinding dataBinding)
{
DataBinding = dataBinding;
}
/// <summary>
/// Gets the data binding this event is related to
/// </summary>
public IDataBinding DataBinding { get; }
}

View File

@ -1,21 +1,20 @@
using System;
namespace Artemis.Core
{
/// <summary>
/// Provides data for the <see langword='DataBindingPropertyUpdatedEvent' /> event.
/// </summary>
/// <typeparam name="T"></typeparam>
public class DataBindingPropertyUpdatedEvent<T> : EventArgs
{
internal DataBindingPropertyUpdatedEvent(T value)
{
Value = value;
}
namespace Artemis.Core;
/// <summary>
/// The updated value that should be applied to the layer property
/// </summary>
public T Value { get; }
/// <summary>
/// Provides data for the <see langword='DataBindingPropertyUpdatedEvent' /> event.
/// </summary>
/// <typeparam name="T"></typeparam>
public class DataBindingPropertyUpdatedEvent<T> : EventArgs
{
internal DataBindingPropertyUpdatedEvent(T value)
{
Value = value;
}
/// <summary>
/// The updated value that should be applied to the layer property
/// </summary>
public T Value { get; }
}

View File

@ -1,20 +1,19 @@
using System;
namespace Artemis.Core
{
/// <summary>
/// Provides data for layer property events.
/// </summary>
public class LayerPropertyEventArgs : EventArgs
{
internal LayerPropertyEventArgs(ILayerProperty layerProperty)
{
LayerProperty = layerProperty;
}
namespace Artemis.Core;
/// <summary>
/// Gets the layer property this event is related to
/// </summary>
public ILayerProperty LayerProperty { get; }
/// <summary>
/// Provides data for layer property events.
/// </summary>
public class LayerPropertyEventArgs : EventArgs
{
internal LayerPropertyEventArgs(ILayerProperty layerProperty)
{
LayerProperty = layerProperty;
}
/// <summary>
/// Gets the layer property this event is related to
/// </summary>
public ILayerProperty LayerProperty { get; }
}

View File

@ -1,20 +1,19 @@
using System;
namespace Artemis.Core
{
/// <summary>
/// Provides data for profile configuration events.
/// </summary>
public class ProfileConfigurationEventArgs : EventArgs
{
internal ProfileConfigurationEventArgs(ProfileConfiguration profileConfiguration)
{
ProfileConfiguration = profileConfiguration;
}
namespace Artemis.Core;
/// <summary>
/// Gets the profile configuration this event is related to
/// </summary>
public ProfileConfiguration ProfileConfiguration { get; }
/// <summary>
/// Provides data for profile configuration events.
/// </summary>
public class ProfileConfigurationEventArgs : EventArgs
{
internal ProfileConfigurationEventArgs(ProfileConfiguration profileConfiguration)
{
ProfileConfiguration = profileConfiguration;
}
/// <summary>
/// Gets the profile configuration this event is related to
/// </summary>
public ProfileConfiguration ProfileConfiguration { get; }
}

View File

@ -1,20 +1,19 @@
using System;
namespace Artemis.Core
{
/// <summary>
/// Provides data for profile element events.
/// </summary>
public class ProfileElementEventArgs : EventArgs
{
internal ProfileElementEventArgs(ProfileElement profileElement)
{
ProfileElement = profileElement;
}
namespace Artemis.Core;
/// <summary>
/// Gets the profile element this event is related to
/// </summary>
public ProfileElement ProfileElement { get; }
/// <summary>
/// Provides data for profile element events.
/// </summary>
public class ProfileElementEventArgs : EventArgs
{
internal ProfileElementEventArgs(ProfileElement profileElement)
{
ProfileElement = profileElement;
}
/// <summary>
/// Gets the profile element this event is related to
/// </summary>
public ProfileElement ProfileElement { get; }
}

View File

@ -1,33 +1,32 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Provides data about application restart events
/// </summary>
public class RestartEventArgs : EventArgs
{
/// <summary>
/// Provides data about application restart events
/// </summary>
public class RestartEventArgs : EventArgs
internal RestartEventArgs(bool elevate, TimeSpan delay, List<string>? extraArgs)
{
internal RestartEventArgs(bool elevate, TimeSpan delay, List<string>? extraArgs)
{
Elevate = elevate;
Delay = delay;
ExtraArgs = extraArgs;
}
/// <summary>
/// Gets a boolean indicating whether the application should be restarted with elevated permissions
/// </summary>
public bool Elevate { get; }
/// <summary>
/// Gets the delay before killing process and restarting
/// </summary>
public TimeSpan Delay { get; }
/// <summary>
/// A list of extra arguments to pass to Artemis when restarting
/// </summary>
public List<string>? ExtraArgs { get; }
Elevate = elevate;
Delay = delay;
ExtraArgs = extraArgs;
}
/// <summary>
/// Gets a boolean indicating whether the application should be restarted with elevated permissions
/// </summary>
public bool Elevate { get; }
/// <summary>
/// Gets the delay before killing process and restarting
/// </summary>
public TimeSpan Delay { get; }
/// <summary>
/// A list of extra arguments to pass to Artemis when restarting
/// </summary>
public List<string>? ExtraArgs { get; }
}

View File

@ -1,12 +1,11 @@
namespace Artemis.Core
{
internal class DataModelStoreEvent
{
public DataModelStoreEvent(DataModelRegistration registration)
{
Registration = registration;
}
namespace Artemis.Core;
public DataModelRegistration Registration { get; }
internal class DataModelStoreEvent
{
public DataModelStoreEvent(DataModelRegistration registration)
{
Registration = registration;
}
public DataModelRegistration Registration { get; }
}

View File

@ -1,12 +1,11 @@
namespace Artemis.Core
{
internal class LayerBrushStoreEvent
{
public LayerBrushStoreEvent(LayerBrushRegistration registration)
{
Registration = registration;
}
namespace Artemis.Core;
public LayerBrushRegistration Registration { get; }
internal class LayerBrushStoreEvent
{
public LayerBrushStoreEvent(LayerBrushRegistration registration)
{
Registration = registration;
}
public LayerBrushRegistration Registration { get; }
}

View File

@ -1,12 +1,11 @@
namespace Artemis.Core
{
internal class LayerEffectStoreEvent
{
public LayerEffectStoreEvent(LayerEffectRegistration registration)
{
Registration = registration;
}
namespace Artemis.Core;
public LayerEffectRegistration Registration { get; }
internal class LayerEffectStoreEvent
{
public LayerEffectStoreEvent(LayerEffectRegistration registration)
{
Registration = registration;
}
public LayerEffectRegistration Registration { get; }
}

View File

@ -1,12 +1,11 @@
namespace Artemis.Core
{
internal class NodeTypeStoreEvent
{
public NodeTypeStoreEvent(NodeTypeRegistration typeRegistration)
{
TypeRegistration = typeRegistration;
}
namespace Artemis.Core;
public NodeTypeRegistration TypeRegistration { get; }
internal class NodeTypeStoreEvent
{
public NodeTypeStoreEvent(NodeTypeRegistration typeRegistration)
{
TypeRegistration = typeRegistration;
}
public NodeTypeRegistration TypeRegistration { get; }
}

View File

@ -1,21 +1,20 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core
{
/// <summary>
/// Provides data about device configuration related events
/// </summary>
public class SurfaceConfigurationEventArgs : EventArgs
{
internal SurfaceConfigurationEventArgs(List<ArtemisDevice> devices)
{
Devices = devices;
}
namespace Artemis.Core;
/// <summary>
/// Gets the current list of devices
/// </summary>
public List<ArtemisDevice> Devices { get; }
/// <summary>
/// Provides data about device configuration related events
/// </summary>
public class SurfaceConfigurationEventArgs : EventArgs
{
internal SurfaceConfigurationEventArgs(List<ArtemisDevice> devices)
{
Devices = devices;
}
/// <summary>
/// Gets the current list of devices
/// </summary>
public List<ArtemisDevice> Devices { get; }
}

View File

@ -1,18 +1,17 @@
using System;
namespace Artemis.Core
{
/// <summary>
/// Represents errors that occur within the Artemis Core
/// </summary>
public class ArtemisCoreException : Exception
{
internal ArtemisCoreException(string message) : base(message)
{
}
namespace Artemis.Core;
internal ArtemisCoreException(string message, Exception inner) : base(message, inner)
{
}
/// <summary>
/// Represents errors that occur within the Artemis Core
/// </summary>
public class ArtemisCoreException : Exception
{
internal ArtemisCoreException(string message) : base(message)
{
}
internal ArtemisCoreException(string message, Exception inner) : base(message, inner)
{
}
}

View File

@ -1,25 +1,24 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents SkiaSharp graphics-context related errors
/// </summary>
public class ArtemisGraphicsContextException : Exception
{
/// <summary>
/// Represents SkiaSharp graphics-context related errors
/// </summary>
public class ArtemisGraphicsContextException : Exception
/// <inheritdoc />
public ArtemisGraphicsContextException()
{
/// <inheritdoc />
public ArtemisGraphicsContextException()
{
}
}
/// <inheritdoc />
public ArtemisGraphicsContextException(string message) : base(message)
{
}
/// <inheritdoc />
public ArtemisGraphicsContextException(string message) : base(message)
{
}
/// <inheritdoc />
public ArtemisGraphicsContextException(string message, Exception innerException) : base(message, innerException)
{
}
/// <inheritdoc />
public ArtemisGraphicsContextException(string message, Exception innerException) : base(message, innerException)
{
}
}

View File

@ -1,53 +1,52 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// An exception thrown when a plugin-related error occurs
/// </summary>
public class ArtemisPluginException : Exception
{
/// <summary>
/// An exception thrown when a plugin-related error occurs
/// Creates a new instance of the <see cref="ArtemisPluginException" /> class
/// </summary>
public class ArtemisPluginException : Exception
public ArtemisPluginException(Plugin plugin)
{
/// <summary>
/// Creates a new instance of the <see cref="ArtemisPluginException" /> class
/// </summary>
public ArtemisPluginException(Plugin plugin)
{
Plugin = plugin;
}
/// <summary>
/// Creates a new instance of the <see cref="ArtemisPluginException" /> class
/// </summary>
public ArtemisPluginException(Plugin plugin, string message) : base(message)
{
Plugin = plugin;
}
/// <summary>
/// Creates a new instance of the <see cref="ArtemisPluginException" /> class
/// </summary>
public ArtemisPluginException(Plugin plugin, string message, Exception inner) : base(message, inner)
{
Plugin = plugin;
}
/// <summary>
/// Creates a new instance of the <see cref="ArtemisPluginException" /> class
/// </summary>
public ArtemisPluginException(string message) : base(message)
{
}
/// <summary>
/// Creates a new instance of the <see cref="ArtemisPluginException" /> class
/// </summary>
public ArtemisPluginException(string message, Exception inner) : base(message, inner)
{
}
/// <summary>
/// Gets the plugin the error is related to
/// </summary>
public Plugin? Plugin { get; }
Plugin = plugin;
}
/// <summary>
/// Creates a new instance of the <see cref="ArtemisPluginException" /> class
/// </summary>
public ArtemisPluginException(Plugin plugin, string message) : base(message)
{
Plugin = plugin;
}
/// <summary>
/// Creates a new instance of the <see cref="ArtemisPluginException" /> class
/// </summary>
public ArtemisPluginException(Plugin plugin, string message, Exception inner) : base(message, inner)
{
Plugin = plugin;
}
/// <summary>
/// Creates a new instance of the <see cref="ArtemisPluginException" /> class
/// </summary>
public ArtemisPluginException(string message) : base(message)
{
}
/// <summary>
/// Creates a new instance of the <see cref="ArtemisPluginException" /> class
/// </summary>
public ArtemisPluginException(string message, Exception inner) : base(message, inner)
{
}
/// <summary>
/// Gets the plugin the error is related to
/// </summary>
public Plugin? Plugin { get; }
}

View File

@ -1,30 +1,29 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// An exception thrown when a plugin feature-related error occurs
/// </summary>
public class ArtemisPluginFeatureException : Exception
{
/// <summary>
/// An exception thrown when a plugin feature-related error occurs
/// </summary>
public class ArtemisPluginFeatureException : Exception
internal ArtemisPluginFeatureException(PluginFeature pluginFeature)
{
internal ArtemisPluginFeatureException(PluginFeature pluginFeature)
{
PluginFeature = pluginFeature;
}
internal ArtemisPluginFeatureException(PluginFeature pluginFeature, string message) : base(message)
{
PluginFeature = pluginFeature;
}
internal ArtemisPluginFeatureException(PluginFeature pluginFeature, string message, Exception inner) : base(message, inner)
{
PluginFeature = pluginFeature;
}
/// <summary>
/// Gets the plugin feature the error is related to
/// </summary>
public PluginFeature PluginFeature { get; }
PluginFeature = pluginFeature;
}
internal ArtemisPluginFeatureException(PluginFeature pluginFeature, string message) : base(message)
{
PluginFeature = pluginFeature;
}
internal ArtemisPluginFeatureException(PluginFeature pluginFeature, string message, Exception inner) : base(message, inner)
{
PluginFeature = pluginFeature;
}
/// <summary>
/// Gets the plugin feature the error is related to
/// </summary>
public PluginFeature PluginFeature { get; }
}

View File

@ -1,21 +1,20 @@
using System;
namespace Artemis.Core
{
/// <summary>
/// An exception thrown when a plugin lock file error occurs
/// </summary>
public class ArtemisPluginLockException : Exception
{
internal ArtemisPluginLockException(Exception? innerException) : base(CreateExceptionMessage(innerException), innerException)
{
}
namespace Artemis.Core;
private static string CreateExceptionMessage(Exception? innerException)
{
return innerException != null
? "Found a lock file, skipping load, see inner exception for last known exception."
: "Found a lock file, skipping load.";
}
/// <summary>
/// An exception thrown when a plugin lock file error occurs
/// </summary>
public class ArtemisPluginLockException : Exception
{
internal ArtemisPluginLockException(Exception? innerException) : base(CreateExceptionMessage(innerException), innerException)
{
}
private static string CreateExceptionMessage(Exception? innerException)
{
return innerException != null
? "Found a lock file, skipping load, see inner exception for last known exception."
: "Found a lock file, skipping load.";
}
}

View File

@ -1,30 +1,29 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// An exception thrown when a plugin prerequisite-related error occurs
/// </summary>
public class ArtemisPluginPrerequisiteException : Exception
{
/// <summary>
/// An exception thrown when a plugin prerequisite-related error occurs
/// </summary>
public class ArtemisPluginPrerequisiteException : Exception
internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject)
{
internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject)
{
Subject = subject;
}
internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject, string message) : base(message)
{
Subject = subject;
}
internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject, string message, Exception inner) : base(message, inner)
{
Subject = subject;
}
/// <summary>
/// Gets the subject the error is related to
/// </summary>
public IPrerequisitesSubject Subject { get; }
Subject = subject;
}
internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject, string message) : base(message)
{
Subject = subject;
}
internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject, string message, Exception inner) : base(message, inner)
{
Subject = subject;
}
/// <summary>
/// Gets the subject the error is related to
/// </summary>
public IPrerequisitesSubject Subject { get; }
}

View File

@ -1,32 +1,31 @@
using System.IO;
namespace Artemis.Core
namespace Artemis.Core;
internal static class DirectoryInfoExtensions
{
internal static class DirectoryInfoExtensions
public static void CopyFilesRecursively(this DirectoryInfo source, DirectoryInfo target)
{
public static void CopyFilesRecursively(this DirectoryInfo source, DirectoryInfo target)
foreach (DirectoryInfo dir in source.GetDirectories())
CopyFilesRecursively(dir, target.CreateSubdirectory(dir.Name));
foreach (FileInfo file in source.GetFiles())
file.CopyTo(Path.Combine(target.FullName, file.Name));
}
public static void DeleteRecursively(this DirectoryInfo baseDir)
{
if (!baseDir.Exists)
return;
foreach (DirectoryInfo dir in baseDir.EnumerateDirectories())
DeleteRecursively(dir);
FileInfo[] files = baseDir.GetFiles();
foreach (FileInfo file in files)
{
foreach (DirectoryInfo dir in source.GetDirectories())
CopyFilesRecursively(dir, target.CreateSubdirectory(dir.Name));
foreach (FileInfo file in source.GetFiles())
file.CopyTo(Path.Combine(target.FullName, file.Name));
file.IsReadOnly = false;
file.Delete();
}
public static void DeleteRecursively(this DirectoryInfo baseDir)
{
if (!baseDir.Exists)
return;
foreach (DirectoryInfo dir in baseDir.EnumerateDirectories())
DeleteRecursively(dir);
FileInfo[] files = baseDir.GetFiles();
foreach (FileInfo file in files)
{
file.IsReadOnly = false;
file.Delete();
}
baseDir.Delete();
}
baseDir.Delete();
}
}

View File

@ -1,22 +1,21 @@
using System;
using System.Runtime.CompilerServices;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// A static class providing <see cref="double" /> extensions
/// </summary>
public static class DoubleExtensions
{
/// <summary>
/// A static class providing <see cref="double" /> extensions
/// Rounds the provided number away to zero and casts the result to an <see cref="int" />
/// </summary>
public static class DoubleExtensions
/// <param name="number">The number to round</param>
/// <returns>The rounded number as an integer</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int RoundToInt(this double number)
{
/// <summary>
/// Rounds the provided number away to zero and casts the result to an <see cref="int" />
/// </summary>
/// <param name="number">The number to round</param>
/// <returns>The rounded number as an integer</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int RoundToInt(this double number)
{
return (int) Math.Round(number, MidpointRounding.AwayFromZero);
}
return (int) Math.Round(number, MidpointRounding.AwayFromZero);
}
}

View File

@ -1,22 +1,21 @@
using System;
using System.Runtime.CompilerServices;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// A static class providing <see cref="float" /> extensions
/// </summary>
public static class FloatExtensions
{
/// <summary>
/// A static class providing <see cref="float" /> extensions
/// Rounds the provided number away to zero and casts the result to an <see cref="int" />
/// </summary>
public static class FloatExtensions
/// <param name="number">The number to round</param>
/// <returns>The rounded number as an integer</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int RoundToInt(this float number)
{
/// <summary>
/// Rounds the provided number away to zero and casts the result to an <see cref="int" />
/// </summary>
/// <param name="number">The number to round</param>
/// <returns>The rounded number as an integer</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int RoundToInt(this float number)
{
return (int) MathF.Round(number, MidpointRounding.AwayFromZero);
}
return (int) MathF.Round(number, MidpointRounding.AwayFromZero);
}
}

View File

@ -19,35 +19,33 @@
#endregion
using System;
using System.Collections.Generic;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// A static class providing <see cref="IEnumerable{T}" /> extensions
/// </summary>
// ReSharper disable once InconsistentNaming
public static class IEnumerableExtensions
{
/// <summary>
/// A static class providing <see cref="IEnumerable{T}" /> extensions
/// Returns the index of the provided element inside the read only collection
/// </summary>
// ReSharper disable once InconsistentNaming
public static class IEnumerableExtensions
/// <typeparam name="T">The type of element to find</typeparam>
/// <param name="self">The collection to search in</param>
/// <param name="elementToFind">The element to find</param>
/// <returns>If found, the index of the element to find; otherwise -1</returns>
public static int IndexOf<T>(this IReadOnlyCollection<T> self, T elementToFind)
{
/// <summary>
/// Returns the index of the provided element inside the read only collection
/// </summary>
/// <typeparam name="T">The type of element to find</typeparam>
/// <param name="self">The collection to search in</param>
/// <param name="elementToFind">The element to find</param>
/// <returns>If found, the index of the element to find; otherwise -1</returns>
public static int IndexOf<T>(this IReadOnlyCollection<T> self, T elementToFind)
int i = 0;
foreach (T element in self)
{
int i = 0;
foreach (T element in self)
{
if (Equals(element, elementToFind))
return i;
i++;
}
return -1;
if (Equals(element, elementToFind))
return i;
i++;
}
return -1;
}
}

View File

@ -1,41 +1,41 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Text;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// A static class providing <see cref="Process" /> extensions
/// </summary>
[SuppressMessage("Design", "CA1060:Move pinvokes to native methods class", Justification = "I don't care, piss off")]
public static class ProcessExtensions
{
/// <summary>
/// A static class providing <see cref="Process" /> extensions
/// Gets the file name of the given process
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1060:Move pinvokes to native methods class", Justification = "I don't care, piss off")]
public static class ProcessExtensions
/// <param name="p">The process</param>
/// <returns>The filename of the given process</returns>
public static string GetProcessFilename(this Process p)
{
/// <summary>
/// Gets the file name of the given process
/// </summary>
/// <param name="p">The process</param>
/// <returns>The filename of the given process</returns>
public static string GetProcessFilename(this Process p)
{
int capacity = 2000;
StringBuilder builder = new(capacity);
IntPtr ptr = OpenProcess(ProcessAccessFlags.QueryLimitedInformation, false, p.Id);
if (!QueryFullProcessImageName(ptr, 0, builder, ref capacity)) return string.Empty;
int capacity = 2000;
StringBuilder builder = new(capacity);
IntPtr ptr = OpenProcess(ProcessAccessFlags.QueryLimitedInformation, false, p.Id);
if (!QueryFullProcessImageName(ptr, 0, builder, ref capacity)) return string.Empty;
return builder.ToString();
}
return builder.ToString();
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern bool QueryFullProcessImageName([In] IntPtr hProcess, [In] int dwFlags, [Out] StringBuilder lpExeName, ref int lpdwSize);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern bool QueryFullProcessImageName([In] IntPtr hProcess, [In] int dwFlags, [Out] StringBuilder lpExeName, ref int lpdwSize);
[DllImport("kernel32.dll")]
private static extern IntPtr OpenProcess(ProcessAccessFlags processAccess, bool bInheritHandle, int processId);
[DllImport("kernel32.dll")]
private static extern IntPtr OpenProcess(ProcessAccessFlags processAccess, bool bInheritHandle, int processId);
[Flags]
private enum ProcessAccessFlags : uint
{
QueryLimitedInformation = 0x00001000
}
[Flags]
private enum ProcessAccessFlags : uint
{
QueryLimitedInformation = 0x00001000
}
}

View File

@ -2,39 +2,38 @@
using RGB.NET.Core;
using SkiaSharp;
namespace Artemis.Core
namespace Artemis.Core;
internal static class RgbDeviceExtensions
{
internal static class RgbDeviceExtensions
public static string GetDeviceIdentifier(this IRGBDevice rgbDevice)
{
public static string GetDeviceIdentifier(this IRGBDevice rgbDevice)
{
StringBuilder builder = new();
builder.Append(rgbDevice.DeviceInfo.DeviceName);
builder.Append('-');
builder.Append(rgbDevice.DeviceInfo.Manufacturer);
builder.Append('-');
builder.Append(rgbDevice.DeviceInfo.Model);
builder.Append('-');
builder.Append(rgbDevice.DeviceInfo.DeviceType);
return builder.ToString();
}
StringBuilder builder = new();
builder.Append(rgbDevice.DeviceInfo.DeviceName);
builder.Append('-');
builder.Append(rgbDevice.DeviceInfo.Manufacturer);
builder.Append('-');
builder.Append(rgbDevice.DeviceInfo.Model);
builder.Append('-');
builder.Append(rgbDevice.DeviceInfo.DeviceType);
return builder.ToString();
}
}
internal static class RgbRectangleExtensions
{
public static SKRect ToSKRect(this Rectangle rectangle)
{
return SKRect.Create(
rectangle.Location.X,
rectangle.Location.Y,
rectangle.Size.Width,
rectangle.Size.Height
);
}
internal static class RgbRectangleExtensions
public static SKRectI ToSKRectI(this Rectangle rectangle)
{
public static SKRect ToSKRect(this Rectangle rectangle)
{
return SKRect.Create(
rectangle.Location.X,
rectangle.Location.Y,
rectangle.Size.Width,
rectangle.Size.Height
);
}
public static SKRectI ToSKRectI(this Rectangle rectangle)
{
return SKRectI.Round(ToSKRect(rectangle));
}
return SKRectI.Round(ToSKRect(rectangle));
}
}

View File

@ -2,77 +2,76 @@
using RGB.NET.Core;
using SkiaSharp;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// A static class providing <see cref="SKColor" /> extensions
/// </summary>
public static class SKColorExtensions
{
/// <summary>
/// A static class providing <see cref="SKColor" /> extensions
/// Converts hte SKColor to an RGB.NET color
/// </summary>
public static class SKColorExtensions
/// <param name="color">The color to convert</param>
/// <returns>The RGB.NET color</returns>
public static Color ToRgbColor(this SKColor color)
{
/// <summary>
/// Converts hte SKColor to an RGB.NET color
/// </summary>
/// <param name="color">The color to convert</param>
/// <returns>The RGB.NET color</returns>
public static Color ToRgbColor(this SKColor color)
{
return new Color(color.Alpha, color.Red, color.Green, color.Blue);
}
return new Color(color.Alpha, color.Red, color.Green, color.Blue);
}
/// <summary>
/// Interpolates a color between the <paramref name="from" /> and <paramref name="to" /> color.
/// </summary>
/// <param name="from">The first color</param>
/// <param name="to">The second color</param>
/// <param name="progress">A value between 0 and 1</param>
/// <returns>The interpolated color</returns>
public static SKColor Interpolate(this SKColor from, SKColor to, float progress)
{
int redDiff = to.Red - from.Red;
int greenDiff = to.Green - from.Green;
int blueDiff = to.Blue - from.Blue;
int alphaDiff = to.Alpha - from.Alpha;
/// <summary>
/// Interpolates a color between the <paramref name="from" /> and <paramref name="to" /> color.
/// </summary>
/// <param name="from">The first color</param>
/// <param name="to">The second color</param>
/// <param name="progress">A value between 0 and 1</param>
/// <returns>The interpolated color</returns>
public static SKColor Interpolate(this SKColor from, SKColor to, float progress)
{
int redDiff = to.Red - from.Red;
int greenDiff = to.Green - from.Green;
int blueDiff = to.Blue - from.Blue;
int alphaDiff = to.Alpha - from.Alpha;
return new SKColor(
ClampToByte(from.Red + redDiff * progress),
ClampToByte(from.Green + greenDiff * progress),
ClampToByte(from.Blue + blueDiff * progress),
ClampToByte(from.Alpha + alphaDiff * progress)
);
}
return new SKColor(
ClampToByte(from.Red + redDiff * progress),
ClampToByte(from.Green + greenDiff * progress),
ClampToByte(from.Blue + blueDiff * progress),
ClampToByte(from.Alpha + alphaDiff * progress)
);
}
/// <summary>
/// Adds the two colors together
/// </summary>
/// <param name="a">The first color</param>
/// <param name="b">The second color</param>
/// <returns>The sum of the two colors</returns>
public static SKColor Sum(this SKColor a, SKColor b)
{
return new SKColor(
ClampToByte(a.Red + b.Red),
ClampToByte(a.Green + b.Green),
ClampToByte(a.Blue + b.Blue),
ClampToByte(a.Alpha + b.Alpha)
);
}
/// <summary>
/// Adds the two colors together
/// </summary>
/// <param name="a">The first color</param>
/// <param name="b">The second color</param>
/// <returns>The sum of the two colors</returns>
public static SKColor Sum(this SKColor a, SKColor b)
{
return new SKColor(
ClampToByte(a.Red + b.Red),
ClampToByte(a.Green + b.Green),
ClampToByte(a.Blue + b.Blue),
ClampToByte(a.Alpha + b.Alpha)
);
}
/// <summary>
/// Darkens the color by the specified amount
/// </summary>
/// <param name="c">The color to darken</param>
/// <param name="amount">The brightness of the new color</param>
/// <returns>The darkened color</returns>
public static SKColor Darken(this SKColor c, float amount)
{
c.ToHsl(out float h, out float s, out float l);
l *= 1f - amount;
return SKColor.FromHsl(h, s, l);
}
/// <summary>
/// Darkens the color by the specified amount
/// </summary>
/// <param name="c">The color to darken</param>
/// <param name="amount">The brightness of the new color</param>
/// <returns>The darkened color</returns>
public static SKColor Darken(this SKColor c, float amount)
{
c.ToHsl(out float h, out float s, out float l);
l *= 1f - amount;
return SKColor.FromHsl(h, s, l);
}
private static byte ClampToByte(float value)
{
return (byte) Math.Clamp(value, 0, 255);
}
private static byte ClampToByte(float value)
{
return (byte) Math.Clamp(value, 0, 255);
}
}

View File

@ -1,16 +1,15 @@
using SkiaSharp;
namespace Artemis.Core
namespace Artemis.Core;
internal static class SKPaintExtensions
{
internal static class SKPaintExtensions
internal static void DisposeSelfAndProperties(this SKPaint paint)
{
internal static void DisposeSelfAndProperties(this SKPaint paint)
{
paint.ImageFilter?.Dispose();
paint.ColorFilter?.Dispose();
paint.MaskFilter?.Dispose();
paint.Shader?.Dispose();
paint.Dispose();
}
paint.ImageFilter?.Dispose();
paint.ColorFilter?.Dispose();
paint.MaskFilter?.Dispose();
paint.Shader?.Dispose();
paint.Dispose();
}
}

View File

@ -25,110 +25,108 @@
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.Core
namespace Artemis.Core;
internal static class StreamExtensions
{
internal static class StreamExtensions
private const int DefaultBufferSize = 81920;
/// <summary>
/// Copies a stream to another stream
/// </summary>
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
/// <param name="sourceLength">The length of the source stream, if known - used for progress reporting</param>
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
/// <param name="bufferSize">The size of the copy block buffer</param>
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>A task representing the operation</returns>
public static async Task CopyToAsync(
this Stream source,
long sourceLength,
Stream destination,
int bufferSize,
IProgress<(long, long)> progress,
CancellationToken cancellationToken)
{
private const int DefaultBufferSize = 81920;
if (source == null)
throw new ArgumentNullException(nameof(source));
if (!source.CanRead)
throw new ArgumentException("Has to be readable", nameof(source));
if (destination == null)
throw new ArgumentNullException(nameof(destination));
if (!destination.CanWrite)
throw new ArgumentException("Has to be writable", nameof(destination));
if (bufferSize <= 0)
bufferSize = DefaultBufferSize;
/// <summary>
/// Copies a stream to another stream
/// </summary>
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
/// <param name="sourceLength">The length of the source stream, if known - used for progress reporting</param>
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
/// <param name="bufferSize">The size of the copy block buffer</param>
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>A task representing the operation</returns>
public static async Task CopyToAsync(
this Stream source,
long sourceLength,
Stream destination,
int bufferSize,
IProgress<(long, long)> progress,
CancellationToken cancellationToken)
byte[] buffer = new byte[bufferSize];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (!source.CanRead)
throw new ArgumentException("Has to be readable", nameof(source));
if (destination == null)
throw new ArgumentNullException(nameof(destination));
if (!destination.CanWrite)
throw new ArgumentException("Has to be writable", nameof(destination));
if (bufferSize <= 0)
bufferSize = DefaultBufferSize;
byte[] buffer = new byte[bufferSize];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
{
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
progress?.Report((totalBytesRead, sourceLength));
}
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
progress?.Report((totalBytesRead, sourceLength));
cancellationToken.ThrowIfCancellationRequested();
}
/// <summary>
/// Copies a stream to another stream
/// </summary>
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
/// <param name="sourceLength">The length of the source stream, if known - used for progress reporting</param>
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>A task representing the operation</returns>
public static Task CopyToAsync(this Stream source, long sourceLength, Stream destination, IProgress<(long, long)> progress, CancellationToken cancellationToken)
{
return CopyToAsync(source, sourceLength, destination, 0, progress, cancellationToken);
}
progress?.Report((totalBytesRead, sourceLength));
cancellationToken.ThrowIfCancellationRequested();
}
/// <summary>
/// Copies a stream to another stream
/// </summary>
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>A task representing the operation</returns>
public static Task CopyToAsync(this Stream source, Stream destination, IProgress<(long, long)> progress, CancellationToken cancellationToken)
{
return CopyToAsync(source, 0L, destination, 0, progress, cancellationToken);
}
/// <summary>
/// Copies a stream to another stream
/// </summary>
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
/// <param name="sourceLength">The length of the source stream, if known - used for progress reporting</param>
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>A task representing the operation</returns>
public static Task CopyToAsync(this Stream source, long sourceLength, Stream destination, IProgress<(long, long)> progress, CancellationToken cancellationToken)
{
return CopyToAsync(source, sourceLength, destination, 0, progress, cancellationToken);
}
/// <summary>
/// Copies a stream to another stream
/// </summary>
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
/// <param name="sourceLength">The length of the source stream, if known - used for progress reporting</param>
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
/// <returns>A task representing the operation</returns>
public static Task CopyToAsync(this Stream source, long sourceLength, Stream destination, IProgress<(long, long)> progress)
{
return CopyToAsync(source, sourceLength, destination, 0, progress, default);
}
/// <summary>
/// Copies a stream to another stream
/// </summary>
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>A task representing the operation</returns>
public static Task CopyToAsync(this Stream source, Stream destination, IProgress<(long, long)> progress, CancellationToken cancellationToken)
{
return CopyToAsync(source, 0L, destination, 0, progress, cancellationToken);
}
/// <summary>
/// Copies a stream to another stream
/// </summary>
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
/// <returns>A task representing the operation</returns>
public static Task CopyToAsync(this Stream source, Stream destination, IProgress<(long, long)> progress)
{
return CopyToAsync(source, 0L, destination, 0, progress, default);
}
/// <summary>
/// Copies a stream to another stream
/// </summary>
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
/// <param name="sourceLength">The length of the source stream, if known - used for progress reporting</param>
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
/// <returns>A task representing the operation</returns>
public static Task CopyToAsync(this Stream source, long sourceLength, Stream destination, IProgress<(long, long)> progress)
{
return CopyToAsync(source, sourceLength, destination, 0, progress, default);
}
/// <summary>
/// Copies a stream to another stream
/// </summary>
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
/// <returns>A task representing the operation</returns>
public static Task CopyToAsync(this Stream source, Stream destination, IProgress<(long, long)> progress)
{
return CopyToAsync(source, 0L, destination, 0, progress, default);
}
}

View File

@ -5,249 +5,250 @@ using System.Linq;
using System.Reflection;
using Humanizer;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// A static class providing <see cref="Type" /> extensions
/// </summary>
public static class TypeExtensions
{
/// <summary>
/// A static class providing <see cref="Type" /> extensions
/// </summary>
public static class TypeExtensions
private static readonly Dictionary<Type, List<Type>> PrimitiveTypeConversions = new()
{
private static readonly Dictionary<Type, List<Type>> PrimitiveTypeConversions = new()
{
{typeof(decimal), new List<Type> {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char)}},
{typeof(double), new List<Type> {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float)}},
{typeof(float), new List<Type> {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float)}},
{typeof(ulong), new List<Type> {typeof(byte), typeof(ushort), typeof(uint), typeof(char)}},
{typeof(long), new List<Type> {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(char)}},
{typeof(uint), new List<Type> {typeof(byte), typeof(ushort), typeof(char)}},
{typeof(int), new List<Type> {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(char)}},
{typeof(ushort), new List<Type> {typeof(byte), typeof(char)}},
{typeof(short), new List<Type> {typeof(byte)}}
};
{typeof(decimal), new List<Type> {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char)}},
{typeof(double), new List<Type> {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float)}},
{typeof(float), new List<Type> {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float)}},
{typeof(ulong), new List<Type> {typeof(byte), typeof(ushort), typeof(uint), typeof(char)}},
{typeof(long), new List<Type> {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(char)}},
{typeof(uint), new List<Type> {typeof(byte), typeof(ushort), typeof(char)}},
{typeof(int), new List<Type> {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(char)}},
{typeof(ushort), new List<Type> {typeof(byte), typeof(char)}},
{typeof(short), new List<Type> {typeof(byte)}}
};
private static readonly Dictionary<Type, string> TypeKeywords = new()
{
{typeof(bool), "bool"},
{typeof(byte), "byte"},
{typeof(sbyte), "sbyte"},
{typeof(char), "char"},
{typeof(decimal), "decimal"},
{typeof(double), "double"},
{typeof(float), "float"},
{typeof(int), "int"},
{typeof(uint), "uint"},
{typeof(long), "long"},
{typeof(ulong), "ulong"},
{typeof(short), "short"},
{typeof(ushort), "ushort"},
{typeof(object), "object"},
{typeof(string), "string"}
};
private static readonly Dictionary<Type, string> TypeKeywords = new()
{
{typeof(bool), "bool"},
{typeof(byte), "byte"},
{typeof(sbyte), "sbyte"},
{typeof(char), "char"},
{typeof(decimal), "decimal"},
{typeof(double), "double"},
{typeof(float), "float"},
{typeof(int), "int"},
{typeof(uint), "uint"},
{typeof(long), "long"},
{typeof(ulong), "ulong"},
{typeof(short), "short"},
{typeof(ushort), "ushort"},
{typeof(object), "object"},
{typeof(string), "string"}
};
/// <summary>
/// Determines whether the provided type is of a specified generic type
/// </summary>
/// <param name="type">The type to check</param>
/// <param name="genericType">The generic type to match</param>
/// <returns>True if the <paramref name="type" /> is generic and of generic type <paramref name="genericType" /></returns>
public static bool IsGenericType(this Type? type, Type genericType)
{
if (type == null)
return false;
/// <summary>
/// Determines whether the provided type is of a specified generic type
/// </summary>
/// <param name="type">The type to check</param>
/// <param name="genericType">The generic type to match</param>
/// <returns>True if the <paramref name="type" /> is generic and of generic type <paramref name="genericType" /></returns>
public static bool IsGenericType(this Type? type, Type genericType)
{
if (type == null)
return false;
return type.BaseType?.GetGenericTypeDefinition() == genericType;
}
return type.BaseType?.GetGenericTypeDefinition() == genericType;
}
/// <summary>
/// Determines whether the provided type is a struct
/// </summary>
/// <param name="type">The type to check</param>
/// <returns><see langword="true" /> if the type is a struct, otherwise <see langword="false" /></returns>
public static bool IsStruct(this Type type)
{
return type.IsValueType && !type.IsPrimitive && !type.IsEnum;
}
/// <summary>
/// Determines whether the provided type is a struct
/// </summary>
/// <param name="type">The type to check</param>
/// <returns><see langword="true" /> if the type is a struct, otherwise <see langword="false" /></returns>
public static bool IsStruct(this Type type)
{
return type.IsValueType && !type.IsPrimitive && !type.IsEnum;
}
/// <summary>
/// Determines whether the provided type is any kind of numeric type
/// </summary>
/// <param name="type">The type to check</param>
/// <returns><see langword="true" /> if the type a numeric type, otherwise <see langword="false" /></returns>
public static bool TypeIsNumber(this Type type)
{
return type == typeof(sbyte)
|| type == typeof(byte)
|| type == typeof(short)
|| type == typeof(ushort)
|| type == typeof(int)
|| type == typeof(uint)
|| type == typeof(long)
|| type == typeof(ulong)
|| type == typeof(float)
|| type == typeof(double)
|| type == typeof(decimal);
}
/// <summary>
/// Determines whether the provided type is any kind of numeric type
/// </summary>
/// <param name="type">The type to check</param>
/// <returns><see langword="true" /> if the type a numeric type, otherwise <see langword="false" /></returns>
public static bool TypeIsNumber(this Type type)
{
return type == typeof(sbyte)
|| type == typeof(byte)
|| type == typeof(short)
|| type == typeof(ushort)
|| type == typeof(int)
|| type == typeof(uint)
|| type == typeof(long)
|| type == typeof(ulong)
|| type == typeof(float)
|| type == typeof(double)
|| type == typeof(decimal);
}
/// <summary>
/// Determines whether the provided value is any kind of numeric type
/// </summary>
/// <param name="value">The value to check</param>
/// <returns><see langword="true" /> if the value is of a numeric type, otherwise <see langword="false" /></returns>
public static bool IsNumber([NotNullWhenAttribute(true)] this object? value)
{
return value is sbyte or byte or short or ushort or int or uint or long or ulong or float or double or decimal;
}
/// <summary>
/// Determines whether the provided value is any kind of numeric type
/// </summary>
/// <param name="value">The value to check</param>
/// <returns><see langword="true" /> if the value is of a numeric type, otherwise <see langword="false" /></returns>
public static bool IsNumber([NotNullWhenAttribute(true)] this object? value)
{
return value is sbyte or byte or short or ushort or int or uint or long or ulong or float or double or decimal;
}
// From https://stackoverflow.com/a/2224421/5015269 but inverted and renamed to match similar framework methods
/// <summary>
/// Determines whether an instance of a specified type can be casted to a variable of the current type
/// </summary>
/// <returns></returns>
public static bool IsCastableFrom(this Type to, Type from)
{
if (to.TypeIsNumber() && from.TypeIsNumber())
return true;
if (to.IsAssignableFrom(from))
return true;
if (PrimitiveTypeConversions.ContainsKey(to) && PrimitiveTypeConversions[to].Contains(from))
return true;
bool castable = from.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Any(m => m.ReturnType == to && (m.Name == "op_Implicit" || m.Name == "op_Explicit"));
return castable;
}
// From https://stackoverflow.com/a/2224421/5015269 but inverted and renamed to match similar framework methods
/// <summary>
/// Determines whether an instance of a specified type can be casted to a variable of the current type
/// </summary>
/// <returns></returns>
public static bool IsCastableFrom(this Type to, Type from)
{
if (to.TypeIsNumber() && from.TypeIsNumber())
return true;
if (to.IsAssignableFrom(from))
return true;
if (PrimitiveTypeConversions.ContainsKey(to) && PrimitiveTypeConversions[to].Contains(from))
return true;
bool castable = from.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Any(m => m.ReturnType == to && (m.Name == "op_Implicit" || m.Name == "op_Explicit"));
return castable;
}
/// <summary>
/// Scores how well the two types can be casted from one to another, 5 being a perfect match and 0 being not castable
/// at all
/// </summary>
/// <returns></returns>
public static int ScoreCastability(this Type to, Type? from)
{
if (from == null)
return 0;
if (to == from)
return 5;
if (to.TypeIsNumber() && from.TypeIsNumber())
return 4;
if (PrimitiveTypeConversions.ContainsKey(to) && PrimitiveTypeConversions[to].Contains(from))
return 3;
bool castable = from.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Any(m => m.ReturnType == to && (m.Name == "op_Implicit" || m.Name == "op_Explicit"));
if (castable)
return 2;
if (to.IsAssignableFrom(from))
return 1;
/// <summary>
/// Scores how well the two types can be casted from one to another, 5 being a perfect match and 0 being not castable
/// at all
/// </summary>
/// <returns></returns>
public static int ScoreCastability(this Type to, Type? from)
{
if (from == null)
return 0;
if (to == from)
return 5;
if (to.TypeIsNumber() && from.TypeIsNumber())
return 4;
if (PrimitiveTypeConversions.ContainsKey(to) && PrimitiveTypeConversions[to].Contains(from))
return 3;
bool castable = from.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Any(m => m.ReturnType == to && (m.Name == "op_Implicit" || m.Name == "op_Explicit"));
if (castable)
return 2;
if (to.IsAssignableFrom(from))
return 1;
return 0;
}
/// <summary>
/// Returns the default value of the given type
/// </summary>
public static object? GetDefault(this Type type)
{
return type.IsValueType ? Activator.CreateInstance(type) : null;
}
/// <summary>
/// Determines whether the given type is a generic enumerable
/// </summary>
public static bool IsGenericEnumerable(this Type type)
{
// String is an IEnumerable to be fair, but not for us
if (type == typeof(string))
return false;
// It may actually be one instead of implementing one ;)
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
return true;
return type.GetInterfaces().Any(x =>
x.IsGenericType &&
x.GetGenericTypeDefinition() == typeof(IEnumerable<>));
}
/// <summary>
/// Determines the type of the provided generic enumerable type
/// </summary>
public static Type? GetGenericEnumerableType(this Type type)
{
// It may actually be one instead of implementing one ;)
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
return type.GenericTypeArguments[0];
Type? enumerableType = type.GetInterfaces().FirstOrDefault(x =>
x.IsGenericType &&
x.GetGenericTypeDefinition() == typeof(IEnumerable<>));
return enumerableType?.GenericTypeArguments[0];
}
/// <summary>
/// Determines if the <paramref name="typeToCheck"></paramref> is of a certain <paramref name="genericType" />.
/// </summary>
/// <param name="typeToCheck">The type to check.</param>
/// <param name="genericType">The generic type it should be or implement</param>
public static bool IsOfGenericType(this Type typeToCheck, Type genericType)
{
return typeToCheck.IsOfGenericType(genericType, out Type? _);
}
/// <summary>
/// Determines a display name for the given type
/// </summary>
/// <param name="type">The type to determine the name for</param>
/// <param name="humanize">Whether or not to humanize the result, defaults to false</param>
/// <returns></returns>
public static string GetDisplayName(this Type type, bool humanize = false)
{
if (!type.IsGenericType)
{
string displayValue = TypeKeywords.TryGetValue(type, out string? keyword) ? keyword! : type.Name;
return humanize ? displayValue.Humanize() : displayValue;
}
/// <summary>
/// Returns the default value of the given type
/// </summary>
public static object? GetDefault(this Type type)
{
return type.IsValueType ? Activator.CreateInstance(type) : null;
}
Type genericTypeDefinition = type.GetGenericTypeDefinition();
if (genericTypeDefinition == typeof(Nullable<>))
return type.GenericTypeArguments[0].GetDisplayName(humanize) + "?";
/// <summary>
/// Determines whether the given type is a generic enumerable
/// </summary>
public static bool IsGenericEnumerable(this Type type)
string stripped = genericTypeDefinition.Name.Split('`')[0];
return $"{stripped}<{string.Join(", ", type.GenericTypeArguments.Select(t => t.GetDisplayName(humanize)))}>";
}
private static bool IsOfGenericType(this Type? typeToCheck, Type genericType, out Type? concreteGenericType)
{
while (true)
{
// String is an IEnumerable to be fair, but not for us
if (type == typeof(string))
concreteGenericType = null;
if (genericType == null)
throw new ArgumentNullException(nameof(genericType));
if (!genericType.IsGenericTypeDefinition)
throw new ArgumentException("The definition needs to be a GenericTypeDefinition", nameof(genericType));
if (typeToCheck == null || typeToCheck == typeof(object))
return false;
// It may actually be one instead of implementing one ;)
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
if (typeToCheck == genericType)
{
concreteGenericType = typeToCheck;
return true;
return type.GetInterfaces().Any(x =>
x.IsGenericType &&
x.GetGenericTypeDefinition() == typeof(IEnumerable<>));
}
/// <summary>
/// Determines the type of the provided generic enumerable type
/// </summary>
public static Type? GetGenericEnumerableType(this Type type)
{
// It may actually be one instead of implementing one ;)
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
return type.GenericTypeArguments[0];
Type? enumerableType = type.GetInterfaces().FirstOrDefault(x =>
x.IsGenericType &&
x.GetGenericTypeDefinition() == typeof(IEnumerable<>));
return enumerableType?.GenericTypeArguments[0];
}
/// <summary>
/// Determines if the <paramref name="typeToCheck"></paramref> is of a certain <paramref name="genericType"/>.
/// </summary>
/// <param name="typeToCheck">The type to check.</param>
/// <param name="genericType">The generic type it should be or implement</param>
public static bool IsOfGenericType(this Type typeToCheck, Type genericType)
{
return typeToCheck.IsOfGenericType(genericType, out Type? _);
}
private static bool IsOfGenericType(this Type? typeToCheck, Type genericType, out Type? concreteGenericType)
{
while (true)
{
concreteGenericType = null;
if (genericType == null)
throw new ArgumentNullException(nameof(genericType));
if (!genericType.IsGenericTypeDefinition)
throw new ArgumentException("The definition needs to be a GenericTypeDefinition", nameof(genericType));
if (typeToCheck == null || typeToCheck == typeof(object))
return false;
if (typeToCheck == genericType)
{
concreteGenericType = typeToCheck;
return true;
}
if ((typeToCheck.IsGenericType ? typeToCheck.GetGenericTypeDefinition() : typeToCheck) == genericType)
{
concreteGenericType = typeToCheck;
return true;
}
if (genericType.IsInterface)
foreach (Type i in typeToCheck.GetInterfaces())
if (i.IsOfGenericType(genericType, out concreteGenericType))
return true;
typeToCheck = typeToCheck.BaseType;
}
}
/// <summary>
/// Determines a display name for the given type
/// </summary>
/// <param name="type">The type to determine the name for</param>
/// <param name="humanize">Whether or not to humanize the result, defaults to false</param>
/// <returns></returns>
public static string GetDisplayName(this Type type, bool humanize = false)
{
if (!type.IsGenericType)
{
string displayValue = TypeKeywords.TryGetValue(type, out string? keyword) ? keyword! : type.Name;
return humanize ? displayValue.Humanize() : displayValue;
}
Type genericTypeDefinition = type.GetGenericTypeDefinition();
if (genericTypeDefinition == typeof(Nullable<>))
return type.GenericTypeArguments[0].GetDisplayName(humanize) + "?";
if ((typeToCheck.IsGenericType ? typeToCheck.GetGenericTypeDefinition() : typeToCheck) == genericType)
{
concreteGenericType = typeToCheck;
return true;
}
string stripped = genericTypeDefinition.Name.Split('`')[0];
return $"{stripped}<{string.Join(", ", type.GenericTypeArguments.Select(t => t.GetDisplayName(humanize)))}>";
if (genericType.IsInterface)
foreach (Type i in typeToCheck.GetInterfaces())
{
if (i.IsOfGenericType(genericType, out concreteGenericType))
return true;
}
typeToCheck = typeToCheck.BaseType;
}
}
}

View File

@ -2,32 +2,31 @@ using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Artemis.Core.JsonConverters
namespace Artemis.Core.JsonConverters;
/// <summary>
/// An int converter that, if required, will round float values
/// </summary>
internal class ForgivingIntConverter : JsonConverter<int>
{
/// <summary>
/// An int converter that, if required, will round float values
/// </summary>
internal class ForgivingIntConverter : JsonConverter<int>
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, int value, JsonSerializer serializer)
{
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, int value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override int ReadJson(JsonReader reader, Type objectType, int existingValue, bool hasExistingValue, JsonSerializer serializer)
{
JValue? jsonValue = serializer.Deserialize<JValue>(reader);
if (jsonValue == null)
throw new JsonReaderException("Failed to deserialize forgiving int value");
if (jsonValue.Type == JTokenType.Float)
return (int) Math.Round(jsonValue.Value<double>());
if (jsonValue.Type == JTokenType.Integer)
return jsonValue.Value<int>();
throw new NotImplementedException();
}
public override int ReadJson(JsonReader reader, Type objectType, int existingValue, bool hasExistingValue, JsonSerializer serializer)
{
JValue? jsonValue = serializer.Deserialize<JValue>(reader);
if (jsonValue == null)
throw new JsonReaderException("Failed to deserialize forgiving int value");
}
if (jsonValue.Type == JTokenType.Float)
return (int) Math.Round(jsonValue.Value<double>());
if (jsonValue.Type == JTokenType.Integer)
return jsonValue.Value<int>();
throw new JsonReaderException("Failed to deserialize forgiving int value");
}
}

View File

@ -1,25 +1,24 @@
using System;
using Newtonsoft.Json;
namespace Artemis.Core.JsonConverters
namespace Artemis.Core.JsonConverters;
internal class NumericJsonConverter : JsonConverter<Numeric>
{
internal class NumericJsonConverter : JsonConverter<Numeric>
#region Overrides of JsonConverter<Numeric>
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, Numeric value, JsonSerializer serializer)
{
#region Overrides of JsonConverter<Numeric>
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, Numeric value, JsonSerializer serializer)
{
float floatValue = value;
writer.WriteValue(floatValue);
}
/// <inheritdoc />
public override Numeric ReadJson(JsonReader reader, Type objectType, Numeric existingValue, bool hasExistingValue, JsonSerializer serializer)
{
return new Numeric(reader.Value);
}
#endregion
float floatValue = value;
writer.WriteValue(floatValue);
}
/// <inheritdoc />
public override Numeric ReadJson(JsonReader reader, Type objectType, Numeric existingValue, bool hasExistingValue, JsonSerializer serializer)
{
return new Numeric(reader.Value);
}
#endregion
}

View File

@ -2,21 +2,20 @@
using Newtonsoft.Json;
using SkiaSharp;
namespace Artemis.Core.JsonConverters
namespace Artemis.Core.JsonConverters;
internal class SKColorConverter : JsonConverter<SKColor>
{
internal class SKColorConverter : JsonConverter<SKColor>
public override void WriteJson(JsonWriter writer, SKColor value, JsonSerializer serializer)
{
public override void WriteJson(JsonWriter writer, SKColor value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
writer.WriteValue(value.ToString());
}
public override SKColor ReadJson(JsonReader reader, Type objectType, SKColor existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.Value is string value && !string.IsNullOrWhiteSpace(value))
return SKColor.Parse(value);
public override SKColor ReadJson(JsonReader reader, Type objectType, SKColor existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.Value is string value && !string.IsNullOrWhiteSpace(value))
return SKColor.Parse(value);
return SKColor.Empty;
}
return SKColor.Empty;
}
}

View File

@ -2,44 +2,43 @@
using System.IO;
using Newtonsoft.Json;
namespace Artemis.Core.JsonConverters
namespace Artemis.Core.JsonConverters;
/// <inheritdoc />
public class StreamConverter : JsonConverter<Stream>
{
#region Overrides of JsonConverter<Stream>
/// <inheritdoc />
public class StreamConverter : JsonConverter<Stream>
public override void WriteJson(JsonWriter writer, Stream? value, JsonSerializer serializer)
{
#region Overrides of JsonConverter<Stream>
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, Stream? value, JsonSerializer serializer)
if (value == null)
{
if (value == null)
{
writer.WriteNull();
return;
}
using MemoryStream memoryStream = new();
value.Position = 0;
value.CopyTo(memoryStream);
writer.WriteValue(memoryStream.ToArray());
writer.WriteNull();
return;
}
/// <inheritdoc />
public override Stream? ReadJson(JsonReader reader, Type objectType, Stream? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.Value is not string base64)
return null;
if (existingValue == null || !hasExistingValue || !existingValue.CanRead)
return new MemoryStream(Convert.FromBase64String(base64));
using MemoryStream memoryStream = new(Convert.FromBase64String(base64));
existingValue.Position = 0;
memoryStream.CopyTo(existingValue);
existingValue.Position = 0;
return existingValue;
}
#endregion
using MemoryStream memoryStream = new();
value.Position = 0;
value.CopyTo(memoryStream);
writer.WriteValue(memoryStream.ToArray());
}
/// <inheritdoc />
public override Stream? ReadJson(JsonReader reader, Type objectType, Stream? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.Value is not string base64)
return null;
if (existingValue == null || !hasExistingValue || !existingValue.CanRead)
return new MemoryStream(Convert.FromBase64String(base64));
using MemoryStream memoryStream = new(Convert.FromBase64String(base64));
existingValue.Position = 0;
memoryStream.CopyTo(existingValue);
existingValue.Position = 0;
return existingValue;
}
#endregion
}

View File

@ -2,73 +2,68 @@
using System.Runtime.CompilerServices;
using Artemis.Core.Properties;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a basic bindable class which notifies when a property value changes.
/// </summary>
public abstract class CorePropertyChanged : INotifyPropertyChanged
{
/// <summary>
/// Represents a basic bindable class which notifies when a property value changes.
/// Occurs when a property value changes.
/// </summary>
public abstract class CorePropertyChanged : INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
#region Methods
/// <summary>
/// Checks if the property already matches the desired value or needs to be updated.
/// </summary>
/// <typeparam name="T">Type of the property.</typeparam>
/// <param name="storage">Reference to the backing-filed.</param>
/// <param name="value">Value to apply.</param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool RequiresUpdate<T>(ref T storage, T value)
{
#region Events
/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
#endregion
#region Methods
/// <summary>
/// Checks if the property already matches the desired value or needs to be updated.
/// </summary>
/// <typeparam name="T">Type of the property.</typeparam>
/// <param name="storage">Reference to the backing-filed.</param>
/// <param name="value">Value to apply.</param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool RequiresUpdate<T>(ref T storage, T value)
{
return !Equals(storage, value);
}
/// <summary>
/// Checks if the property already matches the desired value and updates it if not.
/// </summary>
/// <typeparam name="T">Type of the property.</typeparam>
/// <param name="storage">Reference to the backing-filed.</param>
/// <param name="value">Value to apply.</param>
/// <param name="propertyName">
/// Name of the property used to notify listeners. This value is optional
/// and can be provided automatically when invoked from compilers that support <see cref="CallerMemberNameAttribute" />
/// .
/// </param>
/// <returns><c>true</c> if the value was changed, <c>false</c> if the existing value matched the desired value.</returns>
[NotifyPropertyChangedInvocator]
protected bool SetAndNotify<T>(ref T storage, T value, [CallerMemberName] string? propertyName = null)
{
if (!RequiresUpdate(ref storage, value)) return false;
storage = value;
// ReSharper disable once ExplicitCallerInfoArgument
OnPropertyChanged(propertyName);
return true;
}
/// <summary>
/// Triggers the <see cref="PropertyChanged" />-event when a a property value has changed.
/// </summary>
/// <param name="propertyName">
/// Name of the property used to notify listeners. This value is optional
/// and can be provided automatically when invoked from compilers that support <see cref="CallerMemberNameAttribute" />
/// .
/// </param>
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
return !Equals(storage, value);
}
/// <summary>
/// Checks if the property already matches the desired value and updates it if not.
/// </summary>
/// <typeparam name="T">Type of the property.</typeparam>
/// <param name="storage">Reference to the backing-filed.</param>
/// <param name="value">Value to apply.</param>
/// <param name="propertyName">
/// Name of the property used to notify listeners. This value is optional
/// and can be provided automatically when invoked from compilers that support <see cref="CallerMemberNameAttribute" />
/// .
/// </param>
/// <returns><c>true</c> if the value was changed, <c>false</c> if the existing value matched the desired value.</returns>
[NotifyPropertyChangedInvocator]
protected bool SetAndNotify<T>(ref T storage, T value, [CallerMemberName] string? propertyName = null)
{
if (!RequiresUpdate(ref storage, value)) return false;
storage = value;
// ReSharper disable once ExplicitCallerInfoArgument
OnPropertyChanged(propertyName);
return true;
}
/// <summary>
/// Triggers the <see cref="PropertyChanged" />-event when a a property value has changed.
/// </summary>
/// <param name="propertyName">
/// Name of the property used to notify listeners. This value is optional
/// and can be provided automatically when invoked from compilers that support <see cref="CallerMemberNameAttribute" />
/// .
/// </param>
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}

View File

@ -1,92 +1,91 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Provides a default implementation for models that can have a broken state
/// </summary>
public abstract class BreakableModel : CorePropertyChanged, IBreakableModel
{
private string? _brokenState;
private Exception? _brokenStateException;
/// <summary>
/// Provides a default implementation for models that can have a broken state
/// Invokes the <see cref="BrokenStateChanged" /> event
/// </summary>
public abstract class BreakableModel : CorePropertyChanged, IBreakableModel
protected virtual void OnBrokenStateChanged()
{
private string? _brokenState;
private Exception? _brokenStateException;
/// <summary>
/// Invokes the <see cref="BrokenStateChanged" /> event
/// </summary>
protected virtual void OnBrokenStateChanged()
{
BrokenStateChanged?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc />
public abstract string BrokenDisplayName { get; }
/// <summary>
/// Gets or sets the broken state of this breakable model, if <see langword="null" /> this model is not broken.
/// <para>Note: If setting this manually you are also responsible for invoking <see cref="BrokenStateChanged" /></para>
/// </summary>
public string? BrokenState
{
get => _brokenState;
set => SetAndNotify(ref _brokenState, value);
}
/// <summary>
/// Gets or sets the exception that caused the broken state
/// <para>Note: If setting this manually you are also responsible for invoking <see cref="BrokenStateChanged" /></para>
/// </summary>
public Exception? BrokenStateException
{
get => _brokenStateException;
set => SetAndNotify(ref _brokenStateException, value);
}
/// <inheritdoc />
public bool TryOrBreak(Action action, string breakMessage)
{
try
{
action();
ClearBrokenState(breakMessage);
return true;
}
catch (Exception e)
{
SetBrokenState(breakMessage, e);
return false;
}
}
/// <inheritdoc />
public void SetBrokenState(string state, Exception? exception)
{
BrokenState = state ?? throw new ArgumentNullException(nameof(state));
BrokenStateException = exception;
OnBrokenStateChanged();
}
/// <inheritdoc />
public void ClearBrokenState(string state)
{
if (state == null) throw new ArgumentNullException(nameof(state));
if (BrokenState == null)
return;
if (BrokenState != state) return;
BrokenState = null;
BrokenStateException = null;
OnBrokenStateChanged();
}
/// <inheritdoc />
public virtual IEnumerable<IBreakableModel> GetBrokenHierarchy()
{
if (BrokenState != null)
yield return this;
}
/// <inheritdoc />
public event EventHandler? BrokenStateChanged;
BrokenStateChanged?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc />
public abstract string BrokenDisplayName { get; }
/// <summary>
/// Gets or sets the broken state of this breakable model, if <see langword="null" /> this model is not broken.
/// <para>Note: If setting this manually you are also responsible for invoking <see cref="BrokenStateChanged" /></para>
/// </summary>
public string? BrokenState
{
get => _brokenState;
set => SetAndNotify(ref _brokenState, value);
}
/// <summary>
/// Gets or sets the exception that caused the broken state
/// <para>Note: If setting this manually you are also responsible for invoking <see cref="BrokenStateChanged" /></para>
/// </summary>
public Exception? BrokenStateException
{
get => _brokenStateException;
set => SetAndNotify(ref _brokenStateException, value);
}
/// <inheritdoc />
public bool TryOrBreak(Action action, string breakMessage)
{
try
{
action();
ClearBrokenState(breakMessage);
return true;
}
catch (Exception e)
{
SetBrokenState(breakMessage, e);
return false;
}
}
/// <inheritdoc />
public void SetBrokenState(string state, Exception? exception)
{
BrokenState = state ?? throw new ArgumentNullException(nameof(state));
BrokenStateException = exception;
OnBrokenStateChanged();
}
/// <inheritdoc />
public void ClearBrokenState(string state)
{
if (state == null) throw new ArgumentNullException(nameof(state));
if (BrokenState == null)
return;
if (BrokenState != state) return;
BrokenState = null;
BrokenStateException = null;
OnBrokenStateChanged();
}
/// <inheritdoc />
public virtual IEnumerable<IBreakableModel> GetBrokenHierarchy()
{
if (BrokenState != null)
yield return this;
}
/// <inheritdoc />
public event EventHandler? BrokenStateChanged;
}

View File

@ -1,59 +1,58 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a model that can have a broken state
/// </summary>
public interface IBreakableModel
{
/// <summary>
/// Represents a model that can have a broken state
/// Gets the display name of this breakable model
/// </summary>
public interface IBreakableModel
{
/// <summary>
/// Gets the display name of this breakable model
/// </summary>
string BrokenDisplayName { get; }
string BrokenDisplayName { get; }
/// <summary>
/// Gets or sets the broken state of this breakable model, if <see langword="null" /> this model is not broken.
/// </summary>
string? BrokenState { get; set; }
/// <summary>
/// Gets or sets the broken state of this breakable model, if <see langword="null" /> this model is not broken.
/// </summary>
string? BrokenState { get; set; }
/// <summary>
/// Gets or sets the exception that caused the broken state
/// </summary>
Exception? BrokenStateException { get; set; }
/// <summary>
/// Gets or sets the exception that caused the broken state
/// </summary>
Exception? BrokenStateException { get; set; }
/// <summary>
/// Try to execute the provided action. If the action succeeded the broken state is cleared if it matches
/// <see paramref="breakMessage" />, if the action throws an exception <see cref="BrokenState" /> and
/// <see cref="BrokenStateException" /> are set accordingly.
/// </summary>
/// <param name="action">The action to attempt to execute</param>
/// <param name="breakMessage">The message to clear on succeed or set on failure (exception)</param>
/// <returns><see langword="true" /> if the action succeeded; otherwise <see langword="false" />.</returns>
bool TryOrBreak(Action action, string breakMessage);
/// <summary>
/// Try to execute the provided action. If the action succeeded the broken state is cleared if it matches
/// <see paramref="breakMessage" />, if the action throws an exception <see cref="BrokenState" /> and
/// <see cref="BrokenStateException" /> are set accordingly.
/// </summary>
/// <param name="action">The action to attempt to execute</param>
/// <param name="breakMessage">The message to clear on succeed or set on failure (exception)</param>
/// <returns><see langword="true" /> if the action succeeded; otherwise <see langword="false" />.</returns>
bool TryOrBreak(Action action, string breakMessage);
/// <summary>
/// Sets the broken state to the provided state and optional exception.
/// </summary>
/// <param name="state">The state to set the broken state to</param>
/// <param name="exception">The exception that caused the broken state</param>
public void SetBrokenState(string state, Exception? exception);
/// <summary>
/// Sets the broken state to the provided state and optional exception.
/// </summary>
/// <param name="state">The state to set the broken state to</param>
/// <param name="exception">The exception that caused the broken state</param>
public void SetBrokenState(string state, Exception? exception);
/// <summary>
/// Clears the broken state and exception if <see cref="BrokenState" /> equals <see paramref="state"></see>.
/// </summary>
/// <param name="state"></param>
public void ClearBrokenState(string state);
/// <summary>
/// Returns a list containing all broken models, including self and any children
/// </summary>
IEnumerable<IBreakableModel> GetBrokenHierarchy();
/// <summary>
/// Clears the broken state and exception if <see cref="BrokenState" /> equals <see paramref="state"></see>.
/// </summary>
/// <param name="state"></param>
public void ClearBrokenState(string state);
/// <summary>
/// Occurs when the broken state of this model changes
/// </summary>
event EventHandler BrokenStateChanged;
}
/// <summary>
/// Returns a list containing all broken models, including self and any children
/// </summary>
IEnumerable<IBreakableModel> GetBrokenHierarchy();
/// <summary>
/// Occurs when the broken state of this model changes
/// </summary>
event EventHandler BrokenStateChanged;
}

View File

@ -1,18 +1,17 @@
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a model that can be loaded and saved to persistent storage
/// </summary>
public interface IStorageModel
{
/// <summary>
/// Represents a model that can be loaded and saved to persistent storage
/// Loads the model from its associated entity
/// </summary>
public interface IStorageModel
{
/// <summary>
/// Loads the model from its associated entity
/// </summary>
void Load();
void Load();
/// <summary>
/// Saves the model to its associated entity
/// </summary>
void Save();
}
/// <summary>
/// Saves the model to its associated entity
/// </summary>
void Save();
}

View File

@ -1,14 +1,13 @@
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a model that updates using a delta time
/// </summary>
public interface IUpdateModel
{
/// <summary>
/// Represents a model that updates using a delta time
/// Performs an update on the model
/// </summary>
public interface IUpdateModel
{
/// <summary>
/// Performs an update on the model
/// </summary>
/// <param name="timeline">The timeline to apply during update</param>
void Update(Timeline timeline);
}
/// <param name="timeline">The timeline to apply during update</param>
void Update(Timeline timeline);
}

View File

@ -2,98 +2,97 @@
using System.Linq;
using Artemis.Storage.Entities.Profile.AdaptionHints;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a hint that adapts layers to a certain category of devices
/// </summary>
public class CategoryAdaptionHint : CorePropertyChanged, IAdaptionHint
{
private int _amount;
private DeviceCategory _category;
private bool _limitAmount;
private int _skip;
/// <summary>
/// Represents a hint that adapts layers to a certain category of devices
/// Creates a new instance of the <see cref="CategoryAdaptionHint" /> class
/// </summary>
public class CategoryAdaptionHint : CorePropertyChanged, IAdaptionHint
public CategoryAdaptionHint()
{
private DeviceCategory _category;
private int _skip;
private bool _limitAmount;
private int _amount;
/// <summary>
/// Creates a new instance of the <see cref="CategoryAdaptionHint" /> class
/// </summary>
public CategoryAdaptionHint()
{
}
internal CategoryAdaptionHint(CategoryAdaptionHintEntity entity)
{
Category = (DeviceCategory) entity.Category;
Skip = entity.Skip;
LimitAmount = entity.LimitAmount;
Amount = entity.Amount;
}
/// <summary>
/// Gets or sets the category of devices LEDs will be applied to
/// </summary>
public DeviceCategory Category
{
get => _category;
set => SetAndNotify(ref _category, value);
}
/// <summary>
/// Gets or sets the amount of devices to skip
/// </summary>
public int Skip
{
get => _skip;
set => SetAndNotify(ref _skip, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether a limited amount of devices should be used
/// </summary>
public bool LimitAmount
{
get => _limitAmount;
set => SetAndNotify(ref _limitAmount, value);
}
/// <summary>
/// Gets or sets the amount of devices to limit to if <see cref="LimitAmount" /> is <see langword="true" />
/// </summary>
public int Amount
{
get => _amount;
set => SetAndNotify(ref _amount, value);
}
/// <inheritdoc />
public override string ToString()
{
return $"Category adaption - {nameof(Category)}: {Category}, {nameof(Skip)}: {Skip}, {nameof(LimitAmount)}: {LimitAmount}, {nameof(Amount)}: {Amount}";
}
#region Implementation of IAdaptionHint
/// <inheritdoc />
public void Apply(Layer layer, List<ArtemisDevice> devices)
{
IEnumerable<ArtemisDevice> matches = devices
.Where(d => d.Categories.Contains(Category))
.OrderBy(d => d.Rectangle.Top)
.ThenBy(d => d.Rectangle.Left)
.Skip(Skip);
if (LimitAmount)
matches = matches.Take(Amount);
foreach (ArtemisDevice artemisDevice in matches)
layer.AddLeds(artemisDevice.Leds);
}
/// <inheritdoc />
public IAdaptionHintEntity GetEntry()
{
return new CategoryAdaptionHintEntity {Amount = Amount, LimitAmount = LimitAmount, Category = (int) Category, Skip = Skip};
}
#endregion
}
internal CategoryAdaptionHint(CategoryAdaptionHintEntity entity)
{
Category = (DeviceCategory) entity.Category;
Skip = entity.Skip;
LimitAmount = entity.LimitAmount;
Amount = entity.Amount;
}
/// <summary>
/// Gets or sets the category of devices LEDs will be applied to
/// </summary>
public DeviceCategory Category
{
get => _category;
set => SetAndNotify(ref _category, value);
}
/// <summary>
/// Gets or sets the amount of devices to skip
/// </summary>
public int Skip
{
get => _skip;
set => SetAndNotify(ref _skip, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether a limited amount of devices should be used
/// </summary>
public bool LimitAmount
{
get => _limitAmount;
set => SetAndNotify(ref _limitAmount, value);
}
/// <summary>
/// Gets or sets the amount of devices to limit to if <see cref="LimitAmount" /> is <see langword="true" />
/// </summary>
public int Amount
{
get => _amount;
set => SetAndNotify(ref _amount, value);
}
/// <inheritdoc />
public override string ToString()
{
return $"Category adaption - {nameof(Category)}: {Category}, {nameof(Skip)}: {Skip}, {nameof(LimitAmount)}: {LimitAmount}, {nameof(Amount)}: {Amount}";
}
#region Implementation of IAdaptionHint
/// <inheritdoc />
public void Apply(Layer layer, List<ArtemisDevice> devices)
{
IEnumerable<ArtemisDevice> matches = devices
.Where(d => d.Categories.Contains(Category))
.OrderBy(d => d.Rectangle.Top)
.ThenBy(d => d.Rectangle.Left)
.Skip(Skip);
if (LimitAmount)
matches = matches.Take(Amount);
foreach (ArtemisDevice artemisDevice in matches)
layer.AddLeds(artemisDevice.Leds);
}
/// <inheritdoc />
public IAdaptionHintEntity GetEntry()
{
return new CategoryAdaptionHintEntity {Amount = Amount, LimitAmount = LimitAmount, Category = (int) Category, Skip = Skip};
}
#endregion
}

View File

@ -3,98 +3,97 @@ using System.Linq;
using Artemis.Storage.Entities.Profile.AdaptionHints;
using RGB.NET.Core;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a hint that adapts layers to a certain type of devices
/// </summary>
public class DeviceAdaptionHint : CorePropertyChanged, IAdaptionHint
{
private int _amount;
private RGBDeviceType _deviceType;
private bool _limitAmount;
private int _skip;
/// <summary>
/// Represents a hint that adapts layers to a certain type of devices
/// Creates a new instance of the <see cref="DeviceAdaptionHint" /> class
/// </summary>
public class DeviceAdaptionHint : CorePropertyChanged, IAdaptionHint
public DeviceAdaptionHint()
{
private RGBDeviceType _deviceType;
private int _skip;
private bool _limitAmount;
private int _amount;
/// <summary>
/// Creates a new instance of the <see cref="DeviceAdaptionHint" /> class
/// </summary>
public DeviceAdaptionHint()
{
}
internal DeviceAdaptionHint(DeviceAdaptionHintEntity entity)
{
DeviceType = (RGBDeviceType) entity.DeviceType;
Skip = entity.Skip;
LimitAmount = entity.LimitAmount;
Amount = entity.Amount;
}
/// <summary>
/// Gets or sets the type of devices LEDs will be applied to
/// </summary>
public RGBDeviceType DeviceType
{
get => _deviceType;
set => SetAndNotify(ref _deviceType, value);
}
/// <summary>
/// Gets or sets the amount of devices to skip
/// </summary>
public int Skip
{
get => _skip;
set => SetAndNotify(ref _skip, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether a limited amount of devices should be used
/// </summary>
public bool LimitAmount
{
get => _limitAmount;
set => SetAndNotify(ref _limitAmount, value);
}
/// <summary>
/// Gets or sets the amount of devices to limit to if <see cref="LimitAmount" /> is <see langword="true" />
/// </summary>
public int Amount
{
get => _amount;
set => SetAndNotify(ref _amount, value);
}
#region Implementation of IAdaptionHint
/// <inheritdoc />
public void Apply(Layer layer, List<ArtemisDevice> devices)
{
IEnumerable<ArtemisDevice> matches = devices
.Where(d => DeviceType == RGBDeviceType.All || d.DeviceType == DeviceType)
.OrderBy(d => d.Rectangle.Top)
.ThenBy(d => d.Rectangle.Left)
.Skip(Skip);
if (LimitAmount)
matches = matches.Take(Amount);
foreach (ArtemisDevice artemisDevice in matches)
layer.AddLeds(artemisDevice.Leds);
}
/// <inheritdoc />
public IAdaptionHintEntity GetEntry()
{
return new DeviceAdaptionHintEntity {Amount = Amount, LimitAmount = LimitAmount, DeviceType = (int) DeviceType, Skip = Skip};
}
/// <inheritdoc />
public override string ToString()
{
return $"Device adaption - {nameof(DeviceType)}: {DeviceType}, {nameof(Skip)}: {Skip}, {nameof(LimitAmount)}: {LimitAmount}, {nameof(Amount)}: {Amount}";
}
#endregion
}
internal DeviceAdaptionHint(DeviceAdaptionHintEntity entity)
{
DeviceType = (RGBDeviceType) entity.DeviceType;
Skip = entity.Skip;
LimitAmount = entity.LimitAmount;
Amount = entity.Amount;
}
/// <summary>
/// Gets or sets the type of devices LEDs will be applied to
/// </summary>
public RGBDeviceType DeviceType
{
get => _deviceType;
set => SetAndNotify(ref _deviceType, value);
}
/// <summary>
/// Gets or sets the amount of devices to skip
/// </summary>
public int Skip
{
get => _skip;
set => SetAndNotify(ref _skip, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether a limited amount of devices should be used
/// </summary>
public bool LimitAmount
{
get => _limitAmount;
set => SetAndNotify(ref _limitAmount, value);
}
/// <summary>
/// Gets or sets the amount of devices to limit to if <see cref="LimitAmount" /> is <see langword="true" />
/// </summary>
public int Amount
{
get => _amount;
set => SetAndNotify(ref _amount, value);
}
#region Implementation of IAdaptionHint
/// <inheritdoc />
public void Apply(Layer layer, List<ArtemisDevice> devices)
{
IEnumerable<ArtemisDevice> matches = devices
.Where(d => DeviceType == RGBDeviceType.All || d.DeviceType == DeviceType)
.OrderBy(d => d.Rectangle.Top)
.ThenBy(d => d.Rectangle.Left)
.Skip(Skip);
if (LimitAmount)
matches = matches.Take(Amount);
foreach (ArtemisDevice artemisDevice in matches)
layer.AddLeds(artemisDevice.Leds);
}
/// <inheritdoc />
public IAdaptionHintEntity GetEntry()
{
return new DeviceAdaptionHintEntity {Amount = Amount, LimitAmount = LimitAmount, DeviceType = (int) DeviceType, Skip = Skip};
}
/// <inheritdoc />
public override string ToString()
{
return $"Device adaption - {nameof(DeviceType)}: {DeviceType}, {nameof(Skip)}: {Skip}, {nameof(LimitAmount)}: {LimitAmount}, {nameof(Amount)}: {Amount}";
}
#endregion
}

View File

@ -1,23 +1,22 @@
using System.Collections.Generic;
using Artemis.Storage.Entities.Profile.AdaptionHints;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents an adaption hint that's used to adapt a layer to a set of devices
/// </summary>
public interface IAdaptionHint
{
/// <summary>
/// Represents an adaption hint that's used to adapt a layer to a set of devices
/// Applies the adaptive action to the provided layer
/// </summary>
public interface IAdaptionHint
{
/// <summary>
/// Applies the adaptive action to the provided layer
/// </summary>
/// <param name="layer">The layer to adapt</param>
/// <param name="devices">The devices to adapt the layer for</param>
void Apply(Layer layer, List<ArtemisDevice> devices);
/// <param name="layer">The layer to adapt</param>
/// <param name="devices">The devices to adapt the layer for</param>
void Apply(Layer layer, List<ArtemisDevice> devices);
/// <summary>
/// Returns an adaption hint entry for this adaption hint used for persistent storage
/// </summary>
IAdaptionHintEntity GetEntry();
}
/// <summary>
/// Returns an adaption hint entry for this adaption hint used for persistent storage
/// </summary>
IAdaptionHintEntity GetEntry();
}

View File

@ -4,89 +4,88 @@ using System.Linq;
using Artemis.Storage.Entities.Profile.AdaptionHints;
using RGB.NET.Core;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a hint that adapts layers to a certain region of keyboards
/// </summary>
public class KeyboardSectionAdaptionHint : CorePropertyChanged, IAdaptionHint
{
private static readonly Dictionary<KeyboardSection, List<LedId>> RegionLedIds = new()
{
{KeyboardSection.MacroKeys, Enum.GetValues<LedId>().Where(l => l >= LedId.Keyboard_Programmable1 && l <= LedId.Keyboard_Programmable32).ToList()},
{KeyboardSection.LedStrips, Enum.GetValues<LedId>().Where(l => l >= LedId.LedStripe1 && l <= LedId.LedStripe128).ToList()},
{KeyboardSection.Extra, Enum.GetValues<LedId>().Where(l => l >= LedId.Keyboard_Custom1 && l <= LedId.Keyboard_Custom64).ToList()}
};
private KeyboardSection _section;
/// <summary>
/// Creates a new instance of the <see cref="KeyboardSectionAdaptionHint" /> class
/// </summary>
public KeyboardSectionAdaptionHint()
{
}
internal KeyboardSectionAdaptionHint(KeyboardSectionAdaptionHintEntity entity)
{
Section = (KeyboardSection) entity.Section;
}
/// <summary>
/// Gets or sets the section this hint will apply LEDs to
/// </summary>
public KeyboardSection Section
{
get => _section;
set => SetAndNotify(ref _section, value);
}
#region Implementation of IAdaptionHint
/// <inheritdoc />
public void Apply(Layer layer, List<ArtemisDevice> devices)
{
// Only keyboards should have the LEDs we care about
foreach (ArtemisDevice keyboard in devices.Where(d => d.DeviceType == RGBDeviceType.Keyboard))
{
List<LedId> ledIds = RegionLedIds[Section];
layer.AddLeds(keyboard.Leds.Where(l => ledIds.Contains(l.RgbLed.Id)));
}
}
/// <inheritdoc />
public IAdaptionHintEntity GetEntry()
{
return new KeyboardSectionAdaptionHintEntity {Section = (int) Section};
}
/// <inheritdoc />
public override string ToString()
{
return $"Keyboard section adaption - {nameof(Section)}: {Section}";
}
#endregion
}
/// <summary>
/// Represents a section of LEDs on a keyboard
/// </summary>
public enum KeyboardSection
{
/// <summary>
/// Represents a hint that adapts layers to a certain region of keyboards
/// A region containing the macro keys of a keyboard
/// </summary>
public class KeyboardSectionAdaptionHint : CorePropertyChanged, IAdaptionHint
{
private static readonly Dictionary<KeyboardSection, List<LedId>> RegionLedIds = new()
{
{KeyboardSection.MacroKeys, Enum.GetValues<LedId>().Where(l => l >= LedId.Keyboard_Programmable1 && l <= LedId.Keyboard_Programmable32).ToList()},
{KeyboardSection.LedStrips, Enum.GetValues<LedId>().Where(l => l >= LedId.LedStripe1 && l <= LedId.LedStripe128).ToList()},
{KeyboardSection.Extra, Enum.GetValues<LedId>().Where(l => l >= LedId.Keyboard_Custom1 && l <= LedId.Keyboard_Custom64).ToList()}
};
private KeyboardSection _section;
/// <summary>
/// Creates a new instance of the <see cref="KeyboardSectionAdaptionHint" /> class
/// </summary>
public KeyboardSectionAdaptionHint()
{
}
internal KeyboardSectionAdaptionHint(KeyboardSectionAdaptionHintEntity entity)
{
Section = (KeyboardSection) entity.Section;
}
/// <summary>
/// Gets or sets the section this hint will apply LEDs to
/// </summary>
public KeyboardSection Section
{
get => _section;
set => SetAndNotify(ref _section, value);
}
#region Implementation of IAdaptionHint
/// <inheritdoc />
public void Apply(Layer layer, List<ArtemisDevice> devices)
{
// Only keyboards should have the LEDs we care about
foreach (ArtemisDevice keyboard in devices.Where(d => d.DeviceType == RGBDeviceType.Keyboard))
{
List<LedId> ledIds = RegionLedIds[Section];
layer.AddLeds(keyboard.Leds.Where(l => ledIds.Contains(l.RgbLed.Id)));
}
}
/// <inheritdoc />
public IAdaptionHintEntity GetEntry()
{
return new KeyboardSectionAdaptionHintEntity {Section = (int) Section};
}
/// <inheritdoc />
public override string ToString()
{
return $"Keyboard section adaption - {nameof(Section)}: {Section}";
}
#endregion
}
MacroKeys,
/// <summary>
/// Represents a section of LEDs on a keyboard
/// A region containing the LED strips of a keyboard
/// </summary>
public enum KeyboardSection
{
/// <summary>
/// A region containing the macro keys of a keyboard
/// </summary>
MacroKeys,
LedStrips,
/// <summary>
/// A region containing the LED strips of a keyboard
/// </summary>
LedStrips,
/// <summary>
/// A region containing extra non-standard LEDs of a keyboard
/// </summary>
Extra
}
/// <summary>
/// A region containing extra non-standard LEDs of a keyboard
/// </summary>
Extra
}

View File

@ -83,14 +83,10 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
{
List<SKColor> result = new();
if (timesToRepeat == 0)
{
result = this.Select(c => c.Color).ToList();
}
else
{
for (int i = 0; i <= timesToRepeat; i++)
result.AddRange(this.Select(c => c.Color));
}
if (seamless && !IsSeamless())
result.Add(result[0]);
@ -413,8 +409,10 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
return false;
for (int i = 0; i < Count; i++)
{
if (!Equals(this[i], other[i]))
return false;
}
return true;
}

View File

@ -1,69 +1,68 @@
using System;
using SkiaSharp;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// A color with a position, usually contained in a <see cref="ColorGradient" />
/// </summary>
public class ColorGradientStop : CorePropertyChanged
{
private SKColor _color;
private float _position;
/// <summary>
/// A color with a position, usually contained in a <see cref="ColorGradient" />
/// Creates a new instance of the <see cref="ColorGradientStop" /> class
/// </summary>
public class ColorGradientStop : CorePropertyChanged
public ColorGradientStop(SKColor color, float position)
{
#region Equality members
/// <inheritdoc cref="object.Equals(object)" />
protected bool Equals(ColorGradientStop other)
{
return _color.Equals(other._color) && _position.Equals(other._position);
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj))
return false;
if (ReferenceEquals(this, obj))
return true;
if (obj.GetType() != GetType())
return false;
return Equals((ColorGradientStop) obj);
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(_color, _position);
}
#endregion
private SKColor _color;
private float _position;
/// <summary>
/// Creates a new instance of the <see cref="ColorGradientStop" /> class
/// </summary>
public ColorGradientStop(SKColor color, float position)
{
Color = color;
Position = position;
}
/// <summary>
/// Gets or sets the color of the stop
/// </summary>
public SKColor Color
{
get => _color;
set => SetAndNotify(ref _color, value);
}
/// <summary>
/// Gets or sets the position of the stop
/// </summary>
public float Position
{
get => _position;
set => SetAndNotify(ref _position, value);
}
Color = color;
Position = position;
}
/// <summary>
/// Gets or sets the color of the stop
/// </summary>
public SKColor Color
{
get => _color;
set => SetAndNotify(ref _color, value);
}
/// <summary>
/// Gets or sets the position of the stop
/// </summary>
public float Position
{
get => _position;
set => SetAndNotify(ref _position, value);
}
#region Equality members
/// <inheritdoc cref="object.Equals(object)" />
protected bool Equals(ColorGradientStop other)
{
return _color.Equals(other._color) && _position.Equals(other._position);
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj))
return false;
if (ReferenceEquals(this, obj))
return true;
if (obj.GetType() != GetType())
return false;
return Equals((ColorGradientStop) obj);
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(_color, _position);
}
#endregion
}

View File

@ -2,89 +2,84 @@
using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Conditions;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a condition that is always true.
/// </summary>
public class AlwaysOnCondition : ICondition
{
/// <summary>
/// Represents a condition that is always true.
/// Creates a new instance of the <see cref="AlwaysOnCondition" /> class.
/// </summary>
public class AlwaysOnCondition : ICondition
/// <param name="profileElement">The profile element this condition applies to.</param>
public AlwaysOnCondition(RenderProfileElement profileElement)
{
/// <summary>
/// Creates a new instance of the <see cref="AlwaysOnCondition" /> class.
/// </summary>
/// <param name="profileElement">The profile element this condition applies to.</param>
public AlwaysOnCondition(RenderProfileElement profileElement)
{
ProfileElement = profileElement;
Entity = new AlwaysOnConditionEntity();
}
/// <summary>
/// Creates a new instance of the <see cref="AlwaysOnCondition" /> class.
/// </summary>
/// <param name="alwaysOnConditionEntity">The entity used to store this condition.</param>
/// <param name="profileElement">The profile element this condition applies to.</param>
public AlwaysOnCondition(AlwaysOnConditionEntity alwaysOnConditionEntity, RenderProfileElement profileElement)
{
ProfileElement = profileElement;
Entity = alwaysOnConditionEntity;
}
#region Implementation of IDisposable
/// <inheritdoc />
public void Dispose()
{
}
#endregion
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
}
/// <inheritdoc />
public void Save()
{
}
#endregion
#region Implementation of ICondition
/// <inheritdoc />
public IConditionEntity Entity { get; }
/// <inheritdoc />
public RenderProfileElement ProfileElement { get; }
/// <inheritdoc />
public bool IsMet { get; private set; }
/// <inheritdoc />
public void Update()
{
if (ProfileElement.Parent is RenderProfileElement parent)
IsMet = parent.DisplayConditionMet;
else
IsMet = true;
}
/// <inheritdoc />
public void UpdateTimeline(double deltaTime)
{
ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), true);
}
/// <inheritdoc />
public void OverrideTimeline(TimeSpan position)
{
ProfileElement.Timeline.Override(position, position > ProfileElement.Timeline.Length);
}
#endregion
ProfileElement = profileElement;
Entity = new AlwaysOnConditionEntity();
}
/// <summary>
/// Creates a new instance of the <see cref="AlwaysOnCondition" /> class.
/// </summary>
/// <param name="alwaysOnConditionEntity">The entity used to store this condition.</param>
/// <param name="profileElement">The profile element this condition applies to.</param>
public AlwaysOnCondition(AlwaysOnConditionEntity alwaysOnConditionEntity, RenderProfileElement profileElement)
{
ProfileElement = profileElement;
Entity = alwaysOnConditionEntity;
}
/// <inheritdoc />
public void Dispose()
{
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
}
/// <inheritdoc />
public void Save()
{
}
#endregion
#region Implementation of ICondition
/// <inheritdoc />
public IConditionEntity Entity { get; }
/// <inheritdoc />
public RenderProfileElement ProfileElement { get; }
/// <inheritdoc />
public bool IsMet { get; private set; }
/// <inheritdoc />
public void Update()
{
if (ProfileElement.Parent is RenderProfileElement parent)
IsMet = parent.DisplayConditionMet;
else
IsMet = true;
}
/// <inheritdoc />
public void UpdateTimeline(double deltaTime)
{
ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), true);
}
/// <inheritdoc />
public void OverrideTimeline(TimeSpan position)
{
ProfileElement.Timeline.Override(position, position > ProfileElement.Timeline.Length);
}
#endregion
}

View File

@ -14,15 +14,15 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
{
private readonly string _displayName;
private readonly EventConditionEntity _entity;
private IEventConditionNode _startNode;
private DataModelPath? _eventPath;
private NodeScript<bool> _script;
private bool _wasMet;
private DateTime _lastProcessedTrigger;
private object? _lastProcessedValue;
private EventOverlapMode _overlapMode;
private EventTriggerMode _triggerMode;
private NodeScript<bool> _script;
private IEventConditionNode _startNode;
private EventToggleOffMode _toggleOffMode;
private EventTriggerMode _triggerMode;
private bool _wasMet;
/// <summary>
/// Creates a new instance of the <see cref="EventCondition" /> class
@ -87,7 +87,8 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
}
/// <summary>
/// Gets or sets the mode for render elements when toggling off the event when using <see cref="EventTriggerMode.Toggle"/>.
/// Gets or sets the mode for render elements when toggling off the event when using
/// <see cref="EventTriggerMode.Toggle" />.
/// </summary>
public EventToggleOffMode ToggleOffMode
{
@ -119,7 +120,9 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
_startNode = eventNode;
}
else
{
eventNode = node;
}
IDataModelEvent? dataModelEvent = EventPath?.GetValue() as IDataModelEvent;
eventNode.CreatePins(dataModelEvent);
@ -136,13 +139,25 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
ReplaceStartNode(valueChangedNode);
}
else
{
valueChangedNode = node;
}
valueChangedNode.UpdateOutputPins(EventPath);
}
Script.Save();
}
/// <summary>
/// Gets the start node of the event script, if any
/// </summary>
/// <returns>The start node of the event script, if any.</returns>
public INode GetStartNode()
{
return _startNode;
}
private void ReplaceStartNode(IEventConditionNode newStartNode)
{
if (Script.Nodes.Contains(_startNode))
@ -153,15 +168,6 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
Script.AddNode(_startNode);
}
/// <summary>
/// Gets the start node of the event script, if any
/// </summary>
/// <returns>The start node of the event script, if any.</returns>
public INode GetStartNode()
{
return _startNode;
}
private bool Evaluate()
{
if (EventPath == null)
@ -271,7 +277,7 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
Script = _entity.Script != null
? new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", _entity.Script, ProfileElement.Profile)
: new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile);
: new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile);
UpdateEventNode();
}
@ -356,7 +362,8 @@ public enum EventOverlapMode
}
/// <summary>
/// Represents a mode for render elements when toggling off the event when using <see cref="EventTriggerMode.Toggle"/>.
/// Represents a mode for render elements when toggling off the event when using <see cref="EventTriggerMode.Toggle" />
/// .
/// </summary>
public enum EventToggleOffMode
{

View File

@ -30,12 +30,12 @@ public interface ICondition : IDisposable, IStorageModel
void Update();
/// <summary>
/// Updates the timeline according to the provided <paramref name="deltaTime" /> as the display condition sees fit.
/// Updates the timeline according to the provided <paramref name="deltaTime" /> as the display condition sees fit.
/// </summary>
void UpdateTimeline(double deltaTime);
/// <summary>
/// Overrides the timeline to the provided <paramref name="position"/> as the display condition sees fit.
/// Overrides the timeline to the provided <paramref name="position" /> as the display condition sees fit.
/// </summary>
void OverrideTimeline(TimeSpan position);
}

View File

@ -2,89 +2,84 @@
using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Conditions;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a condition that plays once when its script evaluates to <see langword="true" />.
/// </summary>
public class PlayOnceCondition : ICondition
{
/// <summary>
/// Represents a condition that plays once when its script evaluates to <see langword="true"/>.
/// Creates a new instance of the <see cref="PlayOnceCondition" /> class.
/// </summary>
public class PlayOnceCondition : ICondition
/// <param name="profileElement">The profile element this condition applies to.</param>
public PlayOnceCondition(RenderProfileElement profileElement)
{
/// <summary>
/// Creates a new instance of the <see cref="PlayOnceCondition" /> class.
/// </summary>
/// <param name="profileElement">The profile element this condition applies to.</param>
public PlayOnceCondition(RenderProfileElement profileElement)
{
ProfileElement = profileElement;
Entity = new PlayOnceConditionEntity();
}
/// <summary>
/// Creates a new instance of the <see cref="PlayOnceCondition" /> class.
/// </summary>
/// <param name="entity">The entity used to store this condition.</param>
/// <param name="profileElement">The profile element this condition applies to.</param>
public PlayOnceCondition(PlayOnceConditionEntity entity, RenderProfileElement profileElement)
{
ProfileElement = profileElement;
Entity = entity;
}
#region Implementation of IDisposable
/// <inheritdoc />
public void Dispose()
{
}
#endregion
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
}
/// <inheritdoc />
public void Save()
{
}
#endregion
#region Implementation of ICondition
/// <inheritdoc />
public IConditionEntity Entity { get; }
/// <inheritdoc />
public RenderProfileElement ProfileElement { get; }
/// <inheritdoc />
public bool IsMet { get; private set; }
/// <inheritdoc />
public void Update()
{
if (ProfileElement.Parent is RenderProfileElement parent)
IsMet = parent.DisplayConditionMet;
else
IsMet = true;
}
/// <inheritdoc />
public void UpdateTimeline(double deltaTime)
{
ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), false);
}
/// <inheritdoc />
public void OverrideTimeline(TimeSpan position)
{
ProfileElement.Timeline.Override(position, false);
}
#endregion
ProfileElement = profileElement;
Entity = new PlayOnceConditionEntity();
}
/// <summary>
/// Creates a new instance of the <see cref="PlayOnceCondition" /> class.
/// </summary>
/// <param name="entity">The entity used to store this condition.</param>
/// <param name="profileElement">The profile element this condition applies to.</param>
public PlayOnceCondition(PlayOnceConditionEntity entity, RenderProfileElement profileElement)
{
ProfileElement = profileElement;
Entity = entity;
}
/// <inheritdoc />
public void Dispose()
{
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
}
/// <inheritdoc />
public void Save()
{
}
#endregion
#region Implementation of ICondition
/// <inheritdoc />
public IConditionEntity Entity { get; }
/// <inheritdoc />
public RenderProfileElement ProfileElement { get; }
/// <inheritdoc />
public bool IsMet { get; private set; }
/// <inheritdoc />
public void Update()
{
if (ProfileElement.Parent is RenderProfileElement parent)
IsMet = parent.DisplayConditionMet;
else
IsMet = true;
}
/// <inheritdoc />
public void UpdateTimeline(double deltaTime)
{
ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), false);
}
/// <inheritdoc />
public void OverrideTimeline(TimeSpan position)
{
ProfileElement.Timeline.Override(position, false);
}
#endregion
}

View File

@ -3,191 +3,192 @@ using System.Linq;
using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Conditions;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a condition that is based on a data model value
/// </summary>
public class StaticCondition : CorePropertyChanged, INodeScriptCondition
{
private readonly string _displayName;
private readonly StaticConditionEntity _entity;
private StaticPlayMode _playMode;
private StaticStopMode _stopMode;
private bool _wasMet;
/// <summary>
/// Creates a new instance of the <see cref="StaticCondition" /> class
/// </summary>
public StaticCondition(RenderProfileElement profileElement)
{
_entity = new StaticConditionEntity();
_displayName = profileElement.GetType().Name;
ProfileElement = profileElement;
Script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", profileElement.Profile);
}
internal StaticCondition(StaticConditionEntity entity, RenderProfileElement profileElement)
{
_entity = entity;
_displayName = profileElement.GetType().Name;
ProfileElement = profileElement;
Script = null!;
Load();
}
/// <summary>
/// Gets the script that drives the static condition
/// </summary>
public NodeScript<bool> Script { get; private set; }
/// <summary>
/// Gets or sets the mode in which the render element starts its timeline when display conditions are met
/// </summary>
public StaticPlayMode PlayMode
{
get => _playMode;
set => SetAndNotify(ref _playMode, value);
}
/// <summary>
/// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met
/// </summary>
public StaticStopMode StopMode
{
get => _stopMode;
set => SetAndNotify(ref _stopMode, value);
}
/// <inheritdoc />
public IConditionEntity Entity => _entity;
/// <inheritdoc />
public RenderProfileElement ProfileElement { get; }
/// <inheritdoc />
public bool IsMet { get; private set; }
/// <inheritdoc />
public void Update()
{
_wasMet = IsMet;
// No need to run the script if the parent isn't met anyway
bool parentConditionMet = ProfileElement.Parent is not RenderProfileElement renderProfileElement || renderProfileElement.DisplayConditionMet;
if (!parentConditionMet)
{
IsMet = false;
return;
}
if (!Script.ExitNodeConnected)
{
IsMet = true;
}
else
{
Script.Run();
IsMet = Script.Result;
}
}
/// <inheritdoc />
public void UpdateTimeline(double deltaTime)
{
if (IsMet && !_wasMet && ProfileElement.Timeline.IsFinished)
ProfileElement.Timeline.JumpToStart();
else if (!IsMet && _wasMet && StopMode == StaticStopMode.SkipToEnd)
ProfileElement.Timeline.JumpToEndSegment();
ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), PlayMode == StaticPlayMode.Repeat && IsMet);
}
/// <inheritdoc />
public void OverrideTimeline(TimeSpan position)
{
ProfileElement.Timeline.Override(position, PlayMode == StaticPlayMode.Repeat && position > ProfileElement.Timeline.Length);
}
/// <inheritdoc />
public void Dispose()
{
Script.Dispose();
}
#region Storage
/// <inheritdoc />
public void Load()
{
PlayMode = (StaticPlayMode) _entity.PlayMode;
StopMode = (StaticStopMode) _entity.StopMode;
Script = _entity.Script != null
? new NodeScript<bool>($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", _entity.Script, ProfileElement.Profile)
: new NodeScript<bool>($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", ProfileElement.Profile);
}
/// <inheritdoc />
public void Save()
{
_entity.PlayMode = (int) PlayMode;
_entity.StopMode = (int) StopMode;
// If the exit node isn't connected and there is only the exit node, don't save the script
if (!Script.ExitNodeConnected && Script.Nodes.Count() == 1)
{
_entity.Script = null;
}
else
{
Script.Save();
_entity.Script = Script.Entity;
}
}
/// <inheritdoc />
public INodeScript? NodeScript => Script;
/// <inheritdoc />
public void LoadNodeScript()
{
Script.Load();
}
#endregion
}
/// <summary>
/// Represents a mode for render elements to start their timeline when display conditions are met
/// </summary>
public enum StaticPlayMode
{
/// <summary>
/// Represents a condition that is based on a data model value
/// Continue repeating the main segment of the timeline while the condition is met
/// </summary>
public class StaticCondition : CorePropertyChanged, INodeScriptCondition
{
private readonly string _displayName;
private readonly StaticConditionEntity _entity;
private StaticPlayMode _playMode;
private StaticStopMode _stopMode;
private bool _wasMet;
/// <summary>
/// Creates a new instance of the <see cref="StaticCondition" /> class
/// </summary>
public StaticCondition(RenderProfileElement profileElement)
{
_entity = new StaticConditionEntity();
_displayName = profileElement.GetType().Name;
ProfileElement = profileElement;
Script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", profileElement.Profile);
}
internal StaticCondition(StaticConditionEntity entity, RenderProfileElement profileElement)
{
_entity = entity;
_displayName = profileElement.GetType().Name;
ProfileElement = profileElement;
Script = null!;
Load();
}
/// <summary>
/// Gets the script that drives the static condition
/// </summary>
public NodeScript<bool> Script { get; private set; }
/// <inheritdoc />
public IConditionEntity Entity => _entity;
/// <inheritdoc />
public RenderProfileElement ProfileElement { get; }
/// <inheritdoc />
public bool IsMet { get; private set; }
/// <summary>
/// Gets or sets the mode in which the render element starts its timeline when display conditions are met
/// </summary>
public StaticPlayMode PlayMode
{
get => _playMode;
set => SetAndNotify(ref _playMode, value);
}
/// <summary>
/// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met
/// </summary>
public StaticStopMode StopMode
{
get => _stopMode;
set => SetAndNotify(ref _stopMode, value);
}
/// <inheritdoc />
public void Update()
{
_wasMet = IsMet;
// No need to run the script if the parent isn't met anyway
bool parentConditionMet = ProfileElement.Parent is not RenderProfileElement renderProfileElement || renderProfileElement.DisplayConditionMet;
if (!parentConditionMet)
{
IsMet = false;
return;
}
if (!Script.ExitNodeConnected)
IsMet = true;
else
{
Script.Run();
IsMet = Script.Result;
}
}
/// <inheritdoc />
public void UpdateTimeline(double deltaTime)
{
if (IsMet && !_wasMet && ProfileElement.Timeline.IsFinished)
ProfileElement.Timeline.JumpToStart();
else if (!IsMet && _wasMet && StopMode == StaticStopMode.SkipToEnd)
ProfileElement.Timeline.JumpToEndSegment();
ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), PlayMode == StaticPlayMode.Repeat && IsMet);
}
/// <inheritdoc />
public void OverrideTimeline(TimeSpan position)
{
ProfileElement.Timeline.Override(position, PlayMode == StaticPlayMode.Repeat && position > ProfileElement.Timeline.Length);
}
/// <inheritdoc />
public void Dispose()
{
Script.Dispose();
}
#region Storage
/// <inheritdoc />
public void Load()
{
PlayMode = (StaticPlayMode) _entity.PlayMode;
StopMode = (StaticStopMode) _entity.StopMode;
Script = _entity.Script != null
? new NodeScript<bool>($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", _entity.Script, ProfileElement.Profile)
: new NodeScript<bool>($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", ProfileElement.Profile);
}
/// <inheritdoc />
public void Save()
{
_entity.PlayMode = (int) PlayMode;
_entity.StopMode = (int) StopMode;
// If the exit node isn't connected and there is only the exit node, don't save the script
if (!Script.ExitNodeConnected && Script.Nodes.Count() == 1)
{
_entity.Script = null;
}
else
{
Script.Save();
_entity.Script = Script.Entity;
}
}
/// <inheritdoc />
public INodeScript? NodeScript => Script;
/// <inheritdoc />
public void LoadNodeScript()
{
Script.Load();
}
#endregion
}
Repeat,
/// <summary>
/// Represents a mode for render elements to start their timeline when display conditions are met
/// Only play the timeline once when the condition is met
/// </summary>
public enum StaticPlayMode
{
/// <summary>
/// Continue repeating the main segment of the timeline while the condition is met
/// </summary>
Repeat,
Once
}
/// <summary>
/// Only play the timeline once when the condition is met
/// </summary>
Once
}
/// <summary>
/// Represents a mode for render elements to stop their timeline when display conditions are no longer met
/// </summary>
public enum StaticStopMode
{
/// <summary>
/// When conditions are no longer met, finish the the current run of the main timeline
/// </summary>
Finish,
/// <summary>
/// Represents a mode for render elements to stop their timeline when display conditions are no longer met
/// When conditions are no longer met, skip to the end segment of the timeline
/// </summary>
public enum StaticStopMode
{
/// <summary>
/// When conditions are no longer met, finish the the current run of the main timeline
/// </summary>
Finish,
/// <summary>
/// When conditions are no longer met, skip to the end segment of the timeline
/// </summary>
SkipToEnd
}
SkipToEnd
}

View File

@ -4,242 +4,243 @@ using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Storage.Entities.Profile.DataBindings;
namespace Artemis.Core
namespace Artemis.Core;
/// <inheritdoc />
public class DataBinding<TLayerProperty> : IDataBinding
{
/// <inheritdoc />
public class DataBinding<TLayerProperty> : IDataBinding
private readonly List<IDataBindingProperty> _properties = new();
private bool _disposed;
private bool _isEnabled;
private DataBindingNodeScript<TLayerProperty> _script;
internal DataBinding(LayerProperty<TLayerProperty> layerProperty)
{
private readonly List<IDataBindingProperty> _properties = new();
private bool _disposed;
private bool _isEnabled;
private DataBindingNodeScript<TLayerProperty> _script;
LayerProperty = layerProperty;
internal DataBinding(LayerProperty<TLayerProperty> layerProperty)
{
LayerProperty = layerProperty;
Entity = new DataBindingEntity();
_script = new DataBindingNodeScript<TLayerProperty>(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile);
Entity = new DataBindingEntity();
_script = new DataBindingNodeScript<TLayerProperty>(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile);
Save();
}
internal DataBinding(LayerProperty<TLayerProperty> layerProperty, DataBindingEntity entity)
{
LayerProperty = layerProperty;
Entity = entity;
_script = new DataBindingNodeScript<TLayerProperty>(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile);
// Load will add children so be initialized before that
Load();
}
/// <summary>
/// Gets the layer property this data binding targets
/// </summary>
public LayerProperty<TLayerProperty> LayerProperty { get; }
/// <inheritdoc />
public INodeScript Script => _script;
/// <summary>
/// Gets the data binding entity this data binding uses for persistent storage
/// </summary>
public DataBindingEntity Entity { get; }
/// <summary>
/// Updates the pending values of this data binding
/// </summary>
public void Update()
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
if (!IsEnabled)
return;
// TODO: Update the 'base value' node
Script.Run();
}
/// <summary>
/// Registers a data binding property so that is available to the data binding system
/// </summary>
/// <typeparam name="TProperty">The type of the layer property</typeparam>
/// <param name="getter">The function to call to get the value of the property</param>
/// <param name="setter">The action to call to set the value of the property</param>
/// <param name="displayName">The display name of the data binding property</param>
public DataBindingProperty<TProperty> RegisterDataBindingProperty<TProperty>(Func<TProperty> getter, Action<TProperty?> setter, string displayName)
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
if (Properties.Any(d => d.DisplayName == displayName))
throw new ArtemisCoreException($"A data binding property named '{displayName}' is already registered.");
DataBindingProperty<TProperty> property = new(getter, setter, displayName);
_properties.Add(property);
OnDataBindingPropertyRegistered();
return property;
}
/// <summary>
/// Removes all data binding properties so they are no longer available to the data binding system
/// </summary>
public void ClearDataBindingProperties()
{
if (_disposed)
throw new ObjectDisposedException("LayerProperty");
_properties.Clear();
OnDataBindingPropertiesCleared();
}
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_disposed = true;
_isEnabled = false;
Script.Dispose();
}
}
/// <summary>
/// Invokes the <see cref="DataBindingPropertyRegistered" /> event
/// </summary>
protected virtual void OnDataBindingPropertyRegistered()
{
DataBindingPropertyRegistered?.Invoke(this, new DataBindingEventArgs(this));
}
/// <summary>
/// Invokes the <see cref="DataBindingPropertiesCleared" /> event
/// </summary>
protected virtual void OnDataBindingPropertiesCleared()
{
DataBindingPropertiesCleared?.Invoke(this, new DataBindingEventArgs(this));
}
/// <summary>
/// Invokes the <see cref="DataBindingEnabled" /> event
/// </summary>
protected virtual void OnDataBindingEnabled(DataBindingEventArgs e)
{
DataBindingEnabled?.Invoke(this, e);
}
/// <summary>
/// Invokes the <see cref="DataBindingDisabled" /> event
/// </summary>
protected virtual void OnDataBindingDisabled(DataBindingEventArgs e)
{
DataBindingDisabled?.Invoke(this, e);
}
private string GetScriptName()
{
return LayerProperty.PropertyDescription.Name ?? LayerProperty.Path;
}
/// <inheritdoc />
public ILayerProperty BaseLayerProperty => LayerProperty;
/// <inheritdoc />
public bool IsEnabled
{
get => _isEnabled;
set
{
_isEnabled = value;
if (_isEnabled)
OnDataBindingEnabled(new DataBindingEventArgs(this));
else
OnDataBindingDisabled(new DataBindingEventArgs(this));
}
}
/// <inheritdoc />
public ReadOnlyCollection<IDataBindingProperty> Properties => _properties.AsReadOnly();
/// <inheritdoc />
public void Apply()
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
if (!IsEnabled)
return;
_script.DataBindingExitNode.ApplyToDataBinding();
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <inheritdoc />
public event EventHandler<DataBindingEventArgs>? DataBindingPropertyRegistered;
/// <inheritdoc />
public event EventHandler<DataBindingEventArgs>? DataBindingPropertiesCleared;
/// <inheritdoc />
public event EventHandler<DataBindingEventArgs>? DataBindingEnabled;
/// <inheritdoc />
public event EventHandler<DataBindingEventArgs>? DataBindingDisabled;
#region Storage
/// <inheritdoc />
public void Load()
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
IsEnabled = Entity.IsEnabled;
}
/// <inheritdoc />
public void LoadNodeScript()
{
_script.Dispose();
_script = Entity.NodeScript != null
? new DataBindingNodeScript<TLayerProperty>(GetScriptName(), "The value to put into the data binding", this, Entity.NodeScript, LayerProperty.ProfileElement.Profile)
: new DataBindingNodeScript<TLayerProperty>(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile);
}
/// <inheritdoc />
public void Save()
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
Entity.IsEnabled = IsEnabled;
if (_script.ExitNodeConnected || _script.Nodes.Count() > 1)
{
_script.Save();
Entity.NodeScript = _script.Entity;
}
else
Entity.NodeScript = null;
}
#endregion
Save();
}
internal DataBinding(LayerProperty<TLayerProperty> layerProperty, DataBindingEntity entity)
{
LayerProperty = layerProperty;
Entity = entity;
_script = new DataBindingNodeScript<TLayerProperty>(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile);
// Load will add children so be initialized before that
Load();
}
/// <summary>
/// Gets the layer property this data binding targets
/// </summary>
public LayerProperty<TLayerProperty> LayerProperty { get; }
/// <summary>
/// Gets the data binding entity this data binding uses for persistent storage
/// </summary>
public DataBindingEntity Entity { get; }
/// <summary>
/// Updates the pending values of this data binding
/// </summary>
public void Update()
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
if (!IsEnabled)
return;
// TODO: Update the 'base value' node
Script.Run();
}
/// <summary>
/// Registers a data binding property so that is available to the data binding system
/// </summary>
/// <typeparam name="TProperty">The type of the layer property</typeparam>
/// <param name="getter">The function to call to get the value of the property</param>
/// <param name="setter">The action to call to set the value of the property</param>
/// <param name="displayName">The display name of the data binding property</param>
public DataBindingProperty<TProperty> RegisterDataBindingProperty<TProperty>(Func<TProperty> getter, Action<TProperty?> setter, string displayName)
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
if (Properties.Any(d => d.DisplayName == displayName))
throw new ArtemisCoreException($"A data binding property named '{displayName}' is already registered.");
DataBindingProperty<TProperty> property = new(getter, setter, displayName);
_properties.Add(property);
OnDataBindingPropertyRegistered();
return property;
}
/// <summary>
/// Removes all data binding properties so they are no longer available to the data binding system
/// </summary>
public void ClearDataBindingProperties()
{
if (_disposed)
throw new ObjectDisposedException("LayerProperty");
_properties.Clear();
OnDataBindingPropertiesCleared();
}
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_disposed = true;
_isEnabled = false;
Script.Dispose();
}
}
/// <summary>
/// Invokes the <see cref="DataBindingPropertyRegistered" /> event
/// </summary>
protected virtual void OnDataBindingPropertyRegistered()
{
DataBindingPropertyRegistered?.Invoke(this, new DataBindingEventArgs(this));
}
/// <summary>
/// Invokes the <see cref="DataBindingPropertiesCleared" /> event
/// </summary>
protected virtual void OnDataBindingPropertiesCleared()
{
DataBindingPropertiesCleared?.Invoke(this, new DataBindingEventArgs(this));
}
/// <summary>
/// Invokes the <see cref="DataBindingEnabled" /> event
/// </summary>
protected virtual void OnDataBindingEnabled(DataBindingEventArgs e)
{
DataBindingEnabled?.Invoke(this, e);
}
/// <summary>
/// Invokes the <see cref="DataBindingDisabled" /> event
/// </summary>
protected virtual void OnDataBindingDisabled(DataBindingEventArgs e)
{
DataBindingDisabled?.Invoke(this, e);
}
private string GetScriptName()
{
return LayerProperty.PropertyDescription.Name ?? LayerProperty.Path;
}
/// <inheritdoc />
public INodeScript Script => _script;
/// <inheritdoc />
public ILayerProperty BaseLayerProperty => LayerProperty;
/// <inheritdoc />
public bool IsEnabled
{
get => _isEnabled;
set
{
_isEnabled = value;
if (_isEnabled)
OnDataBindingEnabled(new DataBindingEventArgs(this));
else
OnDataBindingDisabled(new DataBindingEventArgs(this));
}
}
/// <inheritdoc />
public ReadOnlyCollection<IDataBindingProperty> Properties => _properties.AsReadOnly();
/// <inheritdoc />
public void Apply()
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
if (!IsEnabled)
return;
_script.DataBindingExitNode.ApplyToDataBinding();
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <inheritdoc />
public event EventHandler<DataBindingEventArgs>? DataBindingPropertyRegistered;
/// <inheritdoc />
public event EventHandler<DataBindingEventArgs>? DataBindingPropertiesCleared;
/// <inheritdoc />
public event EventHandler<DataBindingEventArgs>? DataBindingEnabled;
/// <inheritdoc />
public event EventHandler<DataBindingEventArgs>? DataBindingDisabled;
#region Storage
/// <inheritdoc />
public void Load()
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
IsEnabled = Entity.IsEnabled;
}
/// <inheritdoc />
public void LoadNodeScript()
{
_script.Dispose();
_script = Entity.NodeScript != null
? new DataBindingNodeScript<TLayerProperty>(GetScriptName(), "The value to put into the data binding", this, Entity.NodeScript, LayerProperty.ProfileElement.Profile)
: new DataBindingNodeScript<TLayerProperty>(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile);
}
/// <inheritdoc />
public void Save()
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
Entity.IsEnabled = IsEnabled;
if (_script.ExitNodeConnected || _script.Nodes.Count() > 1)
{
_script.Save();
Entity.NodeScript = _script.Entity;
}
else
{
Entity.NodeScript = null;
}
}
#endregion
}

View File

@ -1,63 +1,62 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <inheritdoc />
public class DataBindingProperty<TProperty> : IDataBindingProperty
{
/// <inheritdoc />
public class DataBindingProperty<TProperty> : IDataBindingProperty
internal DataBindingProperty(Func<TProperty> getter, Action<TProperty?> setter, string displayName)
{
internal DataBindingProperty(Func<TProperty> getter, Action<TProperty?> setter, string displayName)
Getter = getter ?? throw new ArgumentNullException(nameof(getter));
Setter = setter ?? throw new ArgumentNullException(nameof(setter));
DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName));
}
/// <summary>
/// Gets the function to call to get the value of the property
/// </summary>
public Func<TProperty> Getter { get; }
/// <summary>
/// Gets the action to call to set the value of the property
/// </summary>
public Action<TProperty?> Setter { get; }
/// <inheritdoc />
public string DisplayName { get; }
/// <inheritdoc />
public Type ValueType => typeof(TProperty);
/// <inheritdoc />
public object? GetValue()
{
return Getter();
}
/// <inheritdoc />
public void SetValue(object? value)
{
// Numeric has a bunch of conversion, this seems the cheapest way to use them :)
switch (value)
{
Getter = getter ?? throw new ArgumentNullException(nameof(getter));
Setter = setter ?? throw new ArgumentNullException(nameof(setter));
DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName));
}
/// <summary>
/// Gets the function to call to get the value of the property
/// </summary>
public Func<TProperty> Getter { get; }
/// <summary>
/// Gets the action to call to set the value of the property
/// </summary>
public Action<TProperty?> Setter { get; }
/// <inheritdoc />
public string DisplayName { get; }
/// <inheritdoc />
public Type ValueType => typeof(TProperty);
/// <inheritdoc />
public object? GetValue()
{
return Getter();
}
/// <inheritdoc />
public void SetValue(object? value)
{
// Numeric has a bunch of conversion, this seems the cheapest way to use them :)
switch (value)
{
case TProperty match:
Setter(match);
break;
case Numeric numeric when Setter is Action<float> floatSetter:
floatSetter(numeric);
break;
case Numeric numeric when Setter is Action<int> intSetter:
intSetter(numeric);
break;
case Numeric numeric when Setter is Action<double> doubleSetter:
doubleSetter(numeric);
break;
case Numeric numeric when Setter is Action<byte> byteSetter:
byteSetter(numeric);
break;
default:
throw new ArgumentException("Value must match the type of the data binding registration", nameof(value));
}
case TProperty match:
Setter(match);
break;
case Numeric numeric when Setter is Action<float> floatSetter:
floatSetter(numeric);
break;
case Numeric numeric when Setter is Action<int> intSetter:
intSetter(numeric);
break;
case Numeric numeric when Setter is Action<double> doubleSetter:
doubleSetter(numeric);
break;
case Numeric numeric when Setter is Action<byte> byteSetter:
byteSetter(numeric);
break;
default:
throw new ArgumentException("Value must match the type of the data binding registration", nameof(value));
}
}
}

View File

@ -2,62 +2,61 @@
using System.Collections.ObjectModel;
using Artemis.Core.Modules;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a data binding that binds a certain <see cref="LayerProperty{T}" /> to a value inside a
/// <see cref="DataModel" />
/// </summary>
public interface IDataBinding : IStorageModel, IDisposable
{
/// <summary>
/// Represents a data binding that binds a certain <see cref="LayerProperty{T}" /> to a value inside a
/// <see cref="DataModel" />
/// Gets the layer property the data binding is applied to
/// </summary>
public interface IDataBinding : IStorageModel, IDisposable
{
/// <summary>
/// Gets the layer property the data binding is applied to
/// </summary>
ILayerProperty BaseLayerProperty { get; }
ILayerProperty BaseLayerProperty { get; }
/// <summary>
/// Gets the script used to populate the data binding
/// </summary>
INodeScript Script { get; }
/// <summary>
/// Gets the script used to populate the data binding
/// </summary>
INodeScript Script { get; }
/// <summary>
/// Gets a list of sub-properties this data binding applies to
/// </summary>
ReadOnlyCollection<IDataBindingProperty> Properties { get; }
/// <summary>
/// Gets a list of sub-properties this data binding applies to
/// </summary>
ReadOnlyCollection<IDataBindingProperty> Properties { get; }
/// <summary>
/// Gets a boolean indicating whether the data binding is enabled or not
/// </summary>
bool IsEnabled { get; set; }
/// <summary>
/// Gets a boolean indicating whether the data binding is enabled or not
/// </summary>
bool IsEnabled { get; set; }
/// <summary>
/// Applies the pending value of the data binding to the property
/// </summary>
void Apply();
/// <summary>
/// Applies the pending value of the data binding to the property
/// </summary>
void Apply();
/// <summary>
/// If the data binding is enabled, loads the node script for that data binding
/// </summary>
void LoadNodeScript();
/// <summary>
/// If the data binding is enabled, loads the node script for that data binding
/// </summary>
void LoadNodeScript();
/// <summary>
/// Occurs when a data binding property has been added
/// </summary>
public event EventHandler<DataBindingEventArgs>? DataBindingPropertyRegistered;
/// <summary>
/// Occurs when a data binding property has been added
/// </summary>
public event EventHandler<DataBindingEventArgs>? DataBindingPropertyRegistered;
/// <summary>
/// Occurs when all data binding properties have been removed
/// </summary>
public event EventHandler<DataBindingEventArgs>? DataBindingPropertiesCleared;
/// <summary>
/// Occurs when all data binding properties have been removed
/// </summary>
public event EventHandler<DataBindingEventArgs>? DataBindingPropertiesCleared;
/// <summary>
/// Occurs when a data binding has been enabled
/// </summary>
public event EventHandler<DataBindingEventArgs>? DataBindingEnabled;
/// <summary>
/// Occurs when a data binding has been enabled
/// </summary>
public event EventHandler<DataBindingEventArgs>? DataBindingEnabled;
/// <summary>
/// Occurs when a data binding has been disabled
/// </summary>
public event EventHandler<DataBindingEventArgs>? DataBindingDisabled;
}
/// <summary>
/// Occurs when a data binding has been disabled
/// </summary>
public event EventHandler<DataBindingEventArgs>? DataBindingDisabled;
}

View File

@ -1,32 +1,31 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a data binding registration
/// </summary>
public interface IDataBindingProperty
{
/// <summary>
/// Represents a data binding registration
/// Gets or sets the display name of the data binding registration
/// </summary>
public interface IDataBindingProperty
{
/// <summary>
/// Gets or sets the display name of the data binding registration
/// </summary>
string DisplayName { get; }
string DisplayName { get; }
/// <summary>
/// Gets the type of the value this data binding registration points to
/// </summary>
Type ValueType { get; }
/// <summary>
/// Gets the type of the value this data binding registration points to
/// </summary>
Type ValueType { get; }
/// <summary>
/// Gets the value of the property this registration points to
/// </summary>
/// <returns>A value matching the type of <see cref="ValueType" /></returns>
object? GetValue();
/// <summary>
/// Gets the value of the property this registration points to
/// </summary>
/// <returns>A value matching the type of <see cref="ValueType" /></returns>
object? GetValue();
/// <summary>
/// Sets the value of the property this registration points to
/// </summary>
/// <param name="value">A value matching the type of <see cref="ValueType" /></param>
void SetValue(object? value);
}
/// <summary>
/// Sets the value of the property this registration points to
/// </summary>
/// <param name="value">A value matching the type of <see cref="ValueType" /></param>
void SetValue(object? value);
}

View File

@ -3,249 +3,244 @@ using System.Collections.Generic;
using System.Linq;
using Artemis.Core.Modules;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a data model event with event arguments of type <typeparamref name="T" />
/// </summary>
public class DataModelEvent<T> : IDataModelEvent where T : DataModelEventArgs
{
private bool _trackHistory;
/// <summary>
/// Represents a data model event with event arguments of type <typeparamref name="T" />
/// Creates a new instance of the <see cref="DataModelEvent{T}" /> class with history tracking disabled
/// </summary>
public class DataModelEvent<T> : IDataModelEvent where T : DataModelEventArgs
public DataModelEvent()
{
private bool _trackHistory;
/// <summary>
/// Creates a new instance of the <see cref="DataModelEvent{T}" /> class with history tracking disabled
/// </summary>
public DataModelEvent()
{
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelEvent{T}" />
/// </summary>
/// <param name="trackHistory">A boolean indicating whether the last 20 events should be tracked</param>
public DataModelEvent(bool trackHistory)
{
_trackHistory = trackHistory;
}
/// <inheritdoc />
[DataModelProperty(Name = "Last trigger", Description = "The time at which the event last triggered")]
public DateTime LastTrigger { get; private set; }
/// <inheritdoc />
[DataModelProperty(Name = "Time since trigger", Description = "The time that has passed since the last trigger")]
public TimeSpan TimeSinceLastTrigger => DateTime.Now - LastTrigger;
/// <summary>
/// Gets the event arguments of the last time the event was triggered
/// </summary>
[DataModelProperty(Description = "The arguments of the last time this event triggered")]
public T? LastEventArguments { get; private set; }
/// <inheritdoc />
[DataModelProperty(Description = "The total amount of times this event has triggered since the module was activated")]
public int TriggerCount { get; private set; }
/// <summary>
/// Gets a queue of the last 20 event arguments
/// <para>Always empty if <see cref="TrackHistory" /> is <see langword="false" /></para>
/// </summary>
[DataModelProperty(Description = "The arguments of the last time this event triggered")]
public Queue<T> EventArgumentsHistory { get; } = new(20);
/// <summary>
/// Trigger the event with the given <paramref name="eventArgs" />
/// </summary>
/// <param name="eventArgs">The event argument to pass to the event</param>
public void Trigger(T eventArgs)
{
if (eventArgs == null) throw new ArgumentNullException(nameof(eventArgs));
eventArgs.TriggerTime = DateTime.Now;
LastEventArguments = eventArgs;
LastTrigger = DateTime.Now;
TriggerCount++;
if (TrackHistory)
{
lock (EventArgumentsHistory)
{
if (EventArgumentsHistory.Count == 20)
EventArgumentsHistory.Dequeue();
EventArgumentsHistory.Enqueue(eventArgs);
}
}
OnEventTriggered();
}
internal virtual void OnEventTriggered()
{
EventTriggered?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc />
[DataModelIgnore]
public Type ArgumentsType => typeof(T);
/// <inheritdoc />
[DataModelIgnore]
public string TriggerPastParticiple => "triggered";
/// <inheritdoc />
[DataModelIgnore]
public bool TrackHistory
{
get => _trackHistory;
set
{
EventArgumentsHistory.Clear();
_trackHistory = value;
}
}
/// <inheritdoc />
[DataModelIgnore]
public DataModelEventArgs? LastEventArgumentsUntyped => LastEventArguments;
/// <inheritdoc />
[DataModelIgnore]
public List<DataModelEventArgs> EventArgumentsHistoryUntyped => EventArgumentsHistory.Cast<DataModelEventArgs>().ToList();
/// <inheritdoc />
public event EventHandler? EventTriggered;
/// <inheritdoc />
public void Reset()
{
TriggerCount = 0;
EventArgumentsHistory.Clear();
}
/// <inheritdoc />
public void Update()
{
}
}
/// <summary>
/// Represents a data model event without event arguments
/// Creates a new instance of the <see cref="DataModelEvent{T}" />
/// </summary>
public class DataModelEvent : IDataModelEvent
/// <param name="trackHistory">A boolean indicating whether the last 20 events should be tracked</param>
public DataModelEvent(bool trackHistory)
{
private bool _trackHistory;
_trackHistory = trackHistory;
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelEvent" /> class with history tracking disabled
/// </summary>
public DataModelEvent()
{
}
/// <summary>
/// Gets the event arguments of the last time the event was triggered
/// </summary>
[DataModelProperty(Description = "The arguments of the last time this event triggered")]
public T? LastEventArguments { get; private set; }
/// <summary>
/// Creates a new instance of the <see cref="DataModelEvent" />
/// </summary>
/// <param name="trackHistory">A boolean indicating whether the last 20 events should be tracked</param>
public DataModelEvent(bool trackHistory)
{
_trackHistory = trackHistory;
}
/// <summary>
/// Gets a queue of the last 20 event arguments
/// <para>Always empty if <see cref="TrackHistory" /> is <see langword="false" /></para>
/// </summary>
[DataModelProperty(Description = "The arguments of the last time this event triggered")]
public Queue<T> EventArgumentsHistory { get; } = new(20);
/// <inheritdoc />
[DataModelProperty(Name = "Last trigger", Description = "The time at which the event last triggered")]
public DateTime LastTrigger { get; private set; }
/// <summary>
/// Trigger the event with the given <paramref name="eventArgs" />
/// </summary>
/// <param name="eventArgs">The event argument to pass to the event</param>
public void Trigger(T eventArgs)
{
if (eventArgs == null) throw new ArgumentNullException(nameof(eventArgs));
eventArgs.TriggerTime = DateTime.Now;
/// <inheritdoc />
[DataModelProperty(Name = "Time since trigger", Description = "The time that has passed since the last trigger")]
public TimeSpan TimeSinceLastTrigger => DateTime.Now - LastTrigger;
LastEventArguments = eventArgs;
LastTrigger = DateTime.Now;
TriggerCount++;
/// <summary>
/// Gets the event arguments of the last time the event was triggered
/// </summary>
[DataModelProperty(Description = "The arguments of the last time this event triggered")]
public DataModelEventArgs? LastTriggerArguments { get; private set; }
/// <inheritdoc />
[DataModelProperty(Description = "The total amount of times this event has triggered since the module was activated")]
public int TriggerCount { get; private set; }
/// <summary>
/// Gets a queue of the last 20 event arguments
/// <para>Always empty if <see cref="TrackHistory" /> is <see langword="false" /></para>
/// </summary>
[DataModelProperty(Description = "The arguments of the last time this event triggered")]
public Queue<DataModelEventArgs> EventArgumentsHistory { get; } = new(20);
/// <summary>
/// Trigger the event
/// </summary>
public void Trigger()
{
DataModelEventArgs eventArgs = new() {TriggerTime = DateTime.Now};
LastTriggerArguments = eventArgs;
LastTrigger = DateTime.Now;
TriggerCount++;
if (TrackHistory)
if (TrackHistory)
lock (EventArgumentsHistory)
{
lock (EventArgumentsHistory)
{
if (EventArgumentsHistory.Count == 20)
EventArgumentsHistory.Dequeue();
EventArgumentsHistory.Enqueue(eventArgs);
}
if (EventArgumentsHistory.Count == 20)
EventArgumentsHistory.Dequeue();
EventArgumentsHistory.Enqueue(eventArgs);
}
OnEventTriggered();
}
OnEventTriggered();
}
internal virtual void OnEventTriggered()
internal virtual void OnEventTriggered()
{
EventTriggered?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc />
[DataModelProperty(Name = "Last trigger", Description = "The time at which the event last triggered")]
public DateTime LastTrigger { get; private set; }
/// <inheritdoc />
[DataModelProperty(Name = "Time since trigger", Description = "The time that has passed since the last trigger")]
public TimeSpan TimeSinceLastTrigger => DateTime.Now - LastTrigger;
/// <inheritdoc />
[DataModelProperty(Description = "The total amount of times this event has triggered since the module was activated")]
public int TriggerCount { get; private set; }
/// <inheritdoc />
[DataModelIgnore]
public Type ArgumentsType => typeof(T);
/// <inheritdoc />
[DataModelIgnore]
public string TriggerPastParticiple => "triggered";
/// <inheritdoc />
[DataModelIgnore]
public bool TrackHistory
{
get => _trackHistory;
set
{
EventTriggered?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc />
[DataModelIgnore]
public Type ArgumentsType => typeof(DataModelEventArgs);
/// <inheritdoc />
[DataModelIgnore]
public string TriggerPastParticiple => "triggered";
/// <inheritdoc />
[DataModelIgnore]
public bool TrackHistory
{
get => _trackHistory;
set
{
EventArgumentsHistory.Clear();
_trackHistory = value;
}
}
/// <inheritdoc />
[DataModelIgnore]
public DataModelEventArgs? LastEventArgumentsUntyped => LastTriggerArguments;
/// <inheritdoc />
[DataModelIgnore]
public List<DataModelEventArgs> EventArgumentsHistoryUntyped => EventArgumentsHistory.ToList();
/// <inheritdoc />
public event EventHandler? EventTriggered;
/// <inheritdoc />
public void Reset()
{
TriggerCount = 0;
EventArgumentsHistory.Clear();
_trackHistory = value;
}
}
/// <inheritdoc />
public void Update()
/// <inheritdoc />
[DataModelIgnore]
public DataModelEventArgs? LastEventArgumentsUntyped => LastEventArguments;
/// <inheritdoc />
[DataModelIgnore]
public List<DataModelEventArgs> EventArgumentsHistoryUntyped => EventArgumentsHistory.Cast<DataModelEventArgs>().ToList();
/// <inheritdoc />
public event EventHandler? EventTriggered;
/// <inheritdoc />
public void Reset()
{
TriggerCount = 0;
EventArgumentsHistory.Clear();
}
/// <inheritdoc />
public void Update()
{
}
}
/// <summary>
/// Represents a data model event without event arguments
/// </summary>
public class DataModelEvent : IDataModelEvent
{
private bool _trackHistory;
/// <summary>
/// Creates a new instance of the <see cref="DataModelEvent" /> class with history tracking disabled
/// </summary>
public DataModelEvent()
{
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelEvent" />
/// </summary>
/// <param name="trackHistory">A boolean indicating whether the last 20 events should be tracked</param>
public DataModelEvent(bool trackHistory)
{
_trackHistory = trackHistory;
}
/// <summary>
/// Gets the event arguments of the last time the event was triggered
/// </summary>
[DataModelProperty(Description = "The arguments of the last time this event triggered")]
public DataModelEventArgs? LastTriggerArguments { get; private set; }
/// <summary>
/// Gets a queue of the last 20 event arguments
/// <para>Always empty if <see cref="TrackHistory" /> is <see langword="false" /></para>
/// </summary>
[DataModelProperty(Description = "The arguments of the last time this event triggered")]
public Queue<DataModelEventArgs> EventArgumentsHistory { get; } = new(20);
/// <summary>
/// Trigger the event
/// </summary>
public void Trigger()
{
DataModelEventArgs eventArgs = new() {TriggerTime = DateTime.Now};
LastTriggerArguments = eventArgs;
LastTrigger = DateTime.Now;
TriggerCount++;
if (TrackHistory)
lock (EventArgumentsHistory)
{
if (EventArgumentsHistory.Count == 20)
EventArgumentsHistory.Dequeue();
EventArgumentsHistory.Enqueue(eventArgs);
}
OnEventTriggered();
}
internal virtual void OnEventTriggered()
{
EventTriggered?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc />
[DataModelProperty(Name = "Last trigger", Description = "The time at which the event last triggered")]
public DateTime LastTrigger { get; private set; }
/// <inheritdoc />
[DataModelProperty(Name = "Time since trigger", Description = "The time that has passed since the last trigger")]
public TimeSpan TimeSinceLastTrigger => DateTime.Now - LastTrigger;
/// <inheritdoc />
[DataModelProperty(Description = "The total amount of times this event has triggered since the module was activated")]
public int TriggerCount { get; private set; }
/// <inheritdoc />
[DataModelIgnore]
public Type ArgumentsType => typeof(DataModelEventArgs);
/// <inheritdoc />
[DataModelIgnore]
public string TriggerPastParticiple => "triggered";
/// <inheritdoc />
[DataModelIgnore]
public bool TrackHistory
{
get => _trackHistory;
set
{
EventArgumentsHistory.Clear();
_trackHistory = value;
}
}
/// <inheritdoc />
[DataModelIgnore]
public DataModelEventArgs? LastEventArgumentsUntyped => LastTriggerArguments;
/// <inheritdoc />
[DataModelIgnore]
public List<DataModelEventArgs> EventArgumentsHistoryUntyped => EventArgumentsHistory.ToList();
/// <inheritdoc />
public event EventHandler? EventTriggered;
/// <inheritdoc />
public void Reset()
{
TriggerCount = 0;
EventArgumentsHistory.Clear();
}
/// <inheritdoc />
public void Update()
{
}
}

View File

@ -1,17 +1,16 @@
using System;
using Artemis.Core.Modules;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents the base class for data model events that contain event data
/// </summary>
public class DataModelEventArgs
{
/// <summary>
/// Represents the base class for data model events that contain event data
/// Gets the time at which the event with these arguments was triggered
/// </summary>
public class DataModelEventArgs
{
/// <summary>
/// Gets the time at which the event with these arguments was triggered
/// </summary>
[DataModelIgnore]
public DateTime TriggerTime { get; internal set; }
}
[DataModelIgnore]
public DateTime TriggerTime { get; internal set; }
}

View File

@ -6,388 +6,388 @@ using System.Reflection;
using Artemis.Core.Modules;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a path that points to a property in data model
/// </summary>
public class DataModelPath : IStorageModel, IDisposable
{
private readonly LinkedList<DataModelPathSegment> _segments;
private Expression<Func<object, object>>? _accessorLambda;
private bool _disposed;
/// <summary>
/// Represents a path that points to a property in data model
/// Creates a new instance of the <see cref="DataModelPath" /> class pointing directly to the target
/// </summary>
public class DataModelPath : IStorageModel, IDisposable
/// <param name="target">The target at which this path starts</param>
public DataModelPath(DataModel target)
{
private readonly LinkedList<DataModelPathSegment> _segments;
private Expression<Func<object, object>>? _accessorLambda;
private bool _disposed;
Target = target ?? throw new ArgumentNullException(nameof(target));
Path = "";
Entity = new DataModelPathEntity();
/// <summary>
/// Creates a new instance of the <see cref="DataModelPath" /> class pointing directly to the target
/// </summary>
/// <param name="target">The target at which this path starts</param>
public DataModelPath(DataModel target)
{
Target = target ?? throw new ArgumentNullException(nameof(target));
Path = "";
Entity = new DataModelPathEntity();
_segments = new LinkedList<DataModelPathSegment>();
_segments = new LinkedList<DataModelPathSegment>();
Save();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelPath" /> class pointing to the provided path
/// </summary>
/// <param name="target">The target at which this path starts</param>
/// <param name="path">A point-separated path</param>
public DataModelPath(DataModel target, string path)
{
Target = target ?? throw new ArgumentNullException(nameof(target));
Path = path ?? throw new ArgumentNullException(nameof(path));
Entity = new DataModelPathEntity();
_segments = new LinkedList<DataModelPathSegment>();
Save();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelPath" /> class based on an existing path
/// </summary>
/// <param name="dataModelPath">The path to base the new instance on</param>
public DataModelPath(DataModelPath dataModelPath)
{
if (dataModelPath == null)
throw new ArgumentNullException(nameof(dataModelPath));
Target = dataModelPath.Target;
Path = dataModelPath.Path;
Entity = new DataModelPathEntity();
_segments = new LinkedList<DataModelPathSegment>();
Save();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelPath" /> class based on a <see cref="DataModelPathEntity" />
/// </summary>
/// <param name="entity"></param>
public DataModelPath(DataModelPathEntity entity)
{
Path = entity.Path;
Entity = entity;
_segments = new LinkedList<DataModelPathSegment>();
Load();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Gets the data model at which this path starts
/// </summary>
public DataModel? Target { get; private set; }
/// <summary>
/// Gets the data model ID of the <see cref="Target" /> if it is a <see cref="DataModel" />
/// </summary>
public string? DataModelId => Target?.Module.Id;
/// <summary>
/// Gets the point-separated path associated with this <see cref="DataModelPath" />
/// </summary>
public string Path { get; private set; }
/// <summary>
/// Gets a boolean indicating whether all <see cref="Segments" /> are valid
/// </summary>
public bool IsValid => Segments.Any() && Segments.All(p => p.Type != DataModelPathSegmentType.Invalid);
/// <summary>
/// Gets a read-only list of all segments of this path
/// </summary>
public IReadOnlyCollection<DataModelPathSegment> Segments => _segments.ToList().AsReadOnly();
/// <summary>
/// Gets the entity used for persistent storage
/// </summary>
public DataModelPathEntity Entity { get; }
internal Func<object, object>? Accessor { get; private set; }
/// <summary>
/// Gets the current value of the path
/// </summary>
public object? GetValue()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
if (_accessorLambda == null || Target == null)
return null;
// If the accessor has not yet been compiled do it now that it's first required
if (Accessor == null)
Accessor = _accessorLambda.Compile();
return Accessor(Target);
}
/// <summary>
/// Gets the property info of the property this path points to
/// </summary>
/// <returns>If static, the property info. If dynamic, <c>null</c></returns>
public PropertyInfo? GetPropertyInfo()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
return Segments.LastOrDefault()?.GetPropertyInfo();
}
/// <summary>
/// Gets the type of the property this path points to
/// </summary>
/// <returns>If possible, the property type</returns>
public Type? GetPropertyType()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
return Segments.LastOrDefault()?.GetPropertyType();
}
/// <summary>
/// Gets the property description of the property this path points to
/// </summary>
/// <returns>If found, the data model property description</returns>
public DataModelPropertyAttribute? GetPropertyDescription()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
return Segments.LastOrDefault()?.GetPropertyDescription();
}
/// <inheritdoc />
public override string ToString()
{
return string.IsNullOrWhiteSpace(Path) ? "this" : Path;
}
/// <summary>
/// Occurs whenever the path becomes invalid
/// </summary>
public event EventHandler? PathInvalidated;
/// <summary>
/// Occurs whenever the path becomes valid
/// </summary>
public event EventHandler? PathValidated;
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_disposed = true;
DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded;
DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved;
Invalidate();
}
}
/// <summary>
/// Invokes the <see cref="PathInvalidated" /> event
/// </summary>
protected virtual void OnPathValidated()
{
PathValidated?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Invokes the <see cref="PathValidated" /> event
/// </summary>
protected virtual void OnPathInvalidated()
{
PathInvalidated?.Invoke(this, EventArgs.Empty);
}
internal void Invalidate()
{
Target?.RemoveDataModelPath(this);
foreach (DataModelPathSegment dataModelPathSegment in _segments)
dataModelPathSegment.Dispose();
_segments.Clear();
_accessorLambda = null;
Accessor = null;
OnPathInvalidated();
}
internal void Initialize()
{
if (Target == null)
return;
Target.AddDataModelPath(this);
DataModelPathSegment startSegment = new(this, "target", "target");
startSegment.Node = _segments.AddFirst(startSegment);
// On an empty path don't bother processing segments
if (!string.IsNullOrWhiteSpace(Path))
{
string[] segments = Path.Split(".");
for (int index = 0; index < segments.Length; index++)
{
string identifier = segments[index];
LinkedListNode<DataModelPathSegment> node = _segments.AddLast(
new DataModelPathSegment(this, identifier, string.Join('.', segments.Take(index + 1)))
);
node.Value.Node = node;
}
}
ParameterExpression parameter = Expression.Parameter(typeof(object), "t");
Expression? expression = Expression.Convert(parameter, Target.GetType());
Expression? nullCondition = null;
MethodInfo equals = typeof(object).GetMethod("Equals", BindingFlags.Static | BindingFlags.Public)!;
foreach (DataModelPathSegment segment in _segments)
{
BinaryExpression notNull;
try
{
notNull = Expression.NotEqual(expression, Expression.Default(expression.Type));
}
catch (InvalidOperationException)
{
notNull = Expression.NotEqual(
Expression.Call(
null,
equals,
Expression.Convert(expression, typeof(object)),
Expression.Convert(Expression.Default(expression.Type), typeof(object))),
Expression.Constant(true));
}
nullCondition = nullCondition != null ? Expression.AndAlso(nullCondition, notNull) : notNull;
expression = segment.Initialize(parameter, expression, nullCondition);
if (expression == null)
return;
}
if (nullCondition == null)
return;
_accessorLambda = Expression.Lambda<Func<object, object>>(
// Wrap with a null check
Expression.Condition(
nullCondition,
Expression.Convert(expression, typeof(object)),
Expression.Convert(Expression.Default(expression.Type), typeof(object))
),
parameter
);
if (IsValid)
OnPathValidated();
}
private void SubscribeToDataModelStore()
{
DataModelStore.DataModelAdded += DataModelStoreOnDataModelAdded;
DataModelStore.DataModelRemoved += DataModelStoreOnDataModelRemoved;
}
private void DataModelStoreOnDataModelAdded(object? sender, DataModelStoreEvent e)
{
if (e.Registration.DataModel.Module.Id != Entity.DataModelId)
return;
Invalidate();
Target = e.Registration.DataModel;
Initialize();
}
private void DataModelStoreOnDataModelRemoved(object? sender, DataModelStoreEvent e)
{
if (e.Registration.DataModel.Module.Id != Entity.DataModelId)
return;
Invalidate();
Target = null;
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#region Storage
/// <inheritdoc />
public void Load()
{
Path = Entity.Path;
if (Target == null && Entity.DataModelId != null)
Target = DataModelStore.Get(Entity.DataModelId)?.DataModel;
}
/// <inheritdoc />
public void Save()
{
// Do not save an invalid state
if (!IsValid)
return;
Entity.Path = Path;
Entity.DataModelId = DataModelId;
}
#region Equality members
/// <inheritdoc cref="Equals(object)"/>>
protected bool Equals(DataModelPath other)
{
return ReferenceEquals(Target, other.Target) && Path == other.Path;
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((DataModelPath) obj);
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(Target, Path);
}
#endregion
#endregion
Save();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelPath" /> class pointing to the provided path
/// </summary>
/// <param name="target">The target at which this path starts</param>
/// <param name="path">A point-separated path</param>
public DataModelPath(DataModel target, string path)
{
Target = target ?? throw new ArgumentNullException(nameof(target));
Path = path ?? throw new ArgumentNullException(nameof(path));
Entity = new DataModelPathEntity();
_segments = new LinkedList<DataModelPathSegment>();
Save();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelPath" /> class based on an existing path
/// </summary>
/// <param name="dataModelPath">The path to base the new instance on</param>
public DataModelPath(DataModelPath dataModelPath)
{
if (dataModelPath == null)
throw new ArgumentNullException(nameof(dataModelPath));
Target = dataModelPath.Target;
Path = dataModelPath.Path;
Entity = new DataModelPathEntity();
_segments = new LinkedList<DataModelPathSegment>();
Save();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelPath" /> class based on a <see cref="DataModelPathEntity" />
/// </summary>
/// <param name="entity"></param>
public DataModelPath(DataModelPathEntity entity)
{
Path = entity.Path;
Entity = entity;
_segments = new LinkedList<DataModelPathSegment>();
Load();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Gets the data model at which this path starts
/// </summary>
public DataModel? Target { get; private set; }
/// <summary>
/// Gets the data model ID of the <see cref="Target" /> if it is a <see cref="DataModel" />
/// </summary>
public string? DataModelId => Target?.Module.Id;
/// <summary>
/// Gets the point-separated path associated with this <see cref="DataModelPath" />
/// </summary>
public string Path { get; private set; }
/// <summary>
/// Gets a boolean indicating whether all <see cref="Segments" /> are valid
/// </summary>
public bool IsValid => Segments.Any() && Segments.All(p => p.Type != DataModelPathSegmentType.Invalid);
/// <summary>
/// Gets a read-only list of all segments of this path
/// </summary>
public IReadOnlyCollection<DataModelPathSegment> Segments => _segments.ToList().AsReadOnly();
/// <summary>
/// Gets the entity used for persistent storage
/// </summary>
public DataModelPathEntity Entity { get; }
internal Func<object, object>? Accessor { get; private set; }
/// <summary>
/// Gets the current value of the path
/// </summary>
public object? GetValue()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
if (_accessorLambda == null || Target == null)
return null;
// If the accessor has not yet been compiled do it now that it's first required
if (Accessor == null)
Accessor = _accessorLambda.Compile();
return Accessor(Target);
}
/// <summary>
/// Gets the property info of the property this path points to
/// </summary>
/// <returns>If static, the property info. If dynamic, <c>null</c></returns>
public PropertyInfo? GetPropertyInfo()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
return Segments.LastOrDefault()?.GetPropertyInfo();
}
/// <summary>
/// Gets the type of the property this path points to
/// </summary>
/// <returns>If possible, the property type</returns>
public Type? GetPropertyType()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
return Segments.LastOrDefault()?.GetPropertyType();
}
/// <summary>
/// Gets the property description of the property this path points to
/// </summary>
/// <returns>If found, the data model property description</returns>
public DataModelPropertyAttribute? GetPropertyDescription()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
return Segments.LastOrDefault()?.GetPropertyDescription();
}
/// <inheritdoc />
public override string ToString()
{
return string.IsNullOrWhiteSpace(Path) ? "this" : Path;
}
/// <summary>
/// Occurs whenever the path becomes invalid
/// </summary>
public event EventHandler? PathInvalidated;
/// <summary>
/// Occurs whenever the path becomes valid
/// </summary>
public event EventHandler? PathValidated;
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_disposed = true;
DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded;
DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved;
Invalidate();
}
}
/// <summary>
/// Invokes the <see cref="PathInvalidated" /> event
/// </summary>
protected virtual void OnPathValidated()
{
PathValidated?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Invokes the <see cref="PathValidated" /> event
/// </summary>
protected virtual void OnPathInvalidated()
{
PathInvalidated?.Invoke(this, EventArgs.Empty);
}
internal void Invalidate()
{
Target?.RemoveDataModelPath(this);
foreach (DataModelPathSegment dataModelPathSegment in _segments)
dataModelPathSegment.Dispose();
_segments.Clear();
_accessorLambda = null;
Accessor = null;
OnPathInvalidated();
}
internal void Initialize()
{
if (Target == null)
return;
Target.AddDataModelPath(this);
DataModelPathSegment startSegment = new(this, "target", "target");
startSegment.Node = _segments.AddFirst(startSegment);
// On an empty path don't bother processing segments
if (!string.IsNullOrWhiteSpace(Path))
{
string[] segments = Path.Split(".");
for (int index = 0; index < segments.Length; index++)
{
string identifier = segments[index];
LinkedListNode<DataModelPathSegment> node = _segments.AddLast(
new DataModelPathSegment(this, identifier, string.Join('.', segments.Take(index + 1)))
);
node.Value.Node = node;
}
}
ParameterExpression parameter = Expression.Parameter(typeof(object), "t");
Expression? expression = Expression.Convert(parameter, Target.GetType());
Expression? nullCondition = null;
MethodInfo equals = typeof(object).GetMethod("Equals", BindingFlags.Static | BindingFlags.Public)!;
foreach (DataModelPathSegment segment in _segments)
{
BinaryExpression notNull;
try
{
notNull = Expression.NotEqual(expression, Expression.Default(expression.Type));
}
catch (InvalidOperationException)
{
notNull = Expression.NotEqual(
Expression.Call(
null,
equals,
Expression.Convert(expression, typeof(object)),
Expression.Convert(Expression.Default(expression.Type), typeof(object))),
Expression.Constant(true));
}
nullCondition = nullCondition != null ? Expression.AndAlso(nullCondition, notNull) : notNull;
expression = segment.Initialize(parameter, expression, nullCondition);
if (expression == null)
return;
}
if (nullCondition == null)
return;
_accessorLambda = Expression.Lambda<Func<object, object>>(
// Wrap with a null check
Expression.Condition(
nullCondition,
Expression.Convert(expression, typeof(object)),
Expression.Convert(Expression.Default(expression.Type), typeof(object))
),
parameter
);
if (IsValid)
OnPathValidated();
}
private void SubscribeToDataModelStore()
{
DataModelStore.DataModelAdded += DataModelStoreOnDataModelAdded;
DataModelStore.DataModelRemoved += DataModelStoreOnDataModelRemoved;
}
private void DataModelStoreOnDataModelAdded(object? sender, DataModelStoreEvent e)
{
if (e.Registration.DataModel.Module.Id != Entity.DataModelId)
return;
Invalidate();
Target = e.Registration.DataModel;
Initialize();
}
private void DataModelStoreOnDataModelRemoved(object? sender, DataModelStoreEvent e)
{
if (e.Registration.DataModel.Module.Id != Entity.DataModelId)
return;
Invalidate();
Target = null;
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#region Storage
/// <inheritdoc />
public void Load()
{
Path = Entity.Path;
if (Target == null && Entity.DataModelId != null)
Target = DataModelStore.Get(Entity.DataModelId)?.DataModel;
}
/// <inheritdoc />
public void Save()
{
// Do not save an invalid state
if (!IsValid)
return;
Entity.Path = Path;
Entity.DataModelId = DataModelId;
}
#region Equality members
/// <inheritdoc cref="Equals(object)" />
/// >
protected bool Equals(DataModelPath other)
{
return ReferenceEquals(Target, other.Target) && Path == other.Path;
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((DataModelPath) obj);
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(Target, Path);
}
#endregion
#endregion
}

View File

@ -6,307 +6,294 @@ using System.Reflection;
using Artemis.Core.Modules;
using Humanizer;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a segment of a data model path
/// </summary>
public class DataModelPathSegment : IDisposable
{
/// <summary>
/// Represents a segment of a data model path
/// </summary>
public class DataModelPathSegment : IDisposable
private Expression<Func<object, object>>? _accessorLambda;
private DataModel? _dynamicDataModel;
private DataModelPropertyAttribute? _dynamicDataModelAttribute;
private Type? _dynamicDataModelType;
private PropertyInfo? _property;
internal DataModelPathSegment(DataModelPath dataModelPath, string identifier, string path)
{
private Expression<Func<object, object>>? _accessorLambda;
private DataModel? _dynamicDataModel;
private Type? _dynamicDataModelType;
private DataModelPropertyAttribute? _dynamicDataModelAttribute;
private PropertyInfo? _property;
DataModelPath = dataModelPath;
Identifier = identifier;
Path = path;
IsStartSegment = !DataModelPath.Segments.Any();
}
internal DataModelPathSegment(DataModelPath dataModelPath, string identifier, string path)
/// <summary>
/// Gets the data model path this is a segment of
/// </summary>
public DataModelPath DataModelPath { get; }
/// <summary>
/// Gets the identifier that is associated with this segment
/// </summary>
public string Identifier { get; }
/// <summary>
/// Gets the path that leads to this segment
/// </summary>
public string Path { get; }
/// <summary>
/// Gets a boolean indicating whether this is the first segment in the path
/// </summary>
public bool IsStartSegment { get; }
/// <summary>
/// Gets the type of data model this segment of the path points to
/// </summary>
public DataModelPathSegmentType Type { get; private set; }
/// <summary>
/// Gets the previous segment in the path
/// </summary>
public DataModelPathSegment? Previous => Node?.Previous?.Value;
/// <summary>
/// Gets the next segment in the path
/// </summary>
public DataModelPathSegment? Next => Node?.Next?.Value;
internal Func<object, object>? Accessor { get; set; }
internal LinkedListNode<DataModelPathSegment>? Node { get; set; }
/// <summary>
/// Returns the current value of the path up to this segment
/// </summary>
/// <returns></returns>
public object? GetValue()
{
if (Type == DataModelPathSegmentType.Invalid || DataModelPath.Target == null || _accessorLambda == null)
return null;
// If the accessor has not yet been compiled do it now that it's first required
if (Accessor == null)
Accessor = _accessorLambda.Compile();
return Accessor(DataModelPath.Target);
}
/// <inheritdoc />
public override string ToString()
{
return $"[{Type}] {Path}";
}
/// <summary>
/// Gets the property info of the property this segment points to
/// </summary>
/// <returns>If static, the property info. If dynamic, <c>null</c></returns>
public PropertyInfo? GetPropertyInfo()
{
// Dynamic types have no property and therefore no property info
if (Type == DataModelPathSegmentType.Dynamic)
return null;
// The start segment has none either because it is the datamodel
if (IsStartSegment)
return null;
// If this is not the first segment in a path, the property is located on the previous segment
return Previous?.GetPropertyType()?.GetProperties().FirstOrDefault(p => p.Name == Identifier);
}
/// <summary>
/// Gets the property description of the property this segment points to
/// </summary>
/// <returns>If found, the data model property description</returns>
public DataModelPropertyAttribute? GetPropertyDescription()
{
// Dynamic types have a data model description
if (Type == DataModelPathSegmentType.Dynamic)
return _dynamicDataModelAttribute;
if (IsStartSegment && DataModelPath.Target != null)
return DataModelPath.Target.DataModelDescription;
if (IsStartSegment)
return null;
PropertyInfo? propertyInfo = GetPropertyInfo();
if (propertyInfo == null)
return null;
// Static types may have one as an attribute
DataModelPropertyAttribute? attribute = DataModelPath.Target?.GetPropertyDescription(propertyInfo);
if (attribute != null)
{
DataModelPath = dataModelPath;
Identifier = identifier;
Path = path;
IsStartSegment = !DataModelPath.Segments.Any();
if (string.IsNullOrWhiteSpace(attribute.Name))
attribute.Name = propertyInfo.Name.Humanize();
return attribute;
}
/// <summary>
/// Gets the data model path this is a segment of
/// </summary>
public DataModelPath DataModelPath { get; }
return new DataModelPropertyAttribute {Name = propertyInfo.Name.Humanize(), ResetsDepth = false};
}
/// <summary>
/// Gets the identifier that is associated with this segment
/// </summary>
public string Identifier { get; }
/// <summary>
/// Gets the type of the property this path points to
/// </summary>
/// <returns>If possible, the property type</returns>
public Type? GetPropertyType()
{
// The start segment type is always the target type
if (IsStartSegment)
return DataModelPath.Target?.GetType();
/// <summary>
/// Gets the path that leads to this segment
/// </summary>
public string Path { get; }
/// <summary>
/// Gets a boolean indicating whether this is the first segment in the path
/// </summary>
public bool IsStartSegment { get; }
/// <summary>
/// Gets the type of data model this segment of the path points to
/// </summary>
public DataModelPathSegmentType Type { get; private set; }
/// <summary>
/// Gets the previous segment in the path
/// </summary>
public DataModelPathSegment? Previous => Node?.Previous?.Value;
/// <summary>
/// Gets the next segment in the path
/// </summary>
public DataModelPathSegment? Next => Node?.Next?.Value;
internal Func<object, object>? Accessor { get; set; }
internal LinkedListNode<DataModelPathSegment>? Node { get; set; }
/// <summary>
/// Returns the current value of the path up to this segment
/// </summary>
/// <returns></returns>
public object? GetValue()
// Prefer basing the type on the property info
PropertyInfo? propertyInfo = GetPropertyInfo();
Type? type = propertyInfo?.PropertyType;
// Property info is not available on dynamic paths though, so fall back on the current value
if (propertyInfo == null)
{
if (Type == DataModelPathSegmentType.Invalid || DataModelPath.Target == null || _accessorLambda == null)
return null;
// If the accessor has not yet been compiled do it now that it's first required
if (Accessor == null)
Accessor = _accessorLambda.Compile();
return Accessor(DataModelPath.Target);
object? currentValue = GetValue();
if (currentValue != null)
type = currentValue.GetType();
}
/// <inheritdoc />
public override string ToString()
return type;
}
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
return $"[{Type}] {Path}";
}
/// <summary>
/// Gets the property info of the property this segment points to
/// </summary>
/// <returns>If static, the property info. If dynamic, <c>null</c></returns>
public PropertyInfo? GetPropertyInfo()
{
// Dynamic types have no property and therefore no property info
if (Type == DataModelPathSegmentType.Dynamic)
return null;
// The start segment has none either because it is the datamodel
if (IsStartSegment)
return null;
// If this is not the first segment in a path, the property is located on the previous segment
return Previous?.GetPropertyType()?.GetProperties().FirstOrDefault(p => p.Name == Identifier);
}
/// <summary>
/// Gets the property description of the property this segment points to
/// </summary>
/// <returns>If found, the data model property description</returns>
public DataModelPropertyAttribute? GetPropertyDescription()
{
// Dynamic types have a data model description
if (Type == DataModelPathSegmentType.Dynamic)
return _dynamicDataModelAttribute;
if (IsStartSegment && DataModelPath.Target != null)
return DataModelPath.Target.DataModelDescription;
if (IsStartSegment)
return null;
PropertyInfo? propertyInfo = GetPropertyInfo();
if (propertyInfo == null)
return null;
// Static types may have one as an attribute
DataModelPropertyAttribute? attribute = DataModelPath.Target?.GetPropertyDescription(propertyInfo);
if (attribute != null)
if (_dynamicDataModel != null)
{
if (string.IsNullOrWhiteSpace(attribute.Name))
attribute.Name = propertyInfo.Name.Humanize();
return attribute;
_dynamicDataModel.DynamicChildAdded -= DynamicChildOnDynamicChildAdded;
_dynamicDataModel.DynamicChildRemoved -= DynamicChildOnDynamicChildRemoved;
}
return new DataModelPropertyAttribute {Name = propertyInfo.Name.Humanize(), ResetsDepth = false};
Type = DataModelPathSegmentType.Invalid;
_accessorLambda = null;
Accessor = null;
}
}
/// <summary>
/// Gets the type of the property this path points to
/// </summary>
/// <returns>If possible, the property type</returns>
public Type? GetPropertyType()
internal Expression? Initialize(ParameterExpression parameter, Expression expression, Expression nullCondition)
{
if (IsStartSegment)
{
// The start segment type is always the target type
if (IsStartSegment)
return DataModelPath.Target?.GetType();
// Prefer basing the type on the property info
PropertyInfo? propertyInfo = GetPropertyInfo();
Type? type = propertyInfo?.PropertyType;
// Property info is not available on dynamic paths though, so fall back on the current value
if (propertyInfo == null)
{
object? currentValue = GetValue();
if (currentValue != null)
type = currentValue.GetType();
}
return type;
}
internal Expression? Initialize(ParameterExpression parameter, Expression expression, Expression nullCondition)
{
if (IsStartSegment)
{
Type = DataModelPathSegmentType.Static;
return CreateExpression(parameter, expression, nullCondition);
}
Type? previousType = Previous?.GetPropertyType();
if (previousType == null)
{
Type = DataModelPathSegmentType.Invalid;
return CreateExpression(parameter, expression, nullCondition);
}
// Prefer static since that's faster
DetermineStaticType(previousType);
// If no static type could be found, check if this is a data model and if so, look for a dynamic type
if (Type == DataModelPathSegmentType.Invalid && typeof(DataModel).IsAssignableFrom(previousType))
{
_dynamicDataModel = Previous?.GetValue() as DataModel;
// Cannot determine a dynamic type on a null data model, leave the segment invalid
if (_dynamicDataModel == null)
return CreateExpression(parameter, expression, nullCondition);
// If a dynamic data model is found the use that
bool hasDynamicChild = _dynamicDataModel.DynamicChildren.TryGetValue(Identifier, out DynamicChild? dynamicChild);
if (hasDynamicChild && dynamicChild?.BaseValue != null)
DetermineDynamicType(dynamicChild.BaseValue, dynamicChild.Attribute);
_dynamicDataModel.DynamicChildAdded += DynamicChildOnDynamicChildAdded;
_dynamicDataModel.DynamicChildRemoved += DynamicChildOnDynamicChildRemoved;
}
Type = DataModelPathSegmentType.Static;
return CreateExpression(parameter, expression, nullCondition);
}
private Expression? CreateExpression(ParameterExpression parameter, Expression expression, Expression nullCondition)
Type? previousType = Previous?.GetPropertyType();
if (previousType == null)
{
if (Type == DataModelPathSegmentType.Invalid)
{
_accessorLambda = null;
Accessor = null;
return null;
}
Type = DataModelPathSegmentType.Invalid;
return CreateExpression(parameter, expression, nullCondition);
}
Expression accessorExpression;
// A start segment just accesses the target
if (IsStartSegment)
accessorExpression = expression;
// A static segment just needs to access the property or filed
else if (Type == DataModelPathSegmentType.Static)
{
accessorExpression = _property != null
? Expression.Property(expression, _property)
: Expression.PropertyOrField(expression, Identifier);
}
// A dynamic segment calls the generic method DataModel.DynamicChild<T> and provides the identifier as an argument
else
{
accessorExpression = Expression.Call(
expression,
nameof(DataModel.GetDynamicChildValue),
_dynamicDataModelType != null ? new[] {_dynamicDataModelType} : null,
Expression.Constant(Identifier)
);
}
// Prefer static since that's faster
DetermineStaticType(previousType);
_accessorLambda = Expression.Lambda<Func<object, object>>(
// Wrap with a null check
Expression.Condition(
nullCondition,
Expression.Convert(accessorExpression, typeof(object)),
Expression.Convert(Expression.Default(accessorExpression.Type), typeof(object))
),
parameter
);
// If no static type could be found, check if this is a data model and if so, look for a dynamic type
if (Type == DataModelPathSegmentType.Invalid && typeof(DataModel).IsAssignableFrom(previousType))
{
_dynamicDataModel = Previous?.GetValue() as DataModel;
// Cannot determine a dynamic type on a null data model, leave the segment invalid
if (_dynamicDataModel == null)
return CreateExpression(parameter, expression, nullCondition);
// If a dynamic data model is found the use that
bool hasDynamicChild = _dynamicDataModel.DynamicChildren.TryGetValue(Identifier, out DynamicChild? dynamicChild);
if (hasDynamicChild && dynamicChild?.BaseValue != null)
DetermineDynamicType(dynamicChild.BaseValue, dynamicChild.Attribute);
_dynamicDataModel.DynamicChildAdded += DynamicChildOnDynamicChildAdded;
_dynamicDataModel.DynamicChildRemoved += DynamicChildOnDynamicChildRemoved;
}
return CreateExpression(parameter, expression, nullCondition);
}
private Expression? CreateExpression(ParameterExpression parameter, Expression expression, Expression nullCondition)
{
if (Type == DataModelPathSegmentType.Invalid)
{
_accessorLambda = null;
Accessor = null;
return accessorExpression;
return null;
}
private void DetermineDynamicType(object dynamicDataModel, DataModelPropertyAttribute attribute)
Expression accessorExpression;
// A start segment just accesses the target
if (IsStartSegment)
accessorExpression = expression;
// A static segment just needs to access the property or filed
else if (Type == DataModelPathSegmentType.Static)
accessorExpression = _property != null
? Expression.Property(expression, _property)
: Expression.PropertyOrField(expression, Identifier);
// A dynamic segment calls the generic method DataModel.DynamicChild<T> and provides the identifier as an argument
else
accessorExpression = Expression.Call(
expression,
nameof(DataModel.GetDynamicChildValue),
_dynamicDataModelType != null ? new[] {_dynamicDataModelType} : null,
Expression.Constant(Identifier)
);
_accessorLambda = Expression.Lambda<Func<object, object>>(
// Wrap with a null check
Expression.Condition(
nullCondition,
Expression.Convert(accessorExpression, typeof(object)),
Expression.Convert(Expression.Default(accessorExpression.Type), typeof(object))
),
parameter
);
Accessor = null;
return accessorExpression;
}
private void DetermineDynamicType(object dynamicDataModel, DataModelPropertyAttribute attribute)
{
Type = DataModelPathSegmentType.Dynamic;
_dynamicDataModelType = dynamicDataModel.GetType();
_dynamicDataModelAttribute = attribute;
}
private void DetermineStaticType(Type previousType)
{
// Situations in which AmbiguousMatchException occurs ...
//
// ...derived type declares a property that hides an inherited property with the same name, by using the new modifier
_property = previousType.GetProperties(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(p => p.Name == Identifier);
Type = _property == null ? DataModelPathSegmentType.Invalid : DataModelPathSegmentType.Static;
}
private void DynamicChildOnDynamicChildAdded(object? sender, DynamicDataModelChildEventArgs e)
{
if (e.Key == Identifier)
{
Type = DataModelPathSegmentType.Dynamic;
_dynamicDataModelType = dynamicDataModel.GetType();
_dynamicDataModelAttribute = attribute;
DataModelPath.Invalidate();
DataModelPath.Initialize();
}
}
private void DetermineStaticType(Type previousType)
{
// Situations in which AmbiguousMatchException occurs ...
//
// ...derived type declares a property that hides an inherited property with the same name, by using the new modifier
_property = previousType.GetProperties(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(p => p.Name == Identifier);
Type = _property == null ? DataModelPathSegmentType.Invalid : DataModelPathSegmentType.Static;
}
private void DynamicChildOnDynamicChildRemoved(object? sender, DynamicDataModelChildEventArgs e)
{
if (e.DynamicChild.BaseValue == _dynamicDataModel)
DataModelPath.Invalidate();
}
#region IDisposable
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (_dynamicDataModel != null)
{
_dynamicDataModel.DynamicChildAdded -= DynamicChildOnDynamicChildAdded;
_dynamicDataModel.DynamicChildRemoved -= DynamicChildOnDynamicChildRemoved;
}
Type = DataModelPathSegmentType.Invalid;
_accessorLambda = null;
Accessor = null;
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
#region Event handlers
private void DynamicChildOnDynamicChildAdded(object? sender, DynamicDataModelChildEventArgs e)
{
if (e.Key == Identifier)
{
DataModelPath.Invalidate();
DataModelPath.Initialize();
}
}
private void DynamicChildOnDynamicChildRemoved(object? sender, DynamicDataModelChildEventArgs e)
{
if (e.DynamicChild.BaseValue == _dynamicDataModel)
DataModelPath.Invalidate();
}
#endregion
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}

View File

@ -1,23 +1,22 @@
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a type of data model path
/// </summary>
public enum DataModelPathSegmentType
{
/// <summary>
/// Represents a type of data model path
/// Represents an invalid data model type that points to a missing data model
/// </summary>
public enum DataModelPathSegmentType
{
/// <summary>
/// Represents an invalid data model type that points to a missing data model
/// </summary>
Invalid,
Invalid,
/// <summary>
/// Represents a static data model type that points to a data model defined in code
/// </summary>
Static,
/// <summary>
/// Represents a static data model type that points to a data model defined in code
/// </summary>
Static,
/// <summary>
/// Represents a static data model type that points to a data model defined at runtime
/// </summary>
Dynamic
}
/// <summary>
/// Represents a static data model type that points to a data model defined at runtime
/// </summary>
Dynamic
}

View File

@ -1,69 +1,68 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents an event that is part of a data model
/// </summary>
public interface IDataModelEvent
{
/// <summary>
/// Represents an event that is part of a data model
/// Gets the last time the event was triggered
/// </summary>
public interface IDataModelEvent
{
/// <summary>
/// Gets the last time the event was triggered
/// </summary>
DateTime LastTrigger { get; }
DateTime LastTrigger { get; }
/// <summary>
/// Gets the time that has passed since the last trigger
/// </summary>
TimeSpan TimeSinceLastTrigger { get; }
/// <summary>
/// Gets the amount of times the event was triggered
/// </summary>
int TriggerCount { get; }
/// <summary>
/// Gets the time that has passed since the last trigger
/// </summary>
TimeSpan TimeSinceLastTrigger { get; }
/// <summary>
/// Gets the type of arguments this event contains
/// </summary>
Type ArgumentsType { get; }
/// <summary>
/// Gets the amount of times the event was triggered
/// </summary>
int TriggerCount { get; }
/// <summary>
/// Gets the past participle for this event shown in the UI
/// </summary>
string TriggerPastParticiple { get; }
/// <summary>
/// Gets the type of arguments this event contains
/// </summary>
Type ArgumentsType { get; }
/// <summary>
/// Gets or sets a boolean indicating whether the last 20 events should be tracked
/// <para>Note: setting this to <see langword="false" /> will clear the current history</para>
/// </summary>
bool TrackHistory { get; set; }
/// <summary>
/// Gets the past participle for this event shown in the UI
/// </summary>
string TriggerPastParticiple { get; }
/// <summary>
/// Gets the event arguments of the last time the event was triggered by its base type
/// </summary>
public DataModelEventArgs? LastEventArgumentsUntyped { get; }
/// <summary>
/// Gets or sets a boolean indicating whether the last 20 events should be tracked
/// <para>Note: setting this to <see langword="false" /> will clear the current history</para>
/// </summary>
bool TrackHistory { get; set; }
/// <summary>
/// Gets a list of the last 20 event arguments by their base type.
/// <para>Always empty if <see cref="TrackHistory" /> is <see langword="false" /></para>
/// </summary>
public List<DataModelEventArgs> EventArgumentsHistoryUntyped { get; }
/// <summary>
/// Gets the event arguments of the last time the event was triggered by its base type
/// </summary>
public DataModelEventArgs? LastEventArgumentsUntyped { get; }
/// <summary>
/// Fires when the event is triggered
/// </summary>
event EventHandler EventTriggered;
/// <summary>
/// Gets a list of the last 20 event arguments by their base type.
/// <para>Always empty if <see cref="TrackHistory" /> is <see langword="false" /></para>
/// </summary>
public List<DataModelEventArgs> EventArgumentsHistoryUntyped { get; }
/// <summary>
/// Resets the trigger count and history of this data model event
/// </summary>
void Reset();
/// <summary>
/// Fires when the event is triggered
/// </summary>
event EventHandler EventTriggered;
/// <summary>
/// Updates the event, not required for standard events but included in case your custom event needs to update every
/// tick
/// </summary>
void Update();
}
/// <summary>
/// Resets the trigger count and history of this data model event
/// </summary>
void Reset();
/// <summary>
/// Updates the event, not required for standard events but included in case your custom event needs to update every
/// tick
/// </summary>
void Update();
}

View File

@ -1,360 +1,359 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Artemis.Core.LayerEffects;
using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Entities.Profile.Abstract;
using SkiaSharp;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a folder in a <see cref="Profile" />
/// </summary>
public sealed class Folder : RenderProfileElement
{
private bool _isExpanded;
/// <summary>
/// Represents a folder in a <see cref="Profile" />
/// Creates a new instance of the <see cref="Folder" /> class and adds itself to the child collection of the provided
/// <paramref name="parent" />
/// </summary>
public sealed class Folder : RenderProfileElement
/// <param name="parent">The parent of the folder</param>
/// <param name="name">The name of the folder</param>
public Folder(ProfileElement parent, string name) : base(parent, parent.Profile)
{
private bool _isExpanded;
FolderEntity = new FolderEntity();
EntityId = Guid.NewGuid();
/// <summary>
/// Creates a new instance of the <see cref="Folder" /> class and adds itself to the child collection of the provided
/// <paramref name="parent" />
/// </summary>
/// <param name="parent">The parent of the folder</param>
/// <param name="name">The name of the folder</param>
public Folder(ProfileElement parent, string name) : base(parent, parent.Profile)
Profile = Parent.Profile;
Name = name;
}
/// <summary>
/// Creates a new instance of the <see cref="Folder" /> class based on the provided folder entity
/// </summary>
/// <param name="profile">The profile the folder belongs to</param>
/// <param name="parent">The parent of the folder</param>
/// <param name="folderEntity">The entity of the folder</param>
public Folder(Profile profile, ProfileElement parent, FolderEntity folderEntity) : base(parent, parent.Profile)
{
FolderEntity = folderEntity;
EntityId = folderEntity.Id;
Profile = profile;
Name = folderEntity.Name;
IsExpanded = folderEntity.IsExpanded;
Suspended = folderEntity.Suspended;
Order = folderEntity.Order;
Load();
}
/// <summary>
/// Gets a boolean indicating whether this folder is at the root of the profile tree
/// </summary>
public bool IsRootFolder => Parent == Profile;
/// <summary>
/// Gets or sets a boolean indicating whether this folder is expanded
/// </summary>
public bool IsExpanded
{
get => _isExpanded;
set => SetAndNotify(ref _isExpanded, value);
}
/// <summary>
/// Gets the folder entity this folder uses for persistent storage
/// </summary>
public FolderEntity FolderEntity { get; internal set; }
/// <inheritdoc />
public override bool ShouldBeEnabled => !Suspended && DisplayConditionMet && !Timeline.IsFinished;
internal override RenderElementEntity RenderElementEntity => FolderEntity;
/// <inheritdoc />
public override List<ILayerProperty> GetAllLayerProperties()
{
List<ILayerProperty> result = new();
foreach (BaseLayerEffect layerEffect in LayerEffects)
{
FolderEntity = new FolderEntity();
EntityId = Guid.NewGuid();
Profile = Parent.Profile;
Name = name;
if (layerEffect.BaseProperties != null)
result.AddRange(layerEffect.BaseProperties.GetAllLayerProperties());
}
/// <summary>
/// Creates a new instance of the <see cref="Folder" /> class based on the provided folder entity
/// </summary>
/// <param name="profile">The profile the folder belongs to</param>
/// <param name="parent">The parent of the folder</param>
/// <param name="folderEntity">The entity of the folder</param>
public Folder(Profile profile, ProfileElement parent, FolderEntity folderEntity) : base(parent, parent.Profile)
return result;
}
/// <inheritdoc />
public override void Update(double deltaTime)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
if (Timeline.IsOverridden)
{
FolderEntity = folderEntity;
EntityId = folderEntity.Id;
Profile = profile;
Name = folderEntity.Name;
IsExpanded = folderEntity.IsExpanded;
Suspended = folderEntity.Suspended;
Order = folderEntity.Order;
Load();
Timeline.ClearOverride();
return;
}
/// <summary>
/// Gets a boolean indicating whether this folder is at the root of the profile tree
/// </summary>
public bool IsRootFolder => Parent == Profile;
/// <summary>
/// Gets or sets a boolean indicating whether this folder is expanded
/// </summary>
public bool IsExpanded
{
get => _isExpanded;
set => SetAndNotify(ref _isExpanded, value);
}
/// <summary>
/// Gets the folder entity this folder uses for persistent storage
/// </summary>
public FolderEntity FolderEntity { get; internal set; }
/// <inheritdoc />
public override bool ShouldBeEnabled => !Suspended && DisplayConditionMet && !Timeline.IsFinished;
internal override RenderElementEntity RenderElementEntity => FolderEntity;
/// <inheritdoc />
public override List<ILayerProperty> GetAllLayerProperties()
{
List<ILayerProperty> result = new();
foreach (BaseLayerEffect layerEffect in LayerEffects)
{
if (layerEffect.BaseProperties != null)
result.AddRange(layerEffect.BaseProperties.GetAllLayerProperties());
}
return result;
}
/// <inheritdoc />
public override void Update(double deltaTime)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
if (Timeline.IsOverridden)
{
Timeline.ClearOverride();
return;
}
UpdateDisplayCondition();
UpdateTimeline(deltaTime);
if (ShouldBeEnabled)
Enable();
else if (Timeline.IsFinished)
Disable();
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
baseLayerEffect.InternalUpdate(Timeline);
foreach (ProfileElement child in Children)
child.Update(deltaTime);
}
/// <inheritdoc />
public override void Reset()
{
UpdateDisplayCondition();
if (DisplayConditionMet)
Timeline.JumpToStart();
else
Timeline.JumpToEnd();
foreach (ProfileElement child in Children)
child.Reset();
}
/// <inheritdoc />
public override void AddChild(ProfileElement child, int? order = null)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
base.AddChild(child, order);
CalculateRenderProperties();
}
/// <inheritdoc />
public override void RemoveChild(ProfileElement child)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
base.RemoveChild(child);
CalculateRenderProperties();
}
/// <summary>
/// Creates a deep copy of the folder
/// </summary>
/// <returns>The newly created copy</returns>
public Folder CreateCopy()
{
if (Parent == null)
throw new ArtemisCoreException("Cannot create a copy of a folder without a parent");
FolderEntity entityCopy = CoreJson.DeserializeObject<FolderEntity>(CoreJson.SerializeObject(FolderEntity, true), true)!;
entityCopy.Id = Guid.NewGuid();
entityCopy.Name += " - Copy";
// TODO Children
return new Folder(Profile, Parent, entityCopy);
}
/// <inheritdoc />
public override string ToString()
{
return $"[Folder] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}";
}
#region Rendering
/// <inheritdoc />
public override void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
// Ensure the folder is ready
if (!Enabled || Path == null)
return;
// No point rendering if all children are disabled
if (!Children.Any(c => c is RenderProfileElement {Enabled: true}))
return;
// If the editor focus is on this folder, discard further focus for children to effectively focus the entire folder and all descendants
if (editorFocus == this)
editorFocus = null;
SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low};
try
{
SKRectI rendererBounds = SKRectI.Create(0, 0, Bounds.Width, Bounds.Height);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
{
if (!baseLayerEffect.Suspended)
baseLayerEffect.InternalPreProcess(canvas, rendererBounds, layerPaint);
}
// No point rendering if the alpha was set to zero by one of the effects
if (layerPaint.Color.Alpha == 0)
return;
canvas.SaveLayer(layerPaint);
canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y);
// Iterate the children in reverse because the first layer must be rendered last to end up on top
for (int index = Children.Count - 1; index > -1; index--)
Children[index].Render(canvas, new SKPointI(Bounds.Left, Bounds.Top), editorFocus);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
{
if (!baseLayerEffect.Suspended)
baseLayerEffect.InternalPostProcess(canvas, rendererBounds, layerPaint);
}
}
finally
{
canvas.Restore();
layerPaint.DisposeSelfAndProperties();
}
Timeline.ClearDelta();
}
#endregion
/// <inheritdoc />
public override void Enable()
{
// No checks here, effects will do their own checks to ensure they never enable twice
// Also not enabling children, they'll enable themselves during their own Update
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
baseLayerEffect.InternalEnable();
Enabled = true;
}
/// <inheritdoc />
public override void Disable()
{
// No checks here, effects will do their own checks to ensure they never disable twice
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
baseLayerEffect.InternalDisable();
// Disabling children since their Update won't get called with their parent disabled
foreach (ProfileElement profileElement in Children)
{
if (profileElement is RenderProfileElement renderProfileElement)
renderProfileElement.Disable();
}
Enabled = false;
}
/// <inheritdoc />
public override void OverrideTimelineAndApply(TimeSpan position)
{
DisplayCondition.OverrideTimeline(position);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
baseLayerEffect.InternalUpdate(Timeline); ;
}
/// <summary>
/// Occurs when a property affecting the rendering properties of this folder has been updated
/// </summary>
public event EventHandler? RenderPropertiesUpdated;
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
Disposed = true;
UpdateDisplayCondition();
UpdateTimeline(deltaTime);
if (ShouldBeEnabled)
Enable();
else if (Timeline.IsFinished)
Disable();
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
base.Dispose(disposing);
}
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
baseLayerEffect.InternalUpdate(Timeline);
internal void CalculateRenderProperties()
foreach (ProfileElement child in Children)
child.Update(deltaTime);
}
/// <inheritdoc />
public override void Reset()
{
UpdateDisplayCondition();
if (DisplayConditionMet)
Timeline.JumpToStart();
else
Timeline.JumpToEnd();
foreach (ProfileElement child in Children)
child.Reset();
}
/// <inheritdoc />
public override void AddChild(ProfileElement child, int? order = null)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
base.AddChild(child, order);
CalculateRenderProperties();
}
/// <inheritdoc />
public override void RemoveChild(ProfileElement child)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
base.RemoveChild(child);
CalculateRenderProperties();
}
/// <summary>
/// Creates a deep copy of the folder
/// </summary>
/// <returns>The newly created copy</returns>
public Folder CreateCopy()
{
if (Parent == null)
throw new ArtemisCoreException("Cannot create a copy of a folder without a parent");
FolderEntity entityCopy = CoreJson.DeserializeObject<FolderEntity>(CoreJson.SerializeObject(FolderEntity, true), true)!;
entityCopy.Id = Guid.NewGuid();
entityCopy.Name += " - Copy";
// TODO Children
return new Folder(Profile, Parent, entityCopy);
}
/// <inheritdoc />
public override string ToString()
{
return $"[Folder] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}";
}
#region Rendering
/// <inheritdoc />
public override void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
// Ensure the folder is ready
if (!Enabled || Path == null)
return;
// No point rendering if all children are disabled
if (!Children.Any(c => c is RenderProfileElement {Enabled: true}))
return;
// If the editor focus is on this folder, discard further focus for children to effectively focus the entire folder and all descendants
if (editorFocus == this)
editorFocus = null;
SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low};
try
{
if (Disposed)
throw new ObjectDisposedException("Folder");
SKPath path = new() {FillType = SKPathFillType.Winding};
foreach (ProfileElement child in Children)
SKRectI rendererBounds = SKRectI.Create(0, 0, Bounds.Width, Bounds.Height);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
{
if (child is RenderProfileElement effectChild && effectChild.Path != null)
path.AddPath(effectChild.Path);
if (!baseLayerEffect.Suspended)
baseLayerEffect.InternalPreProcess(canvas, rendererBounds, layerPaint);
}
Path = path;
// No point rendering if the alpha was set to zero by one of the effects
if (layerPaint.Color.Alpha == 0)
return;
// Folder render properties are based on child paths and thus require an update
if (Parent is Folder folder)
folder.CalculateRenderProperties();
canvas.SaveLayer(layerPaint);
canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y);
OnRenderPropertiesUpdated();
// Iterate the children in reverse because the first layer must be rendered last to end up on top
for (int index = Children.Count - 1; index > -1; index--)
Children[index].Render(canvas, new SKPointI(Bounds.Left, Bounds.Top), editorFocus);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
{
if (!baseLayerEffect.Suspended)
baseLayerEffect.InternalPostProcess(canvas, rendererBounds, layerPaint);
}
}
internal override void Load()
finally
{
Reset();
// Load child folders
foreach (FolderEntity childFolder in Profile.ProfileEntity.Folders.Where(f => f.ParentId == EntityId))
ChildrenList.Add(new Folder(Profile, this, childFolder));
// Load child layers
foreach (LayerEntity childLayer in Profile.ProfileEntity.Layers.Where(f => f.ParentId == EntityId))
ChildrenList.Add(new Layer(Profile, this, childLayer));
// Ensure order integrity, should be unnecessary but no one is perfect specially me
ChildrenList.Sort((a, b) => a.Order.CompareTo(b.Order));
for (int index = 0; index < ChildrenList.Count; index++)
ChildrenList[index].Order = index + 1;
LoadRenderElement();
canvas.Restore();
layerPaint.DisposeSelfAndProperties();
}
internal override void Save()
Timeline.ClearDelta();
}
#endregion
/// <inheritdoc />
public override void Enable()
{
// No checks here, effects will do their own checks to ensure they never enable twice
// Also not enabling children, they'll enable themselves during their own Update
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
baseLayerEffect.InternalEnable();
Enabled = true;
}
/// <inheritdoc />
public override void Disable()
{
// No checks here, effects will do their own checks to ensure they never disable twice
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
baseLayerEffect.InternalDisable();
// Disabling children since their Update won't get called with their parent disabled
foreach (ProfileElement profileElement in Children)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
FolderEntity.Id = EntityId;
FolderEntity.ParentId = Parent?.EntityId ?? new Guid();
FolderEntity.Order = Order;
FolderEntity.Name = Name;
FolderEntity.IsExpanded = IsExpanded;
FolderEntity.Suspended = Suspended;
FolderEntity.ProfileId = Profile.EntityId;
SaveRenderElement();
if (profileElement is RenderProfileElement renderProfileElement)
renderProfileElement.Disable();
}
private void OnRenderPropertiesUpdated()
Enabled = false;
}
/// <inheritdoc />
public override void OverrideTimelineAndApply(TimeSpan position)
{
DisplayCondition.OverrideTimeline(position);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
baseLayerEffect.InternalUpdate(Timeline);
;
}
/// <summary>
/// Occurs when a property affecting the rendering properties of this folder has been updated
/// </summary>
public event EventHandler? RenderPropertiesUpdated;
#region Overrides of BreakableModel
/// <inheritdoc />
public override IEnumerable<IBreakableModel> GetBrokenHierarchy()
{
return LayerEffects.Where(e => e.BrokenState != null);
}
#endregion
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
Disposed = true;
Disable();
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
base.Dispose(disposing);
}
internal void CalculateRenderProperties()
{
if (Disposed)
throw new ObjectDisposedException("Folder");
SKPath path = new() {FillType = SKPathFillType.Winding};
foreach (ProfileElement child in Children)
{
RenderPropertiesUpdated?.Invoke(this, EventArgs.Empty);
if (child is RenderProfileElement effectChild && effectChild.Path != null)
path.AddPath(effectChild.Path);
}
#region Overrides of BreakableModel
Path = path;
/// <inheritdoc />
public override IEnumerable<IBreakableModel> GetBrokenHierarchy()
{
return LayerEffects.Where(e => e.BrokenState != null);
}
// Folder render properties are based on child paths and thus require an update
if (Parent is Folder folder)
folder.CalculateRenderProperties();
#endregion
OnRenderPropertiesUpdated();
}
internal override void Load()
{
Reset();
// Load child folders
foreach (FolderEntity childFolder in Profile.ProfileEntity.Folders.Where(f => f.ParentId == EntityId))
ChildrenList.Add(new Folder(Profile, this, childFolder));
// Load child layers
foreach (LayerEntity childLayer in Profile.ProfileEntity.Layers.Where(f => f.ParentId == EntityId))
ChildrenList.Add(new Layer(Profile, this, childLayer));
// Ensure order integrity, should be unnecessary but no one is perfect specially me
ChildrenList.Sort((a, b) => a.Order.CompareTo(b.Order));
for (int index = 0; index < ChildrenList.Count; index++)
ChildrenList[index].Order = index + 1;
LoadRenderElement();
}
internal override void Save()
{
if (Disposed)
throw new ObjectDisposedException("Folder");
FolderEntity.Id = EntityId;
FolderEntity.ParentId = Parent?.EntityId ?? new Guid();
FolderEntity.Order = Order;
FolderEntity.Name = Name;
FolderEntity.IsExpanded = IsExpanded;
FolderEntity.Suspended = Suspended;
FolderEntity.ProfileId = Profile.EntityId;
SaveRenderElement();
}
private void OnRenderPropertiesUpdated()
{
RenderPropertiesUpdated?.Invoke(this, EventArgs.Empty);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -6,194 +6,195 @@ using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Entities.Profile.AdaptionHints;
using RGB.NET.Core;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents an adapter that adapts a layer to a certain set of devices using <see cref="IAdaptionHint" />s
/// </summary>
public class LayerAdapter : IStorageModel
{
/// <summary>
/// Represents an adapter that adapts a layer to a certain set of devices using <see cref="IAdaptionHint" />s
/// </summary>
public class LayerAdapter : IStorageModel
private readonly List<IAdaptionHint> _adaptionHints;
internal LayerAdapter(Layer layer)
{
private readonly List<IAdaptionHint> _adaptionHints;
internal LayerAdapter(Layer layer)
{
_adaptionHints = new List<IAdaptionHint>();
Layer = layer;
AdaptionHints = new ReadOnlyCollection<IAdaptionHint>(_adaptionHints);
}
/// <summary>
/// Gets the layer this adapter can adapt
/// </summary>
public Layer Layer { get; }
/// <summary>
/// Gets or sets a list containing the adaption hints used by this adapter
/// </summary>
public ReadOnlyCollection<IAdaptionHint> AdaptionHints { get; set; }
/// <summary>
/// Modifies the layer, adapting it to the provided <paramref name="devices" />
/// </summary>
/// <param name="devices">The devices to adapt the layer to</param>
public void Adapt(List<ArtemisDevice> devices)
{
// Use adaption hints if provided
if (AdaptionHints.Any())
{
foreach (IAdaptionHint adaptionHint in AdaptionHints)
adaptionHint.Apply(Layer, devices);
}
// If there are no hints, try to find matching LEDs anyway
else
{
List<ArtemisLed> availableLeds = devices.SelectMany(d => d.Leds).ToList();
List<ArtemisLed> usedLeds = new();
foreach (LedEntity ledEntity in Layer.LayerEntity.Leds)
{
// TODO: If this is a keyboard LED and the layouts don't match, convert it before looking for it on the devices
LedId ledId = Enum.Parse<LedId>(ledEntity.LedName);
ArtemisLed? led = availableLeds.FirstOrDefault(l => l.RgbLed.Id == ledId);
if (led != null)
{
availableLeds.Remove(led);
usedLeds.Add(led);
}
}
Layer.AddLeds(usedLeds);
}
}
/// <summary>
/// Automatically determine hints for this layer
/// </summary>
public List<IAdaptionHint> DetermineHints(IEnumerable<ArtemisDevice> devices)
{
List<IAdaptionHint> newHints = new();
if (devices.All(DoesLayerCoverDevice))
{
DeviceAdaptionHint hint = new() {DeviceType = RGBDeviceType.All};
Add(hint);
newHints.Add(hint);
}
else
{
// Any fully covered device will add a device adaption hint for that type
foreach (IGrouping<ArtemisDevice, ArtemisLed> deviceLeds in Layer.Leds.GroupBy(l => l.Device))
{
ArtemisDevice device = deviceLeds.Key;
// If there is already an adaption hint for this type, don't add another
if (AdaptionHints.Any(h => h is DeviceAdaptionHint d && d.DeviceType == device.DeviceType))
continue;
if (DoesLayerCoverDevice(device))
{
DeviceAdaptionHint hint = new() {DeviceType = device.DeviceType};
Add(hint);
newHints.Add(hint);
}
}
// Any fully covered category will add a category adaption hint for its category
foreach (DeviceCategory deviceCategory in Enum.GetValues<DeviceCategory>())
{
if (AdaptionHints.Any(h => h is CategoryAdaptionHint c && c.Category == deviceCategory))
continue;
List<ArtemisDevice> categoryDevices = devices.Where(d => d.Categories.Contains(deviceCategory)).ToList();
if (categoryDevices.Any() && categoryDevices.All(DoesLayerCoverDevice))
{
CategoryAdaptionHint hint = new() {Category = deviceCategory};
Add(hint);
newHints.Add(hint);
}
}
}
return newHints;
}
private bool DoesLayerCoverDevice(ArtemisDevice device)
{
return device.Leds.All(l => Layer.Leds.Contains(l));
}
/// <summary>
/// Adds an adaption hint to the adapter.
/// </summary>
/// <param name="adaptionHint">The adaption hint to add.</param>
public void Add(IAdaptionHint adaptionHint)
{
if (_adaptionHints.Contains(adaptionHint))
return;
_adaptionHints.Add(adaptionHint);
AdapterHintAdded?.Invoke(this, new LayerAdapterHintEventArgs(adaptionHint));
}
/// <summary>
/// Removes the first occurrence of a specific adaption hint from the adapter.
/// </summary>
/// <param name="adaptionHint">The adaption hint to remove.</param>
public void Remove(IAdaptionHint adaptionHint)
{
if (_adaptionHints.Remove(adaptionHint))
AdapterHintRemoved?.Invoke(this, new LayerAdapterHintEventArgs(adaptionHint));
}
/// <summary>
/// Removes all adaption hints from the adapter.
/// </summary>
public void Clear()
{
while (_adaptionHints.Any())
Remove(_adaptionHints.First());
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
_adaptionHints.Clear();
// Kind of meh.
// This leaves the adapter responsible for finding the right hint for the right entity, but it's gotta be done somewhere..
foreach (IAdaptionHintEntity hintEntity in Layer.LayerEntity.AdaptionHints)
switch (hintEntity)
{
case DeviceAdaptionHintEntity entity:
Add(new DeviceAdaptionHint(entity));
break;
case CategoryAdaptionHintEntity entity:
Add(new CategoryAdaptionHint(entity));
break;
case KeyboardSectionAdaptionHintEntity entity:
Add(new KeyboardSectionAdaptionHint(entity));
break;
}
}
/// <inheritdoc />
public void Save()
{
Layer.LayerEntity.AdaptionHints.Clear();
foreach (IAdaptionHint adaptionHint in AdaptionHints)
Layer.LayerEntity.AdaptionHints.Add(adaptionHint.GetEntry());
}
#endregion
/// <summary>
/// Occurs whenever a new adapter hint is added to the adapter.
/// </summary>
public event EventHandler<LayerAdapterHintEventArgs>? AdapterHintAdded;
/// <summary>
/// Occurs whenever an adapter hint is removed from the adapter.
/// </summary>
public event EventHandler<LayerAdapterHintEventArgs>? AdapterHintRemoved;
_adaptionHints = new List<IAdaptionHint>();
Layer = layer;
AdaptionHints = new ReadOnlyCollection<IAdaptionHint>(_adaptionHints);
}
/// <summary>
/// Gets the layer this adapter can adapt
/// </summary>
public Layer Layer { get; }
/// <summary>
/// Gets or sets a list containing the adaption hints used by this adapter
/// </summary>
public ReadOnlyCollection<IAdaptionHint> AdaptionHints { get; set; }
/// <summary>
/// Modifies the layer, adapting it to the provided <paramref name="devices" />
/// </summary>
/// <param name="devices">The devices to adapt the layer to</param>
public void Adapt(List<ArtemisDevice> devices)
{
// Use adaption hints if provided
if (AdaptionHints.Any())
{
foreach (IAdaptionHint adaptionHint in AdaptionHints)
adaptionHint.Apply(Layer, devices);
}
// If there are no hints, try to find matching LEDs anyway
else
{
List<ArtemisLed> availableLeds = devices.SelectMany(d => d.Leds).ToList();
List<ArtemisLed> usedLeds = new();
foreach (LedEntity ledEntity in Layer.LayerEntity.Leds)
{
// TODO: If this is a keyboard LED and the layouts don't match, convert it before looking for it on the devices
LedId ledId = Enum.Parse<LedId>(ledEntity.LedName);
ArtemisLed? led = availableLeds.FirstOrDefault(l => l.RgbLed.Id == ledId);
if (led != null)
{
availableLeds.Remove(led);
usedLeds.Add(led);
}
}
Layer.AddLeds(usedLeds);
}
}
/// <summary>
/// Automatically determine hints for this layer
/// </summary>
public List<IAdaptionHint> DetermineHints(IEnumerable<ArtemisDevice> devices)
{
List<IAdaptionHint> newHints = new();
if (devices.All(DoesLayerCoverDevice))
{
DeviceAdaptionHint hint = new() {DeviceType = RGBDeviceType.All};
Add(hint);
newHints.Add(hint);
}
else
{
// Any fully covered device will add a device adaption hint for that type
foreach (IGrouping<ArtemisDevice, ArtemisLed> deviceLeds in Layer.Leds.GroupBy(l => l.Device))
{
ArtemisDevice device = deviceLeds.Key;
// If there is already an adaption hint for this type, don't add another
if (AdaptionHints.Any(h => h is DeviceAdaptionHint d && d.DeviceType == device.DeviceType))
continue;
if (DoesLayerCoverDevice(device))
{
DeviceAdaptionHint hint = new() {DeviceType = device.DeviceType};
Add(hint);
newHints.Add(hint);
}
}
// Any fully covered category will add a category adaption hint for its category
foreach (DeviceCategory deviceCategory in Enum.GetValues<DeviceCategory>())
{
if (AdaptionHints.Any(h => h is CategoryAdaptionHint c && c.Category == deviceCategory))
continue;
List<ArtemisDevice> categoryDevices = devices.Where(d => d.Categories.Contains(deviceCategory)).ToList();
if (categoryDevices.Any() && categoryDevices.All(DoesLayerCoverDevice))
{
CategoryAdaptionHint hint = new() {Category = deviceCategory};
Add(hint);
newHints.Add(hint);
}
}
}
return newHints;
}
/// <summary>
/// Adds an adaption hint to the adapter.
/// </summary>
/// <param name="adaptionHint">The adaption hint to add.</param>
public void Add(IAdaptionHint adaptionHint)
{
if (_adaptionHints.Contains(adaptionHint))
return;
_adaptionHints.Add(adaptionHint);
AdapterHintAdded?.Invoke(this, new LayerAdapterHintEventArgs(adaptionHint));
}
/// <summary>
/// Removes the first occurrence of a specific adaption hint from the adapter.
/// </summary>
/// <param name="adaptionHint">The adaption hint to remove.</param>
public void Remove(IAdaptionHint adaptionHint)
{
if (_adaptionHints.Remove(adaptionHint))
AdapterHintRemoved?.Invoke(this, new LayerAdapterHintEventArgs(adaptionHint));
}
/// <summary>
/// Removes all adaption hints from the adapter.
/// </summary>
public void Clear()
{
while (_adaptionHints.Any())
Remove(_adaptionHints.First());
}
/// <summary>
/// Occurs whenever a new adapter hint is added to the adapter.
/// </summary>
public event EventHandler<LayerAdapterHintEventArgs>? AdapterHintAdded;
/// <summary>
/// Occurs whenever an adapter hint is removed from the adapter.
/// </summary>
public event EventHandler<LayerAdapterHintEventArgs>? AdapterHintRemoved;
private bool DoesLayerCoverDevice(ArtemisDevice device)
{
return device.Leds.All(l => Layer.Leds.Contains(l));
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
_adaptionHints.Clear();
// Kind of meh.
// This leaves the adapter responsible for finding the right hint for the right entity, but it's gotta be done somewhere..
foreach (IAdaptionHintEntity hintEntity in Layer.LayerEntity.AdaptionHints)
{
switch (hintEntity)
{
case DeviceAdaptionHintEntity entity:
Add(new DeviceAdaptionHint(entity));
break;
case CategoryAdaptionHintEntity entity:
Add(new CategoryAdaptionHint(entity));
break;
case KeyboardSectionAdaptionHintEntity entity:
Add(new KeyboardSectionAdaptionHint(entity));
break;
}
}
}
/// <inheritdoc />
public void Save()
{
Layer.LayerEntity.AdaptionHints.Clear();
foreach (IAdaptionHint adaptionHint in AdaptionHints)
Layer.LayerEntity.AdaptionHints.Add(adaptionHint.GetEntry());
}
#endregion
}

View File

@ -1,37 +1,36 @@
using Artemis.Core.LayerBrushes;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// A reference to a <see cref="LayerBrushDescriptor" />
/// </summary>
public class LayerBrushReference
{
/// <summary>
/// A reference to a <see cref="LayerBrushDescriptor" />
/// Creates a new instance of the <see cref="LayerBrushReference" /> class
/// </summary>
public class LayerBrushReference
public LayerBrushReference()
{
/// <summary>
/// Creates a new instance of the <see cref="LayerBrushReference" /> class
/// </summary>
public LayerBrushReference()
{
}
/// <summary>
/// Creates a new instance of the <see cref="LayerBrushReference" /> class
/// </summary>
/// <param name="descriptor">The descriptor to point the new reference at</param>
public LayerBrushReference(LayerBrushDescriptor descriptor)
{
LayerBrushProviderId = descriptor.Provider.Id;
BrushType = descriptor.LayerBrushType.Name;
}
/// <summary>
/// The ID of the layer brush provided the brush was provided by
/// </summary>
public string? LayerBrushProviderId { get; set; }
/// <summary>
/// The full type name of the brush descriptor
/// </summary>
public string? BrushType { get; set; }
}
/// <summary>
/// Creates a new instance of the <see cref="LayerBrushReference" /> class
/// </summary>
/// <param name="descriptor">The descriptor to point the new reference at</param>
public LayerBrushReference(LayerBrushDescriptor descriptor)
{
LayerBrushProviderId = descriptor.Provider.Id;
BrushType = descriptor.LayerBrushType.Name;
}
/// <summary>
/// The ID of the layer brush provided the brush was provided by
/// </summary>
public string? LayerBrushProviderId { get; set; }
/// <summary>
/// The full type name of the brush descriptor
/// </summary>
public string? BrushType { get; set; }
}

View File

@ -1,25 +1,24 @@
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a property group on a layer
/// <para>
/// Note: You cannot initialize property groups yourself. If properly placed and annotated, the Artemis core will
/// initialize these for you.
/// </para>
/// </summary>
public abstract class LayerEffectPropertyGroup : LayerPropertyGroup
{
/// <summary>
/// Represents a property group on a layer
/// <para>
/// Note: You cannot initialize property groups yourself. If properly placed and annotated, the Artemis core will
/// initialize these for you.
/// </para>
/// Whether or not this layer effect is enabled
/// </summary>
public abstract class LayerEffectPropertyGroup : LayerPropertyGroup
{
/// <summary>
/// Whether or not this layer effect is enabled
/// </summary>
[PropertyDescription(Name = "Enabled", Description = "Whether or not this layer effect is enabled")]
public BoolLayerProperty IsEnabled { get; set; } = null!;
[PropertyDescription(Name = "Enabled", Description = "Whether or not this layer effect is enabled")]
public BoolLayerProperty IsEnabled { get; set; } = null!;
internal void InitializeIsEnabled()
{
IsEnabled.DefaultValue = true;
if (!IsEnabled.IsLoadedFromStorage)
IsEnabled.SetCurrentValue(true);
}
internal void InitializeIsEnabled()
{
IsEnabled.DefaultValue = true;
if (!IsEnabled.IsLoadedFromStorage)
IsEnabled.SetCurrentValue(true);
}
}

View File

@ -2,52 +2,51 @@
#pragma warning disable 8618
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents the general properties of a layer
/// </summary>
public class LayerGeneralProperties : LayerPropertyGroup
{
/// <summary>
/// Represents the general properties of a layer
/// The type of brush to use for this layer
/// </summary>
public class LayerGeneralProperties : LayerPropertyGroup
[PropertyDescription(Name = "Brush type", Description = "The type of brush to use for this layer")]
public LayerBrushReferenceLayerProperty BrushReference { get; set; }
/// <summary>
/// The type of shape to draw in this layer
/// </summary>
[PropertyDescription(Name = "Shape type", Description = "The type of shape to draw in this layer")]
public EnumLayerProperty<LayerShapeType> ShapeType { get; set; }
/// <summary>
/// How to blend this layer into the resulting image
/// </summary>
[PropertyDescription(Name = "Blend mode", Description = "How to blend this layer into the resulting image")]
public EnumLayerProperty<SKBlendMode> BlendMode { get; set; }
/// <summary>
/// How the transformation properties are applied to the layer
/// </summary>
[PropertyDescription(Name = "Transform mode", Description = "How the transformation properties are applied to the layer")]
public EnumLayerProperty<LayerTransformMode> TransformMode { get; set; }
/// <inheritdoc />
protected override void PopulateDefaults()
{
/// <summary>
/// The type of brush to use for this layer
/// </summary>
[PropertyDescription(Name = "Brush type", Description = "The type of brush to use for this layer")]
public LayerBrushReferenceLayerProperty BrushReference { get; set; }
/// <summary>
/// The type of shape to draw in this layer
/// </summary>
[PropertyDescription(Name = "Shape type", Description = "The type of shape to draw in this layer")]
public EnumLayerProperty<LayerShapeType> ShapeType { get; set; }
ShapeType.DefaultValue = LayerShapeType.Rectangle;
BlendMode.DefaultValue = SKBlendMode.SrcOver;
}
/// <summary>
/// How to blend this layer into the resulting image
/// </summary>
[PropertyDescription(Name = "Blend mode", Description = "How to blend this layer into the resulting image")]
public EnumLayerProperty<SKBlendMode> BlendMode { get; set; }
/// <inheritdoc />
protected override void EnableProperties()
{
}
/// <summary>
/// How the transformation properties are applied to the layer
/// </summary>
[PropertyDescription(Name = "Transform mode", Description = "How the transformation properties are applied to the layer")]
public EnumLayerProperty<LayerTransformMode> TransformMode { get; set; }
/// <inheritdoc />
protected override void PopulateDefaults()
{
ShapeType.DefaultValue = LayerShapeType.Rectangle;
BlendMode.DefaultValue = SKBlendMode.SrcOver;
}
/// <inheritdoc />
protected override void EnableProperties()
{
}
/// <inheritdoc />
protected override void DisableProperties()
{
}
/// <inheritdoc />
protected override void DisableProperties()
{
}
}

View File

@ -1,11 +1,10 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents an attribute that marks a layer property to be ignored
/// </summary>
public class LayerPropertyIgnoreAttribute : Attribute
{
/// <summary>
/// Represents an attribute that marks a layer property to be ignored
/// </summary>
public class LayerPropertyIgnoreAttribute : Attribute
{
}
}

View File

@ -1,55 +1,54 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a description attribute used to decorate layer properties
/// </summary>
public class PropertyDescriptionAttribute : Attribute
{
/// <summary>
/// Represents a description attribute used to decorate layer properties
/// The identifier of this property used for storage, if not set one will be generated property name in code
/// </summary>
public class PropertyDescriptionAttribute : Attribute
{
/// <summary>
/// The identifier of this property used for storage, if not set one will be generated property name in code
/// </summary>
public string? Identifier { get; set; }
public string? Identifier { get; set; }
/// <summary>
/// The user-friendly name for this property, shown in the UI
/// </summary>
public string? Name { get; set; }
/// <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>
/// 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 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>
/// 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>
/// 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>
/// 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; }
/// <summary>
/// Maximum input value, only enforced in the UI
/// </summary>
public object? MaxInputValue { get; set; }
/// <summary>
/// Whether or not keyframes are always disabled
/// </summary>
public bool DisableKeyframes { get; set; }
}
/// <summary>
/// Whether or not keyframes are always disabled
/// </summary>
public bool DisableKeyframes { get; set; }
}

View File

@ -1,25 +1,25 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a description attribute used to decorate layer property groups
/// </summary>
public class PropertyGroupDescriptionAttribute : Attribute
{
/// <summary>
/// Represents a description attribute used to decorate layer property groups
/// The identifier of this property group used for storage, if not set one will be generated based on the group name in
/// code
/// </summary>
public class PropertyGroupDescriptionAttribute : Attribute
{
/// <summary>
/// The identifier of this property group used for storage, if not set one will be generated based on the group name in code
/// </summary>
public string? Identifier { get; set; }
public string? Identifier { get; set; }
/// <summary>
/// The user-friendly name for this property group, shown in the UI.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// The user-friendly description for this property group, shown in the UI.
/// </summary>
public string? Description { get; set; }
}
/// <summary>
/// The user-friendly name for this property group, shown in the UI.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// The user-friendly description for this property group, shown in the UI.
/// </summary>
public string? Description { get; set; }
}

View File

@ -1,63 +1,62 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a range between two single-precision floating point numbers
/// </summary>
public readonly struct FloatRange
{
private readonly Random _rand;
/// <summary>
/// Represents a range between two single-precision floating point numbers
/// Creates a new instance of the <see cref="FloatRange" /> class
/// </summary>
public readonly struct FloatRange
/// <param name="start">The start value of the range</param>
/// <param name="end">The end value of the range</param>
public FloatRange(float start, float end)
{
private readonly Random _rand;
Start = start;
End = end;
/// <summary>
/// Creates a new instance of the <see cref="FloatRange" /> class
/// </summary>
/// <param name="start">The start value of the range</param>
/// <param name="end">The end value of the range</param>
public FloatRange(float start, float end)
{
Start = start;
End = end;
_rand = new Random();
}
_rand = new Random();
}
/// <summary>
/// Gets the start value of the range
/// </summary>
public float Start { get; }
/// <summary>
/// Gets the start value of the range
/// </summary>
public float Start { get; }
/// <summary>
/// Gets the end value of the range
/// </summary>
public float End { get; }
/// <summary>
/// Gets the end value of the range
/// </summary>
public float End { get; }
/// <summary>
/// Determines whether the given value is in this range
/// </summary>
/// <param name="value">The value to check</param>
/// <param name="inclusive">
/// Whether the value may be equal to <see cref="Start" /> or <see cref="End" />
/// <para>Defaults to <see langword="true" /></para>
/// </param>
/// <returns></returns>
public bool IsInRange(float value, bool inclusive = true)
{
if (inclusive)
return value >= Start && value <= End;
return value > Start && value < End;
}
/// <summary>
/// Determines whether the given value is in this range
/// </summary>
/// <param name="value">The value to check</param>
/// <param name="inclusive">
/// Whether the value may be equal to <see cref="Start" /> or <see cref="End" />
/// <para>Defaults to <see langword="true" /></para>
/// </param>
/// <returns></returns>
public bool IsInRange(float value, bool inclusive = true)
{
if (inclusive)
return value >= Start && value <= End;
return value > Start && value < End;
}
/// <summary>
/// Returns a pseudo-random value between <see cref="Start" /> and <see cref="End" />
/// </summary>
/// <param name="inclusive">Whether the value may be equal to <see cref="Start" /></param>
/// <returns>The pseudo-random value</returns>
public float GetRandomValue(bool inclusive = true)
{
if (inclusive)
return _rand.Next((int) (Start * 100), (int) (End * 100)) / 100f;
return _rand.Next((int) (Start * 100) + 1, (int) (End * 100)) / 100f;
}
/// <summary>
/// Returns a pseudo-random value between <see cref="Start" /> and <see cref="End" />
/// </summary>
/// <param name="inclusive">Whether the value may be equal to <see cref="Start" /></param>
/// <returns>The pseudo-random value</returns>
public float GetRandomValue(bool inclusive = true)
{
if (inclusive)
return _rand.Next((int) (Start * 100), (int) (End * 100)) / 100f;
return _rand.Next((int) (Start * 100) + 1, (int) (End * 100)) / 100f;
}
}

View File

@ -2,152 +2,151 @@
using System.Collections.ObjectModel;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a property on a layer. Properties are saved in storage and can optionally be modified from the UI.
/// <para>
/// Note: You cannot initialize layer properties yourself. If properly placed and annotated, the Artemis core will
/// initialize these for you.
/// </para>
/// </summary>
public interface ILayerProperty : IStorageModel, IDisposable
{
/// <summary>
/// Represents a property on a layer. Properties are saved in storage and can optionally be modified from the UI.
/// Gets the description attribute applied to this property
/// </summary>
PropertyDescriptionAttribute PropertyDescription { get; }
/// <summary>
/// Gets the profile element (such as layer or folder) this property is applied to
/// </summary>
RenderProfileElement ProfileElement { get; }
/// <summary>
/// The parent group of this layer property, set after construction
/// </summary>
LayerPropertyGroup LayerPropertyGroup { get; }
/// <summary>
/// Gets or sets whether the property is hidden in the UI
/// </summary>
public bool IsHidden { get; set; }
/// <summary>
/// Gets the data binding of this property
/// </summary>
IDataBinding BaseDataBinding { get; }
/// <summary>
/// Gets a boolean indicating whether the layer has any data binding properties
/// </summary>
public bool HasDataBinding { get; }
/// <summary>
/// Gets a boolean indicating whether data bindings are supported on this type of property
/// </summary>
public bool DataBindingsSupported { get; }
/// <summary>
/// Gets the unique path of the property on the render element
/// </summary>
string Path { get; }
/// <summary>
/// Gets a read-only list of all the keyframes on this layer property
/// </summary>
ReadOnlyCollection<ILayerPropertyKeyframe> UntypedKeyframes { get; }
/// <summary>
/// Gets the type of the property
/// </summary>
Type PropertyType { get; }
/// <summary>
/// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied
/// </summary>
bool IsLoadedFromStorage { get; }
/// <summary>
/// Initializes the layer property
/// <para>
/// Note: You cannot initialize layer properties yourself. If properly placed and annotated, the Artemis core will
/// initialize these for you.
/// Note: This isn't done in the constructor to keep it parameterless which is easier for implementations of
/// <see cref="LayerProperty{T}" />
/// </para>
/// </summary>
public interface ILayerProperty : IStorageModel, IDisposable
{
/// <summary>
/// Gets the description attribute applied to this property
/// </summary>
PropertyDescriptionAttribute PropertyDescription { get; }
void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description);
/// <summary>
/// Gets the profile element (such as layer or folder) this property is applied to
/// </summary>
RenderProfileElement ProfileElement { get; }
/// <summary>
/// Attempts to create a keyframe for this property from the provided entity
/// </summary>
/// <param name="keyframeEntity">The entity representing the keyframe to create</param>
/// <returns>If succeeded the resulting keyframe, otherwise <see langword="null" /></returns>
ILayerPropertyKeyframe? CreateKeyframeFromEntity(KeyframeEntity keyframeEntity);
/// <summary>
/// The parent group of this layer property, set after construction
/// </summary>
LayerPropertyGroup LayerPropertyGroup { get; }
/// <summary>
/// Overrides the property value with the default value
/// </summary>
void ApplyDefaultValue();
/// <summary>
/// Gets or sets whether the property is hidden in the UI
/// </summary>
public bool IsHidden { get; set; }
/// <summary>
/// Gets the data binding of this property
/// </summary>
IDataBinding BaseDataBinding { get; }
/// <summary>
/// Gets a boolean indicating whether the layer has any data binding properties
/// </summary>
public bool HasDataBinding { get; }
/// <summary>
/// Gets a boolean indicating whether data bindings are supported on this type of property
/// </summary>
public bool DataBindingsSupported { get; }
/// <summary>
/// Gets the unique path of the property on the render element
/// </summary>
string Path { get; }
/// <summary>
/// Gets a read-only list of all the keyframes on this layer property
/// </summary>
ReadOnlyCollection<ILayerPropertyKeyframe> UntypedKeyframes { get; }
/// <summary>
/// Gets the type of the property
/// </summary>
Type PropertyType { get; }
/// <summary>
/// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied
/// </summary>
bool IsLoadedFromStorage { get; }
/// <summary>
/// Initializes the layer property
/// <para>
/// Note: This isn't done in the constructor to keep it parameterless which is easier for implementations of
/// <see cref="LayerProperty{T}" />
/// </para>
/// </summary>
void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description);
/// <summary>
/// Attempts to create a keyframe for this property from the provided entity
/// </summary>
/// <param name="keyframeEntity">The entity representing the keyframe to create</param>
/// <returns>If succeeded the resulting keyframe, otherwise <see langword="null" /></returns>
ILayerPropertyKeyframe? CreateKeyframeFromEntity(KeyframeEntity keyframeEntity);
/// <summary>
/// Overrides the property value with the default value
/// </summary>
void ApplyDefaultValue();
/// <summary>
/// Updates the layer properties internal state
/// </summary>
/// <param name="timeline">The timeline to apply to the property</param>
void Update(Timeline timeline);
/// <summary>
/// Updates the layer properties internal state
/// </summary>
/// <param name="timeline">The timeline to apply to the property</param>
void Update(Timeline timeline);
/// <summary>
/// Updates just the data binding instead of the entire layer
/// </summary>
void UpdateDataBinding();
/// <summary>
/// Updates just the data binding instead of the entire layer
/// </summary>
void UpdateDataBinding();
/// <summary>
/// Removes a keyframe from the layer property without knowing it's type.
/// <para>Prefer <see cref="LayerProperty{T}.RemoveKeyframe"/>.</para>
/// </summary>
/// <param name="keyframe"></param>
void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe);
/// <summary>
/// Removes a keyframe from the layer property without knowing it's type.
/// <para>Prefer <see cref="LayerProperty{T}.RemoveKeyframe" />.</para>
/// </summary>
/// <param name="keyframe"></param>
void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe);
/// <summary>
/// Adds a keyframe to the layer property without knowing it's type.
/// <para>Prefer <see cref="LayerProperty{T}.AddKeyframe"/>.</para>
/// </summary>
/// <param name="keyframe"></param>
void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe);
/// <summary>
/// Adds a keyframe to the layer property without knowing it's type.
/// <para>Prefer <see cref="LayerProperty{T}.AddKeyframe" />.</para>
/// </summary>
/// <param name="keyframe"></param>
void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe);
/// <summary>
/// Occurs when the layer property is disposed
/// </summary>
public event EventHandler Disposed;
/// <summary>
/// Occurs when the layer property is disposed
/// </summary>
public event EventHandler Disposed;
/// <summary>
/// Occurs once every frame when the layer property is updated
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? Updated;
/// <summary>
/// Occurs once every frame when the layer property is updated
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? Updated;
/// <summary>
/// Occurs when the current value of the layer property was updated by some form of input
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? CurrentValueSet;
/// <summary>
/// Occurs when the current value of the layer property was updated by some form of input
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? CurrentValueSet;
/// <summary>
/// Occurs when the visibility value of the layer property was updated
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? VisibilityChanged;
/// <summary>
/// Occurs when the visibility value of the layer property was updated
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? VisibilityChanged;
/// <summary>
/// Occurs when keyframes are enabled/disabled
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? KeyframesToggled;
/// <summary>
/// Occurs when keyframes are enabled/disabled
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? KeyframesToggled;
/// <summary>
/// Occurs when a new keyframe was added to the layer property
/// </summary>
public event EventHandler<LayerPropertyKeyframeEventArgs>? KeyframeAdded;
/// <summary>
/// Occurs when a new keyframe was added to the layer property
/// </summary>
public event EventHandler<LayerPropertyKeyframeEventArgs>? KeyframeAdded;
/// <summary>
/// Occurs when a keyframe was removed from the layer property
/// </summary>
public event EventHandler<LayerPropertyKeyframeEventArgs>? KeyframeRemoved;
}
/// <summary>
/// Occurs when a keyframe was removed from the layer property
/// </summary>
public event EventHandler<LayerPropertyKeyframeEventArgs>? KeyframeRemoved;
}

View File

@ -2,43 +2,42 @@
using System.ComponentModel;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a keyframe on a <see cref="ILayerProperty" /> containing a value and a timestamp
/// </summary>
public interface ILayerPropertyKeyframe : INotifyPropertyChanged
{
/// <summary>
/// Represents a keyframe on a <see cref="ILayerProperty" /> containing a value and a timestamp
/// Gets an untyped reference to the layer property of this keyframe
/// </summary>
public interface ILayerPropertyKeyframe : INotifyPropertyChanged
{
/// <summary>
/// Gets an untyped reference to the layer property of this keyframe
/// </summary>
ILayerProperty UntypedLayerProperty { get; }
ILayerProperty UntypedLayerProperty { get; }
/// <summary>
/// Gets or sets the position of this keyframe in the timeline
/// </summary>
TimeSpan Position { get; set; }
/// <summary>
/// Gets or sets the position of this keyframe in the timeline
/// </summary>
TimeSpan Position { get; set; }
/// <summary>
/// Gets or sets the easing function applied on the value of the keyframe
/// </summary>
Easings.Functions EasingFunction { get; set; }
/// <summary>
/// Gets or sets the easing function applied on the value of the keyframe
/// </summary>
Easings.Functions EasingFunction { get; set; }
/// <summary>
/// Gets the entity this keyframe uses for persistent storage
/// </summary>
KeyframeEntity GetKeyframeEntity();
/// <summary>
/// Gets the entity this keyframe uses for persistent storage
/// </summary>
KeyframeEntity GetKeyframeEntity();
/// <summary>
/// Removes the keyframe from the layer property
/// </summary>
void Remove();
/// <summary>
/// Removes the keyframe from the layer property
/// </summary>
void Remove();
/// <summary>
/// Creates a copy of this keyframe.
/// <para>Note: The copied keyframe is not added to the layer property.</para>
/// </summary>
/// <returns>The resulting copy</returns>
ILayerPropertyKeyframe CreateCopy();
}
/// <summary>
/// Creates a copy of this keyframe.
/// <para>Note: The copied keyframe is not added to the layer property.</para>
/// </summary>
/// <returns>The resulting copy</returns>
ILayerPropertyKeyframe CreateCopy();
}

View File

@ -1,63 +1,62 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a range between two signed integers
/// </summary>
public readonly struct IntRange
{
private readonly Random _rand;
/// <summary>
/// Represents a range between two signed integers
/// Creates a new instance of the <see cref="IntRange" /> class
/// </summary>
public readonly struct IntRange
/// <param name="start">The start value of the range</param>
/// <param name="end">The end value of the range</param>
public IntRange(int start, int end)
{
private readonly Random _rand;
/// <summary>
/// Creates a new instance of the <see cref="IntRange" /> class
/// </summary>
/// <param name="start">The start value of the range</param>
/// <param name="end">The end value of the range</param>
public IntRange(int start, int end)
{
Start = start;
End = end;
Start = start;
End = end;
_rand = new Random();
}
_rand = new Random();
}
/// <summary>
/// Gets the start value of the range
/// </summary>
public int Start { get; }
/// <summary>
/// Gets the start value of the range
/// </summary>
public int Start { get; }
/// <summary>
/// Gets the end value of the range
/// </summary>
public int End { get; }
/// <summary>
/// Gets the end value of the range
/// </summary>
public int End { get; }
/// <summary>
/// Determines whether the given value is in this range
/// </summary>
/// <param name="value">The value to check</param>
/// <param name="inclusive">
/// Whether the value may be equal to <see cref="Start" /> or <see cref="End" />
/// <para>Defaults to <see langword="true" /></para>
/// </param>
/// <returns></returns>
public bool IsInRange(int value, bool inclusive = true)
{
if (inclusive)
return value >= Start && value <= End;
return value > Start && value < End;
}
/// <summary>
/// Determines whether the given value is in this range
/// </summary>
/// <param name="value">The value to check</param>
/// <param name="inclusive">
/// Whether the value may be equal to <see cref="Start" /> or <see cref="End" />
/// <para>Defaults to <see langword="true" /></para>
/// </param>
/// <returns></returns>
public bool IsInRange(int value, bool inclusive = true)
{
if (inclusive)
return value >= Start && value <= End;
return value > Start && value < End;
}
/// <summary>
/// Returns a pseudo-random value between <see cref="Start" /> and <see cref="End" />
/// </summary>
/// <param name="inclusive">Whether the value may be equal to <see cref="Start" /></param>
/// <returns>The pseudo-random value</returns>
public int GetRandomValue(bool inclusive = true)
{
if (inclusive)
return _rand.Next(Start, End + 1);
return _rand.Next(Start + 1, End);
}
/// <summary>
/// Returns a pseudo-random value between <see cref="Start" /> and <see cref="End" />
/// </summary>
/// <param name="inclusive">Whether the value may be equal to <see cref="Start" /></param>
/// <returns>The pseudo-random value</returns>
public int GetRandomValue(bool inclusive = true)
{
if (inclusive)
return _rand.Next(Start, End + 1);
return _rand.Next(Start + 1, End);
}
}

View File

@ -1,90 +1,89 @@
using System;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a keyframe on a <see cref="LayerProperty{T}" /> containing a value and a timestamp
/// </summary>
public class LayerPropertyKeyframe<T> : CorePropertyChanged, ILayerPropertyKeyframe
{
private LayerProperty<T> _layerProperty;
private TimeSpan _position;
private T _value;
/// <summary>
/// Represents a keyframe on a <see cref="LayerProperty{T}" /> containing a value and a timestamp
/// Creates a new instance of the <see cref="LayerPropertyKeyframe{T}" /> class
/// </summary>
public class LayerPropertyKeyframe<T> : CorePropertyChanged, ILayerPropertyKeyframe
/// <param name="value">The value of the keyframe</param>
/// <param name="position">The position of this keyframe in the timeline</param>
/// <param name="easingFunction">The easing function applied on the value of the keyframe</param>
/// <param name="layerProperty">The layer property this keyframe is applied to</param>
public LayerPropertyKeyframe(T value, TimeSpan position, Easings.Functions easingFunction, LayerProperty<T> layerProperty)
{
private LayerProperty<T> _layerProperty;
private TimeSpan _position;
private T _value;
_position = position;
_layerProperty = layerProperty;
_value = value;
/// <summary>
/// Creates a new instance of the <see cref="LayerPropertyKeyframe{T}" /> class
/// </summary>
/// <param name="value">The value of the keyframe</param>
/// <param name="position">The position of this keyframe in the timeline</param>
/// <param name="easingFunction">The easing function applied on the value of the keyframe</param>
/// <param name="layerProperty">The layer property this keyframe is applied to</param>
public LayerPropertyKeyframe(T value, TimeSpan position, Easings.Functions easingFunction, LayerProperty<T> layerProperty)
EasingFunction = easingFunction;
}
/// <summary>
/// The layer property this keyframe is applied to
/// </summary>
public LayerProperty<T> LayerProperty
{
get => _layerProperty;
internal set => SetAndNotify(ref _layerProperty, value);
}
/// <summary>
/// The value of this keyframe
/// </summary>
public T Value
{
get => _value;
set => SetAndNotify(ref _value, value);
}
/// <inheritdoc />
public ILayerProperty UntypedLayerProperty => LayerProperty;
/// <inheritdoc />
public TimeSpan Position
{
get => _position;
set
{
_position = position;
_layerProperty = layerProperty;
_value = value;
EasingFunction = easingFunction;
}
/// <summary>
/// The layer property this keyframe is applied to
/// </summary>
public LayerProperty<T> LayerProperty
{
get => _layerProperty;
internal set => SetAndNotify(ref _layerProperty, value);
}
/// <summary>
/// The value of this keyframe
/// </summary>
public T Value
{
get => _value;
set => SetAndNotify(ref _value, value);
}
/// <inheritdoc />
public ILayerProperty UntypedLayerProperty => LayerProperty;
/// <inheritdoc />
public TimeSpan Position
{
get => _position;
set
{
SetAndNotify(ref _position, value);
LayerProperty.SortKeyframes();
LayerProperty.ReapplyUpdate();
}
}
/// <inheritdoc />
public Easings.Functions EasingFunction { get; set; }
/// <inheritdoc />
public KeyframeEntity GetKeyframeEntity()
{
return new KeyframeEntity
{
Value = CoreJson.SerializeObject(Value),
Position = Position,
EasingFunction = (int) EasingFunction
};
}
/// <inheritdoc />
public void Remove()
{
LayerProperty.RemoveKeyframe(this);
}
/// <inheritdoc />
public ILayerPropertyKeyframe CreateCopy()
{
return new LayerPropertyKeyframe<T>(Value, Position, EasingFunction, LayerProperty);
SetAndNotify(ref _position, value);
LayerProperty.SortKeyframes();
LayerProperty.ReapplyUpdate();
}
}
/// <inheritdoc />
public Easings.Functions EasingFunction { get; set; }
/// <inheritdoc />
public KeyframeEntity GetKeyframeEntity()
{
return new KeyframeEntity
{
Value = CoreJson.SerializeObject(Value),
Position = Position,
EasingFunction = (int) EasingFunction
};
}
/// <inheritdoc />
public void Remove()
{
LayerProperty.RemoveKeyframe(this);
}
/// <inheritdoc />
public ILayerPropertyKeyframe CreateCopy()
{
return new LayerPropertyKeyframe<T>(Value, Position, EasingFunction, LayerProperty);
}
}

View File

@ -79,7 +79,8 @@ public sealed class LayerPropertyPreview<T> : IDisposable
}
Property.SetCurrentValue(OriginalValue, Time);
return !Equals(OriginalValue, PreviewValue); ;
return !Equals(OriginalValue, PreviewValue);
;
}
/// <summary>

View File

@ -3,346 +3,343 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.Storage.Entities.Profile;
using Humanizer;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a property group on a layer
/// <para>
/// Note: You cannot initialize property groups yourself. If properly placed and annotated, the Artemis core will
/// initialize these for you.
/// </para>
/// </summary>
public abstract class LayerPropertyGroup : IDisposable
{
private readonly List<ILayerProperty> _layerProperties;
private readonly List<LayerPropertyGroup> _layerPropertyGroups;
private bool _disposed;
private bool _isHidden;
/// <summary>
/// Represents a property group on a layer
/// <para>
/// Note: You cannot initialize property groups yourself. If properly placed and annotated, the Artemis core will
/// initialize these for you.
/// </para>
/// A base constructor for a <see cref="LayerPropertyGroup" />
/// </summary>
public abstract class LayerPropertyGroup : IDisposable
protected LayerPropertyGroup()
{
private readonly List<ILayerProperty> _layerProperties;
private readonly List<LayerPropertyGroup> _layerPropertyGroups;
private bool _disposed;
private bool _isHidden;
// These are set right after construction to keep the constructor (and inherited constructs) clean
ProfileElement = null!;
GroupDescription = null!;
Path = "";
/// <summary>
/// A base constructor for a <see cref="LayerPropertyGroup" />
/// </summary>
protected LayerPropertyGroup()
_layerProperties = new List<ILayerProperty>();
_layerPropertyGroups = new List<LayerPropertyGroup>();
LayerProperties = new ReadOnlyCollection<ILayerProperty>(_layerProperties);
LayerPropertyGroups = new ReadOnlyCollection<LayerPropertyGroup>(_layerPropertyGroups);
}
/// <summary>
/// Gets the profile element (such as layer or folder) this group is associated with
/// </summary>
public RenderProfileElement ProfileElement { get; private set; }
/// <summary>
/// Gets the description of this group
/// </summary>
public PropertyGroupDescriptionAttribute GroupDescription { get; private set; }
/// <summary>
/// The parent group of this group
/// </summary>
[LayerPropertyIgnore] // Ignore the parent when selecting child groups
public LayerPropertyGroup? Parent { get; internal set; }
/// <summary>
/// Gets the unique path of the property on the render element
/// </summary>
public string Path { get; private set; }
/// <summary>
/// Gets whether this property groups properties are all initialized
/// </summary>
public bool PropertiesInitialized { get; private set; }
/// <summary>
/// Gets or sets whether the property is hidden in the UI
/// </summary>
public bool IsHidden
{
get => _isHidden;
set
{
// These are set right after construction to keep the constructor (and inherited constructs) clean
ProfileElement = null!;
GroupDescription = null!;
Path = "";
_layerProperties = new List<ILayerProperty>();
_layerPropertyGroups = new List<LayerPropertyGroup>();
LayerProperties = new ReadOnlyCollection<ILayerProperty>(_layerProperties);
LayerPropertyGroups = new ReadOnlyCollection<LayerPropertyGroup>(_layerPropertyGroups);
}
/// <summary>
/// Gets the profile element (such as layer or folder) this group is associated with
/// </summary>
public RenderProfileElement ProfileElement { get; private set; }
/// <summary>
/// Gets the description of this group
/// </summary>
public PropertyGroupDescriptionAttribute GroupDescription { get; private set; }
/// <summary>
/// The parent group of this group
/// </summary>
[LayerPropertyIgnore] // Ignore the parent when selecting child groups
public LayerPropertyGroup? Parent { get; internal set; }
/// <summary>
/// Gets the unique path of the property on the render element
/// </summary>
public string Path { get; private set; }
/// <summary>
/// Gets whether this property groups properties are all initialized
/// </summary>
public bool PropertiesInitialized { get; private set; }
/// <summary>
/// Gets or sets whether the property is hidden in the UI
/// </summary>
public bool IsHidden
{
get => _isHidden;
set
{
_isHidden = value;
OnVisibilityChanged();
}
}
/// <summary>
/// Gets the entity this property group uses for persistent storage
/// </summary>
public PropertyGroupEntity? PropertyGroupEntity { get; internal set; }
/// <summary>
/// A list of all layer properties in this group
/// </summary>
public ReadOnlyCollection<ILayerProperty> LayerProperties { get; }
/// <summary>
/// A list of al child groups in this group
/// </summary>
public ReadOnlyCollection<LayerPropertyGroup> LayerPropertyGroups { get; }
/// <summary>
/// Recursively gets all layer properties on this group and any subgroups
/// </summary>
public IReadOnlyCollection<ILayerProperty> GetAllLayerProperties()
{
if (_disposed)
throw new ObjectDisposedException("LayerPropertyGroup");
if (!PropertiesInitialized)
return new List<ILayerProperty>();
List<ILayerProperty> result = new(LayerProperties);
foreach (LayerPropertyGroup layerPropertyGroup in LayerPropertyGroups)
result.AddRange(layerPropertyGroup.GetAllLayerProperties());
return result.AsReadOnly();
}
/// <summary>
/// Applies the default value to all layer properties
/// </summary>
public void ResetAllLayerProperties()
{
foreach (ILayerProperty layerProperty in GetAllLayerProperties())
layerProperty.ApplyDefaultValue();
}
/// <summary>
/// Occurs when the property group has initialized all its children
/// </summary>
public event EventHandler? PropertyGroupInitialized;
/// <summary>
/// Occurs when one of the current value of one of the layer properties in this group changes by some form of input
/// <para>Note: Will not trigger on properties in child groups</para>
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? LayerPropertyOnCurrentValueSet;
/// <summary>
/// Occurs when the <see cref="IsHidden" /> value of the layer property was updated
/// </summary>
public event EventHandler? VisibilityChanged;
/// <summary>
/// Called before property group is activated to allow you to populate <see cref="LayerProperty{T}.DefaultValue" /> on
/// the properties you want
/// </summary>
protected abstract void PopulateDefaults();
/// <summary>
/// Called when the property group is activated
/// </summary>
protected abstract void EnableProperties();
/// <summary>
/// Called when the property group is deactivated (either the profile unloaded or the related brush/effect was removed)
/// </summary>
protected abstract void DisableProperties();
/// <summary>
/// Called when the property group and all its layer properties have been initialized
/// </summary>
protected virtual void OnPropertyGroupInitialized()
{
PropertyGroupInitialized?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_disposed = true;
DisableProperties();
foreach (ILayerProperty layerProperty in _layerProperties)
layerProperty.Dispose();
foreach (LayerPropertyGroup layerPropertyGroup in _layerPropertyGroups)
layerPropertyGroup.Dispose();
}
}
internal void Initialize(RenderProfileElement profileElement, LayerPropertyGroup? parent, PropertyGroupDescriptionAttribute groupDescription, PropertyGroupEntity? propertyGroupEntity)
{
if (groupDescription.Identifier == null)
throw new ArtemisCoreException("Can't initialize a property group without an identifier");
// Doubt this will happen but let's make sure
if (PropertiesInitialized)
throw new ArtemisCoreException("Layer property group already initialized, wut");
ProfileElement = profileElement;
Parent = parent;
GroupDescription = groupDescription;
PropertyGroupEntity = propertyGroupEntity ?? new PropertyGroupEntity {Identifier = groupDescription.Identifier};
Path = parent != null ? parent.Path + "." + groupDescription.Identifier : groupDescription.Identifier;
// Get all properties implementing ILayerProperty or LayerPropertyGroup
foreach (PropertyInfo propertyInfo in GetType().GetProperties())
{
if (Attribute.IsDefined(propertyInfo, typeof(LayerPropertyIgnoreAttribute)))
continue;
if (typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType))
{
PropertyDescriptionAttribute? propertyDescription =
(PropertyDescriptionAttribute?) Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyDescriptionAttribute));
InitializeProperty(propertyInfo, propertyDescription ?? new PropertyDescriptionAttribute());
}
else if (typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType))
{
PropertyGroupDescriptionAttribute? propertyGroupDescription =
(PropertyGroupDescriptionAttribute?) Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyGroupDescriptionAttribute));
InitializeChildGroup(propertyInfo, propertyGroupDescription ?? new PropertyGroupDescriptionAttribute());
}
}
// Request the property group to populate defaults
PopulateDefaults();
// Load the layer properties after defaults have been applied
foreach (ILayerProperty layerProperty in _layerProperties)
layerProperty.Load();
EnableProperties();
PropertiesInitialized = true;
OnPropertyGroupInitialized();
}
internal void ApplyToEntity()
{
if (!PropertiesInitialized || PropertyGroupEntity == null)
return;
foreach (ILayerProperty layerProperty in LayerProperties)
layerProperty.Save();
PropertyGroupEntity.PropertyGroups.Clear();
foreach (LayerPropertyGroup layerPropertyGroup in LayerPropertyGroups)
{
layerPropertyGroup.ApplyToEntity();
PropertyGroupEntity.PropertyGroups.Add(layerPropertyGroup.PropertyGroupEntity);
}
}
internal void Update(Timeline timeline)
{
foreach (ILayerProperty layerProperty in LayerProperties)
layerProperty.Update(timeline);
foreach (LayerPropertyGroup layerPropertyGroup in LayerPropertyGroups)
layerPropertyGroup.Update(timeline);
}
internal void MoveLayerProperty(ILayerProperty layerProperty, int index)
{
if (!_layerProperties.Contains(layerProperty))
return;
_layerProperties.Remove(layerProperty);
_layerProperties.Insert(index, layerProperty);
}
internal virtual void OnVisibilityChanged()
{
VisibilityChanged?.Invoke(this, EventArgs.Empty);
}
internal virtual void OnLayerPropertyOnCurrentValueSet(LayerPropertyEventArgs e)
{
Parent?.OnLayerPropertyOnCurrentValueSet(e);
LayerPropertyOnCurrentValueSet?.Invoke(this, e);
}
private void InitializeProperty(PropertyInfo propertyInfo, PropertyDescriptionAttribute propertyDescription)
{
// Ensure the description has an identifier and name, if not this is a good point to set it based on the property info
if (string.IsNullOrWhiteSpace(propertyDescription.Identifier))
propertyDescription.Identifier = propertyInfo.Name;
if (string.IsNullOrWhiteSpace(propertyDescription.Name))
propertyDescription.Name = propertyInfo.Name.Humanize();
if (!typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType))
throw new ArtemisPluginException($"Property with PropertyDescription attribute must be of type ILayerProperty: {propertyDescription.Identifier}");
if (Activator.CreateInstance(propertyInfo.PropertyType, true) is not ILayerProperty instance)
throw new ArtemisPluginException($"Failed to create instance of layer property: {propertyDescription.Identifier}");
PropertyEntity entity = GetPropertyEntity(propertyDescription.Identifier, out bool fromStorage);
instance.Initialize(ProfileElement, this, entity, fromStorage, propertyDescription);
propertyInfo.SetValue(this, instance);
_layerProperties.Add(instance);
}
private void InitializeChildGroup(PropertyInfo propertyInfo, PropertyGroupDescriptionAttribute propertyGroupDescription)
{
// Ensure the description has an identifier and name name, if not this is a good point to set it based on the property info
if (string.IsNullOrWhiteSpace(propertyGroupDescription.Identifier))
propertyGroupDescription.Identifier = propertyInfo.Name;
if (string.IsNullOrWhiteSpace(propertyGroupDescription.Name))
propertyGroupDescription.Name = propertyInfo.Name.Humanize();
if (!typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType))
throw new ArtemisPluginException($"Property with PropertyGroupDescription attribute must be of type LayerPropertyGroup: {propertyGroupDescription.Identifier}");
if (!(Activator.CreateInstance(propertyInfo.PropertyType) is LayerPropertyGroup instance))
throw new ArtemisPluginException($"Failed to create instance of layer property group: {propertyGroupDescription.Identifier}");
PropertyGroupEntity entity = GetPropertyGroupEntity(propertyGroupDescription.Identifier);
instance.Initialize(ProfileElement, this, propertyGroupDescription, entity);
propertyInfo.SetValue(this, instance);
_layerPropertyGroups.Add(instance);
}
private PropertyEntity GetPropertyEntity(string identifier, out bool fromStorage)
{
if (PropertyGroupEntity == null)
throw new ArtemisCoreException($"Can't execute {nameof(GetPropertyEntity)} without {nameof(PropertyGroupEntity)} being setup");
PropertyEntity? entity = PropertyGroupEntity.Properties.FirstOrDefault(p => p.Identifier == identifier);
fromStorage = entity != null;
if (entity == null)
{
entity = new PropertyEntity {Identifier = identifier};
PropertyGroupEntity.Properties.Add(entity);
}
return entity;
}
private PropertyGroupEntity GetPropertyGroupEntity(string identifier)
{
if (PropertyGroupEntity == null)
throw new ArtemisCoreException($"Can't execute {nameof(GetPropertyGroupEntity)} without {nameof(PropertyGroupEntity)} being setup");
return PropertyGroupEntity.PropertyGroups.FirstOrDefault(g => g.Identifier == identifier) ?? new PropertyGroupEntity() {Identifier = identifier};
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
_isHidden = value;
OnVisibilityChanged();
}
}
/// <summary>
/// Gets the entity this property group uses for persistent storage
/// </summary>
public PropertyGroupEntity? PropertyGroupEntity { get; internal set; }
/// <summary>
/// A list of all layer properties in this group
/// </summary>
public ReadOnlyCollection<ILayerProperty> LayerProperties { get; }
/// <summary>
/// A list of al child groups in this group
/// </summary>
public ReadOnlyCollection<LayerPropertyGroup> LayerPropertyGroups { get; }
/// <summary>
/// Recursively gets all layer properties on this group and any subgroups
/// </summary>
public IReadOnlyCollection<ILayerProperty> GetAllLayerProperties()
{
if (_disposed)
throw new ObjectDisposedException("LayerPropertyGroup");
if (!PropertiesInitialized)
return new List<ILayerProperty>();
List<ILayerProperty> result = new(LayerProperties);
foreach (LayerPropertyGroup layerPropertyGroup in LayerPropertyGroups)
result.AddRange(layerPropertyGroup.GetAllLayerProperties());
return result.AsReadOnly();
}
/// <summary>
/// Applies the default value to all layer properties
/// </summary>
public void ResetAllLayerProperties()
{
foreach (ILayerProperty layerProperty in GetAllLayerProperties())
layerProperty.ApplyDefaultValue();
}
/// <summary>
/// Occurs when the property group has initialized all its children
/// </summary>
public event EventHandler? PropertyGroupInitialized;
/// <summary>
/// Occurs when one of the current value of one of the layer properties in this group changes by some form of input
/// <para>Note: Will not trigger on properties in child groups</para>
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? LayerPropertyOnCurrentValueSet;
/// <summary>
/// Occurs when the <see cref="IsHidden" /> value of the layer property was updated
/// </summary>
public event EventHandler? VisibilityChanged;
/// <summary>
/// Called before property group is activated to allow you to populate <see cref="LayerProperty{T}.DefaultValue" /> on
/// the properties you want
/// </summary>
protected abstract void PopulateDefaults();
/// <summary>
/// Called when the property group is activated
/// </summary>
protected abstract void EnableProperties();
/// <summary>
/// Called when the property group is deactivated (either the profile unloaded or the related brush/effect was removed)
/// </summary>
protected abstract void DisableProperties();
/// <summary>
/// Called when the property group and all its layer properties have been initialized
/// </summary>
protected virtual void OnPropertyGroupInitialized()
{
PropertyGroupInitialized?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_disposed = true;
DisableProperties();
foreach (ILayerProperty layerProperty in _layerProperties)
layerProperty.Dispose();
foreach (LayerPropertyGroup layerPropertyGroup in _layerPropertyGroups)
layerPropertyGroup.Dispose();
}
}
internal void Initialize(RenderProfileElement profileElement, LayerPropertyGroup? parent, PropertyGroupDescriptionAttribute groupDescription, PropertyGroupEntity? propertyGroupEntity)
{
if (groupDescription.Identifier == null)
throw new ArtemisCoreException("Can't initialize a property group without an identifier");
// Doubt this will happen but let's make sure
if (PropertiesInitialized)
throw new ArtemisCoreException("Layer property group already initialized, wut");
ProfileElement = profileElement;
Parent = parent;
GroupDescription = groupDescription;
PropertyGroupEntity = propertyGroupEntity ?? new PropertyGroupEntity {Identifier = groupDescription.Identifier};
Path = parent != null ? parent.Path + "." + groupDescription.Identifier : groupDescription.Identifier;
// Get all properties implementing ILayerProperty or LayerPropertyGroup
foreach (PropertyInfo propertyInfo in GetType().GetProperties())
{
if (Attribute.IsDefined(propertyInfo, typeof(LayerPropertyIgnoreAttribute)))
continue;
if (typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType))
{
PropertyDescriptionAttribute? propertyDescription =
(PropertyDescriptionAttribute?) Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyDescriptionAttribute));
InitializeProperty(propertyInfo, propertyDescription ?? new PropertyDescriptionAttribute());
}
else if (typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType))
{
PropertyGroupDescriptionAttribute? propertyGroupDescription =
(PropertyGroupDescriptionAttribute?) Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyGroupDescriptionAttribute));
InitializeChildGroup(propertyInfo, propertyGroupDescription ?? new PropertyGroupDescriptionAttribute());
}
}
// Request the property group to populate defaults
PopulateDefaults();
// Load the layer properties after defaults have been applied
foreach (ILayerProperty layerProperty in _layerProperties)
layerProperty.Load();
EnableProperties();
PropertiesInitialized = true;
OnPropertyGroupInitialized();
}
internal void ApplyToEntity()
{
if (!PropertiesInitialized || PropertyGroupEntity == null)
return;
foreach (ILayerProperty layerProperty in LayerProperties)
layerProperty.Save();
PropertyGroupEntity.PropertyGroups.Clear();
foreach (LayerPropertyGroup layerPropertyGroup in LayerPropertyGroups)
{
layerPropertyGroup.ApplyToEntity();
PropertyGroupEntity.PropertyGroups.Add(layerPropertyGroup.PropertyGroupEntity);
}
}
internal void Update(Timeline timeline)
{
foreach (ILayerProperty layerProperty in LayerProperties)
layerProperty.Update(timeline);
foreach (LayerPropertyGroup layerPropertyGroup in LayerPropertyGroups)
layerPropertyGroup.Update(timeline);
}
internal void MoveLayerProperty(ILayerProperty layerProperty, int index)
{
if (!_layerProperties.Contains(layerProperty))
return;
_layerProperties.Remove(layerProperty);
_layerProperties.Insert(index, layerProperty);
}
internal virtual void OnVisibilityChanged()
{
VisibilityChanged?.Invoke(this, EventArgs.Empty);
}
internal virtual void OnLayerPropertyOnCurrentValueSet(LayerPropertyEventArgs e)
{
Parent?.OnLayerPropertyOnCurrentValueSet(e);
LayerPropertyOnCurrentValueSet?.Invoke(this, e);
}
private void InitializeProperty(PropertyInfo propertyInfo, PropertyDescriptionAttribute propertyDescription)
{
// Ensure the description has an identifier and name, if not this is a good point to set it based on the property info
if (string.IsNullOrWhiteSpace(propertyDescription.Identifier))
propertyDescription.Identifier = propertyInfo.Name;
if (string.IsNullOrWhiteSpace(propertyDescription.Name))
propertyDescription.Name = propertyInfo.Name.Humanize();
if (!typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType))
throw new ArtemisPluginException($"Property with PropertyDescription attribute must be of type ILayerProperty: {propertyDescription.Identifier}");
if (Activator.CreateInstance(propertyInfo.PropertyType, true) is not ILayerProperty instance)
throw new ArtemisPluginException($"Failed to create instance of layer property: {propertyDescription.Identifier}");
PropertyEntity entity = GetPropertyEntity(propertyDescription.Identifier, out bool fromStorage);
instance.Initialize(ProfileElement, this, entity, fromStorage, propertyDescription);
propertyInfo.SetValue(this, instance);
_layerProperties.Add(instance);
}
private void InitializeChildGroup(PropertyInfo propertyInfo, PropertyGroupDescriptionAttribute propertyGroupDescription)
{
// Ensure the description has an identifier and name name, if not this is a good point to set it based on the property info
if (string.IsNullOrWhiteSpace(propertyGroupDescription.Identifier))
propertyGroupDescription.Identifier = propertyInfo.Name;
if (string.IsNullOrWhiteSpace(propertyGroupDescription.Name))
propertyGroupDescription.Name = propertyInfo.Name.Humanize();
if (!typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType))
throw new ArtemisPluginException($"Property with PropertyGroupDescription attribute must be of type LayerPropertyGroup: {propertyGroupDescription.Identifier}");
if (!(Activator.CreateInstance(propertyInfo.PropertyType) is LayerPropertyGroup instance))
throw new ArtemisPluginException($"Failed to create instance of layer property group: {propertyGroupDescription.Identifier}");
PropertyGroupEntity entity = GetPropertyGroupEntity(propertyGroupDescription.Identifier);
instance.Initialize(ProfileElement, this, propertyGroupDescription, entity);
propertyInfo.SetValue(this, instance);
_layerPropertyGroups.Add(instance);
}
private PropertyEntity GetPropertyEntity(string identifier, out bool fromStorage)
{
if (PropertyGroupEntity == null)
throw new ArtemisCoreException($"Can't execute {nameof(GetPropertyEntity)} without {nameof(PropertyGroupEntity)} being setup");
PropertyEntity? entity = PropertyGroupEntity.Properties.FirstOrDefault(p => p.Identifier == identifier);
fromStorage = entity != null;
if (entity == null)
{
entity = new PropertyEntity {Identifier = identifier};
PropertyGroupEntity.Properties.Add(entity);
}
return entity;
}
private PropertyGroupEntity GetPropertyGroupEntity(string identifier)
{
if (PropertyGroupEntity == null)
throw new ArtemisCoreException($"Can't execute {nameof(GetPropertyGroupEntity)} without {nameof(PropertyGroupEntity)} being setup");
return PropertyGroupEntity.PropertyGroups.FirstOrDefault(g => g.Identifier == identifier) ?? new PropertyGroupEntity {Identifier = identifier};
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}

View File

@ -1,22 +1,21 @@
using SkiaSharp;
namespace Artemis.Core
{
/// <summary>
/// Represents an ellipse layer shape
/// </summary>
public class EllipseShape : LayerShape
{
internal EllipseShape(Layer layer) : base(layer)
{
}
namespace Artemis.Core;
/// <inheritdoc />
public override void CalculateRenderProperties()
{
SKPath path = new();
path.AddOval(SKRect.Create(Layer.Bounds.Width, Layer.Bounds.Height));
Path = path;
}
/// <summary>
/// Represents an ellipse layer shape
/// </summary>
public class EllipseShape : LayerShape
{
internal EllipseShape(Layer layer) : base(layer)
{
}
/// <inheritdoc />
public override void CalculateRenderProperties()
{
SKPath path = new();
path.AddOval(SKRect.Create(Layer.Bounds.Width, Layer.Bounds.Height));
Path = path;
}
}

View File

@ -1,30 +1,29 @@
using SkiaSharp;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents the shape of a layer
/// </summary>
public abstract class LayerShape
{
/// <summary>
/// Represents the shape of a layer
/// </summary>
public abstract class LayerShape
internal LayerShape(Layer layer)
{
internal LayerShape(Layer layer)
{
Layer = layer;
}
/// <summary>
/// The layer this shape is attached to
/// </summary>
public Layer Layer { get; set; }
/// <summary>
/// Gets a the path outlining the shape
/// </summary>
public SKPath? Path { get; protected set; }
/// <summary>
/// Calculates the <see cref="Path" />
/// </summary>
public abstract void CalculateRenderProperties();
Layer = layer;
}
/// <summary>
/// The layer this shape is attached to
/// </summary>
public Layer Layer { get; set; }
/// <summary>
/// Gets a the path outlining the shape
/// </summary>
public SKPath? Path { get; protected set; }
/// <summary>
/// Calculates the <see cref="Path" />
/// </summary>
public abstract void CalculateRenderProperties();
}

View File

@ -1,22 +1,21 @@
using SkiaSharp;
namespace Artemis.Core
{
/// <summary>
/// Represents a rectangular layer shape
/// </summary>
public class RectangleShape : LayerShape
{
internal RectangleShape(Layer layer) : base(layer)
{
}
namespace Artemis.Core;
/// <inheritdoc />
public override void CalculateRenderProperties()
{
SKPath path = new();
path.AddRect(SKRect.Create(Layer.Bounds.Width, Layer.Bounds.Height));
Path = path;
}
/// <summary>
/// Represents a rectangular layer shape
/// </summary>
public class RectangleShape : LayerShape
{
internal RectangleShape(Layer layer) : base(layer)
{
}
/// <inheritdoc />
public override void CalculateRenderProperties()
{
SKPath path = new();
path.AddRect(SKRect.Create(Layer.Bounds.Width, Layer.Bounds.Height));
Path = path;
}
}

View File

@ -2,60 +2,59 @@
#pragma warning disable 8618
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents the transform properties of a layer
/// </summary>
public class LayerTransformProperties : LayerPropertyGroup
{
/// <summary>
/// Represents the transform properties of a layer
/// The point at which the shape is attached to its position
/// </summary>
public class LayerTransformProperties : LayerPropertyGroup
[PropertyDescription(Description = "The point at which the shape is attached to its position", InputAffix = "%", InputStepSize = 0.001f)]
public SKPointLayerProperty AnchorPoint { get; set; }
/// <summary>
/// The position of the shape
/// </summary>
[PropertyDescription(Description = "The position of the shape", InputAffix = "%", InputStepSize = 0.001f)]
public SKPointLayerProperty Position { get; set; }
/// <summary>
/// The scale of the shape
/// </summary>
[PropertyDescription(Description = "The scale of the shape", InputAffix = "%", MinInputValue = 0f)]
public SKSizeLayerProperty Scale { get; set; }
/// <summary>
/// The rotation of the shape in degree
/// </summary>
[PropertyDescription(Description = "The rotation of the shape in degrees", InputAffix = "°", InputStepSize = 0.5f)]
public FloatLayerProperty Rotation { get; set; }
/// <summary>
/// The opacity of the shape
/// </summary>
[PropertyDescription(Description = "The opacity of the shape", InputAffix = "%", MinInputValue = 0f, MaxInputValue = 100f, InputStepSize = 0.1f)]
public FloatLayerProperty Opacity { get; set; }
/// <inheritdoc />
protected override void PopulateDefaults()
{
/// <summary>
/// The point at which the shape is attached to its position
/// </summary>
[PropertyDescription(Description = "The point at which the shape is attached to its position", InputAffix = "%", InputStepSize = 0.001f)]
public SKPointLayerProperty AnchorPoint { get; set; }
Scale.DefaultValue = new SKSize(100, 100);
AnchorPoint.DefaultValue = new SKPoint(0.5f, 0.5f);
Position.DefaultValue = new SKPoint(0.5f, 0.5f);
Opacity.DefaultValue = 100;
}
/// <summary>
/// The position of the shape
/// </summary>
[PropertyDescription(Description = "The position of the shape", InputAffix = "%", InputStepSize = 0.001f)]
public SKPointLayerProperty Position { get; set; }
/// <inheritdoc />
protected override void EnableProperties()
{
}
/// <summary>
/// The scale of the shape
/// </summary>
[PropertyDescription(Description = "The scale of the shape", InputAffix = "%", MinInputValue = 0f)]
public SKSizeLayerProperty Scale { get; set; }
/// <summary>
/// The rotation of the shape in degree
/// </summary>
[PropertyDescription(Description = "The rotation of the shape in degrees", InputAffix = "°", InputStepSize = 0.5f)]
public FloatLayerProperty Rotation { get; set; }
/// <summary>
/// The opacity of the shape
/// </summary>
[PropertyDescription(Description = "The opacity of the shape", InputAffix = "%", MinInputValue = 0f, MaxInputValue = 100f, InputStepSize = 0.1f)]
public FloatLayerProperty Opacity { get; set; }
/// <inheritdoc />
protected override void PopulateDefaults()
{
Scale.DefaultValue = new SKSize(100, 100);
AnchorPoint.DefaultValue = new SKPoint(0.5f, 0.5f);
Position.DefaultValue = new SKPoint(0.5f, 0.5f);
Opacity.DefaultValue = 100;
}
/// <inheritdoc />
protected override void EnableProperties()
{
}
/// <inheritdoc />
protected override void DisableProperties()
{
}
/// <inheritdoc />
protected override void DisableProperties()
{
}
}

View File

@ -6,301 +6,299 @@ using Artemis.Core.ScriptingProviders;
using Artemis.Storage.Entities.Profile;
using SkiaSharp;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a profile containing folders and layers
/// </summary>
public sealed class Profile : ProfileElement
{
/// <summary>
/// Represents a profile containing folders and layers
/// </summary>
public sealed class Profile : ProfileElement
private readonly object _lock = new();
private readonly ObservableCollection<ScriptConfiguration> _scriptConfigurations;
private readonly ObservableCollection<ProfileScript> _scripts;
private bool _isFreshImport;
private ProfileElement? _lastSelectedProfileElement;
internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!)
{
private readonly object _lock = new();
private bool _isFreshImport;
private ProfileElement? _lastSelectedProfileElement;
private readonly ObservableCollection<ProfileScript> _scripts;
private readonly ObservableCollection<ScriptConfiguration> _scriptConfigurations;
_scripts = new ObservableCollection<ProfileScript>();
_scriptConfigurations = new ObservableCollection<ScriptConfiguration>();
internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!)
{
_scripts = new ObservableCollection<ProfileScript>();
_scriptConfigurations = new ObservableCollection<ScriptConfiguration>();
Configuration = configuration;
Profile = this;
ProfileEntity = profileEntity;
EntityId = profileEntity.Id;
Configuration = configuration;
Profile = this;
ProfileEntity = profileEntity;
EntityId = profileEntity.Id;
Exceptions = new List<Exception>();
Scripts = new ReadOnlyObservableCollection<ProfileScript>(_scripts);
ScriptConfigurations = new ReadOnlyObservableCollection<ScriptConfiguration>(_scriptConfigurations);
Exceptions = new List<Exception>();
Scripts = new ReadOnlyObservableCollection<ProfileScript>(_scripts);
ScriptConfigurations = new ReadOnlyObservableCollection<ScriptConfiguration>(_scriptConfigurations);
Load();
}
Load();
}
/// <summary>
/// Gets the profile configuration of this profile
/// </summary>
public ProfileConfiguration Configuration { get; }
/// <summary>
/// Gets the profile configuration of this profile
/// </summary>
public ProfileConfiguration Configuration { get; }
/// <summary>
/// Gets a collection of all active scripts assigned to this profile
/// </summary>
public ReadOnlyObservableCollection<ProfileScript> Scripts { get; }
/// <summary>
/// Gets a collection of all active scripts assigned to this profile
/// </summary>
public ReadOnlyObservableCollection<ProfileScript> Scripts { get; }
/// <summary>
/// Gets a collection of all script configurations assigned to this profile
/// </summary>
public ReadOnlyObservableCollection<ScriptConfiguration> ScriptConfigurations { get; }
/// <summary>
/// Gets a collection of all script configurations assigned to this profile
/// </summary>
public ReadOnlyObservableCollection<ScriptConfiguration> ScriptConfigurations { get; }
/// <summary>
/// Gets or sets a boolean indicating whether this profile is freshly imported i.e. no changes have been made to it
/// since import
/// <para>
/// Note: As long as this is <see langword="true" />, profile adaption will be performed on load and any surface
/// changes
/// </para>
/// </summary>
public bool IsFreshImport
{
get => _isFreshImport;
set => SetAndNotify(ref _isFreshImport, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether this profile is freshly imported i.e. no changes have been made to it
/// since import
/// <para>
/// Note: As long as this is <see langword="true" />, profile adaption will be performed on load and any surface
/// changes
/// </para>
/// </summary>
public bool IsFreshImport
{
get => _isFreshImport;
set => SetAndNotify(ref _isFreshImport, value);
}
/// <summary>
/// Gets or sets the last selected profile element of this profile
/// </summary>
public ProfileElement? LastSelectedProfileElement
{
get => _lastSelectedProfileElement;
set => SetAndNotify(ref _lastSelectedProfileElement, value);
}
/// <summary>
/// Gets or sets the last selected profile element of this profile
/// </summary>
public ProfileElement? LastSelectedProfileElement
{
get => _lastSelectedProfileElement;
set => SetAndNotify(ref _lastSelectedProfileElement, value);
}
/// <summary>
/// Gets the profile entity this profile uses for persistent storage
/// </summary>
public ProfileEntity ProfileEntity { get; internal set; }
/// <summary>
/// Gets the profile entity this profile uses for persistent storage
/// </summary>
public ProfileEntity ProfileEntity { get; internal set; }
internal List<Exception> Exceptions { get; }
internal List<Exception> Exceptions { get; }
/// <inheritdoc />
public override void Update(double deltaTime)
{
lock (_lock)
{
if (Disposed)
throw new ObjectDisposedException("Profile");
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileUpdating(deltaTime);
foreach (ProfileElement profileElement in Children)
profileElement.Update(deltaTime);
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileUpdated(deltaTime);
}
}
/// <inheritdoc />
public override void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus)
{
lock (_lock)
{
if (Disposed)
throw new ObjectDisposedException("Profile");
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileRendering(canvas, canvas.LocalClipBounds);
foreach (ProfileElement profileElement in Children)
profileElement.Render(canvas, basePosition, editorFocus);
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileRendered(canvas, canvas.LocalClipBounds);
if (!Exceptions.Any())
return;
List<Exception> exceptions = new(Exceptions);
Exceptions.Clear();
throw new AggregateException($"One or more exceptions while rendering profile {Name}", exceptions);
}
}
/// <inheritdoc />
public override void Reset()
{
foreach (ProfileElement child in Children)
child.Reset();
}
/// <summary>
/// Retrieves the root folder of this profile
/// </summary>
/// <returns>The root folder of the profile</returns>
/// <exception cref="ObjectDisposedException"></exception>
public Folder GetRootFolder()
/// <inheritdoc />
public override void Update(double deltaTime)
{
lock (_lock)
{
if (Disposed)
throw new ObjectDisposedException("Profile");
return (Folder) Children.Single();
}
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileUpdating(deltaTime);
/// <inheritdoc />
public override string ToString()
{
return $"[Profile] {nameof(Name)}: {Name}";
}
foreach (ProfileElement profileElement in Children)
profileElement.Update(deltaTime);
/// <summary>
/// Populates all the LEDs on the elements in this profile
/// </summary>
/// <param name="devices">The devices to use while populating LEDs</param>
public void PopulateLeds(IEnumerable<ArtemisDevice> devices)
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileUpdated(deltaTime);
}
}
/// <inheritdoc />
public override void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus)
{
lock (_lock)
{
if (Disposed)
throw new ObjectDisposedException("Profile");
foreach (Layer layer in GetAllLayers())
layer.PopulateLeds(devices);
}
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileRendering(canvas, canvas.LocalClipBounds);
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (!disposing)
foreach (ProfileElement profileElement in Children)
profileElement.Render(canvas, basePosition, editorFocus);
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileRendered(canvas, canvas.LocalClipBounds);
if (!Exceptions.Any())
return;
while (Scripts.Count > 0)
RemoveScript(Scripts[0]);
List<Exception> exceptions = new(Exceptions);
Exceptions.Clear();
throw new AggregateException($"One or more exceptions while rendering profile {Name}", exceptions);
}
}
/// <inheritdoc />
public override void Reset()
{
foreach (ProfileElement child in Children)
child.Reset();
}
/// <summary>
/// Retrieves the root folder of this profile
/// </summary>
/// <returns>The root folder of the profile</returns>
/// <exception cref="ObjectDisposedException"></exception>
public Folder GetRootFolder()
{
if (Disposed)
throw new ObjectDisposedException("Profile");
return (Folder) Children.Single();
}
/// <inheritdoc />
public override string ToString()
{
return $"[Profile] {nameof(Name)}: {Name}";
}
/// <summary>
/// Populates all the LEDs on the elements in this profile
/// </summary>
/// <param name="devices">The devices to use while populating LEDs</param>
public void PopulateLeds(IEnumerable<ArtemisDevice> devices)
{
if (Disposed)
throw new ObjectDisposedException("Profile");
foreach (Layer layer in GetAllLayers())
layer.PopulateLeds(devices);
}
#region Overrides of BreakableModel
/// <inheritdoc />
public override IEnumerable<IBreakableModel> GetBrokenHierarchy()
{
return GetAllRenderElements().SelectMany(folders => folders.GetBrokenHierarchy());
}
#endregion
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (!disposing)
return;
while (Scripts.Count > 0)
RemoveScript(Scripts[0]);
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
ChildrenList.Clear();
Disposed = true;
}
internal override void Load()
{
if (Disposed)
throw new ObjectDisposedException("Profile");
Name = Configuration.Name;
IsFreshImport = ProfileEntity.IsFreshImport;
lock (ChildrenList)
{
// Remove the old profile tree
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
ChildrenList.Clear();
Disposed = true;
}
internal override void Load()
{
if (Disposed)
throw new ObjectDisposedException("Profile");
Name = Configuration.Name;
IsFreshImport = ProfileEntity.IsFreshImport;
lock (ChildrenList)
{
// Remove the old profile tree
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
ChildrenList.Clear();
// Populate the profile starting at the root, the rest is populated recursively
FolderEntity? rootFolder = ProfileEntity.Folders.FirstOrDefault(f => f.ParentId == EntityId);
if (rootFolder == null)
AddChild(new Folder(this, "Root folder"));
else
AddChild(new Folder(this, this, rootFolder));
}
List<RenderProfileElement> renderElements = GetAllRenderElements();
if (ProfileEntity.LastSelectedProfileElement != Guid.Empty)
LastSelectedProfileElement = renderElements.FirstOrDefault(f => f.EntityId == ProfileEntity.LastSelectedProfileElement);
// Populate the profile starting at the root, the rest is populated recursively
FolderEntity? rootFolder = ProfileEntity.Folders.FirstOrDefault(f => f.ParentId == EntityId);
if (rootFolder == null)
AddChild(new Folder(this, "Root folder"));
else
LastSelectedProfileElement = null;
while (_scriptConfigurations.Any())
RemoveScriptConfiguration(_scriptConfigurations[0]);
foreach (ScriptConfiguration scriptConfiguration in ProfileEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e)))
AddScriptConfiguration(scriptConfiguration);
// Load node scripts last since they may rely on the profile structure being in place
foreach (RenderProfileElement renderProfileElement in renderElements)
renderProfileElement.LoadNodeScript();
AddChild(new Folder(this, this, rootFolder));
}
/// <summary>
/// Removes a script configuration from the profile, if the configuration has an active script it is also removed.
/// </summary>
internal void RemoveScriptConfiguration(ScriptConfiguration scriptConfiguration)
List<RenderProfileElement> renderElements = GetAllRenderElements();
if (ProfileEntity.LastSelectedProfileElement != Guid.Empty)
LastSelectedProfileElement = renderElements.FirstOrDefault(f => f.EntityId == ProfileEntity.LastSelectedProfileElement);
else
LastSelectedProfileElement = null;
while (_scriptConfigurations.Any())
RemoveScriptConfiguration(_scriptConfigurations[0]);
foreach (ScriptConfiguration scriptConfiguration in ProfileEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e)))
AddScriptConfiguration(scriptConfiguration);
// Load node scripts last since they may rely on the profile structure being in place
foreach (RenderProfileElement renderProfileElement in renderElements)
renderProfileElement.LoadNodeScript();
}
/// <summary>
/// Removes a script configuration from the profile, if the configuration has an active script it is also removed.
/// </summary>
internal void RemoveScriptConfiguration(ScriptConfiguration scriptConfiguration)
{
if (!_scriptConfigurations.Contains(scriptConfiguration))
return;
Script? script = scriptConfiguration.Script;
if (script != null)
RemoveScript((ProfileScript) script);
_scriptConfigurations.Remove(scriptConfiguration);
}
/// <summary>
/// Adds a script configuration to the profile but does not instantiate it's script.
/// </summary>
internal void AddScriptConfiguration(ScriptConfiguration scriptConfiguration)
{
if (!_scriptConfigurations.Contains(scriptConfiguration))
_scriptConfigurations.Add(scriptConfiguration);
}
/// <summary>
/// Adds a script that has a script configuration belonging to this profile.
/// </summary>
internal void AddScript(ProfileScript script)
{
if (!_scriptConfigurations.Contains(script.ScriptConfiguration))
throw new ArtemisCoreException("Cannot add a script to a profile whose script configuration doesn't belong to the same profile.");
if (!_scripts.Contains(script))
_scripts.Add(script);
}
/// <summary>
/// Removes a script from the profile and disposes it.
/// </summary>
internal void RemoveScript(ProfileScript script)
{
_scripts.Remove(script);
script.Dispose();
}
internal override void Save()
{
if (Disposed)
throw new ObjectDisposedException("Profile");
ProfileEntity.Id = EntityId;
ProfileEntity.Name = Configuration.Name;
ProfileEntity.IsFreshImport = IsFreshImport;
ProfileEntity.LastSelectedProfileElement = LastSelectedProfileElement?.EntityId ?? Guid.Empty;
foreach (ProfileElement profileElement in Children)
profileElement.Save();
ProfileEntity.Folders.Clear();
ProfileEntity.Folders.AddRange(GetAllFolders().Select(f => f.FolderEntity));
ProfileEntity.Layers.Clear();
ProfileEntity.Layers.AddRange(GetAllLayers().Select(f => f.LayerEntity));
ProfileEntity.ScriptConfigurations.Clear();
foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations)
{
if (!_scriptConfigurations.Contains(scriptConfiguration))
return;
Script? script = scriptConfiguration.Script;
if (script != null)
RemoveScript((ProfileScript) script);
_scriptConfigurations.Remove(scriptConfiguration);
scriptConfiguration.Save();
ProfileEntity.ScriptConfigurations.Add(scriptConfiguration.Entity);
}
/// <summary>
/// Adds a script configuration to the profile but does not instantiate it's script.
/// </summary>
internal void AddScriptConfiguration(ScriptConfiguration scriptConfiguration)
{
if (!_scriptConfigurations.Contains(scriptConfiguration))
_scriptConfigurations.Add(scriptConfiguration);
}
/// <summary>
/// Adds a script that has a script configuration belonging to this profile.
/// </summary>
internal void AddScript(ProfileScript script)
{
if (!_scriptConfigurations.Contains(script.ScriptConfiguration))
throw new ArtemisCoreException("Cannot add a script to a profile whose script configuration doesn't belong to the same profile.");
if (!_scripts.Contains(script))
_scripts.Add(script);
}
/// <summary>
/// Removes a script from the profile and disposes it.
/// </summary>
internal void RemoveScript(ProfileScript script)
{
_scripts.Remove(script);
script.Dispose();
}
internal override void Save()
{
if (Disposed)
throw new ObjectDisposedException("Profile");
ProfileEntity.Id = EntityId;
ProfileEntity.Name = Configuration.Name;
ProfileEntity.IsFreshImport = IsFreshImport;
ProfileEntity.LastSelectedProfileElement = LastSelectedProfileElement?.EntityId ?? Guid.Empty;
foreach (ProfileElement profileElement in Children)
profileElement.Save();
ProfileEntity.Folders.Clear();
ProfileEntity.Folders.AddRange(GetAllFolders().Select(f => f.FolderEntity));
ProfileEntity.Layers.Clear();
ProfileEntity.Layers.AddRange(GetAllLayers().Select(f => f.LayerEntity));
ProfileEntity.ScriptConfigurations.Clear();
foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations)
{
scriptConfiguration.Save();
ProfileEntity.ScriptConfigurations.Add(scriptConfiguration.Entity);
}
}
#region Overrides of BreakableModel
/// <inheritdoc />
public override IEnumerable<IBreakableModel> GetBrokenHierarchy()
{
return GetAllRenderElements().SelectMany(folders => folders.GetBrokenHierarchy());
}
#endregion
}
}

View File

@ -3,212 +3,210 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a category containing <see cref="ProfileConfigurations" />
/// </summary>
public class ProfileCategory : CorePropertyChanged, IStorageModel
{
private readonly List<ProfileConfiguration> _profileConfigurations = new();
private bool _isCollapsed;
private bool _isSuspended;
private string _name;
private int _order;
/// <summary>
/// Creates a new instance of the <see cref="ProfileCategory" /> class
/// </summary>
/// <param name="name">The name of the category</param>
internal ProfileCategory(string name)
{
_name = name;
Entity = new ProfileCategoryEntity();
ProfileConfigurations = new ReadOnlyCollection<ProfileConfiguration>(_profileConfigurations);
}
internal ProfileCategory(ProfileCategoryEntity entity)
{
_name = null!;
Entity = entity;
ProfileConfigurations = new ReadOnlyCollection<ProfileConfiguration>(_profileConfigurations);
Load();
}
/// <summary>
/// Gets or sets the name of the profile category
/// </summary>
public string Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// The order in which this category appears in the update loop and sidebar
/// </summary>
public int Order
{
get => _order;
set => SetAndNotify(ref _order, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the category is collapsed or not
/// <para>Note: Has no implications other than inside the UI</para>
/// </summary>
public bool IsCollapsed
{
get => _isCollapsed;
set => SetAndNotify(ref _isCollapsed, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether this category is suspended, disabling all its profiles
/// </summary>
public bool IsSuspended
{
get => _isSuspended;
set => SetAndNotify(ref _isSuspended, value);
}
/// <summary>
/// Gets a read only collection of the profiles inside this category
/// </summary>
public ReadOnlyCollection<ProfileConfiguration> ProfileConfigurations { get; }
/// <summary>
/// Gets the unique ID of this category
/// </summary>
public Guid EntityId => Entity.Id;
internal ProfileCategoryEntity Entity { get; }
/// <summary>
/// Adds a profile configuration to this category
/// </summary>
public void AddProfileConfiguration(ProfileConfiguration configuration, int? targetIndex)
{
// Removing the original will shift every item in the list forwards, keep that in mind with the target index
if (configuration.Category == this && targetIndex != null && targetIndex.Value > _profileConfigurations.IndexOf(configuration))
targetIndex -= 1;
configuration.Category.RemoveProfileConfiguration(configuration);
if (targetIndex != null)
{
targetIndex = Math.Clamp(targetIndex.Value, 0, _profileConfigurations.Count);
_profileConfigurations.Insert(targetIndex.Value, configuration);
}
else
{
_profileConfigurations.Add(configuration);
}
configuration.Category = this;
for (int index = 0; index < _profileConfigurations.Count; index++)
_profileConfigurations[index].Order = index;
OnProfileConfigurationAdded(new ProfileConfigurationEventArgs(configuration));
}
/// <inheritdoc />
public override string ToString()
{
return $"[ProfileCategory] {Order} {nameof(Name)}: {Name}, {nameof(IsSuspended)}: {IsSuspended}";
}
/// <summary>
/// Occurs when a profile configuration is added to this <see cref="ProfileCategory" />
/// </summary>
public event EventHandler<ProfileConfigurationEventArgs>? ProfileConfigurationAdded;
/// <summary>
/// Occurs when a profile configuration is removed from this <see cref="ProfileCategory" />
/// </summary>
public event EventHandler<ProfileConfigurationEventArgs>? ProfileConfigurationRemoved;
/// <summary>
/// Invokes the <see cref="ProfileConfigurationAdded" /> event
/// </summary>
protected virtual void OnProfileConfigurationAdded(ProfileConfigurationEventArgs e)
{
ProfileConfigurationAdded?.Invoke(this, e);
}
/// <summary>
/// Invokes the <see cref="ProfileConfigurationRemoved" /> event
/// </summary>
protected virtual void OnProfileConfigurationRemoved(ProfileConfigurationEventArgs e)
{
ProfileConfigurationRemoved?.Invoke(this, e);
}
internal void RemoveProfileConfiguration(ProfileConfiguration configuration)
{
if (!_profileConfigurations.Remove(configuration))
return;
for (int index = 0; index < _profileConfigurations.Count; index++)
_profileConfigurations[index].Order = index;
OnProfileConfigurationRemoved(new ProfileConfigurationEventArgs(configuration));
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
Name = Entity.Name;
IsCollapsed = Entity.IsCollapsed;
IsSuspended = Entity.IsSuspended;
Order = Entity.Order;
_profileConfigurations.Clear();
foreach (ProfileConfigurationEntity entityProfileConfiguration in Entity.ProfileConfigurations)
_profileConfigurations.Add(new ProfileConfiguration(this, entityProfileConfiguration));
}
/// <inheritdoc />
public void Save()
{
Entity.Name = Name;
Entity.IsCollapsed = IsCollapsed;
Entity.IsSuspended = IsSuspended;
Entity.Order = Order;
Entity.ProfileConfigurations.Clear();
foreach (ProfileConfiguration profileConfiguration in ProfileConfigurations)
{
profileConfiguration.Save();
Entity.ProfileConfigurations.Add(profileConfiguration.Entity);
}
}
#endregion
}
/// <summary>
/// Represents a name of one of the default categories
/// </summary>
public enum DefaultCategoryName
{
/// <summary>
/// Represents a category containing <see cref="ProfileConfigurations" />
/// The category used by profiles tied to games
/// </summary>
public class ProfileCategory : CorePropertyChanged, IStorageModel
{
private readonly List<ProfileConfiguration> _profileConfigurations = new();
private bool _isCollapsed;
private bool _isSuspended;
private string _name;
private int _order;
/// <summary>
/// Creates a new instance of the <see cref="ProfileCategory" /> class
/// </summary>
/// <param name="name">The name of the category</param>
internal ProfileCategory(string name)
{
_name = name;
Entity = new ProfileCategoryEntity();
ProfileConfigurations = new ReadOnlyCollection<ProfileConfiguration>(_profileConfigurations);
}
internal ProfileCategory(ProfileCategoryEntity entity)
{
_name = null!;
Entity = entity;
ProfileConfigurations = new ReadOnlyCollection<ProfileConfiguration>(_profileConfigurations);
Load();
}
/// <summary>
/// Gets or sets the name of the profile category
/// </summary>
public string Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// The order in which this category appears in the update loop and sidebar
/// </summary>
public int Order
{
get => _order;
set => SetAndNotify(ref _order, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the category is collapsed or not
/// <para>Note: Has no implications other than inside the UI</para>
/// </summary>
public bool IsCollapsed
{
get => _isCollapsed;
set => SetAndNotify(ref _isCollapsed, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether this category is suspended, disabling all its profiles
/// </summary>
public bool IsSuspended
{
get => _isSuspended;
set => SetAndNotify(ref _isSuspended, value);
}
/// <summary>
/// Gets a read only collection of the profiles inside this category
/// </summary>
public ReadOnlyCollection<ProfileConfiguration> ProfileConfigurations { get; }
/// <summary>
/// Gets the unique ID of this category
/// </summary>
public Guid EntityId => Entity.Id;
internal ProfileCategoryEntity Entity { get; }
/// <summary>
/// Adds a profile configuration to this category
/// </summary>
public void AddProfileConfiguration(ProfileConfiguration configuration, int? targetIndex)
{
// Removing the original will shift every item in the list forwards, keep that in mind with the target index
if (configuration.Category == this && targetIndex != null && targetIndex.Value > _profileConfigurations.IndexOf(configuration))
targetIndex -= 1;
configuration.Category.RemoveProfileConfiguration(configuration);
if (targetIndex != null)
{
targetIndex = Math.Clamp(targetIndex.Value, 0, _profileConfigurations.Count);
_profileConfigurations.Insert(targetIndex.Value, configuration);
}
else
_profileConfigurations.Add(configuration);
configuration.Category = this;
for (int index = 0; index < _profileConfigurations.Count; index++)
_profileConfigurations[index].Order = index;
OnProfileConfigurationAdded(new ProfileConfigurationEventArgs(configuration));
}
/// <inheritdoc />
public override string ToString()
{
return $"[ProfileCategory] {Order} {nameof(Name)}: {Name}, {nameof(IsSuspended)}: {IsSuspended}";
}
internal void RemoveProfileConfiguration(ProfileConfiguration configuration)
{
if (!_profileConfigurations.Remove(configuration))
return;
for (int index = 0; index < _profileConfigurations.Count; index++)
_profileConfigurations[index].Order = index;
OnProfileConfigurationRemoved(new ProfileConfigurationEventArgs(configuration));
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
Name = Entity.Name;
IsCollapsed = Entity.IsCollapsed;
IsSuspended = Entity.IsSuspended;
Order = Entity.Order;
_profileConfigurations.Clear();
foreach (ProfileConfigurationEntity entityProfileConfiguration in Entity.ProfileConfigurations)
_profileConfigurations.Add(new ProfileConfiguration(this, entityProfileConfiguration));
}
/// <inheritdoc />
public void Save()
{
Entity.Name = Name;
Entity.IsCollapsed = IsCollapsed;
Entity.IsSuspended = IsSuspended;
Entity.Order = Order;
Entity.ProfileConfigurations.Clear();
foreach (ProfileConfiguration profileConfiguration in ProfileConfigurations)
{
profileConfiguration.Save();
Entity.ProfileConfigurations.Add(profileConfiguration.Entity);
}
}
#endregion
#region Events
/// <summary>
/// Occurs when a profile configuration is added to this <see cref="ProfileCategory" />
/// </summary>
public event EventHandler<ProfileConfigurationEventArgs>? ProfileConfigurationAdded;
/// <summary>
/// Occurs when a profile configuration is removed from this <see cref="ProfileCategory" />
/// </summary>
public event EventHandler<ProfileConfigurationEventArgs>? ProfileConfigurationRemoved;
/// <summary>
/// Invokes the <see cref="ProfileConfigurationAdded" /> event
/// </summary>
protected virtual void OnProfileConfigurationAdded(ProfileConfigurationEventArgs e)
{
ProfileConfigurationAdded?.Invoke(this, e);
}
/// <summary>
/// Invokes the <see cref="ProfileConfigurationRemoved" /> event
/// </summary>
protected virtual void OnProfileConfigurationRemoved(ProfileConfigurationEventArgs e)
{
ProfileConfigurationRemoved?.Invoke(this, e);
}
#endregion
}
Games,
/// <summary>
/// Represents a name of one of the default categories
/// The category used by profiles tied to applications
/// </summary>
public enum DefaultCategoryName
{
/// <summary>
/// The category used by profiles tied to games
/// </summary>
Games,
Applications,
/// <summary>
/// The category used by profiles tied to applications
/// </summary>
Applications,
/// <summary>
/// The category used by general profiles
/// </summary>
General
}
/// <summary>
/// The category used by general profiles
/// </summary>
General
}

View File

@ -2,381 +2,379 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text.RegularExpressions;
using SkiaSharp;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents an element of a <see cref="Profile" />
/// </summary>
public abstract class ProfileElement : BreakableModel, IDisposable
{
/// <summary>
/// Represents an element of a <see cref="Profile" />
/// </summary>
public abstract class ProfileElement : BreakableModel, IDisposable
internal readonly List<ProfileElement> ChildrenList;
private Guid _entityId;
private string? _name;
private int _order;
private ProfileElement? _parent;
private Profile _profile;
private bool _suspended;
internal ProfileElement(Profile profile)
{
internal readonly List<ProfileElement> ChildrenList;
private Guid _entityId;
private string? _name;
private int _order;
private ProfileElement? _parent;
private Profile _profile;
private bool _suspended;
internal ProfileElement(Profile profile)
{
_profile = profile;
ChildrenList = new List<ProfileElement>();
Children = new ReadOnlyCollection<ProfileElement>(ChildrenList);
}
/// <summary>
/// Gets the unique ID of this profile element
/// </summary>
public Guid EntityId
{
get => _entityId;
internal set => SetAndNotify(ref _entityId, value);
}
/// <summary>
/// Gets the profile this element belongs to
/// </summary>
public Profile Profile
{
get => _profile;
internal set => SetAndNotify(ref _profile, value);
}
/// <summary>
/// Gets the parent of this element
/// </summary>
public ProfileElement? Parent
{
get => _parent;
internal set => SetAndNotify(ref _parent, value);
}
/// <summary>
/// The element's children
/// </summary>
public ReadOnlyCollection<ProfileElement> Children { get; }
/// <summary>
/// The order in which this element appears in the update loop and editor
/// </summary>
public int Order
{
get => _order;
internal set => SetAndNotify(ref _order, value);
}
/// <summary>
/// The name which appears in the editor
/// </summary>
public string? Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// Gets or sets the suspended state, if suspended the element is skipped in render and update
/// </summary>
public bool Suspended
{
get => _suspended;
set => SetAndNotify(ref _suspended, value);
}
/// <summary>
/// Gets a boolean indicating whether the profile element is disposed
/// </summary>
public bool Disposed { get; protected set; }
#region Overrides of BreakableModel
/// <inheritdoc />
public override string BrokenDisplayName => Name ?? GetType().Name;
#endregion
/// <summary>
/// Updates the element
/// </summary>
/// <param name="deltaTime"></param>
public abstract void Update(double deltaTime);
/// <summary>
/// Renders the element
/// </summary>
/// <param name="canvas">The canvas to render upon.</param>
/// <param name="basePosition">The base position to use to translate relative positions to absolute positions.</param>
/// <param name="editorFocus">An optional element to focus on while rendering (other elements will not render).</param>
public abstract void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus);
/// <summary>
/// Resets the internal state of the element
/// </summary>
public abstract void Reset();
/// <inheritdoc />
public override string ToString()
{
return $"{nameof(EntityId)}: {EntityId}, {nameof(Order)}: {Order}, {nameof(Name)}: {Name}";
}
/// <summary>
/// Occurs when a child was added to the <see cref="Children" /> list
/// </summary>
public event EventHandler<ProfileElementEventArgs>? ChildAdded;
/// <summary>
/// Occurs when a child was removed from the <see cref="Children" /> list
/// </summary>
public event EventHandler<ProfileElementEventArgs>? ChildRemoved;
/// <summary>
/// Occurs when a child was added to the <see cref="Children" /> list of this element or any of it's descendents.
/// </summary>
public event EventHandler<ProfileElementEventArgs>? DescendentAdded;
/// <summary>
/// Occurs when a child was removed from the <see cref="Children" /> list of this element or any of it's descendents.
/// </summary>
public event EventHandler<ProfileElementEventArgs>? DescendentRemoved;
/// <summary>
/// Invokes the <see cref="ChildAdded" /> event
/// </summary>
protected virtual void OnChildAdded(ProfileElement child)
{
ChildAdded?.Invoke(this, new ProfileElementEventArgs(child));
}
/// <summary>
/// Invokes the <see cref="ChildRemoved" /> event
/// </summary>
protected virtual void OnChildRemoved(ProfileElement child)
{
ChildRemoved?.Invoke(this, new ProfileElementEventArgs(child));
}
/// <summary>
/// Invokes the <see cref="DescendentAdded" /> event
/// </summary>
protected virtual void OnDescendentAdded(ProfileElement child)
{
DescendentAdded?.Invoke(this, new ProfileElementEventArgs(child));
Parent?.OnDescendentAdded(child);
}
/// <summary>
/// Invokes the <see cref="DescendentRemoved" /> event
/// </summary>
protected virtual void OnDescendentRemoved(ProfileElement child)
{
DescendentRemoved?.Invoke(this, new ProfileElementEventArgs(child));
Parent?.OnDescendentRemoved(child);
}
/// <summary>
/// Disposes the profile element
/// </summary>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#region Hierarchy
/// <summary>
/// Adds a profile element to the <see cref="Children" /> collection, optionally at the given position (0-based)
/// </summary>
/// <param name="child">The profile element to add</param>
/// <param name="order">The order where to place the child (0-based), defaults to the end of the collection</param>
public virtual void AddChild(ProfileElement child, int? order = null)
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
ProfileElement? current = this;
while (current != null)
{
if (ReferenceEquals(child, this))
throw new ArtemisCoreException("Cannot make an element a child of itself");
current = current.Parent;
}
lock (ChildrenList)
{
if (ChildrenList.Contains(child))
return;
// Add to the end of the list
if (order == null)
{
ChildrenList.Add(child);
}
// Insert at the given index
else
{
if (order < 0)
order = 0;
if (order > ChildrenList.Count)
order = ChildrenList.Count;
ChildrenList.Insert(order.Value, child);
}
child.Parent = this;
StreamlineOrder();
}
OnChildAdded(child);
OnDescendentAdded(child);
}
/// <summary>
/// Removes a profile element from the <see cref="Children" /> collection
/// </summary>
/// <param name="child">The profile element to remove</param>
public virtual void RemoveChild(ProfileElement child)
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
lock (ChildrenList)
{
ChildrenList.Remove(child);
StreamlineOrder();
child.Parent = null;
}
OnChildRemoved(child);
OnDescendentRemoved(child);
}
private void StreamlineOrder()
{
for (int index = 0; index < ChildrenList.Count; index++)
ChildrenList[index].Order = index + 1;
}
/// <summary>
/// Returns a flattened list of all child render elements
/// </summary>
/// <returns></returns>
public List<RenderProfileElement> GetAllRenderElements()
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
List<RenderProfileElement> elements = new();
foreach (RenderProfileElement childElement in Children.Where(c => c is RenderProfileElement).Cast<RenderProfileElement>())
{
// Add all folders in this element
elements.Add(childElement);
// Add all folders in folders inside this element
elements.AddRange(childElement.GetAllRenderElements());
}
return elements;
}
/// <summary>
/// Returns a flattened list of all child folders
/// </summary>
/// <returns></returns>
public List<Folder> GetAllFolders()
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
List<Folder> folders = new();
foreach (Folder childFolder in Children.Where(c => c is Folder).Cast<Folder>())
{
// Add all folders in this element
folders.Add(childFolder);
// Add all folders in folders inside this element
folders.AddRange(childFolder.GetAllFolders());
}
return folders;
}
/// <summary>
/// Returns a flattened list of all child layers
/// </summary>
/// <returns></returns>
public List<Layer> GetAllLayers()
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
List<Layer> layers = new();
// Add all layers in this element
layers.AddRange(Children.Where(c => c is Layer).Cast<Layer>());
// Add all layers in folders inside this element
foreach (Folder childFolder in Children.Where(c => c is Folder).Cast<Folder>())
layers.AddRange(childFolder.GetAllLayers());
return layers;
}
/// <summary>
/// Returns a name for a new layer according to any other layers with a default name similar to creating new folders in
/// Explorer
/// </summary>
/// <returns>The resulting name i.e. <c>New layer</c> or <c>New layer (2)</c></returns>
public string GetNewLayerName(string baseName = "New layer")
{
if (!Children.Any(c => c is Layer && c.Name == baseName))
return baseName;
int current = 2;
while (true)
{
if (Children.Where(c => c is Layer).All(c => c.Name != $"{baseName} ({current})"))
return $"{baseName} ({current})";
current++;
}
}
/// <summary>
/// Returns a name for a new folder according to any other folders with a default name similar to creating new folders
/// in Explorer
/// </summary>
/// <returns>The resulting name i.e. <c>New folder</c> or <c>New folder (2)</c></returns>
public string GetNewFolderName(string baseName = "New folder")
{
if (!Children.Any(c => c is Folder && c.Name == baseName))
return baseName;
int current = 2;
while (true)
{
if (Children.Where(c => c is Folder).All(c => c.Name != $"{baseName} ({current})"))
return $"{baseName} ({current})";
current++;
}
}
#endregion
#region Storage
internal abstract void Load();
internal abstract void Save();
#endregion
_profile = profile;
ChildrenList = new List<ProfileElement>();
Children = new ReadOnlyCollection<ProfileElement>(ChildrenList);
}
/// <summary>
/// Gets the unique ID of this profile element
/// </summary>
public Guid EntityId
{
get => _entityId;
internal set => SetAndNotify(ref _entityId, value);
}
/// <summary>
/// Gets the profile this element belongs to
/// </summary>
public Profile Profile
{
get => _profile;
internal set => SetAndNotify(ref _profile, value);
}
/// <summary>
/// Gets the parent of this element
/// </summary>
public ProfileElement? Parent
{
get => _parent;
internal set => SetAndNotify(ref _parent, value);
}
/// <summary>
/// The element's children
/// </summary>
public ReadOnlyCollection<ProfileElement> Children { get; }
/// <summary>
/// The order in which this element appears in the update loop and editor
/// </summary>
public int Order
{
get => _order;
internal set => SetAndNotify(ref _order, value);
}
/// <summary>
/// The name which appears in the editor
/// </summary>
public string? Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// Gets or sets the suspended state, if suspended the element is skipped in render and update
/// </summary>
public bool Suspended
{
get => _suspended;
set => SetAndNotify(ref _suspended, value);
}
/// <summary>
/// Gets a boolean indicating whether the profile element is disposed
/// </summary>
public bool Disposed { get; protected set; }
#region Overrides of BreakableModel
/// <inheritdoc />
public override string BrokenDisplayName => Name ?? GetType().Name;
#endregion
/// <summary>
/// Updates the element
/// </summary>
/// <param name="deltaTime"></param>
public abstract void Update(double deltaTime);
/// <summary>
/// Renders the element
/// </summary>
/// <param name="canvas">The canvas to render upon.</param>
/// <param name="basePosition">The base position to use to translate relative positions to absolute positions.</param>
/// <param name="editorFocus">An optional element to focus on while rendering (other elements will not render).</param>
public abstract void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus);
/// <summary>
/// Resets the internal state of the element
/// </summary>
public abstract void Reset();
/// <inheritdoc />
public override string ToString()
{
return $"{nameof(EntityId)}: {EntityId}, {nameof(Order)}: {Order}, {nameof(Name)}: {Name}";
}
/// <summary>
/// Occurs when a child was added to the <see cref="Children" /> list
/// </summary>
public event EventHandler<ProfileElementEventArgs>? ChildAdded;
/// <summary>
/// Occurs when a child was removed from the <see cref="Children" /> list
/// </summary>
public event EventHandler<ProfileElementEventArgs>? ChildRemoved;
/// <summary>
/// Occurs when a child was added to the <see cref="Children" /> list of this element or any of it's descendents.
/// </summary>
public event EventHandler<ProfileElementEventArgs>? DescendentAdded;
/// <summary>
/// Occurs when a child was removed from the <see cref="Children" /> list of this element or any of it's descendents.
/// </summary>
public event EventHandler<ProfileElementEventArgs>? DescendentRemoved;
/// <summary>
/// Invokes the <see cref="ChildAdded" /> event
/// </summary>
protected virtual void OnChildAdded(ProfileElement child)
{
ChildAdded?.Invoke(this, new ProfileElementEventArgs(child));
}
/// <summary>
/// Invokes the <see cref="ChildRemoved" /> event
/// </summary>
protected virtual void OnChildRemoved(ProfileElement child)
{
ChildRemoved?.Invoke(this, new ProfileElementEventArgs(child));
}
/// <summary>
/// Invokes the <see cref="DescendentAdded" /> event
/// </summary>
protected virtual void OnDescendentAdded(ProfileElement child)
{
DescendentAdded?.Invoke(this, new ProfileElementEventArgs(child));
Parent?.OnDescendentAdded(child);
}
/// <summary>
/// Invokes the <see cref="DescendentRemoved" /> event
/// </summary>
protected virtual void OnDescendentRemoved(ProfileElement child)
{
DescendentRemoved?.Invoke(this, new ProfileElementEventArgs(child));
Parent?.OnDescendentRemoved(child);
}
/// <summary>
/// Disposes the profile element
/// </summary>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#region Hierarchy
/// <summary>
/// Adds a profile element to the <see cref="Children" /> collection, optionally at the given position (0-based)
/// </summary>
/// <param name="child">The profile element to add</param>
/// <param name="order">The order where to place the child (0-based), defaults to the end of the collection</param>
public virtual void AddChild(ProfileElement child, int? order = null)
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
ProfileElement? current = this;
while (current != null)
{
if (ReferenceEquals(child, this))
throw new ArtemisCoreException("Cannot make an element a child of itself");
current = current.Parent;
}
lock (ChildrenList)
{
if (ChildrenList.Contains(child))
return;
// Add to the end of the list
if (order == null)
{
ChildrenList.Add(child);
}
// Insert at the given index
else
{
if (order < 0)
order = 0;
if (order > ChildrenList.Count)
order = ChildrenList.Count;
ChildrenList.Insert(order.Value, child);
}
child.Parent = this;
StreamlineOrder();
}
OnChildAdded(child);
OnDescendentAdded(child);
}
/// <summary>
/// Removes a profile element from the <see cref="Children" /> collection
/// </summary>
/// <param name="child">The profile element to remove</param>
public virtual void RemoveChild(ProfileElement child)
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
lock (ChildrenList)
{
ChildrenList.Remove(child);
StreamlineOrder();
child.Parent = null;
}
OnChildRemoved(child);
OnDescendentRemoved(child);
}
private void StreamlineOrder()
{
for (int index = 0; index < ChildrenList.Count; index++)
ChildrenList[index].Order = index + 1;
}
/// <summary>
/// Returns a flattened list of all child render elements
/// </summary>
/// <returns></returns>
public List<RenderProfileElement> GetAllRenderElements()
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
List<RenderProfileElement> elements = new();
foreach (RenderProfileElement childElement in Children.Where(c => c is RenderProfileElement).Cast<RenderProfileElement>())
{
// Add all folders in this element
elements.Add(childElement);
// Add all folders in folders inside this element
elements.AddRange(childElement.GetAllRenderElements());
}
return elements;
}
/// <summary>
/// Returns a flattened list of all child folders
/// </summary>
/// <returns></returns>
public List<Folder> GetAllFolders()
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
List<Folder> folders = new();
foreach (Folder childFolder in Children.Where(c => c is Folder).Cast<Folder>())
{
// Add all folders in this element
folders.Add(childFolder);
// Add all folders in folders inside this element
folders.AddRange(childFolder.GetAllFolders());
}
return folders;
}
/// <summary>
/// Returns a flattened list of all child layers
/// </summary>
/// <returns></returns>
public List<Layer> GetAllLayers()
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
List<Layer> layers = new();
// Add all layers in this element
layers.AddRange(Children.Where(c => c is Layer).Cast<Layer>());
// Add all layers in folders inside this element
foreach (Folder childFolder in Children.Where(c => c is Folder).Cast<Folder>())
layers.AddRange(childFolder.GetAllLayers());
return layers;
}
/// <summary>
/// Returns a name for a new layer according to any other layers with a default name similar to creating new folders in
/// Explorer
/// </summary>
/// <returns>The resulting name i.e. <c>New layer</c> or <c>New layer (2)</c></returns>
public string GetNewLayerName(string baseName = "New layer")
{
if (!Children.Any(c => c is Layer && c.Name == baseName))
return baseName;
int current = 2;
while (true)
{
if (Children.Where(c => c is Layer).All(c => c.Name != $"{baseName} ({current})"))
return $"{baseName} ({current})";
current++;
}
}
/// <summary>
/// Returns a name for a new folder according to any other folders with a default name similar to creating new folders
/// in Explorer
/// </summary>
/// <returns>The resulting name i.e. <c>New folder</c> or <c>New folder (2)</c></returns>
public string GetNewFolderName(string baseName = "New folder")
{
if (!Children.Any(c => c is Folder && c.Name == baseName))
return baseName;
int current = 2;
while (true)
{
if (Children.Where(c => c is Folder).All(c => c.Name != $"{baseName} ({current})"))
return $"{baseName} ({current})";
current++;
}
}
#endregion
#region Storage
internal abstract void Load();
internal abstract void Save();
#endregion
}

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