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

Merge pull request #714 from Artemis-RGB/development

Bugfixes
This commit is contained in:
RobertBeekman 2022-08-21 11:38:16 +02:00 committed by GitHub
commit bc8b3b5482
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
698 changed files with 37309 additions and 38379 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,32 +1,31 @@
using System.IO; 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()) file.IsReadOnly = false;
CopyFilesRecursively(dir, target.CreateSubdirectory(dir.Name)); file.Delete();
foreach (FileInfo file in source.GetFiles())
file.CopyTo(Path.Combine(target.FullName, file.Name));
} }
public static void DeleteRecursively(this DirectoryInfo baseDir) baseDir.Delete();
{
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();
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -2,39 +2,38 @@
using RGB.NET.Core; using RGB.NET.Core;
using SkiaSharp; 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);
StringBuilder builder = new(); builder.Append('-');
builder.Append(rgbDevice.DeviceInfo.DeviceName); builder.Append(rgbDevice.DeviceInfo.Manufacturer);
builder.Append('-'); builder.Append('-');
builder.Append(rgbDevice.DeviceInfo.Manufacturer); builder.Append(rgbDevice.DeviceInfo.Model);
builder.Append('-'); builder.Append('-');
builder.Append(rgbDevice.DeviceInfo.Model); builder.Append(rgbDevice.DeviceInfo.DeviceType);
builder.Append('-'); return builder.ToString();
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 SKRectI.Round(ToSKRect(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));
}
} }
} }

View File

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

View File

@ -1,16 +1,15 @@
using SkiaSharp; 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.ImageFilter?.Dispose(); paint.MaskFilter?.Dispose();
paint.ColorFilter?.Dispose(); paint.Shader?.Dispose();
paint.MaskFilter?.Dispose(); paint.Dispose();
paint.Shader?.Dispose();
paint.Dispose();
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,55 +1,54 @@
using System; using System;
namespace Artemis.Core namespace Artemis.Core;
/// <summary>
/// Represents a description attribute used to decorate layer properties
/// </summary>
public class PropertyDescriptionAttribute : Attribute
{ {
/// <summary> /// <summary>
/// Represents a description attribute used to decorate layer properties /// The identifier of this property used for storage, if not set one will be generated property name in code
/// </summary> /// </summary>
public class PropertyDescriptionAttribute : Attribute public string? Identifier { get; set; }
{
/// <summary>
/// The identifier of this property used for storage, if not set one will be generated property name in code
/// </summary>
public string? Identifier { get; set; }
/// <summary> /// <summary>
/// The user-friendly name for this property, shown in the UI /// The user-friendly name for this property, shown in the UI
/// </summary> /// </summary>
public string? Name { get; set; } public string? Name { get; set; }
/// <summary> /// <summary>
/// The user-friendly description for this property, shown in the UI /// The user-friendly description for this property, shown in the UI
/// </summary> /// </summary>
public string? Description { get; set; } public string? Description { get; set; }
/// <summary> /// <summary>
/// Input prefix to show before input elements in the UI /// Input prefix to show before input elements in the UI
/// </summary> /// </summary>
public string? InputPrefix { get; set; } public string? InputPrefix { get; set; }
/// <summary> /// <summary>
/// Input affix to show behind input elements in the UI /// Input affix to show behind input elements in the UI
/// </summary> /// </summary>
public string? InputAffix { get; set; } public string? InputAffix { get; set; }
/// <summary> /// <summary>
/// The input drag step size, used in the UI /// The input drag step size, used in the UI
/// </summary> /// </summary>
public float InputStepSize { get; set; } public float InputStepSize { get; set; }
/// <summary> /// <summary>
/// Minimum input value, only enforced in the UI /// Minimum input value, only enforced in the UI
/// </summary> /// </summary>
public object? MinInputValue { get; set; } public object? MinInputValue { get; set; }
/// <summary> /// <summary>
/// Maximum input value, only enforced in the UI /// Maximum input value, only enforced in the UI
/// </summary> /// </summary>
public object? MaxInputValue { get; set; } public object? MaxInputValue { get; set; }
/// <summary> /// <summary>
/// Whether or not keyframes are always disabled /// Whether or not keyframes are always disabled
/// </summary> /// </summary>
public bool DisableKeyframes { get; set; } public bool DisableKeyframes { get; set; }
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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