diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index 69208acb3..15f337748 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -8,142 +8,148 @@ using Artemis.Core.Services.Core; using Artemis.Core.SkiaSharp; using Newtonsoft.Json; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// A few useful constant values +/// +public static class Constants { /// - /// A few useful constant values + /// The Artemis.Core assembly /// - public static class Constants + public static readonly Assembly CoreAssembly = typeof(Constants).Assembly; + + /// + /// The full path to the Artemis application folder + /// + public static readonly string ApplicationFolder = Path.GetDirectoryName(typeof(Constants).Assembly.Location)!; + + /// + /// The full path to the Artemis executable + /// + public static readonly string ExecutablePath = Utilities.GetCurrentLocation(); + + /// + /// The base path for Artemis application data folder + /// + public static readonly string BaseFolder = Environment.GetFolderPath(OperatingSystem.IsWindows() + ? Environment.SpecialFolder.CommonApplicationData + : Environment.SpecialFolder.LocalApplicationData); + + /// + /// The full path to the Artemis data folder + /// + public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis"); + + /// + /// The full path to the Artemis logs folder + /// + public static readonly string LogsFolder = Path.Combine(DataFolder, "Logs"); + + /// + /// The full path to the Artemis plugins folder + /// + public static readonly string PluginsFolder = Path.Combine(DataFolder, "Plugins"); + + /// + /// The full path to the Artemis user layouts folder + /// + public static readonly string LayoutsFolder = Path.Combine(DataFolder, "User Layouts"); + + /// + /// The current API version for plugins + /// + public static readonly Version PluginApi = new(1, 0); + + /// + /// The plugin info used by core components of Artemis + /// + public static readonly PluginInfo CorePluginInfo = new() { - /// - /// The Artemis.Core assembly - /// - public static readonly Assembly CoreAssembly = typeof(Constants).Assembly; + Guid = Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff"), Name = "Artemis Core", Version = new Version(2, 0) + }; - /// - /// The full path to the Artemis application folder - /// - public static readonly string ApplicationFolder = Path.GetDirectoryName(typeof(Constants).Assembly.Location)!; - - /// - /// The full path to the Artemis executable - /// - public static readonly string ExecutablePath = Utilities.GetCurrentLocation(); - - /// - /// The base path for Artemis application data folder - /// - public static readonly string BaseFolder = Environment.GetFolderPath(OperatingSystem.IsWindows() ? Environment.SpecialFolder.CommonApplicationData : Environment.SpecialFolder.LocalApplicationData); - - /// - /// The full path to the Artemis data folder - /// - public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis"); - - /// - /// The full path to the Artemis logs folder - /// - public static readonly string LogsFolder = Path.Combine(DataFolder, "Logs"); - - /// - /// The full path to the Artemis plugins folder - /// - public static readonly string PluginsFolder = Path.Combine(DataFolder, "Plugins"); - - /// - /// The full path to the Artemis user layouts folder - /// - public static readonly string LayoutsFolder = Path.Combine(DataFolder, "User Layouts"); - - /// - /// The plugin info used by core components of Artemis - /// - public static readonly PluginInfo CorePluginInfo = new() + /// + /// The build information related to the currently running Artemis build + /// Information is retrieved from buildinfo.json + /// + public static readonly BuildInfo BuildInfo = File.Exists(Path.Combine(ApplicationFolder, "buildinfo.json")) + ? JsonConvert.DeserializeObject(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" }; - /// - /// The build information related to the currently running Artemis build - /// Information is retrieved from buildinfo.json - /// - public static readonly BuildInfo BuildInfo = File.Exists(Path.Combine(ApplicationFolder, "buildinfo.json")) - ? JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(ApplicationFolder, "buildinfo.json")))! - : new BuildInfo - { - IsLocalBuild = true, - BuildId = 1337, - BuildNumber = 1337, - SourceBranch = "local", - SourceVersion = "local" - }; + /// + /// The plugin used by core components of Artemis + /// + public static readonly Plugin CorePlugin = new(CorePluginInfo, new DirectoryInfo(ApplicationFolder), null); - /// - /// The plugin used by core components of Artemis - /// - 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 {new SKColorConverter(), new NumericJsonConverter(), new ForgivingIntConverter()} + }; - internal static JsonSerializerSettings JsonConvertSettings = new() - { - Converters = new List {new SKColorConverter(), new NumericJsonConverter(), new ForgivingIntConverter()} - }; + internal static JsonSerializerSettings JsonConvertTypedSettings = new() + { + TypeNameHandling = TypeNameHandling.All, + Converters = new List {new SKColorConverter(), new NumericJsonConverter(), new ForgivingIntConverter()} + }; - internal static JsonSerializerSettings JsonConvertTypedSettings = new() - { - TypeNameHandling = TypeNameHandling.All, - Converters = new List {new SKColorConverter(), new NumericJsonConverter(), new ForgivingIntConverter()} - }; + /// + /// A read-only collection containing all primitive numeric types + /// + public static IReadOnlyCollection NumberTypes = new List + { + typeof(sbyte), + typeof(byte), + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong), + typeof(float), + typeof(double), + typeof(decimal) + }; - /// - /// A read-only collection containing all primitive numeric types - /// - public static IReadOnlyCollection NumberTypes = new List - { - typeof(sbyte), - typeof(byte), - typeof(short), - typeof(ushort), - typeof(int), - typeof(uint), - typeof(long), - typeof(ulong), - typeof(float), - typeof(double), - typeof(decimal) - }; + /// + /// A read-only collection containing all primitive integral numeric types + /// + public static IReadOnlyCollection IntegralNumberTypes = new List + { + typeof(sbyte), + typeof(byte), + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong) + }; - /// - /// A read-only collection containing all primitive integral numeric types - /// - public static IReadOnlyCollection IntegralNumberTypes = new List - { - typeof(sbyte), - typeof(byte), - typeof(short), - typeof(ushort), - typeof(int), - typeof(uint), - typeof(long), - typeof(ulong) - }; + /// + /// A read-only collection containing all primitive floating-point numeric types + /// + public static IReadOnlyCollection FloatNumberTypes = new List + { + typeof(float), + typeof(double), + typeof(decimal) + }; - /// - /// A read-only collection containing all primitive floating-point numeric types - /// - public static IReadOnlyCollection FloatNumberTypes = new List - { - typeof(float), - typeof(double), - typeof(decimal) - }; - - /// - /// Gets the graphics context to be used for rendering by SkiaSharp. Can be set via - /// . - /// - public static IManagedGraphicsContext? ManagedGraphicsContext { get; internal set; } - } + /// + /// Gets the graphics context to be used for rendering by SkiaSharp. Can be set via + /// . + /// + public static IManagedGraphicsContext? ManagedGraphicsContext { get; internal set; } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Properties/BoolLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/BoolLayerProperty.cs index ccaecf8b8..9b9ba3f10 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/BoolLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/BoolLayerProperty.cs @@ -1,31 +1,30 @@ -namespace Artemis.Core +namespace Artemis.Core; + +/// +public class BoolLayerProperty : LayerProperty { - /// - public class BoolLayerProperty : LayerProperty + internal BoolLayerProperty() { - internal BoolLayerProperty() - { - } + } - /// - protected override void OnInitialize() - { - KeyframesSupported = false; - DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value"); - } + /// + /// Implicitly converts an to a + /// + public static implicit operator bool(BoolLayerProperty p) + { + return p.CurrentValue; + } - /// - /// Implicitly converts an to a - /// - public static implicit operator bool(BoolLayerProperty p) - { - return p.CurrentValue; - } + /// + protected override void OnInitialize() + { + KeyframesSupported = false; + DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value"); + } - /// - protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) - { - throw new ArtemisCoreException("Boolean properties do not support keyframes."); - } + /// + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) + { + throw new ArtemisCoreException("Boolean properties do not support keyframes."); } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Properties/EnumLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/EnumLayerProperty.cs index ea7962b7a..482e01487 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/EnumLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/EnumLayerProperty.cs @@ -1,35 +1,34 @@ using System; -namespace Artemis.Core +namespace Artemis.Core; + +/// +public class EnumLayerProperty : LayerProperty where T : Enum { - /// - public class EnumLayerProperty : LayerProperty where T : Enum + internal EnumLayerProperty() { - internal EnumLayerProperty() - { - KeyframesSupported = false; - } + KeyframesSupported = false; + } - /// - /// Implicitly converts an to a - /// - public static implicit operator T(EnumLayerProperty p) - { - return p.CurrentValue; - } + /// + /// Implicitly converts an to a + /// + public static implicit operator T(EnumLayerProperty p) + { + return p.CurrentValue; + } - /// - /// Implicitly converts an to an - /// - public static implicit operator int(EnumLayerProperty p) - { - return Convert.ToInt32(p.CurrentValue); - } + /// + /// Implicitly converts an to an + /// + public static implicit operator int(EnumLayerProperty p) + { + return Convert.ToInt32(p.CurrentValue); + } - /// - protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) - { - throw new ArtemisCoreException("Enum properties do not support keyframes."); - } + /// + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) + { + throw new ArtemisCoreException("Enum properties do not support keyframes."); } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Properties/FloatLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/FloatLayerProperty.cs index 2aec47126..f4076b2b5 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/FloatLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/FloatLayerProperty.cs @@ -1,39 +1,38 @@ -namespace Artemis.Core +namespace Artemis.Core; + +/// +public class FloatLayerProperty : LayerProperty { - /// - public class FloatLayerProperty : LayerProperty + internal FloatLayerProperty() { - internal FloatLayerProperty() - { - } + } - /// - protected override void OnInitialize() - { - DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value"); - } + /// + /// Implicitly converts an to a + /// + public static implicit operator float(FloatLayerProperty p) + { + return p.CurrentValue; + } - /// - /// Implicitly converts an to a - /// - public static implicit operator float(FloatLayerProperty p) - { - return p.CurrentValue; - } + /// + /// Implicitly converts an to a + /// + public static implicit operator double(FloatLayerProperty p) + { + return p.CurrentValue; + } - /// - /// Implicitly converts an to a - /// - public static implicit operator double(FloatLayerProperty p) - { - return p.CurrentValue; - } + /// + protected override void OnInitialize() + { + DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value"); + } - /// - protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) - { - float diff = NextKeyframe!.Value - CurrentKeyframe!.Value; - CurrentValue = CurrentKeyframe!.Value + diff * keyframeProgressEased; - } + /// + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) + { + float diff = NextKeyframe!.Value - CurrentKeyframe!.Value; + CurrentValue = CurrentKeyframe!.Value + diff * keyframeProgressEased; } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Properties/FloatRangeLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/FloatRangeLayerProperty.cs index 56408b59a..a6658537f 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/FloatRangeLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/FloatRangeLayerProperty.cs @@ -1,24 +1,23 @@ -namespace Artemis.Core +namespace Artemis.Core; + +/// +public class FloatRangeLayerProperty : LayerProperty { /// - public class FloatRangeLayerProperty : LayerProperty + protected override void OnInitialize() { - /// - 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"); + } - /// - 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) - ); - } + /// + 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 + ); } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Properties/IntLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/IntLayerProperty.cs index 03681df72..1114c8267 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/IntLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/IntLayerProperty.cs @@ -1,49 +1,48 @@ using System; -namespace Artemis.Core +namespace Artemis.Core; + +/// +public class IntLayerProperty : LayerProperty { - /// - public class IntLayerProperty : LayerProperty + internal IntLayerProperty() { - internal IntLayerProperty() - { - } + } - /// - protected override void OnInitialize() - { - DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value"); - } + /// + /// Implicitly converts an to an + /// + public static implicit operator int(IntLayerProperty p) + { + return p.CurrentValue; + } - /// - /// Implicitly converts an to an - /// - public static implicit operator int(IntLayerProperty p) - { - return p.CurrentValue; - } + /// + /// Implicitly converts an to a + /// + public static implicit operator float(IntLayerProperty p) + { + return p.CurrentValue; + } - /// - /// Implicitly converts an to a - /// - public static implicit operator float(IntLayerProperty p) - { - return p.CurrentValue; - } + /// + /// Implicitly converts an to a + /// + public static implicit operator double(IntLayerProperty p) + { + return p.CurrentValue; + } - /// - /// Implicitly converts an to a - /// - public static implicit operator double(IntLayerProperty p) - { - return p.CurrentValue; - } + /// + protected override void OnInitialize() + { + DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value"); + } - /// - protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) - { - int diff = NextKeyframe!.Value - CurrentKeyframe!.Value; - CurrentValue = (int) Math.Round(CurrentKeyframe!.Value + diff * keyframeProgressEased, MidpointRounding.AwayFromZero); - } + /// + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) + { + int diff = NextKeyframe!.Value - CurrentKeyframe!.Value; + CurrentValue = (int) Math.Round(CurrentKeyframe!.Value + diff * keyframeProgressEased, MidpointRounding.AwayFromZero); } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Properties/IntRangeLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/IntRangeLayerProperty.cs index 481c8f721..167d4786e 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/IntRangeLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/IntRangeLayerProperty.cs @@ -1,24 +1,23 @@ -namespace Artemis.Core +namespace Artemis.Core; + +/// +public class IntRangeLayerProperty : LayerProperty { /// - public class IntRangeLayerProperty : LayerProperty + protected override void OnInitialize() { - /// - 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"); + } - /// - 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) - ); - } + /// + 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) + ); } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Properties/LayerBrushReferenceLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/LayerBrushReferenceLayerProperty.cs index c3341ee32..717e4da27 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/LayerBrushReferenceLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/LayerBrushReferenceLayerProperty.cs @@ -1,27 +1,26 @@ -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// A special layer property used to configure the selected layer brush +/// +public class LayerBrushReferenceLayerProperty : LayerProperty { - /// - /// A special layer property used to configure the selected layer brush - /// - public class LayerBrushReferenceLayerProperty : LayerProperty + internal LayerBrushReferenceLayerProperty() { - internal LayerBrushReferenceLayerProperty() - { - KeyframesSupported = false; - } + KeyframesSupported = false; + } - /// - /// Implicitly converts an to an - /// - public static implicit operator LayerBrushReference?(LayerBrushReferenceLayerProperty p) - { - return p.CurrentValue; - } + /// + /// Implicitly converts an to an + /// + public static implicit operator LayerBrushReference?(LayerBrushReferenceLayerProperty p) + { + return p.CurrentValue; + } - /// - protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) - { - throw new ArtemisCoreException("Layer brush references do not support keyframes."); - } + /// + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) + { + throw new ArtemisCoreException("Layer brush references do not support keyframes."); } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Properties/SKColorLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/SKColorLayerProperty.cs index 38caad133..b0d1a27bd 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/SKColorLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/SKColorLayerProperty.cs @@ -1,34 +1,33 @@ using SkiaSharp; -namespace Artemis.Core +namespace Artemis.Core; + +/// +public class SKColorLayerProperty : LayerProperty { - /// - public class SKColorLayerProperty : LayerProperty + internal SKColorLayerProperty() { - internal SKColorLayerProperty() - { - } + } - /// - protected override void OnInitialize() - { - DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value"); - } + /// + /// Implicitly converts an to an ¶ + /// + /// + /// + public static implicit operator SKColor(SKColorLayerProperty p) + { + return p.CurrentValue; + } - /// - /// Implicitly converts an to an ¶ - /// - /// - /// - public static implicit operator SKColor(SKColorLayerProperty p) - { - return p.CurrentValue; - } + /// + protected override void OnInitialize() + { + DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value"); + } - /// - protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) - { - CurrentValue = CurrentKeyframe!.Value.Interpolate(NextKeyframe!.Value, keyframeProgressEased); - } + /// + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) + { + CurrentValue = CurrentKeyframe!.Value.Interpolate(NextKeyframe!.Value, keyframeProgressEased); } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Properties/SKPointLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/SKPointLayerProperty.cs index df9517eeb..94e24b373 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/SKPointLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/SKPointLayerProperty.cs @@ -1,35 +1,34 @@ using SkiaSharp; -namespace Artemis.Core +namespace Artemis.Core; + +/// +public class SKPointLayerProperty : LayerProperty { - /// - public class SKPointLayerProperty : LayerProperty + internal SKPointLayerProperty() { - internal SKPointLayerProperty() - { - } + } - /// - 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"); - } + /// + /// Implicitly converts an to an + /// + public static implicit operator SKPoint(SKPointLayerProperty p) + { + return p.CurrentValue; + } - /// - /// Implicitly converts an to an - /// - public static implicit operator SKPoint(SKPointLayerProperty p) - { - return p.CurrentValue; - } + /// + 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"); + } - /// - 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); - } + /// + 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); } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Properties/SKSizeLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/SKSizeLayerProperty.cs index e8344e0c9..402f4a320 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/SKSizeLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/SKSizeLayerProperty.cs @@ -1,35 +1,34 @@ using SkiaSharp; -namespace Artemis.Core +namespace Artemis.Core; + +/// +public class SKSizeLayerProperty : LayerProperty { - /// - public class SKSizeLayerProperty : LayerProperty + internal SKSizeLayerProperty() { - internal SKSizeLayerProperty() - { - } + } - /// - 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"); - } + /// + /// Implicitly converts an to an + /// + public static implicit operator SKSize(SKSizeLayerProperty p) + { + return p.CurrentValue; + } - /// - /// Implicitly converts an to an - /// - public static implicit operator SKSize(SKSizeLayerProperty p) - { - return p.CurrentValue; - } + /// + 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"); + } - /// - 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); - } + /// + 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); } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/DataModelPathEventArgs.cs b/src/Artemis.Core/Events/DataModelPathEventArgs.cs index 6e3da98ed..da1a8cc95 100644 --- a/src/Artemis.Core/Events/DataModelPathEventArgs.cs +++ b/src/Artemis.Core/Events/DataModelPathEventArgs.cs @@ -1,20 +1,19 @@ using System; -namespace Artemis.Core -{ - /// - /// Provides data about data model path related events - /// - public class DataModelPathEventArgs : EventArgs - { - internal DataModelPathEventArgs(DataModelPath dataModelPath) - { - DataModelPath = dataModelPath; - } +namespace Artemis.Core; - /// - /// Gets the data model path this event is related to - /// - public DataModelPath DataModelPath { get; } +/// +/// Provides data about data model path related events +/// +public class DataModelPathEventArgs : EventArgs +{ + internal DataModelPathEventArgs(DataModelPath dataModelPath) + { + DataModelPath = dataModelPath; } + + /// + /// Gets the data model path this event is related to + /// + public DataModelPath DataModelPath { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/DeviceEventArgs.cs b/src/Artemis.Core/Events/DeviceEventArgs.cs index 3eaf045bf..4d581763e 100644 --- a/src/Artemis.Core/Events/DeviceEventArgs.cs +++ b/src/Artemis.Core/Events/DeviceEventArgs.cs @@ -1,20 +1,19 @@ using System; -namespace Artemis.Core -{ - /// - /// Provides data about device related events - /// - public class DeviceEventArgs : EventArgs - { - internal DeviceEventArgs(ArtemisDevice device) - { - Device = device; - } +namespace Artemis.Core; - /// - /// Gets the device this event is related to - /// - public ArtemisDevice Device { get; } +/// +/// Provides data about device related events +/// +public class DeviceEventArgs : EventArgs +{ + internal DeviceEventArgs(ArtemisDevice device) + { + Device = device; } + + /// + /// Gets the device this event is related to + /// + public ArtemisDevice Device { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/DynamicDataModelChildEventArgs.cs b/src/Artemis.Core/Events/DynamicDataModelChildEventArgs.cs index 030fd815e..02f6a6b20 100644 --- a/src/Artemis.Core/Events/DynamicDataModelChildEventArgs.cs +++ b/src/Artemis.Core/Events/DynamicDataModelChildEventArgs.cs @@ -1,27 +1,26 @@ using System; using Artemis.Core.Modules; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Provides data about dynamic data model child related events +/// +public class DynamicDataModelChildEventArgs : EventArgs { - /// - /// Provides data about dynamic data model child related events - /// - public class DynamicDataModelChildEventArgs : EventArgs + internal DynamicDataModelChildEventArgs(DynamicChild dynamicChild, string key) { - internal DynamicDataModelChildEventArgs(DynamicChild dynamicChild, string key) - { - DynamicChild = dynamicChild; - Key = key; - } - - /// - /// Gets the dynamic data model child - /// - public DynamicChild DynamicChild { get; } - - /// - /// Gets the key of the dynamic data model on the parent - /// - public string Key { get; } + DynamicChild = dynamicChild; + Key = key; } + + /// + /// Gets the dynamic data model child + /// + public DynamicChild DynamicChild { get; } + + /// + /// Gets the key of the dynamic data model on the parent + /// + public string Key { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/FrameRenderedEventArgs.cs b/src/Artemis.Core/Events/FrameRenderedEventArgs.cs index 65c0c18e9..935375fad 100644 --- a/src/Artemis.Core/Events/FrameRenderedEventArgs.cs +++ b/src/Artemis.Core/Events/FrameRenderedEventArgs.cs @@ -1,27 +1,26 @@ using System; using RGB.NET.Core; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Provides data about frame rendering related events +/// +public class FrameRenderedEventArgs : EventArgs { - /// - /// Provides data about frame rendering related events - /// - public class FrameRenderedEventArgs : EventArgs + internal FrameRenderedEventArgs(SKTexture texture, RGBSurface rgbSurface) { - internal FrameRenderedEventArgs(SKTexture texture, RGBSurface rgbSurface) - { - Texture = texture; - RgbSurface = rgbSurface; - } - - /// - /// Gets the texture used to render this frame - /// - public SKTexture Texture { get; } - - /// - /// Gets the RGB surface used to render this frame - /// - public RGBSurface RgbSurface { get; } + Texture = texture; + RgbSurface = rgbSurface; } + + /// + /// Gets the texture used to render this frame + /// + public SKTexture Texture { get; } + + /// + /// Gets the RGB surface used to render this frame + /// + public RGBSurface RgbSurface { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/FrameRenderingEventArgs.cs b/src/Artemis.Core/Events/FrameRenderingEventArgs.cs index 1b1c0e747..56394587f 100644 --- a/src/Artemis.Core/Events/FrameRenderingEventArgs.cs +++ b/src/Artemis.Core/Events/FrameRenderingEventArgs.cs @@ -2,33 +2,32 @@ using RGB.NET.Core; using SkiaSharp; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Provides data about frame rendered related events +/// +public class FrameRenderingEventArgs : EventArgs { - /// - /// Provides data about frame rendered related events - /// - 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; - } - - /// - /// Gets the canvas this frame is rendering on - /// - public SKCanvas Canvas { get; } - - /// - /// Gets the delta time since the last frame was rendered - /// - public double DeltaTime { get; } - - /// - /// Gets the RGB surface used to render this frame - /// - public RGBSurface RgbSurface { get; } + Canvas = canvas; + DeltaTime = deltaTime; + RgbSurface = rgbSurface; } + + /// + /// Gets the canvas this frame is rendering on + /// + public SKCanvas Canvas { get; } + + /// + /// Gets the delta time since the last frame was rendered + /// + public double DeltaTime { get; } + + /// + /// Gets the RGB surface used to render this frame + /// + public RGBSurface RgbSurface { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/ModuleEventArgs.cs b/src/Artemis.Core/Events/ModuleEventArgs.cs index 44db160a3..dd9d2c8c8 100644 --- a/src/Artemis.Core/Events/ModuleEventArgs.cs +++ b/src/Artemis.Core/Events/ModuleEventArgs.cs @@ -1,21 +1,20 @@ using System; using Artemis.Core.Modules; -namespace Artemis.Core -{ - /// - /// Provides data about module events - /// - public class ModuleEventArgs : EventArgs - { - internal ModuleEventArgs(Module module) - { - Module = module; - } +namespace Artemis.Core; - /// - /// Gets the module this event is related to - /// - public Module Module { get; } +/// +/// Provides data about module events +/// +public class ModuleEventArgs : EventArgs +{ + internal ModuleEventArgs(Module module) + { + Module = module; } + + /// + /// Gets the module this event is related to + /// + public Module Module { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/Plugins/PluginEventArgs.cs b/src/Artemis.Core/Events/Plugins/PluginEventArgs.cs index aa21536bc..fad23069a 100644 --- a/src/Artemis.Core/Events/Plugins/PluginEventArgs.cs +++ b/src/Artemis.Core/Events/Plugins/PluginEventArgs.cs @@ -1,20 +1,19 @@ using System; -namespace Artemis.Core -{ - /// - /// Provides data about plugin related events - /// - public class PluginEventArgs : EventArgs - { - internal PluginEventArgs(Plugin plugin) - { - Plugin = plugin; - } +namespace Artemis.Core; - /// - /// Gets the plugin this event is related to - /// - public Plugin Plugin { get; } +/// +/// Provides data about plugin related events +/// +public class PluginEventArgs : EventArgs +{ + internal PluginEventArgs(Plugin plugin) + { + Plugin = plugin; } + + /// + /// Gets the plugin this event is related to + /// + public Plugin Plugin { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/Plugins/PluginFeatureEventArgs.cs b/src/Artemis.Core/Events/Plugins/PluginFeatureEventArgs.cs index d16789b82..121a76759 100644 --- a/src/Artemis.Core/Events/Plugins/PluginFeatureEventArgs.cs +++ b/src/Artemis.Core/Events/Plugins/PluginFeatureEventArgs.cs @@ -1,36 +1,35 @@ using System; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Provides data about plugin feature related events +/// +public class PluginFeatureEventArgs : EventArgs { - /// - /// Provides data about plugin feature related events - /// - public class PluginFeatureEventArgs : EventArgs + internal PluginFeatureEventArgs(PluginFeature pluginFeature) { - internal PluginFeatureEventArgs(PluginFeature pluginFeature) - { - PluginFeature = pluginFeature; - } - - /// - /// Gets the plugin feature this event is related to - /// - public PluginFeature PluginFeature { get; } + PluginFeature = pluginFeature; } /// - /// Provides data about plugin feature info related events + /// Gets the plugin feature this event is related to /// - public class PluginFeatureInfoEventArgs : EventArgs - { - internal PluginFeatureInfoEventArgs(PluginFeatureInfo pluginFeatureInfo) - { - PluginFeatureInfo = pluginFeatureInfo; - } + public PluginFeature PluginFeature { get; } +} - /// - /// Gets the plugin feature this event is related to - /// - public PluginFeatureInfo PluginFeatureInfo { get; } +/// +/// Provides data about plugin feature info related events +/// +public class PluginFeatureInfoEventArgs : EventArgs +{ + internal PluginFeatureInfoEventArgs(PluginFeatureInfo pluginFeatureInfo) + { + PluginFeatureInfo = pluginFeatureInfo; } + + /// + /// Gets the plugin feature this event is related to + /// + public PluginFeatureInfo PluginFeatureInfo { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/Profiles/DataBindingEventArgs.cs b/src/Artemis.Core/Events/Profiles/DataBindingEventArgs.cs index c01cfae65..927e1b849 100644 --- a/src/Artemis.Core/Events/Profiles/DataBindingEventArgs.cs +++ b/src/Artemis.Core/Events/Profiles/DataBindingEventArgs.cs @@ -1,20 +1,19 @@ using System; -namespace Artemis.Core -{ - /// - /// Provides data for data binding events. - /// - public class DataBindingEventArgs : EventArgs - { - internal DataBindingEventArgs(IDataBinding dataBinding) - { - DataBinding = dataBinding; - } +namespace Artemis.Core; - /// - /// Gets the data binding this event is related to - /// - public IDataBinding DataBinding { get; } +/// +/// Provides data for data binding events. +/// +public class DataBindingEventArgs : EventArgs +{ + internal DataBindingEventArgs(IDataBinding dataBinding) + { + DataBinding = dataBinding; } + + /// + /// Gets the data binding this event is related to + /// + public IDataBinding DataBinding { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/Profiles/DataBindingPropertyUpdatedEvent.cs b/src/Artemis.Core/Events/Profiles/DataBindingPropertyUpdatedEvent.cs index 33ea09229..f67aa3dcb 100644 --- a/src/Artemis.Core/Events/Profiles/DataBindingPropertyUpdatedEvent.cs +++ b/src/Artemis.Core/Events/Profiles/DataBindingPropertyUpdatedEvent.cs @@ -1,21 +1,20 @@ using System; -namespace Artemis.Core -{ - /// - /// Provides data for the event. - /// - /// - public class DataBindingPropertyUpdatedEvent : EventArgs - { - internal DataBindingPropertyUpdatedEvent(T value) - { - Value = value; - } +namespace Artemis.Core; - /// - /// The updated value that should be applied to the layer property - /// - public T Value { get; } +/// +/// Provides data for the event. +/// +/// +public class DataBindingPropertyUpdatedEvent : EventArgs +{ + internal DataBindingPropertyUpdatedEvent(T value) + { + Value = value; } + + /// + /// The updated value that should be applied to the layer property + /// + public T Value { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/Profiles/LayerPropertyEventArgs.cs b/src/Artemis.Core/Events/Profiles/LayerPropertyEventArgs.cs index 86904790a..49bbb0768 100644 --- a/src/Artemis.Core/Events/Profiles/LayerPropertyEventArgs.cs +++ b/src/Artemis.Core/Events/Profiles/LayerPropertyEventArgs.cs @@ -1,20 +1,19 @@ using System; -namespace Artemis.Core -{ - /// - /// Provides data for layer property events. - /// - public class LayerPropertyEventArgs : EventArgs - { - internal LayerPropertyEventArgs(ILayerProperty layerProperty) - { - LayerProperty = layerProperty; - } +namespace Artemis.Core; - /// - /// Gets the layer property this event is related to - /// - public ILayerProperty LayerProperty { get; } +/// +/// Provides data for layer property events. +/// +public class LayerPropertyEventArgs : EventArgs +{ + internal LayerPropertyEventArgs(ILayerProperty layerProperty) + { + LayerProperty = layerProperty; } + + /// + /// Gets the layer property this event is related to + /// + public ILayerProperty LayerProperty { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/Profiles/ProfileConfigurationEventArgs.cs b/src/Artemis.Core/Events/Profiles/ProfileConfigurationEventArgs.cs index 4bfa83c26..faec37c0d 100644 --- a/src/Artemis.Core/Events/Profiles/ProfileConfigurationEventArgs.cs +++ b/src/Artemis.Core/Events/Profiles/ProfileConfigurationEventArgs.cs @@ -1,20 +1,19 @@ using System; -namespace Artemis.Core -{ - /// - /// Provides data for profile configuration events. - /// - public class ProfileConfigurationEventArgs : EventArgs - { - internal ProfileConfigurationEventArgs(ProfileConfiguration profileConfiguration) - { - ProfileConfiguration = profileConfiguration; - } +namespace Artemis.Core; - /// - /// Gets the profile configuration this event is related to - /// - public ProfileConfiguration ProfileConfiguration { get; } +/// +/// Provides data for profile configuration events. +/// +public class ProfileConfigurationEventArgs : EventArgs +{ + internal ProfileConfigurationEventArgs(ProfileConfiguration profileConfiguration) + { + ProfileConfiguration = profileConfiguration; } + + /// + /// Gets the profile configuration this event is related to + /// + public ProfileConfiguration ProfileConfiguration { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/Profiles/ProfileElementEventArgs.cs b/src/Artemis.Core/Events/Profiles/ProfileElementEventArgs.cs index 4f4a740e4..3906d1762 100644 --- a/src/Artemis.Core/Events/Profiles/ProfileElementEventArgs.cs +++ b/src/Artemis.Core/Events/Profiles/ProfileElementEventArgs.cs @@ -1,20 +1,19 @@ using System; -namespace Artemis.Core -{ - /// - /// Provides data for profile element events. - /// - public class ProfileElementEventArgs : EventArgs - { - internal ProfileElementEventArgs(ProfileElement profileElement) - { - ProfileElement = profileElement; - } +namespace Artemis.Core; - /// - /// Gets the profile element this event is related to - /// - public ProfileElement ProfileElement { get; } +/// +/// Provides data for profile element events. +/// +public class ProfileElementEventArgs : EventArgs +{ + internal ProfileElementEventArgs(ProfileElement profileElement) + { + ProfileElement = profileElement; } + + /// + /// Gets the profile element this event is related to + /// + public ProfileElement ProfileElement { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/RestartEventArgs.cs b/src/Artemis.Core/Events/RestartEventArgs.cs index 1fe2b1fae..08458baff 100644 --- a/src/Artemis.Core/Events/RestartEventArgs.cs +++ b/src/Artemis.Core/Events/RestartEventArgs.cs @@ -1,33 +1,32 @@ using System; using System.Collections.Generic; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Provides data about application restart events +/// +public class RestartEventArgs : EventArgs { - /// - /// Provides data about application restart events - /// - public class RestartEventArgs : EventArgs + internal RestartEventArgs(bool elevate, TimeSpan delay, List? extraArgs) { - internal RestartEventArgs(bool elevate, TimeSpan delay, List? extraArgs) - { - Elevate = elevate; - Delay = delay; - ExtraArgs = extraArgs; - } - - /// - /// Gets a boolean indicating whether the application should be restarted with elevated permissions - /// - public bool Elevate { get; } - - /// - /// Gets the delay before killing process and restarting - /// - public TimeSpan Delay { get; } - - /// - /// A list of extra arguments to pass to Artemis when restarting - /// - public List? ExtraArgs { get; } + Elevate = elevate; + Delay = delay; + ExtraArgs = extraArgs; } + + /// + /// Gets a boolean indicating whether the application should be restarted with elevated permissions + /// + public bool Elevate { get; } + + /// + /// Gets the delay before killing process and restarting + /// + public TimeSpan Delay { get; } + + /// + /// A list of extra arguments to pass to Artemis when restarting + /// + public List? ExtraArgs { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/Stores/DataModelStoreEvent.cs b/src/Artemis.Core/Events/Stores/DataModelStoreEvent.cs index eb1132422..15d9e84fb 100644 --- a/src/Artemis.Core/Events/Stores/DataModelStoreEvent.cs +++ b/src/Artemis.Core/Events/Stores/DataModelStoreEvent.cs @@ -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; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/Stores/LayerBrushStoreEvent.cs b/src/Artemis.Core/Events/Stores/LayerBrushStoreEvent.cs index 6d80a46fb..b8c147d86 100644 --- a/src/Artemis.Core/Events/Stores/LayerBrushStoreEvent.cs +++ b/src/Artemis.Core/Events/Stores/LayerBrushStoreEvent.cs @@ -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; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/Stores/LayerEffectStoreEvent.cs b/src/Artemis.Core/Events/Stores/LayerEffectStoreEvent.cs index 8abcecb7f..fc7978d80 100644 --- a/src/Artemis.Core/Events/Stores/LayerEffectStoreEvent.cs +++ b/src/Artemis.Core/Events/Stores/LayerEffectStoreEvent.cs @@ -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; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/Stores/NodeTypeStoreEvent.cs b/src/Artemis.Core/Events/Stores/NodeTypeStoreEvent.cs index e91d27b6a..ac641e729 100644 --- a/src/Artemis.Core/Events/Stores/NodeTypeStoreEvent.cs +++ b/src/Artemis.Core/Events/Stores/NodeTypeStoreEvent.cs @@ -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; } } \ No newline at end of file diff --git a/src/Artemis.Core/Events/SurfaceConfigurationEventArgs.cs b/src/Artemis.Core/Events/SurfaceConfigurationEventArgs.cs index 52f8192a9..68514ccf6 100644 --- a/src/Artemis.Core/Events/SurfaceConfigurationEventArgs.cs +++ b/src/Artemis.Core/Events/SurfaceConfigurationEventArgs.cs @@ -1,21 +1,20 @@ using System; using System.Collections.Generic; -namespace Artemis.Core -{ - /// - /// Provides data about device configuration related events - /// - public class SurfaceConfigurationEventArgs : EventArgs - { - internal SurfaceConfigurationEventArgs(List devices) - { - Devices = devices; - } +namespace Artemis.Core; - /// - /// Gets the current list of devices - /// - public List Devices { get; } +/// +/// Provides data about device configuration related events +/// +public class SurfaceConfigurationEventArgs : EventArgs +{ + internal SurfaceConfigurationEventArgs(List devices) + { + Devices = devices; } + + /// + /// Gets the current list of devices + /// + public List Devices { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Exceptions/ArtemisCoreException.cs b/src/Artemis.Core/Exceptions/ArtemisCoreException.cs index 44faa3083..0718c5548 100644 --- a/src/Artemis.Core/Exceptions/ArtemisCoreException.cs +++ b/src/Artemis.Core/Exceptions/ArtemisCoreException.cs @@ -1,18 +1,17 @@ using System; -namespace Artemis.Core -{ - /// - /// Represents errors that occur within the Artemis Core - /// - public class ArtemisCoreException : Exception - { - internal ArtemisCoreException(string message) : base(message) - { - } +namespace Artemis.Core; - internal ArtemisCoreException(string message, Exception inner) : base(message, inner) - { - } +/// +/// Represents errors that occur within the Artemis Core +/// +public class ArtemisCoreException : Exception +{ + internal ArtemisCoreException(string message) : base(message) + { + } + + internal ArtemisCoreException(string message, Exception inner) : base(message, inner) + { } } \ No newline at end of file diff --git a/src/Artemis.Core/Exceptions/ArtemisGraphicsContextException.cs b/src/Artemis.Core/Exceptions/ArtemisGraphicsContextException.cs index 975dd8c04..6970b445a 100644 --- a/src/Artemis.Core/Exceptions/ArtemisGraphicsContextException.cs +++ b/src/Artemis.Core/Exceptions/ArtemisGraphicsContextException.cs @@ -1,25 +1,24 @@ using System; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents SkiaSharp graphics-context related errors +/// +public class ArtemisGraphicsContextException : Exception { - /// - /// Represents SkiaSharp graphics-context related errors - /// - public class ArtemisGraphicsContextException : Exception + /// + public ArtemisGraphicsContextException() { - /// - public ArtemisGraphicsContextException() - { - } + } - /// - public ArtemisGraphicsContextException(string message) : base(message) - { - } + /// + public ArtemisGraphicsContextException(string message) : base(message) + { + } - /// - public ArtemisGraphicsContextException(string message, Exception innerException) : base(message, innerException) - { - } + /// + public ArtemisGraphicsContextException(string message, Exception innerException) : base(message, innerException) + { } } \ No newline at end of file diff --git a/src/Artemis.Core/Exceptions/ArtemisPluginException.cs b/src/Artemis.Core/Exceptions/ArtemisPluginException.cs index 0e8827443..8e49b0e19 100644 --- a/src/Artemis.Core/Exceptions/ArtemisPluginException.cs +++ b/src/Artemis.Core/Exceptions/ArtemisPluginException.cs @@ -1,53 +1,52 @@ using System; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// An exception thrown when a plugin-related error occurs +/// +public class ArtemisPluginException : Exception { /// - /// An exception thrown when a plugin-related error occurs + /// Creates a new instance of the class /// - public class ArtemisPluginException : Exception + public ArtemisPluginException(Plugin plugin) { - /// - /// Creates a new instance of the class - /// - public ArtemisPluginException(Plugin plugin) - { - Plugin = plugin; - } - - /// - /// Creates a new instance of the class - /// - public ArtemisPluginException(Plugin plugin, string message) : base(message) - { - Plugin = plugin; - } - - /// - /// Creates a new instance of the class - /// - public ArtemisPluginException(Plugin plugin, string message, Exception inner) : base(message, inner) - { - Plugin = plugin; - } - - /// - /// Creates a new instance of the class - /// - public ArtemisPluginException(string message) : base(message) - { - } - - /// - /// Creates a new instance of the class - /// - public ArtemisPluginException(string message, Exception inner) : base(message, inner) - { - } - - /// - /// Gets the plugin the error is related to - /// - public Plugin? Plugin { get; } + Plugin = plugin; } + + /// + /// Creates a new instance of the class + /// + public ArtemisPluginException(Plugin plugin, string message) : base(message) + { + Plugin = plugin; + } + + /// + /// Creates a new instance of the class + /// + public ArtemisPluginException(Plugin plugin, string message, Exception inner) : base(message, inner) + { + Plugin = plugin; + } + + /// + /// Creates a new instance of the class + /// + public ArtemisPluginException(string message) : base(message) + { + } + + /// + /// Creates a new instance of the class + /// + public ArtemisPluginException(string message, Exception inner) : base(message, inner) + { + } + + /// + /// Gets the plugin the error is related to + /// + public Plugin? Plugin { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Exceptions/ArtemisPluginFeatureException.cs b/src/Artemis.Core/Exceptions/ArtemisPluginFeatureException.cs index 3e1766bd7..6b0a6867c 100644 --- a/src/Artemis.Core/Exceptions/ArtemisPluginFeatureException.cs +++ b/src/Artemis.Core/Exceptions/ArtemisPluginFeatureException.cs @@ -1,30 +1,29 @@ using System; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// An exception thrown when a plugin feature-related error occurs +/// +public class ArtemisPluginFeatureException : Exception { - /// - /// An exception thrown when a plugin feature-related error occurs - /// - 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; - } - - /// - /// Gets the plugin feature the error is related to - /// - 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; + } + + /// + /// Gets the plugin feature the error is related to + /// + public PluginFeature PluginFeature { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Exceptions/ArtemisPluginLockException.cs b/src/Artemis.Core/Exceptions/ArtemisPluginLockException.cs index e95d27272..e601e6e26 100644 --- a/src/Artemis.Core/Exceptions/ArtemisPluginLockException.cs +++ b/src/Artemis.Core/Exceptions/ArtemisPluginLockException.cs @@ -1,21 +1,20 @@ using System; -namespace Artemis.Core -{ - /// - /// An exception thrown when a plugin lock file error occurs - /// - 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."; - } +/// +/// An exception thrown when a plugin lock file error occurs +/// +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."; } } \ No newline at end of file diff --git a/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs b/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs index e2f3a850d..7951ccbbd 100644 --- a/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs +++ b/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs @@ -1,30 +1,29 @@ using System; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// An exception thrown when a plugin prerequisite-related error occurs +/// +public class ArtemisPluginPrerequisiteException : Exception { - /// - /// An exception thrown when a plugin prerequisite-related error occurs - /// - 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; - } - - /// - /// Gets the subject the error is related to - /// - 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; + } + + /// + /// Gets the subject the error is related to + /// + public IPrerequisitesSubject Subject { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Extensions/DirectoryInfoExtensions.cs b/src/Artemis.Core/Extensions/DirectoryInfoExtensions.cs index fda99d3b5..5a6864273 100644 --- a/src/Artemis.Core/Extensions/DirectoryInfoExtensions.cs +++ b/src/Artemis.Core/Extensions/DirectoryInfoExtensions.cs @@ -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(); } } \ No newline at end of file diff --git a/src/Artemis.Core/Extensions/DoubleExtensions.cs b/src/Artemis.Core/Extensions/DoubleExtensions.cs index 796f247fb..b8620dc92 100644 --- a/src/Artemis.Core/Extensions/DoubleExtensions.cs +++ b/src/Artemis.Core/Extensions/DoubleExtensions.cs @@ -1,22 +1,21 @@ using System; using System.Runtime.CompilerServices; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// A static class providing extensions +/// +public static class DoubleExtensions { /// - /// A static class providing extensions + /// Rounds the provided number away to zero and casts the result to an /// - public static class DoubleExtensions + /// The number to round + /// The rounded number as an integer + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int RoundToInt(this double number) { - /// - /// Rounds the provided number away to zero and casts the result to an - /// - /// The number to round - /// The rounded number as an integer - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int RoundToInt(this double number) - { - return (int) Math.Round(number, MidpointRounding.AwayFromZero); - } + return (int) Math.Round(number, MidpointRounding.AwayFromZero); } } \ No newline at end of file diff --git a/src/Artemis.Core/Extensions/FloatExtensions.cs b/src/Artemis.Core/Extensions/FloatExtensions.cs index 0a4cb9568..4ce95dfc4 100644 --- a/src/Artemis.Core/Extensions/FloatExtensions.cs +++ b/src/Artemis.Core/Extensions/FloatExtensions.cs @@ -1,22 +1,21 @@ using System; using System.Runtime.CompilerServices; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// A static class providing extensions +/// +public static class FloatExtensions { /// - /// A static class providing extensions + /// Rounds the provided number away to zero and casts the result to an /// - public static class FloatExtensions + /// The number to round + /// The rounded number as an integer + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int RoundToInt(this float number) { - /// - /// Rounds the provided number away to zero and casts the result to an - /// - /// The number to round - /// The rounded number as an integer - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int RoundToInt(this float number) - { - return (int) MathF.Round(number, MidpointRounding.AwayFromZero); - } + return (int) MathF.Round(number, MidpointRounding.AwayFromZero); } } \ No newline at end of file diff --git a/src/Artemis.Core/Extensions/IEnumerableExtensions.cs b/src/Artemis.Core/Extensions/IEnumerableExtensions.cs index 111851187..5504bfe11 100644 --- a/src/Artemis.Core/Extensions/IEnumerableExtensions.cs +++ b/src/Artemis.Core/Extensions/IEnumerableExtensions.cs @@ -19,35 +19,33 @@ #endregion -using System; using System.Collections.Generic; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// A static class providing extensions +/// +// ReSharper disable once InconsistentNaming +public static class IEnumerableExtensions { /// - /// A static class providing extensions + /// Returns the index of the provided element inside the read only collection /// - // ReSharper disable once InconsistentNaming - public static class IEnumerableExtensions + /// The type of element to find + /// The collection to search in + /// The element to find + /// If found, the index of the element to find; otherwise -1 + public static int IndexOf(this IReadOnlyCollection self, T elementToFind) { - /// - /// Returns the index of the provided element inside the read only collection - /// - /// The type of element to find - /// The collection to search in - /// The element to find - /// If found, the index of the element to find; otherwise -1 - public static int IndexOf(this IReadOnlyCollection 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; } } \ No newline at end of file diff --git a/src/Artemis.Core/Extensions/ProcessExtensions.cs b/src/Artemis.Core/Extensions/ProcessExtensions.cs index 8b9ee1213..27475a988 100644 --- a/src/Artemis.Core/Extensions/ProcessExtensions.cs +++ b/src/Artemis.Core/Extensions/ProcessExtensions.cs @@ -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; + +/// +/// A static class providing extensions +/// +[SuppressMessage("Design", "CA1060:Move pinvokes to native methods class", Justification = "I don't care, piss off")] +public static class ProcessExtensions { /// - /// A static class providing extensions + /// Gets the file name of the given process /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1060:Move pinvokes to native methods class", Justification = "I don't care, piss off")] - public static class ProcessExtensions + /// The process + /// The filename of the given process + public static string GetProcessFilename(this Process p) { - /// - /// Gets the file name of the given process - /// - /// The process - /// The filename of the given process - 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 } } \ No newline at end of file diff --git a/src/Artemis.Core/Extensions/RgbDeviceExtensions.cs b/src/Artemis.Core/Extensions/RgbDeviceExtensions.cs index c6139f30c..11871df5c 100644 --- a/src/Artemis.Core/Extensions/RgbDeviceExtensions.cs +++ b/src/Artemis.Core/Extensions/RgbDeviceExtensions.cs @@ -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)); } } \ No newline at end of file diff --git a/src/Artemis.Core/Extensions/SKColorExtensions.cs b/src/Artemis.Core/Extensions/SKColorExtensions.cs index f1e9297ec..b18e35e0f 100644 --- a/src/Artemis.Core/Extensions/SKColorExtensions.cs +++ b/src/Artemis.Core/Extensions/SKColorExtensions.cs @@ -2,77 +2,76 @@ using RGB.NET.Core; using SkiaSharp; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// A static class providing extensions +/// +public static class SKColorExtensions { /// - /// A static class providing extensions + /// Converts hte SKColor to an RGB.NET color /// - public static class SKColorExtensions + /// The color to convert + /// The RGB.NET color + public static Color ToRgbColor(this SKColor color) { - /// - /// Converts hte SKColor to an RGB.NET color - /// - /// The color to convert - /// The RGB.NET color - 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); + } - /// - /// Interpolates a color between the and color. - /// - /// The first color - /// The second color - /// A value between 0 and 1 - /// The interpolated color - 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; + /// + /// Interpolates a color between the and color. + /// + /// The first color + /// The second color + /// A value between 0 and 1 + /// The interpolated color + 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) + ); + } - /// - /// Adds the two colors together - /// - /// The first color - /// The second color - /// The sum of the two colors - 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) - ); - } + /// + /// Adds the two colors together + /// + /// The first color + /// The second color + /// The sum of the two colors + 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) + ); + } - /// - /// Darkens the color by the specified amount - /// - /// The color to darken - /// The brightness of the new color - /// The darkened color - 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); - } + /// + /// Darkens the color by the specified amount + /// + /// The color to darken + /// The brightness of the new color + /// The darkened color + 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); } } \ No newline at end of file diff --git a/src/Artemis.Core/Extensions/SKPaintExtensions.cs b/src/Artemis.Core/Extensions/SKPaintExtensions.cs index ec2ebe6e2..eea179e7a 100644 --- a/src/Artemis.Core/Extensions/SKPaintExtensions.cs +++ b/src/Artemis.Core/Extensions/SKPaintExtensions.cs @@ -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(); } } \ No newline at end of file diff --git a/src/Artemis.Core/Extensions/StreamExtensions.cs b/src/Artemis.Core/Extensions/StreamExtensions.cs index a6b350fd2..cc33dbaa3 100644 --- a/src/Artemis.Core/Extensions/StreamExtensions.cs +++ b/src/Artemis.Core/Extensions/StreamExtensions.cs @@ -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; + + /// + /// Copies a stream to another stream + /// + /// The source to copy from + /// The length of the source stream, if known - used for progress reporting + /// The destination to copy to + /// The size of the copy block buffer + /// An implementation for reporting progress + /// A cancellation token + /// A task representing the operation + 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; - /// - /// Copies a stream to another stream - /// - /// The source to copy from - /// The length of the source stream, if known - used for progress reporting - /// The destination to copy to - /// The size of the copy block buffer - /// An implementation for reporting progress - /// A cancellation token - /// A task representing the operation - 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(); } - /// - /// Copies a stream to another stream - /// - /// The source to copy from - /// The length of the source stream, if known - used for progress reporting - /// The destination to copy to - /// An implementation for reporting progress - /// A cancellation token - /// A task representing the operation - 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(); + } - /// - /// Copies a stream to another stream - /// - /// The source to copy from - /// The destination to copy to - /// An implementation for reporting progress - /// A cancellation token - /// A task representing the operation - public static Task CopyToAsync(this Stream source, Stream destination, IProgress<(long, long)> progress, CancellationToken cancellationToken) - { - return CopyToAsync(source, 0L, destination, 0, progress, cancellationToken); - } + /// + /// Copies a stream to another stream + /// + /// The source to copy from + /// The length of the source stream, if known - used for progress reporting + /// The destination to copy to + /// An implementation for reporting progress + /// A cancellation token + /// A task representing the operation + 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); + } - /// - /// Copies a stream to another stream - /// - /// The source to copy from - /// The length of the source stream, if known - used for progress reporting - /// The destination to copy to - /// An implementation for reporting progress - /// A task representing the operation - public static Task CopyToAsync(this Stream source, long sourceLength, Stream destination, IProgress<(long, long)> progress) - { - return CopyToAsync(source, sourceLength, destination, 0, progress, default); - } + /// + /// Copies a stream to another stream + /// + /// The source to copy from + /// The destination to copy to + /// An implementation for reporting progress + /// A cancellation token + /// A task representing the operation + public static Task CopyToAsync(this Stream source, Stream destination, IProgress<(long, long)> progress, CancellationToken cancellationToken) + { + return CopyToAsync(source, 0L, destination, 0, progress, cancellationToken); + } - /// - /// Copies a stream to another stream - /// - /// The source to copy from - /// The destination to copy to - /// An implementation for reporting progress - /// A task representing the operation - public static Task CopyToAsync(this Stream source, Stream destination, IProgress<(long, long)> progress) - { - return CopyToAsync(source, 0L, destination, 0, progress, default); - } + /// + /// Copies a stream to another stream + /// + /// The source to copy from + /// The length of the source stream, if known - used for progress reporting + /// The destination to copy to + /// An implementation for reporting progress + /// A task representing the operation + public static Task CopyToAsync(this Stream source, long sourceLength, Stream destination, IProgress<(long, long)> progress) + { + return CopyToAsync(source, sourceLength, destination, 0, progress, default); + } + + /// + /// Copies a stream to another stream + /// + /// The source to copy from + /// The destination to copy to + /// An implementation for reporting progress + /// A task representing the operation + public static Task CopyToAsync(this Stream source, Stream destination, IProgress<(long, long)> progress) + { + return CopyToAsync(source, 0L, destination, 0, progress, default); } } \ No newline at end of file diff --git a/src/Artemis.Core/Extensions/TypeExtensions.cs b/src/Artemis.Core/Extensions/TypeExtensions.cs index ed5c5e36e..9e6ba83e3 100644 --- a/src/Artemis.Core/Extensions/TypeExtensions.cs +++ b/src/Artemis.Core/Extensions/TypeExtensions.cs @@ -5,249 +5,250 @@ using System.Linq; using System.Reflection; using Humanizer; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// A static class providing extensions +/// +public static class TypeExtensions { - /// - /// A static class providing extensions - /// - public static class TypeExtensions + private static readonly Dictionary> PrimitiveTypeConversions = new() { - private static readonly Dictionary> PrimitiveTypeConversions = new() - { - {typeof(decimal), new List {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char)}}, - {typeof(double), new List {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float)}}, - {typeof(float), new List {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float)}}, - {typeof(ulong), new List {typeof(byte), typeof(ushort), typeof(uint), typeof(char)}}, - {typeof(long), new List {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(char)}}, - {typeof(uint), new List {typeof(byte), typeof(ushort), typeof(char)}}, - {typeof(int), new List {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(char)}}, - {typeof(ushort), new List {typeof(byte), typeof(char)}}, - {typeof(short), new List {typeof(byte)}} - }; + {typeof(decimal), new List {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char)}}, + {typeof(double), new List {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float)}}, + {typeof(float), new List {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float)}}, + {typeof(ulong), new List {typeof(byte), typeof(ushort), typeof(uint), typeof(char)}}, + {typeof(long), new List {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(char)}}, + {typeof(uint), new List {typeof(byte), typeof(ushort), typeof(char)}}, + {typeof(int), new List {typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(char)}}, + {typeof(ushort), new List {typeof(byte), typeof(char)}}, + {typeof(short), new List {typeof(byte)}} + }; - private static readonly Dictionary 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 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"} + }; - /// - /// Determines whether the provided type is of a specified generic type - /// - /// The type to check - /// The generic type to match - /// True if the is generic and of generic type - public static bool IsGenericType(this Type? type, Type genericType) - { - if (type == null) - return false; + /// + /// Determines whether the provided type is of a specified generic type + /// + /// The type to check + /// The generic type to match + /// True if the is generic and of generic type + public static bool IsGenericType(this Type? type, Type genericType) + { + if (type == null) + return false; - return type.BaseType?.GetGenericTypeDefinition() == genericType; - } + return type.BaseType?.GetGenericTypeDefinition() == genericType; + } - /// - /// Determines whether the provided type is a struct - /// - /// The type to check - /// if the type is a struct, otherwise - public static bool IsStruct(this Type type) - { - return type.IsValueType && !type.IsPrimitive && !type.IsEnum; - } + /// + /// Determines whether the provided type is a struct + /// + /// The type to check + /// if the type is a struct, otherwise + public static bool IsStruct(this Type type) + { + return type.IsValueType && !type.IsPrimitive && !type.IsEnum; + } - /// - /// Determines whether the provided type is any kind of numeric type - /// - /// The type to check - /// if the type a numeric type, otherwise - 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); - } + /// + /// Determines whether the provided type is any kind of numeric type + /// + /// The type to check + /// if the type a numeric type, otherwise + 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); + } - /// - /// Determines whether the provided value is any kind of numeric type - /// - /// The value to check - /// if the value is of a numeric type, otherwise - 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; - } + /// + /// Determines whether the provided value is any kind of numeric type + /// + /// The value to check + /// if the value is of a numeric type, otherwise + 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 - /// - /// Determines whether an instance of a specified type can be casted to a variable of the current type - /// - /// - 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 + /// + /// Determines whether an instance of a specified type can be casted to a variable of the current type + /// + /// + 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; + } - /// - /// 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 - /// - /// - 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; + /// + /// 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 + /// + /// + 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; + } + + /// + /// Returns the default value of the given type + /// + public static object? GetDefault(this Type type) + { + return type.IsValueType ? Activator.CreateInstance(type) : null; + } + + /// + /// Determines whether the given type is a generic enumerable + /// + 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<>)); + } + + /// + /// Determines the type of the provided generic enumerable type + /// + 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]; + } + + /// + /// Determines if the is of a certain . + /// + /// The type to check. + /// The generic type it should be or implement + public static bool IsOfGenericType(this Type typeToCheck, Type genericType) + { + return typeToCheck.IsOfGenericType(genericType, out Type? _); + } + + /// + /// Determines a display name for the given type + /// + /// The type to determine the name for + /// Whether or not to humanize the result, defaults to false + /// + 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; } - /// - /// Returns the default value of the given type - /// - 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) + "?"; - /// - /// Determines whether the given type is a generic enumerable - /// - 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<>)); - } - - /// - /// Determines the type of the provided generic enumerable type - /// - 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]; - } - - /// - /// Determines if the is of a certain . - /// - /// The type to check. - /// The generic type it should be or implement - 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; - } - } - - /// - /// Determines a display name for the given type - /// - /// The type to determine the name for - /// Whether or not to humanize the result, defaults to false - /// - 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; } } } \ No newline at end of file diff --git a/src/Artemis.Core/JsonConverters/ForgivingIntConverter.cs b/src/Artemis.Core/JsonConverters/ForgivingIntConverter.cs index 05845fc2e..c43fa7bf8 100644 --- a/src/Artemis.Core/JsonConverters/ForgivingIntConverter.cs +++ b/src/Artemis.Core/JsonConverters/ForgivingIntConverter.cs @@ -2,32 +2,31 @@ using System; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Artemis.Core.JsonConverters +namespace Artemis.Core.JsonConverters; + +/// +/// An int converter that, if required, will round float values +/// +internal class ForgivingIntConverter : JsonConverter { - /// - /// An int converter that, if required, will round float values - /// - internal class ForgivingIntConverter : JsonConverter + 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(reader); - if (jsonValue == null) - throw new JsonReaderException("Failed to deserialize forgiving int value"); - - if (jsonValue.Type == JTokenType.Float) - return (int) Math.Round(jsonValue.Value()); - if (jsonValue.Type == JTokenType.Integer) - return jsonValue.Value(); + throw new NotImplementedException(); + } + public override int ReadJson(JsonReader reader, Type objectType, int existingValue, bool hasExistingValue, JsonSerializer serializer) + { + JValue? jsonValue = serializer.Deserialize(reader); + if (jsonValue == null) throw new JsonReaderException("Failed to deserialize forgiving int value"); - } + + if (jsonValue.Type == JTokenType.Float) + return (int) Math.Round(jsonValue.Value()); + if (jsonValue.Type == JTokenType.Integer) + return jsonValue.Value(); + + throw new JsonReaderException("Failed to deserialize forgiving int value"); } } \ No newline at end of file diff --git a/src/Artemis.Core/JsonConverters/NumericJsonConverter.cs b/src/Artemis.Core/JsonConverters/NumericJsonConverter.cs index e2ffd62a0..0dea83b5e 100644 --- a/src/Artemis.Core/JsonConverters/NumericJsonConverter.cs +++ b/src/Artemis.Core/JsonConverters/NumericJsonConverter.cs @@ -1,25 +1,24 @@ using System; using Newtonsoft.Json; -namespace Artemis.Core.JsonConverters +namespace Artemis.Core.JsonConverters; + +internal class NumericJsonConverter : JsonConverter { - internal class NumericJsonConverter : JsonConverter + #region Overrides of JsonConverter + + /// + public override void WriteJson(JsonWriter writer, Numeric value, JsonSerializer serializer) { - #region Overrides of JsonConverter - - /// - public override void WriteJson(JsonWriter writer, Numeric value, JsonSerializer serializer) - { - float floatValue = value; - writer.WriteValue(floatValue); - } - - /// - 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); } + + /// + public override Numeric ReadJson(JsonReader reader, Type objectType, Numeric existingValue, bool hasExistingValue, JsonSerializer serializer) + { + return new Numeric(reader.Value); + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/JsonConverters/SKColorConverter.cs b/src/Artemis.Core/JsonConverters/SKColorConverter.cs index b12e1d7c4..40912c153 100644 --- a/src/Artemis.Core/JsonConverters/SKColorConverter.cs +++ b/src/Artemis.Core/JsonConverters/SKColorConverter.cs @@ -2,21 +2,20 @@ using Newtonsoft.Json; using SkiaSharp; -namespace Artemis.Core.JsonConverters +namespace Artemis.Core.JsonConverters; + +internal class SKColorConverter : JsonConverter { - internal class SKColorConverter : JsonConverter + 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; } } \ No newline at end of file diff --git a/src/Artemis.Core/JsonConverters/StreamConverter.cs b/src/Artemis.Core/JsonConverters/StreamConverter.cs index 26e638d0e..7ce3529ee 100644 --- a/src/Artemis.Core/JsonConverters/StreamConverter.cs +++ b/src/Artemis.Core/JsonConverters/StreamConverter.cs @@ -2,44 +2,43 @@ using System.IO; using Newtonsoft.Json; -namespace Artemis.Core.JsonConverters +namespace Artemis.Core.JsonConverters; + +/// +public class StreamConverter : JsonConverter { + #region Overrides of JsonConverter + /// - public class StreamConverter : JsonConverter + public override void WriteJson(JsonWriter writer, Stream? value, JsonSerializer serializer) { - #region Overrides of JsonConverter - - /// - 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; } - /// - 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()); } + + /// + 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 } \ No newline at end of file diff --git a/src/Artemis.Core/MVVM/CorePropertyChanged.cs b/src/Artemis.Core/MVVM/CorePropertyChanged.cs index b9bd735e0..8ab50083a 100644 --- a/src/Artemis.Core/MVVM/CorePropertyChanged.cs +++ b/src/Artemis.Core/MVVM/CorePropertyChanged.cs @@ -2,73 +2,68 @@ using System.Runtime.CompilerServices; using Artemis.Core.Properties; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a basic bindable class which notifies when a property value changes. +/// +public abstract class CorePropertyChanged : INotifyPropertyChanged { /// - /// Represents a basic bindable class which notifies when a property value changes. + /// Occurs when a property value changes. /// - public abstract class CorePropertyChanged : INotifyPropertyChanged + public event PropertyChangedEventHandler? PropertyChanged; + + #region Methods + + /// + /// Checks if the property already matches the desired value or needs to be updated. + /// + /// Type of the property. + /// Reference to the backing-filed. + /// Value to apply. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected bool RequiresUpdate(ref T storage, T value) { - #region Events - - /// - /// Occurs when a property value changes. - /// - public event PropertyChangedEventHandler? PropertyChanged; - - #endregion - - #region Methods - - /// - /// Checks if the property already matches the desired value or needs to be updated. - /// - /// Type of the property. - /// Reference to the backing-filed. - /// Value to apply. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected bool RequiresUpdate(ref T storage, T value) - { - return !Equals(storage, value); - } - - /// - /// Checks if the property already matches the desired value and updates it if not. - /// - /// Type of the property. - /// Reference to the backing-filed. - /// Value to apply. - /// - /// Name of the property used to notify listeners. This value is optional - /// and can be provided automatically when invoked from compilers that support - /// . - /// - /// true if the value was changed, false if the existing value matched the desired value. - [NotifyPropertyChangedInvocator] - protected bool SetAndNotify(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; - } - - /// - /// Triggers the -event when a a property value has changed. - /// - /// - /// Name of the property used to notify listeners. This value is optional - /// and can be provided automatically when invoked from compilers that support - /// . - /// - protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - #endregion + return !Equals(storage, value); } + + /// + /// Checks if the property already matches the desired value and updates it if not. + /// + /// Type of the property. + /// Reference to the backing-filed. + /// Value to apply. + /// + /// Name of the property used to notify listeners. This value is optional + /// and can be provided automatically when invoked from compilers that support + /// . + /// + /// true if the value was changed, false if the existing value matched the desired value. + [NotifyPropertyChangedInvocator] + protected bool SetAndNotify(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; + } + + /// + /// Triggers the -event when a a property value has changed. + /// + /// + /// Name of the property used to notify listeners. This value is optional + /// and can be provided automatically when invoked from compilers that support + /// . + /// + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Models/BreakableModel.cs b/src/Artemis.Core/Models/BreakableModel.cs index 97bd761cb..7276b6adb 100644 --- a/src/Artemis.Core/Models/BreakableModel.cs +++ b/src/Artemis.Core/Models/BreakableModel.cs @@ -1,92 +1,91 @@ using System; using System.Collections.Generic; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Provides a default implementation for models that can have a broken state +/// +public abstract class BreakableModel : CorePropertyChanged, IBreakableModel { + private string? _brokenState; + private Exception? _brokenStateException; + /// - /// Provides a default implementation for models that can have a broken state + /// Invokes the event /// - public abstract class BreakableModel : CorePropertyChanged, IBreakableModel + protected virtual void OnBrokenStateChanged() { - private string? _brokenState; - private Exception? _brokenStateException; - - /// - /// Invokes the event - /// - protected virtual void OnBrokenStateChanged() - { - BrokenStateChanged?.Invoke(this, EventArgs.Empty); - } - - /// - public abstract string BrokenDisplayName { get; } - - /// - /// Gets or sets the broken state of this breakable model, if this model is not broken. - /// Note: If setting this manually you are also responsible for invoking - /// - public string? BrokenState - { - get => _brokenState; - set => SetAndNotify(ref _brokenState, value); - } - - /// - /// Gets or sets the exception that caused the broken state - /// Note: If setting this manually you are also responsible for invoking - /// - public Exception? BrokenStateException - { - get => _brokenStateException; - set => SetAndNotify(ref _brokenStateException, value); - } - - /// - public bool TryOrBreak(Action action, string breakMessage) - { - try - { - action(); - ClearBrokenState(breakMessage); - return true; - } - catch (Exception e) - { - SetBrokenState(breakMessage, e); - return false; - } - } - - /// - public void SetBrokenState(string state, Exception? exception) - { - BrokenState = state ?? throw new ArgumentNullException(nameof(state)); - BrokenStateException = exception; - OnBrokenStateChanged(); - } - - /// - 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(); - } - - /// - public virtual IEnumerable GetBrokenHierarchy() - { - if (BrokenState != null) - yield return this; - } - - /// - public event EventHandler? BrokenStateChanged; + BrokenStateChanged?.Invoke(this, EventArgs.Empty); } + + /// + public abstract string BrokenDisplayName { get; } + + /// + /// Gets or sets the broken state of this breakable model, if this model is not broken. + /// Note: If setting this manually you are also responsible for invoking + /// + public string? BrokenState + { + get => _brokenState; + set => SetAndNotify(ref _brokenState, value); + } + + /// + /// Gets or sets the exception that caused the broken state + /// Note: If setting this manually you are also responsible for invoking + /// + public Exception? BrokenStateException + { + get => _brokenStateException; + set => SetAndNotify(ref _brokenStateException, value); + } + + /// + public bool TryOrBreak(Action action, string breakMessage) + { + try + { + action(); + ClearBrokenState(breakMessage); + return true; + } + catch (Exception e) + { + SetBrokenState(breakMessage, e); + return false; + } + } + + /// + public void SetBrokenState(string state, Exception? exception) + { + BrokenState = state ?? throw new ArgumentNullException(nameof(state)); + BrokenStateException = exception; + OnBrokenStateChanged(); + } + + /// + 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(); + } + + /// + public virtual IEnumerable GetBrokenHierarchy() + { + if (BrokenState != null) + yield return this; + } + + /// + public event EventHandler? BrokenStateChanged; } \ No newline at end of file diff --git a/src/Artemis.Core/Models/IBreakableModel.cs b/src/Artemis.Core/Models/IBreakableModel.cs index 88a5762d6..715a5f34e 100644 --- a/src/Artemis.Core/Models/IBreakableModel.cs +++ b/src/Artemis.Core/Models/IBreakableModel.cs @@ -1,59 +1,58 @@ using System; using System.Collections.Generic; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a model that can have a broken state +/// +public interface IBreakableModel { /// - /// Represents a model that can have a broken state + /// Gets the display name of this breakable model /// - public interface IBreakableModel - { - /// - /// Gets the display name of this breakable model - /// - string BrokenDisplayName { get; } + string BrokenDisplayName { get; } - /// - /// Gets or sets the broken state of this breakable model, if this model is not broken. - /// - string? BrokenState { get; set; } + /// + /// Gets or sets the broken state of this breakable model, if this model is not broken. + /// + string? BrokenState { get; set; } - /// - /// Gets or sets the exception that caused the broken state - /// - Exception? BrokenStateException { get; set; } + /// + /// Gets or sets the exception that caused the broken state + /// + Exception? BrokenStateException { get; set; } - /// - /// Try to execute the provided action. If the action succeeded the broken state is cleared if it matches - /// , if the action throws an exception and - /// are set accordingly. - /// - /// The action to attempt to execute - /// The message to clear on succeed or set on failure (exception) - /// if the action succeeded; otherwise . - bool TryOrBreak(Action action, string breakMessage); + /// + /// Try to execute the provided action. If the action succeeded the broken state is cleared if it matches + /// , if the action throws an exception and + /// are set accordingly. + /// + /// The action to attempt to execute + /// The message to clear on succeed or set on failure (exception) + /// if the action succeeded; otherwise . + bool TryOrBreak(Action action, string breakMessage); - /// - /// Sets the broken state to the provided state and optional exception. - /// - /// The state to set the broken state to - /// The exception that caused the broken state - public void SetBrokenState(string state, Exception? exception); + /// + /// Sets the broken state to the provided state and optional exception. + /// + /// The state to set the broken state to + /// The exception that caused the broken state + public void SetBrokenState(string state, Exception? exception); - /// - /// Clears the broken state and exception if equals . - /// - /// - public void ClearBrokenState(string state); - - /// - /// Returns a list containing all broken models, including self and any children - /// - IEnumerable GetBrokenHierarchy(); + /// + /// Clears the broken state and exception if equals . + /// + /// + public void ClearBrokenState(string state); - /// - /// Occurs when the broken state of this model changes - /// - event EventHandler BrokenStateChanged; - } + /// + /// Returns a list containing all broken models, including self and any children + /// + IEnumerable GetBrokenHierarchy(); + + /// + /// Occurs when the broken state of this model changes + /// + event EventHandler BrokenStateChanged; } \ No newline at end of file diff --git a/src/Artemis.Core/Models/IStorageModel.cs b/src/Artemis.Core/Models/IStorageModel.cs index ce76eb0cd..cb23af0ff 100644 --- a/src/Artemis.Core/Models/IStorageModel.cs +++ b/src/Artemis.Core/Models/IStorageModel.cs @@ -1,18 +1,17 @@ -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a model that can be loaded and saved to persistent storage +/// +public interface IStorageModel { /// - /// Represents a model that can be loaded and saved to persistent storage + /// Loads the model from its associated entity /// - public interface IStorageModel - { - /// - /// Loads the model from its associated entity - /// - void Load(); + void Load(); - /// - /// Saves the model to its associated entity - /// - void Save(); - } + /// + /// Saves the model to its associated entity + /// + void Save(); } \ No newline at end of file diff --git a/src/Artemis.Core/Models/IUpdateModel.cs b/src/Artemis.Core/Models/IUpdateModel.cs index 24c480c73..0fdd1da37 100644 --- a/src/Artemis.Core/Models/IUpdateModel.cs +++ b/src/Artemis.Core/Models/IUpdateModel.cs @@ -1,14 +1,13 @@ -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a model that updates using a delta time +/// +public interface IUpdateModel { /// - /// Represents a model that updates using a delta time + /// Performs an update on the model /// - public interface IUpdateModel - { - /// - /// Performs an update on the model - /// - /// The timeline to apply during update - void Update(Timeline timeline); - } + /// The timeline to apply during update + void Update(Timeline timeline); } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/AdaptionHints/CategoryAdaptionHint.cs b/src/Artemis.Core/Models/Profile/AdaptionHints/CategoryAdaptionHint.cs index 18e87f574..1d526f4b0 100644 --- a/src/Artemis.Core/Models/Profile/AdaptionHints/CategoryAdaptionHint.cs +++ b/src/Artemis.Core/Models/Profile/AdaptionHints/CategoryAdaptionHint.cs @@ -2,98 +2,97 @@ using System.Linq; using Artemis.Storage.Entities.Profile.AdaptionHints; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a hint that adapts layers to a certain category of devices +/// +public class CategoryAdaptionHint : CorePropertyChanged, IAdaptionHint { + private int _amount; + private DeviceCategory _category; + private bool _limitAmount; + private int _skip; + /// - /// Represents a hint that adapts layers to a certain category of devices + /// Creates a new instance of the class /// - public class CategoryAdaptionHint : CorePropertyChanged, IAdaptionHint + public CategoryAdaptionHint() { - private DeviceCategory _category; - private int _skip; - private bool _limitAmount; - private int _amount; - - /// - /// Creates a new instance of the class - /// - public CategoryAdaptionHint() - { - } - - internal CategoryAdaptionHint(CategoryAdaptionHintEntity entity) - { - Category = (DeviceCategory) entity.Category; - Skip = entity.Skip; - LimitAmount = entity.LimitAmount; - Amount = entity.Amount; - } - - /// - /// Gets or sets the category of devices LEDs will be applied to - /// - public DeviceCategory Category - { - get => _category; - set => SetAndNotify(ref _category, value); - } - - /// - /// Gets or sets the amount of devices to skip - /// - public int Skip - { - get => _skip; - set => SetAndNotify(ref _skip, value); - } - - /// - /// Gets or sets a boolean indicating whether a limited amount of devices should be used - /// - public bool LimitAmount - { - get => _limitAmount; - set => SetAndNotify(ref _limitAmount, value); - } - - /// - /// Gets or sets the amount of devices to limit to if is - /// - public int Amount - { - get => _amount; - set => SetAndNotify(ref _amount, value); - } - - /// - public override string ToString() - { - return $"Category adaption - {nameof(Category)}: {Category}, {nameof(Skip)}: {Skip}, {nameof(LimitAmount)}: {LimitAmount}, {nameof(Amount)}: {Amount}"; - } - - #region Implementation of IAdaptionHint - - /// - public void Apply(Layer layer, List devices) - { - IEnumerable 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); - } - - /// - 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; + } + + /// + /// Gets or sets the category of devices LEDs will be applied to + /// + public DeviceCategory Category + { + get => _category; + set => SetAndNotify(ref _category, value); + } + + /// + /// Gets or sets the amount of devices to skip + /// + public int Skip + { + get => _skip; + set => SetAndNotify(ref _skip, value); + } + + /// + /// Gets or sets a boolean indicating whether a limited amount of devices should be used + /// + public bool LimitAmount + { + get => _limitAmount; + set => SetAndNotify(ref _limitAmount, value); + } + + /// + /// Gets or sets the amount of devices to limit to if is + /// + public int Amount + { + get => _amount; + set => SetAndNotify(ref _amount, value); + } + + /// + public override string ToString() + { + return $"Category adaption - {nameof(Category)}: {Category}, {nameof(Skip)}: {Skip}, {nameof(LimitAmount)}: {LimitAmount}, {nameof(Amount)}: {Amount}"; + } + + #region Implementation of IAdaptionHint + + /// + public void Apply(Layer layer, List devices) + { + IEnumerable 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); + } + + /// + public IAdaptionHintEntity GetEntry() + { + return new CategoryAdaptionHintEntity {Amount = Amount, LimitAmount = LimitAmount, Category = (int) Category, Skip = Skip}; + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/AdaptionHints/DeviceAdaptionHint.cs b/src/Artemis.Core/Models/Profile/AdaptionHints/DeviceAdaptionHint.cs index 62ed05b85..d7936bae7 100644 --- a/src/Artemis.Core/Models/Profile/AdaptionHints/DeviceAdaptionHint.cs +++ b/src/Artemis.Core/Models/Profile/AdaptionHints/DeviceAdaptionHint.cs @@ -3,98 +3,97 @@ using System.Linq; using Artemis.Storage.Entities.Profile.AdaptionHints; using RGB.NET.Core; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a hint that adapts layers to a certain type of devices +/// +public class DeviceAdaptionHint : CorePropertyChanged, IAdaptionHint { + private int _amount; + private RGBDeviceType _deviceType; + private bool _limitAmount; + private int _skip; + /// - /// Represents a hint that adapts layers to a certain type of devices + /// Creates a new instance of the class /// - public class DeviceAdaptionHint : CorePropertyChanged, IAdaptionHint + public DeviceAdaptionHint() { - private RGBDeviceType _deviceType; - private int _skip; - private bool _limitAmount; - private int _amount; - - /// - /// Creates a new instance of the class - /// - public DeviceAdaptionHint() - { - } - - internal DeviceAdaptionHint(DeviceAdaptionHintEntity entity) - { - DeviceType = (RGBDeviceType) entity.DeviceType; - Skip = entity.Skip; - LimitAmount = entity.LimitAmount; - Amount = entity.Amount; - } - - /// - /// Gets or sets the type of devices LEDs will be applied to - /// - public RGBDeviceType DeviceType - { - get => _deviceType; - set => SetAndNotify(ref _deviceType, value); - } - - /// - /// Gets or sets the amount of devices to skip - /// - public int Skip - { - get => _skip; - set => SetAndNotify(ref _skip, value); - } - - /// - /// Gets or sets a boolean indicating whether a limited amount of devices should be used - /// - public bool LimitAmount - { - get => _limitAmount; - set => SetAndNotify(ref _limitAmount, value); - } - - /// - /// Gets or sets the amount of devices to limit to if is - /// - public int Amount - { - get => _amount; - set => SetAndNotify(ref _amount, value); - } - - #region Implementation of IAdaptionHint - - /// - public void Apply(Layer layer, List devices) - { - IEnumerable 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); - } - - /// - public IAdaptionHintEntity GetEntry() - { - return new DeviceAdaptionHintEntity {Amount = Amount, LimitAmount = LimitAmount, DeviceType = (int) DeviceType, Skip = Skip}; - } - - /// - 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; + } + + /// + /// Gets or sets the type of devices LEDs will be applied to + /// + public RGBDeviceType DeviceType + { + get => _deviceType; + set => SetAndNotify(ref _deviceType, value); + } + + /// + /// Gets or sets the amount of devices to skip + /// + public int Skip + { + get => _skip; + set => SetAndNotify(ref _skip, value); + } + + /// + /// Gets or sets a boolean indicating whether a limited amount of devices should be used + /// + public bool LimitAmount + { + get => _limitAmount; + set => SetAndNotify(ref _limitAmount, value); + } + + /// + /// Gets or sets the amount of devices to limit to if is + /// + public int Amount + { + get => _amount; + set => SetAndNotify(ref _amount, value); + } + + #region Implementation of IAdaptionHint + + /// + public void Apply(Layer layer, List devices) + { + IEnumerable 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); + } + + /// + public IAdaptionHintEntity GetEntry() + { + return new DeviceAdaptionHintEntity {Amount = Amount, LimitAmount = LimitAmount, DeviceType = (int) DeviceType, Skip = Skip}; + } + + /// + public override string ToString() + { + return $"Device adaption - {nameof(DeviceType)}: {DeviceType}, {nameof(Skip)}: {Skip}, {nameof(LimitAmount)}: {LimitAmount}, {nameof(Amount)}: {Amount}"; + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/AdaptionHints/IAdaptionHint.cs b/src/Artemis.Core/Models/Profile/AdaptionHints/IAdaptionHint.cs index 48c3dc29a..cd86f4606 100644 --- a/src/Artemis.Core/Models/Profile/AdaptionHints/IAdaptionHint.cs +++ b/src/Artemis.Core/Models/Profile/AdaptionHints/IAdaptionHint.cs @@ -1,23 +1,22 @@ using System.Collections.Generic; using Artemis.Storage.Entities.Profile.AdaptionHints; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents an adaption hint that's used to adapt a layer to a set of devices +/// +public interface IAdaptionHint { /// - /// Represents an adaption hint that's used to adapt a layer to a set of devices + /// Applies the adaptive action to the provided layer /// - public interface IAdaptionHint - { - /// - /// Applies the adaptive action to the provided layer - /// - /// The layer to adapt - /// The devices to adapt the layer for - void Apply(Layer layer, List devices); + /// The layer to adapt + /// The devices to adapt the layer for + void Apply(Layer layer, List devices); - /// - /// Returns an adaption hint entry for this adaption hint used for persistent storage - /// - IAdaptionHintEntity GetEntry(); - } + /// + /// Returns an adaption hint entry for this adaption hint used for persistent storage + /// + IAdaptionHintEntity GetEntry(); } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/AdaptionHints/KeyboardSectionAdaptionHint.cs b/src/Artemis.Core/Models/Profile/AdaptionHints/KeyboardSectionAdaptionHint.cs index cf61108c4..2415a9082 100644 --- a/src/Artemis.Core/Models/Profile/AdaptionHints/KeyboardSectionAdaptionHint.cs +++ b/src/Artemis.Core/Models/Profile/AdaptionHints/KeyboardSectionAdaptionHint.cs @@ -4,89 +4,88 @@ using System.Linq; using Artemis.Storage.Entities.Profile.AdaptionHints; using RGB.NET.Core; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a hint that adapts layers to a certain region of keyboards +/// +public class KeyboardSectionAdaptionHint : CorePropertyChanged, IAdaptionHint +{ + private static readonly Dictionary> RegionLedIds = new() + { + {KeyboardSection.MacroKeys, Enum.GetValues().Where(l => l >= LedId.Keyboard_Programmable1 && l <= LedId.Keyboard_Programmable32).ToList()}, + {KeyboardSection.LedStrips, Enum.GetValues().Where(l => l >= LedId.LedStripe1 && l <= LedId.LedStripe128).ToList()}, + {KeyboardSection.Extra, Enum.GetValues().Where(l => l >= LedId.Keyboard_Custom1 && l <= LedId.Keyboard_Custom64).ToList()} + }; + + private KeyboardSection _section; + + /// + /// Creates a new instance of the class + /// + public KeyboardSectionAdaptionHint() + { + } + + internal KeyboardSectionAdaptionHint(KeyboardSectionAdaptionHintEntity entity) + { + Section = (KeyboardSection) entity.Section; + } + + /// + /// Gets or sets the section this hint will apply LEDs to + /// + public KeyboardSection Section + { + get => _section; + set => SetAndNotify(ref _section, value); + } + + #region Implementation of IAdaptionHint + + /// + public void Apply(Layer layer, List devices) + { + // Only keyboards should have the LEDs we care about + foreach (ArtemisDevice keyboard in devices.Where(d => d.DeviceType == RGBDeviceType.Keyboard)) + { + List ledIds = RegionLedIds[Section]; + layer.AddLeds(keyboard.Leds.Where(l => ledIds.Contains(l.RgbLed.Id))); + } + } + + /// + public IAdaptionHintEntity GetEntry() + { + return new KeyboardSectionAdaptionHintEntity {Section = (int) Section}; + } + + /// + public override string ToString() + { + return $"Keyboard section adaption - {nameof(Section)}: {Section}"; + } + + #endregion +} + +/// +/// Represents a section of LEDs on a keyboard +/// +public enum KeyboardSection { /// - /// Represents a hint that adapts layers to a certain region of keyboards + /// A region containing the macro keys of a keyboard /// - public class KeyboardSectionAdaptionHint : CorePropertyChanged, IAdaptionHint - { - private static readonly Dictionary> RegionLedIds = new() - { - {KeyboardSection.MacroKeys, Enum.GetValues().Where(l => l >= LedId.Keyboard_Programmable1 && l <= LedId.Keyboard_Programmable32).ToList()}, - {KeyboardSection.LedStrips, Enum.GetValues().Where(l => l >= LedId.LedStripe1 && l <= LedId.LedStripe128).ToList()}, - {KeyboardSection.Extra, Enum.GetValues().Where(l => l >= LedId.Keyboard_Custom1 && l <= LedId.Keyboard_Custom64).ToList()} - }; - - private KeyboardSection _section; - - /// - /// Creates a new instance of the class - /// - public KeyboardSectionAdaptionHint() - { - } - - internal KeyboardSectionAdaptionHint(KeyboardSectionAdaptionHintEntity entity) - { - Section = (KeyboardSection) entity.Section; - } - - /// - /// Gets or sets the section this hint will apply LEDs to - /// - public KeyboardSection Section - { - get => _section; - set => SetAndNotify(ref _section, value); - } - - #region Implementation of IAdaptionHint - - /// - public void Apply(Layer layer, List devices) - { - // Only keyboards should have the LEDs we care about - foreach (ArtemisDevice keyboard in devices.Where(d => d.DeviceType == RGBDeviceType.Keyboard)) - { - List ledIds = RegionLedIds[Section]; - layer.AddLeds(keyboard.Leds.Where(l => ledIds.Contains(l.RgbLed.Id))); - } - } - - /// - public IAdaptionHintEntity GetEntry() - { - return new KeyboardSectionAdaptionHintEntity {Section = (int) Section}; - } - - /// - public override string ToString() - { - return $"Keyboard section adaption - {nameof(Section)}: {Section}"; - } - - #endregion - } + MacroKeys, /// - /// Represents a section of LEDs on a keyboard + /// A region containing the LED strips of a keyboard /// - public enum KeyboardSection - { - /// - /// A region containing the macro keys of a keyboard - /// - MacroKeys, + LedStrips, - /// - /// A region containing the LED strips of a keyboard - /// - LedStrips, - - /// - /// A region containing extra non-standard LEDs of a keyboard - /// - Extra - } + /// + /// A region containing extra non-standard LEDs of a keyboard + /// + Extra } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs index 1e34d5348..bd9fe7c7a 100644 --- a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs +++ b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs @@ -83,14 +83,10 @@ public class ColorGradient : IList, IList, INotifyCollectionC { List 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, IList, INotifyCollectionC return false; for (int i = 0; i < Count; i++) + { if (!Equals(this[i], other[i])) return false; + } return true; } diff --git a/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs b/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs index 205c772ff..8ccddbf24 100644 --- a/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs +++ b/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs @@ -1,69 +1,68 @@ using System; using SkiaSharp; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// A color with a position, usually contained in a +/// +public class ColorGradientStop : CorePropertyChanged { + private SKColor _color; + private float _position; + /// - /// A color with a position, usually contained in a + /// Creates a new instance of the class /// - public class ColorGradientStop : CorePropertyChanged + public ColorGradientStop(SKColor color, float position) { - #region Equality members - - /// - protected bool Equals(ColorGradientStop other) - { - return _color.Equals(other._color) && _position.Equals(other._position); - } - - /// - 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); - } - - /// - public override int GetHashCode() - { - return HashCode.Combine(_color, _position); - } - - #endregion - - private SKColor _color; - private float _position; - - /// - /// Creates a new instance of the class - /// - public ColorGradientStop(SKColor color, float position) - { - Color = color; - Position = position; - } - - /// - /// Gets or sets the color of the stop - /// - public SKColor Color - { - get => _color; - set => SetAndNotify(ref _color, value); - } - - /// - /// Gets or sets the position of the stop - /// - public float Position - { - get => _position; - set => SetAndNotify(ref _position, value); - } + Color = color; + Position = position; } + + /// + /// Gets or sets the color of the stop + /// + public SKColor Color + { + get => _color; + set => SetAndNotify(ref _color, value); + } + + /// + /// Gets or sets the position of the stop + /// + public float Position + { + get => _position; + set => SetAndNotify(ref _position, value); + } + + #region Equality members + + /// + protected bool Equals(ColorGradientStop other) + { + return _color.Equals(other._color) && _position.Equals(other._position); + } + + /// + 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); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(_color, _position); + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/AlwaysOnCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/AlwaysOnCondition.cs index a5463dfd3..eeeece9c5 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/AlwaysOnCondition.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/AlwaysOnCondition.cs @@ -2,89 +2,84 @@ using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.Conditions; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a condition that is always true. +/// +public class AlwaysOnCondition : ICondition { /// - /// Represents a condition that is always true. + /// Creates a new instance of the class. /// - public class AlwaysOnCondition : ICondition + /// The profile element this condition applies to. + public AlwaysOnCondition(RenderProfileElement profileElement) { - /// - /// Creates a new instance of the class. - /// - /// The profile element this condition applies to. - public AlwaysOnCondition(RenderProfileElement profileElement) - { - ProfileElement = profileElement; - Entity = new AlwaysOnConditionEntity(); - } - - /// - /// Creates a new instance of the class. - /// - /// The entity used to store this condition. - /// The profile element this condition applies to. - public AlwaysOnCondition(AlwaysOnConditionEntity alwaysOnConditionEntity, RenderProfileElement profileElement) - { - ProfileElement = profileElement; - Entity = alwaysOnConditionEntity; - } - - #region Implementation of IDisposable - - /// - public void Dispose() - { - } - - #endregion - - #region Implementation of IStorageModel - - /// - public void Load() - { - } - - /// - public void Save() - { - } - - #endregion - - #region Implementation of ICondition - - /// - public IConditionEntity Entity { get; } - - /// - public RenderProfileElement ProfileElement { get; } - - /// - public bool IsMet { get; private set; } - - /// - public void Update() - { - if (ProfileElement.Parent is RenderProfileElement parent) - IsMet = parent.DisplayConditionMet; - else - IsMet = true; - } - - /// - public void UpdateTimeline(double deltaTime) - { - ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), true); - } - - /// - public void OverrideTimeline(TimeSpan position) - { - ProfileElement.Timeline.Override(position, position > ProfileElement.Timeline.Length); - } - - #endregion + ProfileElement = profileElement; + Entity = new AlwaysOnConditionEntity(); } + + /// + /// Creates a new instance of the class. + /// + /// The entity used to store this condition. + /// The profile element this condition applies to. + public AlwaysOnCondition(AlwaysOnConditionEntity alwaysOnConditionEntity, RenderProfileElement profileElement) + { + ProfileElement = profileElement; + Entity = alwaysOnConditionEntity; + } + + /// + public void Dispose() + { + } + + #region Implementation of IStorageModel + + /// + public void Load() + { + } + + /// + public void Save() + { + } + + #endregion + + #region Implementation of ICondition + + /// + public IConditionEntity Entity { get; } + + /// + public RenderProfileElement ProfileElement { get; } + + /// + public bool IsMet { get; private set; } + + /// + public void Update() + { + if (ProfileElement.Parent is RenderProfileElement parent) + IsMet = parent.DisplayConditionMet; + else + IsMet = true; + } + + /// + public void UpdateTimeline(double deltaTime) + { + ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), true); + } + + /// + public void OverrideTimeline(TimeSpan position) + { + ProfileElement.Timeline.Override(position, position > ProfileElement.Timeline.Length); + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs index 011805946..a517cd0e6 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs @@ -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 _script; - private bool _wasMet; private DateTime _lastProcessedTrigger; private object? _lastProcessedValue; private EventOverlapMode _overlapMode; - private EventTriggerMode _triggerMode; + private NodeScript _script; + private IEventConditionNode _startNode; private EventToggleOffMode _toggleOffMode; + private EventTriggerMode _triggerMode; + private bool _wasMet; /// /// Creates a new instance of the class @@ -87,7 +87,8 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition } /// - /// Gets or sets the mode for render elements when toggling off the event when using . + /// Gets or sets the mode for render elements when toggling off the event when using + /// . /// 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(); } + /// + /// Gets the start node of the event script, if any + /// + /// The start node of the event script, if any. + 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); } - /// - /// Gets the start node of the event script, if any - /// - /// The start node of the event script, if any. - 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($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", _entity.Script, ProfileElement.Profile) - : new NodeScript($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile); + : new NodeScript($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile); UpdateEventNode(); } @@ -356,7 +362,8 @@ public enum EventOverlapMode } /// -/// Represents a mode for render elements when toggling off the event when using . +/// Represents a mode for render elements when toggling off the event when using +/// . /// public enum EventToggleOffMode { diff --git a/src/Artemis.Core/Models/Profile/Conditions/ICondition.cs b/src/Artemis.Core/Models/Profile/Conditions/ICondition.cs index bddd6867b..ef3b17c13 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/ICondition.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/ICondition.cs @@ -30,12 +30,12 @@ public interface ICondition : IDisposable, IStorageModel void Update(); /// - /// Updates the timeline according to the provided as the display condition sees fit. + /// Updates the timeline according to the provided as the display condition sees fit. /// void UpdateTimeline(double deltaTime); /// - /// Overrides the timeline to the provided as the display condition sees fit. + /// Overrides the timeline to the provided as the display condition sees fit. /// void OverrideTimeline(TimeSpan position); } diff --git a/src/Artemis.Core/Models/Profile/Conditions/PlayOnceCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/PlayOnceCondition.cs index 459e24e19..40178ad6e 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/PlayOnceCondition.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/PlayOnceCondition.cs @@ -2,89 +2,84 @@ using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.Conditions; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a condition that plays once when its script evaluates to . +/// +public class PlayOnceCondition : ICondition { /// - /// Represents a condition that plays once when its script evaluates to . + /// Creates a new instance of the class. /// - public class PlayOnceCondition : ICondition + /// The profile element this condition applies to. + public PlayOnceCondition(RenderProfileElement profileElement) { - /// - /// Creates a new instance of the class. - /// - /// The profile element this condition applies to. - public PlayOnceCondition(RenderProfileElement profileElement) - { - ProfileElement = profileElement; - Entity = new PlayOnceConditionEntity(); - } - - /// - /// Creates a new instance of the class. - /// - /// The entity used to store this condition. - /// The profile element this condition applies to. - public PlayOnceCondition(PlayOnceConditionEntity entity, RenderProfileElement profileElement) - { - ProfileElement = profileElement; - Entity = entity; - } - - #region Implementation of IDisposable - - /// - public void Dispose() - { - } - - #endregion - - #region Implementation of IStorageModel - - /// - public void Load() - { - } - - /// - public void Save() - { - } - - #endregion - - #region Implementation of ICondition - - /// - public IConditionEntity Entity { get; } - - /// - public RenderProfileElement ProfileElement { get; } - - /// - public bool IsMet { get; private set; } - - /// - public void Update() - { - if (ProfileElement.Parent is RenderProfileElement parent) - IsMet = parent.DisplayConditionMet; - else - IsMet = true; - } - - /// - public void UpdateTimeline(double deltaTime) - { - ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), false); - } - - /// - public void OverrideTimeline(TimeSpan position) - { - ProfileElement.Timeline.Override(position, false); - } - - #endregion + ProfileElement = profileElement; + Entity = new PlayOnceConditionEntity(); } + + /// + /// Creates a new instance of the class. + /// + /// The entity used to store this condition. + /// The profile element this condition applies to. + public PlayOnceCondition(PlayOnceConditionEntity entity, RenderProfileElement profileElement) + { + ProfileElement = profileElement; + Entity = entity; + } + + /// + public void Dispose() + { + } + + #region Implementation of IStorageModel + + /// + public void Load() + { + } + + /// + public void Save() + { + } + + #endregion + + #region Implementation of ICondition + + /// + public IConditionEntity Entity { get; } + + /// + public RenderProfileElement ProfileElement { get; } + + /// + public bool IsMet { get; private set; } + + /// + public void Update() + { + if (ProfileElement.Parent is RenderProfileElement parent) + IsMet = parent.DisplayConditionMet; + else + IsMet = true; + } + + /// + public void UpdateTimeline(double deltaTime) + { + ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), false); + } + + /// + public void OverrideTimeline(TimeSpan position) + { + ProfileElement.Timeline.Override(position, false); + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/StaticCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/StaticCondition.cs index 8ab4abd96..5ab81e22c 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/StaticCondition.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/StaticCondition.cs @@ -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; + +/// +/// Represents a condition that is based on a data model value +/// +public class StaticCondition : CorePropertyChanged, INodeScriptCondition +{ + private readonly string _displayName; + private readonly StaticConditionEntity _entity; + private StaticPlayMode _playMode; + private StaticStopMode _stopMode; + private bool _wasMet; + + /// + /// Creates a new instance of the class + /// + public StaticCondition(RenderProfileElement profileElement) + { + _entity = new StaticConditionEntity(); + _displayName = profileElement.GetType().Name; + + ProfileElement = profileElement; + Script = new NodeScript($"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(); + } + + /// + /// Gets the script that drives the static condition + /// + public NodeScript Script { get; private set; } + + /// + /// Gets or sets the mode in which the render element starts its timeline when display conditions are met + /// + public StaticPlayMode PlayMode + { + get => _playMode; + set => SetAndNotify(ref _playMode, value); + } + + /// + /// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met + /// + public StaticStopMode StopMode + { + get => _stopMode; + set => SetAndNotify(ref _stopMode, value); + } + + /// + public IConditionEntity Entity => _entity; + + /// + public RenderProfileElement ProfileElement { get; } + + /// + public bool IsMet { get; private set; } + + /// + 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; + } + } + + /// + 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); + } + + /// + public void OverrideTimeline(TimeSpan position) + { + ProfileElement.Timeline.Override(position, PlayMode == StaticPlayMode.Repeat && position > ProfileElement.Timeline.Length); + } + + /// + public void Dispose() + { + Script.Dispose(); + } + + #region Storage + + /// + public void Load() + { + PlayMode = (StaticPlayMode) _entity.PlayMode; + StopMode = (StaticStopMode) _entity.StopMode; + + Script = _entity.Script != null + ? new NodeScript($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", _entity.Script, ProfileElement.Profile) + : new NodeScript($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", ProfileElement.Profile); + } + + /// + 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; + } + } + + /// + public INodeScript? NodeScript => Script; + + /// + public void LoadNodeScript() + { + Script.Load(); + } + + #endregion +} + +/// +/// Represents a mode for render elements to start their timeline when display conditions are met +/// +public enum StaticPlayMode { /// - /// Represents a condition that is based on a data model value + /// Continue repeating the main segment of the timeline while the condition is met /// - public class StaticCondition : CorePropertyChanged, INodeScriptCondition - { - private readonly string _displayName; - private readonly StaticConditionEntity _entity; - private StaticPlayMode _playMode; - private StaticStopMode _stopMode; - private bool _wasMet; - - /// - /// Creates a new instance of the class - /// - public StaticCondition(RenderProfileElement profileElement) - { - _entity = new StaticConditionEntity(); - _displayName = profileElement.GetType().Name; - - ProfileElement = profileElement; - Script = new NodeScript($"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(); - } - - /// - /// Gets the script that drives the static condition - /// - public NodeScript Script { get; private set; } - - /// - public IConditionEntity Entity => _entity; - - /// - public RenderProfileElement ProfileElement { get; } - - /// - public bool IsMet { get; private set; } - - /// - /// Gets or sets the mode in which the render element starts its timeline when display conditions are met - /// - public StaticPlayMode PlayMode - { - get => _playMode; - set => SetAndNotify(ref _playMode, value); - } - - /// - /// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met - /// - public StaticStopMode StopMode - { - get => _stopMode; - set => SetAndNotify(ref _stopMode, value); - } - - /// - 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; - } - } - - /// - 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); - } - - /// - public void OverrideTimeline(TimeSpan position) - { - ProfileElement.Timeline.Override(position, PlayMode == StaticPlayMode.Repeat && position > ProfileElement.Timeline.Length); - } - - /// - public void Dispose() - { - Script.Dispose(); - } - - #region Storage - - /// - public void Load() - { - PlayMode = (StaticPlayMode) _entity.PlayMode; - StopMode = (StaticStopMode) _entity.StopMode; - - Script = _entity.Script != null - ? new NodeScript($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", _entity.Script, ProfileElement.Profile) - : new NodeScript($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", ProfileElement.Profile); - } - - /// - 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; - } - } - - /// - public INodeScript? NodeScript => Script; - - /// - public void LoadNodeScript() - { - Script.Load(); - } - - #endregion - } + Repeat, /// - /// 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 /// - public enum StaticPlayMode - { - /// - /// Continue repeating the main segment of the timeline while the condition is met - /// - Repeat, + Once +} - /// - /// Only play the timeline once when the condition is met - /// - Once - } +/// +/// Represents a mode for render elements to stop their timeline when display conditions are no longer met +/// +public enum StaticStopMode +{ + /// + /// When conditions are no longer met, finish the the current run of the main timeline + /// + Finish, /// - /// 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 /// - public enum StaticStopMode - { - /// - /// When conditions are no longer met, finish the the current run of the main timeline - /// - Finish, - - /// - /// When conditions are no longer met, skip to the end segment of the timeline - /// - SkipToEnd - } + SkipToEnd } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs index d4bdc6d13..42b10ccde 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs @@ -4,242 +4,243 @@ using System.Collections.ObjectModel; using System.Linq; using Artemis.Storage.Entities.Profile.DataBindings; -namespace Artemis.Core +namespace Artemis.Core; + +/// +public class DataBinding : IDataBinding { - /// - public class DataBinding : IDataBinding + private readonly List _properties = new(); + private bool _disposed; + private bool _isEnabled; + private DataBindingNodeScript _script; + + internal DataBinding(LayerProperty layerProperty) { - private readonly List _properties = new(); - private bool _disposed; - private bool _isEnabled; - private DataBindingNodeScript _script; + LayerProperty = layerProperty; - internal DataBinding(LayerProperty layerProperty) - { - LayerProperty = layerProperty; + Entity = new DataBindingEntity(); + _script = new DataBindingNodeScript(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile); - Entity = new DataBindingEntity(); - _script = new DataBindingNodeScript(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile); - - Save(); - } - - internal DataBinding(LayerProperty layerProperty, DataBindingEntity entity) - { - LayerProperty = layerProperty; - - Entity = entity; - _script = new DataBindingNodeScript(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile); - - // Load will add children so be initialized before that - Load(); - } - - /// - /// Gets the layer property this data binding targets - /// - public LayerProperty LayerProperty { get; } - - /// - public INodeScript Script => _script; - - /// - /// Gets the data binding entity this data binding uses for persistent storage - /// - public DataBindingEntity Entity { get; } - - /// - /// Updates the pending values of this data binding - /// - public void Update() - { - if (_disposed) - throw new ObjectDisposedException("DataBinding"); - - if (!IsEnabled) - return; - - // TODO: Update the 'base value' node - - Script.Run(); - } - - /// - /// Registers a data binding property so that is available to the data binding system - /// - /// The type of the layer property - /// The function to call to get the value of the property - /// The action to call to set the value of the property - /// The display name of the data binding property - public DataBindingProperty RegisterDataBindingProperty(Func getter, Action 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 property = new(getter, setter, displayName); - _properties.Add(property); - - OnDataBindingPropertyRegistered(); - return property; - } - - /// - /// Removes all data binding properties so they are no longer available to the data binding system - /// - public void ClearDataBindingProperties() - { - if (_disposed) - throw new ObjectDisposedException("LayerProperty"); - - _properties.Clear(); - OnDataBindingPropertiesCleared(); - } - - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _disposed = true; - _isEnabled = false; - - Script.Dispose(); - } - } - - /// - /// Invokes the event - /// - protected virtual void OnDataBindingPropertyRegistered() - { - DataBindingPropertyRegistered?.Invoke(this, new DataBindingEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnDataBindingPropertiesCleared() - { - DataBindingPropertiesCleared?.Invoke(this, new DataBindingEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnDataBindingEnabled(DataBindingEventArgs e) - { - DataBindingEnabled?.Invoke(this, e); - } - - /// - /// Invokes the event - /// - protected virtual void OnDataBindingDisabled(DataBindingEventArgs e) - { - DataBindingDisabled?.Invoke(this, e); - } - - private string GetScriptName() - { - return LayerProperty.PropertyDescription.Name ?? LayerProperty.Path; - } - - /// - public ILayerProperty BaseLayerProperty => LayerProperty; - - /// - public bool IsEnabled - { - get => _isEnabled; - set - { - _isEnabled = value; - - if (_isEnabled) - OnDataBindingEnabled(new DataBindingEventArgs(this)); - else - OnDataBindingDisabled(new DataBindingEventArgs(this)); - } - } - - /// - public ReadOnlyCollection Properties => _properties.AsReadOnly(); - - /// - public void Apply() - { - if (_disposed) - throw new ObjectDisposedException("DataBinding"); - - if (!IsEnabled) - return; - - _script.DataBindingExitNode.ApplyToDataBinding(); - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - public event EventHandler? DataBindingPropertyRegistered; - - /// - public event EventHandler? DataBindingPropertiesCleared; - - /// - public event EventHandler? DataBindingEnabled; - - /// - public event EventHandler? DataBindingDisabled; - - #region Storage - - /// - public void Load() - { - if (_disposed) - throw new ObjectDisposedException("DataBinding"); - - IsEnabled = Entity.IsEnabled; - } - - /// - public void LoadNodeScript() - { - _script.Dispose(); - _script = Entity.NodeScript != null - ? new DataBindingNodeScript(GetScriptName(), "The value to put into the data binding", this, Entity.NodeScript, LayerProperty.ProfileElement.Profile) - : new DataBindingNodeScript(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile); - } - - /// - 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 layerProperty, DataBindingEntity entity) + { + LayerProperty = layerProperty; + + Entity = entity; + _script = new DataBindingNodeScript(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile); + + // Load will add children so be initialized before that + Load(); + } + + /// + /// Gets the layer property this data binding targets + /// + public LayerProperty LayerProperty { get; } + + /// + /// Gets the data binding entity this data binding uses for persistent storage + /// + public DataBindingEntity Entity { get; } + + /// + /// Updates the pending values of this data binding + /// + public void Update() + { + if (_disposed) + throw new ObjectDisposedException("DataBinding"); + + if (!IsEnabled) + return; + + // TODO: Update the 'base value' node + + Script.Run(); + } + + /// + /// Registers a data binding property so that is available to the data binding system + /// + /// The type of the layer property + /// The function to call to get the value of the property + /// The action to call to set the value of the property + /// The display name of the data binding property + public DataBindingProperty RegisterDataBindingProperty(Func getter, Action 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 property = new(getter, setter, displayName); + _properties.Add(property); + + OnDataBindingPropertyRegistered(); + return property; + } + + /// + /// Removes all data binding properties so they are no longer available to the data binding system + /// + public void ClearDataBindingProperties() + { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + _properties.Clear(); + OnDataBindingPropertiesCleared(); + } + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _disposed = true; + _isEnabled = false; + + Script.Dispose(); + } + } + + /// + /// Invokes the event + /// + protected virtual void OnDataBindingPropertyRegistered() + { + DataBindingPropertyRegistered?.Invoke(this, new DataBindingEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnDataBindingPropertiesCleared() + { + DataBindingPropertiesCleared?.Invoke(this, new DataBindingEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnDataBindingEnabled(DataBindingEventArgs e) + { + DataBindingEnabled?.Invoke(this, e); + } + + /// + /// Invokes the event + /// + protected virtual void OnDataBindingDisabled(DataBindingEventArgs e) + { + DataBindingDisabled?.Invoke(this, e); + } + + private string GetScriptName() + { + return LayerProperty.PropertyDescription.Name ?? LayerProperty.Path; + } + + /// + public INodeScript Script => _script; + + /// + public ILayerProperty BaseLayerProperty => LayerProperty; + + /// + public bool IsEnabled + { + get => _isEnabled; + set + { + _isEnabled = value; + + if (_isEnabled) + OnDataBindingEnabled(new DataBindingEventArgs(this)); + else + OnDataBindingDisabled(new DataBindingEventArgs(this)); + } + } + + /// + public ReadOnlyCollection Properties => _properties.AsReadOnly(); + + /// + public void Apply() + { + if (_disposed) + throw new ObjectDisposedException("DataBinding"); + + if (!IsEnabled) + return; + + _script.DataBindingExitNode.ApplyToDataBinding(); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public event EventHandler? DataBindingPropertyRegistered; + + /// + public event EventHandler? DataBindingPropertiesCleared; + + /// + public event EventHandler? DataBindingEnabled; + + /// + public event EventHandler? DataBindingDisabled; + + #region Storage + + /// + public void Load() + { + if (_disposed) + throw new ObjectDisposedException("DataBinding"); + + IsEnabled = Entity.IsEnabled; + } + + /// + public void LoadNodeScript() + { + _script.Dispose(); + _script = Entity.NodeScript != null + ? new DataBindingNodeScript(GetScriptName(), "The value to put into the data binding", this, Entity.NodeScript, LayerProperty.ProfileElement.Profile) + : new DataBindingNodeScript(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile); + } + + /// + 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 } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingProperty.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingProperty.cs index 7c09381fd..13a5c9e27 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingProperty.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingProperty.cs @@ -1,63 +1,62 @@ using System; -namespace Artemis.Core +namespace Artemis.Core; + +/// +public class DataBindingProperty : IDataBindingProperty { - /// - public class DataBindingProperty : IDataBindingProperty + internal DataBindingProperty(Func getter, Action setter, string displayName) { - internal DataBindingProperty(Func getter, Action setter, string displayName) + Getter = getter ?? throw new ArgumentNullException(nameof(getter)); + Setter = setter ?? throw new ArgumentNullException(nameof(setter)); + DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName)); + } + + /// + /// Gets the function to call to get the value of the property + /// + public Func Getter { get; } + + /// + /// Gets the action to call to set the value of the property + /// + public Action Setter { get; } + + /// + public string DisplayName { get; } + + /// + public Type ValueType => typeof(TProperty); + + /// + public object? GetValue() + { + return Getter(); + } + + /// + 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)); - } - - /// - /// Gets the function to call to get the value of the property - /// - public Func Getter { get; } - - /// - /// Gets the action to call to set the value of the property - /// - public Action Setter { get; } - - /// - public string DisplayName { get; } - - /// - public Type ValueType => typeof(TProperty); - - /// - public object? GetValue() - { - return Getter(); - } - - /// - 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 floatSetter: - floatSetter(numeric); - break; - case Numeric numeric when Setter is Action intSetter: - intSetter(numeric); - break; - case Numeric numeric when Setter is Action doubleSetter: - doubleSetter(numeric); - break; - case Numeric numeric when Setter is Action 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 floatSetter: + floatSetter(numeric); + break; + case Numeric numeric when Setter is Action intSetter: + intSetter(numeric); + break; + case Numeric numeric when Setter is Action doubleSetter: + doubleSetter(numeric); + break; + case Numeric numeric when Setter is Action byteSetter: + byteSetter(numeric); + break; + default: + throw new ArgumentException("Value must match the type of the data binding registration", nameof(value)); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs index 8c56aa298..a5856d018 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs @@ -2,62 +2,61 @@ using System.Collections.ObjectModel; using Artemis.Core.Modules; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a data binding that binds a certain to a value inside a +/// +/// +public interface IDataBinding : IStorageModel, IDisposable { /// - /// Represents a data binding that binds a certain to a value inside a - /// + /// Gets the layer property the data binding is applied to /// - public interface IDataBinding : IStorageModel, IDisposable - { - /// - /// Gets the layer property the data binding is applied to - /// - ILayerProperty BaseLayerProperty { get; } + ILayerProperty BaseLayerProperty { get; } - /// - /// Gets the script used to populate the data binding - /// - INodeScript Script { get; } + /// + /// Gets the script used to populate the data binding + /// + INodeScript Script { get; } - /// - /// Gets a list of sub-properties this data binding applies to - /// - ReadOnlyCollection Properties { get; } + /// + /// Gets a list of sub-properties this data binding applies to + /// + ReadOnlyCollection Properties { get; } - /// - /// Gets a boolean indicating whether the data binding is enabled or not - /// - bool IsEnabled { get; set; } + /// + /// Gets a boolean indicating whether the data binding is enabled or not + /// + bool IsEnabled { get; set; } - /// - /// Applies the pending value of the data binding to the property - /// - void Apply(); + /// + /// Applies the pending value of the data binding to the property + /// + void Apply(); - /// - /// If the data binding is enabled, loads the node script for that data binding - /// - void LoadNodeScript(); + /// + /// If the data binding is enabled, loads the node script for that data binding + /// + void LoadNodeScript(); - /// - /// Occurs when a data binding property has been added - /// - public event EventHandler? DataBindingPropertyRegistered; + /// + /// Occurs when a data binding property has been added + /// + public event EventHandler? DataBindingPropertyRegistered; - /// - /// Occurs when all data binding properties have been removed - /// - public event EventHandler? DataBindingPropertiesCleared; + /// + /// Occurs when all data binding properties have been removed + /// + public event EventHandler? DataBindingPropertiesCleared; - /// - /// Occurs when a data binding has been enabled - /// - public event EventHandler? DataBindingEnabled; + /// + /// Occurs when a data binding has been enabled + /// + public event EventHandler? DataBindingEnabled; - /// - /// Occurs when a data binding has been disabled - /// - public event EventHandler? DataBindingDisabled; - } + /// + /// Occurs when a data binding has been disabled + /// + public event EventHandler? DataBindingDisabled; } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingProperty.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingProperty.cs index c6f299766..7d5dc49bd 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingProperty.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingProperty.cs @@ -1,32 +1,31 @@ using System; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a data binding registration +/// +public interface IDataBindingProperty { /// - /// Represents a data binding registration + /// Gets or sets the display name of the data binding registration /// - public interface IDataBindingProperty - { - /// - /// Gets or sets the display name of the data binding registration - /// - string DisplayName { get; } + string DisplayName { get; } - /// - /// Gets the type of the value this data binding registration points to - /// - Type ValueType { get; } + /// + /// Gets the type of the value this data binding registration points to + /// + Type ValueType { get; } - /// - /// Gets the value of the property this registration points to - /// - /// A value matching the type of - object? GetValue(); + /// + /// Gets the value of the property this registration points to + /// + /// A value matching the type of + object? GetValue(); - /// - /// Sets the value of the property this registration points to - /// - /// A value matching the type of - void SetValue(object? value); - } + /// + /// Sets the value of the property this registration points to + /// + /// A value matching the type of + void SetValue(object? value); } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelEvent.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelEvent.cs index 352c3c09b..a1a98fa83 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/DataModelEvent.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelEvent.cs @@ -3,249 +3,244 @@ using System.Collections.Generic; using System.Linq; using Artemis.Core.Modules; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a data model event with event arguments of type +/// +public class DataModelEvent : IDataModelEvent where T : DataModelEventArgs { + private bool _trackHistory; + /// - /// Represents a data model event with event arguments of type + /// Creates a new instance of the class with history tracking disabled /// - public class DataModelEvent : IDataModelEvent where T : DataModelEventArgs + public DataModelEvent() { - private bool _trackHistory; - - /// - /// Creates a new instance of the class with history tracking disabled - /// - public DataModelEvent() - { - } - - /// - /// Creates a new instance of the - /// - /// A boolean indicating whether the last 20 events should be tracked - public DataModelEvent(bool trackHistory) - { - _trackHistory = trackHistory; - } - - /// - [DataModelProperty(Name = "Last trigger", Description = "The time at which the event last triggered")] - public DateTime LastTrigger { get; private set; } - - /// - [DataModelProperty(Name = "Time since trigger", Description = "The time that has passed since the last trigger")] - public TimeSpan TimeSinceLastTrigger => DateTime.Now - LastTrigger; - - /// - /// Gets the event arguments of the last time the event was triggered - /// - [DataModelProperty(Description = "The arguments of the last time this event triggered")] - public T? LastEventArguments { get; private set; } - - /// - [DataModelProperty(Description = "The total amount of times this event has triggered since the module was activated")] - public int TriggerCount { get; private set; } - - /// - /// Gets a queue of the last 20 event arguments - /// Always empty if is - /// - [DataModelProperty(Description = "The arguments of the last time this event triggered")] - public Queue EventArgumentsHistory { get; } = new(20); - - /// - /// Trigger the event with the given - /// - /// The event argument to pass to the event - 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); - } - - /// - [DataModelIgnore] - public Type ArgumentsType => typeof(T); - - /// - [DataModelIgnore] - public string TriggerPastParticiple => "triggered"; - - /// - [DataModelIgnore] - public bool TrackHistory - { - get => _trackHistory; - set - { - EventArgumentsHistory.Clear(); - _trackHistory = value; - } - } - - /// - [DataModelIgnore] - public DataModelEventArgs? LastEventArgumentsUntyped => LastEventArguments; - - /// - [DataModelIgnore] - public List EventArgumentsHistoryUntyped => EventArgumentsHistory.Cast().ToList(); - - /// - public event EventHandler? EventTriggered; - - /// - public void Reset() - { - TriggerCount = 0; - EventArgumentsHistory.Clear(); - } - - /// - public void Update() - { - } } /// - /// Represents a data model event without event arguments + /// Creates a new instance of the /// - public class DataModelEvent : IDataModelEvent + /// A boolean indicating whether the last 20 events should be tracked + public DataModelEvent(bool trackHistory) { - private bool _trackHistory; + _trackHistory = trackHistory; + } - /// - /// Creates a new instance of the class with history tracking disabled - /// - public DataModelEvent() - { - } + /// + /// Gets the event arguments of the last time the event was triggered + /// + [DataModelProperty(Description = "The arguments of the last time this event triggered")] + public T? LastEventArguments { get; private set; } - /// - /// Creates a new instance of the - /// - /// A boolean indicating whether the last 20 events should be tracked - public DataModelEvent(bool trackHistory) - { - _trackHistory = trackHistory; - } + /// + /// Gets a queue of the last 20 event arguments + /// Always empty if is + /// + [DataModelProperty(Description = "The arguments of the last time this event triggered")] + public Queue EventArgumentsHistory { get; } = new(20); - /// - [DataModelProperty(Name = "Last trigger", Description = "The time at which the event last triggered")] - public DateTime LastTrigger { get; private set; } + /// + /// Trigger the event with the given + /// + /// The event argument to pass to the event + public void Trigger(T eventArgs) + { + if (eventArgs == null) throw new ArgumentNullException(nameof(eventArgs)); + eventArgs.TriggerTime = DateTime.Now; - /// - [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++; - /// - /// Gets the event arguments of the last time the event was triggered - /// - [DataModelProperty(Description = "The arguments of the last time this event triggered")] - public DataModelEventArgs? LastTriggerArguments { get; private set; } - - /// - [DataModelProperty(Description = "The total amount of times this event has triggered since the module was activated")] - public int TriggerCount { get; private set; } - - /// - /// Gets a queue of the last 20 event arguments - /// Always empty if is - /// - [DataModelProperty(Description = "The arguments of the last time this event triggered")] - public Queue EventArgumentsHistory { get; } = new(20); - - /// - /// Trigger the event - /// - 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); + } + + /// + [DataModelProperty(Name = "Last trigger", Description = "The time at which the event last triggered")] + public DateTime LastTrigger { get; private set; } + + /// + [DataModelProperty(Name = "Time since trigger", Description = "The time that has passed since the last trigger")] + public TimeSpan TimeSinceLastTrigger => DateTime.Now - LastTrigger; + + /// + [DataModelProperty(Description = "The total amount of times this event has triggered since the module was activated")] + public int TriggerCount { get; private set; } + + /// + [DataModelIgnore] + public Type ArgumentsType => typeof(T); + + /// + [DataModelIgnore] + public string TriggerPastParticiple => "triggered"; + + /// + [DataModelIgnore] + public bool TrackHistory + { + get => _trackHistory; + set { - EventTriggered?.Invoke(this, EventArgs.Empty); - } - - /// - [DataModelIgnore] - public Type ArgumentsType => typeof(DataModelEventArgs); - - /// - [DataModelIgnore] - public string TriggerPastParticiple => "triggered"; - - /// - [DataModelIgnore] - public bool TrackHistory - { - get => _trackHistory; - set - { - EventArgumentsHistory.Clear(); - _trackHistory = value; - } - } - - /// - [DataModelIgnore] - public DataModelEventArgs? LastEventArgumentsUntyped => LastTriggerArguments; - - /// - [DataModelIgnore] - public List EventArgumentsHistoryUntyped => EventArgumentsHistory.ToList(); - - /// - public event EventHandler? EventTriggered; - - /// - public void Reset() - { - TriggerCount = 0; EventArgumentsHistory.Clear(); + _trackHistory = value; } + } - /// - public void Update() + /// + [DataModelIgnore] + public DataModelEventArgs? LastEventArgumentsUntyped => LastEventArguments; + + /// + [DataModelIgnore] + public List EventArgumentsHistoryUntyped => EventArgumentsHistory.Cast().ToList(); + + /// + public event EventHandler? EventTriggered; + + /// + public void Reset() + { + TriggerCount = 0; + EventArgumentsHistory.Clear(); + } + + /// + public void Update() + { + } +} + +/// +/// Represents a data model event without event arguments +/// +public class DataModelEvent : IDataModelEvent +{ + private bool _trackHistory; + + /// + /// Creates a new instance of the class with history tracking disabled + /// + public DataModelEvent() + { + } + + /// + /// Creates a new instance of the + /// + /// A boolean indicating whether the last 20 events should be tracked + public DataModelEvent(bool trackHistory) + { + _trackHistory = trackHistory; + } + + /// + /// Gets the event arguments of the last time the event was triggered + /// + [DataModelProperty(Description = "The arguments of the last time this event triggered")] + public DataModelEventArgs? LastTriggerArguments { get; private set; } + + /// + /// Gets a queue of the last 20 event arguments + /// Always empty if is + /// + [DataModelProperty(Description = "The arguments of the last time this event triggered")] + public Queue EventArgumentsHistory { get; } = new(20); + + /// + /// Trigger the event + /// + 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); + } + + /// + [DataModelProperty(Name = "Last trigger", Description = "The time at which the event last triggered")] + public DateTime LastTrigger { get; private set; } + + /// + [DataModelProperty(Name = "Time since trigger", Description = "The time that has passed since the last trigger")] + public TimeSpan TimeSinceLastTrigger => DateTime.Now - LastTrigger; + + /// + [DataModelProperty(Description = "The total amount of times this event has triggered since the module was activated")] + public int TriggerCount { get; private set; } + + /// + [DataModelIgnore] + public Type ArgumentsType => typeof(DataModelEventArgs); + + /// + [DataModelIgnore] + public string TriggerPastParticiple => "triggered"; + + /// + [DataModelIgnore] + public bool TrackHistory + { + get => _trackHistory; + set { + EventArgumentsHistory.Clear(); + _trackHistory = value; } } + + /// + [DataModelIgnore] + public DataModelEventArgs? LastEventArgumentsUntyped => LastTriggerArguments; + + /// + [DataModelIgnore] + public List EventArgumentsHistoryUntyped => EventArgumentsHistory.ToList(); + + /// + public event EventHandler? EventTriggered; + + /// + public void Reset() + { + TriggerCount = 0; + EventArgumentsHistory.Clear(); + } + + /// + public void Update() + { + } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelEventArgs.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelEventArgs.cs index edd41bb6a..c6ae1db7c 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/DataModelEventArgs.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelEventArgs.cs @@ -1,17 +1,16 @@ using System; using Artemis.Core.Modules; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents the base class for data model events that contain event data +/// +public class DataModelEventArgs { /// - /// Represents the base class for data model events that contain event data + /// Gets the time at which the event with these arguments was triggered /// - public class DataModelEventArgs - { - /// - /// Gets the time at which the event with these arguments was triggered - /// - [DataModelIgnore] - public DateTime TriggerTime { get; internal set; } - } + [DataModelIgnore] + public DateTime TriggerTime { get; internal set; } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs index 6aaaf0ee9..ea1a85825 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs @@ -6,388 +6,388 @@ using System.Reflection; using Artemis.Core.Modules; using Artemis.Storage.Entities.Profile; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a path that points to a property in data model +/// +public class DataModelPath : IStorageModel, IDisposable { + private readonly LinkedList _segments; + private Expression>? _accessorLambda; + private bool _disposed; + /// - /// Represents a path that points to a property in data model + /// Creates a new instance of the class pointing directly to the target /// - public class DataModelPath : IStorageModel, IDisposable + /// The target at which this path starts + public DataModelPath(DataModel target) { - private readonly LinkedList _segments; - private Expression>? _accessorLambda; - private bool _disposed; + Target = target ?? throw new ArgumentNullException(nameof(target)); + Path = ""; + Entity = new DataModelPathEntity(); - /// - /// Creates a new instance of the class pointing directly to the target - /// - /// The target at which this path starts - public DataModelPath(DataModel target) - { - Target = target ?? throw new ArgumentNullException(nameof(target)); - Path = ""; - Entity = new DataModelPathEntity(); + _segments = new LinkedList(); - _segments = new LinkedList(); - - Save(); - Initialize(); - SubscribeToDataModelStore(); - } - - /// - /// Creates a new instance of the class pointing to the provided path - /// - /// The target at which this path starts - /// A point-separated path - 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(); - - Save(); - Initialize(); - SubscribeToDataModelStore(); - } - - /// - /// Creates a new instance of the class based on an existing path - /// - /// The path to base the new instance on - public DataModelPath(DataModelPath dataModelPath) - { - if (dataModelPath == null) - throw new ArgumentNullException(nameof(dataModelPath)); - - Target = dataModelPath.Target; - Path = dataModelPath.Path; - Entity = new DataModelPathEntity(); - - _segments = new LinkedList(); - - Save(); - Initialize(); - SubscribeToDataModelStore(); - } - - /// - /// Creates a new instance of the class based on a - /// - /// - public DataModelPath(DataModelPathEntity entity) - { - Path = entity.Path; - Entity = entity; - - _segments = new LinkedList(); - - Load(); - Initialize(); - SubscribeToDataModelStore(); - } - - /// - /// Gets the data model at which this path starts - /// - public DataModel? Target { get; private set; } - - /// - /// Gets the data model ID of the if it is a - /// - public string? DataModelId => Target?.Module.Id; - - /// - /// Gets the point-separated path associated with this - /// - public string Path { get; private set; } - - /// - /// Gets a boolean indicating whether all are valid - /// - public bool IsValid => Segments.Any() && Segments.All(p => p.Type != DataModelPathSegmentType.Invalid); - - /// - /// Gets a read-only list of all segments of this path - /// - public IReadOnlyCollection Segments => _segments.ToList().AsReadOnly(); - - /// - /// Gets the entity used for persistent storage - /// - public DataModelPathEntity Entity { get; } - - internal Func? Accessor { get; private set; } - - /// - /// Gets the current value of the path - /// - 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); - } - - /// - /// Gets the property info of the property this path points to - /// - /// If static, the property info. If dynamic, null - public PropertyInfo? GetPropertyInfo() - { - if (_disposed) - throw new ObjectDisposedException("DataModelPath"); - - return Segments.LastOrDefault()?.GetPropertyInfo(); - } - - /// - /// Gets the type of the property this path points to - /// - /// If possible, the property type - public Type? GetPropertyType() - { - if (_disposed) - throw new ObjectDisposedException("DataModelPath"); - - return Segments.LastOrDefault()?.GetPropertyType(); - } - - /// - /// Gets the property description of the property this path points to - /// - /// If found, the data model property description - public DataModelPropertyAttribute? GetPropertyDescription() - { - if (_disposed) - throw new ObjectDisposedException("DataModelPath"); - - return Segments.LastOrDefault()?.GetPropertyDescription(); - } - - /// - public override string ToString() - { - return string.IsNullOrWhiteSpace(Path) ? "this" : Path; - } - - /// - /// Occurs whenever the path becomes invalid - /// - public event EventHandler? PathInvalidated; - - /// - /// Occurs whenever the path becomes valid - /// - public event EventHandler? PathValidated; - - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _disposed = true; - - DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded; - DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved; - - Invalidate(); - } - } - - /// - /// Invokes the event - /// - protected virtual void OnPathValidated() - { - PathValidated?.Invoke(this, EventArgs.Empty); - } - - /// - /// Invokes the event - /// - 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 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>( - // 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; - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #region Storage - - /// - public void Load() - { - Path = Entity.Path; - - if (Target == null && Entity.DataModelId != null) - Target = DataModelStore.Get(Entity.DataModelId)?.DataModel; - } - - /// - public void Save() - { - // Do not save an invalid state - if (!IsValid) - return; - - Entity.Path = Path; - Entity.DataModelId = DataModelId; - } - - #region Equality members - - /// > - protected bool Equals(DataModelPath other) - { - return ReferenceEquals(Target, other.Target) && Path == other.Path; - } - - /// - 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); - } - - /// - public override int GetHashCode() - { - return HashCode.Combine(Target, Path); - } - - #endregion - - #endregion + Save(); + Initialize(); + SubscribeToDataModelStore(); } + + /// + /// Creates a new instance of the class pointing to the provided path + /// + /// The target at which this path starts + /// A point-separated path + 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(); + + Save(); + Initialize(); + SubscribeToDataModelStore(); + } + + /// + /// Creates a new instance of the class based on an existing path + /// + /// The path to base the new instance on + public DataModelPath(DataModelPath dataModelPath) + { + if (dataModelPath == null) + throw new ArgumentNullException(nameof(dataModelPath)); + + Target = dataModelPath.Target; + Path = dataModelPath.Path; + Entity = new DataModelPathEntity(); + + _segments = new LinkedList(); + + Save(); + Initialize(); + SubscribeToDataModelStore(); + } + + /// + /// Creates a new instance of the class based on a + /// + /// + public DataModelPath(DataModelPathEntity entity) + { + Path = entity.Path; + Entity = entity; + + _segments = new LinkedList(); + + Load(); + Initialize(); + SubscribeToDataModelStore(); + } + + /// + /// Gets the data model at which this path starts + /// + public DataModel? Target { get; private set; } + + /// + /// Gets the data model ID of the if it is a + /// + public string? DataModelId => Target?.Module.Id; + + /// + /// Gets the point-separated path associated with this + /// + public string Path { get; private set; } + + /// + /// Gets a boolean indicating whether all are valid + /// + public bool IsValid => Segments.Any() && Segments.All(p => p.Type != DataModelPathSegmentType.Invalid); + + /// + /// Gets a read-only list of all segments of this path + /// + public IReadOnlyCollection Segments => _segments.ToList().AsReadOnly(); + + /// + /// Gets the entity used for persistent storage + /// + public DataModelPathEntity Entity { get; } + + internal Func? Accessor { get; private set; } + + /// + /// Gets the current value of the path + /// + 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); + } + + /// + /// Gets the property info of the property this path points to + /// + /// If static, the property info. If dynamic, null + public PropertyInfo? GetPropertyInfo() + { + if (_disposed) + throw new ObjectDisposedException("DataModelPath"); + + return Segments.LastOrDefault()?.GetPropertyInfo(); + } + + /// + /// Gets the type of the property this path points to + /// + /// If possible, the property type + public Type? GetPropertyType() + { + if (_disposed) + throw new ObjectDisposedException("DataModelPath"); + + return Segments.LastOrDefault()?.GetPropertyType(); + } + + /// + /// Gets the property description of the property this path points to + /// + /// If found, the data model property description + public DataModelPropertyAttribute? GetPropertyDescription() + { + if (_disposed) + throw new ObjectDisposedException("DataModelPath"); + + return Segments.LastOrDefault()?.GetPropertyDescription(); + } + + /// + public override string ToString() + { + return string.IsNullOrWhiteSpace(Path) ? "this" : Path; + } + + /// + /// Occurs whenever the path becomes invalid + /// + public event EventHandler? PathInvalidated; + + /// + /// Occurs whenever the path becomes valid + /// + public event EventHandler? PathValidated; + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _disposed = true; + + DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded; + DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved; + + Invalidate(); + } + } + + /// + /// Invokes the event + /// + protected virtual void OnPathValidated() + { + PathValidated?.Invoke(this, EventArgs.Empty); + } + + /// + /// Invokes the event + /// + 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 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>( + // 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; + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #region Storage + + /// + public void Load() + { + Path = Entity.Path; + + if (Target == null && Entity.DataModelId != null) + Target = DataModelStore.Get(Entity.DataModelId)?.DataModel; + } + + /// + public void Save() + { + // Do not save an invalid state + if (!IsValid) + return; + + Entity.Path = Path; + Entity.DataModelId = DataModelId; + } + + #region Equality members + + /// + /// > + protected bool Equals(DataModelPath other) + { + return ReferenceEquals(Target, other.Target) && Path == other.Path; + } + + /// + 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); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(Target, Path); + } + + #endregion + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegment.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegment.cs index 19c1d87ab..464bbb58c 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegment.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegment.cs @@ -6,307 +6,294 @@ using System.Reflection; using Artemis.Core.Modules; using Humanizer; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a segment of a data model path +/// +public class DataModelPathSegment : IDisposable { - /// - /// Represents a segment of a data model path - /// - public class DataModelPathSegment : IDisposable + private Expression>? _accessorLambda; + private DataModel? _dynamicDataModel; + private DataModelPropertyAttribute? _dynamicDataModelAttribute; + private Type? _dynamicDataModelType; + private PropertyInfo? _property; + + internal DataModelPathSegment(DataModelPath dataModelPath, string identifier, string path) { - private Expression>? _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) + /// + /// Gets the data model path this is a segment of + /// + public DataModelPath DataModelPath { get; } + + /// + /// Gets the identifier that is associated with this segment + /// + public string Identifier { get; } + + /// + /// Gets the path that leads to this segment + /// + public string Path { get; } + + /// + /// Gets a boolean indicating whether this is the first segment in the path + /// + public bool IsStartSegment { get; } + + /// + /// Gets the type of data model this segment of the path points to + /// + public DataModelPathSegmentType Type { get; private set; } + + /// + /// Gets the previous segment in the path + /// + public DataModelPathSegment? Previous => Node?.Previous?.Value; + + /// + /// Gets the next segment in the path + /// + public DataModelPathSegment? Next => Node?.Next?.Value; + + internal Func? Accessor { get; set; } + internal LinkedListNode? Node { get; set; } + + /// + /// Returns the current value of the path up to this segment + /// + /// + 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); + } + + /// + public override string ToString() + { + return $"[{Type}] {Path}"; + } + + /// + /// Gets the property info of the property this segment points to + /// + /// If static, the property info. If dynamic, null + 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); + } + + /// + /// Gets the property description of the property this segment points to + /// + /// If found, the data model property description + 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; } - /// - /// Gets the data model path this is a segment of - /// - public DataModelPath DataModelPath { get; } + return new DataModelPropertyAttribute {Name = propertyInfo.Name.Humanize(), ResetsDepth = false}; + } - /// - /// Gets the identifier that is associated with this segment - /// - public string Identifier { get; } + /// + /// Gets the type of the property this path points to + /// + /// If possible, the property type + public Type? GetPropertyType() + { + // The start segment type is always the target type + if (IsStartSegment) + return DataModelPath.Target?.GetType(); - /// - /// Gets the path that leads to this segment - /// - public string Path { get; } - - /// - /// Gets a boolean indicating whether this is the first segment in the path - /// - public bool IsStartSegment { get; } - - /// - /// Gets the type of data model this segment of the path points to - /// - public DataModelPathSegmentType Type { get; private set; } - - /// - /// Gets the previous segment in the path - /// - public DataModelPathSegment? Previous => Node?.Previous?.Value; - - /// - /// Gets the next segment in the path - /// - public DataModelPathSegment? Next => Node?.Next?.Value; - - internal Func? Accessor { get; set; } - internal LinkedListNode? Node { get; set; } - - /// - /// Returns the current value of the path up to this segment - /// - /// - 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(); } - /// - public override string ToString() + return type; + } + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) { - return $"[{Type}] {Path}"; - } - - /// - /// Gets the property info of the property this segment points to - /// - /// If static, the property info. If dynamic, null - 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); - } - - /// - /// Gets the property description of the property this segment points to - /// - /// If found, the data model property description - 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; } + } - /// - /// Gets the type of the property this path points to - /// - /// If possible, the property type - 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 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>( - // 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 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>( + // 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 - - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - if (_dynamicDataModel != null) - { - _dynamicDataModel.DynamicChildAdded -= DynamicChildOnDynamicChildAdded; - _dynamicDataModel.DynamicChildRemoved -= DynamicChildOnDynamicChildRemoved; - } - - Type = DataModelPathSegmentType.Invalid; - - _accessorLambda = null; - Accessor = null; - } - } - - /// - 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 + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegmentType.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegmentType.cs index 565f0c037..73345e88e 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegmentType.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegmentType.cs @@ -1,23 +1,22 @@ -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a type of data model path +/// +public enum DataModelPathSegmentType { /// - /// Represents a type of data model path + /// Represents an invalid data model type that points to a missing data model /// - public enum DataModelPathSegmentType - { - /// - /// Represents an invalid data model type that points to a missing data model - /// - Invalid, + Invalid, - /// - /// Represents a static data model type that points to a data model defined in code - /// - Static, + /// + /// Represents a static data model type that points to a data model defined in code + /// + Static, - /// - /// Represents a static data model type that points to a data model defined at runtime - /// - Dynamic - } + /// + /// Represents a static data model type that points to a data model defined at runtime + /// + Dynamic } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataModel/IDataModelEvent.cs b/src/Artemis.Core/Models/Profile/DataModel/IDataModelEvent.cs index 4ab9d271c..de3ed81c5 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/IDataModelEvent.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/IDataModelEvent.cs @@ -1,69 +1,68 @@ using System; using System.Collections.Generic; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents an event that is part of a data model +/// +public interface IDataModelEvent { /// - /// Represents an event that is part of a data model + /// Gets the last time the event was triggered /// - public interface IDataModelEvent - { - /// - /// Gets the last time the event was triggered - /// - DateTime LastTrigger { get; } + DateTime LastTrigger { get; } - /// - /// Gets the time that has passed since the last trigger - /// - TimeSpan TimeSinceLastTrigger { get; } - - /// - /// Gets the amount of times the event was triggered - /// - int TriggerCount { get; } + /// + /// Gets the time that has passed since the last trigger + /// + TimeSpan TimeSinceLastTrigger { get; } - /// - /// Gets the type of arguments this event contains - /// - Type ArgumentsType { get; } + /// + /// Gets the amount of times the event was triggered + /// + int TriggerCount { get; } - /// - /// Gets the past participle for this event shown in the UI - /// - string TriggerPastParticiple { get; } + /// + /// Gets the type of arguments this event contains + /// + Type ArgumentsType { get; } - /// - /// Gets or sets a boolean indicating whether the last 20 events should be tracked - /// Note: setting this to will clear the current history - /// - bool TrackHistory { get; set; } + /// + /// Gets the past participle for this event shown in the UI + /// + string TriggerPastParticiple { get; } - /// - /// Gets the event arguments of the last time the event was triggered by its base type - /// - public DataModelEventArgs? LastEventArgumentsUntyped { get; } + /// + /// Gets or sets a boolean indicating whether the last 20 events should be tracked + /// Note: setting this to will clear the current history + /// + bool TrackHistory { get; set; } - /// - /// Gets a list of the last 20 event arguments by their base type. - /// Always empty if is - /// - public List EventArgumentsHistoryUntyped { get; } + /// + /// Gets the event arguments of the last time the event was triggered by its base type + /// + public DataModelEventArgs? LastEventArgumentsUntyped { get; } - /// - /// Fires when the event is triggered - /// - event EventHandler EventTriggered; + /// + /// Gets a list of the last 20 event arguments by their base type. + /// Always empty if is + /// + public List EventArgumentsHistoryUntyped { get; } - /// - /// Resets the trigger count and history of this data model event - /// - void Reset(); + /// + /// Fires when the event is triggered + /// + event EventHandler EventTriggered; - /// - /// Updates the event, not required for standard events but included in case your custom event needs to update every - /// tick - /// - void Update(); - } + /// + /// Resets the trigger count and history of this data model event + /// + void Reset(); + + /// + /// Updates the event, not required for standard events but included in case your custom event needs to update every + /// tick + /// + void Update(); } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs index 36f107598..363a5759b 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -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; + +/// +/// Represents a folder in a +/// +public sealed class Folder : RenderProfileElement { + private bool _isExpanded; + /// - /// Represents a folder in a + /// Creates a new instance of the class and adds itself to the child collection of the provided + /// /// - public sealed class Folder : RenderProfileElement + /// The parent of the folder + /// The name of the folder + public Folder(ProfileElement parent, string name) : base(parent, parent.Profile) { - private bool _isExpanded; + FolderEntity = new FolderEntity(); + EntityId = Guid.NewGuid(); - /// - /// Creates a new instance of the class and adds itself to the child collection of the provided - /// - /// - /// The parent of the folder - /// The name of the folder - public Folder(ProfileElement parent, string name) : base(parent, parent.Profile) + Profile = Parent.Profile; + Name = name; + } + + /// + /// Creates a new instance of the class based on the provided folder entity + /// + /// The profile the folder belongs to + /// The parent of the folder + /// The entity of the folder + 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(); + } + + /// + /// Gets a boolean indicating whether this folder is at the root of the profile tree + /// + public bool IsRootFolder => Parent == Profile; + + /// + /// Gets or sets a boolean indicating whether this folder is expanded + /// + public bool IsExpanded + { + get => _isExpanded; + set => SetAndNotify(ref _isExpanded, value); + } + + /// + /// Gets the folder entity this folder uses for persistent storage + /// + public FolderEntity FolderEntity { get; internal set; } + + /// + public override bool ShouldBeEnabled => !Suspended && DisplayConditionMet && !Timeline.IsFinished; + + internal override RenderElementEntity RenderElementEntity => FolderEntity; + + /// + public override List GetAllLayerProperties() + { + List 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()); } - /// - /// Creates a new instance of the class based on the provided folder entity - /// - /// The profile the folder belongs to - /// The parent of the folder - /// The entity of the folder - public Folder(Profile profile, ProfileElement parent, FolderEntity folderEntity) : base(parent, parent.Profile) + return result; + } + + /// + 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; } - /// - /// Gets a boolean indicating whether this folder is at the root of the profile tree - /// - public bool IsRootFolder => Parent == Profile; - - /// - /// Gets or sets a boolean indicating whether this folder is expanded - /// - public bool IsExpanded - { - get => _isExpanded; - set => SetAndNotify(ref _isExpanded, value); - } - - /// - /// Gets the folder entity this folder uses for persistent storage - /// - public FolderEntity FolderEntity { get; internal set; } - - /// - public override bool ShouldBeEnabled => !Suspended && DisplayConditionMet && !Timeline.IsFinished; - - internal override RenderElementEntity RenderElementEntity => FolderEntity; - - /// - public override List GetAllLayerProperties() - { - List result = new(); - foreach (BaseLayerEffect layerEffect in LayerEffects) - { - if (layerEffect.BaseProperties != null) - result.AddRange(layerEffect.BaseProperties.GetAllLayerProperties()); - } - - return result; - } - - /// - 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); - } - - /// - public override void Reset() - { - UpdateDisplayCondition(); - - if (DisplayConditionMet) - Timeline.JumpToStart(); - else - Timeline.JumpToEnd(); - - foreach (ProfileElement child in Children) - child.Reset(); - } - - /// - public override void AddChild(ProfileElement child, int? order = null) - { - if (Disposed) - throw new ObjectDisposedException("Folder"); - - base.AddChild(child, order); - CalculateRenderProperties(); - } - - /// - public override void RemoveChild(ProfileElement child) - { - if (Disposed) - throw new ObjectDisposedException("Folder"); - - base.RemoveChild(child); - CalculateRenderProperties(); - } - - /// - /// Creates a deep copy of the folder - /// - /// The newly created copy - public Folder CreateCopy() - { - if (Parent == null) - throw new ArtemisCoreException("Cannot create a copy of a folder without a parent"); - - FolderEntity entityCopy = CoreJson.DeserializeObject(CoreJson.SerializeObject(FolderEntity, true), true)!; - entityCopy.Id = Guid.NewGuid(); - entityCopy.Name += " - Copy"; - - // TODO Children - - return new Folder(Profile, Parent, entityCopy); - } - - /// - public override string ToString() - { - return $"[Folder] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}"; - } - - #region Rendering - - /// - 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 - - /// - 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; - } - - /// - 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; - } - - /// - public override void OverrideTimelineAndApply(TimeSpan position) - { - DisplayCondition.OverrideTimeline(position); - foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) - baseLayerEffect.InternalUpdate(Timeline); ; - } - - /// - /// Occurs when a property affecting the rendering properties of this folder has been updated - /// - public event EventHandler? RenderPropertiesUpdated; - - /// - 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); + } + + /// + public override void Reset() + { + UpdateDisplayCondition(); + + if (DisplayConditionMet) + Timeline.JumpToStart(); + else + Timeline.JumpToEnd(); + + foreach (ProfileElement child in Children) + child.Reset(); + } + + /// + public override void AddChild(ProfileElement child, int? order = null) + { + if (Disposed) + throw new ObjectDisposedException("Folder"); + + base.AddChild(child, order); + CalculateRenderProperties(); + } + + /// + public override void RemoveChild(ProfileElement child) + { + if (Disposed) + throw new ObjectDisposedException("Folder"); + + base.RemoveChild(child); + CalculateRenderProperties(); + } + + /// + /// Creates a deep copy of the folder + /// + /// The newly created copy + public Folder CreateCopy() + { + if (Parent == null) + throw new ArtemisCoreException("Cannot create a copy of a folder without a parent"); + + FolderEntity entityCopy = CoreJson.DeserializeObject(CoreJson.SerializeObject(FolderEntity, true), true)!; + entityCopy.Id = Guid.NewGuid(); + entityCopy.Name += " - Copy"; + + // TODO Children + + return new Folder(Profile, Parent, entityCopy); + } + + /// + public override string ToString() + { + return $"[Folder] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}"; + } + + #region Rendering + + /// + 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 + + /// + 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; + } + + /// + 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; + } + + /// + public override void OverrideTimelineAndApply(TimeSpan position) + { + DisplayCondition.OverrideTimeline(position); + foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) + baseLayerEffect.InternalUpdate(Timeline); + ; + } + + /// + /// Occurs when a property affecting the rendering properties of this folder has been updated + /// + public event EventHandler? RenderPropertiesUpdated; + + #region Overrides of BreakableModel + + /// + public override IEnumerable GetBrokenHierarchy() + { + return LayerEffects.Where(e => e.BrokenState != null); + } + + #endregion + + /// + 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; - /// - public override IEnumerable 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); } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 7f2be3fb5..8b0902837 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -9,887 +9,888 @@ using Artemis.Storage.Entities.Profile.Abstract; using RGB.NET.Core; using SkiaSharp; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a layer in a +/// +public sealed class Layer : RenderProfileElement { + private readonly List _renderCopies; + private LayerGeneralProperties _general; + private BaseLayerBrush? _layerBrush; + private LayerShape? _layerShape; + private List _leds; + private LayerTransformProperties _transform; + /// - /// Represents a layer in a + /// Creates a new instance of the class and adds itself to the child collection of the provided + /// /// - public sealed class Layer : RenderProfileElement + /// The parent of the layer + /// The name of the layer + public Layer(ProfileElement parent, string name) : base(parent, parent.Profile) { - private readonly List _renderCopies; - private LayerGeneralProperties _general; - private BaseLayerBrush? _layerBrush; - private LayerShape? _layerShape; - private List _leds; - private LayerTransformProperties _transform; + LayerEntity = new LayerEntity(); + EntityId = Guid.NewGuid(); - /// - /// Creates a new instance of the class and adds itself to the child collection of the provided - /// - /// - /// The parent of the layer - /// The name of the layer - public Layer(ProfileElement parent, string name) : base(parent, parent.Profile) + Profile = Parent.Profile; + Name = name; + Suspended = false; + + // TODO: move to top + _renderCopies = new List(); + _general = new LayerGeneralProperties(); + _transform = new LayerTransformProperties(); + + _leds = new List(); + Leds = new ReadOnlyCollection(_leds); + + Adapter = new LayerAdapter(this); + Initialize(); + } + + /// + /// Creates a new instance of the class based on the provided layer entity + /// + /// The profile the layer belongs to + /// The parent of the layer + /// The entity of the layer + public Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity) : base(parent, parent.Profile) + { + LayerEntity = layerEntity; + EntityId = layerEntity.Id; + + Profile = profile; + Parent = parent; + + // TODO: move to top + _renderCopies = new List(); + _general = new LayerGeneralProperties(); + _transform = new LayerTransformProperties(); + + _leds = new List(); + Leds = new ReadOnlyCollection(_leds); + + Adapter = new LayerAdapter(this); + Load(); + Initialize(); + } + + /// + /// Creates a new instance of the class by copying the provided . + /// + /// The layer to copy + private Layer(Layer source) : base(source, source.Profile) + { + LayerEntity = source.LayerEntity; + + Profile = source.Profile; + Parent = source; + + // TODO: move to top + _renderCopies = new List(); + _general = new LayerGeneralProperties(); + _transform = new LayerTransformProperties(); + + _leds = new List(); + Leds = new ReadOnlyCollection(_leds); + + Adapter = new LayerAdapter(this); + Load(); + Initialize(); + + Timeline.JumpToStart(); + AddLeds(source.Leds); + Enable(); + + // After loading using the source entity create a new entity so the next call to Save won't mess with the source, just in case. + LayerEntity = new LayerEntity(); + } + + /// + /// A collection of all the LEDs this layer is assigned to. + /// + public ReadOnlyCollection Leds { get; private set; } + + /// + /// Defines the shape that is rendered by the . + /// + public LayerShape? LayerShape + { + get => _layerShape; + set { - LayerEntity = new LayerEntity(); - EntityId = Guid.NewGuid(); + SetAndNotify(ref _layerShape, value); + if (Path != null) + CalculateRenderProperties(); + } + } - Profile = Parent.Profile; - Name = name; - Suspended = false; + /// + /// Gets the general properties of the layer + /// + [PropertyGroupDescription(Identifier = "General", Name = "General", Description = "A collection of general properties")] + public LayerGeneralProperties General + { + get => _general; + private set => SetAndNotify(ref _general, value); + } - // TODO: move to top - _renderCopies = new List(); - _general = new LayerGeneralProperties(); - _transform = new LayerTransformProperties(); + /// + /// Gets the transform properties of the layer + /// + [PropertyGroupDescription(Identifier = "Transform", Name = "Transform", Description = "A collection of transformation properties")] + public LayerTransformProperties Transform + { + get => _transform; + private set => SetAndNotify(ref _transform, value); + } - _leds = new List(); - Leds = new ReadOnlyCollection(_leds); + /// + /// The brush that will fill the . + /// + public BaseLayerBrush? LayerBrush + { + get => _layerBrush; + internal set => SetAndNotify(ref _layerBrush, value); + } - Adapter = new LayerAdapter(this); - Initialize(); + /// + /// Gets the layer entity this layer uses for persistent storage + /// + public LayerEntity LayerEntity { get; internal set; } + + /// + /// Gets the layer adapter that can be used to adapt this layer to a different set of devices + /// + public LayerAdapter Adapter { get; } + + /// + public override bool ShouldBeEnabled => !Suspended && DisplayConditionMet; + + internal override RenderElementEntity RenderElementEntity => LayerEntity; + + /// + public override List GetAllLayerProperties() + { + List result = new(); + result.AddRange(General.GetAllLayerProperties()); + result.AddRange(Transform.GetAllLayerProperties()); + if (LayerBrush?.BaseProperties != null) + result.AddRange(LayerBrush.BaseProperties.GetAllLayerProperties()); + foreach (BaseLayerEffect layerEffect in LayerEffects) + { + if (layerEffect.BaseProperties != null) + result.AddRange(layerEffect.BaseProperties.GetAllLayerProperties()); } - /// - /// Creates a new instance of the class based on the provided layer entity - /// - /// The profile the layer belongs to - /// The parent of the layer - /// The entity of the layer - public Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity) : base(parent, parent.Profile) + return result; + } + + /// + public override string ToString() + { + return $"[Layer] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}"; + } + + /// + /// Occurs when a property affecting the rendering properties of this layer has been updated + /// + public event EventHandler? RenderPropertiesUpdated; + + /// + /// Occurs when the layer brush of this layer has been updated + /// + public event EventHandler? LayerBrushUpdated; + + #region Overrides of BreakableModel + + /// + public override IEnumerable GetBrokenHierarchy() + { + if (LayerBrush?.BrokenState != null) + yield return LayerBrush; + foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.BrokenState != null)) + yield return baseLayerEffect; + } + + #endregion + + /// + protected override void Dispose(bool disposing) + { + Disable(); + + Disposed = true; + + LayerBrushStore.LayerBrushAdded -= LayerBrushStoreOnLayerBrushAdded; + LayerBrushStore.LayerBrushRemoved -= LayerBrushStoreOnLayerBrushRemoved; + + // Brush first in case it depends on any of the other disposables during it's own disposal + _layerBrush?.Dispose(); + _general.Dispose(); + _transform.Dispose(); + + base.Dispose(disposing); + } + + internal void OnLayerBrushUpdated() + { + LayerBrushUpdated?.Invoke(this, EventArgs.Empty); + } + + private void Initialize() + { + LayerBrushStore.LayerBrushAdded += LayerBrushStoreOnLayerBrushAdded; + LayerBrushStore.LayerBrushRemoved += LayerBrushStoreOnLayerBrushRemoved; + + // Layers have two hardcoded property groups, instantiate them + PropertyGroupDescriptionAttribute generalAttribute = (PropertyGroupDescriptionAttribute) Attribute.GetCustomAttribute( + GetType().GetProperty(nameof(General))!, + typeof(PropertyGroupDescriptionAttribute) + )!; + PropertyGroupDescriptionAttribute transformAttribute = (PropertyGroupDescriptionAttribute) Attribute.GetCustomAttribute( + GetType().GetProperty(nameof(Transform))!, + typeof(PropertyGroupDescriptionAttribute) + )!; + + LayerEntity.GeneralPropertyGroup ??= new PropertyGroupEntity {Identifier = generalAttribute.Identifier}; + LayerEntity.TransformPropertyGroup ??= new PropertyGroupEntity {Identifier = transformAttribute.Identifier}; + + General.Initialize(this, null, generalAttribute, LayerEntity.GeneralPropertyGroup); + Transform.Initialize(this, null, transformAttribute, LayerEntity.TransformPropertyGroup); + + General.ShapeType.CurrentValueSet += ShapeTypeOnCurrentValueSet; + ApplyShapeType(); + ActivateLayerBrush(); + + Reset(); + } + + private void LayerBrushStoreOnLayerBrushRemoved(object? sender, LayerBrushStoreEvent e) + { + if (LayerBrush?.Descriptor == e.Registration.LayerBrushDescriptor) + DeactivateLayerBrush(); + } + + private void LayerBrushStoreOnLayerBrushAdded(object? sender, LayerBrushStoreEvent e) + { + if (LayerBrush != null || !General.PropertiesInitialized) + return; + + LayerBrushReference? current = General.BrushReference.CurrentValue; + if (e.Registration.PluginFeature.Id == current?.LayerBrushProviderId && e.Registration.LayerBrushDescriptor.LayerBrushType.Name == current.BrushType) + ActivateLayerBrush(); + } + + private void OnRenderPropertiesUpdated() + { + RenderPropertiesUpdated?.Invoke(this, EventArgs.Empty); + } + + #region Storage + + internal override void Load() + { + EntityId = LayerEntity.Id; + Name = LayerEntity.Name; + Suspended = LayerEntity.Suspended; + Order = LayerEntity.Order; + + LoadRenderElement(); + Adapter.Load(); + } + + internal override void Save() + { + if (Disposed) + throw new ObjectDisposedException("Layer"); + + // Properties + LayerEntity.Id = EntityId; + LayerEntity.ParentId = Parent?.EntityId ?? new Guid(); + LayerEntity.Order = Order; + LayerEntity.Suspended = Suspended; + LayerEntity.Name = Name; + LayerEntity.ProfileId = Profile.EntityId; + + General.ApplyToEntity(); + Transform.ApplyToEntity(); + + // Don't override the old value of LayerBrush if the current value is null, this avoid losing settings of an unavailable brush + if (LayerBrush != null) { - LayerEntity = layerEntity; - EntityId = layerEntity.Id; - - Profile = profile; - Parent = parent; - - // TODO: move to top - _renderCopies = new List(); - _general = new LayerGeneralProperties(); - _transform = new LayerTransformProperties(); - - _leds = new List(); - Leds = new ReadOnlyCollection(_leds); - - Adapter = new LayerAdapter(this); - Load(); - Initialize(); + LayerBrush.Save(); + LayerEntity.LayerBrush = LayerBrush.LayerBrushEntity; } - /// - /// Creates a new instance of the class by copying the provided . - /// - /// The layer to copy - private Layer(Layer source) : base(source, source.Profile) + // LEDs + LayerEntity.Leds.Clear(); + foreach (ArtemisLed artemisLed in Leds) { - LayerEntity = source.LayerEntity; - - Profile = source.Profile; - Parent = source; - - // TODO: move to top - _renderCopies = new List(); - _general = new LayerGeneralProperties(); - _transform = new LayerTransformProperties(); - - _leds = new List(); - Leds = new ReadOnlyCollection(_leds); - - Adapter = new LayerAdapter(this); - Load(); - Initialize(); - - Timeline.JumpToStart(); - AddLeds(source.Leds); - Enable(); - - // After loading using the source entity create a new entity so the next call to Save won't mess with the source, just in case. - LayerEntity = new LayerEntity(); - } - - /// - /// A collection of all the LEDs this layer is assigned to. - /// - public ReadOnlyCollection Leds { get; private set; } - - /// - /// Defines the shape that is rendered by the . - /// - public LayerShape? LayerShape - { - get => _layerShape; - set + LedEntity ledEntity = new() { - SetAndNotify(ref _layerShape, value); - if (Path != null) - CalculateRenderProperties(); - } + DeviceIdentifier = artemisLed.Device.Identifier, + LedName = artemisLed.RgbLed.Id.ToString(), + PhysicalLayout = artemisLed.Device.DeviceType == RGBDeviceType.Keyboard ? (int) artemisLed.Device.PhysicalLayout : null + }; + LayerEntity.Leds.Add(ledEntity); } - /// - /// Gets the general properties of the layer - /// - [PropertyGroupDescription(Identifier = "General", Name = "General", Description = "A collection of general properties")] - public LayerGeneralProperties General + // Adaption hints + Adapter.Save(); + + SaveRenderElement(); + } + + #endregion + + #region Shape management + + private void ShapeTypeOnCurrentValueSet(object? sender, EventArgs e) + { + ApplyShapeType(); + } + + private void ApplyShapeType() + { + LayerShape = General.ShapeType.CurrentValue switch { - get => _general; - private set => SetAndNotify(ref _general, value); + LayerShapeType.Ellipse => new EllipseShape(this), + LayerShapeType.Rectangle => new RectangleShape(this), + _ => throw new ArgumentOutOfRangeException() + }; + } + + #endregion + + #region Rendering + + /// + public override void Update(double deltaTime) + { + if (Disposed) + throw new ObjectDisposedException("Layer"); + + if (Timeline.IsOverridden) + { + Timeline.ClearOverride(); + return; } - /// - /// Gets the transform properties of the layer - /// - [PropertyGroupDescription(Identifier = "Transform", Name = "Transform", Description = "A collection of transformation properties")] - public LayerTransformProperties Transform - { - get => _transform; - private set => SetAndNotify(ref _transform, value); - } + UpdateDisplayCondition(); + UpdateTimeline(deltaTime); - /// - /// The brush that will fill the . - /// - public BaseLayerBrush? LayerBrush - { - get => _layerBrush; - internal set => SetAndNotify(ref _layerBrush, value); - } - - /// - /// Gets the layer entity this layer uses for persistent storage - /// - public LayerEntity LayerEntity { get; internal set; } - - /// - /// Gets the layer adapter that can be used to adapt this layer to a different set of devices - /// - public LayerAdapter Adapter { get; } - - /// - public override bool ShouldBeEnabled => !Suspended && DisplayConditionMet; - - internal override RenderElementEntity RenderElementEntity => LayerEntity; - - /// - public override List GetAllLayerProperties() - { - List result = new(); - result.AddRange(General.GetAllLayerProperties()); - result.AddRange(Transform.GetAllLayerProperties()); - if (LayerBrush?.BaseProperties != null) - result.AddRange(LayerBrush.BaseProperties.GetAllLayerProperties()); - foreach (BaseLayerEffect layerEffect in LayerEffects) - if (layerEffect.BaseProperties != null) - result.AddRange(layerEffect.BaseProperties.GetAllLayerProperties()); - - return result; - } - - /// - public override string ToString() - { - return $"[Layer] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}"; - } - - /// - /// Occurs when a property affecting the rendering properties of this layer has been updated - /// - public event EventHandler? RenderPropertiesUpdated; - - /// - /// Occurs when the layer brush of this layer has been updated - /// - public event EventHandler? LayerBrushUpdated; - - #region Overrides of BreakableModel - - /// - public override IEnumerable GetBrokenHierarchy() - { - if (LayerBrush?.BrokenState != null) - yield return LayerBrush; - foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.BrokenState != null)) - yield return baseLayerEffect; - } - - #endregion - - /// - protected override void Dispose(bool disposing) - { + if (ShouldBeEnabled) + Enable(); + else if (Suspended || (Timeline.IsFinished && !_renderCopies.Any())) Disable(); - Disposed = true; + if (Timeline.Delta == TimeSpan.Zero) + return; - LayerBrushStore.LayerBrushAdded -= LayerBrushStoreOnLayerBrushAdded; - LayerBrushStore.LayerBrushRemoved -= LayerBrushStoreOnLayerBrushRemoved; + General.Update(Timeline); + Transform.Update(Timeline); + LayerBrush?.InternalUpdate(Timeline); - // Brush first in case it depends on any of the other disposables during it's own disposal - _layerBrush?.Dispose(); - _general.Dispose(); - _transform.Dispose(); - - base.Dispose(disposing); - } - - internal void OnLayerBrushUpdated() + foreach (BaseLayerEffect baseLayerEffect in LayerEffects) { - LayerBrushUpdated?.Invoke(this, EventArgs.Empty); - } - - private void Initialize() - { - LayerBrushStore.LayerBrushAdded += LayerBrushStoreOnLayerBrushAdded; - LayerBrushStore.LayerBrushRemoved += LayerBrushStoreOnLayerBrushRemoved; - - // Layers have two hardcoded property groups, instantiate them - PropertyGroupDescriptionAttribute generalAttribute = (PropertyGroupDescriptionAttribute) Attribute.GetCustomAttribute( - GetType().GetProperty(nameof(General))!, - typeof(PropertyGroupDescriptionAttribute) - )!; - PropertyGroupDescriptionAttribute transformAttribute = (PropertyGroupDescriptionAttribute) Attribute.GetCustomAttribute( - GetType().GetProperty(nameof(Transform))!, - typeof(PropertyGroupDescriptionAttribute) - )!; - - LayerEntity.GeneralPropertyGroup ??= new PropertyGroupEntity {Identifier = generalAttribute.Identifier}; - LayerEntity.TransformPropertyGroup ??= new PropertyGroupEntity {Identifier = transformAttribute.Identifier}; - - General.Initialize(this, null, generalAttribute, LayerEntity.GeneralPropertyGroup); - Transform.Initialize(this, null, transformAttribute, LayerEntity.TransformPropertyGroup); - - General.ShapeType.CurrentValueSet += ShapeTypeOnCurrentValueSet; - ApplyShapeType(); - ActivateLayerBrush(); - - Reset(); - } - - private void LayerBrushStoreOnLayerBrushRemoved(object? sender, LayerBrushStoreEvent e) - { - if (LayerBrush?.Descriptor == e.Registration.LayerBrushDescriptor) - DeactivateLayerBrush(); - } - - private void LayerBrushStoreOnLayerBrushAdded(object? sender, LayerBrushStoreEvent e) - { - if (LayerBrush != null || !General.PropertiesInitialized) - return; - - LayerBrushReference? current = General.BrushReference.CurrentValue; - if (e.Registration.PluginFeature.Id == current?.LayerBrushProviderId && e.Registration.LayerBrushDescriptor.LayerBrushType.Name == current.BrushType) - ActivateLayerBrush(); - } - - private void OnRenderPropertiesUpdated() - { - RenderPropertiesUpdated?.Invoke(this, EventArgs.Empty); - } - - #region Storage - - internal override void Load() - { - EntityId = LayerEntity.Id; - Name = LayerEntity.Name; - Suspended = LayerEntity.Suspended; - Order = LayerEntity.Order; - - LoadRenderElement(); - Adapter.Load(); - } - - internal override void Save() - { - if (Disposed) - throw new ObjectDisposedException("Layer"); - - // Properties - LayerEntity.Id = EntityId; - LayerEntity.ParentId = Parent?.EntityId ?? new Guid(); - LayerEntity.Order = Order; - LayerEntity.Suspended = Suspended; - LayerEntity.Name = Name; - LayerEntity.ProfileId = Profile.EntityId; - - General.ApplyToEntity(); - Transform.ApplyToEntity(); - - // Don't override the old value of LayerBrush if the current value is null, this avoid losing settings of an unavailable brush - if (LayerBrush != null) - { - LayerBrush.Save(); - LayerEntity.LayerBrush = LayerBrush.LayerBrushEntity; - } - - // LEDs - LayerEntity.Leds.Clear(); - foreach (ArtemisLed artemisLed in Leds) - { - LedEntity ledEntity = new() - { - DeviceIdentifier = artemisLed.Device.Identifier, - LedName = artemisLed.RgbLed.Id.ToString(), - PhysicalLayout = artemisLed.Device.DeviceType == RGBDeviceType.Keyboard ? (int) artemisLed.Device.PhysicalLayout : null - }; - LayerEntity.Leds.Add(ledEntity); - } - - // Adaption hints - Adapter.Save(); - - SaveRenderElement(); - } - - #endregion - - #region Shape management - - private void ShapeTypeOnCurrentValueSet(object? sender, EventArgs e) - { - ApplyShapeType(); - } - - private void ApplyShapeType() - { - LayerShape = General.ShapeType.CurrentValue switch - { - LayerShapeType.Ellipse => new EllipseShape(this), - LayerShapeType.Rectangle => new RectangleShape(this), - _ => throw new ArgumentOutOfRangeException() - }; - } - - #endregion - - #region Rendering - - /// - public override void Update(double deltaTime) - { - if (Disposed) - throw new ObjectDisposedException("Layer"); - - if (Timeline.IsOverridden) - { - Timeline.ClearOverride(); - return; - } - - UpdateDisplayCondition(); - UpdateTimeline(deltaTime); - - if (ShouldBeEnabled) - Enable(); - else if (Suspended || (Timeline.IsFinished && !_renderCopies.Any())) - Disable(); - - if (Timeline.Delta == TimeSpan.Zero) - return; - - General.Update(Timeline); - Transform.Update(Timeline); - LayerBrush?.InternalUpdate(Timeline); - - foreach (BaseLayerEffect baseLayerEffect in LayerEffects) - { - if (!baseLayerEffect.Suspended) - baseLayerEffect.InternalUpdate(Timeline); - } - - // Remove render copies that finished their timeline and update the rest - for (int index = 0; index < _renderCopies.Count; index++) - { - Layer child = _renderCopies[index]; - if (!child.Timeline.IsFinished) - { - child.Update(deltaTime); - } - else - { - _renderCopies.Remove(child); - child.Dispose(); - index--; - } - } - } - - /// - public override void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus) - { - if (Disposed) - throw new ObjectDisposedException("Layer"); - - if (editorFocus != null && editorFocus != this) - return; - - RenderLayer(canvas, basePosition); - RenderCopies(canvas, basePosition); - } - - private void RenderLayer(SKCanvas canvas, SKPointI basePosition) - { - // Ensure the layer is ready - if (!Enabled || Path == null || LayerShape?.Path == null || !General.PropertiesInitialized || !Transform.PropertiesInitialized || !Leds.Any()) - return; - - // Ensure the brush is ready - if (LayerBrush == null || LayerBrush?.BaseProperties?.PropertiesInitialized == false) - return; - - if (Timeline.IsFinished || LayerBrush?.BrushType != LayerBrushType.Regular) - return; - - SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low}; - try - { - using SKAutoCanvasRestore _ = new(canvas); - canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y); - using SKPath clipPath = new(Path); - clipPath.Transform(SKMatrix.CreateTranslation(Bounds.Left * -1, Bounds.Top * -1)); - canvas.ClipPath(clipPath, SKClipOperation.Intersect, true); - SKRectI layerBounds = SKRectI.Create(0, 0, Bounds.Width, Bounds.Height); - - // Apply blend mode and color - layerPaint.BlendMode = General.BlendMode.CurrentValue; - layerPaint.Color = new SKColor(0, 0, 0, (byte) (Transform.Opacity.CurrentValue * 2.55f)); - - using SKPath renderPath = new(); - - if (General.ShapeType.CurrentValue == LayerShapeType.Rectangle) - renderPath.AddRect(layerBounds); - else - renderPath.AddOval(layerBounds); - - if (General.TransformMode.CurrentValue == LayerTransformMode.Normal) - { - // Apply transformation except rotation to the render path - if (LayerBrush.SupportsTransformation) - { - SKMatrix renderPathMatrix = GetTransformMatrix(true, true, true, false); - renderPath.Transform(renderPathMatrix); - } - - // Apply rotation to the canvas - if (LayerBrush.SupportsTransformation) - { - SKMatrix rotationMatrix = GetTransformMatrix(true, false, false, true); - canvas.SetMatrix(canvas.TotalMatrix.PreConcat(rotationMatrix)); - } - - DelegateRendering(canvas, renderPath, renderPath.Bounds, layerPaint); - } - else if (General.TransformMode.CurrentValue == LayerTransformMode.Clip) - { - SKMatrix renderPathMatrix = GetTransformMatrix(true, true, true, true); - renderPath.Transform(renderPathMatrix); - - DelegateRendering(canvas, renderPath, layerBounds, layerPaint); - } - } - finally - { - layerPaint.DisposeSelfAndProperties(); - } - - Timeline.ClearDelta(); - } - - private void RenderCopies(SKCanvas canvas, SKPointI basePosition) - { - for (int i = _renderCopies.Count - 1; i >= 0; i--) - _renderCopies[i].Render(canvas, basePosition, null); - } - - /// - public override void Enable() - { - // No checks here, the brush and effects will do their own checks to ensure they never enable twice - bool tryOrBreak = TryOrBreak(() => LayerBrush?.InternalEnable(), "Failed to enable layer brush"); - if (!tryOrBreak) - return; - - tryOrBreak = TryOrBreak(() => - { - foreach (BaseLayerEffect baseLayerEffect in LayerEffects) - baseLayerEffect.InternalEnable(); - }, "Failed to enable one or more effects"); - if (!tryOrBreak) - return; - - Enabled = true; - } - - /// - public override void Disable() - { - // No checks here, the brush and effects will do their own checks to ensure they never disable twice - LayerBrush?.InternalDisable(); - foreach (BaseLayerEffect baseLayerEffect in LayerEffects) - baseLayerEffect.InternalDisable(); - - Enabled = false; - } - - /// - public override void OverrideTimelineAndApply(TimeSpan position) - { - DisplayCondition.OverrideTimeline(position); - - General.Update(Timeline); - Transform.Update(Timeline); - LayerBrush?.InternalUpdate(Timeline); - - foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) + if (!baseLayerEffect.Suspended) baseLayerEffect.InternalUpdate(Timeline); } - /// - public override void Reset() + // Remove render copies that finished their timeline and update the rest + for (int index = 0; index < _renderCopies.Count; index++) { - UpdateDisplayCondition(); - - if (DisplayConditionMet) - Timeline.JumpToStart(); - else - Timeline.JumpToEnd(); - - while (_renderCopies.Any()) + Layer child = _renderCopies[index]; + if (!child.Timeline.IsFinished) { - _renderCopies[0].Dispose(); - _renderCopies.RemoveAt(0); - } - } - - /// - /// Creates a copy of this layer and renders it alongside this layer for as long as its timeline lasts. - /// - /// The total maximum of render copies to keep - public void CreateRenderCopy(int max) - { - if (_renderCopies.Count >= max) - return; - - Layer copy = new(this); - _renderCopies.Add(copy); - } - - internal void CalculateRenderProperties() - { - if (Disposed) - throw new ObjectDisposedException("Layer"); - - if (!Leds.Any()) - { - Path = new SKPath(); + child.Update(deltaTime); } else { - SKPath path = new() {FillType = SKPathFillType.Winding}; - foreach (ArtemisLed artemisLed in Leds) - path.AddRect(artemisLed.AbsoluteRectangle); - - Path = path; + _renderCopies.Remove(child); + child.Dispose(); + index--; } - - // This is called here so that the shape's render properties are up to date when other code - // responds to OnRenderPropertiesUpdated - LayerShape?.CalculateRenderProperties(); - - // Folder render properties are based on child paths and thus require an update - if (Parent is Folder folder) - folder.CalculateRenderProperties(); - - OnRenderPropertiesUpdated(); } + } - internal SKPoint GetLayerAnchorPosition(bool applyTranslation, bool zeroBased, SKRect? customBounds = null) + /// + public override void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus) + { + if (Disposed) + throw new ObjectDisposedException("Layer"); + + if (editorFocus != null && editorFocus != this) + return; + + RenderLayer(canvas, basePosition); + RenderCopies(canvas, basePosition); + } + + private void RenderLayer(SKCanvas canvas, SKPointI basePosition) + { + // Ensure the layer is ready + if (!Enabled || Path == null || LayerShape?.Path == null || !General.PropertiesInitialized || !Transform.PropertiesInitialized || !Leds.Any()) + return; + + // Ensure the brush is ready + if (LayerBrush == null || LayerBrush?.BaseProperties?.PropertiesInitialized == false) + return; + + if (Timeline.IsFinished || LayerBrush?.BrushType != LayerBrushType.Regular) + return; + + SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low}; + try { - if (Disposed) - throw new ObjectDisposedException("Layer"); + using SKAutoCanvasRestore _ = new(canvas); + canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y); + using SKPath clipPath = new(Path); + clipPath.Transform(SKMatrix.CreateTranslation(Bounds.Left * -1, Bounds.Top * -1)); + canvas.ClipPath(clipPath, SKClipOperation.Intersect, true); + SKRectI layerBounds = SKRectI.Create(0, 0, Bounds.Width, Bounds.Height); - SKRect bounds = customBounds ?? Bounds; - SKPoint positionProperty = Transform.Position.CurrentValue; + // Apply blend mode and color + layerPaint.BlendMode = General.BlendMode.CurrentValue; + layerPaint.Color = new SKColor(0, 0, 0, (byte) (Transform.Opacity.CurrentValue * 2.55f)); - // Start at the top left of the shape - SKPoint position = zeroBased ? new SKPoint(0, 0) : new SKPoint(bounds.Left, bounds.Top); + using SKPath renderPath = new(); - // Apply translation - if (applyTranslation) + if (General.ShapeType.CurrentValue == LayerShapeType.Rectangle) + renderPath.AddRect(layerBounds); + else + renderPath.AddOval(layerBounds); + + if (General.TransformMode.CurrentValue == LayerTransformMode.Normal) { - position.X += positionProperty.X * bounds.Width; - position.Y += positionProperty.Y * bounds.Height; - } + // Apply transformation except rotation to the render path + if (LayerBrush.SupportsTransformation) + { + SKMatrix renderPathMatrix = GetTransformMatrix(true, true, true, false); + renderPath.Transform(renderPathMatrix); + } - return position; + // Apply rotation to the canvas + if (LayerBrush.SupportsTransformation) + { + SKMatrix rotationMatrix = GetTransformMatrix(true, false, false, true); + canvas.SetMatrix(canvas.TotalMatrix.PreConcat(rotationMatrix)); + } + + DelegateRendering(canvas, renderPath, renderPath.Bounds, layerPaint); + } + else if (General.TransformMode.CurrentValue == LayerTransformMode.Clip) + { + SKMatrix renderPathMatrix = GetTransformMatrix(true, true, true, true); + renderPath.Transform(renderPathMatrix); + + DelegateRendering(canvas, renderPath, layerBounds, layerPaint); + } + } + finally + { + layerPaint.DisposeSelfAndProperties(); } - private void DelegateRendering(SKCanvas canvas, SKPath renderPath, SKRect bounds, SKPaint layerPaint) + Timeline.ClearDelta(); + } + + private void RenderCopies(SKCanvas canvas, SKPointI basePosition) + { + for (int i = _renderCopies.Count - 1; i >= 0; i--) + _renderCopies[i].Render(canvas, basePosition, null); + } + + /// + public override void Enable() + { + // No checks here, the brush and effects will do their own checks to ensure they never enable twice + bool tryOrBreak = TryOrBreak(() => LayerBrush?.InternalEnable(), "Failed to enable layer brush"); + if (!tryOrBreak) + return; + + tryOrBreak = TryOrBreak(() => { - if (LayerBrush == null) - throw new ArtemisCoreException("The layer is not yet ready for rendering"); + foreach (BaseLayerEffect baseLayerEffect in LayerEffects) + baseLayerEffect.InternalEnable(); + }, "Failed to enable one or more effects"); + if (!tryOrBreak) + return; + + Enabled = true; + } + + /// + public override void Disable() + { + // No checks here, the brush and effects will do their own checks to ensure they never disable twice + LayerBrush?.InternalDisable(); + foreach (BaseLayerEffect baseLayerEffect in LayerEffects) + baseLayerEffect.InternalDisable(); + + Enabled = false; + } + + /// + public override void OverrideTimelineAndApply(TimeSpan position) + { + DisplayCondition.OverrideTimeline(position); + + General.Update(Timeline); + Transform.Update(Timeline); + LayerBrush?.InternalUpdate(Timeline); + + foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) + baseLayerEffect.InternalUpdate(Timeline); + } + + /// + public override void Reset() + { + UpdateDisplayCondition(); + + if (DisplayConditionMet) + Timeline.JumpToStart(); + else + Timeline.JumpToEnd(); + + while (_renderCopies.Any()) + { + _renderCopies[0].Dispose(); + _renderCopies.RemoveAt(0); + } + } + + /// + /// Creates a copy of this layer and renders it alongside this layer for as long as its timeline lasts. + /// + /// The total maximum of render copies to keep + public void CreateRenderCopy(int max) + { + if (_renderCopies.Count >= max) + return; + + Layer copy = new(this); + _renderCopies.Add(copy); + } + + internal void CalculateRenderProperties() + { + if (Disposed) + throw new ObjectDisposedException("Layer"); + + if (!Leds.Any()) + { + Path = new SKPath(); + } + else + { + SKPath path = new() {FillType = SKPathFillType.Winding}; + foreach (ArtemisLed artemisLed in Leds) + path.AddRect(artemisLed.AbsoluteRectangle); + + Path = path; + } + + // This is called here so that the shape's render properties are up to date when other code + // responds to OnRenderPropertiesUpdated + LayerShape?.CalculateRenderProperties(); + + // Folder render properties are based on child paths and thus require an update + if (Parent is Folder folder) + folder.CalculateRenderProperties(); + + OnRenderPropertiesUpdated(); + } + + internal SKPoint GetLayerAnchorPosition(bool applyTranslation, bool zeroBased, SKRect? customBounds = null) + { + if (Disposed) + throw new ObjectDisposedException("Layer"); + + SKRect bounds = customBounds ?? Bounds; + SKPoint positionProperty = Transform.Position.CurrentValue; + + // Start at the top left of the shape + SKPoint position = zeroBased ? new SKPoint(0, 0) : new SKPoint(bounds.Left, bounds.Top); + + // Apply translation + if (applyTranslation) + { + position.X += positionProperty.X * bounds.Width; + position.Y += positionProperty.Y * bounds.Height; + } + + return position; + } + + private void DelegateRendering(SKCanvas canvas, SKPath renderPath, SKRect bounds, SKPaint layerPaint) + { + if (LayerBrush == null) + throw new ArtemisCoreException("The layer is not yet ready for rendering"); + + foreach (BaseLayerEffect baseLayerEffect in LayerEffects) + { + if (!baseLayerEffect.Suspended) + baseLayerEffect.InternalPreProcess(canvas, bounds, layerPaint); + } + + try + { + canvas.SaveLayer(layerPaint); + canvas.ClipPath(renderPath); + + // Restore the blend mode before doing the actual render + layerPaint.BlendMode = SKBlendMode.SrcOver; + + LayerBrush.InternalRender(canvas, bounds, layerPaint); foreach (BaseLayerEffect baseLayerEffect in LayerEffects) { if (!baseLayerEffect.Suspended) - baseLayerEffect.InternalPreProcess(canvas, bounds, layerPaint); - } - - try - { - canvas.SaveLayer(layerPaint); - canvas.ClipPath(renderPath); - - // Restore the blend mode before doing the actual render - layerPaint.BlendMode = SKBlendMode.SrcOver; - - LayerBrush.InternalRender(canvas, bounds, layerPaint); - - foreach (BaseLayerEffect baseLayerEffect in LayerEffects) - { - if (!baseLayerEffect.Suspended) - baseLayerEffect.InternalPostProcess(canvas, bounds, layerPaint); - } - } - finally - { - canvas.Restore(); + baseLayerEffect.InternalPostProcess(canvas, bounds, layerPaint); } } - - /// - /// Creates a transformation matrix that applies the current transformation settings - /// - /// - /// If true, treats the layer as if it is located at 0,0 instead of its actual position on the - /// surface - /// - /// Whether translation should be included - /// Whether the scale should be included - /// Whether the rotation should be included - /// Optional custom bounds to base the anchor on - /// The transformation matrix containing the current transformation settings - public SKMatrix GetTransformMatrix(bool zeroBased, bool includeTranslation, bool includeScale, bool includeRotation, SKRect? customBounds = null) + finally { - if (Disposed) - throw new ObjectDisposedException("Layer"); - - if (Path == null) - return SKMatrix.Empty; - - SKRect bounds = customBounds ?? Bounds; - SKSize sizeProperty = Transform.Scale.CurrentValue; - float rotationProperty = Transform.Rotation.CurrentValue; - - SKPoint anchorPosition = GetLayerAnchorPosition(true, zeroBased, bounds); - SKPoint anchorProperty = Transform.AnchorPoint.CurrentValue; - - // Translation originates from the top left of the shape and is tied to the anchor - float x = anchorPosition.X - (zeroBased ? 0 : bounds.Left) - anchorProperty.X * bounds.Width; - float y = anchorPosition.Y - (zeroBased ? 0 : bounds.Top) - anchorProperty.Y * bounds.Height; - - SKMatrix transform = SKMatrix.Empty; - - if (includeTranslation) - // transform is always SKMatrix.Empty here... - transform = SKMatrix.CreateTranslation(x, y); - - if (includeScale) - { - if (transform == SKMatrix.Empty) - transform = SKMatrix.CreateScale(sizeProperty.Width / 100f, sizeProperty.Height / 100f, anchorPosition.X, anchorPosition.Y); - else - transform = transform.PostConcat(SKMatrix.CreateScale(sizeProperty.Width / 100f, sizeProperty.Height / 100f, anchorPosition.X, anchorPosition.Y)); - } - - if (includeRotation) - { - if (transform == SKMatrix.Empty) - transform = SKMatrix.CreateRotationDegrees(rotationProperty, anchorPosition.X, anchorPosition.Y); - else - transform = transform.PostConcat(SKMatrix.CreateRotationDegrees(rotationProperty, anchorPosition.X, anchorPosition.Y)); - } - - return transform; + canvas.Restore(); } + } - #endregion + /// + /// Creates a transformation matrix that applies the current transformation settings + /// + /// + /// If true, treats the layer as if it is located at 0,0 instead of its actual position on the + /// surface + /// + /// Whether translation should be included + /// Whether the scale should be included + /// Whether the rotation should be included + /// Optional custom bounds to base the anchor on + /// The transformation matrix containing the current transformation settings + public SKMatrix GetTransformMatrix(bool zeroBased, bool includeTranslation, bool includeScale, bool includeRotation, SKRect? customBounds = null) + { + if (Disposed) + throw new ObjectDisposedException("Layer"); - #region LED management + if (Path == null) + return SKMatrix.Empty; - /// - /// Adds a new to the layer and updates the render properties. - /// - /// The LED to add - public void AddLed(ArtemisLed led) + SKRect bounds = customBounds ?? Bounds; + SKSize sizeProperty = Transform.Scale.CurrentValue; + float rotationProperty = Transform.Rotation.CurrentValue; + + SKPoint anchorPosition = GetLayerAnchorPosition(true, zeroBased, bounds); + SKPoint anchorProperty = Transform.AnchorPoint.CurrentValue; + + // Translation originates from the top left of the shape and is tied to the anchor + float x = anchorPosition.X - (zeroBased ? 0 : bounds.Left) - anchorProperty.X * bounds.Width; + float y = anchorPosition.Y - (zeroBased ? 0 : bounds.Top) - anchorProperty.Y * bounds.Height; + + SKMatrix transform = SKMatrix.Empty; + + if (includeTranslation) + // transform is always SKMatrix.Empty here... + transform = SKMatrix.CreateTranslation(x, y); + + if (includeScale) { - if (Disposed) - throw new ObjectDisposedException("Layer"); - - _leds.Add(led); - CalculateRenderProperties(); - } - - /// - /// Adds a collection of new s to the layer and updates the render properties. - /// - /// The LEDs to add - public void AddLeds(IEnumerable leds) - { - if (Disposed) - throw new ObjectDisposedException("Layer"); - - _leds.AddRange(leds.Except(_leds)); - CalculateRenderProperties(); - } - - /// - /// Removes a from the layer and updates the render properties. - /// - /// The LED to remove - public void RemoveLed(ArtemisLed led) - { - if (Disposed) - throw new ObjectDisposedException("Layer"); - - _leds.Remove(led); - CalculateRenderProperties(); - } - - /// - /// Removes all s from the layer and updates the render properties. - /// - public void ClearLeds() - { - if (Disposed) - throw new ObjectDisposedException("Layer"); - - _leds.Clear(); - CalculateRenderProperties(); - } - - internal void PopulateLeds(IEnumerable devices) - { - if (Disposed) - throw new ObjectDisposedException("Layer"); - - List leds = new(); - - // Get the surface LEDs for this layer - List availableLeds = devices.SelectMany(d => d.Leds).ToList(); - foreach (LedEntity ledEntity in LayerEntity.Leds) - { - ArtemisLed? match = availableLeds.FirstOrDefault(a => a.Device.Identifier == ledEntity.DeviceIdentifier && - a.RgbLed.Id.ToString() == ledEntity.LedName); - if (match != null) - leds.Add(match); - } - - _leds = leds; - Leds = new ReadOnlyCollection(_leds); - CalculateRenderProperties(); - } - - #endregion - - #region Brush management - - /// - /// Changes the current layer brush to the provided layer brush and activates it - /// - public void ChangeLayerBrush(BaseLayerBrush? layerBrush) - { - BaseLayerBrush? oldLayerBrush = LayerBrush; - - General.BrushReference.SetCurrentValue(layerBrush != null ? new LayerBrushReference(layerBrush.Descriptor) : null, null); - LayerBrush = layerBrush; - - oldLayerBrush?.InternalDisable(); - - if (LayerBrush != null) - ActivateLayerBrush(); + if (transform == SKMatrix.Empty) + transform = SKMatrix.CreateScale(sizeProperty.Width / 100f, sizeProperty.Height / 100f, anchorPosition.X, anchorPosition.Y); else - OnLayerBrushUpdated(); + transform = transform.PostConcat(SKMatrix.CreateScale(sizeProperty.Width / 100f, sizeProperty.Height / 100f, anchorPosition.X, anchorPosition.Y)); } - internal void ActivateLayerBrush() + if (includeRotation) { - try - { - if (LayerBrush == null) - { - // If the brush is null, try to instantiate it - LayerBrushReference? brushReference = General.BrushReference.CurrentValue; - if (brushReference?.LayerBrushProviderId != null && brushReference.BrushType != null) - ChangeLayerBrush(LayerBrushStore.Get(brushReference.LayerBrushProviderId, brushReference.BrushType)?.LayerBrushDescriptor.CreateInstance(this, LayerEntity.LayerBrush)); - // If that's not possible there's nothing to do - return; - } - - General.ShapeType.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation; - General.BlendMode.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation; - Transform.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation; - if (LayerBrush != null) - { - if (!LayerBrush.Enabled) - LayerBrush.InternalEnable(); - LayerBrush?.Update(0); - } - - OnLayerBrushUpdated(); - ClearBrokenState("Failed to initialize layer brush"); - } - catch (Exception e) - { - SetBrokenState("Failed to initialize layer brush", e); - } + if (transform == SKMatrix.Empty) + transform = SKMatrix.CreateRotationDegrees(rotationProperty, anchorPosition.X, anchorPosition.Y); + else + transform = transform.PostConcat(SKMatrix.CreateRotationDegrees(rotationProperty, anchorPosition.X, anchorPosition.Y)); } - internal void DeactivateLayerBrush() + return transform; + } + + #endregion + + #region LED management + + /// + /// Adds a new to the layer and updates the render properties. + /// + /// The LED to add + public void AddLed(ArtemisLed led) + { + if (Disposed) + throw new ObjectDisposedException("Layer"); + + _leds.Add(led); + CalculateRenderProperties(); + } + + /// + /// Adds a collection of new s to the layer and updates the render properties. + /// + /// The LEDs to add + public void AddLeds(IEnumerable leds) + { + if (Disposed) + throw new ObjectDisposedException("Layer"); + + _leds.AddRange(leds.Except(_leds)); + CalculateRenderProperties(); + } + + /// + /// Removes a from the layer and updates the render properties. + /// + /// The LED to remove + public void RemoveLed(ArtemisLed led) + { + if (Disposed) + throw new ObjectDisposedException("Layer"); + + _leds.Remove(led); + CalculateRenderProperties(); + } + + /// + /// Removes all s from the layer and updates the render properties. + /// + public void ClearLeds() + { + if (Disposed) + throw new ObjectDisposedException("Layer"); + + _leds.Clear(); + CalculateRenderProperties(); + } + + internal void PopulateLeds(IEnumerable devices) + { + if (Disposed) + throw new ObjectDisposedException("Layer"); + + List leds = new(); + + // Get the surface LEDs for this layer + List availableLeds = devices.SelectMany(d => d.Leds).ToList(); + foreach (LedEntity ledEntity in LayerEntity.Leds) + { + ArtemisLed? match = availableLeds.FirstOrDefault(a => a.Device.Identifier == ledEntity.DeviceIdentifier && + a.RgbLed.Id.ToString() == ledEntity.LedName); + if (match != null) + leds.Add(match); + } + + _leds = leds; + Leds = new ReadOnlyCollection(_leds); + CalculateRenderProperties(); + } + + #endregion + + #region Brush management + + /// + /// Changes the current layer brush to the provided layer brush and activates it + /// + public void ChangeLayerBrush(BaseLayerBrush? layerBrush) + { + BaseLayerBrush? oldLayerBrush = LayerBrush; + + General.BrushReference.SetCurrentValue(layerBrush != null ? new LayerBrushReference(layerBrush.Descriptor) : null); + LayerBrush = layerBrush; + + oldLayerBrush?.InternalDisable(); + + if (LayerBrush != null) + ActivateLayerBrush(); + else + OnLayerBrushUpdated(); + } + + internal void ActivateLayerBrush() + { + try { if (LayerBrush == null) + { + // If the brush is null, try to instantiate it + LayerBrushReference? brushReference = General.BrushReference.CurrentValue; + if (brushReference?.LayerBrushProviderId != null && brushReference.BrushType != null) + ChangeLayerBrush(LayerBrushStore.Get(brushReference.LayerBrushProviderId, brushReference.BrushType)?.LayerBrushDescriptor.CreateInstance(this, LayerEntity.LayerBrush)); + // If that's not possible there's nothing to do return; + } - BaseLayerBrush? brush = LayerBrush; - LayerBrush = null; - brush?.Dispose(); + General.ShapeType.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation; + General.BlendMode.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation; + Transform.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation; + if (LayerBrush != null) + { + if (!LayerBrush.Enabled) + LayerBrush.InternalEnable(); + LayerBrush?.Update(0); + } OnLayerBrushUpdated(); + ClearBrokenState("Failed to initialize layer brush"); + } + catch (Exception e) + { + SetBrokenState("Failed to initialize layer brush", e); } - - #endregion } + internal void DeactivateLayerBrush() + { + if (LayerBrush == null) + return; + + BaseLayerBrush? brush = LayerBrush; + LayerBrush = null; + brush?.Dispose(); + + OnLayerBrushUpdated(); + } + + #endregion +} + +/// +/// Represents a type of layer shape +/// +public enum LayerShapeType +{ + /// + /// A circular layer shape + /// + Ellipse, + /// - /// Represents a type of layer shape + /// A rectangular layer shape /// - public enum LayerShapeType - { - /// - /// A circular layer shape - /// - Ellipse, + Rectangle +} - /// - /// A rectangular layer shape - /// - Rectangle - } +/// +/// Represents a layer transform mode +/// +public enum LayerTransformMode +{ + /// + /// Normal transformation + /// + Normal, /// - /// Represents a layer transform mode + /// Transforms only a clip /// - public enum LayerTransformMode - { - /// - /// Normal transformation - /// - Normal, - - /// - /// Transforms only a clip - /// - Clip - } + Clip } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerAdapter.cs b/src/Artemis.Core/Models/Profile/LayerAdapter.cs index aa8ceeb5c..30c922a1d 100644 --- a/src/Artemis.Core/Models/Profile/LayerAdapter.cs +++ b/src/Artemis.Core/Models/Profile/LayerAdapter.cs @@ -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; + +/// +/// Represents an adapter that adapts a layer to a certain set of devices using s +/// +public class LayerAdapter : IStorageModel { - /// - /// Represents an adapter that adapts a layer to a certain set of devices using s - /// - public class LayerAdapter : IStorageModel + private readonly List _adaptionHints; + + internal LayerAdapter(Layer layer) { - private readonly List _adaptionHints; - - internal LayerAdapter(Layer layer) - { - _adaptionHints = new List(); - Layer = layer; - AdaptionHints = new ReadOnlyCollection(_adaptionHints); - } - - /// - /// Gets the layer this adapter can adapt - /// - public Layer Layer { get; } - - /// - /// Gets or sets a list containing the adaption hints used by this adapter - /// - public ReadOnlyCollection AdaptionHints { get; set; } - - /// - /// Modifies the layer, adapting it to the provided - /// - /// The devices to adapt the layer to - public void Adapt(List 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 availableLeds = devices.SelectMany(d => d.Leds).ToList(); - List 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(ledEntity.LedName); - ArtemisLed? led = availableLeds.FirstOrDefault(l => l.RgbLed.Id == ledId); - - if (led != null) - { - availableLeds.Remove(led); - usedLeds.Add(led); - } - } - - Layer.AddLeds(usedLeds); - } - } - - /// - /// Automatically determine hints for this layer - /// - public List DetermineHints(IEnumerable devices) - { - List 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 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()) - { - if (AdaptionHints.Any(h => h is CategoryAdaptionHint c && c.Category == deviceCategory)) - continue; - - List 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)); - } - - /// - /// Adds an adaption hint to the adapter. - /// - /// The adaption hint to add. - public void Add(IAdaptionHint adaptionHint) - { - if (_adaptionHints.Contains(adaptionHint)) - return; - - _adaptionHints.Add(adaptionHint); - AdapterHintAdded?.Invoke(this, new LayerAdapterHintEventArgs(adaptionHint)); - } - - /// - /// Removes the first occurrence of a specific adaption hint from the adapter. - /// - /// The adaption hint to remove. - public void Remove(IAdaptionHint adaptionHint) - { - if (_adaptionHints.Remove(adaptionHint)) - AdapterHintRemoved?.Invoke(this, new LayerAdapterHintEventArgs(adaptionHint)); - } - - /// - /// Removes all adaption hints from the adapter. - /// - public void Clear() - { - while (_adaptionHints.Any()) - Remove(_adaptionHints.First()); - } - - #region Implementation of IStorageModel - - /// - 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; - } - } - - /// - public void Save() - { - Layer.LayerEntity.AdaptionHints.Clear(); - foreach (IAdaptionHint adaptionHint in AdaptionHints) - Layer.LayerEntity.AdaptionHints.Add(adaptionHint.GetEntry()); - } - - #endregion - - /// - /// Occurs whenever a new adapter hint is added to the adapter. - /// - public event EventHandler? AdapterHintAdded; - - /// - /// Occurs whenever an adapter hint is removed from the adapter. - /// - public event EventHandler? AdapterHintRemoved; + _adaptionHints = new List(); + Layer = layer; + AdaptionHints = new ReadOnlyCollection(_adaptionHints); } + + /// + /// Gets the layer this adapter can adapt + /// + public Layer Layer { get; } + + /// + /// Gets or sets a list containing the adaption hints used by this adapter + /// + public ReadOnlyCollection AdaptionHints { get; set; } + + /// + /// Modifies the layer, adapting it to the provided + /// + /// The devices to adapt the layer to + public void Adapt(List 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 availableLeds = devices.SelectMany(d => d.Leds).ToList(); + List 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(ledEntity.LedName); + ArtemisLed? led = availableLeds.FirstOrDefault(l => l.RgbLed.Id == ledId); + + if (led != null) + { + availableLeds.Remove(led); + usedLeds.Add(led); + } + } + + Layer.AddLeds(usedLeds); + } + } + + /// + /// Automatically determine hints for this layer + /// + public List DetermineHints(IEnumerable devices) + { + List 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 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()) + { + if (AdaptionHints.Any(h => h is CategoryAdaptionHint c && c.Category == deviceCategory)) + continue; + + List 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; + } + + /// + /// Adds an adaption hint to the adapter. + /// + /// The adaption hint to add. + public void Add(IAdaptionHint adaptionHint) + { + if (_adaptionHints.Contains(adaptionHint)) + return; + + _adaptionHints.Add(adaptionHint); + AdapterHintAdded?.Invoke(this, new LayerAdapterHintEventArgs(adaptionHint)); + } + + /// + /// Removes the first occurrence of a specific adaption hint from the adapter. + /// + /// The adaption hint to remove. + public void Remove(IAdaptionHint adaptionHint) + { + if (_adaptionHints.Remove(adaptionHint)) + AdapterHintRemoved?.Invoke(this, new LayerAdapterHintEventArgs(adaptionHint)); + } + + /// + /// Removes all adaption hints from the adapter. + /// + public void Clear() + { + while (_adaptionHints.Any()) + Remove(_adaptionHints.First()); + } + + /// + /// Occurs whenever a new adapter hint is added to the adapter. + /// + public event EventHandler? AdapterHintAdded; + + /// + /// Occurs whenever an adapter hint is removed from the adapter. + /// + public event EventHandler? AdapterHintRemoved; + + private bool DoesLayerCoverDevice(ArtemisDevice device) + { + return device.Leds.All(l => Layer.Leds.Contains(l)); + } + + #region Implementation of IStorageModel + + /// + 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; + } + } + } + + /// + public void Save() + { + Layer.LayerEntity.AdaptionHints.Clear(); + foreach (IAdaptionHint adaptionHint in AdaptionHints) + Layer.LayerEntity.AdaptionHints.Add(adaptionHint.GetEntry()); + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerBrushReference.cs b/src/Artemis.Core/Models/Profile/LayerBrushReference.cs index d36617278..3faede42d 100644 --- a/src/Artemis.Core/Models/Profile/LayerBrushReference.cs +++ b/src/Artemis.Core/Models/Profile/LayerBrushReference.cs @@ -1,37 +1,36 @@ using Artemis.Core.LayerBrushes; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// A reference to a +/// +public class LayerBrushReference { /// - /// A reference to a + /// Creates a new instance of the class /// - public class LayerBrushReference + public LayerBrushReference() { - /// - /// Creates a new instance of the class - /// - public LayerBrushReference() - { - } - - /// - /// Creates a new instance of the class - /// - /// The descriptor to point the new reference at - public LayerBrushReference(LayerBrushDescriptor descriptor) - { - LayerBrushProviderId = descriptor.Provider.Id; - BrushType = descriptor.LayerBrushType.Name; - } - - /// - /// The ID of the layer brush provided the brush was provided by - /// - public string? LayerBrushProviderId { get; set; } - - /// - /// The full type name of the brush descriptor - /// - public string? BrushType { get; set; } } + + /// + /// Creates a new instance of the class + /// + /// The descriptor to point the new reference at + public LayerBrushReference(LayerBrushDescriptor descriptor) + { + LayerBrushProviderId = descriptor.Provider.Id; + BrushType = descriptor.LayerBrushType.Name; + } + + /// + /// The ID of the layer brush provided the brush was provided by + /// + public string? LayerBrushProviderId { get; set; } + + /// + /// The full type name of the brush descriptor + /// + public string? BrushType { get; set; } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerEffectPropertyGroup.cs b/src/Artemis.Core/Models/Profile/LayerEffectPropertyGroup.cs index 5e1b6abb7..dad86bab3 100644 --- a/src/Artemis.Core/Models/Profile/LayerEffectPropertyGroup.cs +++ b/src/Artemis.Core/Models/Profile/LayerEffectPropertyGroup.cs @@ -1,25 +1,24 @@ -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a property group on a layer +/// +/// Note: You cannot initialize property groups yourself. If properly placed and annotated, the Artemis core will +/// initialize these for you. +/// +/// +public abstract class LayerEffectPropertyGroup : LayerPropertyGroup { /// - /// Represents a property group on a layer - /// - /// Note: You cannot initialize property groups yourself. If properly placed and annotated, the Artemis core will - /// initialize these for you. - /// + /// Whether or not this layer effect is enabled /// - public abstract class LayerEffectPropertyGroup : LayerPropertyGroup - { - /// - /// Whether or not this layer effect is enabled - /// - [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); } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerGeneralProperties.cs b/src/Artemis.Core/Models/Profile/LayerGeneralProperties.cs index 98caab37d..186d3d3f9 100644 --- a/src/Artemis.Core/Models/Profile/LayerGeneralProperties.cs +++ b/src/Artemis.Core/Models/Profile/LayerGeneralProperties.cs @@ -2,52 +2,51 @@ #pragma warning disable 8618 -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents the general properties of a layer +/// +public class LayerGeneralProperties : LayerPropertyGroup { /// - /// Represents the general properties of a layer + /// The type of brush to use for this layer /// - public class LayerGeneralProperties : LayerPropertyGroup + [PropertyDescription(Name = "Brush type", Description = "The type of brush to use for this layer")] + public LayerBrushReferenceLayerProperty BrushReference { get; set; } + + /// + /// The type of shape to draw in this layer + /// + [PropertyDescription(Name = "Shape type", Description = "The type of shape to draw in this layer")] + public EnumLayerProperty ShapeType { get; set; } + + /// + /// How to blend this layer into the resulting image + /// + [PropertyDescription(Name = "Blend mode", Description = "How to blend this layer into the resulting image")] + public EnumLayerProperty BlendMode { get; set; } + + /// + /// How the transformation properties are applied to the layer + /// + [PropertyDescription(Name = "Transform mode", Description = "How the transformation properties are applied to the layer")] + public EnumLayerProperty TransformMode { get; set; } + + /// + protected override void PopulateDefaults() { - /// - /// The type of brush to use for this layer - /// - [PropertyDescription(Name = "Brush type", Description = "The type of brush to use for this layer")] - public LayerBrushReferenceLayerProperty BrushReference { get; set; } - - /// - /// The type of shape to draw in this layer - /// - [PropertyDescription(Name = "Shape type", Description = "The type of shape to draw in this layer")] - public EnumLayerProperty ShapeType { get; set; } + ShapeType.DefaultValue = LayerShapeType.Rectangle; + BlendMode.DefaultValue = SKBlendMode.SrcOver; + } - /// - /// How to blend this layer into the resulting image - /// - [PropertyDescription(Name = "Blend mode", Description = "How to blend this layer into the resulting image")] - public EnumLayerProperty BlendMode { get; set; } + /// + protected override void EnableProperties() + { + } - /// - /// How the transformation properties are applied to the layer - /// - [PropertyDescription(Name = "Transform mode", Description = "How the transformation properties are applied to the layer")] - public EnumLayerProperty TransformMode { get; set; } - - /// - protected override void PopulateDefaults() - { - ShapeType.DefaultValue = LayerShapeType.Rectangle; - BlendMode.DefaultValue = SKBlendMode.SrcOver; - } - - /// - protected override void EnableProperties() - { - } - - /// - protected override void DisableProperties() - { - } + /// + protected override void DisableProperties() + { } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/LayerPropertyIgnoreAttribute.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/LayerPropertyIgnoreAttribute.cs index 2926ae774..b037edb87 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/LayerPropertyIgnoreAttribute.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/LayerPropertyIgnoreAttribute.cs @@ -1,11 +1,10 @@ using System; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents an attribute that marks a layer property to be ignored +/// +public class LayerPropertyIgnoreAttribute : Attribute { - /// - /// Represents an attribute that marks a layer property to be ignored - /// - public class LayerPropertyIgnoreAttribute : Attribute - { - } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs index b3b58b7bb..3b5461b42 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs @@ -1,55 +1,54 @@ using System; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a description attribute used to decorate layer properties +/// +public class PropertyDescriptionAttribute : Attribute { /// - /// 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 /// - public class PropertyDescriptionAttribute : Attribute - { - /// - /// The identifier of this property used for storage, if not set one will be generated property name in code - /// - public string? Identifier { get; set; } + public string? Identifier { get; set; } - /// - /// The user-friendly name for this property, shown in the UI - /// - public string? Name { get; set; } + /// + /// The user-friendly name for this property, shown in the UI + /// + public string? Name { get; set; } - /// - /// The user-friendly description for this property, shown in the UI - /// - public string? Description { get; set; } + /// + /// The user-friendly description for this property, shown in the UI + /// + public string? Description { get; set; } - /// - /// Input prefix to show before input elements in the UI - /// - public string? InputPrefix { get; set; } + /// + /// Input prefix to show before input elements in the UI + /// + public string? InputPrefix { get; set; } - /// - /// Input affix to show behind input elements in the UI - /// - public string? InputAffix { get; set; } + /// + /// Input affix to show behind input elements in the UI + /// + public string? InputAffix { get; set; } - /// - /// The input drag step size, used in the UI - /// - public float InputStepSize { get; set; } + /// + /// The input drag step size, used in the UI + /// + public float InputStepSize { get; set; } - /// - /// Minimum input value, only enforced in the UI - /// - public object? MinInputValue { get; set; } + /// + /// Minimum input value, only enforced in the UI + /// + public object? MinInputValue { get; set; } - /// - /// Maximum input value, only enforced in the UI - /// - public object? MaxInputValue { get; set; } + /// + /// Maximum input value, only enforced in the UI + /// + public object? MaxInputValue { get; set; } - /// - /// Whether or not keyframes are always disabled - /// - public bool DisableKeyframes { get; set; } - } + /// + /// Whether or not keyframes are always disabled + /// + public bool DisableKeyframes { get; set; } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs index 4049688b6..86dd39705 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs @@ -1,25 +1,25 @@ using System; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a description attribute used to decorate layer property groups +/// +public class PropertyGroupDescriptionAttribute : Attribute { /// - /// 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 /// - public class PropertyGroupDescriptionAttribute : Attribute - { - /// - /// The identifier of this property group used for storage, if not set one will be generated based on the group name in code - /// - public string? Identifier { get; set; } + public string? Identifier { get; set; } - /// - /// The user-friendly name for this property group, shown in the UI. - /// - public string? Name { get; set; } - - /// - /// The user-friendly description for this property group, shown in the UI. - /// - public string? Description { get; set; } - } + /// + /// The user-friendly name for this property group, shown in the UI. + /// + public string? Name { get; set; } + + /// + /// The user-friendly description for this property group, shown in the UI. + /// + public string? Description { get; set; } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/FloatRange.cs b/src/Artemis.Core/Models/Profile/LayerProperties/FloatRange.cs index 3a990297c..5ecc252c0 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/FloatRange.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/FloatRange.cs @@ -1,63 +1,62 @@ using System; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a range between two single-precision floating point numbers +/// +public readonly struct FloatRange { + private readonly Random _rand; + /// - /// Represents a range between two single-precision floating point numbers + /// Creates a new instance of the class /// - public readonly struct FloatRange + /// The start value of the range + /// The end value of the range + public FloatRange(float start, float end) { - private readonly Random _rand; + Start = start; + End = end; - /// - /// Creates a new instance of the class - /// - /// The start value of the range - /// The end value of the range - public FloatRange(float start, float end) - { - Start = start; - End = end; + _rand = new Random(); + } - _rand = new Random(); - } + /// + /// Gets the start value of the range + /// + public float Start { get; } - /// - /// Gets the start value of the range - /// - public float Start { get; } + /// + /// Gets the end value of the range + /// + public float End { get; } - /// - /// Gets the end value of the range - /// - public float End { get; } + /// + /// Determines whether the given value is in this range + /// + /// The value to check + /// + /// Whether the value may be equal to or + /// Defaults to + /// + /// + public bool IsInRange(float value, bool inclusive = true) + { + if (inclusive) + return value >= Start && value <= End; + return value > Start && value < End; + } - /// - /// Determines whether the given value is in this range - /// - /// The value to check - /// - /// Whether the value may be equal to or - /// Defaults to - /// - /// - public bool IsInRange(float value, bool inclusive = true) - { - if (inclusive) - return value >= Start && value <= End; - return value > Start && value < End; - } - - /// - /// Returns a pseudo-random value between and - /// - /// Whether the value may be equal to - /// The pseudo-random value - 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; - } + /// + /// Returns a pseudo-random value between and + /// + /// Whether the value may be equal to + /// The pseudo-random value + 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; } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs index 4ad645a94..a7b5aa8ef 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs @@ -2,152 +2,151 @@ using System.Collections.ObjectModel; using Artemis.Storage.Entities.Profile; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a property on a layer. Properties are saved in storage and can optionally be modified from the UI. +/// +/// Note: You cannot initialize layer properties yourself. If properly placed and annotated, the Artemis core will +/// initialize these for you. +/// +/// +public interface ILayerProperty : IStorageModel, IDisposable { /// - /// 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 + /// + PropertyDescriptionAttribute PropertyDescription { get; } + + /// + /// Gets the profile element (such as layer or folder) this property is applied to + /// + RenderProfileElement ProfileElement { get; } + + /// + /// The parent group of this layer property, set after construction + /// + LayerPropertyGroup LayerPropertyGroup { get; } + + /// + /// Gets or sets whether the property is hidden in the UI + /// + public bool IsHidden { get; set; } + + /// + /// Gets the data binding of this property + /// + IDataBinding BaseDataBinding { get; } + + /// + /// Gets a boolean indicating whether the layer has any data binding properties + /// + public bool HasDataBinding { get; } + + /// + /// Gets a boolean indicating whether data bindings are supported on this type of property + /// + public bool DataBindingsSupported { get; } + + /// + /// Gets the unique path of the property on the render element + /// + string Path { get; } + + /// + /// Gets a read-only list of all the keyframes on this layer property + /// + ReadOnlyCollection UntypedKeyframes { get; } + + /// + /// Gets the type of the property + /// + Type PropertyType { get; } + + /// + /// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied + /// + bool IsLoadedFromStorage { get; } + + /// + /// Initializes the layer property /// - /// 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 + /// /// /// - public interface ILayerProperty : IStorageModel, IDisposable - { - /// - /// Gets the description attribute applied to this property - /// - PropertyDescriptionAttribute PropertyDescription { get; } + void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description); - /// - /// Gets the profile element (such as layer or folder) this property is applied to - /// - RenderProfileElement ProfileElement { get; } + /// + /// Attempts to create a keyframe for this property from the provided entity + /// + /// The entity representing the keyframe to create + /// If succeeded the resulting keyframe, otherwise + ILayerPropertyKeyframe? CreateKeyframeFromEntity(KeyframeEntity keyframeEntity); - /// - /// The parent group of this layer property, set after construction - /// - LayerPropertyGroup LayerPropertyGroup { get; } + /// + /// Overrides the property value with the default value + /// + void ApplyDefaultValue(); - /// - /// Gets or sets whether the property is hidden in the UI - /// - public bool IsHidden { get; set; } - - /// - /// Gets the data binding of this property - /// - IDataBinding BaseDataBinding { get; } - - /// - /// Gets a boolean indicating whether the layer has any data binding properties - /// - public bool HasDataBinding { get; } - - /// - /// Gets a boolean indicating whether data bindings are supported on this type of property - /// - public bool DataBindingsSupported { get; } - - /// - /// Gets the unique path of the property on the render element - /// - string Path { get; } - - /// - /// Gets a read-only list of all the keyframes on this layer property - /// - ReadOnlyCollection UntypedKeyframes { get; } - - /// - /// Gets the type of the property - /// - Type PropertyType { get; } - - /// - /// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied - /// - bool IsLoadedFromStorage { get; } - - /// - /// Initializes the layer property - /// - /// Note: This isn't done in the constructor to keep it parameterless which is easier for implementations of - /// - /// - /// - void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description); - - /// - /// Attempts to create a keyframe for this property from the provided entity - /// - /// The entity representing the keyframe to create - /// If succeeded the resulting keyframe, otherwise - ILayerPropertyKeyframe? CreateKeyframeFromEntity(KeyframeEntity keyframeEntity); - - /// - /// Overrides the property value with the default value - /// - void ApplyDefaultValue(); - - /// - /// Updates the layer properties internal state - /// - /// The timeline to apply to the property - void Update(Timeline timeline); + /// + /// Updates the layer properties internal state + /// + /// The timeline to apply to the property + void Update(Timeline timeline); - /// - /// Updates just the data binding instead of the entire layer - /// - void UpdateDataBinding(); + /// + /// Updates just the data binding instead of the entire layer + /// + void UpdateDataBinding(); - /// - /// Removes a keyframe from the layer property without knowing it's type. - /// Prefer . - /// - /// - void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe); + /// + /// Removes a keyframe from the layer property without knowing it's type. + /// Prefer . + /// + /// + void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe); - /// - /// Adds a keyframe to the layer property without knowing it's type. - /// Prefer . - /// - /// - void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe); + /// + /// Adds a keyframe to the layer property without knowing it's type. + /// Prefer . + /// + /// + void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe); - /// - /// Occurs when the layer property is disposed - /// - public event EventHandler Disposed; + /// + /// Occurs when the layer property is disposed + /// + public event EventHandler Disposed; - /// - /// Occurs once every frame when the layer property is updated - /// - public event EventHandler? Updated; + /// + /// Occurs once every frame when the layer property is updated + /// + public event EventHandler? Updated; - /// - /// Occurs when the current value of the layer property was updated by some form of input - /// - public event EventHandler? CurrentValueSet; + /// + /// Occurs when the current value of the layer property was updated by some form of input + /// + public event EventHandler? CurrentValueSet; - /// - /// Occurs when the visibility value of the layer property was updated - /// - public event EventHandler? VisibilityChanged; + /// + /// Occurs when the visibility value of the layer property was updated + /// + public event EventHandler? VisibilityChanged; - /// - /// Occurs when keyframes are enabled/disabled - /// - public event EventHandler? KeyframesToggled; + /// + /// Occurs when keyframes are enabled/disabled + /// + public event EventHandler? KeyframesToggled; - /// - /// Occurs when a new keyframe was added to the layer property - /// - public event EventHandler? KeyframeAdded; + /// + /// Occurs when a new keyframe was added to the layer property + /// + public event EventHandler? KeyframeAdded; - /// - /// Occurs when a keyframe was removed from the layer property - /// - public event EventHandler? KeyframeRemoved; - } + /// + /// Occurs when a keyframe was removed from the layer property + /// + public event EventHandler? KeyframeRemoved; } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerPropertyKeyframe.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerPropertyKeyframe.cs index d236e297c..f46a91f80 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerPropertyKeyframe.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerPropertyKeyframe.cs @@ -2,43 +2,42 @@ using System.ComponentModel; using Artemis.Storage.Entities.Profile; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a keyframe on a containing a value and a timestamp +/// +public interface ILayerPropertyKeyframe : INotifyPropertyChanged { /// - /// Represents a keyframe on a containing a value and a timestamp + /// Gets an untyped reference to the layer property of this keyframe /// - public interface ILayerPropertyKeyframe : INotifyPropertyChanged - { - /// - /// Gets an untyped reference to the layer property of this keyframe - /// - ILayerProperty UntypedLayerProperty { get; } + ILayerProperty UntypedLayerProperty { get; } - /// - /// Gets or sets the position of this keyframe in the timeline - /// - TimeSpan Position { get; set; } + /// + /// Gets or sets the position of this keyframe in the timeline + /// + TimeSpan Position { get; set; } - /// - /// Gets or sets the easing function applied on the value of the keyframe - /// - Easings.Functions EasingFunction { get; set; } + /// + /// Gets or sets the easing function applied on the value of the keyframe + /// + Easings.Functions EasingFunction { get; set; } - /// - /// Gets the entity this keyframe uses for persistent storage - /// - KeyframeEntity GetKeyframeEntity(); + /// + /// Gets the entity this keyframe uses for persistent storage + /// + KeyframeEntity GetKeyframeEntity(); - /// - /// Removes the keyframe from the layer property - /// - void Remove(); + /// + /// Removes the keyframe from the layer property + /// + void Remove(); - /// - /// Creates a copy of this keyframe. - /// Note: The copied keyframe is not added to the layer property. - /// - /// The resulting copy - ILayerPropertyKeyframe CreateCopy(); - } + /// + /// Creates a copy of this keyframe. + /// Note: The copied keyframe is not added to the layer property. + /// + /// The resulting copy + ILayerPropertyKeyframe CreateCopy(); } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/IntRange.cs b/src/Artemis.Core/Models/Profile/LayerProperties/IntRange.cs index 8fe97c53d..3b0c2b5a9 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/IntRange.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/IntRange.cs @@ -1,63 +1,62 @@ using System; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a range between two signed integers +/// +public readonly struct IntRange { + private readonly Random _rand; + /// - /// Represents a range between two signed integers + /// Creates a new instance of the class /// - public readonly struct IntRange + /// The start value of the range + /// The end value of the range + public IntRange(int start, int end) { - private readonly Random _rand; - - /// - /// Creates a new instance of the class - /// - /// The start value of the range - /// The end value of the range - public IntRange(int start, int end) - { - Start = start; - End = end; + Start = start; + End = end; - _rand = new Random(); - } + _rand = new Random(); + } - /// - /// Gets the start value of the range - /// - public int Start { get; } + /// + /// Gets the start value of the range + /// + public int Start { get; } - /// - /// Gets the end value of the range - /// - public int End { get; } + /// + /// Gets the end value of the range + /// + public int End { get; } - /// - /// Determines whether the given value is in this range - /// - /// The value to check - /// - /// Whether the value may be equal to or - /// Defaults to - /// - /// - public bool IsInRange(int value, bool inclusive = true) - { - if (inclusive) - return value >= Start && value <= End; - return value > Start && value < End; - } + /// + /// Determines whether the given value is in this range + /// + /// The value to check + /// + /// Whether the value may be equal to or + /// Defaults to + /// + /// + public bool IsInRange(int value, bool inclusive = true) + { + if (inclusive) + return value >= Start && value <= End; + return value > Start && value < End; + } - /// - /// Returns a pseudo-random value between and - /// - /// Whether the value may be equal to - /// The pseudo-random value - public int GetRandomValue(bool inclusive = true) - { - if (inclusive) - return _rand.Next(Start, End + 1); - return _rand.Next(Start + 1, End); - } + /// + /// Returns a pseudo-random value between and + /// + /// Whether the value may be equal to + /// The pseudo-random value + public int GetRandomValue(bool inclusive = true) + { + if (inclusive) + return _rand.Next(Start, End + 1); + return _rand.Next(Start + 1, End); } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index b419263f5..2772c3821 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -5,679 +5,680 @@ using System.Linq; using Artemis.Storage.Entities.Profile; using Newtonsoft.Json; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a property on a layer. Properties are saved in storage and can optionally be modified from the UI. +/// +/// Note: You cannot initialize layer properties yourself. If properly placed and annotated, the Artemis core will +/// initialize these for you. +/// +/// +/// The type of property encapsulated in this layer property +public class LayerProperty : CorePropertyChanged, ILayerProperty { + private bool _disposed; + /// - /// Represents a property on a layer. Properties are saved in storage and can optionally be modified from the UI. - /// - /// Note: You cannot initialize layer properties yourself. If properly placed and annotated, the Artemis core will - /// initialize these for you. - /// + /// Creates a new instance of the class /// - /// The type of property encapsulated in this layer property - public class LayerProperty : CorePropertyChanged, ILayerProperty + protected LayerProperty() { - private bool _disposed; + // These are set right after construction to keep the constructor (and inherited constructs) clean + ProfileElement = null!; + LayerPropertyGroup = null!; + Entity = null!; + PropertyDescription = null!; + DataBinding = null!; + Path = ""; - /// - /// Creates a new instance of the class - /// - protected LayerProperty() + CurrentValue = default!; + DefaultValue = default!; + + // We'll try our best... + // TODO: Consider alternatives + if (typeof(T).IsValueType) + _baseValue = default!; + else if (typeof(T).GetConstructor(Type.EmptyTypes) != null) + _baseValue = Activator.CreateInstance(); + else + _baseValue = default!; + + _keyframes = new List>(); + Keyframes = new ReadOnlyCollection>(_keyframes); + } + + /// + public override string ToString() + { + return $"{Path} - {CurrentValue} ({PropertyType})"; + } + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + _disposed = true; + + DataBinding.Dispose(); + Disposed?.Invoke(this, EventArgs.Empty); + } + + /// + /// Invokes the event + /// + protected virtual void OnUpdated() + { + Updated?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnCurrentValueSet() + { + CurrentValueSet?.Invoke(this, new LayerPropertyEventArgs(this)); + LayerPropertyGroup.OnLayerPropertyOnCurrentValueSet(new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnVisibilityChanged() + { + VisibilityChanged?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnKeyframesToggled() + { + KeyframesToggled?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + /// + protected virtual void OnKeyframeAdded(ILayerPropertyKeyframe keyframe) + { + KeyframeAdded?.Invoke(this, new LayerPropertyKeyframeEventArgs(keyframe)); + } + + /// + /// Invokes the event + /// + /// + protected virtual void OnKeyframeRemoved(ILayerPropertyKeyframe keyframe) + { + KeyframeRemoved?.Invoke(this, new LayerPropertyKeyframeEventArgs(keyframe)); + } + + /// + public PropertyDescriptionAttribute PropertyDescription { get; internal set; } + + /// + public string Path { get; private set; } + + /// + public Type PropertyType => typeof(T); + + /// + public void Update(Timeline timeline) + { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + CurrentValue = BaseValue; + + UpdateKeyframes(timeline); + UpdateDataBinding(); + + // UpdateDataBinding called OnUpdated() + } + + /// + public void UpdateDataBinding() + { + DataBinding.Update(); + DataBinding.Apply(); + + OnUpdated(); + } + + /// + public void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe) + { + if (keyframe is not LayerPropertyKeyframe typedKeyframe) + throw new ArtemisCoreException($"Can't remove a keyframe that is not of type {typeof(T).FullName}."); + + RemoveKeyframe(typedKeyframe); + } + + /// + public void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe) + { + if (keyframe is not LayerPropertyKeyframe typedKeyframe) + throw new ArtemisCoreException($"Can't add a keyframe that is not of type {typeof(T).FullName}."); + + AddKeyframe(typedKeyframe); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public event EventHandler? Disposed; + + /// + public event EventHandler? Updated; + + /// + public event EventHandler? CurrentValueSet; + + /// + public event EventHandler? VisibilityChanged; + + /// + public event EventHandler? KeyframesToggled; + + /// + public event EventHandler? KeyframeAdded; + + /// + public event EventHandler? KeyframeRemoved; + + #region Hierarchy + + private bool _isHidden; + + /// + public bool IsHidden + { + get => _isHidden; + set { - // These are set right after construction to keep the constructor (and inherited constructs) clean - ProfileElement = null!; - LayerPropertyGroup = null!; - Entity = null!; - PropertyDescription = null!; - DataBinding = null!; - Path = ""; - - CurrentValue = default!; - DefaultValue = default!; - - // We'll try our best... - // TODO: Consider alternatives - if (typeof(T).IsValueType) - _baseValue = default!; - else if (typeof(T).GetConstructor(Type.EmptyTypes) != null) - _baseValue = Activator.CreateInstance(); - else - _baseValue = default!; - - _keyframes = new List>(); - Keyframes = new ReadOnlyCollection>(_keyframes); + _isHidden = value; + OnVisibilityChanged(); } + } - /// - public override string ToString() + /// + public RenderProfileElement ProfileElement { get; private set; } + + /// + public LayerPropertyGroup LayerPropertyGroup { get; private set; } + + #endregion + + #region Value management + + private T _baseValue; + + /// + /// Called every update (if keyframes are both supported and enabled) to determine the new + /// based on the provided progress + /// + /// The linear current keyframe progress + /// The current keyframe progress, eased with the current easing function + protected virtual void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) + { + throw new NotImplementedException(); + } + + /// + /// Gets or sets the base value of this layer property without any keyframes applied + /// + public T BaseValue + { + get => _baseValue; + set { - return $"{Path} - {CurrentValue} ({PropertyType})"; - } + if (Equals(_baseValue, value)) + return; - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - _disposed = true; - - DataBinding.Dispose(); - Disposed?.Invoke(this, EventArgs.Empty); - } - - /// - public PropertyDescriptionAttribute PropertyDescription { get; internal set; } - - /// - public string Path { get; private set; } - - /// - public Type PropertyType => typeof(T); - - /// - public void Update(Timeline timeline) - { - if (_disposed) - throw new ObjectDisposedException("LayerProperty"); - - CurrentValue = BaseValue; - - UpdateKeyframes(timeline); - UpdateDataBinding(); - - // UpdateDataBinding called OnUpdated() - } - - /// - public void UpdateDataBinding() - { - DataBinding.Update(); - DataBinding.Apply(); - - OnUpdated(); - } - - /// - public void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe) - { - if (keyframe is not LayerPropertyKeyframe typedKeyframe) - throw new ArtemisCoreException($"Can't remove a keyframe that is not of type {typeof(T).FullName}."); - - RemoveKeyframe(typedKeyframe); - } - - /// - public void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe) - { - if (keyframe is not LayerPropertyKeyframe typedKeyframe) - throw new ArtemisCoreException($"Can't add a keyframe that is not of type {typeof(T).FullName}."); - - AddKeyframe(typedKeyframe); - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #region Hierarchy - - private bool _isHidden; - - /// - public bool IsHidden - { - get => _isHidden; - set - { - _isHidden = value; - OnVisibilityChanged(); - } - } - - /// - public RenderProfileElement ProfileElement { get; private set; } - - /// - public LayerPropertyGroup LayerPropertyGroup { get; private set; } - - #endregion - - #region Value management - - private T _baseValue; - - /// - /// Called every update (if keyframes are both supported and enabled) to determine the new - /// based on the provided progress - /// - /// The linear current keyframe progress - /// The current keyframe progress, eased with the current easing function - protected virtual void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) - { - throw new NotImplementedException(); - } - - /// - /// Gets or sets the base value of this layer property without any keyframes applied - /// - public T BaseValue - { - get => _baseValue; - set - { - if (Equals(_baseValue, value)) - return; - - _baseValue = value; - ReapplyUpdate(); - OnPropertyChanged(nameof(BaseValue)); - } - } - - /// - /// Gets the current value of this property as it is affected by it's keyframes, updated once every frame - /// - public T CurrentValue { get; set; } - - /// - /// Gets or sets the default value of this layer property. If set, this value is automatically applied if the property - /// has no value in storage - /// - public T DefaultValue { get; set; } - - /// - /// Sets the current value, using either keyframes if enabled or the base value. - /// - /// The value to set. - /// - /// An optional time to set the value add, if provided and property is using keyframes the value will be set to an new - /// or existing keyframe. - /// - /// The keyframe if one was created or updated. - public LayerPropertyKeyframe? SetCurrentValue(T value, TimeSpan? time = null) - { - if (_disposed) - throw new ObjectDisposedException("LayerProperty"); - - LayerPropertyKeyframe? keyframe = null; - if (time == null || !KeyframesEnabled || !KeyframesSupported) - BaseValue = value; - else - { - // If on a keyframe, update the keyframe - keyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value); - // Create a new keyframe if none found - if (keyframe == null) - { - keyframe = new LayerPropertyKeyframe(value, time.Value, Easings.Functions.Linear, this); - AddKeyframe(keyframe); - } - else - keyframe.Value = value; - } - - // Force an update so that the base value is applied to the current value and - // keyframes/data bindings are applied using the new base value + _baseValue = value; ReapplyUpdate(); + OnPropertyChanged(nameof(BaseValue)); + } + } + + /// + /// Gets the current value of this property as it is affected by it's keyframes, updated once every frame + /// + public T CurrentValue { get; set; } + + /// + /// Gets or sets the default value of this layer property. If set, this value is automatically applied if the property + /// has no value in storage + /// + public T DefaultValue { get; set; } + + /// + /// Sets the current value, using either keyframes if enabled or the base value. + /// + /// The value to set. + /// + /// An optional time to set the value add, if provided and property is using keyframes the value will be set to an new + /// or existing keyframe. + /// + /// The keyframe if one was created or updated. + public LayerPropertyKeyframe? SetCurrentValue(T value, TimeSpan? time = null) + { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + LayerPropertyKeyframe? keyframe = null; + if (time == null || !KeyframesEnabled || !KeyframesSupported) + { + BaseValue = value; + } + else + { + // If on a keyframe, update the keyframe + keyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value); + // Create a new keyframe if none found + if (keyframe == null) + { + keyframe = new LayerPropertyKeyframe(value, time.Value, Easings.Functions.Linear, this); + AddKeyframe(keyframe); + } + else + { + keyframe.Value = value; + } + } + + // Force an update so that the base value is applied to the current value and + // keyframes/data bindings are applied using the new base value + ReapplyUpdate(); + return keyframe; + } + + /// + public void ApplyDefaultValue() + { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + if (DefaultValue == null) + return; + + KeyframesEnabled = false; + + // For value types there's no need to make a copy + if (DefaultValue.GetType().IsValueType) + { + SetCurrentValue(DefaultValue); + } + // Reference types make a deep clone (ab)using JSON + else + { + string json = CoreJson.SerializeObject(DefaultValue, true); + SetCurrentValue(CoreJson.DeserializeObject(json)!); + } + } + + internal void ReapplyUpdate() + { + // Create a timeline with the same position but a delta of zero + Timeline temporaryTimeline = new(); + temporaryTimeline.Override(ProfileElement.Timeline.Position, false); + temporaryTimeline.ClearDelta(); + + Update(temporaryTimeline); + OnCurrentValueSet(); + } + + #endregion + + #region Keyframes + + private bool _keyframesEnabled; + private readonly List> _keyframes; + + /// + /// Gets whether keyframes are supported on this type of property + /// + public bool KeyframesSupported { get; protected set; } = true; + + /// + /// Gets or sets whether keyframes are enabled on this property, has no effect if is + /// False + /// + public bool KeyframesEnabled + { + get => _keyframesEnabled; + set + { + if (_keyframesEnabled == value) return; + _keyframesEnabled = value; + ReapplyUpdate(); + OnKeyframesToggled(); + OnPropertyChanged(nameof(KeyframesEnabled)); + } + } + + + /// + /// Gets a read-only list of all the keyframes on this layer property + /// + public ReadOnlyCollection> Keyframes { get; } + + /// + public ReadOnlyCollection UntypedKeyframes => new(Keyframes.Cast().ToList()); + + /// + /// Gets the current keyframe in the timeline according to the current progress + /// + public LayerPropertyKeyframe? CurrentKeyframe { get; protected set; } + + /// + /// Gets the next keyframe in the timeline according to the current progress + /// + public LayerPropertyKeyframe? NextKeyframe { get; protected set; } + + /// + /// Adds a keyframe to the layer property + /// + /// The keyframe to add + public void AddKeyframe(LayerPropertyKeyframe keyframe) + { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + if (_keyframes.Contains(keyframe)) + return; + + keyframe.LayerProperty?.RemoveKeyframe(keyframe); + keyframe.LayerProperty = this; + _keyframes.Add(keyframe); + + if (!KeyframesEnabled) + KeyframesEnabled = true; + + SortKeyframes(); + ReapplyUpdate(); + OnKeyframeAdded(keyframe); + } + + /// + public ILayerPropertyKeyframe? CreateKeyframeFromEntity(KeyframeEntity keyframeEntity) + { + if (keyframeEntity.Position > ProfileElement.Timeline.Length) + return null; + + try + { + T? value = CoreJson.DeserializeObject(keyframeEntity.Value); + if (value == null) + return null; + + LayerPropertyKeyframe keyframe = new(value, keyframeEntity.Position, (Easings.Functions) keyframeEntity.EasingFunction, this); return keyframe; } - - /// - public void ApplyDefaultValue() + catch (JsonException) { - if (_disposed) - throw new ObjectDisposedException("LayerProperty"); + return null; + } + } - if (DefaultValue == null) - return; + /// + /// Removes a keyframe from the layer property + /// + /// The keyframe to remove + public void RemoveKeyframe(LayerPropertyKeyframe keyframe) + { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); - KeyframesEnabled = false; - - // For value types there's no need to make a copy - if (DefaultValue.GetType().IsValueType) - SetCurrentValue(DefaultValue); - // Reference types make a deep clone (ab)using JSON - else - { - string json = CoreJson.SerializeObject(DefaultValue, true); - SetCurrentValue(CoreJson.DeserializeObject(json)!); - } + if (!_keyframes.Contains(keyframe)) + return; + + _keyframes.Remove(keyframe); + + SortKeyframes(); + ReapplyUpdate(); + OnKeyframeRemoved(keyframe); + } + + /// + /// Sorts the keyframes in ascending order by position + /// + internal void SortKeyframes() + { + _keyframes.Sort((a, b) => a.Position.CompareTo(b.Position)); + } + + private void UpdateKeyframes(Timeline timeline) + { + if (!KeyframesSupported || !KeyframesEnabled) + return; + + // The current keyframe is the last keyframe before the current time + CurrentKeyframe = _keyframes.LastOrDefault(k => k.Position <= timeline.Position); + // Keyframes are sorted by position so we can safely assume the next keyframe's position is after the current + if (CurrentKeyframe != null) + { + int nextIndex = _keyframes.IndexOf(CurrentKeyframe) + 1; + NextKeyframe = _keyframes.Count > nextIndex ? _keyframes[nextIndex] : null; + } + else + { + NextKeyframe = null; } - internal void ReapplyUpdate() + // No need to update the current value if either of the keyframes are null + if (CurrentKeyframe == null) { - // Create a timeline with the same position but a delta of zero - Timeline temporaryTimeline = new(); - temporaryTimeline.Override(ProfileElement.Timeline.Position, false); - temporaryTimeline.ClearDelta(); - - Update(temporaryTimeline); - OnCurrentValueSet(); + CurrentValue = _keyframes.Any() ? _keyframes[0].Value : BaseValue; } - - #endregion - - #region Keyframes - - private bool _keyframesEnabled; - private readonly List> _keyframes; - - /// - /// Gets whether keyframes are supported on this type of property - /// - public bool KeyframesSupported { get; protected set; } = true; - - /// - /// Gets or sets whether keyframes are enabled on this property, has no effect if is - /// False - /// - public bool KeyframesEnabled + else if (NextKeyframe == null) { - get => _keyframesEnabled; - set - { - if (_keyframesEnabled == value) return; - _keyframesEnabled = value; - ReapplyUpdate(); - OnKeyframesToggled(); - OnPropertyChanged(nameof(KeyframesEnabled)); - } + CurrentValue = CurrentKeyframe.Value; } - - - /// - /// Gets a read-only list of all the keyframes on this layer property - /// - public ReadOnlyCollection> Keyframes { get; } - - /// - public ReadOnlyCollection UntypedKeyframes => new(Keyframes.Cast().ToList()); - - /// - /// Gets the current keyframe in the timeline according to the current progress - /// - public LayerPropertyKeyframe? CurrentKeyframe { get; protected set; } - - /// - /// Gets the next keyframe in the timeline according to the current progress - /// - public LayerPropertyKeyframe? NextKeyframe { get; protected set; } - - /// - /// Adds a keyframe to the layer property - /// - /// The keyframe to add - public void AddKeyframe(LayerPropertyKeyframe keyframe) + // Only determine progress and current value if both keyframes are present + else { - if (_disposed) - throw new ObjectDisposedException("LayerProperty"); - - if (_keyframes.Contains(keyframe)) - return; - - keyframe.LayerProperty?.RemoveKeyframe(keyframe); - keyframe.LayerProperty = this; - _keyframes.Add(keyframe); - - if (!KeyframesEnabled) - KeyframesEnabled = true; - - SortKeyframes(); - ReapplyUpdate(); - OnKeyframeAdded(keyframe); + TimeSpan timeDiff = NextKeyframe.Position - CurrentKeyframe.Position; + float keyframeProgress = (float) ((timeline.Position - CurrentKeyframe.Position).TotalMilliseconds / timeDiff.TotalMilliseconds); + float keyframeProgressEased = (float) Easings.Interpolate(keyframeProgress, CurrentKeyframe.EasingFunction); + UpdateCurrentValue(keyframeProgress, keyframeProgressEased); } + } - /// - public ILayerPropertyKeyframe? CreateKeyframeFromEntity(KeyframeEntity keyframeEntity) + #endregion + + #region Data bindings + + /// + /// Gets the data binding of this property + /// + public DataBinding DataBinding { get; private set; } + + /// + public bool DataBindingsSupported => DataBinding.Properties.Any(); + + /// + public IDataBinding BaseDataBinding => DataBinding; + + /// + public bool HasDataBinding => DataBinding.IsEnabled; + + #endregion + + #region Visbility + + /// + /// Set up a condition to hide the provided layer property when the condition evaluates to + /// Note: overrides previous calls to IsHiddenWhen and IsVisibleWhen + /// + /// The type of the target layer property + /// The target layer property + /// The condition to evaluate to determine whether to hide the current layer property + public void IsHiddenWhen(TP layerProperty, Func condition) where TP : ILayerProperty + { + IsHiddenWhen(layerProperty, condition, false); + } + + /// + /// Set up a condition to show the provided layer property when the condition evaluates to + /// Note: overrides previous calls to IsHiddenWhen and IsVisibleWhen + /// + /// The type of the target layer property + /// The target layer property + /// The condition to evaluate to determine whether to hide the current layer property + public void IsVisibleWhen(TP layerProperty, Func condition) where TP : ILayerProperty + { + IsHiddenWhen(layerProperty, condition, true); + } + + private void IsHiddenWhen(TP layerProperty, Func condition, bool inverse) where TP : ILayerProperty + { + layerProperty.VisibilityChanged += LayerPropertyChanged; + layerProperty.CurrentValueSet += LayerPropertyChanged; + layerProperty.Disposed += LayerPropertyOnDisposed; + + void LayerPropertyChanged(object? sender, LayerPropertyEventArgs e) { - if (keyframeEntity.Position > ProfileElement.Timeline.Length) - return null; - - try - { - T? value = CoreJson.DeserializeObject(keyframeEntity.Value); - if (value == null) - return null; - - LayerPropertyKeyframe keyframe = new(value, keyframeEntity.Position, (Easings.Functions) keyframeEntity.EasingFunction, this); - return keyframe; - } - catch (JsonException) - { - return null; - } - } - - /// - /// Removes a keyframe from the layer property - /// - /// The keyframe to remove - public void RemoveKeyframe(LayerPropertyKeyframe keyframe) - { - if (_disposed) - throw new ObjectDisposedException("LayerProperty"); - - if (!_keyframes.Contains(keyframe)) - return; - - _keyframes.Remove(keyframe); - - SortKeyframes(); - ReapplyUpdate(); - OnKeyframeRemoved(keyframe); - } - - /// - /// Sorts the keyframes in ascending order by position - /// - internal void SortKeyframes() - { - _keyframes.Sort((a, b) => a.Position.CompareTo(b.Position)); - } - - private void UpdateKeyframes(Timeline timeline) - { - if (!KeyframesSupported || !KeyframesEnabled) - return; - - // The current keyframe is the last keyframe before the current time - CurrentKeyframe = _keyframes.LastOrDefault(k => k.Position <= timeline.Position); - // Keyframes are sorted by position so we can safely assume the next keyframe's position is after the current - if (CurrentKeyframe != null) - { - int nextIndex = _keyframes.IndexOf(CurrentKeyframe) + 1; - NextKeyframe = _keyframes.Count > nextIndex ? _keyframes[nextIndex] : null; - } - else - { - NextKeyframe = null; - } - - // No need to update the current value if either of the keyframes are null - if (CurrentKeyframe == null) - { - CurrentValue = _keyframes.Any() ? _keyframes[0].Value : BaseValue; - } - else if (NextKeyframe == null) - { - CurrentValue = CurrentKeyframe.Value; - } - // Only determine progress and current value if both keyframes are present - else - { - TimeSpan timeDiff = NextKeyframe.Position - CurrentKeyframe.Position; - float keyframeProgress = (float) ((timeline.Position - CurrentKeyframe.Position).TotalMilliseconds / timeDiff.TotalMilliseconds); - float keyframeProgressEased = (float) Easings.Interpolate(keyframeProgress, CurrentKeyframe.EasingFunction); - UpdateCurrentValue(keyframeProgress, keyframeProgressEased); - } - } - - #endregion - - #region Data bindings - - /// - /// Gets the data binding of this property - /// - public DataBinding DataBinding { get; private set; } - - /// - public bool DataBindingsSupported => DataBinding.Properties.Any(); - - /// - public IDataBinding BaseDataBinding => DataBinding; - - /// - public bool HasDataBinding => DataBinding.IsEnabled; - - #endregion - - #region Visbility - - /// - /// Set up a condition to hide the provided layer property when the condition evaluates to - /// Note: overrides previous calls to IsHiddenWhen and IsVisibleWhen - /// - /// The type of the target layer property - /// The target layer property - /// The condition to evaluate to determine whether to hide the current layer property - public void IsHiddenWhen(TP layerProperty, Func condition) where TP : ILayerProperty - { - IsHiddenWhen(layerProperty, condition, false); - } - - /// - /// Set up a condition to show the provided layer property when the condition evaluates to - /// Note: overrides previous calls to IsHiddenWhen and IsVisibleWhen - /// - /// The type of the target layer property - /// The target layer property - /// The condition to evaluate to determine whether to hide the current layer property - public void IsVisibleWhen(TP layerProperty, Func condition) where TP : ILayerProperty - { - IsHiddenWhen(layerProperty, condition, true); - } - - private void IsHiddenWhen(TP layerProperty, Func condition, bool inverse) where TP : ILayerProperty - { - layerProperty.VisibilityChanged += LayerPropertyChanged; - layerProperty.CurrentValueSet += LayerPropertyChanged; - layerProperty.Disposed += LayerPropertyOnDisposed; - - void LayerPropertyChanged(object? sender, LayerPropertyEventArgs e) - { - if (inverse) - IsHidden = !condition(layerProperty); - else - IsHidden = condition(layerProperty); - } - - void LayerPropertyOnDisposed(object? sender, EventArgs e) - { - layerProperty.VisibilityChanged -= LayerPropertyChanged; - layerProperty.CurrentValueSet -= LayerPropertyChanged; - layerProperty.Disposed -= LayerPropertyOnDisposed; - } - if (inverse) IsHidden = !condition(layerProperty); else IsHidden = condition(layerProperty); } - #endregion - - #region Storage - - private bool _isInitialized; - - /// - /// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied - /// - public bool IsLoadedFromStorage { get; internal set; } - - internal PropertyEntity Entity { get; set; } - - /// - public void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description) + void LayerPropertyOnDisposed(object? sender, EventArgs e) { - if (_disposed) - throw new ObjectDisposedException("LayerProperty"); - - if (description.Identifier == null) - throw new ArtemisCoreException("Can't initialize a property group without an identifier"); - - _isInitialized = true; - - ProfileElement = profileElement ?? throw new ArgumentNullException(nameof(profileElement)); - LayerPropertyGroup = group ?? throw new ArgumentNullException(nameof(group)); - Entity = entity ?? throw new ArgumentNullException(nameof(entity)); - PropertyDescription = description ?? throw new ArgumentNullException(nameof(description)); - IsLoadedFromStorage = fromStorage; - DataBinding = Entity.DataBinding != null ? new DataBinding(this, Entity.DataBinding) : new DataBinding(this); - - if (PropertyDescription.DisableKeyframes) - KeyframesSupported = false; - - // Create the path to this property by walking up the tree - Path = LayerPropertyGroup.Path + "." + description.Identifier; - - OnInitialize(); + layerProperty.VisibilityChanged -= LayerPropertyChanged; + layerProperty.CurrentValueSet -= LayerPropertyChanged; + layerProperty.Disposed -= LayerPropertyOnDisposed; } - /// - public void Load() - { - if (_disposed) - throw new ObjectDisposedException("LayerProperty"); + if (inverse) + IsHidden = !condition(layerProperty); + else + IsHidden = condition(layerProperty); + } - if (!_isInitialized) - throw new ArtemisCoreException("Layer property is not yet initialized"); + #endregion - if (!IsLoadedFromStorage) - ApplyDefaultValue(); - else - try - { - if (Entity.Value != null) - BaseValue = CoreJson.DeserializeObject(Entity.Value)!; - } - catch (JsonException) - { - // ignored for now - } + #region Storage - CurrentValue = BaseValue; - KeyframesEnabled = Entity.KeyframesEnabled; + private bool _isInitialized; - _keyframes.Clear(); + /// + /// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied + /// + public bool IsLoadedFromStorage { get; internal set; } + + internal PropertyEntity Entity { get; set; } + + /// + public void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description) + { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + if (description.Identifier == null) + throw new ArtemisCoreException("Can't initialize a property group without an identifier"); + + _isInitialized = true; + + ProfileElement = profileElement ?? throw new ArgumentNullException(nameof(profileElement)); + LayerPropertyGroup = group ?? throw new ArgumentNullException(nameof(group)); + Entity = entity ?? throw new ArgumentNullException(nameof(entity)); + PropertyDescription = description ?? throw new ArgumentNullException(nameof(description)); + IsLoadedFromStorage = fromStorage; + DataBinding = Entity.DataBinding != null ? new DataBinding(this, Entity.DataBinding) : new DataBinding(this); + + if (PropertyDescription.DisableKeyframes) + KeyframesSupported = false; + + // Create the path to this property by walking up the tree + Path = LayerPropertyGroup.Path + "." + description.Identifier; + + OnInitialize(); + } + + /// + public void Load() + { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + if (!_isInitialized) + throw new ArtemisCoreException("Layer property is not yet initialized"); + + if (!IsLoadedFromStorage) + ApplyDefaultValue(); + else try { - foreach (KeyframeEntity keyframeEntity in Entity.KeyframeEntities.Where(k => k.Position <= ProfileElement.Timeline.Length)) - { - LayerPropertyKeyframe? keyframe = CreateKeyframeFromEntity(keyframeEntity) as LayerPropertyKeyframe; - if (keyframe != null) - AddKeyframe(keyframe); - } + if (Entity.Value != null) + BaseValue = CoreJson.DeserializeObject(Entity.Value)!; } catch (JsonException) { // ignored for now } - DataBinding.Load(); - } + CurrentValue = BaseValue; + KeyframesEnabled = Entity.KeyframesEnabled; - /// - /// Saves the property to the underlying property entity - /// - public void Save() + _keyframes.Clear(); + try { - if (_disposed) - throw new ObjectDisposedException("LayerProperty"); - - if (!_isInitialized) - throw new ArtemisCoreException("Layer property is not yet initialized"); - - Entity.Value = CoreJson.SerializeObject(BaseValue); - Entity.KeyframesEnabled = KeyframesEnabled; - Entity.KeyframeEntities.Clear(); - Entity.KeyframeEntities.AddRange(Keyframes.Select(k => k.GetKeyframeEntity())); - - DataBinding.Save(); - Entity.DataBinding = DataBinding.Entity; + foreach (KeyframeEntity keyframeEntity in Entity.KeyframeEntities.Where(k => k.Position <= ProfileElement.Timeline.Length)) + { + LayerPropertyKeyframe? keyframe = CreateKeyframeFromEntity(keyframeEntity) as LayerPropertyKeyframe; + if (keyframe != null) + AddKeyframe(keyframe); + } } - - /// - /// Called when the layer property has been initialized - /// - protected virtual void OnInitialize() + catch (JsonException) { + // ignored for now } - #endregion - - #region Events - - /// - public event EventHandler? Disposed; - - /// - public event EventHandler? Updated; - - /// - public event EventHandler? CurrentValueSet; - - /// - public event EventHandler? VisibilityChanged; - - /// - public event EventHandler? KeyframesToggled; - - /// - public event EventHandler? KeyframeAdded; - - /// - public event EventHandler? KeyframeRemoved; - - /// - /// Invokes the event - /// - protected virtual void OnUpdated() - { - Updated?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnCurrentValueSet() - { - CurrentValueSet?.Invoke(this, new LayerPropertyEventArgs(this)); - LayerPropertyGroup.OnLayerPropertyOnCurrentValueSet(new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnVisibilityChanged() - { - VisibilityChanged?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnKeyframesToggled() - { - KeyframesToggled?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - /// - protected virtual void OnKeyframeAdded(ILayerPropertyKeyframe keyframe) - { - KeyframeAdded?.Invoke(this, new LayerPropertyKeyframeEventArgs(keyframe)); - } - - /// - /// Invokes the event - /// - /// - protected virtual void OnKeyframeRemoved(ILayerPropertyKeyframe keyframe) - { - KeyframeRemoved?.Invoke(this, new LayerPropertyKeyframeEventArgs(keyframe)); - } - - #endregion + DataBinding.Load(); } + + /// + /// Saves the property to the underlying property entity + /// + public void Save() + { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + if (!_isInitialized) + throw new ArtemisCoreException("Layer property is not yet initialized"); + + Entity.Value = CoreJson.SerializeObject(BaseValue); + Entity.KeyframesEnabled = KeyframesEnabled; + Entity.KeyframeEntities.Clear(); + Entity.KeyframeEntities.AddRange(Keyframes.Select(k => k.GetKeyframeEntity())); + + DataBinding.Save(); + Entity.DataBinding = DataBinding.Entity; + } + + /// + /// Called when the layer property has been initialized + /// + protected virtual void OnInitialize() + { + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs index 4a889f613..b6f37c9ea 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs @@ -1,90 +1,89 @@ using System; using Artemis.Storage.Entities.Profile; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a keyframe on a containing a value and a timestamp +/// +public class LayerPropertyKeyframe : CorePropertyChanged, ILayerPropertyKeyframe { + private LayerProperty _layerProperty; + private TimeSpan _position; + private T _value; + /// - /// Represents a keyframe on a containing a value and a timestamp + /// Creates a new instance of the class /// - public class LayerPropertyKeyframe : CorePropertyChanged, ILayerPropertyKeyframe + /// The value of the keyframe + /// The position of this keyframe in the timeline + /// The easing function applied on the value of the keyframe + /// The layer property this keyframe is applied to + public LayerPropertyKeyframe(T value, TimeSpan position, Easings.Functions easingFunction, LayerProperty layerProperty) { - private LayerProperty _layerProperty; - private TimeSpan _position; - private T _value; + _position = position; + _layerProperty = layerProperty; + _value = value; - /// - /// Creates a new instance of the class - /// - /// The value of the keyframe - /// The position of this keyframe in the timeline - /// The easing function applied on the value of the keyframe - /// The layer property this keyframe is applied to - public LayerPropertyKeyframe(T value, TimeSpan position, Easings.Functions easingFunction, LayerProperty layerProperty) + EasingFunction = easingFunction; + } + + /// + /// The layer property this keyframe is applied to + /// + public LayerProperty LayerProperty + { + get => _layerProperty; + internal set => SetAndNotify(ref _layerProperty, value); + } + + /// + /// The value of this keyframe + /// + public T Value + { + get => _value; + set => SetAndNotify(ref _value, value); + } + + /// + public ILayerProperty UntypedLayerProperty => LayerProperty; + + /// + public TimeSpan Position + { + get => _position; + set { - _position = position; - _layerProperty = layerProperty; - _value = value; - - EasingFunction = easingFunction; - } - - /// - /// The layer property this keyframe is applied to - /// - public LayerProperty LayerProperty - { - get => _layerProperty; - internal set => SetAndNotify(ref _layerProperty, value); - } - - /// - /// The value of this keyframe - /// - public T Value - { - get => _value; - set => SetAndNotify(ref _value, value); - } - - /// - public ILayerProperty UntypedLayerProperty => LayerProperty; - - /// - public TimeSpan Position - { - get => _position; - set - { - SetAndNotify(ref _position, value); - LayerProperty.SortKeyframes(); - LayerProperty.ReapplyUpdate(); - } - } - - /// - public Easings.Functions EasingFunction { get; set; } - - /// - public KeyframeEntity GetKeyframeEntity() - { - return new KeyframeEntity - { - Value = CoreJson.SerializeObject(Value), - Position = Position, - EasingFunction = (int) EasingFunction - }; - } - - /// - public void Remove() - { - LayerProperty.RemoveKeyframe(this); - } - - /// - public ILayerPropertyKeyframe CreateCopy() - { - return new LayerPropertyKeyframe(Value, Position, EasingFunction, LayerProperty); + SetAndNotify(ref _position, value); + LayerProperty.SortKeyframes(); + LayerProperty.ReapplyUpdate(); } } + + /// + public Easings.Functions EasingFunction { get; set; } + + /// + public KeyframeEntity GetKeyframeEntity() + { + return new KeyframeEntity + { + Value = CoreJson.SerializeObject(Value), + Position = Position, + EasingFunction = (int) EasingFunction + }; + } + + /// + public void Remove() + { + LayerProperty.RemoveKeyframe(this); + } + + /// + public ILayerPropertyKeyframe CreateCopy() + { + return new LayerPropertyKeyframe(Value, Position, EasingFunction, LayerProperty); + } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyPreview.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyPreview.cs index 906c5058b..767d392d5 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyPreview.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyPreview.cs @@ -79,7 +79,8 @@ public sealed class LayerPropertyPreview : IDisposable } Property.SetCurrentValue(OriginalValue, Time); - return !Equals(OriginalValue, PreviewValue); ; + return !Equals(OriginalValue, PreviewValue); + ; } /// diff --git a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs index 637a1ea6e..591dd4faa 100644 --- a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs +++ b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs @@ -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; + +/// +/// Represents a property group on a layer +/// +/// Note: You cannot initialize property groups yourself. If properly placed and annotated, the Artemis core will +/// initialize these for you. +/// +/// +public abstract class LayerPropertyGroup : IDisposable { + private readonly List _layerProperties; + private readonly List _layerPropertyGroups; + private bool _disposed; + private bool _isHidden; + /// - /// Represents a property group on a layer - /// - /// Note: You cannot initialize property groups yourself. If properly placed and annotated, the Artemis core will - /// initialize these for you. - /// + /// A base constructor for a /// - public abstract class LayerPropertyGroup : IDisposable + protected LayerPropertyGroup() { - private readonly List _layerProperties; - private readonly List _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 = ""; - /// - /// A base constructor for a - /// - protected LayerPropertyGroup() + _layerProperties = new List(); + _layerPropertyGroups = new List(); + + LayerProperties = new ReadOnlyCollection(_layerProperties); + LayerPropertyGroups = new ReadOnlyCollection(_layerPropertyGroups); + } + + /// + /// Gets the profile element (such as layer or folder) this group is associated with + /// + public RenderProfileElement ProfileElement { get; private set; } + + /// + /// Gets the description of this group + /// + public PropertyGroupDescriptionAttribute GroupDescription { get; private set; } + + /// + /// The parent group of this group + /// + [LayerPropertyIgnore] // Ignore the parent when selecting child groups + public LayerPropertyGroup? Parent { get; internal set; } + + /// + /// Gets the unique path of the property on the render element + /// + public string Path { get; private set; } + + /// + /// Gets whether this property groups properties are all initialized + /// + public bool PropertiesInitialized { get; private set; } + + /// + /// Gets or sets whether the property is hidden in the UI + /// + 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(); - _layerPropertyGroups = new List(); - - LayerProperties = new ReadOnlyCollection(_layerProperties); - LayerPropertyGroups = new ReadOnlyCollection(_layerPropertyGroups); - } - - /// - /// Gets the profile element (such as layer or folder) this group is associated with - /// - public RenderProfileElement ProfileElement { get; private set; } - - /// - /// Gets the description of this group - /// - public PropertyGroupDescriptionAttribute GroupDescription { get; private set; } - - /// - /// The parent group of this group - /// - [LayerPropertyIgnore] // Ignore the parent when selecting child groups - public LayerPropertyGroup? Parent { get; internal set; } - - /// - /// Gets the unique path of the property on the render element - /// - public string Path { get; private set; } - - /// - /// Gets whether this property groups properties are all initialized - /// - public bool PropertiesInitialized { get; private set; } - - /// - /// Gets or sets whether the property is hidden in the UI - /// - public bool IsHidden - { - get => _isHidden; - set - { - _isHidden = value; - OnVisibilityChanged(); - } - } - - /// - /// Gets the entity this property group uses for persistent storage - /// - public PropertyGroupEntity? PropertyGroupEntity { get; internal set; } - - /// - /// A list of all layer properties in this group - /// - public ReadOnlyCollection LayerProperties { get; } - - /// - /// A list of al child groups in this group - /// - public ReadOnlyCollection LayerPropertyGroups { get; } - - /// - /// Recursively gets all layer properties on this group and any subgroups - /// - public IReadOnlyCollection GetAllLayerProperties() - { - if (_disposed) - throw new ObjectDisposedException("LayerPropertyGroup"); - - if (!PropertiesInitialized) - return new List(); - - List result = new(LayerProperties); - foreach (LayerPropertyGroup layerPropertyGroup in LayerPropertyGroups) - result.AddRange(layerPropertyGroup.GetAllLayerProperties()); - - return result.AsReadOnly(); - } - - /// - /// Applies the default value to all layer properties - /// - public void ResetAllLayerProperties() - { - foreach (ILayerProperty layerProperty in GetAllLayerProperties()) - layerProperty.ApplyDefaultValue(); - } - - /// - /// Occurs when the property group has initialized all its children - /// - public event EventHandler? PropertyGroupInitialized; - - /// - /// Occurs when one of the current value of one of the layer properties in this group changes by some form of input - /// Note: Will not trigger on properties in child groups - /// - public event EventHandler? LayerPropertyOnCurrentValueSet; - - /// - /// Occurs when the value of the layer property was updated - /// - public event EventHandler? VisibilityChanged; - - /// - /// Called before property group is activated to allow you to populate on - /// the properties you want - /// - protected abstract void PopulateDefaults(); - - /// - /// Called when the property group is activated - /// - protected abstract void EnableProperties(); - - /// - /// Called when the property group is deactivated (either the profile unloaded or the related brush/effect was removed) - /// - protected abstract void DisableProperties(); - - /// - /// Called when the property group and all its layer properties have been initialized - /// - protected virtual void OnPropertyGroupInitialized() - { - PropertyGroupInitialized?.Invoke(this, EventArgs.Empty); - } - - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - 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}; - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); + _isHidden = value; + OnVisibilityChanged(); } } + + /// + /// Gets the entity this property group uses for persistent storage + /// + public PropertyGroupEntity? PropertyGroupEntity { get; internal set; } + + /// + /// A list of all layer properties in this group + /// + public ReadOnlyCollection LayerProperties { get; } + + /// + /// A list of al child groups in this group + /// + public ReadOnlyCollection LayerPropertyGroups { get; } + + /// + /// Recursively gets all layer properties on this group and any subgroups + /// + public IReadOnlyCollection GetAllLayerProperties() + { + if (_disposed) + throw new ObjectDisposedException("LayerPropertyGroup"); + + if (!PropertiesInitialized) + return new List(); + + List result = new(LayerProperties); + foreach (LayerPropertyGroup layerPropertyGroup in LayerPropertyGroups) + result.AddRange(layerPropertyGroup.GetAllLayerProperties()); + + return result.AsReadOnly(); + } + + /// + /// Applies the default value to all layer properties + /// + public void ResetAllLayerProperties() + { + foreach (ILayerProperty layerProperty in GetAllLayerProperties()) + layerProperty.ApplyDefaultValue(); + } + + /// + /// Occurs when the property group has initialized all its children + /// + public event EventHandler? PropertyGroupInitialized; + + /// + /// Occurs when one of the current value of one of the layer properties in this group changes by some form of input + /// Note: Will not trigger on properties in child groups + /// + public event EventHandler? LayerPropertyOnCurrentValueSet; + + /// + /// Occurs when the value of the layer property was updated + /// + public event EventHandler? VisibilityChanged; + + /// + /// Called before property group is activated to allow you to populate on + /// the properties you want + /// + protected abstract void PopulateDefaults(); + + /// + /// Called when the property group is activated + /// + protected abstract void EnableProperties(); + + /// + /// Called when the property group is deactivated (either the profile unloaded or the related brush/effect was removed) + /// + protected abstract void DisableProperties(); + + /// + /// Called when the property group and all its layer properties have been initialized + /// + protected virtual void OnPropertyGroupInitialized() + { + PropertyGroupInitialized?.Invoke(this, EventArgs.Empty); + } + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + 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}; + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerShapes/EllipseShape.cs b/src/Artemis.Core/Models/Profile/LayerShapes/EllipseShape.cs index 766837492..7f0d98f39 100644 --- a/src/Artemis.Core/Models/Profile/LayerShapes/EllipseShape.cs +++ b/src/Artemis.Core/Models/Profile/LayerShapes/EllipseShape.cs @@ -1,22 +1,21 @@ using SkiaSharp; -namespace Artemis.Core -{ - /// - /// Represents an ellipse layer shape - /// - public class EllipseShape : LayerShape - { - internal EllipseShape(Layer layer) : base(layer) - { - } +namespace Artemis.Core; - /// - public override void CalculateRenderProperties() - { - SKPath path = new(); - path.AddOval(SKRect.Create(Layer.Bounds.Width, Layer.Bounds.Height)); - Path = path; - } +/// +/// Represents an ellipse layer shape +/// +public class EllipseShape : LayerShape +{ + internal EllipseShape(Layer layer) : base(layer) + { + } + + /// + public override void CalculateRenderProperties() + { + SKPath path = new(); + path.AddOval(SKRect.Create(Layer.Bounds.Width, Layer.Bounds.Height)); + Path = path; } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerShapes/LayerShape.cs b/src/Artemis.Core/Models/Profile/LayerShapes/LayerShape.cs index 6b5f4e825..466db3ec4 100644 --- a/src/Artemis.Core/Models/Profile/LayerShapes/LayerShape.cs +++ b/src/Artemis.Core/Models/Profile/LayerShapes/LayerShape.cs @@ -1,30 +1,29 @@ using SkiaSharp; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents the shape of a layer +/// +public abstract class LayerShape { - /// - /// Represents the shape of a layer - /// - public abstract class LayerShape + internal LayerShape(Layer layer) { - internal LayerShape(Layer layer) - { - Layer = layer; - } - - /// - /// The layer this shape is attached to - /// - public Layer Layer { get; set; } - - /// - /// Gets a the path outlining the shape - /// - public SKPath? Path { get; protected set; } - - /// - /// Calculates the - /// - public abstract void CalculateRenderProperties(); + Layer = layer; } + + /// + /// The layer this shape is attached to + /// + public Layer Layer { get; set; } + + /// + /// Gets a the path outlining the shape + /// + public SKPath? Path { get; protected set; } + + /// + /// Calculates the + /// + public abstract void CalculateRenderProperties(); } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerShapes/RectangleShape.cs b/src/Artemis.Core/Models/Profile/LayerShapes/RectangleShape.cs index 93a2006a1..e41c0f18a 100644 --- a/src/Artemis.Core/Models/Profile/LayerShapes/RectangleShape.cs +++ b/src/Artemis.Core/Models/Profile/LayerShapes/RectangleShape.cs @@ -1,22 +1,21 @@ using SkiaSharp; -namespace Artemis.Core -{ - /// - /// Represents a rectangular layer shape - /// - public class RectangleShape : LayerShape - { - internal RectangleShape(Layer layer) : base(layer) - { - } +namespace Artemis.Core; - /// - public override void CalculateRenderProperties() - { - SKPath path = new(); - path.AddRect(SKRect.Create(Layer.Bounds.Width, Layer.Bounds.Height)); - Path = path; - } +/// +/// Represents a rectangular layer shape +/// +public class RectangleShape : LayerShape +{ + internal RectangleShape(Layer layer) : base(layer) + { + } + + /// + public override void CalculateRenderProperties() + { + SKPath path = new(); + path.AddRect(SKRect.Create(Layer.Bounds.Width, Layer.Bounds.Height)); + Path = path; } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs b/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs index 5e2447c2e..4333f15f6 100644 --- a/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs +++ b/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs @@ -2,60 +2,59 @@ #pragma warning disable 8618 -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents the transform properties of a layer +/// +public class LayerTransformProperties : LayerPropertyGroup { /// - /// Represents the transform properties of a layer + /// The point at which the shape is attached to its position /// - 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; } + + /// + /// The position of the shape + /// + [PropertyDescription(Description = "The position of the shape", InputAffix = "%", InputStepSize = 0.001f)] + public SKPointLayerProperty Position { get; set; } + + /// + /// The scale of the shape + /// + [PropertyDescription(Description = "The scale of the shape", InputAffix = "%", MinInputValue = 0f)] + public SKSizeLayerProperty Scale { get; set; } + + /// + /// The rotation of the shape in degree + /// + [PropertyDescription(Description = "The rotation of the shape in degrees", InputAffix = "°", InputStepSize = 0.5f)] + public FloatLayerProperty Rotation { get; set; } + + /// + /// The opacity of the shape + /// + [PropertyDescription(Description = "The opacity of the shape", InputAffix = "%", MinInputValue = 0f, MaxInputValue = 100f, InputStepSize = 0.1f)] + public FloatLayerProperty Opacity { get; set; } + + /// + protected override void PopulateDefaults() { - /// - /// The point at which the shape is attached to its position - /// - [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; + } - /// - /// The position of the shape - /// - [PropertyDescription(Description = "The position of the shape", InputAffix = "%", InputStepSize = 0.001f)] - public SKPointLayerProperty Position { get; set; } + /// + protected override void EnableProperties() + { + } - /// - /// The scale of the shape - /// - [PropertyDescription(Description = "The scale of the shape", InputAffix = "%", MinInputValue = 0f)] - public SKSizeLayerProperty Scale { get; set; } - - /// - /// The rotation of the shape in degree - /// - [PropertyDescription(Description = "The rotation of the shape in degrees", InputAffix = "°", InputStepSize = 0.5f)] - public FloatLayerProperty Rotation { get; set; } - - /// - /// The opacity of the shape - /// - [PropertyDescription(Description = "The opacity of the shape", InputAffix = "%", MinInputValue = 0f, MaxInputValue = 100f, InputStepSize = 0.1f)] - public FloatLayerProperty Opacity { get; set; } - - /// - 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; - } - - /// - protected override void EnableProperties() - { - } - - /// - protected override void DisableProperties() - { - } + /// + protected override void DisableProperties() + { } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs index eaa4abce4..5534e686d 100644 --- a/src/Artemis.Core/Models/Profile/Profile.cs +++ b/src/Artemis.Core/Models/Profile/Profile.cs @@ -6,301 +6,299 @@ using Artemis.Core.ScriptingProviders; using Artemis.Storage.Entities.Profile; using SkiaSharp; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a profile containing folders and layers +/// +public sealed class Profile : ProfileElement { - /// - /// Represents a profile containing folders and layers - /// - public sealed class Profile : ProfileElement + private readonly object _lock = new(); + private readonly ObservableCollection _scriptConfigurations; + private readonly ObservableCollection _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 _scripts; - private readonly ObservableCollection _scriptConfigurations; + _scripts = new ObservableCollection(); + _scriptConfigurations = new ObservableCollection(); - internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!) - { - _scripts = new ObservableCollection(); - _scriptConfigurations = new ObservableCollection(); + Configuration = configuration; + Profile = this; + ProfileEntity = profileEntity; + EntityId = profileEntity.Id; - Configuration = configuration; - Profile = this; - ProfileEntity = profileEntity; - EntityId = profileEntity.Id; + Exceptions = new List(); + Scripts = new ReadOnlyObservableCollection(_scripts); + ScriptConfigurations = new ReadOnlyObservableCollection(_scriptConfigurations); - Exceptions = new List(); - Scripts = new ReadOnlyObservableCollection(_scripts); - ScriptConfigurations = new ReadOnlyObservableCollection(_scriptConfigurations); + Load(); + } - Load(); - } + /// + /// Gets the profile configuration of this profile + /// + public ProfileConfiguration Configuration { get; } - /// - /// Gets the profile configuration of this profile - /// - public ProfileConfiguration Configuration { get; } + /// + /// Gets a collection of all active scripts assigned to this profile + /// + public ReadOnlyObservableCollection Scripts { get; } - /// - /// Gets a collection of all active scripts assigned to this profile - /// - public ReadOnlyObservableCollection Scripts { get; } + /// + /// Gets a collection of all script configurations assigned to this profile + /// + public ReadOnlyObservableCollection ScriptConfigurations { get; } - /// - /// Gets a collection of all script configurations assigned to this profile - /// - public ReadOnlyObservableCollection ScriptConfigurations { get; } + /// + /// Gets or sets a boolean indicating whether this profile is freshly imported i.e. no changes have been made to it + /// since import + /// + /// Note: As long as this is , profile adaption will be performed on load and any surface + /// changes + /// + /// + public bool IsFreshImport + { + get => _isFreshImport; + set => SetAndNotify(ref _isFreshImport, value); + } - /// - /// Gets or sets a boolean indicating whether this profile is freshly imported i.e. no changes have been made to it - /// since import - /// - /// Note: As long as this is , profile adaption will be performed on load and any surface - /// changes - /// - /// - public bool IsFreshImport - { - get => _isFreshImport; - set => SetAndNotify(ref _isFreshImport, value); - } + /// + /// Gets or sets the last selected profile element of this profile + /// + public ProfileElement? LastSelectedProfileElement + { + get => _lastSelectedProfileElement; + set => SetAndNotify(ref _lastSelectedProfileElement, value); + } - /// - /// Gets or sets the last selected profile element of this profile - /// - public ProfileElement? LastSelectedProfileElement - { - get => _lastSelectedProfileElement; - set => SetAndNotify(ref _lastSelectedProfileElement, value); - } + /// + /// Gets the profile entity this profile uses for persistent storage + /// + public ProfileEntity ProfileEntity { get; internal set; } - /// - /// Gets the profile entity this profile uses for persistent storage - /// - public ProfileEntity ProfileEntity { get; internal set; } + internal List Exceptions { get; } - internal List Exceptions { get; } - - /// - 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); - } - } - - /// - 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 exceptions = new(Exceptions); - Exceptions.Clear(); - throw new AggregateException($"One or more exceptions while rendering profile {Name}", exceptions); - } - } - - /// - public override void Reset() - { - foreach (ProfileElement child in Children) - child.Reset(); - } - - /// - /// Retrieves the root folder of this profile - /// - /// The root folder of the profile - /// - public Folder GetRootFolder() + /// + 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); - /// - public override string ToString() - { - return $"[Profile] {nameof(Name)}: {Name}"; - } + foreach (ProfileElement profileElement in Children) + profileElement.Update(deltaTime); - /// - /// Populates all the LEDs on the elements in this profile - /// - /// The devices to use while populating LEDs - public void PopulateLeds(IEnumerable devices) + foreach (ProfileScript profileScript in Scripts) + profileScript.OnProfileUpdated(deltaTime); + } + } + + /// + 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); - /// - 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 exceptions = new(Exceptions); + Exceptions.Clear(); + throw new AggregateException($"One or more exceptions while rendering profile {Name}", exceptions); + } + } + /// + public override void Reset() + { + foreach (ProfileElement child in Children) + child.Reset(); + } + + /// + /// Retrieves the root folder of this profile + /// + /// The root folder of the profile + /// + public Folder GetRootFolder() + { + if (Disposed) + throw new ObjectDisposedException("Profile"); + + return (Folder) Children.Single(); + } + + /// + public override string ToString() + { + return $"[Profile] {nameof(Name)}: {Name}"; + } + + /// + /// Populates all the LEDs on the elements in this profile + /// + /// The devices to use while populating LEDs + public void PopulateLeds(IEnumerable devices) + { + if (Disposed) + throw new ObjectDisposedException("Profile"); + + foreach (Layer layer in GetAllLayers()) + layer.PopulateLeds(devices); + } + + #region Overrides of BreakableModel + + /// + public override IEnumerable GetBrokenHierarchy() + { + return GetAllRenderElements().SelectMany(folders => folders.GetBrokenHierarchy()); + } + + #endregion + + /// + 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 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)); } - /// - /// Removes a script configuration from the profile, if the configuration has an active script it is also removed. - /// - internal void RemoveScriptConfiguration(ScriptConfiguration scriptConfiguration) + List 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(); + } + + /// + /// Removes a script configuration from the profile, if the configuration has an active script it is also removed. + /// + internal void RemoveScriptConfiguration(ScriptConfiguration scriptConfiguration) + { + if (!_scriptConfigurations.Contains(scriptConfiguration)) + return; + + Script? script = scriptConfiguration.Script; + if (script != null) + RemoveScript((ProfileScript) script); + + _scriptConfigurations.Remove(scriptConfiguration); + } + + /// + /// Adds a script configuration to the profile but does not instantiate it's script. + /// + internal void AddScriptConfiguration(ScriptConfiguration scriptConfiguration) + { + if (!_scriptConfigurations.Contains(scriptConfiguration)) + _scriptConfigurations.Add(scriptConfiguration); + } + + /// + /// Adds a script that has a script configuration belonging to this profile. + /// + 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); + } + + /// + /// Removes a script from the profile and disposes it. + /// + 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); } - - /// - /// Adds a script configuration to the profile but does not instantiate it's script. - /// - internal void AddScriptConfiguration(ScriptConfiguration scriptConfiguration) - { - if (!_scriptConfigurations.Contains(scriptConfiguration)) - _scriptConfigurations.Add(scriptConfiguration); - } - - /// - /// Adds a script that has a script configuration belonging to this profile. - /// - 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); - } - - /// - /// Removes a script from the profile and disposes it. - /// - 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 - - /// - public override IEnumerable GetBrokenHierarchy() - { - return GetAllRenderElements().SelectMany(folders => folders.GetBrokenHierarchy()); - } - - #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/ProfileCategory.cs b/src/Artemis.Core/Models/Profile/ProfileCategory.cs index 6ecba4ae2..3c3358fd7 100644 --- a/src/Artemis.Core/Models/Profile/ProfileCategory.cs +++ b/src/Artemis.Core/Models/Profile/ProfileCategory.cs @@ -3,212 +3,210 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using Artemis.Storage.Entities.Profile; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a category containing +/// +public class ProfileCategory : CorePropertyChanged, IStorageModel +{ + private readonly List _profileConfigurations = new(); + private bool _isCollapsed; + private bool _isSuspended; + private string _name; + private int _order; + + /// + /// Creates a new instance of the class + /// + /// The name of the category + internal ProfileCategory(string name) + { + _name = name; + Entity = new ProfileCategoryEntity(); + ProfileConfigurations = new ReadOnlyCollection(_profileConfigurations); + } + + internal ProfileCategory(ProfileCategoryEntity entity) + { + _name = null!; + Entity = entity; + ProfileConfigurations = new ReadOnlyCollection(_profileConfigurations); + + Load(); + } + + /// + /// Gets or sets the name of the profile category + /// + public string Name + { + get => _name; + set => SetAndNotify(ref _name, value); + } + + /// + /// The order in which this category appears in the update loop and sidebar + /// + public int Order + { + get => _order; + set => SetAndNotify(ref _order, value); + } + + /// + /// Gets or sets a boolean indicating whether the category is collapsed or not + /// Note: Has no implications other than inside the UI + /// + public bool IsCollapsed + { + get => _isCollapsed; + set => SetAndNotify(ref _isCollapsed, value); + } + + /// + /// Gets or sets a boolean indicating whether this category is suspended, disabling all its profiles + /// + public bool IsSuspended + { + get => _isSuspended; + set => SetAndNotify(ref _isSuspended, value); + } + + /// + /// Gets a read only collection of the profiles inside this category + /// + public ReadOnlyCollection ProfileConfigurations { get; } + + /// + /// Gets the unique ID of this category + /// + public Guid EntityId => Entity.Id; + + internal ProfileCategoryEntity Entity { get; } + + + /// + /// Adds a profile configuration to this category + /// + 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)); + } + + /// + public override string ToString() + { + return $"[ProfileCategory] {Order} {nameof(Name)}: {Name}, {nameof(IsSuspended)}: {IsSuspended}"; + } + + /// + /// Occurs when a profile configuration is added to this + /// + public event EventHandler? ProfileConfigurationAdded; + + /// + /// Occurs when a profile configuration is removed from this + /// + public event EventHandler? ProfileConfigurationRemoved; + + /// + /// Invokes the event + /// + protected virtual void OnProfileConfigurationAdded(ProfileConfigurationEventArgs e) + { + ProfileConfigurationAdded?.Invoke(this, e); + } + + /// + /// Invokes the event + /// + 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 + + /// + 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)); + } + + /// + 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 +} + +/// +/// Represents a name of one of the default categories +/// +public enum DefaultCategoryName { /// - /// Represents a category containing + /// The category used by profiles tied to games /// - public class ProfileCategory : CorePropertyChanged, IStorageModel - { - private readonly List _profileConfigurations = new(); - private bool _isCollapsed; - private bool _isSuspended; - private string _name; - private int _order; - - /// - /// Creates a new instance of the class - /// - /// The name of the category - internal ProfileCategory(string name) - { - _name = name; - Entity = new ProfileCategoryEntity(); - ProfileConfigurations = new ReadOnlyCollection(_profileConfigurations); - } - - internal ProfileCategory(ProfileCategoryEntity entity) - { - _name = null!; - Entity = entity; - ProfileConfigurations = new ReadOnlyCollection(_profileConfigurations); - - Load(); - } - - /// - /// Gets or sets the name of the profile category - /// - public string Name - { - get => _name; - set => SetAndNotify(ref _name, value); - } - - /// - /// The order in which this category appears in the update loop and sidebar - /// - public int Order - { - get => _order; - set => SetAndNotify(ref _order, value); - } - - /// - /// Gets or sets a boolean indicating whether the category is collapsed or not - /// Note: Has no implications other than inside the UI - /// - public bool IsCollapsed - { - get => _isCollapsed; - set => SetAndNotify(ref _isCollapsed, value); - } - - /// - /// Gets or sets a boolean indicating whether this category is suspended, disabling all its profiles - /// - public bool IsSuspended - { - get => _isSuspended; - set => SetAndNotify(ref _isSuspended, value); - } - - /// - /// Gets a read only collection of the profiles inside this category - /// - public ReadOnlyCollection ProfileConfigurations { get; } - - /// - /// Gets the unique ID of this category - /// - public Guid EntityId => Entity.Id; - - internal ProfileCategoryEntity Entity { get; } - - - /// - /// Adds a profile configuration to this category - /// - 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)); - } - - /// - 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 - - /// - 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)); - } - - /// - 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 - - /// - /// Occurs when a profile configuration is added to this - /// - public event EventHandler? ProfileConfigurationAdded; - - /// - /// Occurs when a profile configuration is removed from this - /// - public event EventHandler? ProfileConfigurationRemoved; - - /// - /// Invokes the event - /// - protected virtual void OnProfileConfigurationAdded(ProfileConfigurationEventArgs e) - { - ProfileConfigurationAdded?.Invoke(this, e); - } - - /// - /// Invokes the event - /// - protected virtual void OnProfileConfigurationRemoved(ProfileConfigurationEventArgs e) - { - ProfileConfigurationRemoved?.Invoke(this, e); - } - - #endregion - } + Games, /// - /// Represents a name of one of the default categories + /// The category used by profiles tied to applications /// - public enum DefaultCategoryName - { - /// - /// The category used by profiles tied to games - /// - Games, + Applications, - /// - /// The category used by profiles tied to applications - /// - Applications, - - /// - /// The category used by general profiles - /// - General - } + /// + /// The category used by general profiles + /// + General } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/ProfileElement.cs b/src/Artemis.Core/Models/Profile/ProfileElement.cs index 83a131739..dbce946a8 100644 --- a/src/Artemis.Core/Models/Profile/ProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/ProfileElement.cs @@ -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; + +/// +/// Represents an element of a +/// +public abstract class ProfileElement : BreakableModel, IDisposable { - /// - /// Represents an element of a - /// - public abstract class ProfileElement : BreakableModel, IDisposable + internal readonly List 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 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(); - Children = new ReadOnlyCollection(ChildrenList); - } - - /// - /// Gets the unique ID of this profile element - /// - public Guid EntityId - { - get => _entityId; - internal set => SetAndNotify(ref _entityId, value); - } - - /// - /// Gets the profile this element belongs to - /// - public Profile Profile - { - get => _profile; - internal set => SetAndNotify(ref _profile, value); - } - - /// - /// Gets the parent of this element - /// - public ProfileElement? Parent - { - get => _parent; - internal set => SetAndNotify(ref _parent, value); - } - - /// - /// The element's children - /// - public ReadOnlyCollection Children { get; } - - /// - /// The order in which this element appears in the update loop and editor - /// - public int Order - { - get => _order; - internal set => SetAndNotify(ref _order, value); - } - - /// - /// The name which appears in the editor - /// - public string? Name - { - get => _name; - set => SetAndNotify(ref _name, value); - } - - /// - /// Gets or sets the suspended state, if suspended the element is skipped in render and update - /// - public bool Suspended - { - get => _suspended; - set => SetAndNotify(ref _suspended, value); - } - - /// - /// Gets a boolean indicating whether the profile element is disposed - /// - public bool Disposed { get; protected set; } - - #region Overrides of BreakableModel - - /// - public override string BrokenDisplayName => Name ?? GetType().Name; - - #endregion - - /// - /// Updates the element - /// - /// - public abstract void Update(double deltaTime); - - /// - /// Renders the element - /// - /// The canvas to render upon. - /// The base position to use to translate relative positions to absolute positions. - /// An optional element to focus on while rendering (other elements will not render). - public abstract void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus); - - /// - /// Resets the internal state of the element - /// - public abstract void Reset(); - - /// - public override string ToString() - { - return $"{nameof(EntityId)}: {EntityId}, {nameof(Order)}: {Order}, {nameof(Name)}: {Name}"; - } - - /// - /// Occurs when a child was added to the list - /// - public event EventHandler? ChildAdded; - - /// - /// Occurs when a child was removed from the list - /// - public event EventHandler? ChildRemoved; - - /// - /// Occurs when a child was added to the list of this element or any of it's descendents. - /// - public event EventHandler? DescendentAdded; - - /// - /// Occurs when a child was removed from the list of this element or any of it's descendents. - /// - public event EventHandler? DescendentRemoved; - - /// - /// Invokes the event - /// - protected virtual void OnChildAdded(ProfileElement child) - { - ChildAdded?.Invoke(this, new ProfileElementEventArgs(child)); - } - - /// - /// Invokes the event - /// - protected virtual void OnChildRemoved(ProfileElement child) - { - ChildRemoved?.Invoke(this, new ProfileElementEventArgs(child)); - } - - /// - /// Invokes the event - /// - protected virtual void OnDescendentAdded(ProfileElement child) - { - DescendentAdded?.Invoke(this, new ProfileElementEventArgs(child)); - Parent?.OnDescendentAdded(child); - } - - /// - /// Invokes the event - /// - protected virtual void OnDescendentRemoved(ProfileElement child) - { - DescendentRemoved?.Invoke(this, new ProfileElementEventArgs(child)); - Parent?.OnDescendentRemoved(child); - } - - /// - /// Disposes the profile element - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - } - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #region Hierarchy - - /// - /// Adds a profile element to the collection, optionally at the given position (0-based) - /// - /// The profile element to add - /// The order where to place the child (0-based), defaults to the end of the collection - 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); - } - - /// - /// Removes a profile element from the collection - /// - /// The profile element to remove - 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; - } - - /// - /// Returns a flattened list of all child render elements - /// - /// - public List GetAllRenderElements() - { - if (Disposed) - throw new ObjectDisposedException(GetType().Name); - - List elements = new(); - foreach (RenderProfileElement childElement in Children.Where(c => c is RenderProfileElement).Cast()) - { - // Add all folders in this element - elements.Add(childElement); - // Add all folders in folders inside this element - elements.AddRange(childElement.GetAllRenderElements()); - } - - return elements; - } - - /// - /// Returns a flattened list of all child folders - /// - /// - public List GetAllFolders() - { - if (Disposed) - throw new ObjectDisposedException(GetType().Name); - - List folders = new(); - foreach (Folder childFolder in Children.Where(c => c is Folder).Cast()) - { - // Add all folders in this element - folders.Add(childFolder); - // Add all folders in folders inside this element - folders.AddRange(childFolder.GetAllFolders()); - } - - return folders; - } - - /// - /// Returns a flattened list of all child layers - /// - /// - public List GetAllLayers() - { - if (Disposed) - throw new ObjectDisposedException(GetType().Name); - - List layers = new(); - - // Add all layers in this element - layers.AddRange(Children.Where(c => c is Layer).Cast()); - - // Add all layers in folders inside this element - foreach (Folder childFolder in Children.Where(c => c is Folder).Cast()) - layers.AddRange(childFolder.GetAllLayers()); - - return layers; - } - - /// - /// Returns a name for a new layer according to any other layers with a default name similar to creating new folders in - /// Explorer - /// - /// The resulting name i.e. New layer or New layer (2) - 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++; - } - } - - /// - /// Returns a name for a new folder according to any other folders with a default name similar to creating new folders - /// in Explorer - /// - /// The resulting name i.e. New folder or New folder (2) - 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(); + Children = new ReadOnlyCollection(ChildrenList); } + + /// + /// Gets the unique ID of this profile element + /// + public Guid EntityId + { + get => _entityId; + internal set => SetAndNotify(ref _entityId, value); + } + + /// + /// Gets the profile this element belongs to + /// + public Profile Profile + { + get => _profile; + internal set => SetAndNotify(ref _profile, value); + } + + /// + /// Gets the parent of this element + /// + public ProfileElement? Parent + { + get => _parent; + internal set => SetAndNotify(ref _parent, value); + } + + /// + /// The element's children + /// + public ReadOnlyCollection Children { get; } + + /// + /// The order in which this element appears in the update loop and editor + /// + public int Order + { + get => _order; + internal set => SetAndNotify(ref _order, value); + } + + /// + /// The name which appears in the editor + /// + public string? Name + { + get => _name; + set => SetAndNotify(ref _name, value); + } + + /// + /// Gets or sets the suspended state, if suspended the element is skipped in render and update + /// + public bool Suspended + { + get => _suspended; + set => SetAndNotify(ref _suspended, value); + } + + /// + /// Gets a boolean indicating whether the profile element is disposed + /// + public bool Disposed { get; protected set; } + + #region Overrides of BreakableModel + + /// + public override string BrokenDisplayName => Name ?? GetType().Name; + + #endregion + + /// + /// Updates the element + /// + /// + public abstract void Update(double deltaTime); + + /// + /// Renders the element + /// + /// The canvas to render upon. + /// The base position to use to translate relative positions to absolute positions. + /// An optional element to focus on while rendering (other elements will not render). + public abstract void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus); + + /// + /// Resets the internal state of the element + /// + public abstract void Reset(); + + /// + public override string ToString() + { + return $"{nameof(EntityId)}: {EntityId}, {nameof(Order)}: {Order}, {nameof(Name)}: {Name}"; + } + + /// + /// Occurs when a child was added to the list + /// + public event EventHandler? ChildAdded; + + /// + /// Occurs when a child was removed from the list + /// + public event EventHandler? ChildRemoved; + + /// + /// Occurs when a child was added to the list of this element or any of it's descendents. + /// + public event EventHandler? DescendentAdded; + + /// + /// Occurs when a child was removed from the list of this element or any of it's descendents. + /// + public event EventHandler? DescendentRemoved; + + /// + /// Invokes the event + /// + protected virtual void OnChildAdded(ProfileElement child) + { + ChildAdded?.Invoke(this, new ProfileElementEventArgs(child)); + } + + /// + /// Invokes the event + /// + protected virtual void OnChildRemoved(ProfileElement child) + { + ChildRemoved?.Invoke(this, new ProfileElementEventArgs(child)); + } + + /// + /// Invokes the event + /// + protected virtual void OnDescendentAdded(ProfileElement child) + { + DescendentAdded?.Invoke(this, new ProfileElementEventArgs(child)); + Parent?.OnDescendentAdded(child); + } + + /// + /// Invokes the event + /// + protected virtual void OnDescendentRemoved(ProfileElement child) + { + DescendentRemoved?.Invoke(this, new ProfileElementEventArgs(child)); + Parent?.OnDescendentRemoved(child); + } + + /// + /// Disposes the profile element + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #region Hierarchy + + /// + /// Adds a profile element to the collection, optionally at the given position (0-based) + /// + /// The profile element to add + /// The order where to place the child (0-based), defaults to the end of the collection + 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); + } + + /// + /// Removes a profile element from the collection + /// + /// The profile element to remove + 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; + } + + /// + /// Returns a flattened list of all child render elements + /// + /// + public List GetAllRenderElements() + { + if (Disposed) + throw new ObjectDisposedException(GetType().Name); + + List elements = new(); + foreach (RenderProfileElement childElement in Children.Where(c => c is RenderProfileElement).Cast()) + { + // Add all folders in this element + elements.Add(childElement); + // Add all folders in folders inside this element + elements.AddRange(childElement.GetAllRenderElements()); + } + + return elements; + } + + /// + /// Returns a flattened list of all child folders + /// + /// + public List GetAllFolders() + { + if (Disposed) + throw new ObjectDisposedException(GetType().Name); + + List folders = new(); + foreach (Folder childFolder in Children.Where(c => c is Folder).Cast()) + { + // Add all folders in this element + folders.Add(childFolder); + // Add all folders in folders inside this element + folders.AddRange(childFolder.GetAllFolders()); + } + + return folders; + } + + /// + /// Returns a flattened list of all child layers + /// + /// + public List GetAllLayers() + { + if (Disposed) + throw new ObjectDisposedException(GetType().Name); + + List layers = new(); + + // Add all layers in this element + layers.AddRange(Children.Where(c => c is Layer).Cast()); + + // Add all layers in folders inside this element + foreach (Folder childFolder in Children.Where(c => c is Folder).Cast()) + layers.AddRange(childFolder.GetAllLayers()); + + return layers; + } + + /// + /// Returns a name for a new layer according to any other layers with a default name similar to creating new folders in + /// Explorer + /// + /// The resulting name i.e. New layer or New layer (2) + 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++; + } + } + + /// + /// Returns a name for a new folder according to any other folders with a default name similar to creating new folders + /// in Explorer + /// + /// The resulting name i.e. New folder or New folder (2) + 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 } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/ProfileRightSideType.cs b/src/Artemis.Core/Models/Profile/ProfileRightSideType.cs index b07c20bda..db11877c5 100644 --- a/src/Artemis.Core/Models/Profile/ProfileRightSideType.cs +++ b/src/Artemis.Core/Models/Profile/ProfileRightSideType.cs @@ -1,18 +1,17 @@ -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// An enum defining the right side type of a profile entity +/// +public enum ProfileRightSideType { /// - /// An enum defining the right side type of a profile entity + /// A static right side value /// - public enum ProfileRightSideType - { - /// - /// A static right side value - /// - Static, + Static, - /// - /// A dynamic right side value based on a path in a data model - /// - Dynamic - } + /// + /// A dynamic right side value based on a path in a data model + /// + Dynamic } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index fe3d1017b..ad2528865 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -182,7 +182,7 @@ public abstract class RenderProfileElement : ProfileElement get => _bounds; private set => SetAndNotify(ref _bounds, value); } - + #endregion #region State @@ -256,7 +256,7 @@ public abstract class RenderProfileElement : ProfileElement foreach (BaseLayerEffect baseLayerEffect in _layerEffects) baseLayerEffect.Dispose(); _layerEffects.Clear(); - + foreach (LayerEffectEntity layerEffectEntity in RenderElementEntity.LayerEffects.OrderBy(e => e.Order)) LoadLayerEffect(layerEffectEntity); } @@ -276,7 +276,7 @@ public abstract class RenderProfileElement : ProfileElement descriptor = PlaceholderLayerEffectDescriptor.Create(layerEffectEntity.ProviderId); layerEffect = descriptor.CreateInstance(this, layerEffectEntity); } - + _layerEffects.Add(layerEffect); } @@ -285,7 +285,7 @@ public abstract class RenderProfileElement : ProfileElement int index = _layerEffects.IndexOf(layerEffect); if (index == -1) return; - + LayerEffectDescriptor descriptor = PlaceholderLayerEffectDescriptor.Create(layerEffect.ProviderId); BaseLayerEffect placeholder = descriptor.CreateInstance(this, layerEffect.LayerEffectEntity); _layerEffects[index] = placeholder; @@ -298,17 +298,17 @@ public abstract class RenderProfileElement : ProfileElement int index = _layerEffects.IndexOf(placeholder); if (index == -1) return; - + LayerEffectDescriptor? descriptor = LayerEffectStore.Get(placeholder.OriginalEntity.ProviderId, placeholder.PlaceholderFor)?.LayerEffectDescriptor; if (descriptor == null) throw new ArtemisCoreException("Can't replace a placeholder effect because the real effect isn't available."); - + BaseLayerEffect layerEffect = descriptor.CreateInstance(this, placeholder.OriginalEntity); _layerEffects[index] = layerEffect; placeholder.Dispose(); OnLayerEffectsUpdated(); } - + private void OrderEffects() { int index = 0; @@ -320,7 +320,7 @@ public abstract class RenderProfileElement : ProfileElement _layerEffects.Sort((a, b) => a.Order.CompareTo(b.Order)); } - + private void LayerEffectStoreOnLayerEffectRemoved(object? sender, LayerEffectStoreEvent e) { // Find effects that just got disabled and replace them with placeholders @@ -328,7 +328,7 @@ public abstract class RenderProfileElement : ProfileElement if (!affectedLayerEffects.Any()) return; - + foreach (BaseLayerEffect baseLayerEffect in affectedLayerEffects) ReplaceLayerEffectWithPlaceholder(baseLayerEffect); OnLayerEffectsUpdated(); @@ -344,7 +344,7 @@ public abstract class RenderProfileElement : ProfileElement if (!affectedPlaceholders.Any()) return; - + foreach (PlaceholderLayerEffect placeholderLayerEffect in affectedPlaceholders) ReplacePlaceholderWithLayerEffect(placeholderLayerEffect); OnLayerEffectsUpdated(); @@ -393,4 +393,4 @@ public abstract class RenderProfileElement : ProfileElement } #endregion -} +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Renderer.cs b/src/Artemis.Core/Models/Profile/Renderer.cs index 4de0fa2d5..b72d48319 100644 --- a/src/Artemis.Core/Models/Profile/Renderer.cs +++ b/src/Artemis.Core/Models/Profile/Renderer.cs @@ -1,118 +1,117 @@ using System; using SkiaSharp; -namespace Artemis.Core +namespace Artemis.Core; + +internal class Renderer : IDisposable { - internal class Renderer : IDisposable + private bool _disposed; + private SKRect _lastBounds; + private GRContext? _lastGraphicsContext; + private SKRect _lastParentBounds; + private bool _valid; + public SKSurface? Surface { get; private set; } + public SKPaint? Paint { get; private set; } + public SKPath? Path { get; private set; } + public SKPoint TargetLocation { get; private set; } + + public bool IsOpen { get; private set; } + + /// + /// Opens the render context using the dimensions of the provided path + /// + public void Open(SKPath path, Folder? parent) { - private bool _valid; - private bool _disposed; - private SKRect _lastBounds; - private SKRect _lastParentBounds; - private GRContext? _lastGraphicsContext; - public SKSurface? Surface { get; private set; } - public SKPaint? Paint { get; private set; } - public SKPath? Path { get; private set; } - public SKPoint TargetLocation { get; private set; } + if (_disposed) + throw new ObjectDisposedException("Renderer"); - public bool IsOpen { get; private set; } + if (IsOpen) + throw new ArtemisCoreException("Cannot open render context because it is already open"); - /// - /// Opens the render context using the dimensions of the provided path - /// - public void Open(SKPath path, Folder? parent) + if (path.Bounds != _lastBounds || (parent != null && parent.Bounds != _lastParentBounds) || _lastGraphicsContext != Constants.ManagedGraphicsContext?.GraphicsContext) + Invalidate(); + + if (!_valid || Surface == null) { - if (_disposed) - throw new ObjectDisposedException("Renderer"); + SKRect pathBounds = path.Bounds; + int width = (int) pathBounds.Width; + int height = (int) pathBounds.Height; - if (IsOpen) - throw new ArtemisCoreException("Cannot open render context because it is already open"); + SKImageInfo imageInfo = new(width, height); + if (Constants.ManagedGraphicsContext?.GraphicsContext == null) + Surface = SKSurface.Create(imageInfo); + else + Surface = SKSurface.Create(Constants.ManagedGraphicsContext.GraphicsContext, true, imageInfo); - if (path.Bounds != _lastBounds || (parent != null && parent.Bounds != _lastParentBounds) || _lastGraphicsContext != Constants.ManagedGraphicsContext?.GraphicsContext) - Invalidate(); + Path = new SKPath(path); + Path.Transform(SKMatrix.CreateTranslation(pathBounds.Left * -1, pathBounds.Top * -1)); - if (!_valid || Surface == null) - { - SKRect pathBounds = path.Bounds; - int width = (int) pathBounds.Width; - int height = (int) pathBounds.Height; + TargetLocation = new SKPoint(pathBounds.Location.X, pathBounds.Location.Y); + if (parent != null) + TargetLocation -= parent.Bounds.Location; - SKImageInfo imageInfo = new(width, height); - if (Constants.ManagedGraphicsContext?.GraphicsContext == null) - Surface = SKSurface.Create(imageInfo); - else - Surface = SKSurface.Create(Constants.ManagedGraphicsContext.GraphicsContext, true, imageInfo); + Surface.Canvas.ClipPath(Path); - Path = new SKPath(path); - Path.Transform(SKMatrix.CreateTranslation(pathBounds.Left * -1, pathBounds.Top * -1)); - - TargetLocation = new SKPoint(pathBounds.Location.X, pathBounds.Location.Y); - if (parent != null) - TargetLocation -= parent.Bounds.Location; - - Surface.Canvas.ClipPath(Path); - - _lastParentBounds = parent?.Bounds ?? new SKRect(); - _lastBounds = path.Bounds; - _lastGraphicsContext = Constants.ManagedGraphicsContext?.GraphicsContext; - _valid = true; - } - - Paint = new SKPaint(); - - Surface.Canvas.Clear(); - Surface.Canvas.Save(); - - IsOpen = true; + _lastParentBounds = parent?.Bounds ?? new SKRect(); + _lastBounds = path.Bounds; + _lastGraphicsContext = Constants.ManagedGraphicsContext?.GraphicsContext; + _valid = true; } - public void Close() - { - if (_disposed) - throw new ObjectDisposedException("Renderer"); + Paint = new SKPaint(); - Surface?.Canvas.Restore(); + Surface.Canvas.Clear(); + Surface.Canvas.Save(); - // Looks like every part of the paint needs to be disposed :( - Paint?.ColorFilter?.Dispose(); - Paint?.ImageFilter?.Dispose(); - Paint?.MaskFilter?.Dispose(); - Paint?.PathEffect?.Dispose(); - Paint?.Dispose(); + IsOpen = true; + } - Paint = null; + public void Close() + { + if (_disposed) + throw new ObjectDisposedException("Renderer"); - IsOpen = false; - } + Surface?.Canvas.Restore(); - public void Invalidate() - { - if (_disposed) - throw new ObjectDisposedException("Renderer"); + // Looks like every part of the paint needs to be disposed :( + Paint?.ColorFilter?.Dispose(); + Paint?.ImageFilter?.Dispose(); + Paint?.MaskFilter?.Dispose(); + Paint?.PathEffect?.Dispose(); + Paint?.Dispose(); - _valid = false; - } + Paint = null; - public void Dispose() - { - if (IsOpen) - Close(); + IsOpen = false; + } - Surface?.Dispose(); - Paint?.Dispose(); - Path?.Dispose(); + public void Invalidate() + { + if (_disposed) + throw new ObjectDisposedException("Renderer"); - Surface = null; - Paint = null; - Path = null; + _valid = false; + } - _disposed = true; - } + ~Renderer() + { + if (IsOpen) + Close(); + } - ~Renderer() - { - if (IsOpen) - Close(); - } + public void Dispose() + { + if (IsOpen) + Close(); + + Surface?.Dispose(); + Paint?.Dispose(); + Path?.Dispose(); + + Surface = null; + Paint = null; + Path = null; + + _disposed = true; } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Timeline.cs b/src/Artemis.Core/Models/Profile/Timeline.cs index 016ea1ecb..6ba2b7001 100644 --- a/src/Artemis.Core/Models/Profile/Timeline.cs +++ b/src/Artemis.Core/Models/Profile/Timeline.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; using Artemis.Storage.Entities.Profile; namespace Artemis.Core; @@ -67,7 +64,6 @@ public class Timeline : CorePropertyChanged, IStorageModel } - /// /// Gets a boolean indicating whether the timeline has finished its run /// @@ -391,4 +387,4 @@ internal enum TimelineSegment Start, Main, End -} +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/ProfileConfiguration/Hotkey.cs b/src/Artemis.Core/Models/ProfileConfiguration/Hotkey.cs index aaa5485e6..51d0ce1ef 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/Hotkey.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/Hotkey.cs @@ -1,65 +1,63 @@ using Artemis.Core.Services; using Artemis.Storage.Entities.Profile; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a key or combination of keys that changes the suspension status of a +/// +public class Hotkey : CorePropertyChanged, IStorageModel { /// - /// Represents a key or combination of keys that changes the suspension status of a + /// Creates a new instance of /// - public class Hotkey : CorePropertyChanged, IStorageModel + public Hotkey() { - - /// - /// Creates a new instance of - /// - public Hotkey() - { - Entity = new ProfileConfigurationHotkeyEntity(); - } - - internal Hotkey(ProfileConfigurationHotkeyEntity entity) - { - Entity = entity; - Load(); - } - - internal ProfileConfigurationHotkeyEntity Entity { get; } - - /// - /// Gets or sets the of the hotkey - /// - public KeyboardKey? Key { get; set; } - - /// - /// Gets or sets the s of the hotkey - /// - public KeyboardModifierKey? Modifiers { get; set; } - - /// - /// Determines whether the provided match the hotkey - /// - /// if the event args match the hotkey; otherwise - public bool MatchesEventArgs(ArtemisKeyboardKeyEventArgs eventArgs) - { - return eventArgs.Key == Key && eventArgs.Modifiers == Modifiers; - } - - #region Implementation of IStorageModel - - /// - public void Load() - { - Key = (KeyboardKey?) Entity.Key; - Modifiers = (KeyboardModifierKey?) Entity.Modifiers; - } - - /// - public void Save() - { - Entity.Key = (int?) Key; - Entity.Modifiers = (int?) Modifiers; - } - - #endregion + Entity = new ProfileConfigurationHotkeyEntity(); } + + internal Hotkey(ProfileConfigurationHotkeyEntity entity) + { + Entity = entity; + Load(); + } + + /// + /// Gets or sets the of the hotkey + /// + public KeyboardKey? Key { get; set; } + + /// + /// Gets or sets the s of the hotkey + /// + public KeyboardModifierKey? Modifiers { get; set; } + + internal ProfileConfigurationHotkeyEntity Entity { get; } + + /// + /// Determines whether the provided match the hotkey + /// + /// if the event args match the hotkey; otherwise + public bool MatchesEventArgs(ArtemisKeyboardKeyEventArgs eventArgs) + { + return eventArgs.Key == Key && eventArgs.Modifiers == Modifiers; + } + + #region Implementation of IStorageModel + + /// + public void Load() + { + Key = (KeyboardKey?) Entity.Key; + Modifiers = (KeyboardModifierKey?) Entity.Modifiers; + } + + /// + public void Save() + { + Entity.Key = (int?) Key; + Entity.Modifiers = (int?) Modifiers; + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs index f993e3ae5..3390a2863 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs @@ -4,376 +4,373 @@ using System.Linq; using Artemis.Core.Modules; using Artemis.Storage.Entities.Profile; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents the configuration of a profile, contained in a +/// +public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable +{ + private ActivationBehaviour _activationBehaviour; + private bool _activationConditionMet; + private ProfileCategory _category; + private Hotkey? _disableHotkey; + private bool _disposed; + private Hotkey? _enableHotkey; + private ProfileConfigurationHotkeyMode _hotkeyMode; + private bool _isBeingEdited; + private bool _isMissingModule; + private bool _isSuspended; + private Module? _module; + + private string _name; + private int _order; + private Profile? _profile; + + internal ProfileConfiguration(ProfileCategory category, string name, string icon) + { + _name = name; + _category = category; + + Entity = new ProfileConfigurationEntity(); + Icon = new ProfileConfigurationIcon(Entity); + Icon.SetIconByName(icon); + ActivationCondition = new NodeScript("Activate profile", "Whether or not the profile should be active", this); + } + + internal ProfileConfiguration(ProfileCategory category, ProfileConfigurationEntity entity) + { + // Will be loaded from the entity + _name = null!; + _category = category; + + Entity = entity; + Icon = new ProfileConfigurationIcon(Entity); + ActivationCondition = new NodeScript("Activate profile", "Whether or not the profile should be active", this); + + Load(); + } + + /// + /// Gets or sets the name of this profile configuration + /// + public string Name + { + get => _name; + set => SetAndNotify(ref _name, value); + } + + /// + /// The order in which this profile appears in the update loop and sidebar + /// + public int Order + { + get => _order; + set => SetAndNotify(ref _order, value); + } + + /// + /// Gets or sets a boolean indicating whether this profile is suspended, disabling it regardless of the + /// + /// + public bool IsSuspended + { + get => _isSuspended; + set => SetAndNotify(ref _isSuspended, value); + } + + /// + /// Gets a boolean indicating whether this profile configuration is missing any modules + /// + public bool IsMissingModule + { + get => _isMissingModule; + private set => SetAndNotify(ref _isMissingModule, value); + } + + /// + /// Gets or sets the category of this profile configuration + /// + public ProfileCategory Category + { + get => _category; + internal set => SetAndNotify(ref _category, value); + } + + /// + /// Gets or sets the used to determine hotkey behaviour + /// + public ProfileConfigurationHotkeyMode HotkeyMode + { + get => _hotkeyMode; + set => SetAndNotify(ref _hotkeyMode, value); + } + + /// + /// Gets or sets the hotkey used to enable or toggle the profile + /// + public Hotkey? EnableHotkey + { + get => _enableHotkey; + set => SetAndNotify(ref _enableHotkey, value); + } + + /// + /// Gets or sets the hotkey used to disable the profile + /// + public Hotkey? DisableHotkey + { + get => _disableHotkey; + set => SetAndNotify(ref _disableHotkey, value); + } + + /// + /// Gets or sets the behaviour of when this profile is activated + /// + public ActivationBehaviour ActivationBehaviour + { + get => _activationBehaviour; + set => SetAndNotify(ref _activationBehaviour, value); + } + + /// + /// Gets a boolean indicating whether the activation conditions where met during the last call + /// + public bool ActivationConditionMet + { + get => _activationConditionMet; + private set => SetAndNotify(ref _activationConditionMet, value); + } + + /// + /// Gets or sets a boolean indicating whether this profile configuration is being edited + /// + public bool IsBeingEdited + { + get => _isBeingEdited; + set => SetAndNotify(ref _isBeingEdited, value); + } + + /// + /// Gets the profile of this profile configuration + /// + public Profile? Profile + { + get => _profile; + internal set => SetAndNotify(ref _profile, value); + } + + /// + /// Gets or sets the module this profile uses + /// + public Module? Module + { + get => _module; + set + { + SetAndNotify(ref _module, value); + IsMissingModule = false; + } + } + + /// + /// Gets the icon configuration + /// + public ProfileConfigurationIcon Icon { get; } + + /// + /// Gets the data model condition that must evaluate to for this profile to be activated + /// alongside any activation requirements of the , if set + /// + public NodeScript ActivationCondition { get; } + + /// + /// Gets the entity used by this profile config + /// + public ProfileConfigurationEntity Entity { get; } + + /// + /// Gets the ID of the profile of this profile configuration + /// + public Guid ProfileId => Entity.ProfileId; + + #region Overrides of BreakableModel + + /// + public override string BrokenDisplayName => "Profile Configuration"; + + #endregion + + /// + /// Updates this configurations activation condition status + /// + public void Update() + { + if (_disposed) + throw new ObjectDisposedException("ProfileConfiguration"); + + if (!ActivationCondition.ExitNodeConnected) + { + ActivationConditionMet = true; + } + else + { + ActivationCondition.Run(); + ActivationConditionMet = ActivationCondition.Result; + } + } + + /// + /// Determines whether the profile of this configuration should be active + /// + /// Whether or not to take activation conditions into consideration + public bool ShouldBeActive(bool includeActivationCondition) + { + if (_disposed) + throw new ObjectDisposedException("ProfileConfiguration"); + if (IsBeingEdited) + return true; + if (Category.IsSuspended || IsSuspended || IsMissingModule) + return false; + + if (includeActivationCondition) + return ActivationConditionMet && (Module == null || Module.IsActivated); + return Module == null || Module.IsActivated; + } + + /// + public override string ToString() + { + return $"[ProfileConfiguration] {nameof(Name)}: {Name}"; + } + + internal void LoadModules(List enabledModules) + { + if (_disposed) + throw new ObjectDisposedException("ProfileConfiguration"); + + Module = enabledModules.FirstOrDefault(m => m.Id == Entity.ModuleId); + IsMissingModule = Module == null && Entity.ModuleId != null; + } + + /// + public void Dispose() + { + _disposed = true; + ActivationCondition.Dispose(); + } + + #region Implementation of IStorageModel + + /// + public void Load() + { + if (_disposed) + throw new ObjectDisposedException("ProfileConfiguration"); + + Name = Entity.Name; + IsSuspended = Entity.IsSuspended; + ActivationBehaviour = (ActivationBehaviour) Entity.ActivationBehaviour; + HotkeyMode = (ProfileConfigurationHotkeyMode) Entity.HotkeyMode; + Order = Entity.Order; + + Icon.Load(); + + if (Entity.ActivationCondition != null) + ActivationCondition.LoadFromEntity(Entity.ActivationCondition); + + EnableHotkey = Entity.EnableHotkey != null ? new Hotkey(Entity.EnableHotkey) : null; + DisableHotkey = Entity.DisableHotkey != null ? new Hotkey(Entity.DisableHotkey) : null; + } + + /// + public void Save() + { + if (_disposed) + throw new ObjectDisposedException("ProfileConfiguration"); + + Entity.Name = Name; + Entity.IsSuspended = IsSuspended; + Entity.ActivationBehaviour = (int) ActivationBehaviour; + Entity.HotkeyMode = (int) HotkeyMode; + Entity.ProfileCategoryId = Category.Entity.Id; + Entity.Order = Order; + + Icon.Save(); + + ActivationCondition.Save(); + Entity.ActivationCondition = ActivationCondition.Entity; + + EnableHotkey?.Save(); + Entity.EnableHotkey = EnableHotkey?.Entity; + DisableHotkey?.Save(); + Entity.DisableHotkey = DisableHotkey?.Entity; + + if (!IsMissingModule) + Entity.ModuleId = Module?.Id; + } + + #endregion +} + +/// +/// Represents a type of behaviour when this profile is activated +/// +public enum ActivationBehaviour { /// - /// Represents the configuration of a profile, contained in a + /// Do nothing to other profiles /// - public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable - { - private bool _disposed; - - private string _name; - private int _order; - private bool _isSuspended; - private bool _isMissingModule; - private ProfileCategory _category; - private ProfileConfigurationHotkeyMode _hotkeyMode; - private Hotkey? _enableHotkey; - private Hotkey? _disableHotkey; - private ActivationBehaviour _activationBehaviour; - private bool _activationConditionMet; - private bool _isBeingEdited; - private Profile? _profile; - private Module? _module; - - internal ProfileConfiguration(ProfileCategory category, string name, string icon) - { - _name = name; - _category = category; - - Entity = new ProfileConfigurationEntity(); - Icon = new ProfileConfigurationIcon(Entity); - Icon.SetIconByName(icon); - ActivationCondition = new NodeScript("Activate profile", "Whether or not the profile should be active", this); - } - - internal ProfileConfiguration(ProfileCategory category, ProfileConfigurationEntity entity) - { - // Will be loaded from the entity - _name = null!; - _category = category; - - Entity = entity; - Icon = new ProfileConfigurationIcon(Entity); - ActivationCondition = new NodeScript("Activate profile", "Whether or not the profile should be active", this); - - Load(); - } - - /// - /// Gets or sets the name of this profile configuration - /// - public string Name - { - get => _name; - set => SetAndNotify(ref _name, value); - } - - /// - /// The order in which this profile appears in the update loop and sidebar - /// - public int Order - { - get => _order; - set => SetAndNotify(ref _order, value); - } - - /// - /// Gets or sets a boolean indicating whether this profile is suspended, disabling it regardless of the - /// - /// - public bool IsSuspended - { - get => _isSuspended; - set => SetAndNotify(ref _isSuspended, value); - } - - /// - /// Gets a boolean indicating whether this profile configuration is missing any modules - /// - public bool IsMissingModule - { - get => _isMissingModule; - private set => SetAndNotify(ref _isMissingModule, value); - } - - /// - /// Gets or sets the category of this profile configuration - /// - public ProfileCategory Category - { - get => _category; - internal set => SetAndNotify(ref _category, value); - } - - /// - /// Gets or sets the used to determine hotkey behaviour - /// - public ProfileConfigurationHotkeyMode HotkeyMode - { - get => _hotkeyMode; - set => SetAndNotify(ref _hotkeyMode, value); - } - - /// - /// Gets or sets the hotkey used to enable or toggle the profile - /// - public Hotkey? EnableHotkey - { - get => _enableHotkey; - set => SetAndNotify(ref _enableHotkey, value); - } - - /// - /// Gets or sets the hotkey used to disable the profile - /// - public Hotkey? DisableHotkey - { - get => _disableHotkey; - set => SetAndNotify(ref _disableHotkey, value); - } - - /// - /// Gets or sets the behaviour of when this profile is activated - /// - public ActivationBehaviour ActivationBehaviour - { - get => _activationBehaviour; - set => SetAndNotify(ref _activationBehaviour, value); - } - - /// - /// Gets a boolean indicating whether the activation conditions where met during the last call - /// - public bool ActivationConditionMet - { - get => _activationConditionMet; - private set => SetAndNotify(ref _activationConditionMet, value); - } - - /// - /// Gets or sets a boolean indicating whether this profile configuration is being edited - /// - public bool IsBeingEdited - { - get => _isBeingEdited; - set => SetAndNotify(ref _isBeingEdited, value); - } - - /// - /// Gets the profile of this profile configuration - /// - public Profile? Profile - { - get => _profile; - internal set => SetAndNotify(ref _profile, value); - } - - /// - /// Gets or sets the module this profile uses - /// - public Module? Module - { - get => _module; - set - { - SetAndNotify(ref _module, value); - IsMissingModule = false; - } - } - - /// - /// Gets the icon configuration - /// - public ProfileConfigurationIcon Icon { get; } - - /// - /// Gets the data model condition that must evaluate to for this profile to be activated - /// alongside any activation requirements of the , if set - /// - public NodeScript ActivationCondition { get; } - - /// - /// Gets the entity used by this profile config - /// - public ProfileConfigurationEntity Entity { get; } - - /// - /// Gets the ID of the profile of this profile configuration - /// - public Guid ProfileId => Entity.ProfileId; - - /// - /// Updates this configurations activation condition status - /// - public void Update() - { - if (_disposed) - throw new ObjectDisposedException("ProfileConfiguration"); - - if (!ActivationCondition.ExitNodeConnected) - ActivationConditionMet = true; - else - { - ActivationCondition.Run(); - ActivationConditionMet = ActivationCondition.Result; - } - } - - /// - /// Determines whether the profile of this configuration should be active - /// - /// Whether or not to take activation conditions into consideration - public bool ShouldBeActive(bool includeActivationCondition) - { - if (_disposed) - throw new ObjectDisposedException("ProfileConfiguration"); - if (IsBeingEdited) - return true; - if (Category.IsSuspended || IsSuspended || IsMissingModule) - return false; - - if (includeActivationCondition) - return ActivationConditionMet && (Module == null || Module.IsActivated); - return Module == null || Module.IsActivated; - } - - /// - public override string ToString() - { - return $"[ProfileConfiguration] {nameof(Name)}: {Name}"; - } - - internal void LoadModules(List enabledModules) - { - if (_disposed) - throw new ObjectDisposedException("ProfileConfiguration"); - - Module = enabledModules.FirstOrDefault(m => m.Id == Entity.ModuleId); - IsMissingModule = Module == null && Entity.ModuleId != null; - } - - #region IDisposable - - /// - public void Dispose() - { - _disposed = true; - ActivationCondition.Dispose(); - } - - #endregion - - #region Implementation of IStorageModel - - /// - public void Load() - { - if (_disposed) - throw new ObjectDisposedException("ProfileConfiguration"); - - Name = Entity.Name; - IsSuspended = Entity.IsSuspended; - ActivationBehaviour = (ActivationBehaviour) Entity.ActivationBehaviour; - HotkeyMode = (ProfileConfigurationHotkeyMode) Entity.HotkeyMode; - Order = Entity.Order; - - Icon.Load(); - - if (Entity.ActivationCondition != null) - ActivationCondition.LoadFromEntity(Entity.ActivationCondition); - - EnableHotkey = Entity.EnableHotkey != null ? new Hotkey(Entity.EnableHotkey) : null; - DisableHotkey = Entity.DisableHotkey != null ? new Hotkey(Entity.DisableHotkey) : null; - } - - /// - public void Save() - { - if (_disposed) - throw new ObjectDisposedException("ProfileConfiguration"); - - Entity.Name = Name; - Entity.IsSuspended = IsSuspended; - Entity.ActivationBehaviour = (int) ActivationBehaviour; - Entity.HotkeyMode = (int) HotkeyMode; - Entity.ProfileCategoryId = Category.Entity.Id; - Entity.Order = Order; - - Icon.Save(); - - ActivationCondition.Save(); - Entity.ActivationCondition = ActivationCondition.Entity; - - EnableHotkey?.Save(); - Entity.EnableHotkey = EnableHotkey?.Entity; - DisableHotkey?.Save(); - Entity.DisableHotkey = DisableHotkey?.Entity; - - if (!IsMissingModule) - Entity.ModuleId = Module?.Id; - } - - #endregion - - #region Overrides of BreakableModel - - /// - public override string BrokenDisplayName => "Profile Configuration"; - - #endregion - } + None, /// - /// Represents a type of behaviour when this profile is activated + /// Disable all other profiles /// - public enum ActivationBehaviour - { - /// - /// Do nothing to other profiles - /// - None, - - /// - /// Disable all other profiles - /// - DisableOthers, - - /// - /// Disable all other profiles below this one - /// - DisableOthersBelow, - - /// - /// Disable all other profiles above this one - /// - DisableOthersAbove, - - /// - /// Disable all other profiles in the same category - /// - DisableOthersInCategory, - - /// - /// Disable all other profiles below this one in the same category - /// - DisableOthersBelowInCategory, - - /// - /// Disable all other profiles above this one in the same category - /// - DisableOthersAboveInCategory - } + DisableOthers, /// - /// Represents a hotkey mode for a profile configuration + /// Disable all other profiles below this one /// - public enum ProfileConfigurationHotkeyMode - { - /// - /// Use no hotkeys - /// - None, + DisableOthersBelow, - /// - /// Toggle the profile with one hotkey - /// - Toggle, + /// + /// Disable all other profiles above this one + /// + DisableOthersAbove, - /// - /// Enable and disable the profile with two separate hotkeys - /// - EnableDisable - } + /// + /// Disable all other profiles in the same category + /// + DisableOthersInCategory, + + /// + /// Disable all other profiles below this one in the same category + /// + DisableOthersBelowInCategory, + + /// + /// Disable all other profiles above this one in the same category + /// + DisableOthersAboveInCategory +} + +/// +/// Represents a hotkey mode for a profile configuration +/// +public enum ProfileConfigurationHotkeyMode +{ + /// + /// Use no hotkeys + /// + None, + + /// + /// Toggle the profile with one hotkey + /// + Toggle, + + /// + /// Enable and disable the profile with two separate hotkeys + /// + EnableDisable } \ No newline at end of file diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationExportModel.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationExportModel.cs index 20ea8f088..075c5f54b 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationExportModel.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationExportModel.cs @@ -4,34 +4,33 @@ using Artemis.Core.JsonConverters; using Artemis.Storage.Entities.Profile; using Newtonsoft.Json; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// A model that can be used to serialize a profile configuration, it's profile and it's icon +/// +public class ProfileConfigurationExportModel : IDisposable { /// - /// A model that can be used to serialize a profile configuration, it's profile and it's icon + /// Gets or sets the storage entity of the profile configuration /// - public class ProfileConfigurationExportModel : IDisposable + public ProfileConfigurationEntity? ProfileConfigurationEntity { get; set; } + + /// + /// Gets or sets the storage entity of the profile + /// + [JsonProperty(Required = Required.Always)] + public ProfileEntity ProfileEntity { get; set; } = null!; + + /// + /// Gets or sets a stream containing the profile image + /// + [JsonConverter(typeof(StreamConverter))] + public Stream? ProfileImage { get; set; } + + /// + public void Dispose() { - /// - /// Gets or sets the storage entity of the profile configuration - /// - public ProfileConfigurationEntity? ProfileConfigurationEntity { get; set; } - - /// - /// Gets or sets the storage entity of the profile - /// - [JsonProperty(Required = Required.Always)] - public ProfileEntity ProfileEntity { get; set; } = null!; - - /// - /// Gets or sets a stream containing the profile image - /// - [JsonConverter(typeof(StreamConverter))] - public Stream? ProfileImage { get; set; } - - /// - public void Dispose() - { - ProfileImage?.Dispose(); - } + ProfileImage?.Dispose(); } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs index d76bf7495..058e95687 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs @@ -3,168 +3,167 @@ using System.ComponentModel; using System.IO; using Artemis.Storage.Entities.Profile; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents the icon of a +/// +public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel +{ + private readonly ProfileConfigurationEntity _entity; + private bool _fill; + private string? _iconName; + private Stream? _iconStream; + private ProfileConfigurationIconType _iconType; + private string? _originalFileName; + + internal ProfileConfigurationIcon(ProfileConfigurationEntity entity) + { + _entity = entity; + } + + /// + /// Gets the type of icon this profile configuration uses + /// + public ProfileConfigurationIconType IconType + { + get => _iconType; + private set => SetAndNotify(ref _iconType, value); + } + + /// + /// Gets the name of the icon if is + /// + public string? IconName + { + get => _iconName; + private set => SetAndNotify(ref _iconName, value); + } + + /// + /// Gets the original file name of the icon (if applicable) + /// + public string? OriginalFileName + { + get => _originalFileName; + private set => SetAndNotify(ref _originalFileName, value); + } + + /// + /// Gets or sets a boolean indicating whether or not this icon should be filled. + /// + public bool Fill + { + get => _fill; + set => SetAndNotify(ref _fill, value); + } + + /// + /// Updates the to the provided value and changes the is + /// + /// + /// The name of the icon + public void SetIconByName(string iconName) + { + if (iconName == null) throw new ArgumentNullException(nameof(iconName)); + + _iconStream?.Dispose(); + IconName = iconName; + OriginalFileName = null; + IconType = ProfileConfigurationIconType.MaterialIcon; + + OnIconUpdated(); + } + + /// + /// Updates the stream returned by to the provided stream + /// + /// The original file name backing the stream, should include the extension + /// The stream to copy + public void SetIconByStream(string originalFileName, Stream stream) + { + if (originalFileName == null) throw new ArgumentNullException(nameof(originalFileName)); + if (stream == null) throw new ArgumentNullException(nameof(stream)); + + _iconStream?.Dispose(); + _iconStream = new MemoryStream(); + stream.Seek(0, SeekOrigin.Begin); + stream.CopyTo(_iconStream); + _iconStream.Seek(0, SeekOrigin.Begin); + + IconName = null; + OriginalFileName = originalFileName; + IconType = ProfileConfigurationIconType.BitmapImage; + OnIconUpdated(); + } + + /// + /// Creates a copy of the stream containing the icon + /// + /// A stream containing the icon + public Stream? GetIconStream() + { + if (_iconStream == null) + return null; + + MemoryStream stream = new(); + _iconStream.CopyTo(stream); + + stream.Seek(0, SeekOrigin.Begin); + _iconStream.Seek(0, SeekOrigin.Begin); + return stream; + } + + /// + /// Occurs when the icon was updated + /// + public event EventHandler? IconUpdated; + + /// + /// Invokes the event + /// + protected virtual void OnIconUpdated() + { + IconUpdated?.Invoke(this, EventArgs.Empty); + } + + #region Implementation of IStorageModel + + /// + public void Load() + { + IconType = (ProfileConfigurationIconType) _entity.IconType; + Fill = _entity.IconFill; + if (IconType != ProfileConfigurationIconType.MaterialIcon) + return; + + IconName = _entity.MaterialIcon; + OnIconUpdated(); + } + + /// + public void Save() + { + _entity.IconType = (int) IconType; + _entity.MaterialIcon = IconType == ProfileConfigurationIconType.MaterialIcon ? IconName : null; + _entity.IconFill = Fill; + } + + #endregion +} + +/// +/// Represents a type of profile icon +/// +public enum ProfileConfigurationIconType { /// - /// Represents the icon of a + /// An icon picked from the Material Design Icons collection /// - public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel - { - private readonly ProfileConfigurationEntity _entity; - private Stream? _iconStream; - private ProfileConfigurationIconType _iconType; - private string? _iconName; - private string? _originalFileName; - private bool _fill; - - internal ProfileConfigurationIcon(ProfileConfigurationEntity entity) - { - _entity = entity; - } - - /// - /// Gets the type of icon this profile configuration uses - /// - public ProfileConfigurationIconType IconType - { - get => _iconType; - private set => SetAndNotify(ref _iconType, value); - } - - /// - /// Gets the name of the icon if is - /// - public string? IconName - { - get => _iconName; - private set => SetAndNotify(ref _iconName, value); - } - - /// - /// Gets the original file name of the icon (if applicable) - /// - public string? OriginalFileName - { - get => _originalFileName; - private set => SetAndNotify(ref _originalFileName, value); - } - - /// - /// Gets or sets a boolean indicating whether or not this icon should be filled. - /// - public bool Fill - { - get => _fill; - set => SetAndNotify(ref _fill, value); - } - - /// - /// Updates the to the provided value and changes the is - /// - /// - /// The name of the icon - public void SetIconByName(string iconName) - { - if (iconName == null) throw new ArgumentNullException(nameof(iconName)); - - _iconStream?.Dispose(); - IconName = iconName; - OriginalFileName = null; - IconType = ProfileConfigurationIconType.MaterialIcon; - - OnIconUpdated(); - } - - /// - /// Updates the stream returned by to the provided stream - /// - /// The original file name backing the stream, should include the extension - /// The stream to copy - public void SetIconByStream(string originalFileName, Stream stream) - { - if (originalFileName == null) throw new ArgumentNullException(nameof(originalFileName)); - if (stream == null) throw new ArgumentNullException(nameof(stream)); - - _iconStream?.Dispose(); - _iconStream = new MemoryStream(); - stream.Seek(0, SeekOrigin.Begin); - stream.CopyTo(_iconStream); - _iconStream.Seek(0, SeekOrigin.Begin); - - IconName = null; - OriginalFileName = originalFileName; - IconType = ProfileConfigurationIconType.BitmapImage; - OnIconUpdated(); - } - - /// - /// Creates a copy of the stream containing the icon - /// - /// A stream containing the icon - public Stream? GetIconStream() - { - if (_iconStream == null) - return null; - - MemoryStream stream = new(); - _iconStream.CopyTo(stream); - - stream.Seek(0, SeekOrigin.Begin); - _iconStream.Seek(0, SeekOrigin.Begin); - return stream; - } - - /// - /// Occurs when the icon was updated - /// - public event EventHandler? IconUpdated; - - /// - /// Invokes the event - /// - protected virtual void OnIconUpdated() - { - IconUpdated?.Invoke(this, EventArgs.Empty); - } - - #region Implementation of IStorageModel - - /// - public void Load() - { - IconType = (ProfileConfigurationIconType) _entity.IconType; - Fill = _entity.IconFill; - if (IconType != ProfileConfigurationIconType.MaterialIcon) - return; - - IconName = _entity.MaterialIcon; - OnIconUpdated(); - } - - /// - public void Save() - { - _entity.IconType = (int) IconType; - _entity.MaterialIcon = IconType == ProfileConfigurationIconType.MaterialIcon ? IconName : null; - _entity.IconFill = Fill; - } - - #endregion - } + [Description("Material Design Icon")] MaterialIcon, /// - /// Represents a type of profile icon + /// A bitmap image icon /// - public enum ProfileConfigurationIconType - { - /// - /// An icon picked from the Material Design Icons collection - /// - [Description("Material Design Icon")] MaterialIcon, - - /// - /// A bitmap image icon - /// - [Description("Bitmap Image")] BitmapImage, - } + [Description("Bitmap Image")] BitmapImage } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs index aa8991159..bf1e6211b 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs @@ -8,561 +8,562 @@ using Artemis.Storage.Entities.Surface; using RGB.NET.Core; using SkiaSharp; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents an RGB device usable by Artemis, provided by a +/// +public class ArtemisDevice : CorePropertyChanged +{ + private SKPath? _path; + private SKRect _rectangle; + + internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider) + { + Identifier = rgbDevice.GetDeviceIdentifier(); + DeviceEntity = new DeviceEntity(); + RgbDevice = rgbDevice; + DeviceProvider = deviceProvider; + + Rotation = 0; + Scale = 1; + ZIndex = 1; + RedScale = 1; + GreenScale = 1; + BlueScale = 1; + IsEnabled = true; + + LedIds = new ReadOnlyDictionary(new Dictionary()); + Leds = new ReadOnlyCollection(new List()); + InputIdentifiers = new List(); + InputMappings = new Dictionary(); + Categories = new HashSet(); + + UpdateLeds(); + ApplyKeyboardLayout(); + ApplyToEntity(); + ApplyDefaultCategories(); + CalculateRenderProperties(); + } + + internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider, DeviceEntity deviceEntity) + { + Identifier = rgbDevice.GetDeviceIdentifier(); + DeviceEntity = deviceEntity; + RgbDevice = rgbDevice; + DeviceProvider = deviceProvider; + + LedIds = new ReadOnlyDictionary(new Dictionary()); + Leds = new ReadOnlyCollection(new List()); + InputIdentifiers = new List(); + InputMappings = new Dictionary(); + Categories = new HashSet(); + + foreach (DeviceInputIdentifierEntity identifierEntity in DeviceEntity.InputIdentifiers) + InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(identifierEntity.InputProvider, identifierEntity.Identifier)); + + UpdateLeds(); + ApplyKeyboardLayout(); + } + + /// + /// Gets the (hopefully unique and persistent) ID of this device + /// + public string Identifier { get; } + + /// + /// Gets the rectangle covering the device + /// + public SKRect Rectangle + { + get => _rectangle; + private set => SetAndNotify(ref _rectangle, value); + } + + /// + /// Gets the path surrounding the device + /// + public SKPath? Path + { + get => _path; + private set => SetAndNotify(ref _path, value); + } + + /// + /// Gets the RGB.NET device backing this Artemis device + /// + public IRGBDevice RgbDevice { get; } + + /// + /// Gets the device type of the ArtemisDevice + /// + public RGBDeviceType DeviceType => RgbDevice.DeviceInfo.DeviceType; + + /// + /// Gets the device provider that provided this device + /// + public DeviceProvider DeviceProvider { get; } + + /// + /// Gets a read only collection containing the LEDs of this device + /// + public ReadOnlyCollection Leds { get; private set; } + + /// + /// Gets a dictionary containing all the LEDs of this device with their corresponding RGB.NET as + /// key + /// + public ReadOnlyDictionary LedIds { get; private set; } + + /// + /// Gets a list of input identifiers associated with this device + /// + public List InputIdentifiers { get; } + + /// + /// Gets a list of input mappings configured on the device + /// + public Dictionary InputMappings { get; } + + /// + /// Gets a list containing the categories of this device + /// + public HashSet Categories { get; } + + /// + /// Gets or sets the X-position of the device + /// + public float X + { + get => DeviceEntity.X; + set + { + DeviceEntity.X = value; + OnPropertyChanged(nameof(X)); + } + } + + /// + /// Gets or sets the Y-position of the device + /// + public float Y + { + get => DeviceEntity.Y; + set + { + DeviceEntity.Y = value; + OnPropertyChanged(nameof(Y)); + } + } + + /// + /// Gets or sets the rotation of the device + /// + public float Rotation + { + get => DeviceEntity.Rotation; + set + { + DeviceEntity.Rotation = value; + OnPropertyChanged(nameof(Rotation)); + } + } + + /// + /// Gets or sets the scale of the device + /// + public float Scale + { + get => DeviceEntity.Scale; + set + { + DeviceEntity.Scale = value; + OnPropertyChanged(nameof(Scale)); + } + } + + /// + /// Gets or sets the Z-index of the device + /// + public int ZIndex + { + get => DeviceEntity.ZIndex; + set + { + DeviceEntity.ZIndex = value; + OnPropertyChanged(nameof(ZIndex)); + } + } + + /// + /// Gets or sets the scale of the red color component used for calibration + /// + public float RedScale + { + get => DeviceEntity.RedScale; + set + { + DeviceEntity.RedScale = value; + OnPropertyChanged(nameof(RedScale)); + } + } + + /// + /// Gets or sets the scale of the green color component used for calibration + /// + public float GreenScale + { + get => DeviceEntity.GreenScale; + set + { + DeviceEntity.GreenScale = value; + OnPropertyChanged(nameof(GreenScale)); + } + } + + /// + /// Gets or sets the scale of the blue color component used for calibration + /// + public float BlueScale + { + get => DeviceEntity.BlueScale; + set + { + DeviceEntity.BlueScale = value; + OnPropertyChanged(nameof(BlueScale)); + } + } + + /// + /// Gets a boolean indicating whether this devices is enabled or not + /// Note: To enable/disable a device use the methods provided by + /// + public bool IsEnabled + { + get => DeviceEntity.IsEnabled; + internal set + { + DeviceEntity.IsEnabled = value; + OnPropertyChanged(nameof(IsEnabled)); + } + } + + /// + /// Gets or sets the physical layout of the device e.g. ISO or ANSI. + /// Only applicable to keyboards + /// + public KeyboardLayoutType PhysicalLayout + { + get => (KeyboardLayoutType) DeviceEntity.PhysicalLayout; + set + { + DeviceEntity.PhysicalLayout = (int) value; + OnPropertyChanged(nameof(PhysicalLayout)); + } + } + + /// + /// Gets or sets a boolean indicating whether falling back to default layouts is enabled or not + /// + public bool DisableDefaultLayout + { + get => DeviceEntity.DisableDefaultLayout; + set + { + DeviceEntity.DisableDefaultLayout = value; + OnPropertyChanged(nameof(DisableDefaultLayout)); + } + } + + /// + /// Gets or sets the logical layout of the device e.g. DE, UK or US. + /// Only applicable to keyboards + /// + public string? LogicalLayout + { + get => DeviceEntity.LogicalLayout; + set + { + DeviceEntity.LogicalLayout = value; + OnPropertyChanged(nameof(LogicalLayout)); + } + } + + /// + /// Gets or sets the path of the custom layout to load when calling + /// for this device + /// + public string? CustomLayoutPath + { + get => DeviceEntity.CustomLayoutPath; + set + { + DeviceEntity.CustomLayoutPath = value; + OnPropertyChanged(nameof(CustomLayoutPath)); + } + } + + /// + /// Gets the layout of the device expanded with Artemis-specific data + /// + public ArtemisLayout? Layout { get; internal set; } + + internal DeviceEntity DeviceEntity { get; } + + /// + public override string ToString() + { + return $"[{RgbDevice.DeviceInfo.DeviceType}] {RgbDevice.DeviceInfo.DeviceName} - {X}.{Y}.{ZIndex}"; + } + + /// + /// Attempts to retrieve the that corresponds the provided RGB.NET + /// + /// The RGB.NET to find the corresponding for + /// + /// If , LEDs mapped to different LEDs + /// are taken into consideration + /// + /// If found, the corresponding ; otherwise . + public ArtemisLed? GetLed(Led led, bool applyInputMapping) + { + return GetLed(led.Id, applyInputMapping); + } + + /// + /// Attempts to retrieve the that corresponds the provided RGB.NET + /// + /// The RGB.NET to find the corresponding for + /// + /// If , LEDs mapped to different LEDs + /// are taken into consideration + /// + /// If found, the corresponding ; otherwise . + public ArtemisLed? GetLed(LedId ledId, bool applyInputMapping) + { + LedIds.TryGetValue(ledId, out ArtemisLed? artemisLed); + if (artemisLed == null) + return null; + + if (applyInputMapping && InputMappings.TryGetValue(artemisLed, out ArtemisLed? mappedLed)) + return mappedLed; + return artemisLed; + } + + /// + /// Occurs when the underlying RGB.NET device was updated + /// + public event EventHandler? DeviceUpdated; + + /// + /// Applies the default categories for this device to the list + /// + public void ApplyDefaultCategories() + { + switch (RgbDevice.DeviceInfo.DeviceType) + { + case RGBDeviceType.Keyboard: + case RGBDeviceType.Mouse: + case RGBDeviceType.Headset: + case RGBDeviceType.Mousepad: + case RGBDeviceType.HeadsetStand: + case RGBDeviceType.Keypad: + if (!Categories.Contains(DeviceCategory.Peripherals)) + Categories.Add(DeviceCategory.Peripherals); + break; + case RGBDeviceType.Mainboard: + case RGBDeviceType.GraphicsCard: + case RGBDeviceType.DRAM: + case RGBDeviceType.Fan: + case RGBDeviceType.LedStripe: + case RGBDeviceType.Cooler: + if (!Categories.Contains(DeviceCategory.Case)) + Categories.Add(DeviceCategory.Case); + break; + case RGBDeviceType.Speaker: + if (!Categories.Contains(DeviceCategory.Desk)) + Categories.Add(DeviceCategory.Desk); + break; + case RGBDeviceType.Monitor: + if (!Categories.Contains(DeviceCategory.Monitor)) + Categories.Add(DeviceCategory.Monitor); + break; + case RGBDeviceType.LedMatrix: + if (!Categories.Contains(DeviceCategory.Room)) + Categories.Add(DeviceCategory.Room); + break; + } + } + + /// + /// Invokes the event + /// + protected virtual void OnDeviceUpdated() + { + DeviceUpdated?.Invoke(this, EventArgs.Empty); + } + + /// + /// Applies the provided layout to the device + /// + /// The layout to apply + /// + /// A boolean indicating whether to add missing LEDs defined in the layout but missing on + /// the device + /// + /// + /// A boolean indicating whether to remove excess LEDs present in the device but missing + /// in the layout + /// + internal void ApplyLayout(ArtemisLayout layout, bool createMissingLeds, bool removeExcessiveLeds) + { + if (createMissingLeds && !DeviceProvider.CreateMissingLedsSupported) + throw new ArtemisCoreException($"Cannot apply layout with {nameof(createMissingLeds)} " + + "set to true because the device provider does not support it"); + if (removeExcessiveLeds && !DeviceProvider.RemoveExcessiveLedsSupported) + throw new ArtemisCoreException($"Cannot apply layout with {nameof(removeExcessiveLeds)} " + + "set to true because the device provider does not support it"); + + if (layout.IsValid) + layout.ApplyTo(RgbDevice, createMissingLeds, removeExcessiveLeds); + + + UpdateLeds(); + + Layout = layout; + Layout.ApplyDevice(this); + CalculateRenderProperties(); + OnDeviceUpdated(); + } + + internal void ApplyToEntity() + { + // Other properties are computed + DeviceEntity.Id = Identifier; + DeviceEntity.DeviceProvider = DeviceProvider.Plugin.Guid.ToString(); + + DeviceEntity.InputIdentifiers.Clear(); + foreach (ArtemisDeviceInputIdentifier identifier in InputIdentifiers) + { + DeviceEntity.InputIdentifiers.Add(new DeviceInputIdentifierEntity + { + InputProvider = identifier.InputProvider, + Identifier = identifier.Identifier + }); + } + + DeviceEntity.InputMappings.Clear(); + foreach ((ArtemisLed? original, ArtemisLed? mapped) in InputMappings) + DeviceEntity.InputMappings.Add(new InputMappingEntity {OriginalLedId = (int) original.RgbLed.Id, MappedLedId = (int) mapped.RgbLed.Id}); + + DeviceEntity.Categories.Clear(); + foreach (DeviceCategory deviceCategory in Categories) + DeviceEntity.Categories.Add((int) deviceCategory); + } + + internal void ApplyToRgbDevice() + { + RgbDevice.Rotation = DeviceEntity.Rotation; + RgbDevice.Scale = DeviceEntity.Scale; + + // Workaround for device rotation not applying + if (DeviceEntity.X == 0 && DeviceEntity.Y == 0) + RgbDevice.Location = new Point(1, 1); + RgbDevice.Location = new Point(DeviceEntity.X, DeviceEntity.Y); + + InputIdentifiers.Clear(); + foreach (DeviceInputIdentifierEntity identifierEntity in DeviceEntity.InputIdentifiers) + InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(identifierEntity.InputProvider, identifierEntity.Identifier)); + + if (!RgbDevice.ColorCorrections.Any()) + RgbDevice.ColorCorrections.Add(new ScaleColorCorrection(this)); + + Categories.Clear(); + foreach (int deviceEntityCategory in DeviceEntity.Categories) + Categories.Add((DeviceCategory) deviceEntityCategory); + if (!Categories.Any()) + ApplyDefaultCategories(); + + CalculateRenderProperties(); + OnDeviceUpdated(); + } + + internal void CalculateRenderProperties() + { + Rectangle = RgbDevice.Boundary.ToSKRect(); + if (!Leds.Any()) + return; + + foreach (ArtemisLed led in Leds) + led.CalculateRectangles(); + + SKPath path = new() {FillType = SKPathFillType.Winding}; + foreach (ArtemisLed artemisLed in Leds) + path.AddRect(artemisLed.AbsoluteRectangle); + + Path = path; + } + + private void UpdateLeds() + { + Leds = RgbDevice.Select(l => new ArtemisLed(l, this)).ToList().AsReadOnly(); + LedIds = new ReadOnlyDictionary(Leds.ToDictionary(l => l.RgbLed.Id, l => l)); + + InputMappings.Clear(); + foreach (InputMappingEntity deviceEntityInputMapping in DeviceEntity.InputMappings) + { + ArtemisLed? original = Leds.FirstOrDefault(l => l.RgbLed.Id == (LedId) deviceEntityInputMapping.OriginalLedId); + ArtemisLed? mapped = Leds.FirstOrDefault(l => l.RgbLed.Id == (LedId) deviceEntityInputMapping.MappedLedId); + if (original != null && mapped != null) + InputMappings.Add(original, mapped); + } + } + + private void ApplyKeyboardLayout() + { + if (RgbDevice.DeviceInfo.DeviceType != RGBDeviceType.Keyboard) + return; + + IKeyboard? keyboard = RgbDevice as IKeyboard; + // If supported, detect the device layout so that we can load the correct one + if (DeviceProvider.CanDetectPhysicalLayout && keyboard != null) + PhysicalLayout = (KeyboardLayoutType) keyboard.DeviceInfo.Layout; + else + PhysicalLayout = (KeyboardLayoutType) DeviceEntity.PhysicalLayout; + if (DeviceProvider.CanDetectLogicalLayout && keyboard != null) + LogicalLayout = DeviceProvider.GetLogicalLayout(keyboard); + else + LogicalLayout = DeviceEntity.LogicalLayout; + } +} + +/// +/// Represents a device category +/// +public enum DeviceCategory { /// - /// Represents an RGB device usable by Artemis, provided by a + /// A device used to light up (part of) the desk /// - public class ArtemisDevice : CorePropertyChanged - { - private SKPath? _path; - private SKRect _rectangle; - - internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider) - { - Identifier = rgbDevice.GetDeviceIdentifier(); - DeviceEntity = new DeviceEntity(); - RgbDevice = rgbDevice; - DeviceProvider = deviceProvider; - - Rotation = 0; - Scale = 1; - ZIndex = 1; - RedScale = 1; - GreenScale = 1; - BlueScale = 1; - IsEnabled = true; - - LedIds = new ReadOnlyDictionary(new Dictionary()); - Leds = new ReadOnlyCollection(new List()); - InputIdentifiers = new List(); - InputMappings = new Dictionary(); - Categories = new HashSet(); - - UpdateLeds(); - ApplyKeyboardLayout(); - ApplyToEntity(); - ApplyDefaultCategories(); - CalculateRenderProperties(); - } - - internal ArtemisDevice(IRGBDevice rgbDevice, DeviceProvider deviceProvider, DeviceEntity deviceEntity) - { - Identifier = rgbDevice.GetDeviceIdentifier(); - DeviceEntity = deviceEntity; - RgbDevice = rgbDevice; - DeviceProvider = deviceProvider; - - LedIds = new ReadOnlyDictionary(new Dictionary()); - Leds = new ReadOnlyCollection(new List()); - InputIdentifiers = new List(); - InputMappings = new Dictionary(); - Categories = new HashSet(); - - foreach (DeviceInputIdentifierEntity identifierEntity in DeviceEntity.InputIdentifiers) - InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(identifierEntity.InputProvider, identifierEntity.Identifier)); - - UpdateLeds(); - ApplyKeyboardLayout(); - } - - /// - /// Gets the (hopefully unique and persistent) ID of this device - /// - public string Identifier { get; } - - /// - /// Gets the rectangle covering the device - /// - public SKRect Rectangle - { - get => _rectangle; - private set => SetAndNotify(ref _rectangle, value); - } - - /// - /// Gets the path surrounding the device - /// - public SKPath? Path - { - get => _path; - private set => SetAndNotify(ref _path, value); - } - - /// - /// Gets the RGB.NET device backing this Artemis device - /// - public IRGBDevice RgbDevice { get; } - - /// - /// Gets the device type of the ArtemisDevice - /// - public RGBDeviceType DeviceType => RgbDevice.DeviceInfo.DeviceType; - - /// - /// Gets the device provider that provided this device - /// - public DeviceProvider DeviceProvider { get; } - - /// - /// Gets a read only collection containing the LEDs of this device - /// - public ReadOnlyCollection Leds { get; private set; } - - /// - /// Gets a dictionary containing all the LEDs of this device with their corresponding RGB.NET as - /// key - /// - public ReadOnlyDictionary LedIds { get; private set; } - - /// - /// Gets a list of input identifiers associated with this device - /// - public List InputIdentifiers { get; } - - /// - /// Gets a list of input mappings configured on the device - /// - public Dictionary InputMappings { get; } - - /// - /// Gets a list containing the categories of this device - /// - public HashSet Categories { get; } - - /// - /// Gets or sets the X-position of the device - /// - public float X - { - get => DeviceEntity.X; - set - { - DeviceEntity.X = value; - OnPropertyChanged(nameof(X)); - } - } - - /// - /// Gets or sets the Y-position of the device - /// - public float Y - { - get => DeviceEntity.Y; - set - { - DeviceEntity.Y = value; - OnPropertyChanged(nameof(Y)); - } - } - - /// - /// Gets or sets the rotation of the device - /// - public float Rotation - { - get => DeviceEntity.Rotation; - set - { - DeviceEntity.Rotation = value; - OnPropertyChanged(nameof(Rotation)); - } - } - - /// - /// Gets or sets the scale of the device - /// - public float Scale - { - get => DeviceEntity.Scale; - set - { - DeviceEntity.Scale = value; - OnPropertyChanged(nameof(Scale)); - } - } - - /// - /// Gets or sets the Z-index of the device - /// - public int ZIndex - { - get => DeviceEntity.ZIndex; - set - { - DeviceEntity.ZIndex = value; - OnPropertyChanged(nameof(ZIndex)); - } - } - - /// - /// Gets or sets the scale of the red color component used for calibration - /// - public float RedScale - { - get => DeviceEntity.RedScale; - set - { - DeviceEntity.RedScale = value; - OnPropertyChanged(nameof(RedScale)); - } - } - - /// - /// Gets or sets the scale of the green color component used for calibration - /// - public float GreenScale - { - get => DeviceEntity.GreenScale; - set - { - DeviceEntity.GreenScale = value; - OnPropertyChanged(nameof(GreenScale)); - } - } - - /// - /// Gets or sets the scale of the blue color component used for calibration - /// - public float BlueScale - { - get => DeviceEntity.BlueScale; - set - { - DeviceEntity.BlueScale = value; - OnPropertyChanged(nameof(BlueScale)); - } - } - - /// - /// Gets a boolean indicating whether this devices is enabled or not - /// Note: To enable/disable a device use the methods provided by - /// - public bool IsEnabled - { - get => DeviceEntity.IsEnabled; - internal set - { - DeviceEntity.IsEnabled = value; - OnPropertyChanged(nameof(IsEnabled)); - } - } - - /// - /// Gets or sets the physical layout of the device e.g. ISO or ANSI. - /// Only applicable to keyboards - /// - public KeyboardLayoutType PhysicalLayout - { - get => (KeyboardLayoutType) DeviceEntity.PhysicalLayout; - set - { - DeviceEntity.PhysicalLayout = (int) value; - OnPropertyChanged(nameof(PhysicalLayout)); - } - } - - /// - /// Gets or sets a boolean indicating whether falling back to default layouts is enabled or not - /// - public bool DisableDefaultLayout - { - get => DeviceEntity.DisableDefaultLayout; - set - { - DeviceEntity.DisableDefaultLayout = value; - OnPropertyChanged(nameof(DisableDefaultLayout)); - } - } - - /// - /// Gets or sets the logical layout of the device e.g. DE, UK or US. - /// Only applicable to keyboards - /// - public string? LogicalLayout - { - get => DeviceEntity.LogicalLayout; - set - { - DeviceEntity.LogicalLayout = value; - OnPropertyChanged(nameof(LogicalLayout)); - } - } - - /// - /// Gets or sets the path of the custom layout to load when calling - /// for this device - /// - public string? CustomLayoutPath - { - get => DeviceEntity.CustomLayoutPath; - set - { - DeviceEntity.CustomLayoutPath = value; - OnPropertyChanged(nameof(CustomLayoutPath)); - } - } - - /// - /// Gets the layout of the device expanded with Artemis-specific data - /// - public ArtemisLayout? Layout { get; internal set; } - - internal DeviceEntity DeviceEntity { get; } - - /// - public override string ToString() - { - return $"[{RgbDevice.DeviceInfo.DeviceType}] {RgbDevice.DeviceInfo.DeviceName} - {X}.{Y}.{ZIndex}"; - } - - /// - /// Attempts to retrieve the that corresponds the provided RGB.NET - /// - /// The RGB.NET to find the corresponding for - /// - /// If , LEDs mapped to different LEDs - /// are taken into consideration - /// - /// If found, the corresponding ; otherwise . - public ArtemisLed? GetLed(Led led, bool applyInputMapping) - { - return GetLed(led.Id, applyInputMapping); - } - - /// - /// Attempts to retrieve the that corresponds the provided RGB.NET - /// - /// The RGB.NET to find the corresponding for - /// - /// If , LEDs mapped to different LEDs - /// are taken into consideration - /// - /// If found, the corresponding ; otherwise . - public ArtemisLed? GetLed(LedId ledId, bool applyInputMapping) - { - LedIds.TryGetValue(ledId, out ArtemisLed? artemisLed); - if (artemisLed == null) - return null; - - if (applyInputMapping && InputMappings.TryGetValue(artemisLed, out ArtemisLed? mappedLed)) - return mappedLed; - return artemisLed; - } - - /// - /// Occurs when the underlying RGB.NET device was updated - /// - public event EventHandler? DeviceUpdated; - - /// - /// Applies the default categories for this device to the list - /// - public void ApplyDefaultCategories() - { - switch (RgbDevice.DeviceInfo.DeviceType) - { - case RGBDeviceType.Keyboard: - case RGBDeviceType.Mouse: - case RGBDeviceType.Headset: - case RGBDeviceType.Mousepad: - case RGBDeviceType.HeadsetStand: - case RGBDeviceType.Keypad: - if (!Categories.Contains(DeviceCategory.Peripherals)) - Categories.Add(DeviceCategory.Peripherals); - break; - case RGBDeviceType.Mainboard: - case RGBDeviceType.GraphicsCard: - case RGBDeviceType.DRAM: - case RGBDeviceType.Fan: - case RGBDeviceType.LedStripe: - case RGBDeviceType.Cooler: - if (!Categories.Contains(DeviceCategory.Case)) - Categories.Add(DeviceCategory.Case); - break; - case RGBDeviceType.Speaker: - if (!Categories.Contains(DeviceCategory.Desk)) - Categories.Add(DeviceCategory.Desk); - break; - case RGBDeviceType.Monitor: - if (!Categories.Contains(DeviceCategory.Monitor)) - Categories.Add(DeviceCategory.Monitor); - break; - case RGBDeviceType.LedMatrix: - if (!Categories.Contains(DeviceCategory.Room)) - Categories.Add(DeviceCategory.Room); - break; - } - } - - /// - /// Invokes the event - /// - protected virtual void OnDeviceUpdated() - { - DeviceUpdated?.Invoke(this, EventArgs.Empty); - } - - /// - /// Applies the provided layout to the device - /// - /// The layout to apply - /// - /// A boolean indicating whether to add missing LEDs defined in the layout but missing on - /// the device - /// - /// - /// A boolean indicating whether to remove excess LEDs present in the device but missing - /// in the layout - /// - internal void ApplyLayout(ArtemisLayout layout, bool createMissingLeds, bool removeExcessiveLeds) - { - if (createMissingLeds && !DeviceProvider.CreateMissingLedsSupported) - throw new ArtemisCoreException($"Cannot apply layout with {nameof(createMissingLeds)} " + - "set to true because the device provider does not support it"); - if (removeExcessiveLeds && !DeviceProvider.RemoveExcessiveLedsSupported) - throw new ArtemisCoreException($"Cannot apply layout with {nameof(removeExcessiveLeds)} " + - "set to true because the device provider does not support it"); - - if (layout.IsValid) - layout.ApplyTo(RgbDevice, createMissingLeds, removeExcessiveLeds); - - - UpdateLeds(); - - Layout = layout; - Layout.ApplyDevice(this); - CalculateRenderProperties(); - OnDeviceUpdated(); - } - - internal void ApplyToEntity() - { - // Other properties are computed - DeviceEntity.Id = Identifier; - DeviceEntity.DeviceProvider = DeviceProvider.Plugin.Guid.ToString(); - - DeviceEntity.InputIdentifiers.Clear(); - foreach (ArtemisDeviceInputIdentifier identifier in InputIdentifiers) - DeviceEntity.InputIdentifiers.Add(new DeviceInputIdentifierEntity - { - InputProvider = identifier.InputProvider, - Identifier = identifier.Identifier - }); - - DeviceEntity.InputMappings.Clear(); - foreach ((ArtemisLed? original, ArtemisLed? mapped) in InputMappings) - DeviceEntity.InputMappings.Add(new InputMappingEntity {OriginalLedId = (int) original.RgbLed.Id, MappedLedId = (int) mapped.RgbLed.Id}); - - DeviceEntity.Categories.Clear(); - foreach (DeviceCategory deviceCategory in Categories) - DeviceEntity.Categories.Add((int) deviceCategory); - } - - internal void ApplyToRgbDevice() - { - RgbDevice.Rotation = DeviceEntity.Rotation; - RgbDevice.Scale = DeviceEntity.Scale; - - // Workaround for device rotation not applying - if (DeviceEntity.X == 0 && DeviceEntity.Y == 0) - RgbDevice.Location = new Point(1, 1); - RgbDevice.Location = new Point(DeviceEntity.X, DeviceEntity.Y); - - InputIdentifiers.Clear(); - foreach (DeviceInputIdentifierEntity identifierEntity in DeviceEntity.InputIdentifiers) - InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(identifierEntity.InputProvider, identifierEntity.Identifier)); - - if (!RgbDevice.ColorCorrections.Any()) - RgbDevice.ColorCorrections.Add(new ScaleColorCorrection(this)); - - Categories.Clear(); - foreach (int deviceEntityCategory in DeviceEntity.Categories) - Categories.Add((DeviceCategory) deviceEntityCategory); - if (!Categories.Any()) - ApplyDefaultCategories(); - - CalculateRenderProperties(); - OnDeviceUpdated(); - } - - internal void CalculateRenderProperties() - { - Rectangle = RgbDevice.Boundary.ToSKRect(); - if (!Leds.Any()) - return; - - foreach (ArtemisLed led in Leds) - led.CalculateRectangles(); - - SKPath path = new() {FillType = SKPathFillType.Winding}; - foreach (ArtemisLed artemisLed in Leds) - path.AddRect(artemisLed.AbsoluteRectangle); - - Path = path; - } - - private void UpdateLeds() - { - Leds = RgbDevice.Select(l => new ArtemisLed(l, this)).ToList().AsReadOnly(); - LedIds = new ReadOnlyDictionary(Leds.ToDictionary(l => l.RgbLed.Id, l => l)); - - InputMappings.Clear(); - foreach (InputMappingEntity deviceEntityInputMapping in DeviceEntity.InputMappings) - { - ArtemisLed? original = Leds.FirstOrDefault(l => l.RgbLed.Id == (LedId) deviceEntityInputMapping.OriginalLedId); - ArtemisLed? mapped = Leds.FirstOrDefault(l => l.RgbLed.Id == (LedId) deviceEntityInputMapping.MappedLedId); - if (original != null && mapped != null) - InputMappings.Add(original, mapped); - } - } - - private void ApplyKeyboardLayout() - { - if (RgbDevice.DeviceInfo.DeviceType != RGBDeviceType.Keyboard) - return; - - IKeyboard? keyboard = RgbDevice as IKeyboard; - // If supported, detect the device layout so that we can load the correct one - if (DeviceProvider.CanDetectPhysicalLayout && keyboard != null) - PhysicalLayout = (KeyboardLayoutType) keyboard.DeviceInfo.Layout; - else - PhysicalLayout = (KeyboardLayoutType) DeviceEntity.PhysicalLayout; - if (DeviceProvider.CanDetectLogicalLayout && keyboard != null) - LogicalLayout = DeviceProvider.GetLogicalLayout(keyboard); - else - LogicalLayout = DeviceEntity.LogicalLayout; - } - } + Desk, /// - /// Represents a device category + /// A device attached or embedded into the monitor /// - public enum DeviceCategory - { - /// - /// A device used to light up (part of) the desk - /// - Desk, + Monitor, - /// - /// A device attached or embedded into the monitor - /// - Monitor, + /// + /// A device placed or embedded into the case + /// + Case, - /// - /// A device placed or embedded into the case - /// - Case, + /// + /// A device used to light up (part of) the room + /// + Room, - /// - /// A device used to light up (part of) the room - /// - Room, - - /// - /// A peripheral - /// - Peripherals - } + /// + /// A peripheral + /// + Peripherals } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/ArtemisDeviceInputIdentifier.cs b/src/Artemis.Core/Models/Surface/ArtemisDeviceInputIdentifier.cs index fb05e25b7..6d101cdf7 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisDeviceInputIdentifier.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisDeviceInputIdentifier.cs @@ -1,33 +1,32 @@ -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a device input identifier used by a specific to identify the +/// device +/// +public class ArtemisDeviceInputIdentifier { /// - /// Represents a device input identifier used by a specific to identify the - /// device + /// Creates a new instance of the class /// - public class ArtemisDeviceInputIdentifier + /// + /// The full type and namespace of the this identifier is + /// used by + /// + /// A value used to identify the device + internal ArtemisDeviceInputIdentifier(string inputProvider, object identifier) { - /// - /// Creates a new instance of the class - /// - /// - /// The full type and namespace of the this identifier is - /// used by - /// - /// A value used to identify the device - internal ArtemisDeviceInputIdentifier(string inputProvider, object identifier) - { - InputProvider = inputProvider; - Identifier = identifier; - } - - /// - /// Gets or sets the full type and namespace of the this identifier is used by - /// - public string InputProvider { get; set; } - - /// - /// Gets or sets a value used to identify the device - /// - public object Identifier { get; set; } + InputProvider = inputProvider; + Identifier = identifier; } + + /// + /// Gets or sets the full type and namespace of the this identifier is used by + /// + public string InputProvider { get; set; } + + /// + /// Gets or sets a value used to identify the device + /// + public object Identifier { get; set; } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/ArtemisLed.cs b/src/Artemis.Core/Models/Surface/ArtemisLed.cs index bfd9501ff..48d1be479 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisLed.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisLed.cs @@ -1,76 +1,75 @@ using RGB.NET.Core; using SkiaSharp; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents an RGB LED contained in an +/// +public class ArtemisLed : CorePropertyChanged { - /// - /// Represents an RGB LED contained in an - /// - public class ArtemisLed : CorePropertyChanged + private SKRect _absoluteRectangle; + private SKRect _rectangle; + + internal ArtemisLed(Led led, ArtemisDevice device) { - private SKRect _absoluteRectangle; - private SKRect _rectangle; + RgbLed = led; + Device = device; + CalculateRectangles(); + } - internal ArtemisLed(Led led, ArtemisDevice device) - { - RgbLed = led; - Device = device; - CalculateRectangles(); - } + /// + /// Gets the RGB.NET LED backing this Artemis LED + /// + public Led RgbLed { get; } - /// - /// Gets the RGB.NET LED backing this Artemis LED - /// - public Led RgbLed { get; } + /// + /// Gets the device that contains this LED + /// + public ArtemisDevice Device { get; } - /// - /// Gets the device that contains this LED - /// - public ArtemisDevice Device { get; } + /// + /// Gets the rectangle covering the LED positioned relative to the + /// + public SKRect Rectangle + { + get => _rectangle; + private set => SetAndNotify(ref _rectangle, value); + } - /// - /// Gets the rectangle covering the LED positioned relative to the - /// - public SKRect Rectangle - { - get => _rectangle; - private set => SetAndNotify(ref _rectangle, value); - } + /// + /// Gets the rectangle covering the LED + /// + public SKRect AbsoluteRectangle + { + get => _absoluteRectangle; + private set => SetAndNotify(ref _absoluteRectangle, value); + } - /// - /// Gets the rectangle covering the LED - /// - public SKRect AbsoluteRectangle - { - get => _absoluteRectangle; - private set => SetAndNotify(ref _absoluteRectangle, value); - } + /// + /// Gets the layout applied to this LED + /// + public ArtemisLedLayout? Layout { get; internal set; } - /// - /// Gets the layout applied to this LED - /// - public ArtemisLedLayout? Layout { get; internal set; } + /// + public override string ToString() + { + return RgbLed.ToString(); + } - /// - public override string ToString() - { - return RgbLed.ToString(); - } - - internal void CalculateRectangles() - { - Rectangle = Utilities.CreateScaleCompatibleRect( - RgbLed.Boundary.Location.X, - RgbLed.Boundary.Location.Y, - RgbLed.Boundary.Size.Width, - RgbLed.Boundary.Size.Height - ); - AbsoluteRectangle = Utilities.CreateScaleCompatibleRect( - RgbLed.AbsoluteBoundary.Location.X, - RgbLed.AbsoluteBoundary.Location.Y, - RgbLed.AbsoluteBoundary.Size.Width, - RgbLed.AbsoluteBoundary.Size.Height - ); - } + internal void CalculateRectangles() + { + Rectangle = Utilities.CreateScaleCompatibleRect( + RgbLed.Boundary.Location.X, + RgbLed.Boundary.Location.Y, + RgbLed.Boundary.Size.Width, + RgbLed.Boundary.Size.Height + ); + AbsoluteRectangle = Utilities.CreateScaleCompatibleRect( + RgbLed.AbsoluteBoundary.Location.X, + RgbLed.AbsoluteBoundary.Location.Y, + RgbLed.AbsoluteBoundary.Size.Width, + RgbLed.AbsoluteBoundary.Size.Height + ); } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/KeyboardLayoutType.cs b/src/Artemis.Core/Models/Surface/KeyboardLayoutType.cs index 43cd2597f..438879077 100644 --- a/src/Artemis.Core/Models/Surface/KeyboardLayoutType.cs +++ b/src/Artemis.Core/Models/Surface/KeyboardLayoutType.cs @@ -1,41 +1,40 @@ // ReSharper disable InconsistentNaming -namespace Artemis.Core +namespace Artemis.Core; + +// Copied from RGB.NET to avoid needing to reference RGB.NET +/// +/// Represents a physical layout type for a keyboard +/// +public enum KeyboardLayoutType { - // Copied from RGB.NET to avoid needing to reference RGB.NET /// - /// Represents a physical layout type for a keyboard + /// An unknown layout type /// - public enum KeyboardLayoutType - { - /// - /// An unknown layout type - /// - Unknown = 0, + Unknown = 0, - /// - /// The ANSI layout type, often used in the US (uses a short enter) - /// - ANSI = 1, + /// + /// The ANSI layout type, often used in the US (uses a short enter) + /// + ANSI = 1, - /// - /// The ISO layout type, often used in the EU (uses a tall enter) - /// - ISO = 2, + /// + /// The ISO layout type, often used in the EU (uses a tall enter) + /// + ISO = 2, - /// - /// The JIS layout type, often used in Japan (based on ISO) - /// - JIS = 3, + /// + /// The JIS layout type, often used in Japan (based on ISO) + /// + JIS = 3, - /// - /// The ABNT layout type, often used in Brazil/Portugal (based on ISO) - /// - ABNT = 4, + /// + /// The ABNT layout type, often used in Brazil/Portugal (based on ISO) + /// + ABNT = 4, - /// - /// The KS layout type, often used in South Korea - /// - KS = 5 - } + /// + /// The KS layout type, often used in South Korea + /// + KS = 5 } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs b/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs index 245b9eeb1..a931dcb0a 100644 --- a/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs +++ b/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs @@ -5,244 +5,243 @@ using System.Linq; using RGB.NET.Core; using RGB.NET.Layout; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a device layout decorated with extra Artemis-specific data +/// +public class ArtemisLayout { /// - /// Represents a device layout decorated with extra Artemis-specific data + /// Creates a new instance of the class /// - public class ArtemisLayout + /// The path of the layout XML file + /// The source from where this layout is being loaded + public ArtemisLayout(string filePath, LayoutSource source) { - /// - /// Creates a new instance of the class - /// - /// The path of the layout XML file - /// The source from where this layout is being loaded - public ArtemisLayout(string filePath, LayoutSource source) - { - FilePath = filePath; - Source = source; - Leds = new List(); + FilePath = filePath; + Source = source; + Leds = new List(); - LoadLayout(); - } - - /// - /// Gets the file path the layout was (attempted to be) loaded from - /// - public string FilePath { get; } - - /// - /// Gets the source from where this layout was loaded - /// - public LayoutSource Source { get; } - - /// - /// Gets the device this layout is applied to - /// - public ArtemisDevice? Device { get; private set; } - - /// - /// Gets a boolean indicating whether a valid layout was loaded - /// - public bool IsValid { get; private set; } - - /// - /// Gets the image of the device - /// - public Uri? Image { get; private set; } - - /// - /// Gets a list of LEDs this layout contains - /// - public List Leds { get; } - - /// - /// Gets the RGB.NET device layout - /// - public DeviceLayout RgbLayout { get; private set; } = null!; - - /// - /// Gets the custom layout data embedded in the RGB.NET layout - /// - public LayoutCustomDeviceData LayoutCustomDeviceData { get; private set; } = null!; - - /// - /// Applies the layout to the provided device - /// - public void ApplyTo(IRGBDevice device, bool createMissingLeds = false, bool removeExcessiveLeds = false) - { - device.Size = new Size(MathF.Round(RgbLayout.Width), MathF.Round(RgbLayout.Height)); - device.DeviceInfo.LayoutMetadata = RgbLayout.CustomData; - - HashSet ledIds = new(); - foreach (ILedLayout layoutLed in RgbLayout.Leds) - { - if (Enum.TryParse(layoutLed.Id, true, out LedId ledId)) - { - ledIds.Add(ledId); - - Led? led = device[ledId]; - if (led == null && createMissingLeds) - led = device.AddLed(ledId, new Point(), new Size()); - - if (led != null) - { - led.Location = new Point(layoutLed.X, layoutLed.Y); - led.Size = new Size(layoutLed.Width, layoutLed.Height); - led.Shape = layoutLed.Shape; - led.ShapeData = layoutLed.ShapeData; - led.LayoutMetadata = layoutLed.CustomData; - } - } - } - - if (removeExcessiveLeds) - { - List ledsToRemove = device.Select(led => led.Id).Where(id => !ledIds.Contains(id)).ToList(); - foreach (LedId led in ledsToRemove) - device.RemoveLed(led); - } - - List deviceLeds = device.ToList(); - foreach (Led led in deviceLeds) - { - float x = led.Location.X; - float y = led.Location.Y; - - // Try to move the LED if it falls outside the boundaries of the layout - if (led.Location.X + led.Size.Width > device.Size.Width) - x -= led.Location.X + led.Size.Width - device.Size.Width; - - if (led.Location.Y + led.Size.Height > device.Size.Height) - y -= led.Location.Y + led.Size.Height - device.Size.Height; - - // If not possible because it's too large we'll have to drop it to avoid rendering issues - if (x < 0 || y < 0) - device.RemoveLed(led.Id); - else - led.Location = new Point(x, y); - } - } - - internal void ApplyDevice(ArtemisDevice artemisDevice) - { - Device = artemisDevice; - foreach (ArtemisLedLayout artemisLedLayout in Leds) - artemisLedLayout.ApplyDevice(Device); - } - - private void LoadLayout() - { - DeviceLayout? deviceLayout = DeviceLayout.Load(FilePath, typeof(LayoutCustomDeviceData), typeof(LayoutCustomLedData)); - if (deviceLayout != null) - { - RgbLayout = deviceLayout; - IsValid = true; - } - else - { - RgbLayout = new DeviceLayout(); - IsValid = false; - } - - if (IsValid) - Leds.AddRange(RgbLayout.Leds.Select(l => new ArtemisLedLayout(this, l))); - - LayoutCustomDeviceData = (LayoutCustomDeviceData?) RgbLayout.CustomData ?? new LayoutCustomDeviceData(); - ApplyCustomDeviceData(); - } - - private void ApplyCustomDeviceData() - { - if (!IsValid) - { - Image = null; - return; - } - - Uri layoutDirectory = new(Path.GetDirectoryName(FilePath)! + "/", UriKind.Absolute); - if (LayoutCustomDeviceData.DeviceImage != null) - Image = new Uri(layoutDirectory, new Uri(LayoutCustomDeviceData.DeviceImage, UriKind.Relative)); - else - Image = null; - } - - internal static ArtemisLayout? GetDefaultLayout(ArtemisDevice device) - { - string layoutFolder = Path.Combine(Constants.ApplicationFolder, "DefaultLayouts", "Artemis"); - if (device.DeviceType == RGBDeviceType.Keyboard) - { - // XL layout is defined by its programmable macro keys - if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_Programmable1 && l.RgbLed.Id <= LedId.Keyboard_Programmable32)) - { - if (device.PhysicalLayout == KeyboardLayoutType.ANSI) - return new ArtemisLayout(Path.Combine(layoutFolder, "Keyboard", "Artemis XL keyboard-ANSI.xml"), LayoutSource.Default); - return new ArtemisLayout(Path.Combine(layoutFolder, "Keyboard", "Artemis XL keyboard-ISO.xml"), LayoutSource.Default); - } - - // L layout is defined by its numpad - if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_NumLock && l.RgbLed.Id <= LedId.Keyboard_NumPeriodAndDelete)) - { - if (device.PhysicalLayout == KeyboardLayoutType.ANSI) - return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard","Artemis L keyboard-ANSI.xml"), LayoutSource.Default); - return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard","Artemis L keyboard-ISO.xml"), LayoutSource.Default); - } - - // No numpad will result in TKL - if (device.PhysicalLayout == KeyboardLayoutType.ANSI) - return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard","Artemis TKL keyboard-ANSI.xml"), LayoutSource.Default); - return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard","Artemis TKL keyboard-ISO.xml"), LayoutSource.Default); - } - - // if (device.DeviceType == RGBDeviceType.Mouse) - // { - // if (device.Leds.Count == 1) - // { - // if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo)) - // return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "1 LED mouse logo.xml"), LayoutSource.Default); - // return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "1 LED mouse.xml"), LayoutSource.Default); - // } - // if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo)) - // return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "4 LED mouse logo.xml"), LayoutSource.Default); - // return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "4 LED mouse.xml"), LayoutSource.Default); - // } - - if (device.DeviceType == RGBDeviceType.Headset) - { - if (device.Leds.Count == 1) - return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 1 LED headset.xml"), LayoutSource.Default); - if (device.Leds.Count == 2) - return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 2 LED headset.xml"), LayoutSource.Default); - return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 4 LED headset.xml"), LayoutSource.Default); - } - - return null; - } + LoadLayout(); } /// - /// Represents a source from where a layout came + /// Gets the file path the layout was (attempted to be) loaded from /// - public enum LayoutSource + public string FilePath { get; } + + /// + /// Gets the source from where this layout was loaded + /// + public LayoutSource Source { get; } + + /// + /// Gets the device this layout is applied to + /// + public ArtemisDevice? Device { get; private set; } + + /// + /// Gets a boolean indicating whether a valid layout was loaded + /// + public bool IsValid { get; private set; } + + /// + /// Gets the image of the device + /// + public Uri? Image { get; private set; } + + /// + /// Gets a list of LEDs this layout contains + /// + public List Leds { get; } + + /// + /// Gets the RGB.NET device layout + /// + public DeviceLayout RgbLayout { get; private set; } = null!; + + /// + /// Gets the custom layout data embedded in the RGB.NET layout + /// + public LayoutCustomDeviceData LayoutCustomDeviceData { get; private set; } = null!; + + /// + /// Applies the layout to the provided device + /// + public void ApplyTo(IRGBDevice device, bool createMissingLeds = false, bool removeExcessiveLeds = false) { - /// - /// A layout loaded from config - /// - Configured, + device.Size = new Size(MathF.Round(RgbLayout.Width), MathF.Round(RgbLayout.Height)); + device.DeviceInfo.LayoutMetadata = RgbLayout.CustomData; - /// - /// A layout loaded from the user layout folder - /// - User, + HashSet ledIds = new(); + foreach (ILedLayout layoutLed in RgbLayout.Leds) + { + if (Enum.TryParse(layoutLed.Id, true, out LedId ledId)) + { + ledIds.Add(ledId); - /// - /// A layout loaded from the plugin folder - /// - Plugin, + Led? led = device[ledId]; + if (led == null && createMissingLeds) + led = device.AddLed(ledId, new Point(), new Size()); - /// - /// A default layout loaded as a fallback option - /// - Default + if (led != null) + { + led.Location = new Point(layoutLed.X, layoutLed.Y); + led.Size = new Size(layoutLed.Width, layoutLed.Height); + led.Shape = layoutLed.Shape; + led.ShapeData = layoutLed.ShapeData; + led.LayoutMetadata = layoutLed.CustomData; + } + } + } + + if (removeExcessiveLeds) + { + List ledsToRemove = device.Select(led => led.Id).Where(id => !ledIds.Contains(id)).ToList(); + foreach (LedId led in ledsToRemove) + device.RemoveLed(led); + } + + List deviceLeds = device.ToList(); + foreach (Led led in deviceLeds) + { + float x = led.Location.X; + float y = led.Location.Y; + + // Try to move the LED if it falls outside the boundaries of the layout + if (led.Location.X + led.Size.Width > device.Size.Width) + x -= led.Location.X + led.Size.Width - device.Size.Width; + + if (led.Location.Y + led.Size.Height > device.Size.Height) + y -= led.Location.Y + led.Size.Height - device.Size.Height; + + // If not possible because it's too large we'll have to drop it to avoid rendering issues + if (x < 0 || y < 0) + device.RemoveLed(led.Id); + else + led.Location = new Point(x, y); + } } + + internal void ApplyDevice(ArtemisDevice artemisDevice) + { + Device = artemisDevice; + foreach (ArtemisLedLayout artemisLedLayout in Leds) + artemisLedLayout.ApplyDevice(Device); + } + + internal static ArtemisLayout? GetDefaultLayout(ArtemisDevice device) + { + string layoutFolder = Path.Combine(Constants.ApplicationFolder, "DefaultLayouts", "Artemis"); + if (device.DeviceType == RGBDeviceType.Keyboard) + { + // XL layout is defined by its programmable macro keys + if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_Programmable1 && l.RgbLed.Id <= LedId.Keyboard_Programmable32)) + { + if (device.PhysicalLayout == KeyboardLayoutType.ANSI) + return new ArtemisLayout(Path.Combine(layoutFolder, "Keyboard", "Artemis XL keyboard-ANSI.xml"), LayoutSource.Default); + return new ArtemisLayout(Path.Combine(layoutFolder, "Keyboard", "Artemis XL keyboard-ISO.xml"), LayoutSource.Default); + } + + // L layout is defined by its numpad + if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_NumLock && l.RgbLed.Id <= LedId.Keyboard_NumPeriodAndDelete)) + { + if (device.PhysicalLayout == KeyboardLayoutType.ANSI) + return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis L keyboard-ANSI.xml"), LayoutSource.Default); + return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis L keyboard-ISO.xml"), LayoutSource.Default); + } + + // No numpad will result in TKL + if (device.PhysicalLayout == KeyboardLayoutType.ANSI) + return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis TKL keyboard-ANSI.xml"), LayoutSource.Default); + return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis TKL keyboard-ISO.xml"), LayoutSource.Default); + } + + // if (device.DeviceType == RGBDeviceType.Mouse) + // { + // if (device.Leds.Count == 1) + // { + // if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo)) + // return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "1 LED mouse logo.xml"), LayoutSource.Default); + // return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "1 LED mouse.xml"), LayoutSource.Default); + // } + // if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo)) + // return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "4 LED mouse logo.xml"), LayoutSource.Default); + // return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "4 LED mouse.xml"), LayoutSource.Default); + // } + + if (device.DeviceType == RGBDeviceType.Headset) + { + if (device.Leds.Count == 1) + return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 1 LED headset.xml"), LayoutSource.Default); + if (device.Leds.Count == 2) + return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 2 LED headset.xml"), LayoutSource.Default); + return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 4 LED headset.xml"), LayoutSource.Default); + } + + return null; + } + + private void LoadLayout() + { + DeviceLayout? deviceLayout = DeviceLayout.Load(FilePath, typeof(LayoutCustomDeviceData), typeof(LayoutCustomLedData)); + if (deviceLayout != null) + { + RgbLayout = deviceLayout; + IsValid = true; + } + else + { + RgbLayout = new DeviceLayout(); + IsValid = false; + } + + if (IsValid) + Leds.AddRange(RgbLayout.Leds.Select(l => new ArtemisLedLayout(this, l))); + + LayoutCustomDeviceData = (LayoutCustomDeviceData?) RgbLayout.CustomData ?? new LayoutCustomDeviceData(); + ApplyCustomDeviceData(); + } + + private void ApplyCustomDeviceData() + { + if (!IsValid) + { + Image = null; + return; + } + + Uri layoutDirectory = new(Path.GetDirectoryName(FilePath)! + "/", UriKind.Absolute); + if (LayoutCustomDeviceData.DeviceImage != null) + Image = new Uri(layoutDirectory, new Uri(LayoutCustomDeviceData.DeviceImage, UriKind.Relative)); + else + Image = null; + } +} + +/// +/// Represents a source from where a layout came +/// +public enum LayoutSource +{ + /// + /// A layout loaded from config + /// + Configured, + + /// + /// A layout loaded from the user layout folder + /// + User, + + /// + /// A layout loaded from the plugin folder + /// + Plugin, + + /// + /// A default layout loaded as a fallback option + /// + Default } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs b/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs index 01fcce12d..1baca1d95 100644 --- a/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs +++ b/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs @@ -3,72 +3,71 @@ using System.IO; using System.Linq; using RGB.NET.Layout; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a LED layout decorated with extra Artemis-specific data +/// +public class ArtemisLedLayout { - /// - /// Represents a LED layout decorated with extra Artemis-specific data - /// - public class ArtemisLedLayout + internal ArtemisLedLayout(ArtemisLayout deviceLayout, ILedLayout led) { - internal ArtemisLedLayout(ArtemisLayout deviceLayout, ILedLayout led) - { - DeviceLayout = deviceLayout; - RgbLayout = led; - LayoutCustomLedData = (LayoutCustomLedData?) led.CustomData ?? new LayoutCustomLedData(); - } + DeviceLayout = deviceLayout; + RgbLayout = led; + LayoutCustomLedData = (LayoutCustomLedData?) led.CustomData ?? new LayoutCustomLedData(); + } - /// - /// Gets the device layout of this LED layout - /// - public ArtemisLayout DeviceLayout { get; } + /// + /// Gets the device layout of this LED layout + /// + public ArtemisLayout DeviceLayout { get; } - /// - /// Gets the RGB.NET LED Layout of this LED layout - /// - public ILedLayout RgbLayout { get; } + /// + /// Gets the RGB.NET LED Layout of this LED layout + /// + public ILedLayout RgbLayout { get; } - /// - /// Gets the LED this layout is applied to - /// - public ArtemisLed? Led { get; protected set; } + /// + /// Gets the LED this layout is applied to + /// + public ArtemisLed? Led { get; protected set; } - /// - /// Gets the name of the logical layout this LED belongs to - /// - public string? LogicalName { get; private set; } + /// + /// Gets the name of the logical layout this LED belongs to + /// + public string? LogicalName { get; private set; } - /// - /// Gets the image of the LED - /// - public Uri? Image { get; private set; } + /// + /// Gets the image of the LED + /// + public Uri? Image { get; private set; } - /// - /// Gets the custom layout data embedded in the RGB.NET layout - /// - public LayoutCustomLedData LayoutCustomLedData { get; } + /// + /// Gets the custom layout data embedded in the RGB.NET layout + /// + public LayoutCustomLedData LayoutCustomLedData { get; } - internal void ApplyDevice(ArtemisDevice device) - { - Led = device.Leds.FirstOrDefault(d => d.RgbLed.Id.ToString() == RgbLayout.Id); - if (Led != null) - Led.Layout = this; + internal void ApplyDevice(ArtemisDevice device) + { + Led = device.Leds.FirstOrDefault(d => d.RgbLed.Id.ToString() == RgbLayout.Id); + if (Led != null) + Led.Layout = this; - ApplyCustomLedData(device); - } + ApplyCustomLedData(device); + } - private void ApplyCustomLedData(ArtemisDevice artemisDevice) - { - if (LayoutCustomLedData.LogicalLayouts == null || !LayoutCustomLedData.LogicalLayouts.Any()) - return; + private void ApplyCustomLedData(ArtemisDevice artemisDevice) + { + if (LayoutCustomLedData.LogicalLayouts == null || !LayoutCustomLedData.LogicalLayouts.Any()) + return; - // Prefer a matching layout or else a default layout (that has no name) - LayoutCustomLedDataLogicalLayout logicalLayout = LayoutCustomLedData.LogicalLayouts - .OrderBy(l => l.Name == artemisDevice.LogicalLayout) - .ThenBy(l => l.Name == null) - .First(); + // Prefer a matching layout or else a default layout (that has no name) + LayoutCustomLedDataLogicalLayout logicalLayout = LayoutCustomLedData.LogicalLayouts + .OrderBy(l => l.Name == artemisDevice.LogicalLayout) + .ThenBy(l => l.Name == null) + .First(); - LogicalName = logicalLayout.Name; - Image = new Uri(Path.Combine(Path.GetDirectoryName(DeviceLayout.FilePath)!, logicalLayout.Image!), UriKind.Absolute); - } + LogicalName = logicalLayout.Name; + Image = new Uri(Path.Combine(Path.GetDirectoryName(DeviceLayout.FilePath)!, logicalLayout.Image!), UriKind.Absolute); } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/Layout/LayoutCustomDeviceData.cs b/src/Artemis.Core/Models/Surface/Layout/LayoutCustomDeviceData.cs index 56144ca56..12aa12b71 100644 --- a/src/Artemis.Core/Models/Surface/Layout/LayoutCustomDeviceData.cs +++ b/src/Artemis.Core/Models/Surface/Layout/LayoutCustomDeviceData.cs @@ -1,15 +1,15 @@ using System.Xml.Serialization; + #pragma warning disable 1591 -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents extra Artemis-specific information stored in RGB.NET layouts +/// +[XmlRoot("CustomData")] +public class LayoutCustomDeviceData { - /// - /// Represents extra Artemis-specific information stored in RGB.NET layouts - /// - [XmlRoot("CustomData")] - public class LayoutCustomDeviceData - { - [XmlElement("DeviceImage")] - public string? DeviceImage { get; set; } - } + [XmlElement("DeviceImage")] + public string? DeviceImage { get; set; } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/Layout/LayoutCustomLedData.cs b/src/Artemis.Core/Models/Surface/Layout/LayoutCustomLedData.cs index 62e029e53..ac440c8e6 100644 --- a/src/Artemis.Core/Models/Surface/Layout/LayoutCustomLedData.cs +++ b/src/Artemis.Core/Models/Surface/Layout/LayoutCustomLedData.cs @@ -1,29 +1,29 @@ using System.Collections.Generic; using System.Xml.Serialization; + #pragma warning disable 1591 -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents extra Artemis-specific information stored in RGB.NET layouts +/// +[XmlRoot("CustomData")] +public class LayoutCustomLedData { - /// - /// Represents extra Artemis-specific information stored in RGB.NET layouts - /// - [XmlRoot("CustomData")] - public class LayoutCustomLedData - { - [XmlArray("LogicalLayouts")] - public List? LogicalLayouts { get; set; } - } + [XmlArray("LogicalLayouts")] + public List? LogicalLayouts { get; set; } +} - /// - /// Represents extra Artemis-specific information stored in RGB.NET layouts - /// - [XmlType("LogicalLayout")] - public class LayoutCustomLedDataLogicalLayout - { - [XmlAttribute("Name")] - public string? Name { get; set; } +/// +/// Represents extra Artemis-specific information stored in RGB.NET layouts +/// +[XmlType("LogicalLayout")] +public class LayoutCustomLedDataLogicalLayout +{ + [XmlAttribute("Name")] + public string? Name { get; set; } - [XmlAttribute("Image")] - public string? Image { get; set; } - } + [XmlAttribute("Image")] + public string? Image { get; set; } } \ No newline at end of file diff --git a/src/Artemis.Core/Ninject/CoreModule.cs b/src/Artemis.Core/Ninject/CoreModule.cs index 5c572b62c..14d1c08dc 100644 --- a/src/Artemis.Core/Ninject/CoreModule.cs +++ b/src/Artemis.Core/Ninject/CoreModule.cs @@ -9,76 +9,75 @@ using Ninject.Modules; using Ninject.Planning.Bindings.Resolvers; using Serilog; -namespace Artemis.Core.Ninject +namespace Artemis.Core.Ninject; + +/// +/// The main of the Artemis Core that binds all services +/// +public class CoreModule : NinjectModule { - /// - /// The main of the Artemis Core that binds all services - /// - public class CoreModule : NinjectModule + /// + public override void Load() { - /// - public override void Load() + if (Kernel == null) + throw new ArtemisCoreException("Failed to bind Ninject Core module, kernel is null."); + + Kernel.Components.Remove(); + + // Bind all services as singletons + Kernel.Bind(x => { - if (Kernel == null) - throw new ArtemisCoreException("Failed to bind Ninject Core module, kernel is null."); + x.FromThisAssembly() + .IncludingNonPublicTypes() + .SelectAllClasses() + .InheritedFrom() + .BindAllInterfaces() + .Configure(c => c.InSingletonScope()); + }); - Kernel.Components.Remove(); - - // Bind all services as singletons - Kernel.Bind(x => - { - x.FromThisAssembly() - .IncludingNonPublicTypes() - .SelectAllClasses() - .InheritedFrom() - .BindAllInterfaces() - .Configure(c => c.InSingletonScope()); - }); - - // Bind all protected services as singletons - Kernel.Bind(x => - { - x.FromThisAssembly() - .IncludingNonPublicTypes() - .SelectAllClasses() - .InheritedFrom() - .BindAllInterfaces() - .Configure(c => c.When(HasAccessToProtectedService).InSingletonScope()); - }); - - Kernel.Bind().ToMethod(_ => StorageManager.CreateRepository(Constants.DataFolder)).InSingletonScope(); - Kernel.Bind().ToSelf().InSingletonScope(); - - // Bind all migrations as singletons - Kernel.Bind(x => - { - x.FromAssemblyContaining() - .IncludingNonPublicTypes() - .SelectAllClasses() - .InheritedFrom() - .BindAllInterfaces() - .Configure(c => c.InSingletonScope()); - }); - - // Bind all repositories as singletons - Kernel.Bind(x => - { - x.FromAssemblyContaining() - .IncludingNonPublicTypes() - .SelectAllClasses() - .InheritedFrom() - .BindAllInterfaces() - .Configure(c => c.InSingletonScope()); - }); - - Kernel.Bind().ToProvider(); - Kernel.Bind().ToProvider(); - Kernel.Bind().ToSelf(); - } - - private bool HasAccessToProtectedService(IRequest r) + // Bind all protected services as singletons + Kernel.Bind(x => { - return r.ParentRequest != null && !r.ParentRequest.Service.Assembly.Location.StartsWith(Constants.PluginsFolder); - } + x.FromThisAssembly() + .IncludingNonPublicTypes() + .SelectAllClasses() + .InheritedFrom() + .BindAllInterfaces() + .Configure(c => c.When(HasAccessToProtectedService).InSingletonScope()); + }); + + Kernel.Bind().ToMethod(_ => StorageManager.CreateRepository(Constants.DataFolder)).InSingletonScope(); + Kernel.Bind().ToSelf().InSingletonScope(); + + // Bind all migrations as singletons + Kernel.Bind(x => + { + x.FromAssemblyContaining() + .IncludingNonPublicTypes() + .SelectAllClasses() + .InheritedFrom() + .BindAllInterfaces() + .Configure(c => c.InSingletonScope()); + }); + + // Bind all repositories as singletons + Kernel.Bind(x => + { + x.FromAssemblyContaining() + .IncludingNonPublicTypes() + .SelectAllClasses() + .InheritedFrom() + .BindAllInterfaces() + .Configure(c => c.InSingletonScope()); + }); + + Kernel.Bind().ToProvider(); + Kernel.Bind().ToProvider(); + Kernel.Bind().ToSelf(); + } + + private bool HasAccessToProtectedService(IRequest r) + { + return r.ParentRequest != null && !r.ParentRequest.Service.Assembly.Location.StartsWith(Constants.PluginsFolder); } } \ No newline at end of file diff --git a/src/Artemis.Core/Ninject/LoggerProvider.cs b/src/Artemis.Core/Ninject/LoggerProvider.cs index 0f76c85a2..5759557c6 100644 --- a/src/Artemis.Core/Ninject/LoggerProvider.cs +++ b/src/Artemis.Core/Ninject/LoggerProvider.cs @@ -5,40 +5,39 @@ using Serilog; using Serilog.Core; using Serilog.Events; -namespace Artemis.Core.Ninject +namespace Artemis.Core.Ninject; + +internal class LoggerProvider : Provider { - internal class LoggerProvider : Provider - { - internal static readonly LoggingLevelSwitch LoggingLevelSwitch = new(LogEventLevel.Verbose); + internal static readonly LoggingLevelSwitch LoggingLevelSwitch = new(LogEventLevel.Verbose); - private static readonly ILogger Logger = new LoggerConfiguration() - .Enrich.FromLogContext() - .WriteTo.File(Path.Combine(Constants.LogsFolder, "Artemis log-.log"), - rollingInterval: RollingInterval.Day, - outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}") - .WriteTo.Console() + private static readonly ILogger Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.File(Path.Combine(Constants.LogsFolder, "Artemis log-.log"), + rollingInterval: RollingInterval.Day, + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}") + .WriteTo.Console() #if DEBUG - .WriteTo.Debug() + .WriteTo.Debug() #endif - .WriteTo.Sink() - .MinimumLevel.ControlledBy(LoggingLevelSwitch) - .CreateLogger(); + .WriteTo.Sink() + .MinimumLevel.ControlledBy(LoggingLevelSwitch) + .CreateLogger(); - protected override ILogger CreateInstance(IContext context) - { - Type? requestingType = context.Request.ParentContext?.Plan?.Type; - if (requestingType != null) - return Logger.ForContext(requestingType); - return Logger; - } - } - - internal class ArtemisSink : ILogEventSink + protected override ILogger CreateInstance(IContext context) { - /// - public void Emit(LogEvent logEvent) - { - LogStore.Emit(logEvent); - } + Type? requestingType = context.Request.ParentContext?.Plan?.Type; + if (requestingType != null) + return Logger.ForContext(requestingType); + return Logger; + } +} + +internal class ArtemisSink : ILogEventSink +{ + /// + public void Emit(LogEvent logEvent) + { + LogStore.Emit(logEvent); } } \ No newline at end of file diff --git a/src/Artemis.Core/Ninject/PluginModule.cs b/src/Artemis.Core/Ninject/PluginModule.cs index 8dfed2f48..612685567 100644 --- a/src/Artemis.Core/Ninject/PluginModule.cs +++ b/src/Artemis.Core/Ninject/PluginModule.cs @@ -4,49 +4,48 @@ using Ninject.Extensions.Conventions; using Ninject.Modules; using Ninject.Planning.Bindings.Resolvers; -namespace Artemis.Core.Ninject +namespace Artemis.Core.Ninject; + +internal class PluginModule : NinjectModule { - internal class PluginModule : NinjectModule + public PluginModule(Plugin plugin) { - public PluginModule(Plugin plugin) + Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin)); + } + + public Plugin Plugin { get; } + + public override void Load() + { + if (Kernel == null) + throw new ArtemisCoreException("Failed to bind plugin child module, kernel is null."); + + Kernel.Components.Remove(); + + Kernel.Bind().ToConstant(Plugin).InTransientScope(); + + // Bind plugin service interfaces + Kernel.Bind(x => { - Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin)); - } + x.From(Plugin.Assembly) + .IncludingNonPublicTypes() + .SelectAllClasses() + .InheritedFrom() + .BindAllInterfaces() + .Configure(c => c.InSingletonScope()); + }); - public Plugin Plugin { get; } - - public override void Load() + // Plugin developers may not use an interface so bind the plugin services to themselves + // Sadly if they do both, the kernel will treat the interface and the base type as two different singletons + // perhaps we can avoid that, but I'm not sure how + Kernel.Bind(x => { - if (Kernel == null) - throw new ArtemisCoreException("Failed to bind plugin child module, kernel is null."); - - Kernel.Components.Remove(); - - Kernel.Bind().ToConstant(Plugin).InTransientScope(); - - // Bind plugin service interfaces - Kernel.Bind(x => - { - x.From(Plugin.Assembly) - .IncludingNonPublicTypes() - .SelectAllClasses() - .InheritedFrom() - .BindAllInterfaces() - .Configure(c => c.InSingletonScope()); - }); - - // Plugin developers may not use an interface so bind the plugin services to themselves - // Sadly if they do both, the kernel will treat the interface and the base type as two different singletons - // perhaps we can avoid that, but I'm not sure how - Kernel.Bind(x => - { - x.From(Plugin.Assembly) - .IncludingNonPublicTypes() - .SelectAllClasses() - .InheritedFrom() - .BindToSelf() - .Configure(c => c.InSingletonScope()); - }); - } + x.From(Plugin.Assembly) + .IncludingNonPublicTypes() + .SelectAllClasses() + .InheritedFrom() + .BindToSelf() + .Configure(c => c.InSingletonScope()); + }); } } \ No newline at end of file diff --git a/src/Artemis.Core/Ninject/PluginSettingsProvider.cs b/src/Artemis.Core/Ninject/PluginSettingsProvider.cs index 878180ee1..3fb83fda8 100644 --- a/src/Artemis.Core/Ninject/PluginSettingsProvider.cs +++ b/src/Artemis.Core/Ninject/PluginSettingsProvider.cs @@ -4,47 +4,46 @@ using Artemis.Core.Services; using Artemis.Storage.Repositories.Interfaces; using Ninject.Activation; -namespace Artemis.Core.Ninject +namespace Artemis.Core.Ninject; + +// TODO: Investigate if this can't just be set as a constant on the plugin child kernel +internal class PluginSettingsProvider : Provider { - // TODO: Investigate if this can't just be set as a constant on the plugin child kernel - internal class PluginSettingsProvider : Provider + private static readonly List PluginSettings = new(); + private readonly IPluginManagementService _pluginManagementService; + private readonly IPluginRepository _pluginRepository; + + public PluginSettingsProvider(IPluginRepository pluginRepository, IPluginManagementService pluginManagementService) { - private static readonly List PluginSettings = new(); - private readonly IPluginRepository _pluginRepository; - private readonly IPluginManagementService _pluginManagementService; + _pluginRepository = pluginRepository; + _pluginManagementService = pluginManagementService; + } - public PluginSettingsProvider(IPluginRepository pluginRepository, IPluginManagementService pluginManagementService) + protected override PluginSettings CreateInstance(IContext context) + { + IRequest parentRequest = context.Request.ParentRequest; + if (parentRequest == null) + throw new ArtemisCoreException("PluginSettings couldn't be injected, failed to get the injection parent request"); + + // First try by PluginInfo parameter + Plugin? plugin = parentRequest.Parameters.FirstOrDefault(p => p.Name == "Plugin")?.GetValue(context, null) as Plugin; + // Fall back to assembly based detection + if (plugin == null) + plugin = _pluginManagementService.GetPluginByAssembly(parentRequest.Service.Assembly); + + if (plugin == null) + throw new ArtemisCoreException("PluginSettings can only be injected with the PluginInfo parameter provided " + + "or into a class defined in a plugin assembly"); + + lock (PluginSettings) { - _pluginRepository = pluginRepository; - _pluginManagementService = pluginManagementService; - } + PluginSettings? existingSettings = PluginSettings.FirstOrDefault(p => p.Plugin == plugin); + if (existingSettings != null) + return existingSettings; - protected override PluginSettings CreateInstance(IContext context) - { - IRequest parentRequest = context.Request.ParentRequest; - if (parentRequest == null) - throw new ArtemisCoreException("PluginSettings couldn't be injected, failed to get the injection parent request"); - - // First try by PluginInfo parameter - Plugin? plugin = parentRequest.Parameters.FirstOrDefault(p => p.Name == "Plugin")?.GetValue(context, null) as Plugin; - // Fall back to assembly based detection - if (plugin == null) - plugin = _pluginManagementService.GetPluginByAssembly(parentRequest.Service.Assembly); - - if (plugin == null) - throw new ArtemisCoreException("PluginSettings can only be injected with the PluginInfo parameter provided " + - "or into a class defined in a plugin assembly"); - - lock (PluginSettings) - { - PluginSettings? existingSettings = PluginSettings.FirstOrDefault(p => p.Plugin == plugin); - if (existingSettings != null) - return existingSettings; - - PluginSettings? settings = new(plugin, _pluginRepository); - PluginSettings.Add(settings); - return settings; - } + PluginSettings? settings = new(plugin, _pluginRepository); + PluginSettings.Add(settings); + return settings; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Ninject/SettingsServiceProvider.cs b/src/Artemis.Core/Ninject/SettingsServiceProvider.cs index 3a1f3efa0..a7c022ae6 100644 --- a/src/Artemis.Core/Ninject/SettingsServiceProvider.cs +++ b/src/Artemis.Core/Ninject/SettingsServiceProvider.cs @@ -2,25 +2,24 @@ using Ninject; using Ninject.Activation; -namespace Artemis.Core.Ninject +namespace Artemis.Core.Ninject; + +internal class SettingsServiceProvider : Provider { - internal class SettingsServiceProvider : Provider + private readonly SettingsService _instance; + + public SettingsServiceProvider(IKernel kernel) { - private readonly SettingsService _instance; + // This is not lazy, but the core is always going to be using this anyway + _instance = kernel.Get(); + } - public SettingsServiceProvider(IKernel kernel) - { - // This is not lazy, but the core is always going to be using this anyway - _instance = kernel.Get(); - } + protected override ISettingsService CreateInstance(IContext context) + { + IRequest parentRequest = context.Request.ParentRequest; + if (parentRequest == null || typeof(PluginFeature).IsAssignableFrom(parentRequest.Service)) + throw new ArtemisPluginException($"SettingsService can not be injected into a plugin. Inject {nameof(PluginSettings)} instead."); - protected override ISettingsService CreateInstance(IContext context) - { - IRequest parentRequest = context.Request.ParentRequest; - if (parentRequest == null || typeof(PluginFeature).IsAssignableFrom(parentRequest.Service)) - throw new ArtemisPluginException($"SettingsService can not be injected into a plugin. Inject {nameof(PluginSettings)} instead."); - - return _instance; - } + return _instance; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs b/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs index abafb8031..67d5ce8d4 100644 --- a/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs +++ b/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs @@ -5,124 +5,126 @@ using Ninject; using RGB.NET.Core; using Serilog; -namespace Artemis.Core.DeviceProviders +namespace Artemis.Core.DeviceProviders; + +/// +/// +/// Allows you to implement and register your own device provider +/// +public abstract class DeviceProvider : PluginFeature { - /// /// - /// Allows you to implement and register your own device provider + /// Creates a new instance of the class /// - public abstract class DeviceProvider : PluginFeature + /// + protected DeviceProvider(IRGBDeviceProvider rgbDeviceProvider) { - /// - /// Creates a new instance of the class - /// - /// - protected DeviceProvider(IRGBDeviceProvider rgbDeviceProvider) - { - RgbDeviceProvider = rgbDeviceProvider ?? throw new ArgumentNullException(nameof(rgbDeviceProvider)); - } + RgbDeviceProvider = rgbDeviceProvider ?? throw new ArgumentNullException(nameof(rgbDeviceProvider)); + } - /// - /// The RGB.NET device provider backing this Artemis device provider - /// - public IRGBDeviceProvider RgbDeviceProvider { get; } + /// + /// The RGB.NET device provider backing this Artemis device provider + /// + public IRGBDeviceProvider RgbDeviceProvider { get; } - /// - /// TODO: Make internal while still injecting. - /// A logger used by the device provider internally, ignore this - /// - [Inject] - public ILogger? Logger { get; set; } + /// + /// TODO: Make internal while still injecting. + /// A logger used by the device provider internally, ignore this + /// + [Inject] + public ILogger? Logger { get; set; } - /// - /// A boolean indicating whether this device provider detects the physical layout of connected keyboards. - /// - /// Note: is only called when this or - /// is . - /// - /// - public bool CanDetectPhysicalLayout { get; protected set; } + /// + /// A boolean indicating whether this device provider detects the physical layout of connected keyboards. + /// + /// Note: is only called when this or + /// is . + /// + /// + public bool CanDetectPhysicalLayout { get; protected set; } - /// - /// A boolean indicating whether this device provider detects the logical layout of connected keyboards - /// - /// Note: is only called when this or - /// is . - /// - /// - public bool CanDetectLogicalLayout { get; protected set; } + /// + /// A boolean indicating whether this device provider detects the logical layout of connected keyboards + /// + /// Note: is only called when this or + /// is . + /// + /// + public bool CanDetectLogicalLayout { get; protected set; } - /// - /// Gets or sets a boolean indicating whether adding missing LEDs defined in a layout but missing on the device is supported - /// Note: Defaults to . - /// - public bool CreateMissingLedsSupported { get; protected set; } = true; + /// + /// Gets or sets a boolean indicating whether adding missing LEDs defined in a layout but missing on the device is + /// supported + /// Note: Defaults to . + /// + public bool CreateMissingLedsSupported { get; protected set; } = true; - /// - /// Gets or sets a boolean indicating whether removing excess LEDs present in the device but missing in the layout is supported - /// Note: Defaults to . - /// - public bool RemoveExcessiveLedsSupported { get; protected set; } = true; + /// + /// Gets or sets a boolean indicating whether removing excess LEDs present in the device but missing in the layout is + /// supported + /// Note: Defaults to . + /// + public bool RemoveExcessiveLedsSupported { get; protected set; } = true; - /// - /// Loads a layout for the specified device and wraps it in an - /// - /// The device to load the layout for - /// The resulting Artemis layout - public virtual ArtemisLayout LoadLayout(ArtemisDevice device) - { - string layoutDir = Path.Combine(Plugin.Directory.FullName, "Layouts"); - string filePath = Path.Combine( - layoutDir, - device.RgbDevice.DeviceInfo.Manufacturer, - device.DeviceType.ToString(), - GetDeviceLayoutName(device) - ); - return new ArtemisLayout(filePath, LayoutSource.Plugin); - } + /// + /// Loads a layout for the specified device and wraps it in an + /// + /// The device to load the layout for + /// The resulting Artemis layout + public virtual ArtemisLayout LoadLayout(ArtemisDevice device) + { + string layoutDir = Path.Combine(Plugin.Directory.FullName, "Layouts"); + string filePath = Path.Combine( + layoutDir, + device.RgbDevice.DeviceInfo.Manufacturer, + device.DeviceType.ToString(), + GetDeviceLayoutName(device) + ); + return new ArtemisLayout(filePath, LayoutSource.Plugin); + } - /// - /// Loads a layout from the user layout folder for the specified device and wraps it in an - /// - /// The device to load the layout for - /// The resulting Artemis layout - public virtual ArtemisLayout LoadUserLayout(ArtemisDevice device) - { - string layoutDir = Constants.LayoutsFolder; - string filePath = Path.Combine( - layoutDir, - device.RgbDevice.DeviceInfo.Manufacturer, - device.DeviceType.ToString(), - GetDeviceLayoutName(device) - ); - return new ArtemisLayout(filePath, LayoutSource.User); - } + /// + /// Loads a layout from the user layout folder for the specified device and wraps it in an + /// + /// The device to load the layout for + /// The resulting Artemis layout + public virtual ArtemisLayout LoadUserLayout(ArtemisDevice device) + { + string layoutDir = Constants.LayoutsFolder; + string filePath = Path.Combine( + layoutDir, + device.RgbDevice.DeviceInfo.Manufacturer, + device.DeviceType.ToString(), + GetDeviceLayoutName(device) + ); + return new ArtemisLayout(filePath, LayoutSource.User); + } - /// - /// Called when a specific RGB device's logical and physical layout must be detected - /// - /// Note: Only called when is . - /// - /// - /// The device to detect the layout for, always a keyboard - public virtual string GetLogicalLayout(IKeyboard keyboard) - { - throw new NotImplementedException("Device provider does not support detecting logical layouts (don't call base.GetLogicalLayout())"); - } + /// + /// Called when a specific RGB device's logical and physical layout must be detected + /// + /// Note: Only called when is . + /// + /// + /// The device to detect the layout for, always a keyboard + public virtual string GetLogicalLayout(IKeyboard keyboard) + { + throw new NotImplementedException("Device provider does not support detecting logical layouts (don't call base.GetLogicalLayout())"); + } - /// - /// Called when determining which file name to use when loading the layout of the specified . - /// - /// The device to determine the layout file name for. - /// A file name, including an extension - public virtual string GetDeviceLayoutName(ArtemisDevice device) - { - // Take out invalid file name chars, may not be perfect but neither are you - string fileName = Path.GetInvalidFileNameChars().Aggregate(device.RgbDevice.DeviceInfo.Model, (current, c) => current.Replace(c, '-')); - if (device.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Keyboard) - fileName = $"{fileName}-{device.PhysicalLayout.ToString().ToUpper()}"; + /// + /// Called when determining which file name to use when loading the layout of the specified + /// . + /// + /// The device to determine the layout file name for. + /// A file name, including an extension + public virtual string GetDeviceLayoutName(ArtemisDevice device) + { + // Take out invalid file name chars, may not be perfect but neither are you + string fileName = Path.GetInvalidFileNameChars().Aggregate(device.RgbDevice.DeviceInfo.Model, (current, c) => current.Replace(c, '-')); + if (device.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Keyboard) + fileName = $"{fileName}-{device.PhysicalLayout.ToString().ToUpper()}"; - return fileName + ".xml"; - } + return fileName + ".xml"; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/IPluginConfigurationDialog.cs b/src/Artemis.Core/Plugins/IPluginConfigurationDialog.cs index 5d179d0d7..7df4491bb 100644 --- a/src/Artemis.Core/Plugins/IPluginConfigurationDialog.cs +++ b/src/Artemis.Core/Plugins/IPluginConfigurationDialog.cs @@ -1,15 +1,14 @@ using System; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a configuration dialog for a +/// +public interface IPluginConfigurationDialog { /// - /// Represents a configuration dialog for a + /// The type of view model the tab contains /// - public interface IPluginConfigurationDialog - { - /// - /// The type of view model the tab contains - /// - Type Type { get; } - } + Type Type { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/IPluginConfigurationViewModel.cs b/src/Artemis.Core/Plugins/IPluginConfigurationViewModel.cs index f2e711397..20937c3bf 100644 --- a/src/Artemis.Core/Plugins/IPluginConfigurationViewModel.cs +++ b/src/Artemis.Core/Plugins/IPluginConfigurationViewModel.cs @@ -1,10 +1,8 @@ -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a view model for a plugin configuration window +/// +public interface IPluginConfigurationViewModel { - /// - /// Represents a view model for a plugin configuration window - /// - public interface IPluginConfigurationViewModel - { - - } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/ILayerBrushConfigurationDialog.cs b/src/Artemis.Core/Plugins/LayerBrushes/ILayerBrushConfigurationDialog.cs index c0ad0df9d..348170ad0 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/ILayerBrushConfigurationDialog.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/ILayerBrushConfigurationDialog.cs @@ -1,9 +1,8 @@ -namespace Artemis.Core.LayerBrushes +namespace Artemis.Core.LayerBrushes; + +/// +/// Represents the configuration dialog of a layer brush +/// +public interface ILayerBrushConfigurationDialog { - /// - /// Represents the configuration dialog of a layer brush - /// - public interface ILayerBrushConfigurationDialog - { - } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/ILayerBrushPreset.cs b/src/Artemis.Core/Plugins/LayerBrushes/ILayerBrushPreset.cs index 72fa2aa40..79adeb564 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/ILayerBrushPreset.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/ILayerBrushPreset.cs @@ -1,28 +1,27 @@ -namespace Artemis.Core.LayerBrushes +namespace Artemis.Core.LayerBrushes; + +/// +/// Represents a brush preset for a brush. +/// +public interface ILayerBrushPreset { /// - /// Represents a brush preset for a brush. + /// Gets the name of the preset /// - public interface ILayerBrushPreset - { - /// - /// Gets the name of the preset - /// - string Name { get; } + string Name { get; } - /// - /// Gets the description of the preset - /// - string Description { get; } + /// + /// Gets the description of the preset + /// + string Description { get; } - /// - /// Gets the icon of the preset - /// - string Icon { get; } + /// + /// Gets the icon of the preset + /// + string Icon { get; } - /// - /// Applies the preset to the layer brush - /// - void Apply(); - } + /// + /// Applies the preset to the layer brush + /// + void Apply(); } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs index 24d54ae82..cadbd5e7e 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs @@ -1,220 +1,218 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using Artemis.Storage.Entities.Profile; using SkiaSharp; -namespace Artemis.Core.LayerBrushes +namespace Artemis.Core.LayerBrushes; + +/// +/// For internal use only, please use or or instead +/// +public abstract class BaseLayerBrush : BreakableModel, IDisposable +{ + private LayerBrushType _brushType; + private ILayerBrushConfigurationDialog? _configurationDialog; + private LayerBrushDescriptor _descriptor; + private Layer _layer; + private bool _supportsTransformation = true; + + /// + /// Creates a new instance of the class + /// + protected BaseLayerBrush() + { + // Both are set right after construction to keep the constructor of inherited classes clean + _layer = null!; + _descriptor = null!; + LayerBrushEntity = null!; + } + + /// + /// Gets the layer this brush is applied to + /// + public Layer Layer + { + get => _layer; + internal set => SetAndNotify(ref _layer, value); + } + + /// + /// Gets the brush entity this brush uses for persistent storage + /// + public LayerBrushEntity LayerBrushEntity { get; internal set; } + + /// + /// Gets the descriptor of this brush + /// + public LayerBrushDescriptor Descriptor + { + get => _descriptor; + internal set => SetAndNotify(ref _descriptor, value); + } + + /// + /// Gets or sets a configuration dialog complementing the regular properties + /// + public ILayerBrushConfigurationDialog? ConfigurationDialog + { + get => _configurationDialog; + protected set => SetAndNotify(ref _configurationDialog, value); + } + + /// + /// Gets the type of layer brush + /// + public LayerBrushType BrushType + { + get => _brushType; + internal set => SetAndNotify(ref _brushType, value); + } + + /// + /// Gets the ID of the that provided this effect + /// + public string? ProviderId => Descriptor?.Provider.Id; + + /// + /// Gets a reference to the layer property group without knowing it's type + /// + public virtual LayerPropertyGroup? BaseProperties => null; + + /// + /// Gets a list of presets available to this layer brush + /// + public virtual List? Presets => null; + + /// + /// Gets the default preset used for new instances of this layer brush + /// + public virtual ILayerBrushPreset? DefaultPreset => Presets?.FirstOrDefault(); + + /// + /// Gets a boolean indicating whether the layer brush is enabled or not + /// + public bool Enabled { get; private set; } + + /// + /// Gets or sets whether the brush supports transformations + /// Note: RGB.NET brushes can never be transformed and setting this to true will throw an exception + /// + public bool SupportsTransformation + { + get => _supportsTransformation; + protected set + { + if (value && BrushType == LayerBrushType.RgbNet) + throw new ArtemisPluginFeatureException(Descriptor?.Provider!, "An RGB.NET brush cannot support transformation"); + _supportsTransformation = value; + } + } + + #region Overrides of BreakableModel + + /// + public override string BrokenDisplayName => Descriptor.DisplayName; + + #endregion + + /// + /// Called when the layer brush is activated + /// + public abstract void EnableLayerBrush(); + + /// + /// Called when the layer brush is deactivated + /// + public abstract void DisableLayerBrush(); + + /// + /// Called before rendering every frame, write your update logic here + /// + /// Seconds passed since last update + public abstract void Update(double deltaTime); + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + DisableLayerBrush(); + BaseProperties?.Dispose(); + } + } + + internal void InternalUpdate(Timeline timeline) + { + BaseProperties?.Update(timeline); + TryOrBreak(() => Update(timeline.Delta.TotalSeconds), "Failed to update"); + } + + /// + /// Enables the layer brush if it isn't already enabled + /// + internal void InternalEnable() + { + if (Enabled) + return; + + if (!TryOrBreak(EnableLayerBrush, "Failed to enable")) + return; + + Enabled = true; + } + + /// + /// Disables the layer brush if it isn't already disabled + /// + internal void InternalDisable() + { + if (!Enabled) + return; + + DisableLayerBrush(); + Enabled = false; + } + + // Not only is this needed to initialize properties on the layer brushes, it also prevents implementing anything + // but LayerBrush and RgbNetLayerBrush outside the core + internal abstract void Initialize(); + + internal abstract void InternalRender(SKCanvas canvas, SKRect path, SKPaint paint); + + internal void Save() + { + // No need to update the type or provider ID, they're set once by the LayerBrushDescriptors CreateInstance and can't change + BaseProperties?.ApplyToEntity(); + LayerBrushEntity.PropertyGroup = BaseProperties?.PropertyGroupEntity; + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} + +/// +/// Describes the type of a layer brush +/// +public enum LayerBrushType { /// - /// For internal use only, please use or or instead + /// A regular brush that users Artemis' SkiaSharp-based rendering engine /// - public abstract class BaseLayerBrush : BreakableModel, IDisposable - { - private LayerBrushType _brushType; - private ILayerBrushConfigurationDialog? _configurationDialog; - private LayerBrushDescriptor _descriptor; - private Layer _layer; - private bool _supportsTransformation = true; - - /// - /// Creates a new instance of the class - /// - protected BaseLayerBrush() - { - // Both are set right after construction to keep the constructor of inherited classes clean - _layer = null!; - _descriptor = null!; - LayerBrushEntity = null!; - } - - /// - /// Gets the layer this brush is applied to - /// - public Layer Layer - { - get => _layer; - internal set => SetAndNotify(ref _layer, value); - } - - /// - /// Gets the brush entity this brush uses for persistent storage - /// - public LayerBrushEntity LayerBrushEntity { get; internal set; } - - /// - /// Gets the descriptor of this brush - /// - public LayerBrushDescriptor Descriptor - { - get => _descriptor; - internal set => SetAndNotify(ref _descriptor, value); - } - - /// - /// Gets or sets a configuration dialog complementing the regular properties - /// - public ILayerBrushConfigurationDialog? ConfigurationDialog - { - get => _configurationDialog; - protected set => SetAndNotify(ref _configurationDialog, value); - } - - /// - /// Gets the type of layer brush - /// - public LayerBrushType BrushType - { - get => _brushType; - internal set => SetAndNotify(ref _brushType, value); - } - - /// - /// Gets the ID of the that provided this effect - /// - public string? ProviderId => Descriptor?.Provider.Id; - - /// - /// Gets a reference to the layer property group without knowing it's type - /// - public virtual LayerPropertyGroup? BaseProperties => null; - - /// - /// Gets a list of presets available to this layer brush - /// - public virtual List? Presets => null; - - /// - /// Gets the default preset used for new instances of this layer brush - /// - public virtual ILayerBrushPreset? DefaultPreset => Presets?.FirstOrDefault(); - - /// - /// Gets a boolean indicating whether the layer brush is enabled or not - /// - public bool Enabled { get; private set; } - - /// - /// Gets or sets whether the brush supports transformations - /// Note: RGB.NET brushes can never be transformed and setting this to true will throw an exception - /// - public bool SupportsTransformation - { - get => _supportsTransformation; - protected set - { - if (value && BrushType == LayerBrushType.RgbNet) - throw new ArtemisPluginFeatureException(Descriptor?.Provider!, "An RGB.NET brush cannot support transformation"); - _supportsTransformation = value; - } - } - - /// - /// Called when the layer brush is activated - /// - public abstract void EnableLayerBrush(); - - /// - /// Called when the layer brush is deactivated - /// - public abstract void DisableLayerBrush(); - - /// - /// Called before rendering every frame, write your update logic here - /// - /// Seconds passed since last update - public abstract void Update(double deltaTime); - - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - DisableLayerBrush(); - BaseProperties?.Dispose(); - } - } - - internal void InternalUpdate(Timeline timeline) - { - BaseProperties?.Update(timeline); - TryOrBreak(() => Update(timeline.Delta.TotalSeconds), "Failed to update"); - } - - /// - /// Enables the layer brush if it isn't already enabled - /// - internal void InternalEnable() - { - if (Enabled) - return; - - if (!TryOrBreak(EnableLayerBrush, "Failed to enable")) - return; - - Enabled = true; - } - - /// - /// Disables the layer brush if it isn't already disabled - /// - internal void InternalDisable() - { - if (!Enabled) - return; - - DisableLayerBrush(); - Enabled = false; - } - - // Not only is this needed to initialize properties on the layer brushes, it also prevents implementing anything - // but LayerBrush and RgbNetLayerBrush outside the core - internal abstract void Initialize(); - - internal abstract void InternalRender(SKCanvas canvas, SKRect path, SKPaint paint); - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #region Overrides of BreakableModel - - /// - public override string BrokenDisplayName => Descriptor.DisplayName; - - #endregion - - internal void Save() - { - // No need to update the type or provider ID, they're set once by the LayerBrushDescriptors CreateInstance and can't change - BaseProperties?.ApplyToEntity(); - LayerBrushEntity.PropertyGroup = BaseProperties?.PropertyGroupEntity; - } - } + Regular, /// - /// Describes the type of a layer brush + /// An RGB.NET brush that uses RGB.NET's per-LED rendering engine. /// - public enum LayerBrushType - { - /// - /// A regular brush that users Artemis' SkiaSharp-based rendering engine - /// - Regular, - - /// - /// An RGB.NET brush that uses RGB.NET's per-LED rendering engine. - /// - RgbNet - } + RgbNet } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs index 52b6c3ab4..fc7cdb6ea 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs @@ -1,44 +1,42 @@ using System; -using Artemis.Storage.Entities.Profile; -namespace Artemis.Core.LayerBrushes +namespace Artemis.Core.LayerBrushes; + +/// +/// For internal use only, please use or or instead +/// +public abstract class PropertiesLayerBrush : BaseLayerBrush where T : LayerPropertyGroup, new() { + private T _properties = null!; + /// - /// For internal use only, please use or or instead + /// Gets whether all properties on this brush are initialized /// - public abstract class PropertiesLayerBrush : BaseLayerBrush where T : LayerPropertyGroup, new() + public bool PropertiesInitialized { get; internal set; } + + /// + public override LayerPropertyGroup BaseProperties => Properties; + + /// + /// Gets the properties of this brush. + /// + public T Properties { - private T _properties = null!; - - /// - /// Gets whether all properties on this brush are initialized - /// - public bool PropertiesInitialized { get; internal set; } - - /// - public override LayerPropertyGroup BaseProperties => Properties; - - /// - /// Gets the properties of this brush. - /// - public T Properties + get { - get - { - // I imagine a null reference here can be confusing, so lets throw an exception explaining what to do - if (_properties == null) - throw new InvalidOperationException("Cannot access brush properties until OnPropertiesInitialized has been called"); - return _properties; - } - internal set => _properties = value; + // I imagine a null reference here can be confusing, so lets throw an exception explaining what to do + if (_properties == null) + throw new InvalidOperationException("Cannot access brush properties until OnPropertiesInitialized has been called"); + return _properties; } + internal set => _properties = value; + } - internal void InitializeProperties() - { - Properties = new T(); - PropertyGroupDescriptionAttribute groupDescription = new() {Identifier = "Brush", Name = Descriptor.DisplayName, Description = Descriptor.Description}; - Properties.Initialize(Layer, null, groupDescription, LayerBrushEntity.PropertyGroup); - PropertiesInitialized = true; - } + internal void InitializeProperties() + { + Properties = new T(); + PropertyGroupDescriptionAttribute groupDescription = new() {Identifier = "Brush", Name = Descriptor.DisplayName, Description = Descriptor.Description}; + Properties.Initialize(Layer, null, groupDescription, LayerBrushEntity.PropertyGroup); + PropertiesInitialized = true; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs index 4db8f176e..08f86eafa 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs @@ -1,39 +1,38 @@ using SkiaSharp; -namespace Artemis.Core.LayerBrushes +namespace Artemis.Core.LayerBrushes; + +/// +/// Represents a brush that renders on a layer +/// +/// The type of brush properties +public abstract class LayerBrush : PropertiesLayerBrush where T : LayerPropertyGroup, new() { /// - /// Represents a brush that renders on a layer + /// Creates a new instance of the class /// - /// The type of brush properties - public abstract class LayerBrush : PropertiesLayerBrush where T : LayerPropertyGroup, new() + protected LayerBrush() { - /// - /// Creates a new instance of the class - /// - protected LayerBrush() - { - BrushType = LayerBrushType.Regular; - } + BrushType = LayerBrushType.Regular; + } - /// - /// The main method of rendering anything to the layer. The provided is specific to the layer - /// and matches it's width and height. - /// Called during rendering or layer preview, in the order configured on the layer - /// - /// The layer canvas - /// The area to be filled, covers the shape - /// The paint to be used to fill the shape - public abstract void Render(SKCanvas canvas, SKRect bounds, SKPaint paint); + /// + /// The main method of rendering anything to the layer. The provided is specific to the layer + /// and matches it's width and height. + /// Called during rendering or layer preview, in the order configured on the layer + /// + /// The layer canvas + /// The area to be filled, covers the shape + /// The paint to be used to fill the shape + public abstract void Render(SKCanvas canvas, SKRect bounds, SKPaint paint); - internal override void InternalRender(SKCanvas canvas, SKRect bounds, SKPaint paint) - { - TryOrBreak(() => Render(canvas, bounds, paint), "Failed to render"); - } + internal override void InternalRender(SKCanvas canvas, SKRect bounds, SKPaint paint) + { + TryOrBreak(() => Render(canvas, bounds, paint), "Failed to render"); + } - internal override void Initialize() - { - TryOrBreak(InitializeProperties, "Failed to initialize"); - } + internal override void Initialize() + { + TryOrBreak(InitializeProperties, "Failed to initialize"); } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs index b2fad30cc..4279b2810 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs @@ -2,74 +2,73 @@ using Artemis.Storage.Entities.Profile; using Ninject; -namespace Artemis.Core.LayerBrushes +namespace Artemis.Core.LayerBrushes; + +/// +/// A class that describes a layer brush +/// +public class LayerBrushDescriptor { - /// - /// A class that describes a layer brush - /// - public class LayerBrushDescriptor + internal LayerBrushDescriptor(string displayName, string description, string icon, Type layerBrushType, LayerBrushProvider provider) { - internal LayerBrushDescriptor(string displayName, string description, string icon, Type layerBrushType, LayerBrushProvider provider) - { - DisplayName = displayName; - Description = description; - Icon = icon; - LayerBrushType = layerBrushType; - Provider = provider; - } + DisplayName = displayName; + Description = description; + Icon = icon; + LayerBrushType = layerBrushType; + Provider = provider; + } - /// - /// The name that is displayed in the UI - /// - public string DisplayName { get; } + /// + /// The name that is displayed in the UI + /// + public string DisplayName { get; } - /// - /// The description that is displayed in the UI - /// - public string Description { get; } + /// + /// The description that is displayed in the UI + /// + public string Description { get; } - /// - /// The Material icon to display in the UI, a full reference can be found - /// here - /// - public string Icon { get; } + /// + /// The Material icon to display in the UI, a full reference can be found + /// here + /// + public string Icon { get; } - /// - /// The type of the layer brush - /// - public Type LayerBrushType { get; } + /// + /// The type of the layer brush + /// + public Type LayerBrushType { get; } - /// - /// The plugin that provided this - /// - public LayerBrushProvider Provider { get; } + /// + /// The plugin that provided this + /// + public LayerBrushProvider Provider { get; } - /// - /// Determines whether the provided references to a brush provided by this descriptor - /// - public bool MatchesLayerBrushReference(LayerBrushReference? reference) - { - if (reference == null) - return false; + /// + /// Determines whether the provided references to a brush provided by this descriptor + /// + public bool MatchesLayerBrushReference(LayerBrushReference? reference) + { + if (reference == null) + return false; - return Provider.Id == reference.LayerBrushProviderId && LayerBrushType.Name == reference.BrushType; - } + return Provider.Id == reference.LayerBrushProviderId && LayerBrushType.Name == reference.BrushType; + } - /// - /// Creates an instance of the described brush and applies it to the layer - /// - public BaseLayerBrush CreateInstance(Layer layer, LayerBrushEntity? entity) - { - if (layer == null) - throw new ArgumentNullException(nameof(layer)); + /// + /// Creates an instance of the described brush and applies it to the layer + /// + public BaseLayerBrush CreateInstance(Layer layer, LayerBrushEntity? entity) + { + if (layer == null) + throw new ArgumentNullException(nameof(layer)); - BaseLayerBrush brush = (BaseLayerBrush) Provider.Plugin.Kernel!.Get(LayerBrushType); - brush.Layer = layer; - brush.Descriptor = this; - brush.LayerBrushEntity = entity ?? new LayerBrushEntity { ProviderId = Provider.Id, BrushType = LayerBrushType.FullName }; - - brush.Initialize(); - return brush; - } + BaseLayerBrush brush = (BaseLayerBrush) Provider.Plugin.Kernel!.Get(LayerBrushType); + brush.Layer = layer; + brush.Descriptor = this; + brush.LayerBrushEntity = entity ?? new LayerBrushEntity {ProviderId = Provider.Id, BrushType = LayerBrushType.FullName}; + + brush.Initialize(); + return brush; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushProvider.cs b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushProvider.cs index 43a200fc5..5ef30246c 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushProvider.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushProvider.cs @@ -1,59 +1,57 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Text.RegularExpressions; -namespace Artemis.Core.LayerBrushes +namespace Artemis.Core.LayerBrushes; + +/// +/// Allows you to create one or more s usable by profile layers. +/// +public abstract class LayerBrushProvider : PluginFeature { + private readonly List _layerBrushDescriptors; + /// - /// Allows you to create one or more s usable by profile layers. + /// Allows you to register one or more s usable by profile layers. /// - public abstract class LayerBrushProvider : PluginFeature + protected LayerBrushProvider() { - private readonly List _layerBrushDescriptors; + _layerBrushDescriptors = new List(); + LayerBrushDescriptors = new ReadOnlyCollection(_layerBrushDescriptors); + Disabled += OnDisabled; + } - /// - /// Allows you to register one or more s usable by profile layers. - /// - protected LayerBrushProvider() - { - _layerBrushDescriptors = new List(); - LayerBrushDescriptors = new ReadOnlyCollection(_layerBrushDescriptors); - Disabled += OnDisabled; - } + /// + /// A read-only collection of all layer brushes added with + /// + public ReadOnlyCollection LayerBrushDescriptors { get; } - /// - /// A read-only collection of all layer brushes added with - /// - public ReadOnlyCollection LayerBrushDescriptors { get; } + /// + /// Registers a layer brush descriptor for a given layer brush, so that it appears in the UI. + /// Note: You do not need to manually remove these on disable + /// + /// The type of the layer brush you wish to register + /// The name to display in the UI + /// The description to display in the UI + /// + /// The Material icon to display in the UI, a full reference can be found + /// here + /// + protected void RegisterLayerBrushDescriptor(string displayName, string description, string icon) where T : BaseLayerBrush + { + if (!IsEnabled) + throw new ArtemisPluginException(Plugin, "Can only add a layer brush descriptor when the plugin is enabled"); - /// - /// Registers a layer brush descriptor for a given layer brush, so that it appears in the UI. - /// Note: You do not need to manually remove these on disable - /// - /// The type of the layer brush you wish to register - /// The name to display in the UI - /// The description to display in the UI - /// - /// The Material icon to display in the UI, a full reference can be found - /// here - /// - protected void RegisterLayerBrushDescriptor(string displayName, string description, string icon) where T : BaseLayerBrush - { - if (!IsEnabled) - throw new ArtemisPluginException(Plugin, "Can only add a layer brush descriptor when the plugin is enabled"); + if (icon.Contains('.')) + icon = Plugin.ResolveRelativePath(icon); + LayerBrushDescriptor descriptor = new(displayName, description, icon, typeof(T), this); + _layerBrushDescriptors.Add(descriptor); + LayerBrushStore.Add(descriptor); + } - if (icon.Contains('.')) - icon = Plugin.ResolveRelativePath(icon); - LayerBrushDescriptor descriptor = new(displayName, description, icon, typeof(T), this); - _layerBrushDescriptors.Add(descriptor); - LayerBrushStore.Add(descriptor); - } - - private void OnDisabled(object? sender, EventArgs e) - { - // The store will clean up the registrations by itself, the plugin just needs to clear its own list - _layerBrushDescriptors.Clear(); - } + private void OnDisabled(object? sender, EventArgs e) + { + // The store will clean up the registrations by itself, the plugin just needs to clear its own list + _layerBrushDescriptors.Clear(); } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs index cfaeb5a48..e1623b3fc 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs @@ -1,84 +1,80 @@ -using System; -using SkiaSharp; +using SkiaSharp; -namespace Artemis.Core.LayerBrushes +namespace Artemis.Core.LayerBrushes; + +/// +/// Represents a brush that renders on a per-layer basis +/// +/// The type of brush properties +public abstract class PerLedLayerBrush : PropertiesLayerBrush where T : LayerPropertyGroup, new() { /// - /// Represents a brush that renders on a per-layer basis + /// Creates a new instance of the class /// - /// The type of brush properties - public abstract class PerLedLayerBrush : PropertiesLayerBrush where T : LayerPropertyGroup, new() + protected PerLedLayerBrush() { - /// - /// Creates a new instance of the class - /// - protected PerLedLayerBrush() + BrushType = LayerBrushType.Regular; + } + + /// + /// The main method of rendering for this type of brush. Called once per frame for each LED in the layer + /// + /// Note: Due to transformations, the render point may not match the position of the LED, always use the render + /// point to determine where the color will go. + /// + /// + /// The LED that will receive the color + /// The point at which the color is located + /// The color the LED will receive + public abstract SKColor GetColor(ArtemisLed led, SKPoint renderPoint); + + internal override void InternalRender(SKCanvas canvas, SKRect bounds, SKPaint paint) + { + // We don't want rotation on this canvas because that'll displace the LEDs, translations are applied to the points of each LED instead + if (Layer.General.TransformMode.CurrentValue == LayerTransformMode.Normal && SupportsTransformation) + canvas.SetMatrix(canvas.TotalMatrix.PreConcat(Layer.GetTransformMatrix(true, false, false, true).Invert())); + + using SKPath pointsPath = new(); + foreach (ArtemisLed artemisLed in Layer.Leds) { - BrushType = LayerBrushType.Regular; + pointsPath.AddPoly(new[] + { + new SKPoint(0, 0), + new SKPoint(artemisLed.AbsoluteRectangle.Left - Layer.Bounds.Left, artemisLed.AbsoluteRectangle.Top - Layer.Bounds.Top) + }); } - /// - /// The main method of rendering for this type of brush. Called once per frame for each LED in the layer - /// - /// Note: Due to transformations, the render point may not match the position of the LED, always use the render - /// point to determine where the color will go. - /// - /// - /// The LED that will receive the color - /// The point at which the color is located - /// The color the LED will receive - public abstract SKColor GetColor(ArtemisLed led, SKPoint renderPoint); + // Apply the translation to the points of each LED instead + if (Layer.General.TransformMode.CurrentValue == LayerTransformMode.Normal && SupportsTransformation) + pointsPath.Transform(Layer.GetTransformMatrix(true, true, true, true).Invert()); - internal override void InternalRender(SKCanvas canvas, SKRect bounds, SKPaint paint) + SKPoint[] points = pointsPath.Points; + + TryOrBreak(() => { - // We don't want rotation on this canvas because that'll displace the LEDs, translations are applied to the points of each LED instead - if (Layer.General.TransformMode.CurrentValue == LayerTransformMode.Normal && SupportsTransformation) - canvas.SetMatrix(canvas.TotalMatrix.PreConcat(Layer.GetTransformMatrix(true, false, false, true).Invert())); - - using SKPath pointsPath = new(); - foreach (ArtemisLed artemisLed in Layer.Leds) + for (int index = 0; index < Layer.Leds.Count; index++) { - pointsPath.AddPoly(new[] - { - new SKPoint(0, 0), - new SKPoint(artemisLed.AbsoluteRectangle.Left - Layer.Bounds.Left, artemisLed.AbsoluteRectangle.Top - Layer.Bounds.Top) - }); + ArtemisLed artemisLed = Layer.Leds[index]; + SKPoint renderPoint = points[index * 2 + 1]; + if (!float.IsFinite(renderPoint.X) || !float.IsFinite(renderPoint.Y)) + continue; + + // Let the brush determine the color + paint.Color = GetColor(artemisLed, renderPoint); + SKRect ledRectangle = SKRect.Create( + artemisLed.AbsoluteRectangle.Left - Layer.Bounds.Left, + artemisLed.AbsoluteRectangle.Top - Layer.Bounds.Top, + artemisLed.AbsoluteRectangle.Width, + artemisLed.AbsoluteRectangle.Height + ); + + canvas.DrawRect(ledRectangle, paint); } + }, "Failed to render"); + } - // Apply the translation to the points of each LED instead - if (Layer.General.TransformMode.CurrentValue == LayerTransformMode.Normal && SupportsTransformation) - pointsPath.Transform(Layer.GetTransformMatrix(true, true, true, true).Invert()); - - SKPoint[] points = pointsPath.Points; - - TryOrBreak(() => - { - - - for (int index = 0; index < Layer.Leds.Count; index++) - { - ArtemisLed artemisLed = Layer.Leds[index]; - SKPoint renderPoint = points[index * 2 + 1]; - if (!float.IsFinite(renderPoint.X) || !float.IsFinite(renderPoint.Y)) - continue; - - // Let the brush determine the color - paint.Color = GetColor(artemisLed, renderPoint); - SKRect ledRectangle = SKRect.Create( - artemisLed.AbsoluteRectangle.Left - Layer.Bounds.Left, - artemisLed.AbsoluteRectangle.Top - Layer.Bounds.Top, - artemisLed.AbsoluteRectangle.Width, - artemisLed.AbsoluteRectangle.Height - ); - - canvas.DrawRect(ledRectangle, paint); - } - }, "Failed to render"); - } - - internal override void Initialize() - { - TryOrBreak(InitializeProperties, "Failed to initialize"); - } + internal override void Initialize() + { + TryOrBreak(InitializeProperties, "Failed to initialize"); } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerEffects/ILayerEffectConfigurationDialog.cs b/src/Artemis.Core/Plugins/LayerEffects/ILayerEffectConfigurationDialog.cs index 238e07a51..551c8fe4b 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/ILayerEffectConfigurationDialog.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/ILayerEffectConfigurationDialog.cs @@ -1,9 +1,8 @@ -namespace Artemis.Core.LayerEffects +namespace Artemis.Core.LayerEffects; + +/// +/// Represents a configuration dialog for a +/// +public interface ILayerEffectConfigurationDialog { - /// - /// Represents a configuration dialog for a - /// - public interface ILayerEffectConfigurationDialog - { - } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs b/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs index bbd355e2c..d574c2e08 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs @@ -2,241 +2,239 @@ using Artemis.Storage.Entities.Profile; using SkiaSharp; -namespace Artemis.Core.LayerEffects +namespace Artemis.Core.LayerEffects; + +/// +/// For internal use only, please use instead +/// +public abstract class BaseLayerEffect : BreakableModel, IDisposable, IStorageModel { - /// - /// For internal use only, please use instead - /// - public abstract class BaseLayerEffect : BreakableModel, IDisposable, IStorageModel + private ILayerEffectConfigurationDialog? _configurationDialog; + private LayerEffectDescriptor _descriptor; + private bool _hasBeenRenamed; + private string _name; + private int _order; + private RenderProfileElement _profileElement; + + /// + protected BaseLayerEffect() { - private ILayerEffectConfigurationDialog? _configurationDialog; - private LayerEffectDescriptor _descriptor; - private bool _hasBeenRenamed; - private string _name; - private int _order; - private RenderProfileElement _profileElement; + // These are set right after construction to keep the constructor of inherited classes clean + _profileElement = null!; + _descriptor = null!; + _name = null!; + LayerEffectEntity = null!; + } - /// - protected BaseLayerEffect() + /// + /// Gets the + /// + public LayerEffectEntity LayerEffectEntity { get; internal set; } + + /// + /// Gets the profile element (such as layer or folder) this effect is applied to + /// + public RenderProfileElement ProfileElement + { + get => _profileElement; + internal set => SetAndNotify(ref _profileElement, value); + } + + /// + /// The name which appears in the editor + /// + public string Name + { + get => _name; + set => SetAndNotify(ref _name, value); + } + + /// + /// Gets or sets whether the effect has been renamed by the user, if true consider refraining from changing the name + /// programatically + /// + public bool HasBeenRenamed + { + get => _hasBeenRenamed; + set => SetAndNotify(ref _hasBeenRenamed, value); + } + + /// + /// Gets the order in which this effect appears in the update loop and editor + /// + public int Order + { + get => _order; + set => SetAndNotify(ref _order, value); + } + + /// + /// Gets the that registered this effect + /// + public LayerEffectDescriptor Descriptor + { + get => _descriptor; + internal set => SetAndNotify(ref _descriptor, value); + } + + /// + /// Gets or sets a configuration dialog complementing the regular properties + /// + public ILayerEffectConfigurationDialog? ConfigurationDialog + { + get => _configurationDialog; + protected set => SetAndNotify(ref _configurationDialog, value); + } + + /// + /// Gets the ID of the that provided this effect + /// + public string ProviderId => Descriptor.Provider.Id; + + /// + /// Gets a reference to the layer property group without knowing it's type + /// + public virtual LayerEffectPropertyGroup? BaseProperties => null; + + /// + /// Gets a boolean indicating whether the layer effect is enabled or not + /// + public bool Enabled { get; private set; } + + /// + /// Gets a boolean indicating whether the layer effect is suspended or not + /// + public bool Suspended => BaseProperties is not {PropertiesInitialized: true} || !BaseProperties.IsEnabled; + + #region Overrides of BreakableModel + + /// + public override string BrokenDisplayName => Name; + + #endregion + + /// + /// Called when the layer effect is activated + /// + public abstract void EnableLayerEffect(); + + /// + /// Called when the layer effect is deactivated + /// + public abstract void DisableLayerEffect(); + + /// + /// Called before rendering every frame, write your update logic here + /// + /// + public abstract void Update(double deltaTime); + + /// + /// Called before the layer or folder will be rendered + /// + /// The canvas used to render the frame + /// The bounds this layer/folder will render in + /// The paint this layer/folder will use to render + public abstract void PreProcess(SKCanvas canvas, SKRect renderBounds, SKPaint paint); + + /// + /// Called after the layer of folder has been rendered + /// + /// The canvas used to render the frame + /// The bounds this layer/folder rendered in + /// The paint this layer/folder used to render + public abstract void PostProcess(SKCanvas canvas, SKRect renderBounds, SKPaint paint); + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) { - // These are set right after construction to keep the constructor of inherited classes clean - _profileElement = null!; - _descriptor = null!; - _name = null!; - LayerEffectEntity = null!; - } - - /// - /// Gets the - /// - public LayerEffectEntity LayerEffectEntity { get; internal set; } - - /// - /// Gets the profile element (such as layer or folder) this effect is applied to - /// - public RenderProfileElement ProfileElement - { - get => _profileElement; - internal set => SetAndNotify(ref _profileElement, value); - } - - /// - /// The name which appears in the editor - /// - public string Name - { - get => _name; - set => SetAndNotify(ref _name, value); - } - - /// - /// Gets or sets whether the effect has been renamed by the user, if true consider refraining from changing the name - /// programatically - /// - public bool HasBeenRenamed - { - get => _hasBeenRenamed; - set => SetAndNotify(ref _hasBeenRenamed, value); - } - - /// - /// Gets the order in which this effect appears in the update loop and editor - /// - public int Order - { - get => _order; - set => SetAndNotify(ref _order, value); - } - - /// - /// Gets the that registered this effect - /// - public LayerEffectDescriptor Descriptor - { - get => _descriptor; - internal set => SetAndNotify(ref _descriptor, value); - } - - /// - /// Gets or sets a configuration dialog complementing the regular properties - /// - public ILayerEffectConfigurationDialog? ConfigurationDialog - { - get => _configurationDialog; - protected set => SetAndNotify(ref _configurationDialog, value); - } - - /// - /// Gets the ID of the that provided this effect - /// - public string ProviderId => Descriptor.Provider.Id; - - /// - /// Gets a reference to the layer property group without knowing it's type - /// - public virtual LayerEffectPropertyGroup? BaseProperties => null; - - /// - /// Gets a boolean indicating whether the layer effect is enabled or not - /// - public bool Enabled { get; private set; } - - /// - /// Gets a boolean indicating whether the layer effect is suspended or not - /// - public bool Suspended => BaseProperties is not {PropertiesInitialized: true} || !BaseProperties.IsEnabled; - - #region IDisposable - - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - DisableLayerEffect(); - BaseProperties?.Dispose(); - } - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #endregion - - /// - /// Called when the layer effect is activated - /// - public abstract void EnableLayerEffect(); - - /// - /// Called when the layer effect is deactivated - /// - public abstract void DisableLayerEffect(); - - /// - /// Called before rendering every frame, write your update logic here - /// - /// - public abstract void Update(double deltaTime); - - /// - /// Called before the layer or folder will be rendered - /// - /// The canvas used to render the frame - /// The bounds this layer/folder will render in - /// The paint this layer/folder will use to render - public abstract void PreProcess(SKCanvas canvas, SKRect renderBounds, SKPaint paint); - - /// - /// Called after the layer of folder has been rendered - /// - /// The canvas used to render the frame - /// The bounds this layer/folder rendered in - /// The paint this layer/folder used to render - public abstract void PostProcess(SKCanvas canvas, SKRect renderBounds, SKPaint paint); - - // Not only is this needed to initialize properties on the layer effects, it also prevents implementing anything - // but LayerEffect outside the core - internal abstract void Initialize(); - - internal virtual string GetEffectTypeName() => GetType().Name; - - internal void InternalUpdate(Timeline timeline) - { - BaseProperties?.Update(timeline); - TryOrBreak(() => Update(timeline.Delta.TotalSeconds), "Failed to update"); - } - - internal void InternalPreProcess(SKCanvas canvas, SKRect renderBounds, SKPaint paint) - { - TryOrBreak(() => PreProcess(canvas, renderBounds, paint), "Failed to pre-process"); - } - - internal void InternalPostProcess(SKCanvas canvas, SKRect renderBounds, SKPaint paint) - { - TryOrBreak(() => PostProcess(canvas, renderBounds, paint), "Failed to post-process"); - } - - /// - /// Enables the layer effect if it isn't already enabled - /// - internal void InternalEnable() - { - if (Enabled) - return; - - EnableLayerEffect(); - Enabled = true; - } - - /// - /// Disables the layer effect if it isn't already disabled - /// - internal void InternalDisable() - { - if (!Enabled) - return; - DisableLayerEffect(); - Enabled = false; - } - - #region Overrides of BreakableModel - - /// - public override string BrokenDisplayName => Name; - - #endregion - - /// - public void Load() - { - HasBeenRenamed = LayerEffectEntity.HasBeenRenamed; - Name = HasBeenRenamed ? LayerEffectEntity.Name : Descriptor.DisplayName; - Order = LayerEffectEntity.Order; - } - - /// - public void Save() - { - LayerEffectEntity.ProviderId = Descriptor.Provider.Id; - LayerEffectEntity.EffectType = GetType().FullName; - LayerEffectEntity.Name = Name; - LayerEffectEntity.HasBeenRenamed = HasBeenRenamed; - LayerEffectEntity.Order = Order; - - BaseProperties?.ApplyToEntity(); - LayerEffectEntity.PropertyGroup = BaseProperties?.PropertyGroupEntity; + BaseProperties?.Dispose(); } } + + // Not only is this needed to initialize properties on the layer effects, it also prevents implementing anything + // but LayerEffect outside the core + internal abstract void Initialize(); + + internal virtual string GetEffectTypeName() + { + return GetType().Name; + } + + internal void InternalUpdate(Timeline timeline) + { + BaseProperties?.Update(timeline); + TryOrBreak(() => Update(timeline.Delta.TotalSeconds), "Failed to update"); + } + + internal void InternalPreProcess(SKCanvas canvas, SKRect renderBounds, SKPaint paint) + { + TryOrBreak(() => PreProcess(canvas, renderBounds, paint), "Failed to pre-process"); + } + + internal void InternalPostProcess(SKCanvas canvas, SKRect renderBounds, SKPaint paint) + { + TryOrBreak(() => PostProcess(canvas, renderBounds, paint), "Failed to post-process"); + } + + /// + /// Enables the layer effect if it isn't already enabled + /// + internal void InternalEnable() + { + if (Enabled) + return; + + EnableLayerEffect(); + Enabled = true; + } + + /// + /// Disables the layer effect if it isn't already disabled + /// + internal void InternalDisable() + { + if (!Enabled) + return; + + DisableLayerEffect(); + Enabled = false; + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public void Load() + { + HasBeenRenamed = LayerEffectEntity.HasBeenRenamed; + Name = HasBeenRenamed ? LayerEffectEntity.Name : Descriptor.DisplayName; + Order = LayerEffectEntity.Order; + } + + /// + public void Save() + { + LayerEffectEntity.ProviderId = Descriptor.Provider.Id; + LayerEffectEntity.EffectType = GetType().FullName; + LayerEffectEntity.Name = Name; + LayerEffectEntity.HasBeenRenamed = HasBeenRenamed; + LayerEffectEntity.Order = Order; + + BaseProperties?.ApplyToEntity(); + LayerEffectEntity.PropertyGroup = BaseProperties?.PropertyGroupEntity; + } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerEffects/LayerEffect.cs b/src/Artemis.Core/Plugins/LayerEffects/LayerEffect.cs index 87259d44b..2ea8264bf 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/LayerEffect.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/LayerEffect.cs @@ -1,53 +1,52 @@ using System; -namespace Artemis.Core.LayerEffects +namespace Artemis.Core.LayerEffects; + +/// +/// Represents an effect that applies preprocessing and/or postprocessing to a layer +/// +/// +public abstract class LayerEffect : BaseLayerEffect where T : LayerEffectPropertyGroup, new() { + private T _properties = null!; + /// - /// Represents an effect that applies preprocessing and/or postprocessing to a layer + /// Gets whether all properties on this effect are initialized /// - /// - public abstract class LayerEffect : BaseLayerEffect where T : LayerEffectPropertyGroup, new() + public bool PropertiesInitialized { get; internal set; } + + /// + public override LayerEffectPropertyGroup BaseProperties => Properties; + + /// + /// Gets the properties of this effect. + /// + public T Properties { - private T _properties = null!; - - /// - /// Gets whether all properties on this effect are initialized - /// - public bool PropertiesInitialized { get; internal set; } - - /// - public override LayerEffectPropertyGroup BaseProperties => Properties; - - /// - /// Gets the properties of this effect. - /// - public T Properties + get { - get - { - // I imagine a null reference here can be confusing, so lets throw an exception explaining what to do - if (_properties == null) - throw new InvalidOperationException("Cannot access effect properties until OnPropertiesInitialized has been called"); - return _properties; - } - internal set => _properties = value; + // I imagine a null reference here can be confusing, so lets throw an exception explaining what to do + if (_properties == null) + throw new InvalidOperationException("Cannot access effect properties until OnPropertiesInitialized has been called"); + return _properties; } - - internal override void Initialize() - { - InitializeProperties(); - } - - private void InitializeProperties() - { - Properties = new T(); - Properties.Initialize(ProfileElement, null, new PropertyGroupDescriptionAttribute {Identifier = "LayerEffect"}, LayerEffectEntity.PropertyGroup); - - // Initialize will call PopulateDefaults but that is for plugin developers so can't rely on that to default IsEnabled to true - Properties.InitializeIsEnabled(); - PropertiesInitialized = true; + internal set => _properties = value; + } - EnableLayerEffect(); - } + internal override void Initialize() + { + InitializeProperties(); + } + + private void InitializeProperties() + { + Properties = new T(); + Properties.Initialize(ProfileElement, null, new PropertyGroupDescriptionAttribute {Identifier = "LayerEffect"}, LayerEffectEntity.PropertyGroup); + + // Initialize will call PopulateDefaults but that is for plugin developers so can't rely on that to default IsEnabled to true + Properties.InitializeIsEnabled(); + PropertiesInitialized = true; + + EnableLayerEffect(); } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerEffects/LayerEffectProvider.cs b/src/Artemis.Core/Plugins/LayerEffects/LayerEffectProvider.cs index d777ad622..4c307acd3 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/LayerEffectProvider.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/LayerEffectProvider.cs @@ -1,59 +1,58 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.IO; -namespace Artemis.Core.LayerEffects +namespace Artemis.Core.LayerEffects; + +/// +/// Allows you to register one or more s usable by profile layers. +/// +public abstract class LayerEffectProvider : PluginFeature { + private readonly List _layerEffectDescriptors; + /// /// Allows you to register one or more s usable by profile layers. /// - public abstract class LayerEffectProvider : PluginFeature + protected LayerEffectProvider() { - private readonly List _layerEffectDescriptors; + _layerEffectDescriptors = new List(); + LayerEffectDescriptors = new ReadOnlyCollection(_layerEffectDescriptors); + Disabled += OnDisabled; + } - /// - /// Allows you to register one or more s usable by profile layers. - /// - protected LayerEffectProvider() - { - _layerEffectDescriptors = new List(); - LayerEffectDescriptors = new ReadOnlyCollection(_layerEffectDescriptors); - Disabled += OnDisabled; - } + /// + /// A read-only collection of all layer effects added with + /// + public ReadOnlyCollection LayerEffectDescriptors { get; } - /// - /// A read-only collection of all layer effects added with - /// - public ReadOnlyCollection LayerEffectDescriptors { get; } + /// + /// Adds a layer effect descriptor for a given layer effect, so that it appears in the UI. + /// Note: You do not need to manually remove these on disable + /// + /// The type of the layer effect you wish to register + /// The name to display in the UI + /// The description to display in the UI + /// + /// The Material icon to display in the UI, a full reference can be found + /// here. + /// May also be a path to an SVG file relative to the directory of the plugin. + /// + protected void RegisterLayerEffectDescriptor(string displayName, string description, string icon) where T : BaseLayerEffect + { + if (!IsEnabled) + throw new ArtemisPluginFeatureException(this, "Can only add a layer effect descriptor when the plugin is enabled"); - /// - /// Adds a layer effect descriptor for a given layer effect, so that it appears in the UI. - /// Note: You do not need to manually remove these on disable - /// - /// The type of the layer effect you wish to register - /// The name to display in the UI - /// The description to display in the UI - /// - /// The Material icon to display in the UI, a full reference can be found here. - /// May also be a path to an SVG file relative to the directory of the plugin. - /// - protected void RegisterLayerEffectDescriptor(string displayName, string description, string icon) where T : BaseLayerEffect - { - if (!IsEnabled) - throw new ArtemisPluginFeatureException(this, "Can only add a layer effect descriptor when the plugin is enabled"); + if (icon.Contains('.')) + icon = Plugin.ResolveRelativePath(icon); + LayerEffectDescriptor descriptor = new(displayName, description, icon, typeof(T), this); + _layerEffectDescriptors.Add(descriptor); + LayerEffectStore.Add(descriptor); + } - if (icon.Contains('.')) - icon = Plugin.ResolveRelativePath(icon); - LayerEffectDescriptor descriptor = new(displayName, description, icon, typeof(T), this); - _layerEffectDescriptors.Add(descriptor); - LayerEffectStore.Add(descriptor); - } - - private void OnDisabled(object? sender, EventArgs e) - { - // The store will clean up the registrations by itself, the plugin just needs to clear its own list - _layerEffectDescriptors.Clear(); - } + private void OnDisabled(object? sender, EventArgs e) + { + // The store will clean up the registrations by itself, the plugin just needs to clear its own list + _layerEffectDescriptors.Clear(); } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffect.cs b/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffect.cs index c9b187088..5ccbdd05b 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffect.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffect.cs @@ -1,74 +1,73 @@ using Artemis.Storage.Entities.Profile; using SkiaSharp; -namespace Artemis.Core.LayerEffects.Placeholder +namespace Artemis.Core.LayerEffects.Placeholder; + +/// +/// Represents a layer effect that could not be loaded due to a missing plugin +/// +internal class PlaceholderLayerEffect : LayerEffect { - /// - /// Represents a layer effect that could not be loaded due to a missing plugin - /// - internal class PlaceholderLayerEffect : LayerEffect + internal PlaceholderLayerEffect(LayerEffectEntity originalEntity, string placeholderFor) { - internal PlaceholderLayerEffect(LayerEffectEntity originalEntity, string placeholderFor) - { - OriginalEntity = originalEntity; - PlaceholderFor = placeholderFor; + OriginalEntity = originalEntity; + PlaceholderFor = placeholderFor; - LayerEffectEntity = originalEntity; - Order = OriginalEntity.Order; - Name = OriginalEntity.Name; - HasBeenRenamed = OriginalEntity.HasBeenRenamed; - } - - public string PlaceholderFor { get; } - - internal LayerEffectEntity OriginalEntity { get; } - - /// - public override void EnableLayerEffect() - { - } - - /// - public override void DisableLayerEffect() - { - } - - /// - public override void Update(double deltaTime) - { - } - - /// - public override void PreProcess(SKCanvas canvas, SKRect bounds, SKPaint paint) - { - } - - /// - public override void PostProcess(SKCanvas canvas, SKRect bounds, SKPaint paint) - { - } - - internal override string GetEffectTypeName() - { - return OriginalEntity.EffectType; - } + LayerEffectEntity = originalEntity; + Order = OriginalEntity.Order; + Name = OriginalEntity.Name; + HasBeenRenamed = OriginalEntity.HasBeenRenamed; } - /// - /// This is in place so that the UI has something to show - /// - internal class PlaceholderProperties : LayerEffectPropertyGroup + public string PlaceholderFor { get; } + + internal LayerEffectEntity OriginalEntity { get; } + + /// + public override void EnableLayerEffect() { - protected override void PopulateDefaults() - { - } + } - protected override void EnableProperties() - { - } + /// + public override void DisableLayerEffect() + { + } - protected override void DisableProperties() - { - } + /// + public override void Update(double deltaTime) + { + } + + /// + public override void PreProcess(SKCanvas canvas, SKRect bounds, SKPaint paint) + { + } + + /// + public override void PostProcess(SKCanvas canvas, SKRect bounds, SKPaint paint) + { + } + + internal override string GetEffectTypeName() + { + return OriginalEntity.EffectType; + } +} + +/// +/// This is in place so that the UI has something to show +/// +internal class PlaceholderProperties : LayerEffectPropertyGroup +{ + protected override void PopulateDefaults() + { + } + + protected override void EnableProperties() + { + } + + protected override void DisableProperties() + { } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffectDescriptor.cs b/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffectDescriptor.cs index a5496d986..27628a2d2 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffectDescriptor.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffectDescriptor.cs @@ -1,11 +1,10 @@ -namespace Artemis.Core.LayerEffects.Placeholder +namespace Artemis.Core.LayerEffects.Placeholder; + +internal static class PlaceholderLayerEffectDescriptor { - internal static class PlaceholderLayerEffectDescriptor + public static LayerEffectDescriptor Create(string missingProviderId) { - public static LayerEffectDescriptor Create(string missingProviderId) - { - LayerEffectDescriptor descriptor = new(missingProviderId, Constants.EffectPlaceholderPlugin); - return descriptor; - } + LayerEffectDescriptor descriptor = new(missingProviderId, Constants.EffectPlaceholderPlugin); + return descriptor; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Modules/ActivationRequirements/BooleanActivationRequirement.cs b/src/Artemis.Core/Plugins/Modules/ActivationRequirements/BooleanActivationRequirement.cs index ca240c5ab..c3c98d319 100644 --- a/src/Artemis.Core/Plugins/Modules/ActivationRequirements/BooleanActivationRequirement.cs +++ b/src/Artemis.Core/Plugins/Modules/ActivationRequirements/BooleanActivationRequirement.cs @@ -1,25 +1,24 @@ -namespace Artemis.Core.Modules +namespace Artemis.Core.Modules; + +/// +/// Evaluates to true or false by returning the value of ActivationMet +/// +public class BooleanActivationRequirement : IModuleActivationRequirement { /// - /// Evaluates to true or false by returning the value of ActivationMet + /// Gets or sets whether the activation requirement is met /// - public class BooleanActivationRequirement : IModuleActivationRequirement + public bool ActivationMet { get; set; } + + /// + public bool Evaluate() { - /// - /// Gets or sets whether the activation requirement is met - /// - public bool ActivationMet { get; set; } + return ActivationMet; + } - /// - public bool Evaluate() - { - return ActivationMet; - } - - /// - public string GetUserFriendlyDescription() - { - return "No description available"; - } + /// + public string GetUserFriendlyDescription() + { + return "No description available"; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Modules/ActivationRequirements/IModuleActivationRequirement.cs b/src/Artemis.Core/Plugins/Modules/ActivationRequirements/IModuleActivationRequirement.cs index 7df147b8b..810b83ca8 100644 --- a/src/Artemis.Core/Plugins/Modules/ActivationRequirements/IModuleActivationRequirement.cs +++ b/src/Artemis.Core/Plugins/Modules/ActivationRequirements/IModuleActivationRequirement.cs @@ -1,20 +1,19 @@ -namespace Artemis.Core.Modules +namespace Artemis.Core.Modules; + +/// +/// Evaluates to true or false by checking requirements specific to the implementation +/// +public interface IModuleActivationRequirement { /// - /// Evaluates to true or false by checking requirements specific to the implementation + /// Called to determine whether the activation requirement is met /// - public interface IModuleActivationRequirement - { - /// - /// Called to determine whether the activation requirement is met - /// - /// - bool Evaluate(); + /// + bool Evaluate(); - /// - /// Returns a user-friendly description of the activation requirement, should include parameters if applicable - /// - /// A user-friendly description of the activation requirement - string GetUserFriendlyDescription(); - } + /// + /// Returns a user-friendly description of the activation requirement, should include parameters if applicable + /// + /// A user-friendly description of the activation requirement + string GetUserFriendlyDescription(); } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Modules/ActivationRequirements/ProcessActivationRequirement.cs b/src/Artemis.Core/Plugins/Modules/ActivationRequirements/ProcessActivationRequirement.cs index 87ee34783..0be74fe7b 100644 --- a/src/Artemis.Core/Plugins/Modules/ActivationRequirements/ProcessActivationRequirement.cs +++ b/src/Artemis.Core/Plugins/Modules/ActivationRequirements/ProcessActivationRequirement.cs @@ -5,62 +5,61 @@ using System.IO; using System.Linq; using Artemis.Core.Services; -namespace Artemis.Core.Modules +namespace Artemis.Core.Modules; + +/// +/// Evaluates to true or false by checking if the specified process is running +/// +public class ProcessActivationRequirement : IModuleActivationRequirement { /// - /// Evaluates to true or false by checking if the specified process is running + /// Creates a new instance of the class /// - public class ProcessActivationRequirement : IModuleActivationRequirement + /// The name of the process that must run + /// The location of where the process must be running from (optional) + public ProcessActivationRequirement(string? processName, string? location = null) { - /// - /// Creates a new instance of the class - /// - /// The name of the process that must run - /// The location of where the process must be running from (optional) - public ProcessActivationRequirement(string? processName, string? location = null) - { - if (string.IsNullOrWhiteSpace(processName) && string.IsNullOrWhiteSpace(location)) - throw new ArgumentNullException($"Atleast one {nameof(processName)} and {nameof(location)} must not be null"); + if (string.IsNullOrWhiteSpace(processName) && string.IsNullOrWhiteSpace(location)) + throw new ArgumentNullException($"Atleast one {nameof(processName)} and {nameof(location)} must not be null"); - ProcessName = processName; - Location = location; - } + ProcessName = processName; + Location = location; + } - /// - /// The name of the process that must run - /// - public string? ProcessName { get; set; } + /// + /// The name of the process that must run + /// + public string? ProcessName { get; set; } - /// - /// The location of where the process must be running from - /// - public string? Location { get; set; } + /// + /// The location of where the process must be running from + /// + public string? Location { get; set; } - internal static IProcessMonitorService? ProcessMonitorService { get; set; } + internal static IProcessMonitorService? ProcessMonitorService { get; set; } - /// - public bool Evaluate() - { - if (ProcessMonitorService == null || ProcessName == null && Location == null) - return false; + /// + public bool Evaluate() + { + if (ProcessMonitorService == null || (ProcessName == null && Location == null)) + return false; - IEnumerable processes = ProcessMonitorService.GetRunningProcesses(); - if (ProcessName != null) - processes = processes.Where(p => string.Equals(p.ProcessName, ProcessName, StringComparison.InvariantCultureIgnoreCase)); - if (Location != null) - processes = processes.Where(p => string.Equals(Path.GetDirectoryName(p.GetProcessFilename()), Location, StringComparison.InvariantCultureIgnoreCase)); + IEnumerable processes = ProcessMonitorService.GetRunningProcesses(); + if (ProcessName != null) + processes = processes.Where(p => string.Equals(p.ProcessName, ProcessName, StringComparison.InvariantCultureIgnoreCase)); + if (Location != null) + processes = processes.Where(p => string.Equals(Path.GetDirectoryName(p.GetProcessFilename()), Location, StringComparison.InvariantCultureIgnoreCase)); - return processes.Any(); - } + return processes.Any(); + } - /// - public string GetUserFriendlyDescription() - { - string description = $"Requirement met when \"{ProcessName}.exe\" is running"; - if (Location != null) - description += $" from \"{Location}\""; + /// + public string GetUserFriendlyDescription() + { + string description = $"Requirement met when \"{ProcessName}.exe\" is running"; + if (Location != null) + description += $" from \"{Location}\""; - return description; - } + return description; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Modules/Attributes/DataModelIgnoreAttribute.cs b/src/Artemis.Core/Plugins/Modules/Attributes/DataModelIgnoreAttribute.cs index 6f3d06387..852afadd8 100644 --- a/src/Artemis.Core/Plugins/Modules/Attributes/DataModelIgnoreAttribute.cs +++ b/src/Artemis.Core/Plugins/Modules/Attributes/DataModelIgnoreAttribute.cs @@ -1,11 +1,10 @@ using System; -namespace Artemis.Core.Modules +namespace Artemis.Core.Modules; + +/// +/// Represents an attribute that marks a data model property to be ignored by the UI +/// +public class DataModelIgnoreAttribute : Attribute { - /// - /// Represents an attribute that marks a data model property to be ignored by the UI - /// - public class DataModelIgnoreAttribute : Attribute - { - } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Modules/Attributes/DataModelProperty.cs b/src/Artemis.Core/Plugins/Modules/Attributes/DataModelProperty.cs index 1b7793f2d..9eed7ec15 100644 --- a/src/Artemis.Core/Plugins/Modules/Attributes/DataModelProperty.cs +++ b/src/Artemis.Core/Plugins/Modules/Attributes/DataModelProperty.cs @@ -1,51 +1,50 @@ using System; -namespace Artemis.Core.Modules +namespace Artemis.Core.Modules; + +/// +/// Represents an attribute that describes a data model property +/// +[AttributeUsage(AttributeTargets.Property)] +public class DataModelPropertyAttribute : Attribute { /// - /// Represents an attribute that describes a data model property + /// Gets or sets the user-friendly name for this property, shown in the UI. /// - [AttributeUsage(AttributeTargets.Property)] - public class DataModelPropertyAttribute : Attribute - { - /// - /// Gets or sets the user-friendly name for this property, shown in the UI. - /// - public string? Name { get; set; } + public string? Name { get; set; } - /// - /// Gets or sets the user-friendly description for this property, shown in the UI. - /// - public string? Description { get; set; } + /// + /// Gets or sets the user-friendly description for this property, shown in the UI. + /// + public string? Description { get; set; } - /// - /// Gets or sets the an optional prefix to show before displaying elements in the UI. - /// - public string? Prefix { get; set; } + /// + /// Gets or sets the an optional prefix to show before displaying elements in the UI. + /// + public string? Prefix { get; set; } - /// - /// Gets or sets an optional affix to show behind displaying elements in the UI. - /// - public string? Affix { get; set; } + /// + /// Gets or sets an optional affix to show behind displaying elements in the UI. + /// + public string? Affix { get; set; } - /// - /// Gets or sets the name of list items, only applicable to enumerable data model properties - /// - public string? ListItemName { get; set; } + /// + /// Gets or sets the name of list items, only applicable to enumerable data model properties + /// + public string? ListItemName { get; set; } - /// - /// Gets or sets an optional maximum value, this value is not enforced but used for percentage calculations. - /// - public object? MaxValue { get; set; } + /// + /// Gets or sets an optional maximum value, this value is not enforced but used for percentage calculations. + /// + public object? MaxValue { get; set; } - /// - /// Gets or sets an optional minimum value, this value is not enforced but used for percentage calculations. - /// - public object? MinValue { get; set; } + /// + /// Gets or sets an optional minimum value, this value is not enforced but used for percentage calculations. + /// + public object? MinValue { get; set; } - /// - /// Gets or sets whether this property resets the max depth of the data model, defaults to true - /// - public bool ResetsDepth { get; set; } = true; - } + /// + /// Gets or sets whether this property resets the max depth of the data model, defaults to true + /// + public bool ResetsDepth { get; set; } = true; } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Modules/DataModel.cs b/src/Artemis.Core/Plugins/Modules/DataModel.cs index ea1cacd6d..2fabac52f 100644 --- a/src/Artemis.Core/Plugins/Modules/DataModel.cs +++ b/src/Artemis.Core/Plugins/Modules/DataModel.cs @@ -7,381 +7,372 @@ using System.Reflection; using Humanizer; using Newtonsoft.Json; -namespace Artemis.Core.Modules +namespace Artemis.Core.Modules; + +/// +/// Represents a data model that contains information on a game/application etc. +/// +public abstract class DataModel { + private readonly List _activePaths = new(); + private readonly HashSet _activePathsHashSet = new(); + private readonly Dictionary _dynamicChildren = new(); + /// - /// Represents a data model that contains information on a game/application etc. + /// Creates a new instance of the class /// - public abstract class DataModel + protected DataModel() { - private readonly HashSet _activePathsHashSet = new(); - private readonly List _activePaths = new(); - private readonly Dictionary _dynamicChildren = new(); + // These are both set right after construction to keep the constructor of inherited classes clean + Module = null!; + DataModelDescription = null!; - /// - /// Creates a new instance of the class - /// - protected DataModel() + ActivePaths = new ReadOnlyCollection(_activePaths); + DynamicChildren = new ReadOnlyDictionary(_dynamicChildren); + } + + /// + /// Gets the module this data model belongs to + /// + [JsonIgnore] + [DataModelIgnore] + public Module Module { get; internal set; } + + /// + /// Gets the describing this data model + /// + [JsonIgnore] + [DataModelIgnore] + public DataModelPropertyAttribute DataModelDescription { get; internal set; } + + /// + /// Gets the is expansion status indicating whether this data model expands the main data model + /// + [DataModelIgnore] + public bool IsExpansion { get; internal set; } + + /// + /// Gets an read-only dictionary of all dynamic children + /// + [DataModelIgnore] + public ReadOnlyDictionary DynamicChildren { get; } + + /// + /// Gets a read-only list of s targeting this data model + /// + [DataModelIgnore] + public ReadOnlyCollection ActivePaths { get; } + + /// + /// Returns a read-only collection of all properties in this datamodel that are to be ignored + /// + /// + public ReadOnlyCollection GetHiddenProperties() + { + return Module.HiddenProperties; + } + + /// + /// Gets the property description of the provided property info + /// + /// If found, the property description attribute, otherwise . + public virtual DataModelPropertyAttribute? GetPropertyDescription(PropertyInfo propertyInfo) + { + return (DataModelPropertyAttribute?) Attribute.GetCustomAttribute(propertyInfo, typeof(DataModelPropertyAttribute)); + } + + /// + /// Occurs when a dynamic child has been added to this data model + /// + public event EventHandler? DynamicChildAdded; + + /// + /// Occurs when a dynamic child has been removed from this data model + /// + public event EventHandler? DynamicChildRemoved; + + /// + /// Occurs when a dynamic child has been added to this data model + /// + public event EventHandler? ActivePathAdded; + + /// + /// Occurs when a dynamic child has been removed from this data model + /// + public event EventHandler? ActivePathRemoved; + + /// + /// Invokes the event + /// + protected virtual void OnDynamicDataModelAdded(DynamicDataModelChildEventArgs e) + { + DynamicChildAdded?.Invoke(this, e); + } + + /// + /// Invokes the event + /// + protected virtual void OnDynamicDataModelRemoved(DynamicDataModelChildEventArgs e) + { + DynamicChildRemoved?.Invoke(this, e); + } + + /// + /// Invokes the event + /// + protected virtual void OnActivePathAdded(DataModelPathEventArgs e) + { + ActivePathAdded?.Invoke(this, e); + } + + /// + /// Invokes the event + /// + protected virtual void OnActivePathRemoved(DataModelPathEventArgs e) + { + ActivePathRemoved?.Invoke(this, e); + } + + #region Dynamic children + + /// + /// Adds a dynamic child to this data model + /// + /// The key of the child, must be unique to this data model + /// The initial value of the dynamic child + /// The resulting dynamic child which can be used to further update the value + public DynamicChild AddDynamicChild(string key, T initialValue) + { + return AddDynamicChild(key, initialValue, new DataModelPropertyAttribute()); + } + + /// + /// Adds a dynamic child to this data model + /// + /// The key of the child, must be unique to this data model + /// The initial value of the dynamic child + /// A human readable name for your dynamic child, shown in the UI + /// An optional description, shown in the UI + /// The resulting dynamic child which can be used to further update the value + public DynamicChild AddDynamicChild(string key, T initialValue, string name, string? description = null) + { + return AddDynamicChild(key, initialValue, new DataModelPropertyAttribute {Name = name, Description = description}); + } + + /// + /// Adds a dynamic child to this data model + /// + /// The key of the child, must be unique to this data model + /// The initial value of the dynamic child + /// A data model property attribute describing the dynamic child + /// The resulting dynamic child which can be used to further update the value + public DynamicChild AddDynamicChild(string key, T initialValue, DataModelPropertyAttribute attribute) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + if (initialValue == null) throw new ArgumentNullException(nameof(initialValue)); + if (attribute == null) throw new ArgumentNullException(nameof(attribute)); + if (key.Contains('.')) + throw new ArtemisCoreException("The provided key contains an illegal character (.)"); + if (_dynamicChildren.ContainsKey(key)) + throw new ArtemisCoreException($"Cannot add a dynamic child with key '{key}' " + + "because the key is already in use on by another dynamic property this data model."); + + if (GetType().GetProperty(key) != null) + throw new ArtemisCoreException($"Cannot add a dynamic child with key '{key}' " + + "because the key is already in use by a static property on this data model."); + + // Make sure a name is on the attribute or funny things might happen + attribute.Name ??= key.Humanize(); + if (initialValue is DataModel dynamicDataModel) { - // These are both set right after construction to keep the constructor of inherited classes clean - Module = null!; - DataModelDescription = null!; - - ActivePaths = new ReadOnlyCollection(_activePaths); - DynamicChildren = new ReadOnlyDictionary(_dynamicChildren); + dynamicDataModel.Module = Module; + dynamicDataModel.DataModelDescription = attribute; } - /// - /// Gets the module this data model belongs to - /// - [JsonIgnore] - [DataModelIgnore] - public Module Module { get; internal set; } + DynamicChild dynamicChild = new(initialValue, key, attribute); + _dynamicChildren.Add(key, dynamicChild); - /// - /// Gets the describing this data model - /// - [JsonIgnore] - [DataModelIgnore] - public DataModelPropertyAttribute DataModelDescription { get; internal set; } + OnDynamicDataModelAdded(new DynamicDataModelChildEventArgs(dynamicChild, key)); + return dynamicChild; + } - /// - /// Gets the is expansion status indicating whether this data model expands the main data model - /// - [DataModelIgnore] - public bool IsExpansion { get; internal set; } + /// + /// Gets a previously added dynamic child by its key + /// + /// The key of the dynamic child + public DynamicChild GetDynamicChild(string key) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + return DynamicChildren[key]; + } - /// - /// Gets an read-only dictionary of all dynamic children - /// - [DataModelIgnore] - public ReadOnlyDictionary DynamicChildren { get; } + /// + /// Gets a previously added dynamic child by its key + /// + /// The typer of dynamic child you are expecting + /// The key of the dynamic child + /// + public DynamicChild GetDynamicChild(string key) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + return (DynamicChild) DynamicChildren[key]; + } - /// - /// Gets a read-only list of s targeting this data model - /// - [DataModelIgnore] - public ReadOnlyCollection ActivePaths { get; } + /// + /// Gets a previously added dynamic child by its key + /// + /// The key of the dynamic child + /// + /// When this method returns, the associated with the specified key, + /// if the key is found; otherwise, . This parameter is passed uninitialized. + /// + /// + /// if the data model contains the dynamic child; otherwise + /// + public bool TryGetDynamicChild(string key, [MaybeNullWhen(false)] out DynamicChild dynamicChild) + { + if (key == null) throw new ArgumentNullException(nameof(key)); - /// - /// Returns a read-only collection of all properties in this datamodel that are to be ignored - /// - /// - public ReadOnlyCollection GetHiddenProperties() + dynamicChild = null; + if (!DynamicChildren.TryGetValue(key, out DynamicChild? value)) + return false; + + dynamicChild = value; + return true; + } + + /// + /// Gets a previously added dynamic child by its key + /// + /// The typer of dynamic child you are expecting + /// The key of the dynamic child + /// + /// When this method returns, the associated with the specified + /// key, if the key is found and the type matches; otherwise, . This parameter is passed + /// uninitialized. + /// + /// + /// if the data model contains the dynamic child; otherwise + /// + public bool TryGetDynamicChild(string key, [MaybeNullWhen(false)] out DynamicChild dynamicChild) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + + dynamicChild = null; + if (!DynamicChildren.TryGetValue(key, out DynamicChild? value)) + return false; + if (value is not DynamicChild typedDynamicChild) + return false; + dynamicChild = typedDynamicChild; + return true; + } + + /// + /// Removes a dynamic child from the data model by its key + /// + /// The key of the dynamic child to remove + public void RemoveDynamicChildByKey(string key) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + if (!_dynamicChildren.TryGetValue(key, out DynamicChild? dynamicChild)) + return; + + _dynamicChildren.Remove(key); + OnDynamicDataModelRemoved(new DynamicDataModelChildEventArgs(dynamicChild, key)); + } + + /// + /// Removes a dynamic child from this data model + /// + /// The dynamic data child to remove + public void RemoveDynamicChild(DynamicChild dynamicChild) + { + if (dynamicChild == null) throw new ArgumentNullException(nameof(dynamicChild)); + List keys = _dynamicChildren.Where(kvp => kvp.Value.BaseValue == dynamicChild).Select(kvp => kvp.Key).ToList(); + foreach (string key in keys) { - return Module.HiddenProperties; - } - - /// - /// Gets the property description of the provided property info - /// - /// If found, the property description attribute, otherwise . - public virtual DataModelPropertyAttribute? GetPropertyDescription(PropertyInfo propertyInfo) - { - return (DataModelPropertyAttribute?) Attribute.GetCustomAttribute(propertyInfo, typeof(DataModelPropertyAttribute)); - } - - #region Dynamic children - - /// - /// Adds a dynamic child to this data model - /// - /// The key of the child, must be unique to this data model - /// The initial value of the dynamic child - /// The resulting dynamic child which can be used to further update the value - public DynamicChild AddDynamicChild(string key, T initialValue) - { - return AddDynamicChild(key, initialValue, new DataModelPropertyAttribute()); - } - - /// - /// Adds a dynamic child to this data model - /// - /// The key of the child, must be unique to this data model - /// The initial value of the dynamic child - /// A human readable name for your dynamic child, shown in the UI - /// An optional description, shown in the UI - /// The resulting dynamic child which can be used to further update the value - public DynamicChild AddDynamicChild(string key, T initialValue, string name, string? description = null) - { - return AddDynamicChild(key, initialValue, new DataModelPropertyAttribute {Name = name, Description = description}); - } - - /// - /// Adds a dynamic child to this data model - /// - /// The key of the child, must be unique to this data model - /// The initial value of the dynamic child - /// A data model property attribute describing the dynamic child - /// The resulting dynamic child which can be used to further update the value - public DynamicChild AddDynamicChild(string key, T initialValue, DataModelPropertyAttribute attribute) - { - if (key == null) throw new ArgumentNullException(nameof(key)); - if (initialValue == null) throw new ArgumentNullException(nameof(initialValue)); - if (attribute == null) throw new ArgumentNullException(nameof(attribute)); - if (key.Contains('.')) - throw new ArtemisCoreException("The provided key contains an illegal character (.)"); - if (_dynamicChildren.ContainsKey(key)) - { - throw new ArtemisCoreException($"Cannot add a dynamic child with key '{key}' " + - "because the key is already in use on by another dynamic property this data model."); - } - - if (GetType().GetProperty(key) != null) - { - throw new ArtemisCoreException($"Cannot add a dynamic child with key '{key}' " + - "because the key is already in use by a static property on this data model."); - } - - // Make sure a name is on the attribute or funny things might happen - attribute.Name ??= key.Humanize(); - if (initialValue is DataModel dynamicDataModel) - { - dynamicDataModel.Module = Module; - dynamicDataModel.DataModelDescription = attribute; - } - - DynamicChild dynamicChild = new(initialValue, key, attribute); - _dynamicChildren.Add(key, dynamicChild); - - OnDynamicDataModelAdded(new DynamicDataModelChildEventArgs(dynamicChild, key)); - return dynamicChild; - } - - /// - /// Gets a previously added dynamic child by its key - /// - /// The key of the dynamic child - public DynamicChild GetDynamicChild(string key) - { - if (key == null) throw new ArgumentNullException(nameof(key)); - return DynamicChildren[key]; - } - - /// - /// Gets a previously added dynamic child by its key - /// - /// The typer of dynamic child you are expecting - /// The key of the dynamic child - /// - public DynamicChild GetDynamicChild(string key) - { - if (key == null) throw new ArgumentNullException(nameof(key)); - return (DynamicChild) DynamicChildren[key]; - } - - /// - /// Gets a previously added dynamic child by its key - /// - /// The key of the dynamic child - /// - /// When this method returns, the associated with the specified key, - /// if the key is found; otherwise, . This parameter is passed uninitialized. - /// - /// - /// if the data model contains the dynamic child; otherwise - /// - public bool TryGetDynamicChild(string key, [MaybeNullWhen(false)] out DynamicChild dynamicChild) - { - if (key == null) throw new ArgumentNullException(nameof(key)); - - dynamicChild = null; - if (!DynamicChildren.TryGetValue(key, out DynamicChild? value)) - return false; - - dynamicChild = value; - return true; - } - - /// - /// Gets a previously added dynamic child by its key - /// - /// The typer of dynamic child you are expecting - /// The key of the dynamic child - /// - /// When this method returns, the associated with the specified - /// key, if the key is found and the type matches; otherwise, . This parameter is passed - /// uninitialized. - /// - /// - /// if the data model contains the dynamic child; otherwise - /// - public bool TryGetDynamicChild(string key, [MaybeNullWhen(false)] out DynamicChild dynamicChild) - { - if (key == null) throw new ArgumentNullException(nameof(key)); - - dynamicChild = null; - if (!DynamicChildren.TryGetValue(key, out DynamicChild? value)) - return false; - if (value is not DynamicChild typedDynamicChild) - return false; - dynamicChild = typedDynamicChild; - return true; - } - - /// - /// Removes a dynamic child from the data model by its key - /// - /// The key of the dynamic child to remove - public void RemoveDynamicChildByKey(string key) - { - if (key == null) throw new ArgumentNullException(nameof(key)); - if (!_dynamicChildren.TryGetValue(key, out DynamicChild? dynamicChild)) - return; - _dynamicChildren.Remove(key); OnDynamicDataModelRemoved(new DynamicDataModelChildEventArgs(dynamicChild, key)); } + } - /// - /// Removes a dynamic child from this data model - /// - /// The dynamic data child to remove - public void RemoveDynamicChild(DynamicChild dynamicChild) + /// + /// Removes all dynamic children from this data model + /// + public void ClearDynamicChildren() + { + while (_dynamicChildren.Any()) + RemoveDynamicChildByKey(_dynamicChildren.First().Key); + } + + // Used a runtime by data model paths only + internal T? GetDynamicChildValue(string key) + { + if (TryGetDynamicChild(key, out DynamicChild? dynamicChild) && dynamicChild.BaseValue != null) { - if (dynamicChild == null) throw new ArgumentNullException(nameof(dynamicChild)); - List keys = _dynamicChildren.Where(kvp => kvp.Value.BaseValue == dynamicChild).Select(kvp => kvp.Key).ToList(); - foreach (string key in keys) - { - _dynamicChildren.Remove(key); - OnDynamicDataModelRemoved(new DynamicDataModelChildEventArgs(dynamicChild, key)); - } - } - - /// - /// Removes all dynamic children from this data model - /// - public void ClearDynamicChildren() - { - while (_dynamicChildren.Any()) - RemoveDynamicChildByKey(_dynamicChildren.First().Key); - } - - // Used a runtime by data model paths only - internal T? GetDynamicChildValue(string key) - { - if (TryGetDynamicChild(key, out DynamicChild? dynamicChild) && dynamicChild.BaseValue != null) - { - if (dynamicChild.BaseValue is T value) - return value; - return default; - } - + if (dynamicChild.BaseValue is T value) + return value; return default; } - #endregion - - #region Paths - - /// - /// Determines whether the provided dot-separated path is in use - /// - /// The path to check per example: MyDataModelChild.MyDataModelProperty - /// - /// If any child of the given path will return true as well; if - /// only an exact path match returns . - /// - internal bool IsPropertyInUse(string path, bool includeChildren) - { - path = path.ToUpperInvariant(); - lock (_activePaths) - { - return includeChildren - ? _activePathsHashSet.Any(p => p.StartsWith(path, StringComparison.Ordinal)) - : _activePathsHashSet.Contains(path); - } - } - - internal void AddDataModelPath(DataModelPath path) - { - lock (_activePaths) - { - if (_activePaths.Contains(path)) - return; - - _activePaths.Add(path); - - // Add to the hashset if this is the first path pointing - string hashPath = path.Path.ToUpperInvariant(); - if (!_activePathsHashSet.Contains(hashPath)) - _activePathsHashSet.Add(hashPath); - } - - OnActivePathAdded(new DataModelPathEventArgs(path)); - } - - internal void RemoveDataModelPath(DataModelPath path) - { - lock (_activePaths) - { - if (!_activePaths.Remove(path)) - return; - - // Remove from the hashset if this was the last path pointing there - if (_activePaths.All(p => p.Path != path.Path)) - _activePathsHashSet.Remove(path.Path.ToUpperInvariant()); - } - - OnActivePathRemoved(new DataModelPathEventArgs(path)); - } - - #endregion - - #region Events - - /// - /// Occurs when a dynamic child has been added to this data model - /// - public event EventHandler? DynamicChildAdded; - - /// - /// Occurs when a dynamic child has been removed from this data model - /// - public event EventHandler? DynamicChildRemoved; - - /// - /// Occurs when a dynamic child has been added to this data model - /// - public event EventHandler? ActivePathAdded; - - /// - /// Occurs when a dynamic child has been removed from this data model - /// - public event EventHandler? ActivePathRemoved; - - /// - /// Invokes the event - /// - protected virtual void OnDynamicDataModelAdded(DynamicDataModelChildEventArgs e) - { - DynamicChildAdded?.Invoke(this, e); - } - - /// - /// Invokes the event - /// - protected virtual void OnDynamicDataModelRemoved(DynamicDataModelChildEventArgs e) - { - DynamicChildRemoved?.Invoke(this, e); - } - - /// - /// Invokes the event - /// - protected virtual void OnActivePathAdded(DataModelPathEventArgs e) - { - ActivePathAdded?.Invoke(this, e); - } - - /// - /// Invokes the event - /// - protected virtual void OnActivePathRemoved(DataModelPathEventArgs e) - { - ActivePathRemoved?.Invoke(this, e); - } - - #endregion + return default; } + + #endregion + + #region Paths + + /// + /// Determines whether the provided dot-separated path is in use + /// + /// The path to check per example: MyDataModelChild.MyDataModelProperty + /// + /// If any child of the given path will return true as well; if + /// only an exact path match returns . + /// + internal bool IsPropertyInUse(string path, bool includeChildren) + { + path = path.ToUpperInvariant(); + lock (_activePaths) + { + return includeChildren + ? _activePathsHashSet.Any(p => p.StartsWith(path, StringComparison.Ordinal)) + : _activePathsHashSet.Contains(path); + } + } + + internal void AddDataModelPath(DataModelPath path) + { + lock (_activePaths) + { + if (_activePaths.Contains(path)) + return; + + _activePaths.Add(path); + + // Add to the hashset if this is the first path pointing + string hashPath = path.Path.ToUpperInvariant(); + if (!_activePathsHashSet.Contains(hashPath)) + _activePathsHashSet.Add(hashPath); + } + + OnActivePathAdded(new DataModelPathEventArgs(path)); + } + + internal void RemoveDataModelPath(DataModelPath path) + { + lock (_activePaths) + { + if (!_activePaths.Remove(path)) + return; + + // Remove from the hashset if this was the last path pointing there + if (_activePaths.All(p => p.Path != path.Path)) + _activePathsHashSet.Remove(path.Path.ToUpperInvariant()); + } + + OnActivePathRemoved(new DataModelPathEventArgs(path)); + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Modules/DynamicChild.cs b/src/Artemis.Core/Plugins/Modules/DynamicChild.cs index a0fff499c..889b89bdc 100644 --- a/src/Artemis.Core/Plugins/Modules/DynamicChild.cs +++ b/src/Artemis.Core/Plugins/Modules/DynamicChild.cs @@ -1,66 +1,65 @@ using System; -namespace Artemis.Core.Modules +namespace Artemis.Core.Modules; + +/// +/// Represents a dynamic child value with its property attribute +/// +public class DynamicChild : DynamicChild { - /// - /// Represents a dynamic child value with its property attribute - /// - public class DynamicChild : DynamicChild + internal DynamicChild(T value, string key, DataModelPropertyAttribute attribute) : base(key, attribute, typeof(T)) { - internal DynamicChild(T value, string key, DataModelPropertyAttribute attribute) : base(key, attribute, typeof(T)) - { - Value = value; - } - - /// - /// Gets or sets the current value of the dynamic child - /// - public T Value { get; set; } - - /// - protected override object? GetValue() - { - return Value; - } + Value = value; } /// - /// Represents a dynamic child value with its property attribute + /// Gets or sets the current value of the dynamic child /// - public abstract class DynamicChild + public T Value { get; set; } + + /// + protected override object? GetValue() { - internal DynamicChild(string key, DataModelPropertyAttribute attribute, Type type) - { - if (type == null) throw new ArgumentNullException(nameof(type)); - Key = key ?? throw new ArgumentNullException(nameof(key)); - Attribute = attribute ?? throw new ArgumentNullException(nameof(attribute)); - Type = type; - } - - /// - /// Gets the key of the dynamic child - /// - public string Key { get; } - - /// - /// Gets the attribute describing the dynamic child - /// - public DataModelPropertyAttribute Attribute { get; } - - /// - /// Gets the type of - /// - public Type Type { get; } - - /// - /// Gets the current value of the dynamic child - /// - public object? BaseValue => GetValue(); - - /// - /// Gets the current value of the dynamic child - /// - /// The current value of the dynamic child - protected abstract object? GetValue(); + return Value; } +} + +/// +/// Represents a dynamic child value with its property attribute +/// +public abstract class DynamicChild +{ + internal DynamicChild(string key, DataModelPropertyAttribute attribute, Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + Key = key ?? throw new ArgumentNullException(nameof(key)); + Attribute = attribute ?? throw new ArgumentNullException(nameof(attribute)); + Type = type; + } + + /// + /// Gets the key of the dynamic child + /// + public string Key { get; } + + /// + /// Gets the attribute describing the dynamic child + /// + public DataModelPropertyAttribute Attribute { get; } + + /// + /// Gets the type of + /// + public Type Type { get; } + + /// + /// Gets the current value of the dynamic child + /// + public object? BaseValue => GetValue(); + + /// + /// Gets the current value of the dynamic child + /// + /// The current value of the dynamic child + protected abstract object? GetValue(); } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Modules/IModuleViewModel.cs b/src/Artemis.Core/Plugins/Modules/IModuleViewModel.cs index 861336edc..744d5f87c 100644 --- a/src/Artemis.Core/Plugins/Modules/IModuleViewModel.cs +++ b/src/Artemis.Core/Plugins/Modules/IModuleViewModel.cs @@ -1,10 +1,8 @@ -namespace Artemis.Core.Modules +namespace Artemis.Core.Modules; + +/// +/// The base class for any view model that belongs to a module +/// +public interface IModuleViewModel { - /// - /// The base class for any view model that belongs to a module - /// - public interface IModuleViewModel - { - - } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Modules/Module.cs b/src/Artemis.Core/Plugins/Modules/Module.cs index 5b62963a2..94f73228b 100644 --- a/src/Artemis.Core/Plugins/Modules/Module.cs +++ b/src/Artemis.Core/Plugins/Modules/Module.cs @@ -7,341 +7,340 @@ using System.Linq.Expressions; using System.Reflection; using System.Text; -namespace Artemis.Core.Modules +namespace Artemis.Core.Modules; + +/// +/// Allows you to add new data to the Artemis data model +/// +public abstract class Module : Module where T : DataModel, new() { /// - /// Allows you to add new data to the Artemis data model + /// The data model driving this module + /// Note: This default data model is automatically registered and instantiated upon plugin enable /// - public abstract class Module : Module where T : DataModel, new() + public T DataModel { - /// - /// The data model driving this module - /// Note: This default data model is automatically registered and instantiated upon plugin enable - /// - public T DataModel - { - get => InternalDataModel as T ?? throw new InvalidOperationException("Internal datamodel does not match the type of the data model"); - internal set => InternalDataModel = value; - } - - /// - /// Hide the provided property using a lambda expression, e.g. - /// HideProperty(dm => dm.TimeDataModel.CurrentTimeUTC) - /// - /// A lambda expression pointing to the property to ignore - public void HideProperty(Expression> propertyLambda) - { - PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda); - if (!HiddenPropertiesList.Any(p => p.Equals(propertyInfo))) - HiddenPropertiesList.Add(propertyInfo); - } - - /// - /// Stop hiding the provided property using a lambda expression, e.g. - /// ShowProperty(dm => dm.TimeDataModel.CurrentTimeUTC) - /// - /// A lambda expression pointing to the property to stop ignoring - public void ShowProperty(Expression> propertyLambda) - { - PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda); - HiddenPropertiesList.RemoveAll(p => p.Equals(propertyInfo)); - } - - /// - /// Determines whether the provided dot-separated path is actively being used by Artemis - /// Note: is slightly faster but string-based. - /// - /// - /// The path to check per example: IsPropertyInUse(dm => dm.TimeDataModel.CurrentTimeUTC) - /// - /// - /// If any child of the given path will return true as well; if - /// only an exact path match returns . - /// - public bool IsPropertyInUse(Expression> propertyLambda, bool includeChildren) - { - string path = GetMemberPath((MemberExpression) propertyLambda.Body); - return IsPropertyInUse(path, includeChildren); - } - - /// - /// Determines whether the provided dot-separated path is actively being used by Artemis - /// - /// The path to check per example: MyDataModelChild.MyDataModelProperty - /// - /// If any child of the given path will return true as well; if - /// only an exact path match returns . - /// - public bool IsPropertyInUse(string path, bool includeChildren) - { - return DataModel.IsPropertyInUse(path, includeChildren); - } - - internal override void InternalEnable() - { - DataModel = new T(); - DataModel.Module = this; - DataModel.DataModelDescription = GetDataModelDescription(); - base.InternalEnable(); - } - - internal override void InternalDisable() - { - Deactivate(true); - base.InternalDisable(); - } - - private static string GetMemberPath(MemberExpression? me) - { - StringBuilder builder = new(); - while (me != null) - { - builder.Insert(0, me.Member.Name); - me = me.Expression as MemberExpression; - if (me != null) - builder.Insert(0, "."); - } - - return builder.ToString(); - } + get => InternalDataModel as T ?? throw new InvalidOperationException("Internal datamodel does not match the type of the data model"); + internal set => InternalDataModel = value; } /// - /// For internal use only, please use . + /// Hide the provided property using a lambda expression, e.g. + /// HideProperty(dm => dm.TimeDataModel.CurrentTimeUTC) /// - public abstract class Module : PluginFeature + /// A lambda expression pointing to the property to ignore + public void HideProperty(Expression> propertyLambda) { - private readonly List<(DefaultCategoryName, string)> _defaultProfilePaths = new(); - private readonly List<(DefaultCategoryName, string)> _pendingDefaultProfilePaths = new(); + PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda); + if (!HiddenPropertiesList.Any(p => p.Equals(propertyInfo))) + HiddenPropertiesList.Add(propertyInfo); + } - /// - /// Gets a list of all properties ignored at runtime using IgnoreProperty(x => x.y) - /// - protected internal readonly List HiddenPropertiesList = new(); + /// + /// Stop hiding the provided property using a lambda expression, e.g. + /// ShowProperty(dm => dm.TimeDataModel.CurrentTimeUTC) + /// + /// A lambda expression pointing to the property to stop ignoring + public void ShowProperty(Expression> propertyLambda) + { + PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda); + HiddenPropertiesList.RemoveAll(p => p.Equals(propertyInfo)); + } - /// - /// The base constructor of the class. - /// - protected Module() + /// + /// Determines whether the provided dot-separated path is actively being used by Artemis + /// Note: is slightly faster but string-based. + /// + /// + /// The path to check per example: IsPropertyInUse(dm => dm.TimeDataModel.CurrentTimeUTC) + /// + /// + /// If any child of the given path will return true as well; if + /// only an exact path match returns . + /// + public bool IsPropertyInUse(Expression> propertyLambda, bool includeChildren) + { + string path = GetMemberPath((MemberExpression) propertyLambda.Body); + return IsPropertyInUse(path, includeChildren); + } + + /// + /// Determines whether the provided dot-separated path is actively being used by Artemis + /// + /// The path to check per example: MyDataModelChild.MyDataModelProperty + /// + /// If any child of the given path will return true as well; if + /// only an exact path match returns . + /// + public bool IsPropertyInUse(string path, bool includeChildren) + { + return DataModel.IsPropertyInUse(path, includeChildren); + } + + internal override void InternalEnable() + { + DataModel = new T(); + DataModel.Module = this; + DataModel.DataModelDescription = GetDataModelDescription(); + base.InternalEnable(); + } + + internal override void InternalDisable() + { + Deactivate(true); + base.InternalDisable(); + } + + private static string GetMemberPath(MemberExpression? me) + { + StringBuilder builder = new(); + while (me != null) { - DefaultProfilePaths = new ReadOnlyCollection<(DefaultCategoryName, string)>(_defaultProfilePaths); - HiddenProperties = new ReadOnlyCollection(HiddenPropertiesList); + builder.Insert(0, me.Member.Name); + me = me.Expression as MemberExpression; + if (me != null) + builder.Insert(0, "."); } - /// - /// Gets a read only collection of default profile paths - /// - public IReadOnlyCollection<(DefaultCategoryName, string)> DefaultProfilePaths { get; } + return builder.ToString(); + } +} - /// - /// A list of activation requirements - /// - /// If this list is not and not empty becomes - /// and the data of this module is only available to profiles specifically targeting it. - /// - /// - public abstract List? ActivationRequirements { get; } +/// +/// For internal use only, please use . +/// +public abstract class Module : PluginFeature +{ + private readonly List<(DefaultCategoryName, string)> _defaultProfilePaths = new(); + private readonly List<(DefaultCategoryName, string)> _pendingDefaultProfilePaths = new(); - /// - /// Gets whether this module is activated. A module can only be active while its - /// are met - /// - public bool IsActivated { get; internal set; } + /// + /// Gets a list of all properties ignored at runtime using IgnoreProperty(x => x.y) + /// + protected internal readonly List HiddenPropertiesList = new(); - /// - /// Gets whether this module's activation was due to an override, can only be true if is - /// - /// - public bool IsActivatedOverride { get; private set; } + /// + /// The base constructor of the class. + /// + protected Module() + { + DefaultProfilePaths = new ReadOnlyCollection<(DefaultCategoryName, string)>(_defaultProfilePaths); + HiddenProperties = new ReadOnlyCollection(HiddenPropertiesList); + } - /// - /// Gets whether this module should update while is . When - /// set to and any timed updates will not get called during an - /// activation override. - /// Defaults to - /// - public bool UpdateDuringActivationOverride { get; protected set; } + /// + /// Gets a read only collection of default profile paths + /// + public IReadOnlyCollection<(DefaultCategoryName, string)> DefaultProfilePaths { get; } - /// - /// Gets or sets the activation requirement mode, defaults to - /// - public ActivationRequirementType ActivationRequirementMode { get; set; } = ActivationRequirementType.Any; + /// + /// A list of activation requirements + /// + /// If this list is not and not empty becomes + /// and the data of this module is only available to profiles specifically targeting it. + /// + /// + public abstract List? ActivationRequirements { get; } - /// - /// Gets a boolean indicating whether this module is always available to profiles or only to profiles that specifically - /// target this module. - /// - /// Note: if there are any ; otherwise - /// - /// - /// - public bool IsAlwaysAvailable => ActivationRequirements == null || ActivationRequirements.Count == 0; + /// + /// Gets whether this module is activated. A module can only be active while its + /// are met + /// + public bool IsActivated { get; internal set; } - /// - /// Gets whether updating this module is currently allowed - /// - public bool IsUpdateAllowed => IsActivated && (UpdateDuringActivationOverride || !IsActivatedOverride); + /// + /// Gets whether this module's activation was due to an override, can only be true if is + /// + /// + public bool IsActivatedOverride { get; private set; } - /// - /// Gets a list of all properties ignored at runtime using IgnoreProperty(x => x.y) - /// - public ReadOnlyCollection HiddenProperties { get; } + /// + /// Gets whether this module should update while is . When + /// set to and any timed updates will not get called during an + /// activation override. + /// Defaults to + /// + public bool UpdateDuringActivationOverride { get; protected set; } - internal DataModel? InternalDataModel { get; set; } + /// + /// Gets or sets the activation requirement mode, defaults to + /// + public ActivationRequirementType ActivationRequirementMode { get; set; } = ActivationRequirementType.Any; - /// - /// Called each frame when the module should update - /// - /// Time in seconds since the last update - public abstract void Update(double deltaTime); + /// + /// Gets a boolean indicating whether this module is always available to profiles or only to profiles that specifically + /// target this module. + /// + /// Note: if there are any ; otherwise + /// + /// + /// + public bool IsAlwaysAvailable => ActivationRequirements == null || ActivationRequirements.Count == 0; - /// - /// Called when the are met or during an override - /// - /// - /// If true, the activation was due to an override. This usually means the module was activated - /// by the profile editor - /// - public virtual void ModuleActivated(bool isOverride) + /// + /// Gets whether updating this module is currently allowed + /// + public bool IsUpdateAllowed => IsActivated && (UpdateDuringActivationOverride || !IsActivatedOverride); + + /// + /// Gets a list of all properties ignored at runtime using IgnoreProperty(x => x.y) + /// + public ReadOnlyCollection HiddenProperties { get; } + + internal DataModel? InternalDataModel { get; set; } + + /// + /// Called each frame when the module should update + /// + /// Time in seconds since the last update + public abstract void Update(double deltaTime); + + /// + /// Called when the are met or during an override + /// + /// + /// If true, the activation was due to an override. This usually means the module was activated + /// by the profile editor + /// + public virtual void ModuleActivated(bool isOverride) + { + } + + /// + /// Called when the are no longer met or during an override + /// + /// + /// If true, the deactivation was due to an override. This usually means the module was deactivated + /// by the profile editor + /// + public virtual void ModuleDeactivated(bool isOverride) + { + } + + /// + /// Evaluates the activation requirements following the and returns the result + /// + /// The evaluated result of the activation requirements + public bool EvaluateActivationRequirements() + { + if (IsAlwaysAvailable) + return true; + if (ActivationRequirementMode == ActivationRequirementType.All) + return ActivationRequirements!.All(r => r.Evaluate()); + if (ActivationRequirementMode == ActivationRequirementType.Any) + return ActivationRequirements!.Any(r => r.Evaluate()); + + return false; + } + + /// + /// Override to provide your own data model description. By default this returns a description matching your plugin + /// name and description + /// + /// + public virtual DataModelPropertyAttribute GetDataModelDescription() + { + return new DataModelPropertyAttribute {Name = Info.Name, Description = Info.Description}; + } + + /// + /// Adds a default profile by reading it from the file found at the provided path + /// + /// The category in which to place the default profile + /// A path pointing towards a profile file. May be relative to the plugin directory. + /// + /// if the default profile was added; if it was not because it is + /// already in the list. + /// + protected bool AddDefaultProfile(DefaultCategoryName category, string file) + { + // It can be null if the plugin has not loaded yet in which case Plugin.ResolveRelativePath fails + if (Plugin == null!) { - } - - /// - /// Called when the are no longer met or during an override - /// - /// - /// If true, the deactivation was due to an override. This usually means the module was deactivated - /// by the profile editor - /// - public virtual void ModuleDeactivated(bool isOverride) - { - } - - /// - /// Evaluates the activation requirements following the and returns the result - /// - /// The evaluated result of the activation requirements - public bool EvaluateActivationRequirements() - { - if (IsAlwaysAvailable) - return true; - if (ActivationRequirementMode == ActivationRequirementType.All) - return ActivationRequirements!.All(r => r.Evaluate()); - if (ActivationRequirementMode == ActivationRequirementType.Any) - return ActivationRequirements!.Any(r => r.Evaluate()); - - return false; - } - - /// - /// Override to provide your own data model description. By default this returns a description matching your plugin - /// name and description - /// - /// - public virtual DataModelPropertyAttribute GetDataModelDescription() - { - return new DataModelPropertyAttribute {Name = Info.Name, Description = Info.Description}; - } - - /// - /// Adds a default profile by reading it from the file found at the provided path - /// - /// The category in which to place the default profile - /// A path pointing towards a profile file. May be relative to the plugin directory. - /// - /// if the default profile was added; if it was not because it is - /// already in the list. - /// - protected bool AddDefaultProfile(DefaultCategoryName category, string file) - { - // It can be null if the plugin has not loaded yet in which case Plugin.ResolveRelativePath fails - if (Plugin == null!) - { - if (_pendingDefaultProfilePaths.Contains((category, file))) - return false; - _pendingDefaultProfilePaths.Add((category, file)); - return true; - } - - if (!Path.IsPathRooted(file)) - file = Plugin.ResolveRelativePath(file); - - // Ensure the file exists - if (!File.Exists(file)) - throw new ArtemisPluginFeatureException(this, $"Could not find default profile at {file}."); - - if (_defaultProfilePaths.Contains((category, file))) + if (_pendingDefaultProfilePaths.Contains((category, file))) return false; - _defaultProfilePaths.Add((category, file)); - + _pendingDefaultProfilePaths.Add((category, file)); return true; } - internal virtual void InternalUpdate(double deltaTime) - { - StartUpdateMeasure(); - if (IsUpdateAllowed) - Update(deltaTime); - StopUpdateMeasure(); - } + if (!Path.IsPathRooted(file)) + file = Plugin.ResolveRelativePath(file); - internal virtual void Activate(bool isOverride) - { - if (IsActivated) - return; + // Ensure the file exists + if (!File.Exists(file)) + throw new ArtemisPluginFeatureException(this, $"Could not find default profile at {file}."); - IsActivatedOverride = isOverride; - ModuleActivated(isOverride); - IsActivated = true; - } + if (_defaultProfilePaths.Contains((category, file))) + return false; + _defaultProfilePaths.Add((category, file)); - internal virtual void Deactivate(bool isOverride) - { - if (!IsActivated) - return; - - IsActivatedOverride = false; - IsActivated = false; - ModuleDeactivated(isOverride); - } - - #region Overrides of PluginFeature - - /// - internal override void InternalEnable() - { - foreach ((DefaultCategoryName categoryName, string? path) in _pendingDefaultProfilePaths) - AddDefaultProfile(categoryName, path); - _pendingDefaultProfilePaths.Clear(); - - base.InternalEnable(); - } - - #endregion - - internal virtual void Reactivate(bool isDeactivateOverride, bool isActivateOverride) - { - if (!IsActivated) - return; - - Deactivate(isDeactivateOverride); - Activate(isActivateOverride); - } + return true; } + internal virtual void InternalUpdate(double deltaTime) + { + StartUpdateMeasure(); + if (IsUpdateAllowed) + Update(deltaTime); + StopUpdateMeasure(); + } + + internal virtual void Activate(bool isOverride) + { + if (IsActivated) + return; + + IsActivatedOverride = isOverride; + ModuleActivated(isOverride); + IsActivated = true; + } + + internal virtual void Deactivate(bool isOverride) + { + if (!IsActivated) + return; + + IsActivatedOverride = false; + IsActivated = false; + ModuleDeactivated(isOverride); + } + + #region Overrides of PluginFeature + + /// + internal override void InternalEnable() + { + foreach ((DefaultCategoryName categoryName, string? path) in _pendingDefaultProfilePaths) + AddDefaultProfile(categoryName, path); + _pendingDefaultProfilePaths.Clear(); + + base.InternalEnable(); + } + + #endregion + + internal virtual void Reactivate(bool isDeactivateOverride, bool isActivateOverride) + { + if (!IsActivated) + return; + + Deactivate(isDeactivateOverride); + Activate(isActivateOverride); + } +} + +/// +/// Describes in what way the activation requirements of a module must be met +/// +public enum ActivationRequirementType +{ + /// + /// Any activation requirement must be met for the module to activate + /// + Any, + /// - /// Describes in what way the activation requirements of a module must be met + /// All activation requirements must be met for the module to activate /// - public enum ActivationRequirementType - { - /// - /// Any activation requirement must be met for the module to activate - /// - Any, - - /// - /// All activation requirements must be met for the module to activate - /// - All - } + All } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs index e7e463238..4c0d2cfb9 100644 --- a/src/Artemis.Core/Plugins/Plugin.cs +++ b/src/Artemis.Core/Plugins/Plugin.cs @@ -10,337 +10,334 @@ using Artemis.Storage.Entities.Plugins; using McMaster.NETCore.Plugins; using Ninject; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a plugin +/// +public class Plugin : CorePropertyChanged, IDisposable { - /// - /// Represents a plugin - /// - public class Plugin : CorePropertyChanged, IDisposable + private readonly List _features; + private readonly bool _loadedFromStorage; + private readonly List _profilers; + + private bool _isEnabled; + + internal Plugin(PluginInfo info, DirectoryInfo directory, PluginEntity? pluginEntity) { - private readonly bool _loadedFromStorage; - private readonly List _features; - private readonly List _profilers; + Info = info; + Directory = directory; + Entity = pluginEntity ?? new PluginEntity {Id = Guid}; + Info.Plugin = this; - private bool _isEnabled; + _loadedFromStorage = pluginEntity != null; + _features = new List(); + _profilers = new List(); - internal Plugin(PluginInfo info, DirectoryInfo directory, PluginEntity? pluginEntity) - { - Info = info; - Directory = directory; - Entity = pluginEntity ?? new PluginEntity {Id = Guid}; - Info.Plugin = this; + Features = new ReadOnlyCollection(_features); + Profilers = new ReadOnlyCollection(_profilers); + } - _loadedFromStorage = pluginEntity != null; - _features = new List(); - _profilers = new List(); + /// + /// Gets the plugin GUID + /// + public Guid Guid => Info.Guid; - Features = new ReadOnlyCollection(_features); - Profilers = new ReadOnlyCollection(_profilers); - } + /// + /// Gets the plugin info related to this plugin + /// + public PluginInfo Info { get; } - /// - /// Gets the plugin GUID - /// - public Guid Guid => Info.Guid; + /// + /// The plugins root directory + /// + public DirectoryInfo Directory { get; } - /// - /// Gets the plugin info related to this plugin - /// - public PluginInfo Info { get; } + /// + /// Gets or sets a configuration dialog for this plugin that is accessible in the UI under Settings > Plugins + /// + public IPluginConfigurationDialog? ConfigurationDialog { get; set; } - /// - /// The plugins root directory - /// - public DirectoryInfo Directory { get; } + /// + /// Indicates whether the user enabled the plugin or not + /// + public bool IsEnabled + { + get => _isEnabled; + private set => SetAndNotify(ref _isEnabled, value); + } - /// - /// Gets or sets a configuration dialog for this plugin that is accessible in the UI under Settings > Plugins - /// - public IPluginConfigurationDialog? ConfigurationDialog { get; set; } + /// + /// Gets a read-only collection of all features this plugin provides + /// + public ReadOnlyCollection Features { get; } - /// - /// Indicates whether the user enabled the plugin or not - /// - public bool IsEnabled - { - get => _isEnabled; - private set => SetAndNotify(ref _isEnabled, value); - } + /// + /// Gets a read-only collection of profiles running on the plugin + /// + public ReadOnlyCollection Profilers { get; } - /// - /// Gets a read-only collection of all features this plugin provides - /// - public ReadOnlyCollection Features { get; } + /// + /// The assembly the plugin code lives in + /// + public Assembly? Assembly { get; internal set; } - /// - /// Gets a read-only collection of profiles running on the plugin - /// - public ReadOnlyCollection Profilers { get; } + /// + /// Gets the plugin bootstrapper + /// + public PluginBootstrapper? Bootstrapper { get; internal set; } - /// - /// The assembly the plugin code lives in - /// - public Assembly? Assembly { get; internal set; } + /// + /// The Ninject kernel of the plugin + /// + public IKernel? Kernel { get; internal set; } - /// - /// Gets the plugin bootstrapper - /// - public PluginBootstrapper? Bootstrapper { get; internal set; } + /// + /// The PluginLoader backing this plugin + /// + internal PluginLoader? PluginLoader { get; set; } - /// - /// The Ninject kernel of the plugin - /// - public IKernel? Kernel { get; internal set; } + /// + /// The entity representing the plugin + /// + internal PluginEntity Entity { get; set; } - /// - /// The PluginLoader backing this plugin - /// - internal PluginLoader? PluginLoader { get; set; } + /// + /// Populated when plugin settings are first loaded + /// + internal PluginSettings? Settings { get; set; } - /// - /// The entity representing the plugin - /// - internal PluginEntity Entity { get; set; } + /// + /// Resolves the relative path provided in the parameter to an absolute path + /// + /// The path to resolve + /// An absolute path pointing to the provided relative path + [return: NotNullIfNotNull("path")] + public string? ResolveRelativePath(string? path) + { + return path == null ? null : Path.Combine(Directory.FullName, path); + } - /// - /// Populated when plugin settings are first loaded - /// - internal PluginSettings? Settings { get; set; } + /// + /// Looks up the instance of the feature of type + /// + /// The type of feature to find + /// If found, the instance of the feature + public T? GetFeature() where T : PluginFeature + { + return _features.FirstOrDefault(i => i.Instance is T)?.Instance as T; + } - /// - /// Resolves the relative path provided in the parameter to an absolute path - /// - /// The path to resolve - /// An absolute path pointing to the provided relative path - [return: NotNullIfNotNull("path")] - public string? ResolveRelativePath(string? path) - { - return path == null ? null : Path.Combine(Directory.FullName, path); - } + /// + /// Looks up the feature info the feature of type + /// + /// The type of feature to find + /// Feature info of the feature + public PluginFeatureInfo GetFeatureInfo() where T : PluginFeature + { + // This should be a safe assumption because any type of PluginFeature is registered and added + return _features.First(i => i.FeatureType == typeof(T)); + } - /// - /// Looks up the instance of the feature of type - /// - /// The type of feature to find - /// If found, the instance of the feature - public T? GetFeature() where T : PluginFeature - { - return _features.FirstOrDefault(i => i.Instance is T)?.Instance as T; - } - - /// - /// Looks up the feature info the feature of type - /// - /// The type of feature to find - /// Feature info of the feature - public PluginFeatureInfo GetFeatureInfo() where T : PluginFeature - { - // This should be a safe assumption because any type of PluginFeature is registered and added - return _features.First(i => i.FeatureType == typeof(T)); - } - - /// - /// Gets a profiler with the provided , if it does not yet exist it will be created. - /// - /// The name of the profiler - /// A new or existing profiler with the provided - public Profiler GetProfiler(string name) - { - Profiler? profiler = _profilers.FirstOrDefault(p => p.Name == name); - if (profiler != null) - return profiler; - - profiler = new Profiler(this, name); - _profilers.Add(profiler); + /// + /// Gets a profiler with the provided , if it does not yet exist it will be created. + /// + /// The name of the profiler + /// A new or existing profiler with the provided + public Profiler GetProfiler(string name) + { + Profiler? profiler = _profilers.FirstOrDefault(p => p.Name == name); + if (profiler != null) return profiler; - } - /// - /// Removes a profiler from the plugin - /// - /// The profiler to remove - public void RemoveProfiler(Profiler profiler) + profiler = new Profiler(this, name); + _profilers.Add(profiler); + return profiler; + } + + /// + /// Removes a profiler from the plugin + /// + /// The profiler to remove + public void RemoveProfiler(Profiler profiler) + { + _profilers.Remove(profiler); + } + + /// + /// Gets an instance of the specified service using the plugins dependency injection container. + /// Note: To use parameters reference Ninject and use directly. + /// + /// The service to resolve. + /// An instance of the service. + public T Get() + { + if (Kernel == null) + throw new ArtemisPluginException("Cannot use Get before the plugin finished loading"); + return Kernel.Get(); + } + + /// + public override string ToString() + { + return Info.ToString(); + } + + /// + /// Occurs when the plugin is enabled + /// + public event EventHandler? Enabled; + + /// + /// Occurs when the plugin is disabled + /// + public event EventHandler? Disabled; + + /// + /// Occurs when an feature is loaded and added to the plugin + /// + public event EventHandler? FeatureAdded; + + /// + /// Occurs when an feature is disabled and removed from the plugin + /// + public event EventHandler? FeatureRemoved; + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) { - _profilers.Remove(profiler); - } + foreach (PluginFeatureInfo feature in Features) + feature.Instance?.Dispose(); + SetEnabled(false); - /// - /// Gets an instance of the specified service using the plugins dependency injection container. - /// Note: To use parameters reference Ninject and use directly. - /// - /// The service to resolve. - /// An instance of the service. - public T Get() + Kernel?.Dispose(); + PluginLoader?.Dispose(); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + + _features.Clear(); + } + } + + /// + /// Invokes the Enabled event + /// + protected virtual void OnEnabled() + { + Enabled?.Invoke(this, EventArgs.Empty); + } + + /// + /// Invokes the Disabled event + /// + protected virtual void OnDisabled() + { + Disabled?.Invoke(this, EventArgs.Empty); + } + + /// + /// Invokes the FeatureAdded event + /// + protected virtual void OnFeatureAdded(PluginFeatureInfoEventArgs e) + { + FeatureAdded?.Invoke(this, e); + } + + /// + /// Invokes the FeatureRemoved event + /// + protected virtual void OnFeatureRemoved(PluginFeatureInfoEventArgs e) + { + FeatureRemoved?.Invoke(this, e); + } + + internal void ApplyToEntity() + { + Entity.Id = Guid; + Entity.IsEnabled = IsEnabled; + } + + internal void AddFeature(PluginFeatureInfo featureInfo) + { + if (featureInfo.Plugin != this) + throw new ArtemisCoreException("Feature is not associated with this plugin"); + _features.Add(featureInfo); + + OnFeatureAdded(new PluginFeatureInfoEventArgs(featureInfo)); + } + + internal void RemoveFeature(PluginFeatureInfo featureInfo) + { + if (featureInfo.Instance != null && featureInfo.Instance.IsEnabled) + throw new ArtemisCoreException("Cannot remove an enabled feature from a plugin"); + + _features.Remove(featureInfo); + featureInfo.Instance?.Dispose(); + + OnFeatureRemoved(new PluginFeatureInfoEventArgs(featureInfo)); + } + + internal void SetEnabled(bool enable) + { + if (IsEnabled == enable) + return; + + if (!enable && Features.Any(e => e.Instance != null && e.Instance.IsEnabled)) + throw new ArtemisCoreException("Cannot disable this plugin because it still has enabled features"); + + IsEnabled = enable; + + if (enable) { - if (Kernel == null) - throw new ArtemisPluginException("Cannot use Get before the plugin finished loading"); - return Kernel.Get(); + Bootstrapper?.OnPluginEnabled(this); + OnEnabled(); } - - /// - public override string ToString() + else { - return Info.ToString(); + Bootstrapper?.OnPluginDisabled(this); + OnDisabled(); } + } - /// - /// Occurs when the plugin is enabled - /// - public event EventHandler? Enabled; + internal bool HasEnabledFeatures() + { + return Entity.Features.Any(f => f.IsEnabled) || Features.Any(f => f.AlwaysEnabled); + } - /// - /// Occurs when the plugin is disabled - /// - public event EventHandler? Disabled; + internal void AutoEnableIfNew() + { + if (_loadedFromStorage) + return; - /// - /// Occurs when an feature is loaded and added to the plugin - /// - public event EventHandler? FeatureAdded; + // Enabled is preset to true if the plugin meets the following criteria + // - Requires no admin rights + // - No always-enabled device providers + // - Either has no prerequisites or they are all met + Entity.IsEnabled = !Info.RequiresAdmin && + Features.All(f => !f.AlwaysEnabled || !f.FeatureType.IsAssignableTo(typeof(DeviceProvider))) && + Info.ArePrerequisitesMet(); - /// - /// Occurs when an feature is disabled and removed from the plugin - /// - public event EventHandler? FeatureRemoved; + if (!Entity.IsEnabled) + return; - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - foreach (PluginFeatureInfo feature in Features) - feature.Instance?.Dispose(); - SetEnabled(false); + // Also auto-enable any non-device provider feature + foreach (PluginFeatureInfo pluginFeatureInfo in Features) + pluginFeatureInfo.Entity.IsEnabled = !pluginFeatureInfo.FeatureType.IsAssignableTo(typeof(DeviceProvider)); + } - Kernel?.Dispose(); - PluginLoader?.Dispose(); - - GC.Collect(); - GC.WaitForPendingFinalizers(); - - _features.Clear(); - } - } - - /// - /// Invokes the Enabled event - /// - protected virtual void OnEnabled() - { - Enabled?.Invoke(this, EventArgs.Empty); - } - - /// - /// Invokes the Disabled event - /// - protected virtual void OnDisabled() - { - Disabled?.Invoke(this, EventArgs.Empty); - } - - /// - /// Invokes the FeatureAdded event - /// - protected virtual void OnFeatureAdded(PluginFeatureInfoEventArgs e) - { - FeatureAdded?.Invoke(this, e); - } - - /// - /// Invokes the FeatureRemoved event - /// - protected virtual void OnFeatureRemoved(PluginFeatureInfoEventArgs e) - { - FeatureRemoved?.Invoke(this, e); - } - - internal void ApplyToEntity() - { - Entity.Id = Guid; - Entity.IsEnabled = IsEnabled; - } - - internal void AddFeature(PluginFeatureInfo featureInfo) - { - if (featureInfo.Plugin != this) - throw new ArtemisCoreException("Feature is not associated with this plugin"); - _features.Add(featureInfo); - - OnFeatureAdded(new PluginFeatureInfoEventArgs(featureInfo)); - } - - internal void RemoveFeature(PluginFeatureInfo featureInfo) - { - if (featureInfo.Instance != null && featureInfo.Instance.IsEnabled) - throw new ArtemisCoreException("Cannot remove an enabled feature from a plugin"); - - _features.Remove(featureInfo); - featureInfo.Instance?.Dispose(); - - OnFeatureRemoved(new PluginFeatureInfoEventArgs(featureInfo)); - } - - internal void SetEnabled(bool enable) - { - if (IsEnabled == enable) - return; - - if (!enable && Features.Any(e => e.Instance != null && e.Instance.IsEnabled)) - throw new ArtemisCoreException("Cannot disable this plugin because it still has enabled features"); - - IsEnabled = enable; - - if (enable) - { - Bootstrapper?.OnPluginEnabled(this); - OnEnabled(); - } - else - { - Bootstrapper?.OnPluginDisabled(this); - OnDisabled(); - } - } - - internal bool HasEnabledFeatures() - { - return Entity.Features.Any(f => f.IsEnabled) || Features.Any(f => f.AlwaysEnabled); - } - - internal void AutoEnableIfNew() - { - if (_loadedFromStorage) - return; - - // Enabled is preset to true if the plugin meets the following criteria - // - Requires no admin rights - // - No always-enabled device providers - // - Either has no prerequisites or they are all met - Entity.IsEnabled = !Info.RequiresAdmin && - Features.All(f => !f.AlwaysEnabled || !f.FeatureType.IsAssignableTo(typeof(DeviceProvider))) && - Info.ArePrerequisitesMet(); - - if (!Entity.IsEnabled) - return; - - // Also auto-enable any non-device provider feature - foreach (PluginFeatureInfo pluginFeatureInfo in Features) - pluginFeatureInfo.Entity.IsEnabled = !pluginFeatureInfo.FeatureType.IsAssignableTo(typeof(DeviceProvider)); - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginBootstrapper.cs b/src/Artemis.Core/Plugins/PluginBootstrapper.cs index 0d41ad36d..961c32a07 100644 --- a/src/Artemis.Core/Plugins/PluginBootstrapper.cs +++ b/src/Artemis.Core/Plugins/PluginBootstrapper.cs @@ -1,100 +1,99 @@ -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// An optional entry point for your plugin +/// +public abstract class PluginBootstrapper { + private Plugin? _plugin; + /// - /// An optional entry point for your plugin + /// Called when the plugin is loaded /// - public abstract class PluginBootstrapper + /// + public virtual void OnPluginLoaded(Plugin plugin) { - private Plugin? _plugin; + } - /// - /// Called when the plugin is loaded - /// - /// - public virtual void OnPluginLoaded(Plugin plugin) - { - } + /// + /// Called when the plugin is activated + /// + /// The plugin instance of your plugin + public virtual void OnPluginEnabled(Plugin plugin) + { + } - /// - /// Called when the plugin is activated - /// - /// The plugin instance of your plugin - public virtual void OnPluginEnabled(Plugin plugin) - { - } + /// + /// Called when the plugin is deactivated or when Artemis shuts down + /// + /// The plugin instance of your plugin + public virtual void OnPluginDisabled(Plugin plugin) + { + } - /// - /// Called when the plugin is deactivated or when Artemis shuts down - /// - /// The plugin instance of your plugin - public virtual void OnPluginDisabled(Plugin plugin) - { - } + /// + /// Adds the provided prerequisite to the plugin. + /// + /// The prerequisite to add + public void AddPluginPrerequisite(PluginPrerequisite prerequisite) + { + // TODO: We can keep track of them and add them after load, same goes for the others + if (_plugin == null) + throw new ArtemisPluginException("Cannot add plugin prerequisites before the plugin is loaded"); - /// - /// Adds the provided prerequisite to the plugin. - /// - /// The prerequisite to add - public void AddPluginPrerequisite(PluginPrerequisite prerequisite) - { - // TODO: We can keep track of them and add them after load, same goes for the others - if (_plugin == null) - throw new ArtemisPluginException("Cannot add plugin prerequisites before the plugin is loaded"); + if (!_plugin.Info.Prerequisites.Contains(prerequisite)) + _plugin.Info.Prerequisites.Add(prerequisite); + } - if (!_plugin.Info.Prerequisites.Contains(prerequisite)) - _plugin.Info.Prerequisites.Add(prerequisite); - } + /// + /// Removes the provided prerequisite from the plugin. + /// + /// The prerequisite to remove + /// + /// is successfully removed; otherwise . This method also returns + /// if the prerequisite was not found. + /// + public bool RemovePluginPrerequisite(PluginPrerequisite prerequisite) + { + if (_plugin == null) + throw new ArtemisPluginException("Cannot add plugin prerequisites before the plugin is loaded"); - /// - /// Removes the provided prerequisite from the plugin. - /// - /// The prerequisite to remove - /// - /// is successfully removed; otherwise . This method also returns - /// if the prerequisite was not found. - /// - public bool RemovePluginPrerequisite(PluginPrerequisite prerequisite) - { - if (_plugin == null) - throw new ArtemisPluginException("Cannot add plugin prerequisites before the plugin is loaded"); + return _plugin.Info.Prerequisites.Remove(prerequisite); + } - return _plugin.Info.Prerequisites.Remove(prerequisite); - } + /// + /// Adds the provided prerequisite to the feature of type . + /// + /// The prerequisite to add + public void AddFeaturePrerequisite(PluginPrerequisite prerequisite) where T : PluginFeature + { + if (_plugin == null) + throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded"); - /// - /// Adds the provided prerequisite to the feature of type . - /// - /// The prerequisite to add - public void AddFeaturePrerequisite(PluginPrerequisite prerequisite) where T : PluginFeature - { - if (_plugin == null) - throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded"); + PluginFeatureInfo info = _plugin.GetFeatureInfo(); + if (!info.Prerequisites.Contains(prerequisite)) + info.Prerequisites.Add(prerequisite); + } - PluginFeatureInfo info = _plugin.GetFeatureInfo(); - if (!info.Prerequisites.Contains(prerequisite)) - info.Prerequisites.Add(prerequisite); - } + /// + /// Removes the provided prerequisite from the feature of type . + /// + /// The prerequisite to remove + /// + /// is successfully removed; otherwise . This method also returns + /// if the prerequisite was not found. + /// + public bool RemoveFeaturePrerequisite(PluginPrerequisite prerequisite) where T : PluginFeature + { + if (_plugin == null) + throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded"); - /// - /// Removes the provided prerequisite from the feature of type . - /// - /// The prerequisite to remove - /// - /// is successfully removed; otherwise . This method also returns - /// if the prerequisite was not found. - /// - public bool RemoveFeaturePrerequisite(PluginPrerequisite prerequisite) where T : PluginFeature - { - if (_plugin == null) - throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded"); + return _plugin.GetFeatureInfo().Prerequisites.Remove(prerequisite); + } - return _plugin.GetFeatureInfo().Prerequisites.Remove(prerequisite); - } - - internal void InternalOnPluginLoaded(Plugin plugin) - { - _plugin = plugin; - OnPluginLoaded(plugin); - } + internal void InternalOnPluginLoaded(Plugin plugin) + { + _plugin = plugin; + OnPluginLoaded(plugin); } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginFeature.cs b/src/Artemis.Core/Plugins/PluginFeature.cs index 7d58d31cb..a7430da08 100644 --- a/src/Artemis.Core/Plugins/PluginFeature.cs +++ b/src/Artemis.Core/Plugins/PluginFeature.cs @@ -3,263 +3,263 @@ using System.IO; using System.Threading.Tasks; using Artemis.Storage.Entities.Plugins; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents an feature of a certain type provided by a plugin +/// +public abstract class PluginFeature : CorePropertyChanged, IDisposable { + private bool _isEnabled; + + /// - /// Represents an feature of a certain type provided by a plugin + /// Gets the plugin feature info related to this feature /// - public abstract class PluginFeature : CorePropertyChanged, IDisposable + public PluginFeatureInfo Info { get; internal set; } = null!; // Will be set right after construction + + /// + /// Gets the plugin that provides this feature + /// + public Plugin Plugin { get; internal set; } = null!; // Will be set right after construction + + /// + /// Gets the profiler that can be used to take profiling measurements + /// + public Profiler Profiler { get; internal set; } = null!; // Will be set right after construction + + /// + /// Gets whether the plugin is enabled + /// + public bool IsEnabled { - private bool _isEnabled; + get => _isEnabled; + internal set => SetAndNotify(ref _isEnabled, value); + } + /// + /// Gets the identifier of this plugin feature + /// + public string Id => $"{GetType().FullName}-{Plugin.Guid.ToString().Substring(0, 8)}"; // Not as unique as a GUID but good enough and stays readable - /// - /// Gets the plugin feature info related to this feature - /// - public PluginFeatureInfo Info { get; internal set; } = null!; // Will be set right after construction + internal int AutoEnableAttempts { get; set; } - /// - /// Gets the plugin that provides this feature - /// - public Plugin Plugin { get; internal set; } = null!; // Will be set right after construction + internal PluginFeatureEntity Entity { get; set; } = null!; // Will be set right after construction - /// - /// Gets the profiler that can be used to take profiling measurements - /// - public Profiler Profiler { get; internal set; } = null!; // Will be set right after construction + /// + /// Called when the feature is activated + /// + public abstract void Enable(); - /// - /// Gets whether the plugin is enabled - /// - public bool IsEnabled + /// + /// Called when the feature is deactivated or when Artemis shuts down + /// + public abstract void Disable(); + + /// + /// Occurs when the feature is enabled + /// + public event EventHandler? Enabled; + + /// + /// Occurs when the feature is disabled + /// + public event EventHandler? Disabled; + + /// + /// Releases the unmanaged resources used by the plugin feature and optionally releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + SetEnabled(false); + } + + /// + /// Triggers the Enabled event + /// + protected virtual void OnEnabled() + { + Enabled?.Invoke(this, EventArgs.Empty); + } + + /// + /// Triggers the Disabled event + /// + protected virtual void OnDisabled() + { + Disabled?.Invoke(this, EventArgs.Empty); + } + + internal void StartUpdateMeasure() + { + Profiler.StartMeasurement("Update"); + } + + internal void StopUpdateMeasure() + { + Profiler.StopMeasurement("Update"); + } + + internal void SetEnabled(bool enable, bool isAutoEnable = false) + { + if (enable == IsEnabled) + return; + + if (Plugin == null) + throw new ArtemisCoreException("Cannot enable a plugin feature that is not associated with a plugin"); + + lock (Plugin) { - get => _isEnabled; - internal set => SetAndNotify(ref _isEnabled, value); - } + if (!Plugin.IsEnabled) + throw new ArtemisCoreException("Cannot enable a plugin feature of a disabled plugin"); - internal int AutoEnableAttempts { get; set; } - - /// - /// Gets the identifier of this plugin feature - /// - public string Id => $"{GetType().FullName}-{Plugin.Guid.ToString().Substring(0, 8)}"; // Not as unique as a GUID but good enough and stays readable - - internal PluginFeatureEntity Entity { get; set; } = null!; // Will be set right after construction - - /// - /// Called when the feature is activated - /// - public abstract void Enable(); - - /// - /// Called when the feature is deactivated or when Artemis shuts down - /// - public abstract void Disable(); - - /// - /// Occurs when the feature is enabled - /// - public event EventHandler? Enabled; - - /// - /// Occurs when the feature is disabled - /// - public event EventHandler? Disabled; - - /// - /// Releases the unmanaged resources used by the plugin feature and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - SetEnabled(false); - } - - /// - /// Triggers the Enabled event - /// - protected virtual void OnEnabled() - { - Enabled?.Invoke(this, EventArgs.Empty); - } - - /// - /// Triggers the Disabled event - /// - protected virtual void OnDisabled() - { - Disabled?.Invoke(this, EventArgs.Empty); - } - - internal void StartUpdateMeasure() - { - Profiler.StartMeasurement("Update"); - } - - internal void StopUpdateMeasure() - { - Profiler.StopMeasurement("Update"); - } - - internal void SetEnabled(bool enable, bool isAutoEnable = false) - { - if (enable == IsEnabled) - return; - - if (Plugin == null) - throw new ArtemisCoreException("Cannot enable a plugin feature that is not associated with a plugin"); - - lock (Plugin) + if (!enable) { - if (!Plugin.IsEnabled) - throw new ArtemisCoreException("Cannot enable a plugin feature of a disabled plugin"); + // Even if disable failed, still leave it in a disabled state to avoid more issues + InternalDisable(); + IsEnabled = false; - if (!enable) + OnDisabled(); + return; + } + + try + { + if (isAutoEnable) + AutoEnableAttempts++; + + if (isAutoEnable && GetLockFileCreated()) { - // Even if disable failed, still leave it in a disabled state to avoid more issues - InternalDisable(); - IsEnabled = false; + // Don't wrap existing lock exceptions, simply rethrow them + if (Info.LoadException is ArtemisPluginLockException) + throw Info.LoadException; - OnDisabled(); - return; + throw new ArtemisPluginLockException(Info.LoadException); } - try - { - if (isAutoEnable) - AutoEnableAttempts++; + CreateLockFile(); + IsEnabled = true; - if (isAutoEnable && GetLockFileCreated()) - { - // Don't wrap existing lock exceptions, simply rethrow them - if (Info.LoadException is ArtemisPluginLockException) - throw Info.LoadException; + // Allow up to 15 seconds for plugins to activate. + // This means plugins that need more time should do their long running tasks in a background thread, which is intentional + // This would've been a perfect match for Thread.Abort but that didn't make it into .NET Core + Task enableTask = Task.Run(InternalEnable); + if (!enableTask.Wait(TimeSpan.FromSeconds(15))) + throw new ArtemisPluginException(Plugin, "Plugin load timeout"); - throw new ArtemisPluginLockException(Info.LoadException); - } - - CreateLockFile(); - IsEnabled = true; - - // Allow up to 15 seconds for plugins to activate. - // This means plugins that need more time should do their long running tasks in a background thread, which is intentional - // This would've been a perfect match for Thread.Abort but that didn't make it into .NET Core - Task enableTask = Task.Run(InternalEnable); - if (!enableTask.Wait(TimeSpan.FromSeconds(15))) - throw new ArtemisPluginException(Plugin, "Plugin load timeout"); - - Info.LoadException = null; - AutoEnableAttempts = 0; - OnEnabled(); - } - // If enable failed, put it back in a disabled state - catch (Exception e) - { - IsEnabled = false; - Info.LoadException = e; - throw; - } - finally - { - // Clean up the lock file unless the failure was due to the lock file - // After all, we failed but not miserably :) - if (Info.LoadException is not ArtemisPluginLockException) - DeleteLockFile(); - } + Info.LoadException = null; + AutoEnableAttempts = 0; + OnEnabled(); + } + // If enable failed, put it back in a disabled state + catch (Exception e) + { + IsEnabled = false; + Info.LoadException = e; + throw; + } + finally + { + // Clean up the lock file unless the failure was due to the lock file + // After all, we failed but not miserably :) + if (Info.LoadException is not ArtemisPluginLockException) + DeleteLockFile(); } } - - internal virtual void InternalEnable() - { - Enable(); - } - - internal virtual void InternalDisable() - { - if (IsEnabled) - Disable(); - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #region Loading - - internal void CreateLockFile() - { - if (Plugin == null) - throw new ArtemisCoreException("Cannot lock a plugin feature that is not associated with a plugin"); - - File.Create(Plugin.ResolveRelativePath($"{GetType().FullName}.lock")).Close(); - } - - internal void DeleteLockFile() - { - if (Plugin == null) - throw new ArtemisCoreException("Cannot lock a plugin feature that is not associated with a plugin"); - - if (GetLockFileCreated()) - File.Delete(Plugin.ResolveRelativePath($"{GetType().FullName}.lock")); - } - - internal bool GetLockFileCreated() - { - if (Plugin == null) - throw new ArtemisCoreException("Cannot lock a plugin feature that is not associated with a plugin"); - - return File.Exists(Plugin.ResolveRelativePath($"{GetType().FullName}.lock")); - } - - #endregion - - #region Timed updates - - /// - /// Registers a timed update that whenever the plugin is enabled calls the provided at the - /// provided - /// - /// - /// The interval at which the update should occur - /// - /// The action to call every time the interval has passed. The delta time parameter represents the - /// time passed since the last update in seconds - /// - /// An optional name used in exceptions and profiling - /// The resulting plugin update registration which can be used to stop the update - public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Action action, string? name = null) - { - if (action == null) - throw new ArgumentNullException(nameof(action)); - return new TimedUpdateRegistration(this, interval, action, name); - } - - /// - /// Registers a timed update that whenever the plugin is enabled calls the provided at the - /// provided - /// - /// - /// The interval at which the update should occur - /// - /// The async action to call every time the interval has passed. The delta time parameter - /// represents the time passed since the last update in seconds - /// - /// An optional name used in exceptions and profiling - /// The resulting plugin update registration - public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Func asyncAction, string? name = null) - { - if (asyncAction == null) - throw new ArgumentNullException(nameof(asyncAction)); - return new TimedUpdateRegistration(this, interval, asyncAction, name); - } - - #endregion } + + internal virtual void InternalEnable() + { + Enable(); + } + + internal virtual void InternalDisable() + { + if (IsEnabled) + Disable(); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #region Loading + + internal void CreateLockFile() + { + if (Plugin == null) + throw new ArtemisCoreException("Cannot lock a plugin feature that is not associated with a plugin"); + + File.Create(Plugin.ResolveRelativePath($"{GetType().FullName}.lock")).Close(); + } + + internal void DeleteLockFile() + { + if (Plugin == null) + throw new ArtemisCoreException("Cannot lock a plugin feature that is not associated with a plugin"); + + if (GetLockFileCreated()) + File.Delete(Plugin.ResolveRelativePath($"{GetType().FullName}.lock")); + } + + internal bool GetLockFileCreated() + { + if (Plugin == null) + throw new ArtemisCoreException("Cannot lock a plugin feature that is not associated with a plugin"); + + return File.Exists(Plugin.ResolveRelativePath($"{GetType().FullName}.lock")); + } + + #endregion + + #region Timed updates + + /// + /// Registers a timed update that whenever the plugin is enabled calls the provided at the + /// provided + /// + /// + /// The interval at which the update should occur + /// + /// The action to call every time the interval has passed. The delta time parameter represents the + /// time passed since the last update in seconds + /// + /// An optional name used in exceptions and profiling + /// The resulting plugin update registration which can be used to stop the update + public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Action action, string? name = null) + { + if (action == null) + throw new ArgumentNullException(nameof(action)); + return new TimedUpdateRegistration(this, interval, action, name); + } + + /// + /// Registers a timed update that whenever the plugin is enabled calls the provided at + /// the + /// provided + /// + /// + /// The interval at which the update should occur + /// + /// The async action to call every time the interval has passed. The delta time parameter + /// represents the time passed since the last update in seconds + /// + /// An optional name used in exceptions and profiling + /// The resulting plugin update registration + public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Func asyncAction, string? name = null) + { + if (asyncAction == null) + throw new ArgumentNullException(nameof(asyncAction)); + return new TimedUpdateRegistration(this, interval, asyncAction, name); + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs b/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs index e761b7a3e..19acfa645 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs @@ -1,33 +1,32 @@ using System; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents an attribute that describes a plugin feature +/// +[AttributeUsage(AttributeTargets.Class)] +public class PluginFeatureAttribute : Attribute { /// - /// Represents an attribute that describes a plugin feature + /// Gets or sets the user-friendly name for this property, shown in the UI. /// - [AttributeUsage(AttributeTargets.Class)] - public class PluginFeatureAttribute : Attribute - { - /// - /// Gets or sets the user-friendly name for this property, shown in the UI. - /// - public string? Name { get; set; } + public string? Name { get; set; } - /// - /// Gets or sets the user-friendly description for this property, shown in the UI. - /// - public string? Description { get; set; } + /// + /// Gets or sets the user-friendly description for this property, shown in the UI. + /// + public string? Description { get; set; } - /// - /// The plugins display icon that's shown in the settings see for - /// available icons - /// - public string? Icon { get; set; } + /// + /// The plugins display icon that's shown in the settings see for + /// available icons + /// + public string? Icon { get; set; } - /// - /// Marks the feature to always be enabled as long as the plugin is enabled - /// Note: always if this is the plugin's only feature - /// - public bool AlwaysEnabled { get; set; } - } + /// + /// Marks the feature to always be enabled as long as the plugin is enabled + /// Note: always if this is the plugin's only feature + /// + public bool AlwaysEnabled { get; set; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs index ed9064f70..35798a064 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs @@ -9,171 +9,170 @@ using Artemis.Storage.Entities.Plugins; using Humanizer; using Newtonsoft.Json; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents basic info about a plugin feature and contains a reference to the instance of said feature +/// +[JsonObject(MemberSerialization.OptIn)] +public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject { - /// - /// Represents basic info about a plugin feature and contains a reference to the instance of said feature - /// - [JsonObject(MemberSerialization.OptIn)] - public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject + private string? _description; + private string? _icon; + private PluginFeature? _instance; + private Exception? _loadException; + private string _name = null!; + + internal PluginFeatureInfo(Plugin plugin, Type featureType, PluginFeatureEntity pluginFeatureEntity, PluginFeatureAttribute? attribute) { - private Exception? _loadException; - private string? _description; - private string? _icon; - private PluginFeature? _instance; - private string _name = null!; + Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin)); + FeatureType = featureType ?? throw new ArgumentNullException(nameof(featureType)); + Entity = pluginFeatureEntity; - internal PluginFeatureInfo(Plugin plugin, Type featureType, PluginFeatureEntity pluginFeatureEntity, PluginFeatureAttribute? attribute) + Name = attribute?.Name ?? featureType.Name.Humanize(LetterCasing.Title); + Description = attribute?.Description; + Icon = attribute?.Icon; + AlwaysEnabled = attribute?.AlwaysEnabled ?? false; + + if (Icon != null) return; + if (typeof(DeviceProvider).IsAssignableFrom(featureType)) + Icon = "Devices"; + else if (typeof(Module).IsAssignableFrom(featureType)) + Icon = "VectorRectangle"; + else if (typeof(LayerBrushProvider).IsAssignableFrom(featureType)) + Icon = "Brush"; + else if (typeof(LayerEffectProvider).IsAssignableFrom(featureType)) + Icon = "AutoAwesome"; + else + Icon = "Plugin"; + } + + internal PluginFeatureInfo(Plugin plugin, PluginFeatureAttribute? attribute, PluginFeature instance) + { + if (instance == null) throw new ArgumentNullException(nameof(instance)); + Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin)); + FeatureType = instance.GetType(); + Entity = new PluginFeatureEntity(); + + Name = attribute?.Name ?? instance.GetType().Name.Humanize(LetterCasing.Title); + Description = attribute?.Description; + Icon = attribute?.Icon; + AlwaysEnabled = attribute?.AlwaysEnabled ?? false; + Instance = instance; + + if (Icon != null) return; + Icon = Instance switch { - Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin)); - FeatureType = featureType ?? throw new ArgumentNullException(nameof(featureType)); - Entity = pluginFeatureEntity; + DeviceProvider => "Devices", + Module => "VectorRectangle", + LayerBrushProvider => "Brush", + LayerEffectProvider => "AutoAwesome", + _ => "Plugin" + }; + } - Name = attribute?.Name ?? featureType.Name.Humanize(LetterCasing.Title); - Description = attribute?.Description; - Icon = attribute?.Icon; - AlwaysEnabled = attribute?.AlwaysEnabled ?? false; + /// + /// Gets the plugin this feature info is associated with + /// + public Plugin Plugin { get; } - if (Icon != null) return; - if (typeof(DeviceProvider).IsAssignableFrom(featureType)) - Icon = "Devices"; - else if (typeof(Module).IsAssignableFrom(featureType)) - Icon = "VectorRectangle"; - else if (typeof(LayerBrushProvider).IsAssignableFrom(featureType)) - Icon = "Brush"; - else if (typeof(LayerEffectProvider).IsAssignableFrom(featureType)) - Icon = "AutoAwesome"; - else - Icon = "Plugin"; - } + /// + /// Gets the type of the feature + /// + public Type FeatureType { get; } - internal PluginFeatureInfo(Plugin plugin, PluginFeatureAttribute? attribute, PluginFeature instance) + /// + /// Gets the exception thrown while loading + /// + public Exception? LoadException + { + get => _loadException; + internal set => SetAndNotify(ref _loadException, value); + } + + /// + /// The name of the feature + /// + [JsonProperty(Required = Required.Always)] + public string Name + { + get => _name; + internal set => SetAndNotify(ref _name, value); + } + + /// + /// A short description of the feature + /// + [JsonProperty] + public string? Description + { + get => _description; + set => SetAndNotify(ref _description, value); + } + + /// + /// The plugins display icon that's shown in the settings see for + /// available icons + /// + [JsonProperty] + public string? Icon + { + get => _icon; + set => SetAndNotify(ref _icon, value); + } + + /// + /// Marks the feature to always be enabled as long as the plugin is enabled and cannot be disabled. + /// Note: always if this is the plugin's only feature + /// + [JsonProperty] + public bool AlwaysEnabled { get; internal set; } + + /// + /// Gets a boolean indicating whether the feature is enabled in persistent storage + /// + public bool EnabledInStorage => Entity.IsEnabled; + + /// + /// Gets the feature this info is associated with + /// Note: if the associated is disabled + /// + public PluginFeature? Instance + { + get => _instance; + internal set => SetAndNotify(ref _instance, value); + } + + /// + /// Gets a string representing either a full path pointing to an svg or the markdown icon + /// + public string? ResolvedIcon + { + get { - if (instance == null) throw new ArgumentNullException(nameof(instance)); - Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin)); - FeatureType = instance.GetType(); - Entity = new PluginFeatureEntity(); - - Name = attribute?.Name ?? instance.GetType().Name.Humanize(LetterCasing.Title); - Description = attribute?.Description; - Icon = attribute?.Icon; - AlwaysEnabled = attribute?.AlwaysEnabled ?? false; - Instance = instance; - - if (Icon != null) return; - Icon = Instance switch - { - DeviceProvider => "Devices", - Module => "VectorRectangle", - LayerBrushProvider => "Brush", - LayerEffectProvider => "AutoAwesome", - _ => "Plugin" - }; - } - - /// - /// Gets the plugin this feature info is associated with - /// - public Plugin Plugin { get; } - - /// - /// Gets the type of the feature - /// - public Type FeatureType { get; } - - /// - /// Gets the exception thrown while loading - /// - public Exception? LoadException - { - get => _loadException; - internal set => SetAndNotify(ref _loadException, value); - } - - /// - /// The name of the feature - /// - [JsonProperty(Required = Required.Always)] - public string Name - { - get => _name; - internal set => SetAndNotify(ref _name, value); - } - - /// - /// A short description of the feature - /// - [JsonProperty] - public string? Description - { - get => _description; - set => SetAndNotify(ref _description, value); - } - - /// - /// The plugins display icon that's shown in the settings see for - /// available icons - /// - [JsonProperty] - public string? Icon - { - get => _icon; - set => SetAndNotify(ref _icon, value); - } - - /// - /// Marks the feature to always be enabled as long as the plugin is enabled and cannot be disabled. - /// Note: always if this is the plugin's only feature - /// - [JsonProperty] - public bool AlwaysEnabled { get; internal set; } - - /// - /// Gets a boolean indicating whether the feature is enabled in persistent storage - /// - public bool EnabledInStorage => Entity.IsEnabled; - - /// - /// Gets the feature this info is associated with - /// Note: if the associated is disabled - /// - public PluginFeature? Instance - { - get => _instance; - internal set => SetAndNotify(ref _instance, value); - } - - /// - /// Gets a string representing either a full path pointing to an svg or the markdown icon - /// - public string? ResolvedIcon - { - get - { - if (Icon == null) - return null; - return Icon.Contains('.') ? Plugin.ResolveRelativePath(Icon) : Icon; - } - } - - internal PluginFeatureEntity Entity { get; } - - /// - public override string ToString() - { - return Instance?.Id ?? "Uninitialized feature"; - } - - /// - public List Prerequisites { get; } = new(); - - /// - public IEnumerable PlatformPrerequisites => Prerequisites.Where(p => p.AppliesToPlatform()); - - /// - public bool ArePrerequisitesMet() - { - return PlatformPrerequisites.All(p => p.IsMet()); + if (Icon == null) + return null; + return Icon.Contains('.') ? Plugin.ResolveRelativePath(Icon) : Icon; } } + + internal PluginFeatureEntity Entity { get; } + + /// + public override string ToString() + { + return Instance?.Id ?? "Uninitialized feature"; + } + + /// + public List Prerequisites { get; } = new(); + + /// + public IEnumerable PlatformPrerequisites => Prerequisites.Where(p => p.AppliesToPlatform()); + + /// + public bool ArePrerequisitesMet() + { + return PlatformPrerequisites.All(p => p.IsMet()); + } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs index e0850b3a0..7e3957b56 100644 --- a/src/Artemis.Core/Plugins/PluginInfo.cs +++ b/src/Artemis.Core/Plugins/PluginInfo.cs @@ -4,200 +4,210 @@ using System.ComponentModel; using System.Linq; using Newtonsoft.Json; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents basic info about a plugin and contains a reference to the instance of said plugin +/// +[JsonObject(MemberSerialization.OptIn)] +public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject { - /// - /// Represents basic info about a plugin and contains a reference to the instance of said plugin - /// - [JsonObject(MemberSerialization.OptIn)] - public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject + private Version? _api; + private string? _author; + private bool _autoEnableFeatures = true; + private string? _description; + private Guid _guid; + private string? _icon; + private string _main = null!; + private string _name = null!; + private PluginPlatform? _platforms; + private Plugin _plugin = null!; + private Uri? _repository; + private bool _requiresAdmin; + private Version _version = null!; + private Uri? _website; + + internal PluginInfo() { - private Guid _guid; - private string? _description; - private string? _author; - private Uri? _website; - private Uri? _repository; - private string? _icon; - private string _main = null!; - private bool _autoEnableFeatures = true; - private string _name = null!; - private Plugin _plugin = null!; - private Version _version = null!; - private bool _requiresAdmin; - private PluginPlatform? _platforms; + } - internal PluginInfo() + /// + /// The plugins GUID + /// + [JsonProperty(Required = Required.Always)] + public Guid Guid + { + get => _guid; + internal set => SetAndNotify(ref _guid, value); + } + + /// + /// The name of the plugin + /// + [JsonProperty(Required = Required.Always)] + public string Name + { + get => _name; + internal set => SetAndNotify(ref _name, value); + } + + /// + /// A short description of the plugin + /// + [JsonProperty] + public string? Description + { + get => _description; + set => SetAndNotify(ref _description, value); + } + + /// + /// Gets or sets the author of this plugin + /// + [JsonProperty] + public string? Author + { + get => _author; + set => SetAndNotify(ref _author, value); + } + + /// + /// Gets or sets the website of this plugin or its author + /// + [JsonProperty] + public Uri? Website + { + get => _website; + set => SetAndNotify(ref _website, value); + } + + /// + /// Gets or sets the repository of this plugin + /// + [JsonProperty] + public Uri? Repository + { + get => _repository; + set => SetAndNotify(ref _repository, value); + } + + /// + /// The plugins display icon that's shown in the settings see for + /// available icons + /// + [JsonProperty] + public string? Icon + { + get => _icon; + set => SetAndNotify(ref _icon, value); + } + + /// + /// The version of the plugin + /// + [JsonProperty(Required = Required.Always)] + public Version Version + { + get => _version; + internal set => SetAndNotify(ref _version, value); + } + + /// + /// The main entry DLL, should contain a class implementing Plugin + /// + [JsonProperty(Required = Required.Always)] + public string Main + { + get => _main; + internal set => SetAndNotify(ref _main, value); + } + + /// + /// Gets or sets a boolean indicating whether this plugin should automatically enable all its features when it is first + /// loaded + /// + [DefaultValue(true)] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public bool AutoEnableFeatures + { + get => _autoEnableFeatures; + set => SetAndNotify(ref _autoEnableFeatures, value); + } + + /// + /// Gets a boolean indicating whether this plugin requires elevated admin privileges + /// + [JsonProperty] + public bool RequiresAdmin + { + get => _requiresAdmin; + internal set => SetAndNotify(ref _requiresAdmin, value); + } + + /// + /// Gets + /// + [JsonProperty] + public PluginPlatform? Platforms + { + get => _platforms; + internal set => SetAndNotify(ref _platforms, value); + } + + /// + /// Gets the API version the plugin was built for + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public Version? Api + { + get => _api; + internal set => SetAndNotify(ref _api, value); + } + + /// + /// Gets the plugin this info is associated with + /// + public Plugin Plugin + { + get => _plugin; + internal set => SetAndNotify(ref _plugin, value); + } + + /// + /// Gets a string representing either a full path pointing to an svg or the markdown icon + /// + public string? ResolvedIcon + { + get { - } - - /// - /// The plugins GUID - /// - [JsonProperty(Required = Required.Always)] - public Guid Guid - { - get => _guid; - internal set => SetAndNotify(ref _guid, value); - } - - /// - /// The name of the plugin - /// - [JsonProperty(Required = Required.Always)] - public string Name - { - get => _name; - internal set => SetAndNotify(ref _name, value); - } - - /// - /// A short description of the plugin - /// - [JsonProperty] - public string? Description - { - get => _description; - set => SetAndNotify(ref _description, value); - } - - /// - /// Gets or sets the author of this plugin - /// - [JsonProperty] - public string? Author - { - get => _author; - set => SetAndNotify(ref _author, value); - } - - /// - /// Gets or sets the website of this plugin or its author - /// - [JsonProperty] - public Uri? Website - { - get => _website; - set => SetAndNotify(ref _website, value); - } - - /// - /// Gets or sets the repository of this plugin - /// - [JsonProperty] - public Uri? Repository - { - get => _repository; - set => SetAndNotify(ref _repository, value); - } - - /// - /// The plugins display icon that's shown in the settings see for - /// available icons - /// - [JsonProperty] - public string? Icon - { - get => _icon; - set => SetAndNotify(ref _icon, value); - } - - /// - /// The version of the plugin - /// - [JsonProperty(Required = Required.Always)] - public Version Version - { - get => _version; - internal set => SetAndNotify(ref _version, value); - } - - /// - /// The main entry DLL, should contain a class implementing Plugin - /// - [JsonProperty(Required = Required.Always)] - public string Main - { - get => _main; - internal set => SetAndNotify(ref _main, value); - } - - /// - /// Gets or sets a boolean indicating whether this plugin should automatically enable all its features when it is first - /// loaded - /// - [DefaultValue(true)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public bool AutoEnableFeatures - { - get => _autoEnableFeatures; - set => SetAndNotify(ref _autoEnableFeatures, value); - } - - /// - /// Gets a boolean indicating whether this plugin requires elevated admin privileges - /// - [JsonProperty] - public bool RequiresAdmin - { - get => _requiresAdmin; - internal set => SetAndNotify(ref _requiresAdmin, value); - } - - /// - /// Gets - /// - [JsonProperty] - public PluginPlatform? Platforms - { - get => _platforms; - internal set => _platforms = value; - } - - /// - /// Gets the plugin this info is associated with - /// - public Plugin Plugin - { - get => _plugin; - internal set => SetAndNotify(ref _plugin, value); - } - - /// - /// Gets a string representing either a full path pointing to an svg or the markdown icon - /// - public string? ResolvedIcon - { - get - { - if (Icon == null) - return null; - return Icon.Contains('.') ? Plugin.ResolveRelativePath(Icon) : Icon; - } - } - - /// - /// Gets a boolean indicating whether this plugin is compatible with the current operating system - /// - public bool IsCompatible => Platforms.MatchesCurrentOperatingSystem(); - - internal string PreferredPluginDirectory => $"{Main.Split(".dll")[0].Replace("/", "").Replace("\\", "")}-{Guid.ToString().Substring(0, 8)}"; - - /// - public override string ToString() - { - return $"{Name} v{Version} - {Guid}"; - } - - /// - public List Prerequisites { get; } = new(); - - /// - public IEnumerable PlatformPrerequisites => Prerequisites.Where(p => p.AppliesToPlatform()); - - /// - public bool ArePrerequisitesMet() - { - return PlatformPrerequisites.All(p => p.IsMet()); + if (Icon == null) + return null; + return Icon.Contains('.') ? Plugin.ResolveRelativePath(Icon) : Icon; } } + + /// + /// Gets a boolean indicating whether this plugin is compatible with the current operating system and API version + /// + public bool IsCompatible => Platforms.MatchesCurrentOperatingSystem() && Api != null && Api >= Constants.PluginApi; + + internal string PreferredPluginDirectory => $"{Main.Split(".dll")[0].Replace("/", "").Replace("\\", "")}-{Guid.ToString().Substring(0, 8)}"; + + /// + public override string ToString() + { + return $"{Name} v{Version} - {Guid}"; + } + + /// + public List Prerequisites { get; } = new(); + + /// + public IEnumerable PlatformPrerequisites => Prerequisites.Where(p => p.AppliesToPlatform()); + + /// + public bool ArePrerequisitesMet() + { + return PlatformPrerequisites.All(p => p.IsMet()); + } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginPlatform.cs b/src/Artemis.Core/Plugins/PluginPlatform.cs index fa07d2646..6f5ce1c34 100644 --- a/src/Artemis.Core/Plugins/PluginPlatform.cs +++ b/src/Artemis.Core/Plugins/PluginPlatform.cs @@ -1,5 +1,6 @@ using System; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace Artemis.Core; @@ -7,7 +8,7 @@ namespace Artemis.Core; /// Specifies OS platforms a plugin may support. /// [Flags] -[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] +[JsonConverter(typeof(StringEnumConverter))] public enum PluginPlatform { /// The Windows platform. diff --git a/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs b/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs index 6fa0d25cd..be6700c92 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs @@ -1,25 +1,24 @@ using System.Collections.Generic; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a type that has prerequisites +/// +public interface IPrerequisitesSubject { /// - /// Represents a type that has prerequisites + /// Gets a list of prerequisites for this plugin /// - public interface IPrerequisitesSubject - { - /// - /// Gets a list of prerequisites for this plugin - /// - List Prerequisites { get; } - - /// - /// Gets a list of prerequisites of the current platform for this plugin - /// - IEnumerable PlatformPrerequisites { get; } + List Prerequisites { get; } - /// - /// Determines whether the prerequisites of this plugin are met - /// - bool ArePrerequisitesMet(); - } + /// + /// Gets a list of prerequisites of the current platform for this plugin + /// + IEnumerable PlatformPrerequisites { get; } + + /// + /// Determines whether the prerequisites of this plugin are met + /// + bool ArePrerequisitesMet(); } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs index f4bc30b79..1d2662846 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs @@ -1,135 +1,133 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a prerequisite for a or +/// +public abstract class PluginPrerequisite : CorePropertyChanged { + private PluginPrerequisiteAction? _currentAction; + /// - /// Represents a prerequisite for a or + /// Gets the name of the prerequisite /// - public abstract class PluginPrerequisite : CorePropertyChanged + public abstract string Name { get; } + + /// + /// Gets the description of the prerequisite + /// + public abstract string Description { get; } + + /// + /// Gets or sets the platform(s) this prerequisite applies to. + /// + public PluginPlatform? Platform { get; protected set; } + + /// + /// Gets a list of actions to execute when is called + /// + public abstract List InstallActions { get; } + + /// + /// Gets a list of actions to execute when is called + /// + public abstract List UninstallActions { get; } + + /// + /// Gets or sets the action currently being executed + /// + public PluginPrerequisiteAction? CurrentAction { - private PluginPrerequisiteAction? _currentAction; + get => _currentAction; + private set => SetAndNotify(ref _currentAction, value); + } - /// - /// Gets the name of the prerequisite - /// - public abstract string Name { get; } - - /// - /// Gets the description of the prerequisite - /// - public abstract string Description { get; } - - /// - /// Gets or sets the platform(s) this prerequisite applies to. - /// - public PluginPlatform? Platform { get; protected set; } - - /// - /// Gets a list of actions to execute when is called - /// - public abstract List InstallActions { get; } - - /// - /// Gets a list of actions to execute when is called - /// - public abstract List UninstallActions { get; } - - /// - /// Gets or sets the action currently being executed - /// - public PluginPrerequisiteAction? CurrentAction + /// + /// Execute all install actions + /// + public async Task Install(CancellationToken cancellationToken) + { + try { - get => _currentAction; - private set => SetAndNotify(ref _currentAction, value); - } - - /// - /// Execute all install actions - /// - public async Task Install(CancellationToken cancellationToken) - { - try + OnInstallStarting(); + foreach (PluginPrerequisiteAction installAction in InstallActions) { - OnInstallStarting(); - foreach (PluginPrerequisiteAction installAction in InstallActions) - { - cancellationToken.ThrowIfCancellationRequested(); - CurrentAction = installAction; - await installAction.Execute(cancellationToken); - } - } - finally - { - CurrentAction = null; - OnInstallFinished(); + cancellationToken.ThrowIfCancellationRequested(); + CurrentAction = installAction; + await installAction.Execute(cancellationToken); } } - - /// - /// Execute all uninstall actions - /// - public async Task Uninstall(CancellationToken cancellationToken) - { - try - { - OnUninstallStarting(); - foreach (PluginPrerequisiteAction uninstallAction in UninstallActions) - { - cancellationToken.ThrowIfCancellationRequested(); - CurrentAction = uninstallAction; - await uninstallAction.Execute(cancellationToken); - } - } - finally - { - CurrentAction = null; - OnUninstallFinished(); - } - } - - /// - /// Called to determine whether the prerequisite is met - /// - /// if the prerequisite is met; otherwise - public abstract bool IsMet(); - - /// - /// Determines whether this prerequisite applies to the current operating system. - /// - public bool AppliesToPlatform() - { - return Platform.MatchesCurrentOperatingSystem(); - } - - /// - /// Called before installation starts - /// - protected virtual void OnInstallStarting() - { - } - - /// - /// Called after installation finishes - /// - protected virtual void OnInstallFinished() - { - } - - /// - /// Called before uninstall starts - /// - protected virtual void OnUninstallStarting() - { - } - - /// - /// Called after uninstall finished - /// - protected virtual void OnUninstallFinished() + finally { + CurrentAction = null; + OnInstallFinished(); } } + + /// + /// Execute all uninstall actions + /// + public async Task Uninstall(CancellationToken cancellationToken) + { + try + { + OnUninstallStarting(); + foreach (PluginPrerequisiteAction uninstallAction in UninstallActions) + { + cancellationToken.ThrowIfCancellationRequested(); + CurrentAction = uninstallAction; + await uninstallAction.Execute(cancellationToken); + } + } + finally + { + CurrentAction = null; + OnUninstallFinished(); + } + } + + /// + /// Called to determine whether the prerequisite is met + /// + /// if the prerequisite is met; otherwise + public abstract bool IsMet(); + + /// + /// Determines whether this prerequisite applies to the current operating system. + /// + public bool AppliesToPlatform() + { + return Platform.MatchesCurrentOperatingSystem(); + } + + /// + /// Called before installation starts + /// + protected virtual void OnInstallStarting() + { + } + + /// + /// Called after installation finishes + /// + protected virtual void OnInstallFinished() + { + } + + /// + /// Called before uninstall starts + /// + protected virtual void OnUninstallStarting() + { + } + + /// + /// Called after uninstall finished + /// + protected virtual void OnUninstallFinished() + { + } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisiteAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisiteAction.cs index 90d9a787e..4bf597702 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisiteAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisiteAction.cs @@ -2,95 +2,94 @@ using System.Threading; using System.Threading.Tasks; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents an action that must be taken to install or uninstall a plugin prerequisite +/// +public abstract class PluginPrerequisiteAction : CorePropertyChanged { + private bool _progressIndeterminate; + private bool _showProgressBar; + private bool _showSubProgressBar; + private string? _status; + private bool _subProgressIndeterminate; + /// - /// Represents an action that must be taken to install or uninstall a plugin prerequisite + /// The base constructor for all plugin prerequisite actions /// - public abstract class PluginPrerequisiteAction : CorePropertyChanged + /// The name of the action + protected PluginPrerequisiteAction(string name) { - private bool _progressIndeterminate; - private bool _showProgressBar; - private bool _showSubProgressBar; - private string? _status; - private bool _subProgressIndeterminate; - - /// - /// The base constructor for all plugin prerequisite actions - /// - /// The name of the action - protected PluginPrerequisiteAction(string name) - { - Name = name ?? throw new ArgumentNullException(nameof(name)); - } - - #region Implementation of IPluginPrerequisiteAction - - /// - /// Gets the name of the action - /// - public string Name { get; } - - /// - /// Gets or sets the status of the action - /// - public string? Status - { - get => _status; - set => SetAndNotify(ref _status, value); - } - - /// - /// Gets or sets a boolean indicating whether the progress is indeterminate or not - /// - public bool ProgressIndeterminate - { - get => _progressIndeterminate; - set => SetAndNotify(ref _progressIndeterminate, value); - } - - /// - /// Gets or sets a boolean indicating whether the progress is indeterminate or not - /// - public bool SubProgressIndeterminate - { - get => _subProgressIndeterminate; - set => SetAndNotify(ref _subProgressIndeterminate, value); - } - - /// - /// Gets or sets a boolean indicating whether the progress bar should be shown - /// - public bool ShowProgressBar - { - get => _showProgressBar; - set => SetAndNotify(ref _showProgressBar, value); - } - - /// - /// Gets or sets a boolean indicating whether the sub progress bar should be shown - /// - public bool ShowSubProgressBar - { - get => _showSubProgressBar; - set => SetAndNotify(ref _showSubProgressBar, value); - } - - /// - /// Gets or sets the progress of the action (0 to 100) - /// - public PrerequisiteActionProgress Progress { get; } = new(); - - /// - /// Gets or sets the sub progress of the action - /// - public PrerequisiteActionProgress SubProgress { get; } = new(); - - /// - /// Called when the action must execute - /// - public abstract Task Execute(CancellationToken cancellationToken); - - #endregion + Name = name ?? throw new ArgumentNullException(nameof(name)); } + + #region Implementation of IPluginPrerequisiteAction + + /// + /// Gets the name of the action + /// + public string Name { get; } + + /// + /// Gets or sets the status of the action + /// + public string? Status + { + get => _status; + set => SetAndNotify(ref _status, value); + } + + /// + /// Gets or sets a boolean indicating whether the progress is indeterminate or not + /// + public bool ProgressIndeterminate + { + get => _progressIndeterminate; + set => SetAndNotify(ref _progressIndeterminate, value); + } + + /// + /// Gets or sets a boolean indicating whether the progress is indeterminate or not + /// + public bool SubProgressIndeterminate + { + get => _subProgressIndeterminate; + set => SetAndNotify(ref _subProgressIndeterminate, value); + } + + /// + /// Gets or sets a boolean indicating whether the progress bar should be shown + /// + public bool ShowProgressBar + { + get => _showProgressBar; + set => SetAndNotify(ref _showProgressBar, value); + } + + /// + /// Gets or sets a boolean indicating whether the sub progress bar should be shown + /// + public bool ShowSubProgressBar + { + get => _showSubProgressBar; + set => SetAndNotify(ref _showSubProgressBar, value); + } + + /// + /// Gets or sets the progress of the action (0 to 100) + /// + public PrerequisiteActionProgress Progress { get; } = new(); + + /// + /// Gets or sets the sub progress of the action + /// + public PrerequisiteActionProgress SubProgress { get; } = new(); + + /// + /// Called when the action must execute + /// + public abstract Task Execute(CancellationToken cancellationToken); + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/CopyFolderAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/CopyFolderAction.cs index 6602567e0..5c5e29b94 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/CopyFolderAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/CopyFolderAction.cs @@ -4,78 +4,77 @@ using System.Threading; using System.Threading.Tasks; using Humanizer; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a plugin prerequisite action that copies a folder +/// +public class CopyFolderAction : PluginPrerequisiteAction { /// - /// Represents a plugin prerequisite action that copies a folder + /// Creates a new instance of a copy folder action /// - public class CopyFolderAction : PluginPrerequisiteAction + /// The name of the action + /// The source folder to copy + /// The target folder to copy to (will be created if needed) + public CopyFolderAction(string name, string source, string target) : base(name) { - /// - /// Creates a new instance of a copy folder action - /// - /// The name of the action - /// The source folder to copy - /// The target folder to copy to (will be created if needed) - public CopyFolderAction(string name, string source, string target) : base(name) + Source = source; + Target = target; + + ShowProgressBar = true; + ShowSubProgressBar = true; + } + + /// + /// Gets the source directory + /// + public string Source { get; } + + /// + /// Gets or sets the target directory + /// + public string Target { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + DirectoryInfo source = new(Source); + DirectoryInfo target = new(Target); + + if (!source.Exists) + throw new ArtemisCoreException($"The source directory at '{source}' was not found."); + + int filesCopied = 0; + FileInfo[] files = source.GetFiles("*", SearchOption.AllDirectories); + + foreach (FileInfo fileInfo in files) { - Source = source; - Target = target; + string outputPath = fileInfo.FullName.Replace(source.FullName, target.FullName); + string outputDir = Path.GetDirectoryName(outputPath)!; + Utilities.CreateAccessibleDirectory(outputDir); - ShowProgressBar = true; - ShowSubProgressBar = true; - } - - /// - /// Gets the source directory - /// - public string Source { get; } - - /// - /// Gets or sets the target directory - /// - public string Target { get; } - - /// - public override async Task Execute(CancellationToken cancellationToken) - { - DirectoryInfo source = new(Source); - DirectoryInfo target = new(Target); - - if (!source.Exists) - throw new ArtemisCoreException($"The source directory at '{source}' was not found."); - - int filesCopied = 0; - FileInfo[] files = source.GetFiles("*", SearchOption.AllDirectories); - - foreach (FileInfo fileInfo in files) + void SubProgressOnProgressReported(object? sender, EventArgs e) { - string outputPath = fileInfo.FullName.Replace(source.FullName, target.FullName); - string outputDir = Path.GetDirectoryName(outputPath)!; - Utilities.CreateAccessibleDirectory(outputDir); - - void SubProgressOnProgressReported(object? sender, EventArgs e) - { - if (SubProgress.ProgressPerSecond != 0) - Status = $"Copying {fileInfo.Name} - {SubProgress.ProgressPerSecond.Bytes().Humanize("#.##")}/sec"; - else - Status = $"Copying {fileInfo.Name}"; - } - - Progress.Report((filesCopied, files.Length)); - SubProgress.ProgressReported += SubProgressOnProgressReported; - - await using FileStream sourceStream = fileInfo.OpenRead(); - await using FileStream destinationStream = File.Create(outputPath); - - await sourceStream.CopyToAsync(fileInfo.Length, destinationStream, SubProgress, cancellationToken); - - filesCopied++; - SubProgress.ProgressReported -= SubProgressOnProgressReported; + if (SubProgress.ProgressPerSecond != 0) + Status = $"Copying {fileInfo.Name} - {SubProgress.ProgressPerSecond.Bytes().Humanize("#.##")}/sec"; + else + Status = $"Copying {fileInfo.Name}"; } Progress.Report((filesCopied, files.Length)); - Status = $"Finished copying {filesCopied} file(s)"; + SubProgress.ProgressReported += SubProgressOnProgressReported; + + await using FileStream sourceStream = fileInfo.OpenRead(); + await using FileStream destinationStream = File.Create(outputPath); + + await sourceStream.CopyToAsync(fileInfo.Length, destinationStream, SubProgress, cancellationToken); + + filesCopied++; + SubProgress.ProgressReported -= SubProgressOnProgressReported; } + + Progress.Report((filesCopied, files.Length)); + Status = $"Finished copying {filesCopied} file(s)"; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFileAction.cs index e6324bf8f..9ee9836ec 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFileAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFileAction.cs @@ -2,43 +2,42 @@ using System.Threading; using System.Threading.Tasks; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a plugin prerequisite action that deletes a file +/// +public class DeleteFileAction : PluginPrerequisiteAction { /// - /// Represents a plugin prerequisite action that deletes a file + /// Creates a new instance of a copy folder action /// - public class DeleteFileAction : PluginPrerequisiteAction + /// The name of the action + /// The target folder to delete recursively + public DeleteFileAction(string name, string target) : base(name) { - /// - /// Creates a new instance of a copy folder action - /// - /// The name of the action - /// The target folder to delete recursively - public DeleteFileAction(string name, string target) : base(name) + Target = target; + ProgressIndeterminate = true; + } + + /// + /// Gets or sets the target directory + /// + public string Target { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + ShowProgressBar = true; + Status = $"Removing {Target}"; + + await Task.Run(() => { - Target = target; - ProgressIndeterminate = true; - } + if (File.Exists(Target)) + File.Delete(Target); + }, cancellationToken); - /// - /// Gets or sets the target directory - /// - public string Target { get; } - - /// - public override async Task Execute(CancellationToken cancellationToken) - { - ShowProgressBar = true; - Status = $"Removing {Target}"; - - await Task.Run(() => - { - if (File.Exists(Target)) - File.Delete(Target); - }, cancellationToken); - - ShowProgressBar = false; - Status = $"Removed {Target}"; - } + ShowProgressBar = false; + Status = $"Removed {Target}"; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFolderAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFolderAction.cs index 62b4dfc41..aa8e2f779 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFolderAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFolderAction.cs @@ -4,46 +4,45 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a plugin prerequisite action that recursively deletes a folder +/// +public class DeleteFolderAction : PluginPrerequisiteAction { /// - /// Represents a plugin prerequisite action that recursively deletes a folder + /// Creates a new instance of a copy folder action /// - public class DeleteFolderAction : PluginPrerequisiteAction + /// The name of the action + /// The target folder to delete recursively + public DeleteFolderAction(string name, string target) : base(name) { - /// - /// Creates a new instance of a copy folder action - /// - /// The name of the action - /// The target folder to delete recursively - public DeleteFolderAction(string name, string target) : base(name) + if (Enum.GetValues().Select(Environment.GetFolderPath).Contains(target)) + throw new ArtemisCoreException($"Cannot delete special folder {target}, silly goose."); + + Target = target; + ProgressIndeterminate = true; + } + + /// + /// Gets or sets the target directory + /// + public string Target { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + ShowProgressBar = true; + Status = $"Removing {Target}"; + + await Task.Run(() => { - if (Enum.GetValues().Select(Environment.GetFolderPath).Contains(target)) - throw new ArtemisCoreException($"Cannot delete special folder {target}, silly goose."); + if (Directory.Exists(Target)) + Directory.Delete(Target, true); + }, cancellationToken); - Target = target; - ProgressIndeterminate = true; - } - - /// - /// Gets or sets the target directory - /// - public string Target { get; } - - /// - public override async Task Execute(CancellationToken cancellationToken) - { - ShowProgressBar = true; - Status = $"Removing {Target}"; - - await Task.Run(() => - { - if (Directory.Exists(Target)) - Directory.Delete(Target, true); - }, cancellationToken); - - ShowProgressBar = false; - Status = $"Removed {Target}"; - } + ShowProgressBar = false; + Status = $"Removed {Target}"; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DownloadFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DownloadFileAction.cs index 8d09f4766..1c7f64bc1 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DownloadFileAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DownloadFileAction.cs @@ -5,102 +5,101 @@ using System.Threading; using System.Threading.Tasks; using Humanizer; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a plugin prerequisite action that downloads a file +/// +public class DownloadFileAction : PluginPrerequisiteAction { /// - /// Represents a plugin prerequisite action that downloads a file + /// Creates a new instance of a copy folder action /// - public class DownloadFileAction : PluginPrerequisiteAction + /// The name of the action + /// The source URL to download + /// The target file to save as (will be created if needed) + public DownloadFileAction(string name, string url, string fileName) : base(name) { - /// - /// Creates a new instance of a copy folder action - /// - /// The name of the action - /// The source URL to download - /// The target file to save as (will be created if needed) - public DownloadFileAction(string name, string url, string fileName) : base(name) - { - Url = url ?? throw new ArgumentNullException(nameof(url)); - FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + Url = url ?? throw new ArgumentNullException(nameof(url)); + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); - ShowProgressBar = true; + ShowProgressBar = true; + } + + /// + /// Creates a new instance of a copy folder action + /// + /// The name of the action + /// A function returning the URL to download + /// The target file to save as (will be created if needed) + public DownloadFileAction(string name, Func> urlFunction, string fileName) : base(name) + { + UrlFunction = urlFunction ?? throw new ArgumentNullException(nameof(urlFunction)); + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + + ShowProgressBar = true; + } + + /// + /// Gets the source URL to download + /// + public string? Url { get; } + + /// + /// Gets the function returning the URL to download + /// + public Func>? UrlFunction { get; } + + /// + /// Gets the target file to save as (will be created if needed) + /// + public string FileName { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + using HttpClient client = new(); + await using FileStream destinationStream = new(FileName, FileMode.OpenOrCreate); + string? url = Url; + if (url is null) + { + Status = "Retrieving download URL"; + url = await UrlFunction!(); } - /// - /// Creates a new instance of a copy folder action - /// - /// The name of the action - /// A function returning the URL to download - /// The target file to save as (will be created if needed) - public DownloadFileAction(string name, Func> urlFunction, string fileName) : base(name) + void ProgressOnProgressReported(object? sender, EventArgs e) { - UrlFunction = urlFunction ?? throw new ArgumentNullException(nameof(urlFunction)); - FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); - - ShowProgressBar = true; - } - - /// - /// Gets the source URL to download - /// - public string? Url { get; } - - /// - /// Gets the function returning the URL to download - /// - public Func>? UrlFunction { get; } - - /// - /// Gets the target file to save as (will be created if needed) - /// - public string FileName { get; } - - /// - public override async Task Execute(CancellationToken cancellationToken) - { - using HttpClient client = new(); - await using FileStream destinationStream = new(FileName, FileMode.OpenOrCreate); - string? url = Url; - if (url is null) - { - Status = "Retrieving download URL"; - url = await UrlFunction!(); - } - - void ProgressOnProgressReported(object? sender, EventArgs e) - { - if (Progress.ProgressPerSecond != 0) - Status = $"Downloading {url} - {Progress.ProgressPerSecond.Bytes().Humanize("#.##")}/sec"; - else - Status = $"Downloading {url}"; - } - - Progress.ProgressReported += ProgressOnProgressReported; - - // Get the http headers first to examine the content length - using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - await using Stream download = await response.Content.ReadAsStreamAsync(cancellationToken); - long? contentLength = response.Content.Headers.ContentLength; - - // Ignore progress reporting when no progress reporter was - // passed or when the content length is unknown - if (!contentLength.HasValue) - { - ProgressIndeterminate = true; - await download.CopyToAsync(destinationStream, Progress, cancellationToken); - ProgressIndeterminate = false; - } + if (Progress.ProgressPerSecond != 0) + Status = $"Downloading {url} - {Progress.ProgressPerSecond.Bytes().Humanize("#.##")}/sec"; else - { - ProgressIndeterminate = false; - await download.CopyToAsync(contentLength.Value, destinationStream, Progress, cancellationToken); - } - - cancellationToken.ThrowIfCancellationRequested(); - - Progress.ProgressReported -= ProgressOnProgressReported; - Progress.Report((1, 1)); - Status = "Finished downloading"; + Status = $"Downloading {url}"; } + + Progress.ProgressReported += ProgressOnProgressReported; + + // Get the http headers first to examine the content length + using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + await using Stream download = await response.Content.ReadAsStreamAsync(cancellationToken); + long? contentLength = response.Content.Headers.ContentLength; + + // Ignore progress reporting when no progress reporter was + // passed or when the content length is unknown + if (!contentLength.HasValue) + { + ProgressIndeterminate = true; + await download.CopyToAsync(destinationStream, Progress, cancellationToken); + ProgressIndeterminate = false; + } + else + { + ProgressIndeterminate = false; + await download.CopyToAsync(contentLength.Value, destinationStream, Progress, cancellationToken); + } + + cancellationToken.ThrowIfCancellationRequested(); + + Progress.ProgressReported -= ProgressOnProgressReported; + Progress.Report((1, 1)); + Status = "Finished downloading"; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExecuteFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExecuteFileAction.cs index e98f0f354..404310ec1 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExecuteFileAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExecuteFileAction.cs @@ -4,109 +4,108 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a plugin prerequisite action that executes a file +/// +public class ExecuteFileAction : PluginPrerequisiteAction { /// - /// Represents a plugin prerequisite action that executes a file + /// Creates a new instance of /// - public class ExecuteFileAction : PluginPrerequisiteAction + /// The name of the action + /// The target file to execute + /// A set of command-line arguments to use when starting the application + /// A boolean indicating whether the action should wait for the process to exit + /// A boolean indicating whether the file should run with administrator privileges + public ExecuteFileAction(string name, string fileName, string? arguments = null, bool waitForExit = true, bool elevate = false) : base(name) { - /// - /// Creates a new instance of - /// - /// The name of the action - /// The target file to execute - /// A set of command-line arguments to use when starting the application - /// A boolean indicating whether the action should wait for the process to exit - /// A boolean indicating whether the file should run with administrator privileges - public ExecuteFileAction(string name, string fileName, string? arguments = null, bool waitForExit = true, bool elevate = false) : base(name) + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + Arguments = arguments; + WaitForExit = waitForExit; + Elevate = elevate; + } + + /// + /// Gets the target file to execute + /// + public string FileName { get; } + + /// + /// Gets a set of command-line arguments to use when starting the application + /// + public string? Arguments { get; } + + /// + /// Gets a boolean indicating whether the action should wait for the process to exit + /// + public bool WaitForExit { get; } + + /// + /// Gets a boolean indicating whether the file should run with administrator privileges + /// + public bool Elevate { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + if (WaitForExit) { - FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); - Arguments = arguments; - WaitForExit = waitForExit; - Elevate = elevate; + Status = $"Running {FileName} and waiting for exit.."; + ShowProgressBar = true; + ProgressIndeterminate = true; + + int result = await RunProcessAsync(FileName, Arguments, Elevate); + + Status = $"{FileName} exited with code {result}"; } - - /// - /// Gets the target file to execute - /// - public string FileName { get; } - - /// - /// Gets a set of command-line arguments to use when starting the application - /// - public string? Arguments { get; } - - /// - /// Gets a boolean indicating whether the action should wait for the process to exit - /// - public bool WaitForExit { get; } - - /// - /// Gets a boolean indicating whether the file should run with administrator privileges - /// - public bool Elevate { get; } - - /// - public override async Task Execute(CancellationToken cancellationToken) + else { - if (WaitForExit) - { - Status = $"Running {FileName} and waiting for exit.."; - ShowProgressBar = true; - ProgressIndeterminate = true; - - int result = await RunProcessAsync(FileName, Arguments, Elevate); - - Status = $"{FileName} exited with code {result}"; - } - else - { - Status = $"Running {FileName}"; - Process process = new() - { - StartInfo = {FileName = FileName, Arguments = Arguments!}, - EnableRaisingEvents = true - }; - process.Start(); - } - } - - internal static Task RunProcessAsync(string fileName, string? arguments, bool elevate) - { - TaskCompletionSource tcs = new(); - + Status = $"Running {FileName}"; Process process = new() { - StartInfo = - { - FileName = fileName, - Arguments = arguments!, - Verb = elevate ? "RunAs" : "", - UseShellExecute = elevate - }, + StartInfo = {FileName = FileName, Arguments = Arguments!}, EnableRaisingEvents = true }; - - process.Exited += (_, _) => - { - tcs.SetResult(process.ExitCode); - process.Dispose(); - }; - - try - { - process.Start(); - } - catch (Win32Exception e) - { - if (!elevate || e.NativeErrorCode != 0x4c7) - throw; - tcs.SetResult(-1); - } - - - return tcs.Task; + process.Start(); } } + + internal static Task RunProcessAsync(string fileName, string? arguments, bool elevate) + { + TaskCompletionSource tcs = new(); + + Process process = new() + { + StartInfo = + { + FileName = fileName, + Arguments = arguments!, + Verb = elevate ? "RunAs" : "", + UseShellExecute = elevate + }, + EnableRaisingEvents = true + }; + + process.Exited += (_, _) => + { + tcs.SetResult(process.ExitCode); + process.Dispose(); + }; + + try + { + process.Start(); + } + catch (Win32Exception e) + { + if (!elevate || e.NativeErrorCode != 0x4c7) + throw; + tcs.SetResult(-1); + } + + + return tcs.Task; + } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExtractArchiveAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExtractArchiveAction.cs index 033379a6f..3135675a8 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExtractArchiveAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExtractArchiveAction.cs @@ -7,89 +7,88 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a plugin prerequisite action that extracts a ZIP file +/// +public class ExtractArchiveAction : PluginPrerequisiteAction { /// - /// Represents a plugin prerequisite action that extracts a ZIP file + /// Creates a new instance of . /// - public class ExtractArchiveAction : PluginPrerequisiteAction + /// The name of the action + /// The ZIP file to extract + /// The folder into which to extract the file + public ExtractArchiveAction(string name, string fileName, string target) : base(name) { - /// - /// Creates a new instance of . - /// - /// The name of the action - /// The ZIP file to extract - /// The folder into which to extract the file - public ExtractArchiveAction(string name, string fileName, string target) : base(name) + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + Target = target ?? throw new ArgumentNullException(nameof(target)); + + ShowProgressBar = true; + } + + /// + /// Gets the file to extract + /// + public string FileName { get; } + + /// + /// Gets the folder into which to extract the file + /// + public string Target { get; } + + /// + /// Gets or sets an optional list of files to extract, if all files will be extracted. + /// + public List? FilesToExtract { get; set; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + using HttpClient client = new(); + + ShowSubProgressBar = true; + Status = $"Extracting {FileName}"; + + Utilities.CreateAccessibleDirectory(Target); + + await using (FileStream fileStream = new(FileName, FileMode.Open)) { - FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); - Target = target ?? throw new ArgumentNullException(nameof(target)); + ZipArchive archive = new(fileStream); + long count = 0; - ShowProgressBar = true; - } + List entries = new(archive.Entries); + if (FilesToExtract != null) + entries = entries.Where(e => FilesToExtract.Contains(e.FullName)).ToList(); - /// - /// Gets the file to extract - /// - public string FileName { get; } - - /// - /// Gets the folder into which to extract the file - /// - public string Target { get; } - - /// - /// Gets or sets an optional list of files to extract, if all files will be extracted. - /// - public List? FilesToExtract { get; set; } - - /// - public override async Task Execute(CancellationToken cancellationToken) - { - using HttpClient client = new(); - - ShowSubProgressBar = true; - Status = $"Extracting {FileName}"; - - Utilities.CreateAccessibleDirectory(Target); - - await using (FileStream fileStream = new(FileName, FileMode.Open)) + foreach (ZipArchiveEntry entry in entries) { - ZipArchive archive = new(fileStream); - long count = 0; - - List entries = new(archive.Entries); - if (FilesToExtract != null) - entries = entries.Where(e => FilesToExtract.Contains(e.FullName)).ToList(); - - foreach (ZipArchiveEntry entry in entries) + await using Stream unzippedEntryStream = entry.Open(); + Progress.Report((count, entries.Count)); + if (entry.Length > 0) { - await using Stream unzippedEntryStream = entry.Open(); - Progress.Report((count, entries.Count)); - if (entry.Length > 0) - { - string path = Path.Combine(Target, entry.FullName); - CreateDirectoryForFile(path); - await using Stream extractStream = new FileStream(path, FileMode.OpenOrCreate); - await unzippedEntryStream.CopyToAsync(entry.Length, extractStream, SubProgress, cancellationToken); - } - - count++; + string path = Path.Combine(Target, entry.FullName); + CreateDirectoryForFile(path); + await using Stream extractStream = new FileStream(path, FileMode.OpenOrCreate); + await unzippedEntryStream.CopyToAsync(entry.Length, extractStream, SubProgress, cancellationToken); } + + count++; } - - Progress.Report((1, 1)); - ShowSubProgressBar = false; - Status = "Finished extracting"; } - private static void CreateDirectoryForFile(string path) - { - string? directory = Path.GetDirectoryName(path); - if (directory == null) - throw new ArtemisCoreException($"Failed to get directory from path {path}"); - - Utilities.CreateAccessibleDirectory(directory); - } + Progress.Report((1, 1)); + ShowSubProgressBar = false; + Status = "Finished extracting"; + } + + private static void CreateDirectoryForFile(string path) + { + string? directory = Path.GetDirectoryName(path); + if (directory == null) + throw new ArtemisCoreException($"Failed to get directory from path {path}"); + + Utilities.CreateAccessibleDirectory(directory); } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunInlinePowerShellAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunInlinePowerShellAction.cs index df50a122c..f8ce9f688 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunInlinePowerShellAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunInlinePowerShellAction.cs @@ -2,55 +2,55 @@ using System.Threading; using System.Threading.Tasks; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a plugin prerequisite action runs inline powershell +/// +public class RunInlinePowerShellAction : PluginPrerequisiteAction { /// - /// Represents a plugin prerequisite action runs inline powershell + /// Creates a new instance of a copy folder action /// - public class RunInlinePowerShellAction : PluginPrerequisiteAction + /// The name of the action + /// The inline code to run + /// A boolean indicating whether the file should run with administrator privileges + /// + /// Optional arguments to pass to your script, you are responsible for proper quoting etc. + /// Arguments are available in PowerShell as $args[0], $args[1] etc. + /// + public RunInlinePowerShellAction(string name, string code, bool elevate = false, string? arguments = null) : base(name) { - /// - /// Creates a new instance of a copy folder action - /// - /// The name of the action - /// The inline code to run - /// A boolean indicating whether the file should run with administrator privileges - /// - /// Optional arguments to pass to your script, you are responsible for proper quoting etc. - /// Arguments are available in PowerShell as $args[0], $args[1] etc. - /// - public RunInlinePowerShellAction(string name, string code, bool elevate = false, string? arguments = null) : base(name) + Code = code; + Elevate = elevate; + Arguments = arguments; + ProgressIndeterminate = true; + } + + /// + /// Gets the inline code to run + /// + public string Code { get; } + + /// + /// Gets a boolean indicating whether the file should run with administrator privileges + /// + public bool Elevate { get; } + + /// + /// Gets optional arguments to pass to your script, you are responsible for proper quoting etc. + /// Arguments are available in PowerShell as $args[0], $args[1] etc. + /// + public string? Arguments { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + string file = Path.GetTempFileName().Replace(".tmp", ".ps1"); + try { - Code = code; - Elevate = elevate; - Arguments = arguments; - ProgressIndeterminate = true; - } - - /// - /// Gets the inline code to run - /// - public string Code { get; } - - /// - /// Gets a boolean indicating whether the file should run with administrator privileges - /// - public bool Elevate { get; } - - /// - /// Gets optional arguments to pass to your script, you are responsible for proper quoting etc. - /// Arguments are available in PowerShell as $args[0], $args[1] etc. - /// - public string? Arguments { get; } - - /// - public override async Task Execute(CancellationToken cancellationToken) - { - string file = Path.GetTempFileName().Replace(".tmp", ".ps1"); - try - { - string code = - @"try + string code = + @"try { " + Code + @" Start-Sleep 1 @@ -60,22 +60,21 @@ namespace Artemis.Core Write-Error $_.Exception.ToString() pause }"; - - await File.WriteAllTextAsync(file, code, cancellationToken); - cancellationToken.ThrowIfCancellationRequested(); - Status = "Running PowerShell script and waiting for exit.."; - ShowProgressBar = true; - ProgressIndeterminate = true; + await File.WriteAllTextAsync(file, code, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); - int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-ExecutionPolicy Unrestricted -File {file} {Arguments}", Elevate); + Status = "Running PowerShell script and waiting for exit.."; + ShowProgressBar = true; + ProgressIndeterminate = true; - Status = $"PowerShell exited with code {result}"; - } - finally - { - File.Delete(file); - } + int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-ExecutionPolicy Unrestricted -File {file} {Arguments}", Elevate); + + Status = $"PowerShell exited with code {result}"; + } + finally + { + File.Delete(file); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunPowerShellAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunPowerShellAction.cs index 4b19a03a6..060ef07e2 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunPowerShellAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/RunPowerShellAction.cs @@ -2,63 +2,62 @@ using System.Threading; using System.Threading.Tasks; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a plugin prerequisite action that runs a PowerShell script +/// Note: To run an inline script instead, use +/// +public class RunPowerShellAction : PluginPrerequisiteAction { /// - /// Represents a plugin prerequisite action that runs a PowerShell script - /// Note: To run an inline script instead, use + /// Creates a new instance of a copy folder action /// - public class RunPowerShellAction : PluginPrerequisiteAction + /// The name of the action + /// The full path of the script to run + /// A boolean indicating whether the file should run with administrator privileges + /// + /// Optional arguments to pass to your script, you are responsible for proper quoting etc. + /// Arguments are available in PowerShell as $args[0], $args[1] etc. + /// + public RunPowerShellAction(string name, string scriptPath, bool elevate = false, string? arguments = null) : base(name) { - /// - /// Creates a new instance of a copy folder action - /// - /// The name of the action - /// The full path of the script to run - /// A boolean indicating whether the file should run with administrator privileges - /// - /// Optional arguments to pass to your script, you are responsible for proper quoting etc. - /// Arguments are available in PowerShell as $args[0], $args[1] etc. - /// - public RunPowerShellAction(string name, string scriptPath, bool elevate = false, string? arguments = null) : base(name) - { - ScriptPath = scriptPath; - Elevate = elevate; - Arguments = arguments; - ProgressIndeterminate = true; - } + ScriptPath = scriptPath; + Elevate = elevate; + Arguments = arguments; + ProgressIndeterminate = true; + } - /// - /// Gets the inline full path of the script to run - /// - public string ScriptPath { get; } + /// + /// Gets the inline full path of the script to run + /// + public string ScriptPath { get; } - /// - /// Gets a boolean indicating whether the file should run with administrator privileges - /// - public bool Elevate { get; } + /// + /// Gets a boolean indicating whether the file should run with administrator privileges + /// + public bool Elevate { get; } - /// - /// Gets optional arguments to pass to your script, you are responsible for proper quoting etc. - /// Arguments are available in PowerShell as $args[0], $args[1] etc. - /// - public string? Arguments { get; } + /// + /// Gets optional arguments to pass to your script, you are responsible for proper quoting etc. + /// Arguments are available in PowerShell as $args[0], $args[1] etc. + /// + public string? Arguments { get; } - /// - public override async Task Execute(CancellationToken cancellationToken) - { - if (!ScriptPath.EndsWith(".ps1")) - throw new ArtemisPluginException($"Script at path {ScriptPath} must have the .ps1 extension or PowerShell will refuse to run it"); - if (!File.Exists(ScriptPath)) - throw new ArtemisCoreException($"Script not found at path {ScriptPath}"); + /// + public override async Task Execute(CancellationToken cancellationToken) + { + if (!ScriptPath.EndsWith(".ps1")) + throw new ArtemisPluginException($"Script at path {ScriptPath} must have the .ps1 extension or PowerShell will refuse to run it"); + if (!File.Exists(ScriptPath)) + throw new ArtemisCoreException($"Script not found at path {ScriptPath}"); - Status = "Running PowerShell script and waiting for exit.."; - ShowProgressBar = true; - ProgressIndeterminate = true; + Status = "Running PowerShell script and waiting for exit.."; + ShowProgressBar = true; + ProgressIndeterminate = true; - int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-ExecutionPolicy Unrestricted -File {ScriptPath} {Arguments}", Elevate); + int result = await ExecuteFileAction.RunProcessAsync("powershell.exe", $"-ExecutionPolicy Unrestricted -File {ScriptPath} {Arguments}", Elevate); - Status = $"PowerShell exited with code {result}"; - } + Status = $"PowerShell exited with code {result}"; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteBytesToFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteBytesToFileAction.cs index 92c7d0294..45ed78446 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteBytesToFileAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteBytesToFileAction.cs @@ -1,62 +1,60 @@ using System; using System.IO; -using System.Text; using System.Threading; using System.Threading.Tasks; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a plugin prerequisite action that copies a folder +/// +public class WriteBytesToFileAction : PluginPrerequisiteAction { /// - /// Represents a plugin prerequisite action that copies a folder + /// Creates a new instance of a copy folder action /// - public class WriteBytesToFileAction : PluginPrerequisiteAction + /// The name of the action + /// The target file to write to (will be created if needed) + /// The contents to write + public WriteBytesToFileAction(string name, string target, byte[] content) : base(name) { - /// - /// Creates a new instance of a copy folder action - /// - /// The name of the action - /// The target file to write to (will be created if needed) - /// The contents to write - public WriteBytesToFileAction(string name, string target, byte[] content) : base(name) - { - Target = target; - ByteContent = content ?? throw new ArgumentNullException(nameof(content)); - } + Target = target; + ByteContent = content ?? throw new ArgumentNullException(nameof(content)); + } - /// - /// Gets or sets the target file - /// - public string Target { get; } + /// + /// Gets or sets the target file + /// + public string Target { get; } - /// - /// Gets or sets a boolean indicating whether or not to append to the file if it exists already, if set to - /// the file will be deleted and recreated - /// - public bool Append { get; set; } = false; + /// + /// Gets or sets a boolean indicating whether or not to append to the file if it exists already, if set to + /// the file will be deleted and recreated + /// + public bool Append { get; set; } = false; - /// - /// Gets the bytes that will be written - /// - public byte[] ByteContent { get; } + /// + /// Gets the bytes that will be written + /// + public byte[] ByteContent { get; } - /// - public override async Task Execute(CancellationToken cancellationToken) - { - string outputDir = Path.GetDirectoryName(Target)!; - Utilities.CreateAccessibleDirectory(outputDir); + /// + public override async Task Execute(CancellationToken cancellationToken) + { + string outputDir = Path.GetDirectoryName(Target)!; + Utilities.CreateAccessibleDirectory(outputDir); - ShowProgressBar = true; - Status = $"Writing to {Path.GetFileName(Target)}..."; + ShowProgressBar = true; + Status = $"Writing to {Path.GetFileName(Target)}..."; - if (!Append && File.Exists(Target)) - File.Delete(Target); - - await using Stream fileStream = File.OpenWrite(Target); - await using MemoryStream sourceStream = new(ByteContent); - await sourceStream.CopyToAsync(sourceStream.Length, fileStream, Progress, cancellationToken); + if (!Append && File.Exists(Target)) + File.Delete(Target); - ShowProgressBar = false; - Status = $"Finished writing to {Path.GetFileName(Target)}"; - } + await using Stream fileStream = File.OpenWrite(Target); + await using MemoryStream sourceStream = new(ByteContent); + await sourceStream.CopyToAsync(sourceStream.Length, fileStream, Progress, cancellationToken); + + ShowProgressBar = false; + Status = $"Finished writing to {Path.GetFileName(Target)}"; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteStringToFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteStringToFileAction.cs index e4e6a9875..e704e028f 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteStringToFileAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteStringToFileAction.cs @@ -1,62 +1,60 @@ using System; using System.IO; -using System.Text; using System.Threading; using System.Threading.Tasks; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a plugin prerequisite action that copies a folder +/// +public class WriteStringToFileAction : PluginPrerequisiteAction { /// - /// Represents a plugin prerequisite action that copies a folder + /// Creates a new instance of a copy folder action /// - public class WriteStringToFileAction : PluginPrerequisiteAction + /// The name of the action + /// The target file to write to (will be created if needed) + /// The contents to write + public WriteStringToFileAction(string name, string target, string content) : base(name) { - /// - /// Creates a new instance of a copy folder action - /// - /// The name of the action - /// The target file to write to (will be created if needed) - /// The contents to write - public WriteStringToFileAction(string name, string target, string content) : base(name) - { - Target = target; - Content = content ?? throw new ArgumentNullException(nameof(content)); + Target = target; + Content = content ?? throw new ArgumentNullException(nameof(content)); - ProgressIndeterminate = true; - } + ProgressIndeterminate = true; + } - /// - /// Gets or sets the target file - /// - public string Target { get; } + /// + /// Gets or sets the target file + /// + public string Target { get; } - /// - /// Gets or sets a boolean indicating whether or not to append to the file if it exists already, if set to - /// the file will be deleted and recreated - /// - public bool Append { get; set; } = false; + /// + /// Gets or sets a boolean indicating whether or not to append to the file if it exists already, if set to + /// the file will be deleted and recreated + /// + public bool Append { get; set; } = false; - /// - /// Gets the string that will be written - /// - public string Content { get; } + /// + /// Gets the string that will be written + /// + public string Content { get; } - /// - public override async Task Execute(CancellationToken cancellationToken) - { - string outputDir = Path.GetDirectoryName(Target)!; - Utilities.CreateAccessibleDirectory(outputDir); + /// + public override async Task Execute(CancellationToken cancellationToken) + { + string outputDir = Path.GetDirectoryName(Target)!; + Utilities.CreateAccessibleDirectory(outputDir); - ShowProgressBar = true; - Status = $"Writing to {Path.GetFileName(Target)}..."; + ShowProgressBar = true; + Status = $"Writing to {Path.GetFileName(Target)}..."; - if (Append) - await File.AppendAllTextAsync(Target, Content, cancellationToken); - else - await File.WriteAllTextAsync(Target, Content, cancellationToken); + if (Append) + await File.AppendAllTextAsync(Target, Content, cancellationToken); + else + await File.WriteAllTextAsync(Target, Content, cancellationToken); - ShowProgressBar = false; - Status = $"Finished writing to {Path.GetFileName(Target)}"; - } + ShowProgressBar = false; + Status = $"Finished writing to {Path.GetFileName(Target)}"; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteActionProgress.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteActionProgress.cs index 4cefa4582..23dd8e1fa 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteActionProgress.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteActionProgress.cs @@ -1,94 +1,89 @@ using System; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents progress on a plugin prerequisite action +/// +public class PrerequisiteActionProgress : CorePropertyChanged, IProgress<(long, long)> { + private long _current; + private DateTime _lastReport; + private long _lastReportValue; + private double _percentage; + private double _progressPerSecond; + private long _total; + /// - /// Represents progress on a plugin prerequisite action + /// The current amount /// - public class PrerequisiteActionProgress : CorePropertyChanged, IProgress<(long, long)> + public long Current { - private long _current; - private DateTime _lastReport; - private long _lastReportValue; - private double _percentage; - private double _progressPerSecond; - private long _total; - - /// - /// The current amount - /// - public long Current - { - get => _current; - set => SetAndNotify(ref _current, value); - } - - /// - /// The total amount - /// - public long Total - { - get => _total; - set => SetAndNotify(ref _total, value); - } - - /// - /// The percentage - /// - public double Percentage - { - get => _percentage; - set => SetAndNotify(ref _percentage, value); - } - - /// - /// Gets or sets the progress per second - /// - public double ProgressPerSecond - { - get => _progressPerSecond; - set => SetAndNotify(ref _progressPerSecond, value); - } - - #region Implementation of IProgress - - /// - public void Report((long, long) value) - { - (long newCurrent, long newTotal) = value; - - TimeSpan timePassed = DateTime.Now - _lastReport; - if (timePassed >= TimeSpan.FromSeconds(1)) - { - ProgressPerSecond = Math.Max(0, Math.Round(1.0 / timePassed.TotalSeconds * (newCurrent - _lastReportValue), 2)); - _lastReportValue = newCurrent; - _lastReport = DateTime.Now; - } - - Current = newCurrent; - Total = newTotal; - Percentage = Math.Round((double) Current / Total * 100.0, 2); - - OnProgressReported(); - } - - #endregion - - #region Events - - /// - /// Occurs when progress has been reported - /// - public event EventHandler? ProgressReported; - - /// - /// Invokes the event - /// - protected virtual void OnProgressReported() - { - ProgressReported?.Invoke(this, EventArgs.Empty); - } - - #endregion + get => _current; + set => SetAndNotify(ref _current, value); } + + /// + /// The total amount + /// + public long Total + { + get => _total; + set => SetAndNotify(ref _total, value); + } + + /// + /// The percentage + /// + public double Percentage + { + get => _percentage; + set => SetAndNotify(ref _percentage, value); + } + + /// + /// Gets or sets the progress per second + /// + public double ProgressPerSecond + { + get => _progressPerSecond; + set => SetAndNotify(ref _progressPerSecond, value); + } + + /// + /// Occurs when progress has been reported + /// + public event EventHandler? ProgressReported; + + /// + /// Invokes the event + /// + protected virtual void OnProgressReported() + { + ProgressReported?.Invoke(this, EventArgs.Empty); + } + + #region Implementation of IProgress + + /// + public void Report((long, long) value) + { + (long newCurrent, long newTotal) = value; + + TimeSpan timePassed = DateTime.Now - _lastReport; + if (timePassed >= TimeSpan.FromSeconds(1)) + { + ProgressPerSecond = Math.Max(0, Math.Round(1.0 / timePassed.TotalSeconds * (newCurrent - _lastReportValue), 2)); + _lastReportValue = newCurrent; + _lastReport = DateTime.Now; + } + + Current = newCurrent; + Total = newTotal; + Percentage = Math.Round((double) Current / Total * 100.0, 2); + + OnProgressReported(); + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Profiling/Profiler.cs b/src/Artemis.Core/Plugins/Profiling/Profiler.cs index c424c19f1..e920ed3da 100644 --- a/src/Artemis.Core/Plugins/Profiling/Profiler.cs +++ b/src/Artemis.Core/Plugins/Profiling/Profiler.cs @@ -1,84 +1,82 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a profiler that can measure time between calls distinguished by identifiers +/// +public class Profiler { - /// - /// Represents a profiler that can measure time between calls distinguished by identifiers - /// - public class Profiler + internal Profiler(Plugin plugin, string name) { - internal Profiler(Plugin plugin, string name) + Plugin = plugin; + Name = name; + } + + /// + /// Gets the plugin this profiler belongs to + /// + public Plugin Plugin { get; } + + /// + /// Gets the name of this profiler + /// + public string Name { get; } + + + /// + /// Gets a dictionary containing measurements by their identifiers + /// + public Dictionary Measurements { get; set; } = new(); + + /// + /// Starts measuring time for the provided + /// + /// A unique identifier for this measurement + public void StartMeasurement(string identifier) + { + lock (Measurements) { - Plugin = plugin; - Name = name; - } - - /// - /// Gets the plugin this profiler belongs to - /// - public Plugin Plugin { get; } - - /// - /// Gets the name of this profiler - /// - public string Name { get; } - - - /// - /// Gets a dictionary containing measurements by their identifiers - /// - public Dictionary Measurements { get; set; } = new(); - - /// - /// Starts measuring time for the provided - /// - /// A unique identifier for this measurement - public void StartMeasurement(string identifier) - { - lock (Measurements) + if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement)) { - if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement)) - { - measurement = new ProfilingMeasurement(identifier); - Measurements.Add(identifier, measurement); - } - - measurement.Start(); + measurement = new ProfilingMeasurement(identifier); + Measurements.Add(identifier, measurement); } + + measurement.Start(); } + } - /// - /// Stops measuring time for the provided - /// - /// A unique identifier for this measurement - /// The number of ticks that passed since the call with the same identifier - public long StopMeasurement(string identifier) + /// + /// Stops measuring time for the provided + /// + /// A unique identifier for this measurement + /// The number of ticks that passed since the call with the same identifier + public long StopMeasurement(string identifier) + { + long lockRequestedAt = Stopwatch.GetTimestamp(); + lock (Measurements) { - long lockRequestedAt = Stopwatch.GetTimestamp(); - lock (Measurements) + if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement)) { - if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement)) - { - measurement = new ProfilingMeasurement(identifier); - Measurements.Add(identifier, measurement); - } - - return measurement.Stop(Stopwatch.GetTimestamp() - lockRequestedAt); + measurement = new ProfilingMeasurement(identifier); + Measurements.Add(identifier, measurement); } - } - /// - /// Clears measurements with the the provided - /// - /// - public void ClearMeasurements(string identifier) + return measurement.Stop(Stopwatch.GetTimestamp() - lockRequestedAt); + } + } + + /// + /// Clears measurements with the the provided + /// + /// + public void ClearMeasurements(string identifier) + { + lock (Measurements) { - lock (Measurements) - { - Measurements.Remove(identifier); - } + Measurements.Remove(identifier); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs b/src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs index a6d16fef6..1b18d96f7 100644 --- a/src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs +++ b/src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs @@ -1,141 +1,139 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a set of profiling measurements +/// +public class ProfilingMeasurement { - /// - /// Represents a set of profiling measurements - /// - public class ProfilingMeasurement + private bool _filledArray; + private int _index; + private long _last; + private bool _open; + private long _start; + + internal ProfilingMeasurement(string identifier) { - private bool _filledArray; - private int _index; - private long _last; - private bool _open; - private long _start; + Identifier = identifier; + } - internal ProfilingMeasurement(string identifier) + /// + /// Gets the unique identifier of this measurement + /// + public string Identifier { get; } + + /// + /// Gets the last 1000 measurements + /// + public long[] Measurements { get; } = new long[1000]; + + /// + /// Starts measuring time until is called + /// + public void Start() + { + _start = Stopwatch.GetTimestamp(); + _open = true; + } + + /// + /// Stops measuring time and stores the time passed in the list + /// + /// An optional correction in ticks to subtract from the measurement + /// The time passed since the last call + public long Stop(long correction = 0) + { + if (!_open) + return 0; + + long difference = Stopwatch.GetTimestamp() - _start - correction; + _open = false; + Measurements[_index] = difference; + + _index++; + if (_index >= 1000) { - Identifier = identifier; + _filledArray = true; + _index = 0; } - /// - /// Gets the unique identifier of this measurement - /// - public string Identifier { get; } + _last = difference; + return difference; + } - /// - /// Gets the last 1000 measurements - /// - public long[] Measurements { get; } = new long[1000]; + /// + /// Gets the last measured time + /// + public TimeSpan GetLast() + { + return new TimeSpan(_last); + } - /// - /// Starts measuring time until is called - /// - public void Start() - { - _start = Stopwatch.GetTimestamp(); - _open = true; - } + /// + /// Gets the average time of the last 1000 measurements + /// + public TimeSpan GetAverage() + { + if (!_filledArray && _index == 0) + return TimeSpan.Zero; - /// - /// Stops measuring time and stores the time passed in the list - /// - /// An optional correction in ticks to subtract from the measurement - /// The time passed since the last call - public long Stop(long correction = 0) - { - if (!_open) - return 0; + return _filledArray + ? new TimeSpan((long) Measurements.Average(m => m)) + : new TimeSpan((long) Measurements.Take(_index).Average(m => m)); + } - long difference = Stopwatch.GetTimestamp() - _start - correction; - _open = false; - Measurements[_index] = difference; + /// + /// Gets the min time of the last 1000 measurements + /// + public TimeSpan GetMin() + { + if (!_filledArray && _index == 0) + return TimeSpan.Zero; - _index++; - if (_index >= 1000) - { - _filledArray = true; - _index = 0; - } + return _filledArray + ? new TimeSpan(Measurements.Min()) + : new TimeSpan(Measurements.Take(_index).Min()); + } - _last = difference; - return difference; - } + /// + /// Gets the max time of the last 1000 measurements + /// + public TimeSpan GetMax() + { + if (!_filledArray && _index == 0) + return TimeSpan.Zero; - /// - /// Gets the last measured time - /// - public TimeSpan GetLast() - { - return new TimeSpan(_last); - } + return _filledArray + ? new TimeSpan(Measurements.Max()) + : new TimeSpan(Measurements.Take(_index).Max()); + } - /// - /// Gets the average time of the last 1000 measurements - /// - public TimeSpan GetAverage() - { - if (!_filledArray && _index == 0) - return TimeSpan.Zero; + /// + /// Gets the nth percentile of the last 1000 measurements + /// + public TimeSpan GetPercentile(double percentile) + { + if (!_filledArray && _index == 0) + return TimeSpan.Zero; - return _filledArray - ? new TimeSpan((long) Measurements.Average(m => m)) - : new TimeSpan((long) Measurements.Take(_index).Average(m => m)); - } + long[] collection = _filledArray + ? Measurements.OrderBy(l => l).ToArray() + : Measurements.Take(_index).OrderBy(l => l).ToArray(); - /// - /// Gets the min time of the last 1000 measurements - /// - public TimeSpan GetMin() - { - if (!_filledArray && _index == 0) - return TimeSpan.Zero; + return new TimeSpan((long) Percentile(collection, percentile)); + } - return _filledArray - ? new TimeSpan(Measurements.Min()) - : new TimeSpan(Measurements.Take(_index).Min()); - } - - /// - /// Gets the max time of the last 1000 measurements - /// - public TimeSpan GetMax() - { - if (!_filledArray && _index == 0) - return TimeSpan.Zero; - - return _filledArray - ? new TimeSpan(Measurements.Max()) - : new TimeSpan(Measurements.Take(_index).Max()); - } - - /// - /// Gets the nth percentile of the last 1000 measurements - /// - public TimeSpan GetPercentile(double percentile) - { - if (!_filledArray && _index == 0) - return TimeSpan.Zero; - - long[] collection = _filledArray - ? Measurements.OrderBy(l => l).ToArray() - : Measurements.Take(_index).OrderBy(l => l).ToArray(); - - return new TimeSpan((long) Percentile(collection, percentile)); - } - - private static double Percentile(long[] elements, double percentile) - { - Array.Sort(elements); - double realIndex = percentile * (elements.Length - 1); - int index = (int) realIndex; - double frac = realIndex - index; - if (index + 1 < elements.Length) - return elements[index] * (1 - frac) + elements[index + 1] * frac; - return elements[index]; - } + private static double Percentile(long[] elements, double percentile) + { + Array.Sort(elements); + double realIndex = percentile * (elements.Length - 1); + int index = (int) realIndex; + double frac = realIndex - index; + if (index + 1 < elements.Length) + return elements[index] * (1 - frac) + elements[index + 1] * frac; + return elements[index]; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/ScriptingProviders/IScriptEditorViewModel.cs b/src/Artemis.Core/Plugins/ScriptingProviders/IScriptEditorViewModel.cs index 92ba0a646..9e7674b35 100644 --- a/src/Artemis.Core/Plugins/ScriptingProviders/IScriptEditorViewModel.cs +++ b/src/Artemis.Core/Plugins/ScriptingProviders/IScriptEditorViewModel.cs @@ -1,24 +1,23 @@ -namespace Artemis.Core.ScriptingProviders +namespace Artemis.Core.ScriptingProviders; + +/// +/// Represents a view model containing a script editor +/// +public interface IScriptEditorViewModel { /// - /// Represents a view model containing a script editor + /// Gets the script type this view model was created for /// - public interface IScriptEditorViewModel - { - /// - /// Gets the script type this view model was created for - /// - ScriptType ScriptType { get; } + ScriptType ScriptType { get; } - /// - /// Gets the script this editor is editing - /// - Script? Script { get; } + /// + /// Gets the script this editor is editing + /// + Script? Script { get; } - /// - /// Called whenever the view model must display a different script - /// - /// The script to display or if no script is to be displayed - void ChangeScript(Script? script); - } + /// + /// Called whenever the view model must display a different script + /// + /// The script to display or if no script is to be displayed + void ChangeScript(Script? script); } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs index f93fd0413..eb9c2c287 100644 --- a/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs +++ b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs @@ -1,168 +1,163 @@ using System; using Artemis.Storage.Entities.General; -namespace Artemis.Core.ScriptingProviders +namespace Artemis.Core.ScriptingProviders; + +/// +/// Represents the configuration of a script +/// +public class ScriptConfiguration : CorePropertyChanged, IStorageModel { + private bool _hasChanges; + private bool _isSuspended; + private string _name; + private string? _pendingScriptContent; + private string? _scriptContent; + private string _scriptingProviderId; + /// - /// Represents the configuration of a script + /// Creates a new instance of the class /// - public class ScriptConfiguration : CorePropertyChanged, IStorageModel + public ScriptConfiguration(ScriptingProvider provider, string name, ScriptType scriptType) { - private bool _hasChanges; - private bool _isSuspended; - private string _name; - private string? _pendingScriptContent; - private string? _scriptContent; - private string _scriptingProviderId; - - /// - /// Creates a new instance of the class - /// - public ScriptConfiguration(ScriptingProvider provider, string name, ScriptType scriptType) - { - _scriptingProviderId = provider.Id; - _name = name; - Entity = new ScriptConfigurationEntity(); - PendingScriptContent = provider.GetDefaultScriptContent(scriptType); - ScriptContent = PendingScriptContent; - } - - internal ScriptConfiguration(ScriptConfigurationEntity entity) - { - _scriptingProviderId = null!; - _name = null!; - Entity = entity; - - Load(); - } - - /// - /// Gets or sets the ID of the scripting provider - /// - public string ScriptingProviderId - { - get => _scriptingProviderId; - set => SetAndNotify(ref _scriptingProviderId, value); - } - - /// - /// Gets or sets the name of the script - /// - public string Name - { - get => _name; - set => SetAndNotify(ref _name, value); - } - - /// - /// Gets or sets the script's content - /// - public string? ScriptContent - { - get => _scriptContent; - private set - { - if (!SetAndNotify(ref _scriptContent, value)) return; - OnScriptContentChanged(); - } - } - - /// - /// Gets or sets the pending changes to the script's content - /// - public string? PendingScriptContent - { - get => _pendingScriptContent; - set - { - if (string.IsNullOrWhiteSpace(value)) - value = null; - if (!SetAndNotify(ref _pendingScriptContent, value)) return; - HasChanges = ScriptContent != PendingScriptContent; - } - } - - // TODO: Implement suspension - /// - /// [NYI] Gets or sets a boolean indicating whether this configuration is suspended - /// - public bool IsSuspended - { - get => _isSuspended; - set => SetAndNotify(ref _isSuspended, value); - } - - /// - /// Gets or sets a boolean indicating whether this configuration has pending changes to it's - /// - /// - public bool HasChanges - { - get => _hasChanges; - set => SetAndNotify(ref _hasChanges, value); - } - - /// - /// If active, gets the script - /// - public Script? Script { get; internal set; } - - internal ScriptConfigurationEntity Entity { get; } - - /// - /// Applies the to the - /// - public void ApplyPendingChanges() - { - ScriptContent = PendingScriptContent; - HasChanges = false; - } - - /// - /// Discards the - /// - public void DiscardPendingChanges() - { - PendingScriptContent = ScriptContent; - HasChanges = false; - } - - #region Implementation of IStorageModel - - /// - public void Load() - { - ScriptingProviderId = Entity.ScriptingProviderId; - ScriptContent = Entity.ScriptContent; - PendingScriptContent = Entity.ScriptContent; - Name = Entity.Name; - } - - /// - public void Save() - { - Entity.ScriptingProviderId = ScriptingProviderId; - Entity.ScriptContent = ScriptContent; - Entity.Name = Name; - } - - #endregion - - #region Events - - /// - /// Occurs whenever the contents of the script have changed - /// - public event EventHandler? ScriptContentChanged; - - /// - /// Invokes the event - /// - protected virtual void OnScriptContentChanged() - { - ScriptContentChanged?.Invoke(this, EventArgs.Empty); - } - - #endregion + _scriptingProviderId = provider.Id; + _name = name; + Entity = new ScriptConfigurationEntity(); + PendingScriptContent = provider.GetDefaultScriptContent(scriptType); + ScriptContent = PendingScriptContent; } + + internal ScriptConfiguration(ScriptConfigurationEntity entity) + { + _scriptingProviderId = null!; + _name = null!; + Entity = entity; + + Load(); + } + + /// + /// Gets or sets the ID of the scripting provider + /// + public string ScriptingProviderId + { + get => _scriptingProviderId; + set => SetAndNotify(ref _scriptingProviderId, value); + } + + /// + /// Gets or sets the name of the script + /// + public string Name + { + get => _name; + set => SetAndNotify(ref _name, value); + } + + /// + /// Gets or sets the script's content + /// + public string? ScriptContent + { + get => _scriptContent; + private set + { + if (!SetAndNotify(ref _scriptContent, value)) return; + OnScriptContentChanged(); + } + } + + /// + /// Gets or sets the pending changes to the script's content + /// + public string? PendingScriptContent + { + get => _pendingScriptContent; + set + { + if (string.IsNullOrWhiteSpace(value)) + value = null; + if (!SetAndNotify(ref _pendingScriptContent, value)) return; + HasChanges = ScriptContent != PendingScriptContent; + } + } + + // TODO: Implement suspension + /// + /// [NYI] Gets or sets a boolean indicating whether this configuration is suspended + /// + public bool IsSuspended + { + get => _isSuspended; + set => SetAndNotify(ref _isSuspended, value); + } + + /// + /// Gets or sets a boolean indicating whether this configuration has pending changes to it's + /// + /// + public bool HasChanges + { + get => _hasChanges; + set => SetAndNotify(ref _hasChanges, value); + } + + /// + /// If active, gets the script + /// + public Script? Script { get; internal set; } + + internal ScriptConfigurationEntity Entity { get; } + + /// + /// Applies the to the + /// + public void ApplyPendingChanges() + { + ScriptContent = PendingScriptContent; + HasChanges = false; + } + + /// + /// Discards the + /// + public void DiscardPendingChanges() + { + PendingScriptContent = ScriptContent; + HasChanges = false; + } + + /// + /// Occurs whenever the contents of the script have changed + /// + public event EventHandler? ScriptContentChanged; + + /// + /// Invokes the event + /// + protected virtual void OnScriptContentChanged() + { + ScriptContentChanged?.Invoke(this, EventArgs.Empty); + } + + #region Implementation of IStorageModel + + /// + public void Load() + { + ScriptingProviderId = Entity.ScriptingProviderId; + ScriptContent = Entity.ScriptContent; + PendingScriptContent = Entity.ScriptContent; + Name = Entity.Name; + } + + /// + public void Save() + { + Entity.ScriptingProviderId = ScriptingProviderId; + Entity.ScriptContent = ScriptContent; + Entity.Name = Name; + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs index cb69e82ad..5148c823a 100644 --- a/src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs +++ b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs @@ -2,80 +2,79 @@ using System.Collections.Generic; using System.Collections.ObjectModel; -namespace Artemis.Core.ScriptingProviders +namespace Artemis.Core.ScriptingProviders; + +/// +/// Allows you to implement and register your own scripting provider. +/// +public abstract class ScriptingProvider : ScriptingProvider + where TGlobalScript : GlobalScript + where TProfileScript : ProfileScript +{ + #region Overrides of PluginFeature + + /// + internal override void InternalDisable() + { + base.InternalDisable(); + + while (Scripts.Count > 0) + Scripts[0].Dispose(); + } + + #endregion + + #region Overrides of ScriptingProvider + + /// + internal override Type ProfileScriptType => typeof(TProfileScript); + + /// + internal override Type GlobalScriptType => typeof(TGlobalScript); + + #endregion +} + +/// +/// Allows you to implement and register your own scripting provider. +/// +/// Note: You can't implement this, implement +/// instead. +/// +/// +public abstract class ScriptingProvider : PluginFeature { /// - /// Allows you to implement and register your own scripting provider. + /// The base constructor of the class /// - public abstract class ScriptingProvider : ScriptingProvider - where TGlobalScript : GlobalScript - where TProfileScript : ProfileScript + protected ScriptingProvider() { - #region Overrides of PluginFeature - - /// - internal override void InternalDisable() - { - base.InternalDisable(); - - while (Scripts.Count > 0) - Scripts[0].Dispose(); - } - - #endregion - - #region Overrides of ScriptingProvider - - /// - internal override Type ProfileScriptType => typeof(TProfileScript); - - /// - internal override Type GlobalScriptType => typeof(TGlobalScript); - - #endregion + Scripts = new ReadOnlyCollection