Code style - Use file scoped namespaces

Code style - Ran code cleanup
This commit is contained in:
Robert committed 2022-08-21 11:36:15 +02:00
1 parent e68e16df4d
commit f6090dc296
691 files changed
+37301 -38119

No files matched your search

+82 -83
View File
@@ -1,92 +1,91 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Provides a default implementation for models that can have a broken state
/// </summary>
public abstract class BreakableModel : CorePropertyChanged, IBreakableModel
{
private string? _brokenState;
private Exception? _brokenStateException;
/// <summary>
/// Provides a default implementation for models that can have a broken state
/// Invokes the <see cref="BrokenStateChanged" /> event
/// </summary>
public abstract class BreakableModel : CorePropertyChanged, IBreakableModel
protected virtual void OnBrokenStateChanged()
{
private string? _brokenState;
private Exception? _brokenStateException;
/// <summary>
/// Invokes the <see cref="BrokenStateChanged" /> event
/// </summary>
protected virtual void OnBrokenStateChanged()
{
BrokenStateChanged?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc />
public abstract string BrokenDisplayName { get; }
/// <summary>
/// Gets or sets the broken state of this breakable model, if <see langword="null" /> this model is not broken.
/// <para>Note: If setting this manually you are also responsible for invoking <see cref="BrokenStateChanged" /></para>
/// </summary>
public string? BrokenState
{
get => _brokenState;
set => SetAndNotify(ref _brokenState, value);
}
/// <summary>
/// Gets or sets the exception that caused the broken state
/// <para>Note: If setting this manually you are also responsible for invoking <see cref="BrokenStateChanged" /></para>
/// </summary>
public Exception? BrokenStateException
{
get => _brokenStateException;
set => SetAndNotify(ref _brokenStateException, value);
}
/// <inheritdoc />
public bool TryOrBreak(Action action, string breakMessage)
{
try
{
action();
ClearBrokenState(breakMessage);
return true;
}
catch (Exception e)
{
SetBrokenState(breakMessage, e);
return false;
}
}
/// <inheritdoc />
public void SetBrokenState(string state, Exception? exception)
{
BrokenState = state ?? throw new ArgumentNullException(nameof(state));
BrokenStateException = exception;
OnBrokenStateChanged();
}
/// <inheritdoc />
public void ClearBrokenState(string state)
{
if (state == null) throw new ArgumentNullException(nameof(state));
if (BrokenState == null)
return;
if (BrokenState != state) return;
BrokenState = null;
BrokenStateException = null;
OnBrokenStateChanged();
}
/// <inheritdoc />
public virtual IEnumerable<IBreakableModel> GetBrokenHierarchy()
{
if (BrokenState != null)
yield return this;
}
/// <inheritdoc />
public event EventHandler? BrokenStateChanged;
BrokenStateChanged?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc />
public abstract string BrokenDisplayName { get; }
/// <summary>
/// Gets or sets the broken state of this breakable model, if <see langword="null" /> this model is not broken.
/// <para>Note: If setting this manually you are also responsible for invoking <see cref="BrokenStateChanged" /></para>
/// </summary>
public string? BrokenState
{
get => _brokenState;
set => SetAndNotify(ref _brokenState, value);
}
/// <summary>
/// Gets or sets the exception that caused the broken state
/// <para>Note: If setting this manually you are also responsible for invoking <see cref="BrokenStateChanged" /></para>
/// </summary>
public Exception? BrokenStateException
{
get => _brokenStateException;
set => SetAndNotify(ref _brokenStateException, value);
}
/// <inheritdoc />
public bool TryOrBreak(Action action, string breakMessage)
{
try
{
action();
ClearBrokenState(breakMessage);
return true;
}
catch (Exception e)
{
SetBrokenState(breakMessage, e);
return false;
}
}
/// <inheritdoc />
public void SetBrokenState(string state, Exception? exception)
{
BrokenState = state ?? throw new ArgumentNullException(nameof(state));
BrokenStateException = exception;
OnBrokenStateChanged();
}
/// <inheritdoc />
public void ClearBrokenState(string state)
{
if (state == null) throw new ArgumentNullException(nameof(state));
if (BrokenState == null)
return;
if (BrokenState != state) return;
BrokenState = null;
BrokenStateException = null;
OnBrokenStateChanged();
}
/// <inheritdoc />
public virtual IEnumerable<IBreakableModel> GetBrokenHierarchy()
{
if (BrokenState != null)
yield return this;
}
/// <inheritdoc />
public event EventHandler? BrokenStateChanged;
}
+45 -46
View File
@@ -1,59 +1,58 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a model that can have a broken state
/// </summary>
public interface IBreakableModel
{
/// <summary>
/// Represents a model that can have a broken state
/// Gets the display name of this breakable model
/// </summary>
public interface IBreakableModel
{
/// <summary>
/// Gets the display name of this breakable model
/// </summary>
string BrokenDisplayName { get; }
string BrokenDisplayName { get; }
/// <summary>
/// Gets or sets the broken state of this breakable model, if <see langword="null" /> this model is not broken.
/// </summary>
string? BrokenState { get; set; }
/// <summary>
/// Gets or sets the broken state of this breakable model, if <see langword="null" /> this model is not broken.
/// </summary>
string? BrokenState { get; set; }
/// <summary>
/// Gets or sets the exception that caused the broken state
/// </summary>
Exception? BrokenStateException { get; set; }
/// <summary>
/// Gets or sets the exception that caused the broken state
/// </summary>
Exception? BrokenStateException { get; set; }
/// <summary>
/// Try to execute the provided action. If the action succeeded the broken state is cleared if it matches
/// <see paramref="breakMessage" />, if the action throws an exception <see cref="BrokenState" /> and
/// <see cref="BrokenStateException" /> are set accordingly.
/// </summary>
/// <param name="action">The action to attempt to execute</param>
/// <param name="breakMessage">The message to clear on succeed or set on failure (exception)</param>
/// <returns><see langword="true" /> if the action succeeded; otherwise <see langword="false" />.</returns>
bool TryOrBreak(Action action, string breakMessage);
/// <summary>
/// Try to execute the provided action. If the action succeeded the broken state is cleared if it matches
/// <see paramref="breakMessage" />, if the action throws an exception <see cref="BrokenState" /> and
/// <see cref="BrokenStateException" /> are set accordingly.
/// </summary>
/// <param name="action">The action to attempt to execute</param>
/// <param name="breakMessage">The message to clear on succeed or set on failure (exception)</param>
/// <returns><see langword="true" /> if the action succeeded; otherwise <see langword="false" />.</returns>
bool TryOrBreak(Action action, string breakMessage);
/// <summary>
/// Sets the broken state to the provided state and optional exception.
/// </summary>
/// <param name="state">The state to set the broken state to</param>
/// <param name="exception">The exception that caused the broken state</param>
public void SetBrokenState(string state, Exception? exception);
/// <summary>
/// Sets the broken state to the provided state and optional exception.
/// </summary>
/// <param name="state">The state to set the broken state to</param>
/// <param name="exception">The exception that caused the broken state</param>
public void SetBrokenState(string state, Exception? exception);
/// <summary>
/// Clears the broken state and exception if <see cref="BrokenState" /> equals <see paramref="state"></see>.
/// </summary>
/// <param name="state"></param>
public void ClearBrokenState(string state);
/// <summary>
/// Returns a list containing all broken models, including self and any children
/// </summary>
IEnumerable<IBreakableModel> GetBrokenHierarchy();
/// <summary>
/// Clears the broken state and exception if <see cref="BrokenState" /> equals <see paramref="state"></see>.
/// </summary>
/// <param name="state"></param>
public void ClearBrokenState(string state);
/// <summary>
/// Occurs when the broken state of this model changes
/// </summary>
event EventHandler BrokenStateChanged;
}
/// <summary>
/// Returns a list containing all broken models, including self and any children
/// </summary>
IEnumerable<IBreakableModel> GetBrokenHierarchy();
/// <summary>
/// Occurs when the broken state of this model changes
/// </summary>
event EventHandler BrokenStateChanged;
}
+12 -13
View File
@@ -1,18 +1,17 @@
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a model that can be loaded and saved to persistent storage
/// </summary>
public interface IStorageModel
{
/// <summary>
/// Represents a model that can be loaded and saved to persistent storage
/// Loads the model from its associated entity
/// </summary>
public interface IStorageModel
{
/// <summary>
/// Loads the model from its associated entity
/// </summary>
void Load();
void Load();
/// <summary>
/// Saves the model to its associated entity
/// </summary>
void Save();
}
/// <summary>
/// Saves the model to its associated entity
/// </summary>
void Save();
}
+9 -10
View File
@@ -1,14 +1,13 @@
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a model that updates using a delta time
/// </summary>
public interface IUpdateModel
{
/// <summary>
/// Represents a model that updates using a delta time
/// Performs an update on the model
/// </summary>
public interface IUpdateModel
{
/// <summary>
/// Performs an update on the model
/// </summary>
/// <param name="timeline">The timeline to apply during update</param>
void Update(Timeline timeline);
}
/// <param name="timeline">The timeline to apply during update</param>
void Update(Timeline timeline);
}
@@ -2,98 +2,97 @@
using System.Linq;
using Artemis.Storage.Entities.Profile.AdaptionHints;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a hint that adapts layers to a certain category of devices
/// </summary>
public class CategoryAdaptionHint : CorePropertyChanged, IAdaptionHint
{
private int _amount;
private DeviceCategory _category;
private bool _limitAmount;
private int _skip;
/// <summary>
/// Represents a hint that adapts layers to a certain category of devices
/// Creates a new instance of the <see cref="CategoryAdaptionHint" /> class
/// </summary>
public class CategoryAdaptionHint : CorePropertyChanged, IAdaptionHint
public CategoryAdaptionHint()
{
private DeviceCategory _category;
private int _skip;
private bool _limitAmount;
private int _amount;
/// <summary>
/// Creates a new instance of the <see cref="CategoryAdaptionHint" /> class
/// </summary>
public CategoryAdaptionHint()
{
}
internal CategoryAdaptionHint(CategoryAdaptionHintEntity entity)
{
Category = (DeviceCategory) entity.Category;
Skip = entity.Skip;
LimitAmount = entity.LimitAmount;
Amount = entity.Amount;
}
/// <summary>
/// Gets or sets the category of devices LEDs will be applied to
/// </summary>
public DeviceCategory Category
{
get => _category;
set => SetAndNotify(ref _category, value);
}
/// <summary>
/// Gets or sets the amount of devices to skip
/// </summary>
public int Skip
{
get => _skip;
set => SetAndNotify(ref _skip, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether a limited amount of devices should be used
/// </summary>
public bool LimitAmount
{
get => _limitAmount;
set => SetAndNotify(ref _limitAmount, value);
}
/// <summary>
/// Gets or sets the amount of devices to limit to if <see cref="LimitAmount" /> is <see langword="true" />
/// </summary>
public int Amount
{
get => _amount;
set => SetAndNotify(ref _amount, value);
}
/// <inheritdoc />
public override string ToString()
{
return $"Category adaption - {nameof(Category)}: {Category}, {nameof(Skip)}: {Skip}, {nameof(LimitAmount)}: {LimitAmount}, {nameof(Amount)}: {Amount}";
}
#region Implementation of IAdaptionHint
/// <inheritdoc />
public void Apply(Layer layer, List<ArtemisDevice> devices)
{
IEnumerable<ArtemisDevice> matches = devices
.Where(d => d.Categories.Contains(Category))
.OrderBy(d => d.Rectangle.Top)
.ThenBy(d => d.Rectangle.Left)
.Skip(Skip);
if (LimitAmount)
matches = matches.Take(Amount);
foreach (ArtemisDevice artemisDevice in matches)
layer.AddLeds(artemisDevice.Leds);
}
/// <inheritdoc />
public IAdaptionHintEntity GetEntry()
{
return new CategoryAdaptionHintEntity {Amount = Amount, LimitAmount = LimitAmount, Category = (int) Category, Skip = Skip};
}
#endregion
}
internal CategoryAdaptionHint(CategoryAdaptionHintEntity entity)
{
Category = (DeviceCategory) entity.Category;
Skip = entity.Skip;
LimitAmount = entity.LimitAmount;
Amount = entity.Amount;
}
/// <summary>
/// Gets or sets the category of devices LEDs will be applied to
/// </summary>
public DeviceCategory Category
{
get => _category;
set => SetAndNotify(ref _category, value);
}
/// <summary>
/// Gets or sets the amount of devices to skip
/// </summary>
public int Skip
{
get => _skip;
set => SetAndNotify(ref _skip, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether a limited amount of devices should be used
/// </summary>
public bool LimitAmount
{
get => _limitAmount;
set => SetAndNotify(ref _limitAmount, value);
}
/// <summary>
/// Gets or sets the amount of devices to limit to if <see cref="LimitAmount" /> is <see langword="true" />
/// </summary>
public int Amount
{
get => _amount;
set => SetAndNotify(ref _amount, value);
}
/// <inheritdoc />
public override string ToString()
{
return $"Category adaption - {nameof(Category)}: {Category}, {nameof(Skip)}: {Skip}, {nameof(LimitAmount)}: {LimitAmount}, {nameof(Amount)}: {Amount}";
}
#region Implementation of IAdaptionHint
/// <inheritdoc />
public void Apply(Layer layer, List<ArtemisDevice> devices)
{
IEnumerable<ArtemisDevice> matches = devices
.Where(d => d.Categories.Contains(Category))
.OrderBy(d => d.Rectangle.Top)
.ThenBy(d => d.Rectangle.Left)
.Skip(Skip);
if (LimitAmount)
matches = matches.Take(Amount);
foreach (ArtemisDevice artemisDevice in matches)
layer.AddLeds(artemisDevice.Leds);
}
/// <inheritdoc />
public IAdaptionHintEntity GetEntry()
{
return new CategoryAdaptionHintEntity {Amount = Amount, LimitAmount = LimitAmount, Category = (int) Category, Skip = Skip};
}
#endregion
}
@@ -3,98 +3,97 @@ using System.Linq;
using Artemis.Storage.Entities.Profile.AdaptionHints;
using RGB.NET.Core;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a hint that adapts layers to a certain type of devices
/// </summary>
public class DeviceAdaptionHint : CorePropertyChanged, IAdaptionHint
{
private int _amount;
private RGBDeviceType _deviceType;
private bool _limitAmount;
private int _skip;
/// <summary>
/// Represents a hint that adapts layers to a certain type of devices
/// Creates a new instance of the <see cref="DeviceAdaptionHint" /> class
/// </summary>
public class DeviceAdaptionHint : CorePropertyChanged, IAdaptionHint
public DeviceAdaptionHint()
{
private RGBDeviceType _deviceType;
private int _skip;
private bool _limitAmount;
private int _amount;
/// <summary>
/// Creates a new instance of the <see cref="DeviceAdaptionHint" /> class
/// </summary>
public DeviceAdaptionHint()
{
}
internal DeviceAdaptionHint(DeviceAdaptionHintEntity entity)
{
DeviceType = (RGBDeviceType) entity.DeviceType;
Skip = entity.Skip;
LimitAmount = entity.LimitAmount;
Amount = entity.Amount;
}
/// <summary>
/// Gets or sets the type of devices LEDs will be applied to
/// </summary>
public RGBDeviceType DeviceType
{
get => _deviceType;
set => SetAndNotify(ref _deviceType, value);
}
/// <summary>
/// Gets or sets the amount of devices to skip
/// </summary>
public int Skip
{
get => _skip;
set => SetAndNotify(ref _skip, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether a limited amount of devices should be used
/// </summary>
public bool LimitAmount
{
get => _limitAmount;
set => SetAndNotify(ref _limitAmount, value);
}
/// <summary>
/// Gets or sets the amount of devices to limit to if <see cref="LimitAmount" /> is <see langword="true" />
/// </summary>
public int Amount
{
get => _amount;
set => SetAndNotify(ref _amount, value);
}
#region Implementation of IAdaptionHint
/// <inheritdoc />
public void Apply(Layer layer, List<ArtemisDevice> devices)
{
IEnumerable<ArtemisDevice> matches = devices
.Where(d => DeviceType == RGBDeviceType.All || d.DeviceType == DeviceType)
.OrderBy(d => d.Rectangle.Top)
.ThenBy(d => d.Rectangle.Left)
.Skip(Skip);
if (LimitAmount)
matches = matches.Take(Amount);
foreach (ArtemisDevice artemisDevice in matches)
layer.AddLeds(artemisDevice.Leds);
}
/// <inheritdoc />
public IAdaptionHintEntity GetEntry()
{
return new DeviceAdaptionHintEntity {Amount = Amount, LimitAmount = LimitAmount, DeviceType = (int) DeviceType, Skip = Skip};
}
/// <inheritdoc />
public override string ToString()
{
return $"Device adaption - {nameof(DeviceType)}: {DeviceType}, {nameof(Skip)}: {Skip}, {nameof(LimitAmount)}: {LimitAmount}, {nameof(Amount)}: {Amount}";
}
#endregion
}
internal DeviceAdaptionHint(DeviceAdaptionHintEntity entity)
{
DeviceType = (RGBDeviceType) entity.DeviceType;
Skip = entity.Skip;
LimitAmount = entity.LimitAmount;
Amount = entity.Amount;
}
/// <summary>
/// Gets or sets the type of devices LEDs will be applied to
/// </summary>
public RGBDeviceType DeviceType
{
get => _deviceType;
set => SetAndNotify(ref _deviceType, value);
}
/// <summary>
/// Gets or sets the amount of devices to skip
/// </summary>
public int Skip
{
get => _skip;
set => SetAndNotify(ref _skip, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether a limited amount of devices should be used
/// </summary>
public bool LimitAmount
{
get => _limitAmount;
set => SetAndNotify(ref _limitAmount, value);
}
/// <summary>
/// Gets or sets the amount of devices to limit to if <see cref="LimitAmount" /> is <see langword="true" />
/// </summary>
public int Amount
{
get => _amount;
set => SetAndNotify(ref _amount, value);
}
#region Implementation of IAdaptionHint
/// <inheritdoc />
public void Apply(Layer layer, List<ArtemisDevice> devices)
{
IEnumerable<ArtemisDevice> matches = devices
.Where(d => DeviceType == RGBDeviceType.All || d.DeviceType == DeviceType)
.OrderBy(d => d.Rectangle.Top)
.ThenBy(d => d.Rectangle.Left)
.Skip(Skip);
if (LimitAmount)
matches = matches.Take(Amount);
foreach (ArtemisDevice artemisDevice in matches)
layer.AddLeds(artemisDevice.Leds);
}
/// <inheritdoc />
public IAdaptionHintEntity GetEntry()
{
return new DeviceAdaptionHintEntity {Amount = Amount, LimitAmount = LimitAmount, DeviceType = (int) DeviceType, Skip = Skip};
}
/// <inheritdoc />
public override string ToString()
{
return $"Device adaption - {nameof(DeviceType)}: {DeviceType}, {nameof(Skip)}: {Skip}, {nameof(LimitAmount)}: {LimitAmount}, {nameof(Amount)}: {Amount}";
}
#endregion
}
@@ -1,23 +1,22 @@
using System.Collections.Generic;
using Artemis.Storage.Entities.Profile.AdaptionHints;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents an adaption hint that's used to adapt a layer to a set of devices
/// </summary>
public interface IAdaptionHint
{
/// <summary>
/// Represents an adaption hint that's used to adapt a layer to a set of devices
/// Applies the adaptive action to the provided layer
/// </summary>
public interface IAdaptionHint
{
/// <summary>
/// Applies the adaptive action to the provided layer
/// </summary>
/// <param name="layer">The layer to adapt</param>
/// <param name="devices">The devices to adapt the layer for</param>
void Apply(Layer layer, List<ArtemisDevice> devices);
/// <param name="layer">The layer to adapt</param>
/// <param name="devices">The devices to adapt the layer for</param>
void Apply(Layer layer, List<ArtemisDevice> devices);
/// <summary>
/// Returns an adaption hint entry for this adaption hint used for persistent storage
/// </summary>
IAdaptionHintEntity GetEntry();
}
/// <summary>
/// Returns an adaption hint entry for this adaption hint used for persistent storage
/// </summary>
IAdaptionHintEntity GetEntry();
}
@@ -4,89 +4,88 @@ using System.Linq;
using Artemis.Storage.Entities.Profile.AdaptionHints;
using RGB.NET.Core;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a hint that adapts layers to a certain region of keyboards
/// </summary>
public class KeyboardSectionAdaptionHint : CorePropertyChanged, IAdaptionHint
{
private static readonly Dictionary<KeyboardSection, List<LedId>> RegionLedIds = new()
{
{KeyboardSection.MacroKeys, Enum.GetValues<LedId>().Where(l => l >= LedId.Keyboard_Programmable1 && l <= LedId.Keyboard_Programmable32).ToList()},
{KeyboardSection.LedStrips, Enum.GetValues<LedId>().Where(l => l >= LedId.LedStripe1 && l <= LedId.LedStripe128).ToList()},
{KeyboardSection.Extra, Enum.GetValues<LedId>().Where(l => l >= LedId.Keyboard_Custom1 && l <= LedId.Keyboard_Custom64).ToList()}
};
private KeyboardSection _section;
/// <summary>
/// Creates a new instance of the <see cref="KeyboardSectionAdaptionHint" /> class
/// </summary>
public KeyboardSectionAdaptionHint()
{
}
internal KeyboardSectionAdaptionHint(KeyboardSectionAdaptionHintEntity entity)
{
Section = (KeyboardSection) entity.Section;
}
/// <summary>
/// Gets or sets the section this hint will apply LEDs to
/// </summary>
public KeyboardSection Section
{
get => _section;
set => SetAndNotify(ref _section, value);
}
#region Implementation of IAdaptionHint
/// <inheritdoc />
public void Apply(Layer layer, List<ArtemisDevice> devices)
{
// Only keyboards should have the LEDs we care about
foreach (ArtemisDevice keyboard in devices.Where(d => d.DeviceType == RGBDeviceType.Keyboard))
{
List<LedId> ledIds = RegionLedIds[Section];
layer.AddLeds(keyboard.Leds.Where(l => ledIds.Contains(l.RgbLed.Id)));
}
}
/// <inheritdoc />
public IAdaptionHintEntity GetEntry()
{
return new KeyboardSectionAdaptionHintEntity {Section = (int) Section};
}
/// <inheritdoc />
public override string ToString()
{
return $"Keyboard section adaption - {nameof(Section)}: {Section}";
}
#endregion
}
/// <summary>
/// Represents a section of LEDs on a keyboard
/// </summary>
public enum KeyboardSection
{
/// <summary>
/// Represents a hint that adapts layers to a certain region of keyboards
/// A region containing the macro keys of a keyboard
/// </summary>
public class KeyboardSectionAdaptionHint : CorePropertyChanged, IAdaptionHint
{
private static readonly Dictionary<KeyboardSection, List<LedId>> RegionLedIds = new()
{
{KeyboardSection.MacroKeys, Enum.GetValues<LedId>().Where(l => l >= LedId.Keyboard_Programmable1 && l <= LedId.Keyboard_Programmable32).ToList()},
{KeyboardSection.LedStrips, Enum.GetValues<LedId>().Where(l => l >= LedId.LedStripe1 && l <= LedId.LedStripe128).ToList()},
{KeyboardSection.Extra, Enum.GetValues<LedId>().Where(l => l >= LedId.Keyboard_Custom1 && l <= LedId.Keyboard_Custom64).ToList()}
};
private KeyboardSection _section;
/// <summary>
/// Creates a new instance of the <see cref="KeyboardSectionAdaptionHint" /> class
/// </summary>
public KeyboardSectionAdaptionHint()
{
}
internal KeyboardSectionAdaptionHint(KeyboardSectionAdaptionHintEntity entity)
{
Section = (KeyboardSection) entity.Section;
}
/// <summary>
/// Gets or sets the section this hint will apply LEDs to
/// </summary>
public KeyboardSection Section
{
get => _section;
set => SetAndNotify(ref _section, value);
}
#region Implementation of IAdaptionHint
/// <inheritdoc />
public void Apply(Layer layer, List<ArtemisDevice> devices)
{
// Only keyboards should have the LEDs we care about
foreach (ArtemisDevice keyboard in devices.Where(d => d.DeviceType == RGBDeviceType.Keyboard))
{
List<LedId> ledIds = RegionLedIds[Section];
layer.AddLeds(keyboard.Leds.Where(l => ledIds.Contains(l.RgbLed.Id)));
}
}
/// <inheritdoc />
public IAdaptionHintEntity GetEntry()
{
return new KeyboardSectionAdaptionHintEntity {Section = (int) Section};
}
/// <inheritdoc />
public override string ToString()
{
return $"Keyboard section adaption - {nameof(Section)}: {Section}";
}
#endregion
}
MacroKeys,
/// <summary>
/// Represents a section of LEDs on a keyboard
/// A region containing the LED strips of a keyboard
/// </summary>
public enum KeyboardSection
{
/// <summary>
/// A region containing the macro keys of a keyboard
/// </summary>
MacroKeys,
LedStrips,
/// <summary>
/// A region containing the LED strips of a keyboard
/// </summary>
LedStrips,
/// <summary>
/// A region containing extra non-standard LEDs of a keyboard
/// </summary>
Extra
}
/// <summary>
/// A region containing extra non-standard LEDs of a keyboard
/// </summary>
Extra
}
@@ -83,14 +83,10 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
{
List<SKColor> result = new();
if (timesToRepeat == 0)
{
result = this.Select(c => c.Color).ToList();
}
else
{
for (int i = 0; i <= timesToRepeat; i++)
result.AddRange(this.Select(c => c.Color));
}
if (seamless && !IsSeamless())
result.Add(result[0]);
@@ -413,8 +409,10 @@ public class ColorGradient : IList<ColorGradientStop>, IList, INotifyCollectionC
return false;
for (int i = 0; i < Count; i++)
{
if (!Equals(this[i], other[i]))
return false;
}
return true;
}
@@ -1,69 +1,68 @@
using System;
using SkiaSharp;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// A color with a position, usually contained in a <see cref="ColorGradient" />
/// </summary>
public class ColorGradientStop : CorePropertyChanged
{
private SKColor _color;
private float _position;
/// <summary>
/// A color with a position, usually contained in a <see cref="ColorGradient" />
/// Creates a new instance of the <see cref="ColorGradientStop" /> class
/// </summary>
public class ColorGradientStop : CorePropertyChanged
public ColorGradientStop(SKColor color, float position)
{
#region Equality members
/// <inheritdoc cref="object.Equals(object)" />
protected bool Equals(ColorGradientStop other)
{
return _color.Equals(other._color) && _position.Equals(other._position);
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj))
return false;
if (ReferenceEquals(this, obj))
return true;
if (obj.GetType() != GetType())
return false;
return Equals((ColorGradientStop) obj);
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(_color, _position);
}
#endregion
private SKColor _color;
private float _position;
/// <summary>
/// Creates a new instance of the <see cref="ColorGradientStop" /> class
/// </summary>
public ColorGradientStop(SKColor color, float position)
{
Color = color;
Position = position;
}
/// <summary>
/// Gets or sets the color of the stop
/// </summary>
public SKColor Color
{
get => _color;
set => SetAndNotify(ref _color, value);
}
/// <summary>
/// Gets or sets the position of the stop
/// </summary>
public float Position
{
get => _position;
set => SetAndNotify(ref _position, value);
}
Color = color;
Position = position;
}
/// <summary>
/// Gets or sets the color of the stop
/// </summary>
public SKColor Color
{
get => _color;
set => SetAndNotify(ref _color, value);
}
/// <summary>
/// Gets or sets the position of the stop
/// </summary>
public float Position
{
get => _position;
set => SetAndNotify(ref _position, value);
}
#region Equality members
/// <inheritdoc cref="object.Equals(object)" />
protected bool Equals(ColorGradientStop other)
{
return _color.Equals(other._color) && _position.Equals(other._position);
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj))
return false;
if (ReferenceEquals(this, obj))
return true;
if (obj.GetType() != GetType())
return false;
return Equals((ColorGradientStop) obj);
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(_color, _position);
}
#endregion
}
@@ -2,89 +2,84 @@
using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Conditions;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a condition that is always true.
/// </summary>
public class AlwaysOnCondition : ICondition
{
/// <summary>
/// Represents a condition that is always true.
/// Creates a new instance of the <see cref="AlwaysOnCondition" /> class.
/// </summary>
public class AlwaysOnCondition : ICondition
/// <param name="profileElement">The profile element this condition applies to.</param>
public AlwaysOnCondition(RenderProfileElement profileElement)
{
/// <summary>
/// Creates a new instance of the <see cref="AlwaysOnCondition" /> class.
/// </summary>
/// <param name="profileElement">The profile element this condition applies to.</param>
public AlwaysOnCondition(RenderProfileElement profileElement)
{
ProfileElement = profileElement;
Entity = new AlwaysOnConditionEntity();
}
/// <summary>
/// Creates a new instance of the <see cref="AlwaysOnCondition" /> class.
/// </summary>
/// <param name="alwaysOnConditionEntity">The entity used to store this condition.</param>
/// <param name="profileElement">The profile element this condition applies to.</param>
public AlwaysOnCondition(AlwaysOnConditionEntity alwaysOnConditionEntity, RenderProfileElement profileElement)
{
ProfileElement = profileElement;
Entity = alwaysOnConditionEntity;
}
#region Implementation of IDisposable
/// <inheritdoc />
public void Dispose()
{
}
#endregion
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
}
/// <inheritdoc />
public void Save()
{
}
#endregion
#region Implementation of ICondition
/// <inheritdoc />
public IConditionEntity Entity { get; }
/// <inheritdoc />
public RenderProfileElement ProfileElement { get; }
/// <inheritdoc />
public bool IsMet { get; private set; }
/// <inheritdoc />
public void Update()
{
if (ProfileElement.Parent is RenderProfileElement parent)
IsMet = parent.DisplayConditionMet;
else
IsMet = true;
}
/// <inheritdoc />
public void UpdateTimeline(double deltaTime)
{
ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), true);
}
/// <inheritdoc />
public void OverrideTimeline(TimeSpan position)
{
ProfileElement.Timeline.Override(position, position > ProfileElement.Timeline.Length);
}
#endregion
ProfileElement = profileElement;
Entity = new AlwaysOnConditionEntity();
}
/// <summary>
/// Creates a new instance of the <see cref="AlwaysOnCondition" /> class.
/// </summary>
/// <param name="alwaysOnConditionEntity">The entity used to store this condition.</param>
/// <param name="profileElement">The profile element this condition applies to.</param>
public AlwaysOnCondition(AlwaysOnConditionEntity alwaysOnConditionEntity, RenderProfileElement profileElement)
{
ProfileElement = profileElement;
Entity = alwaysOnConditionEntity;
}
/// <inheritdoc />
public void Dispose()
{
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
}
/// <inheritdoc />
public void Save()
{
}
#endregion
#region Implementation of ICondition
/// <inheritdoc />
public IConditionEntity Entity { get; }
/// <inheritdoc />
public RenderProfileElement ProfileElement { get; }
/// <inheritdoc />
public bool IsMet { get; private set; }
/// <inheritdoc />
public void Update()
{
if (ProfileElement.Parent is RenderProfileElement parent)
IsMet = parent.DisplayConditionMet;
else
IsMet = true;
}
/// <inheritdoc />
public void UpdateTimeline(double deltaTime)
{
ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), true);
}
/// <inheritdoc />
public void OverrideTimeline(TimeSpan position)
{
ProfileElement.Timeline.Override(position, position > ProfileElement.Timeline.Length);
}
#endregion
}
@@ -14,15 +14,15 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
{
private readonly string _displayName;
private readonly EventConditionEntity _entity;
private IEventConditionNode _startNode;
private DataModelPath? _eventPath;
private NodeScript<bool> _script;
private bool _wasMet;
private DateTime _lastProcessedTrigger;
private object? _lastProcessedValue;
private EventOverlapMode _overlapMode;
private EventTriggerMode _triggerMode;
private NodeScript<bool> _script;
private IEventConditionNode _startNode;
private EventToggleOffMode _toggleOffMode;
private EventTriggerMode _triggerMode;
private bool _wasMet;
/// <summary>
/// Creates a new instance of the <see cref="EventCondition" /> class
@@ -87,7 +87,8 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
}
/// <summary>
/// Gets or sets the mode for render elements when toggling off the event when using <see cref="EventTriggerMode.Toggle"/>.
/// Gets or sets the mode for render elements when toggling off the event when using
/// <see cref="EventTriggerMode.Toggle" />.
/// </summary>
public EventToggleOffMode ToggleOffMode
{
@@ -119,7 +120,9 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
_startNode = eventNode;
}
else
{
eventNode = node;
}
IDataModelEvent? dataModelEvent = EventPath?.GetValue() as IDataModelEvent;
eventNode.CreatePins(dataModelEvent);
@@ -136,13 +139,25 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
ReplaceStartNode(valueChangedNode);
}
else
{
valueChangedNode = node;
}
valueChangedNode.UpdateOutputPins(EventPath);
}
Script.Save();
}
/// <summary>
/// Gets the start node of the event script, if any
/// </summary>
/// <returns>The start node of the event script, if any.</returns>
public INode GetStartNode()
{
return _startNode;
}
private void ReplaceStartNode(IEventConditionNode newStartNode)
{
if (Script.Nodes.Contains(_startNode))
@@ -153,15 +168,6 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
Script.AddNode(_startNode);
}
/// <summary>
/// Gets the start node of the event script, if any
/// </summary>
/// <returns>The start node of the event script, if any.</returns>
public INode GetStartNode()
{
return _startNode;
}
private bool Evaluate()
{
if (EventPath == null)
@@ -271,7 +277,7 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
Script = _entity.Script != null
? new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", _entity.Script, ProfileElement.Profile)
: new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile);
: new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile);
UpdateEventNode();
}
@@ -356,7 +362,8 @@ public enum EventOverlapMode
}
/// <summary>
/// Represents a mode for render elements when toggling off the event when using <see cref="EventTriggerMode.Toggle"/>.
/// Represents a mode for render elements when toggling off the event when using <see cref="EventTriggerMode.Toggle" />
/// .
/// </summary>
public enum EventToggleOffMode
{
@@ -30,12 +30,12 @@ public interface ICondition : IDisposable, IStorageModel
void Update();
/// <summary>
/// Updates the timeline according to the provided <paramref name="deltaTime" /> as the display condition sees fit.
/// Updates the timeline according to the provided <paramref name="deltaTime" /> as the display condition sees fit.
/// </summary>
void UpdateTimeline(double deltaTime);
/// <summary>
/// Overrides the timeline to the provided <paramref name="position"/> as the display condition sees fit.
/// Overrides the timeline to the provided <paramref name="position" /> as the display condition sees fit.
/// </summary>
void OverrideTimeline(TimeSpan position);
}
@@ -2,89 +2,84 @@
using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Conditions;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a condition that plays once when its script evaluates to <see langword="true" />.
/// </summary>
public class PlayOnceCondition : ICondition
{
/// <summary>
/// Represents a condition that plays once when its script evaluates to <see langword="true"/>.
/// Creates a new instance of the <see cref="PlayOnceCondition" /> class.
/// </summary>
public class PlayOnceCondition : ICondition
/// <param name="profileElement">The profile element this condition applies to.</param>
public PlayOnceCondition(RenderProfileElement profileElement)
{
/// <summary>
/// Creates a new instance of the <see cref="PlayOnceCondition" /> class.
/// </summary>
/// <param name="profileElement">The profile element this condition applies to.</param>
public PlayOnceCondition(RenderProfileElement profileElement)
{
ProfileElement = profileElement;
Entity = new PlayOnceConditionEntity();
}
/// <summary>
/// Creates a new instance of the <see cref="PlayOnceCondition" /> class.
/// </summary>
/// <param name="entity">The entity used to store this condition.</param>
/// <param name="profileElement">The profile element this condition applies to.</param>
public PlayOnceCondition(PlayOnceConditionEntity entity, RenderProfileElement profileElement)
{
ProfileElement = profileElement;
Entity = entity;
}
#region Implementation of IDisposable
/// <inheritdoc />
public void Dispose()
{
}
#endregion
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
}
/// <inheritdoc />
public void Save()
{
}
#endregion
#region Implementation of ICondition
/// <inheritdoc />
public IConditionEntity Entity { get; }
/// <inheritdoc />
public RenderProfileElement ProfileElement { get; }
/// <inheritdoc />
public bool IsMet { get; private set; }
/// <inheritdoc />
public void Update()
{
if (ProfileElement.Parent is RenderProfileElement parent)
IsMet = parent.DisplayConditionMet;
else
IsMet = true;
}
/// <inheritdoc />
public void UpdateTimeline(double deltaTime)
{
ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), false);
}
/// <inheritdoc />
public void OverrideTimeline(TimeSpan position)
{
ProfileElement.Timeline.Override(position, false);
}
#endregion
ProfileElement = profileElement;
Entity = new PlayOnceConditionEntity();
}
/// <summary>
/// Creates a new instance of the <see cref="PlayOnceCondition" /> class.
/// </summary>
/// <param name="entity">The entity used to store this condition.</param>
/// <param name="profileElement">The profile element this condition applies to.</param>
public PlayOnceCondition(PlayOnceConditionEntity entity, RenderProfileElement profileElement)
{
ProfileElement = profileElement;
Entity = entity;
}
/// <inheritdoc />
public void Dispose()
{
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
}
/// <inheritdoc />
public void Save()
{
}
#endregion
#region Implementation of ICondition
/// <inheritdoc />
public IConditionEntity Entity { get; }
/// <inheritdoc />
public RenderProfileElement ProfileElement { get; }
/// <inheritdoc />
public bool IsMet { get; private set; }
/// <inheritdoc />
public void Update()
{
if (ProfileElement.Parent is RenderProfileElement parent)
IsMet = parent.DisplayConditionMet;
else
IsMet = true;
}
/// <inheritdoc />
public void UpdateTimeline(double deltaTime)
{
ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), false);
}
/// <inheritdoc />
public void OverrideTimeline(TimeSpan position)
{
ProfileElement.Timeline.Override(position, false);
}
#endregion
}
@@ -3,191 +3,192 @@ using System.Linq;
using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Conditions;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a condition that is based on a data model value
/// </summary>
public class StaticCondition : CorePropertyChanged, INodeScriptCondition
{
private readonly string _displayName;
private readonly StaticConditionEntity _entity;
private StaticPlayMode _playMode;
private StaticStopMode _stopMode;
private bool _wasMet;
/// <summary>
/// Creates a new instance of the <see cref="StaticCondition" /> class
/// </summary>
public StaticCondition(RenderProfileElement profileElement)
{
_entity = new StaticConditionEntity();
_displayName = profileElement.GetType().Name;
ProfileElement = profileElement;
Script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", profileElement.Profile);
}
internal StaticCondition(StaticConditionEntity entity, RenderProfileElement profileElement)
{
_entity = entity;
_displayName = profileElement.GetType().Name;
ProfileElement = profileElement;
Script = null!;
Load();
}
/// <summary>
/// Gets the script that drives the static condition
/// </summary>
public NodeScript<bool> Script { get; private set; }
/// <summary>
/// Gets or sets the mode in which the render element starts its timeline when display conditions are met
/// </summary>
public StaticPlayMode PlayMode
{
get => _playMode;
set => SetAndNotify(ref _playMode, value);
}
/// <summary>
/// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met
/// </summary>
public StaticStopMode StopMode
{
get => _stopMode;
set => SetAndNotify(ref _stopMode, value);
}
/// <inheritdoc />
public IConditionEntity Entity => _entity;
/// <inheritdoc />
public RenderProfileElement ProfileElement { get; }
/// <inheritdoc />
public bool IsMet { get; private set; }
/// <inheritdoc />
public void Update()
{
_wasMet = IsMet;
// No need to run the script if the parent isn't met anyway
bool parentConditionMet = ProfileElement.Parent is not RenderProfileElement renderProfileElement || renderProfileElement.DisplayConditionMet;
if (!parentConditionMet)
{
IsMet = false;
return;
}
if (!Script.ExitNodeConnected)
{
IsMet = true;
}
else
{
Script.Run();
IsMet = Script.Result;
}
}
/// <inheritdoc />
public void UpdateTimeline(double deltaTime)
{
if (IsMet && !_wasMet && ProfileElement.Timeline.IsFinished)
ProfileElement.Timeline.JumpToStart();
else if (!IsMet && _wasMet && StopMode == StaticStopMode.SkipToEnd)
ProfileElement.Timeline.JumpToEndSegment();
ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), PlayMode == StaticPlayMode.Repeat && IsMet);
}
/// <inheritdoc />
public void OverrideTimeline(TimeSpan position)
{
ProfileElement.Timeline.Override(position, PlayMode == StaticPlayMode.Repeat && position > ProfileElement.Timeline.Length);
}
/// <inheritdoc />
public void Dispose()
{
Script.Dispose();
}
#region Storage
/// <inheritdoc />
public void Load()
{
PlayMode = (StaticPlayMode) _entity.PlayMode;
StopMode = (StaticStopMode) _entity.StopMode;
Script = _entity.Script != null
? new NodeScript<bool>($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", _entity.Script, ProfileElement.Profile)
: new NodeScript<bool>($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", ProfileElement.Profile);
}
/// <inheritdoc />
public void Save()
{
_entity.PlayMode = (int) PlayMode;
_entity.StopMode = (int) StopMode;
// If the exit node isn't connected and there is only the exit node, don't save the script
if (!Script.ExitNodeConnected && Script.Nodes.Count() == 1)
{
_entity.Script = null;
}
else
{
Script.Save();
_entity.Script = Script.Entity;
}
}
/// <inheritdoc />
public INodeScript? NodeScript => Script;
/// <inheritdoc />
public void LoadNodeScript()
{
Script.Load();
}
#endregion
}
/// <summary>
/// Represents a mode for render elements to start their timeline when display conditions are met
/// </summary>
public enum StaticPlayMode
{
/// <summary>
/// Represents a condition that is based on a data model value
/// Continue repeating the main segment of the timeline while the condition is met
/// </summary>
public class StaticCondition : CorePropertyChanged, INodeScriptCondition
{
private readonly string _displayName;
private readonly StaticConditionEntity _entity;
private StaticPlayMode _playMode;
private StaticStopMode _stopMode;
private bool _wasMet;
/// <summary>
/// Creates a new instance of the <see cref="StaticCondition" /> class
/// </summary>
public StaticCondition(RenderProfileElement profileElement)
{
_entity = new StaticConditionEntity();
_displayName = profileElement.GetType().Name;
ProfileElement = profileElement;
Script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", profileElement.Profile);
}
internal StaticCondition(StaticConditionEntity entity, RenderProfileElement profileElement)
{
_entity = entity;
_displayName = profileElement.GetType().Name;
ProfileElement = profileElement;
Script = null!;
Load();
}
/// <summary>
/// Gets the script that drives the static condition
/// </summary>
public NodeScript<bool> Script { get; private set; }
/// <inheritdoc />
public IConditionEntity Entity => _entity;
/// <inheritdoc />
public RenderProfileElement ProfileElement { get; }
/// <inheritdoc />
public bool IsMet { get; private set; }
/// <summary>
/// Gets or sets the mode in which the render element starts its timeline when display conditions are met
/// </summary>
public StaticPlayMode PlayMode
{
get => _playMode;
set => SetAndNotify(ref _playMode, value);
}
/// <summary>
/// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met
/// </summary>
public StaticStopMode StopMode
{
get => _stopMode;
set => SetAndNotify(ref _stopMode, value);
}
/// <inheritdoc />
public void Update()
{
_wasMet = IsMet;
// No need to run the script if the parent isn't met anyway
bool parentConditionMet = ProfileElement.Parent is not RenderProfileElement renderProfileElement || renderProfileElement.DisplayConditionMet;
if (!parentConditionMet)
{
IsMet = false;
return;
}
if (!Script.ExitNodeConnected)
IsMet = true;
else
{
Script.Run();
IsMet = Script.Result;
}
}
/// <inheritdoc />
public void UpdateTimeline(double deltaTime)
{
if (IsMet && !_wasMet && ProfileElement.Timeline.IsFinished)
ProfileElement.Timeline.JumpToStart();
else if (!IsMet && _wasMet && StopMode == StaticStopMode.SkipToEnd)
ProfileElement.Timeline.JumpToEndSegment();
ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), PlayMode == StaticPlayMode.Repeat && IsMet);
}
/// <inheritdoc />
public void OverrideTimeline(TimeSpan position)
{
ProfileElement.Timeline.Override(position, PlayMode == StaticPlayMode.Repeat && position > ProfileElement.Timeline.Length);
}
/// <inheritdoc />
public void Dispose()
{
Script.Dispose();
}
#region Storage
/// <inheritdoc />
public void Load()
{
PlayMode = (StaticPlayMode) _entity.PlayMode;
StopMode = (StaticStopMode) _entity.StopMode;
Script = _entity.Script != null
? new NodeScript<bool>($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", _entity.Script, ProfileElement.Profile)
: new NodeScript<bool>($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", ProfileElement.Profile);
}
/// <inheritdoc />
public void Save()
{
_entity.PlayMode = (int) PlayMode;
_entity.StopMode = (int) StopMode;
// If the exit node isn't connected and there is only the exit node, don't save the script
if (!Script.ExitNodeConnected && Script.Nodes.Count() == 1)
{
_entity.Script = null;
}
else
{
Script.Save();
_entity.Script = Script.Entity;
}
}
/// <inheritdoc />
public INodeScript? NodeScript => Script;
/// <inheritdoc />
public void LoadNodeScript()
{
Script.Load();
}
#endregion
}
Repeat,
/// <summary>
/// Represents a mode for render elements to start their timeline when display conditions are met
/// Only play the timeline once when the condition is met
/// </summary>
public enum StaticPlayMode
{
/// <summary>
/// Continue repeating the main segment of the timeline while the condition is met
/// </summary>
Repeat,
Once
}
/// <summary>
/// Only play the timeline once when the condition is met
/// </summary>
Once
}
/// <summary>
/// Represents a mode for render elements to stop their timeline when display conditions are no longer met
/// </summary>
public enum StaticStopMode
{
/// <summary>
/// When conditions are no longer met, finish the the current run of the main timeline
/// </summary>
Finish,
/// <summary>
/// Represents a mode for render elements to stop their timeline when display conditions are no longer met
/// When conditions are no longer met, skip to the end segment of the timeline
/// </summary>
public enum StaticStopMode
{
/// <summary>
/// When conditions are no longer met, finish the the current run of the main timeline
/// </summary>
Finish,
/// <summary>
/// When conditions are no longer met, skip to the end segment of the timeline
/// </summary>
SkipToEnd
}
SkipToEnd
}
@@ -4,242 +4,243 @@ using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Storage.Entities.Profile.DataBindings;
namespace Artemis.Core
namespace Artemis.Core;
/// <inheritdoc />
public class DataBinding<TLayerProperty> : IDataBinding
{
/// <inheritdoc />
public class DataBinding<TLayerProperty> : IDataBinding
private readonly List<IDataBindingProperty> _properties = new();
private bool _disposed;
private bool _isEnabled;
private DataBindingNodeScript<TLayerProperty> _script;
internal DataBinding(LayerProperty<TLayerProperty> layerProperty)
{
private readonly List<IDataBindingProperty> _properties = new();
private bool _disposed;
private bool _isEnabled;
private DataBindingNodeScript<TLayerProperty> _script;
LayerProperty = layerProperty;
internal DataBinding(LayerProperty<TLayerProperty> layerProperty)
{
LayerProperty = layerProperty;
Entity = new DataBindingEntity();
_script = new DataBindingNodeScript<TLayerProperty>(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile);
Entity = new DataBindingEntity();
_script = new DataBindingNodeScript<TLayerProperty>(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile);
Save();
}
internal DataBinding(LayerProperty<TLayerProperty> layerProperty, DataBindingEntity entity)
{
LayerProperty = layerProperty;
Entity = entity;
_script = new DataBindingNodeScript<TLayerProperty>(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile);
// Load will add children so be initialized before that
Load();
}
/// <summary>
/// Gets the layer property this data binding targets
/// </summary>
public LayerProperty<TLayerProperty> LayerProperty { get; }
/// <inheritdoc />
public INodeScript Script => _script;
/// <summary>
/// Gets the data binding entity this data binding uses for persistent storage
/// </summary>
public DataBindingEntity Entity { get; }
/// <summary>
/// Updates the pending values of this data binding
/// </summary>
public void Update()
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
if (!IsEnabled)
return;
// TODO: Update the 'base value' node
Script.Run();
}
/// <summary>
/// Registers a data binding property so that is available to the data binding system
/// </summary>
/// <typeparam name="TProperty">The type of the layer property</typeparam>
/// <param name="getter">The function to call to get the value of the property</param>
/// <param name="setter">The action to call to set the value of the property</param>
/// <param name="displayName">The display name of the data binding property</param>
public DataBindingProperty<TProperty> RegisterDataBindingProperty<TProperty>(Func<TProperty> getter, Action<TProperty?> setter, string displayName)
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
if (Properties.Any(d => d.DisplayName == displayName))
throw new ArtemisCoreException($"A data binding property named '{displayName}' is already registered.");
DataBindingProperty<TProperty> property = new(getter, setter, displayName);
_properties.Add(property);
OnDataBindingPropertyRegistered();
return property;
}
/// <summary>
/// Removes all data binding properties so they are no longer available to the data binding system
/// </summary>
public void ClearDataBindingProperties()
{
if (_disposed)
throw new ObjectDisposedException("LayerProperty");
_properties.Clear();
OnDataBindingPropertiesCleared();
}
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_disposed = true;
_isEnabled = false;
Script.Dispose();
}
}
/// <summary>
/// Invokes the <see cref="DataBindingPropertyRegistered" /> event
/// </summary>
protected virtual void OnDataBindingPropertyRegistered()
{
DataBindingPropertyRegistered?.Invoke(this, new DataBindingEventArgs(this));
}
/// <summary>
/// Invokes the <see cref="DataBindingPropertiesCleared" /> event
/// </summary>
protected virtual void OnDataBindingPropertiesCleared()
{
DataBindingPropertiesCleared?.Invoke(this, new DataBindingEventArgs(this));
}
/// <summary>
/// Invokes the <see cref="DataBindingEnabled" /> event
/// </summary>
protected virtual void OnDataBindingEnabled(DataBindingEventArgs e)
{
DataBindingEnabled?.Invoke(this, e);
}
/// <summary>
/// Invokes the <see cref="DataBindingDisabled" /> event
/// </summary>
protected virtual void OnDataBindingDisabled(DataBindingEventArgs e)
{
DataBindingDisabled?.Invoke(this, e);
}
private string GetScriptName()
{
return LayerProperty.PropertyDescription.Name ?? LayerProperty.Path;
}
/// <inheritdoc />
public ILayerProperty BaseLayerProperty => LayerProperty;
/// <inheritdoc />
public bool IsEnabled
{
get => _isEnabled;
set
{
_isEnabled = value;
if (_isEnabled)
OnDataBindingEnabled(new DataBindingEventArgs(this));
else
OnDataBindingDisabled(new DataBindingEventArgs(this));
}
}
/// <inheritdoc />
public ReadOnlyCollection<IDataBindingProperty> Properties => _properties.AsReadOnly();
/// <inheritdoc />
public void Apply()
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
if (!IsEnabled)
return;
_script.DataBindingExitNode.ApplyToDataBinding();
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <inheritdoc />
public event EventHandler<DataBindingEventArgs>? DataBindingPropertyRegistered;
/// <inheritdoc />
public event EventHandler<DataBindingEventArgs>? DataBindingPropertiesCleared;
/// <inheritdoc />
public event EventHandler<DataBindingEventArgs>? DataBindingEnabled;
/// <inheritdoc />
public event EventHandler<DataBindingEventArgs>? DataBindingDisabled;
#region Storage
/// <inheritdoc />
public void Load()
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
IsEnabled = Entity.IsEnabled;
}
/// <inheritdoc />
public void LoadNodeScript()
{
_script.Dispose();
_script = Entity.NodeScript != null
? new DataBindingNodeScript<TLayerProperty>(GetScriptName(), "The value to put into the data binding", this, Entity.NodeScript, LayerProperty.ProfileElement.Profile)
: new DataBindingNodeScript<TLayerProperty>(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile);
}
/// <inheritdoc />
public void Save()
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
Entity.IsEnabled = IsEnabled;
if (_script.ExitNodeConnected || _script.Nodes.Count() > 1)
{
_script.Save();
Entity.NodeScript = _script.Entity;
}
else
Entity.NodeScript = null;
}
#endregion
Save();
}
internal DataBinding(LayerProperty<TLayerProperty> layerProperty, DataBindingEntity entity)
{
LayerProperty = layerProperty;
Entity = entity;
_script = new DataBindingNodeScript<TLayerProperty>(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile);
// Load will add children so be initialized before that
Load();
}
/// <summary>
/// Gets the layer property this data binding targets
/// </summary>
public LayerProperty<TLayerProperty> LayerProperty { get; }
/// <summary>
/// Gets the data binding entity this data binding uses for persistent storage
/// </summary>
public DataBindingEntity Entity { get; }
/// <summary>
/// Updates the pending values of this data binding
/// </summary>
public void Update()
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
if (!IsEnabled)
return;
// TODO: Update the 'base value' node
Script.Run();
}
/// <summary>
/// Registers a data binding property so that is available to the data binding system
/// </summary>
/// <typeparam name="TProperty">The type of the layer property</typeparam>
/// <param name="getter">The function to call to get the value of the property</param>
/// <param name="setter">The action to call to set the value of the property</param>
/// <param name="displayName">The display name of the data binding property</param>
public DataBindingProperty<TProperty> RegisterDataBindingProperty<TProperty>(Func<TProperty> getter, Action<TProperty?> setter, string displayName)
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
if (Properties.Any(d => d.DisplayName == displayName))
throw new ArtemisCoreException($"A data binding property named '{displayName}' is already registered.");
DataBindingProperty<TProperty> property = new(getter, setter, displayName);
_properties.Add(property);
OnDataBindingPropertyRegistered();
return property;
}
/// <summary>
/// Removes all data binding properties so they are no longer available to the data binding system
/// </summary>
public void ClearDataBindingProperties()
{
if (_disposed)
throw new ObjectDisposedException("LayerProperty");
_properties.Clear();
OnDataBindingPropertiesCleared();
}
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_disposed = true;
_isEnabled = false;
Script.Dispose();
}
}
/// <summary>
/// Invokes the <see cref="DataBindingPropertyRegistered" /> event
/// </summary>
protected virtual void OnDataBindingPropertyRegistered()
{
DataBindingPropertyRegistered?.Invoke(this, new DataBindingEventArgs(this));
}
/// <summary>
/// Invokes the <see cref="DataBindingPropertiesCleared" /> event
/// </summary>
protected virtual void OnDataBindingPropertiesCleared()
{
DataBindingPropertiesCleared?.Invoke(this, new DataBindingEventArgs(this));
}
/// <summary>
/// Invokes the <see cref="DataBindingEnabled" /> event
/// </summary>
protected virtual void OnDataBindingEnabled(DataBindingEventArgs e)
{
DataBindingEnabled?.Invoke(this, e);
}
/// <summary>
/// Invokes the <see cref="DataBindingDisabled" /> event
/// </summary>
protected virtual void OnDataBindingDisabled(DataBindingEventArgs e)
{
DataBindingDisabled?.Invoke(this, e);
}
private string GetScriptName()
{
return LayerProperty.PropertyDescription.Name ?? LayerProperty.Path;
}
/// <inheritdoc />
public INodeScript Script => _script;
/// <inheritdoc />
public ILayerProperty BaseLayerProperty => LayerProperty;
/// <inheritdoc />
public bool IsEnabled
{
get => _isEnabled;
set
{
_isEnabled = value;
if (_isEnabled)
OnDataBindingEnabled(new DataBindingEventArgs(this));
else
OnDataBindingDisabled(new DataBindingEventArgs(this));
}
}
/// <inheritdoc />
public ReadOnlyCollection<IDataBindingProperty> Properties => _properties.AsReadOnly();
/// <inheritdoc />
public void Apply()
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
if (!IsEnabled)
return;
_script.DataBindingExitNode.ApplyToDataBinding();
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <inheritdoc />
public event EventHandler<DataBindingEventArgs>? DataBindingPropertyRegistered;
/// <inheritdoc />
public event EventHandler<DataBindingEventArgs>? DataBindingPropertiesCleared;
/// <inheritdoc />
public event EventHandler<DataBindingEventArgs>? DataBindingEnabled;
/// <inheritdoc />
public event EventHandler<DataBindingEventArgs>? DataBindingDisabled;
#region Storage
/// <inheritdoc />
public void Load()
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
IsEnabled = Entity.IsEnabled;
}
/// <inheritdoc />
public void LoadNodeScript()
{
_script.Dispose();
_script = Entity.NodeScript != null
? new DataBindingNodeScript<TLayerProperty>(GetScriptName(), "The value to put into the data binding", this, Entity.NodeScript, LayerProperty.ProfileElement.Profile)
: new DataBindingNodeScript<TLayerProperty>(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile);
}
/// <inheritdoc />
public void Save()
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
Entity.IsEnabled = IsEnabled;
if (_script.ExitNodeConnected || _script.Nodes.Count() > 1)
{
_script.Save();
Entity.NodeScript = _script.Entity;
}
else
{
Entity.NodeScript = null;
}
}
#endregion
}
@@ -1,63 +1,62 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <inheritdoc />
public class DataBindingProperty<TProperty> : IDataBindingProperty
{
/// <inheritdoc />
public class DataBindingProperty<TProperty> : IDataBindingProperty
internal DataBindingProperty(Func<TProperty> getter, Action<TProperty?> setter, string displayName)
{
internal DataBindingProperty(Func<TProperty> getter, Action<TProperty?> setter, string displayName)
Getter = getter ?? throw new ArgumentNullException(nameof(getter));
Setter = setter ?? throw new ArgumentNullException(nameof(setter));
DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName));
}
/// <summary>
/// Gets the function to call to get the value of the property
/// </summary>
public Func<TProperty> Getter { get; }
/// <summary>
/// Gets the action to call to set the value of the property
/// </summary>
public Action<TProperty?> Setter { get; }
/// <inheritdoc />
public string DisplayName { get; }
/// <inheritdoc />
public Type ValueType => typeof(TProperty);
/// <inheritdoc />
public object? GetValue()
{
return Getter();
}
/// <inheritdoc />
public void SetValue(object? value)
{
// Numeric has a bunch of conversion, this seems the cheapest way to use them :)
switch (value)
{
Getter = getter ?? throw new ArgumentNullException(nameof(getter));
Setter = setter ?? throw new ArgumentNullException(nameof(setter));
DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName));
}
/// <summary>
/// Gets the function to call to get the value of the property
/// </summary>
public Func<TProperty> Getter { get; }
/// <summary>
/// Gets the action to call to set the value of the property
/// </summary>
public Action<TProperty?> Setter { get; }
/// <inheritdoc />
public string DisplayName { get; }
/// <inheritdoc />
public Type ValueType => typeof(TProperty);
/// <inheritdoc />
public object? GetValue()
{
return Getter();
}
/// <inheritdoc />
public void SetValue(object? value)
{
// Numeric has a bunch of conversion, this seems the cheapest way to use them :)
switch (value)
{
case TProperty match:
Setter(match);
break;
case Numeric numeric when Setter is Action<float> floatSetter:
floatSetter(numeric);
break;
case Numeric numeric when Setter is Action<int> intSetter:
intSetter(numeric);
break;
case Numeric numeric when Setter is Action<double> doubleSetter:
doubleSetter(numeric);
break;
case Numeric numeric when Setter is Action<byte> byteSetter:
byteSetter(numeric);
break;
default:
throw new ArgumentException("Value must match the type of the data binding registration", nameof(value));
}
case TProperty match:
Setter(match);
break;
case Numeric numeric when Setter is Action<float> floatSetter:
floatSetter(numeric);
break;
case Numeric numeric when Setter is Action<int> intSetter:
intSetter(numeric);
break;
case Numeric numeric when Setter is Action<double> doubleSetter:
doubleSetter(numeric);
break;
case Numeric numeric when Setter is Action<byte> byteSetter:
byteSetter(numeric);
break;
default:
throw new ArgumentException("Value must match the type of the data binding registration", nameof(value));
}
}
}
@@ -2,62 +2,61 @@
using System.Collections.ObjectModel;
using Artemis.Core.Modules;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a data binding that binds a certain <see cref="LayerProperty{T}" /> to a value inside a
/// <see cref="DataModel" />
/// </summary>
public interface IDataBinding : IStorageModel, IDisposable
{
/// <summary>
/// Represents a data binding that binds a certain <see cref="LayerProperty{T}" /> to a value inside a
/// <see cref="DataModel" />
/// Gets the layer property the data binding is applied to
/// </summary>
public interface IDataBinding : IStorageModel, IDisposable
{
/// <summary>
/// Gets the layer property the data binding is applied to
/// </summary>
ILayerProperty BaseLayerProperty { get; }
ILayerProperty BaseLayerProperty { get; }
/// <summary>
/// Gets the script used to populate the data binding
/// </summary>
INodeScript Script { get; }
/// <summary>
/// Gets the script used to populate the data binding
/// </summary>
INodeScript Script { get; }
/// <summary>
/// Gets a list of sub-properties this data binding applies to
/// </summary>
ReadOnlyCollection<IDataBindingProperty> Properties { get; }
/// <summary>
/// Gets a list of sub-properties this data binding applies to
/// </summary>
ReadOnlyCollection<IDataBindingProperty> Properties { get; }
/// <summary>
/// Gets a boolean indicating whether the data binding is enabled or not
/// </summary>
bool IsEnabled { get; set; }
/// <summary>
/// Gets a boolean indicating whether the data binding is enabled or not
/// </summary>
bool IsEnabled { get; set; }
/// <summary>
/// Applies the pending value of the data binding to the property
/// </summary>
void Apply();
/// <summary>
/// Applies the pending value of the data binding to the property
/// </summary>
void Apply();
/// <summary>
/// If the data binding is enabled, loads the node script for that data binding
/// </summary>
void LoadNodeScript();
/// <summary>
/// If the data binding is enabled, loads the node script for that data binding
/// </summary>
void LoadNodeScript();
/// <summary>
/// Occurs when a data binding property has been added
/// </summary>
public event EventHandler<DataBindingEventArgs>? DataBindingPropertyRegistered;
/// <summary>
/// Occurs when a data binding property has been added
/// </summary>
public event EventHandler<DataBindingEventArgs>? DataBindingPropertyRegistered;
/// <summary>
/// Occurs when all data binding properties have been removed
/// </summary>
public event EventHandler<DataBindingEventArgs>? DataBindingPropertiesCleared;
/// <summary>
/// Occurs when all data binding properties have been removed
/// </summary>
public event EventHandler<DataBindingEventArgs>? DataBindingPropertiesCleared;
/// <summary>
/// Occurs when a data binding has been enabled
/// </summary>
public event EventHandler<DataBindingEventArgs>? DataBindingEnabled;
/// <summary>
/// Occurs when a data binding has been enabled
/// </summary>
public event EventHandler<DataBindingEventArgs>? DataBindingEnabled;
/// <summary>
/// Occurs when a data binding has been disabled
/// </summary>
public event EventHandler<DataBindingEventArgs>? DataBindingDisabled;
}
/// <summary>
/// Occurs when a data binding has been disabled
/// </summary>
public event EventHandler<DataBindingEventArgs>? DataBindingDisabled;
}
@@ -1,32 +1,31 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a data binding registration
/// </summary>
public interface IDataBindingProperty
{
/// <summary>
/// Represents a data binding registration
/// Gets or sets the display name of the data binding registration
/// </summary>
public interface IDataBindingProperty
{
/// <summary>
/// Gets or sets the display name of the data binding registration
/// </summary>
string DisplayName { get; }
string DisplayName { get; }
/// <summary>
/// Gets the type of the value this data binding registration points to
/// </summary>
Type ValueType { get; }
/// <summary>
/// Gets the type of the value this data binding registration points to
/// </summary>
Type ValueType { get; }
/// <summary>
/// Gets the value of the property this registration points to
/// </summary>
/// <returns>A value matching the type of <see cref="ValueType" /></returns>
object? GetValue();
/// <summary>
/// Gets the value of the property this registration points to
/// </summary>
/// <returns>A value matching the type of <see cref="ValueType" /></returns>
object? GetValue();
/// <summary>
/// Sets the value of the property this registration points to
/// </summary>
/// <param name="value">A value matching the type of <see cref="ValueType" /></param>
void SetValue(object? value);
}
/// <summary>
/// Sets the value of the property this registration points to
/// </summary>
/// <param name="value">A value matching the type of <see cref="ValueType" /></param>
void SetValue(object? value);
}
@@ -3,249 +3,244 @@ using System.Collections.Generic;
using System.Linq;
using Artemis.Core.Modules;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a data model event with event arguments of type <typeparamref name="T" />
/// </summary>
public class DataModelEvent<T> : IDataModelEvent where T : DataModelEventArgs
{
private bool _trackHistory;
/// <summary>
/// Represents a data model event with event arguments of type <typeparamref name="T" />
/// Creates a new instance of the <see cref="DataModelEvent{T}" /> class with history tracking disabled
/// </summary>
public class DataModelEvent<T> : IDataModelEvent where T : DataModelEventArgs
public DataModelEvent()
{
private bool _trackHistory;
/// <summary>
/// Creates a new instance of the <see cref="DataModelEvent{T}" /> class with history tracking disabled
/// </summary>
public DataModelEvent()
{
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelEvent{T}" />
/// </summary>
/// <param name="trackHistory">A boolean indicating whether the last 20 events should be tracked</param>
public DataModelEvent(bool trackHistory)
{
_trackHistory = trackHistory;
}
/// <inheritdoc />
[DataModelProperty(Name = "Last trigger", Description = "The time at which the event last triggered")]
public DateTime LastTrigger { get; private set; }
/// <inheritdoc />
[DataModelProperty(Name = "Time since trigger", Description = "The time that has passed since the last trigger")]
public TimeSpan TimeSinceLastTrigger => DateTime.Now - LastTrigger;
/// <summary>
/// Gets the event arguments of the last time the event was triggered
/// </summary>
[DataModelProperty(Description = "The arguments of the last time this event triggered")]
public T? LastEventArguments { get; private set; }
/// <inheritdoc />
[DataModelProperty(Description = "The total amount of times this event has triggered since the module was activated")]
public int TriggerCount { get; private set; }
/// <summary>
/// Gets a queue of the last 20 event arguments
/// <para>Always empty if <see cref="TrackHistory" /> is <see langword="false" /></para>
/// </summary>
[DataModelProperty(Description = "The arguments of the last time this event triggered")]
public Queue<T> EventArgumentsHistory { get; } = new(20);
/// <summary>
/// Trigger the event with the given <paramref name="eventArgs" />
/// </summary>
/// <param name="eventArgs">The event argument to pass to the event</param>
public void Trigger(T eventArgs)
{
if (eventArgs == null) throw new ArgumentNullException(nameof(eventArgs));
eventArgs.TriggerTime = DateTime.Now;
LastEventArguments = eventArgs;
LastTrigger = DateTime.Now;
TriggerCount++;
if (TrackHistory)
{
lock (EventArgumentsHistory)
{
if (EventArgumentsHistory.Count == 20)
EventArgumentsHistory.Dequeue();
EventArgumentsHistory.Enqueue(eventArgs);
}
}
OnEventTriggered();
}
internal virtual void OnEventTriggered()
{
EventTriggered?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc />
[DataModelIgnore]
public Type ArgumentsType => typeof(T);
/// <inheritdoc />
[DataModelIgnore]
public string TriggerPastParticiple => "triggered";
/// <inheritdoc />
[DataModelIgnore]
public bool TrackHistory
{
get => _trackHistory;
set
{
EventArgumentsHistory.Clear();
_trackHistory = value;
}
}
/// <inheritdoc />
[DataModelIgnore]
public DataModelEventArgs? LastEventArgumentsUntyped => LastEventArguments;
/// <inheritdoc />
[DataModelIgnore]
public List<DataModelEventArgs> EventArgumentsHistoryUntyped => EventArgumentsHistory.Cast<DataModelEventArgs>().ToList();
/// <inheritdoc />
public event EventHandler? EventTriggered;
/// <inheritdoc />
public void Reset()
{
TriggerCount = 0;
EventArgumentsHistory.Clear();
}
/// <inheritdoc />
public void Update()
{
}
}
/// <summary>
/// Represents a data model event without event arguments
/// Creates a new instance of the <see cref="DataModelEvent{T}" />
/// </summary>
public class DataModelEvent : IDataModelEvent
/// <param name="trackHistory">A boolean indicating whether the last 20 events should be tracked</param>
public DataModelEvent(bool trackHistory)
{
private bool _trackHistory;
_trackHistory = trackHistory;
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelEvent" /> class with history tracking disabled
/// </summary>
public DataModelEvent()
{
}
/// <summary>
/// Gets the event arguments of the last time the event was triggered
/// </summary>
[DataModelProperty(Description = "The arguments of the last time this event triggered")]
public T? LastEventArguments { get; private set; }
/// <summary>
/// Creates a new instance of the <see cref="DataModelEvent" />
/// </summary>
/// <param name="trackHistory">A boolean indicating whether the last 20 events should be tracked</param>
public DataModelEvent(bool trackHistory)
{
_trackHistory = trackHistory;
}
/// <summary>
/// Gets a queue of the last 20 event arguments
/// <para>Always empty if <see cref="TrackHistory" /> is <see langword="false" /></para>
/// </summary>
[DataModelProperty(Description = "The arguments of the last time this event triggered")]
public Queue<T> EventArgumentsHistory { get; } = new(20);
/// <inheritdoc />
[DataModelProperty(Name = "Last trigger", Description = "The time at which the event last triggered")]
public DateTime LastTrigger { get; private set; }
/// <summary>
/// Trigger the event with the given <paramref name="eventArgs" />
/// </summary>
/// <param name="eventArgs">The event argument to pass to the event</param>
public void Trigger(T eventArgs)
{
if (eventArgs == null) throw new ArgumentNullException(nameof(eventArgs));
eventArgs.TriggerTime = DateTime.Now;
/// <inheritdoc />
[DataModelProperty(Name = "Time since trigger", Description = "The time that has passed since the last trigger")]
public TimeSpan TimeSinceLastTrigger => DateTime.Now - LastTrigger;
LastEventArguments = eventArgs;
LastTrigger = DateTime.Now;
TriggerCount++;
/// <summary>
/// Gets the event arguments of the last time the event was triggered
/// </summary>
[DataModelProperty(Description = "The arguments of the last time this event triggered")]
public DataModelEventArgs? LastTriggerArguments { get; private set; }
/// <inheritdoc />
[DataModelProperty(Description = "The total amount of times this event has triggered since the module was activated")]
public int TriggerCount { get; private set; }
/// <summary>
/// Gets a queue of the last 20 event arguments
/// <para>Always empty if <see cref="TrackHistory" /> is <see langword="false" /></para>
/// </summary>
[DataModelProperty(Description = "The arguments of the last time this event triggered")]
public Queue<DataModelEventArgs> EventArgumentsHistory { get; } = new(20);
/// <summary>
/// Trigger the event
/// </summary>
public void Trigger()
{
DataModelEventArgs eventArgs = new() {TriggerTime = DateTime.Now};
LastTriggerArguments = eventArgs;
LastTrigger = DateTime.Now;
TriggerCount++;
if (TrackHistory)
if (TrackHistory)
lock (EventArgumentsHistory)
{
lock (EventArgumentsHistory)
{
if (EventArgumentsHistory.Count == 20)
EventArgumentsHistory.Dequeue();
EventArgumentsHistory.Enqueue(eventArgs);
}
if (EventArgumentsHistory.Count == 20)
EventArgumentsHistory.Dequeue();
EventArgumentsHistory.Enqueue(eventArgs);
}
OnEventTriggered();
}
OnEventTriggered();
}
internal virtual void OnEventTriggered()
internal virtual void OnEventTriggered()
{
EventTriggered?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc />
[DataModelProperty(Name = "Last trigger", Description = "The time at which the event last triggered")]
public DateTime LastTrigger { get; private set; }
/// <inheritdoc />
[DataModelProperty(Name = "Time since trigger", Description = "The time that has passed since the last trigger")]
public TimeSpan TimeSinceLastTrigger => DateTime.Now - LastTrigger;
/// <inheritdoc />
[DataModelProperty(Description = "The total amount of times this event has triggered since the module was activated")]
public int TriggerCount { get; private set; }
/// <inheritdoc />
[DataModelIgnore]
public Type ArgumentsType => typeof(T);
/// <inheritdoc />
[DataModelIgnore]
public string TriggerPastParticiple => "triggered";
/// <inheritdoc />
[DataModelIgnore]
public bool TrackHistory
{
get => _trackHistory;
set
{
EventTriggered?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc />
[DataModelIgnore]
public Type ArgumentsType => typeof(DataModelEventArgs);
/// <inheritdoc />
[DataModelIgnore]
public string TriggerPastParticiple => "triggered";
/// <inheritdoc />
[DataModelIgnore]
public bool TrackHistory
{
get => _trackHistory;
set
{
EventArgumentsHistory.Clear();
_trackHistory = value;
}
}
/// <inheritdoc />
[DataModelIgnore]
public DataModelEventArgs? LastEventArgumentsUntyped => LastTriggerArguments;
/// <inheritdoc />
[DataModelIgnore]
public List<DataModelEventArgs> EventArgumentsHistoryUntyped => EventArgumentsHistory.ToList();
/// <inheritdoc />
public event EventHandler? EventTriggered;
/// <inheritdoc />
public void Reset()
{
TriggerCount = 0;
EventArgumentsHistory.Clear();
_trackHistory = value;
}
}
/// <inheritdoc />
public void Update()
/// <inheritdoc />
[DataModelIgnore]
public DataModelEventArgs? LastEventArgumentsUntyped => LastEventArguments;
/// <inheritdoc />
[DataModelIgnore]
public List<DataModelEventArgs> EventArgumentsHistoryUntyped => EventArgumentsHistory.Cast<DataModelEventArgs>().ToList();
/// <inheritdoc />
public event EventHandler? EventTriggered;
/// <inheritdoc />
public void Reset()
{
TriggerCount = 0;
EventArgumentsHistory.Clear();
}
/// <inheritdoc />
public void Update()
{
}
}
/// <summary>
/// Represents a data model event without event arguments
/// </summary>
public class DataModelEvent : IDataModelEvent
{
private bool _trackHistory;
/// <summary>
/// Creates a new instance of the <see cref="DataModelEvent" /> class with history tracking disabled
/// </summary>
public DataModelEvent()
{
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelEvent" />
/// </summary>
/// <param name="trackHistory">A boolean indicating whether the last 20 events should be tracked</param>
public DataModelEvent(bool trackHistory)
{
_trackHistory = trackHistory;
}
/// <summary>
/// Gets the event arguments of the last time the event was triggered
/// </summary>
[DataModelProperty(Description = "The arguments of the last time this event triggered")]
public DataModelEventArgs? LastTriggerArguments { get; private set; }
/// <summary>
/// Gets a queue of the last 20 event arguments
/// <para>Always empty if <see cref="TrackHistory" /> is <see langword="false" /></para>
/// </summary>
[DataModelProperty(Description = "The arguments of the last time this event triggered")]
public Queue<DataModelEventArgs> EventArgumentsHistory { get; } = new(20);
/// <summary>
/// Trigger the event
/// </summary>
public void Trigger()
{
DataModelEventArgs eventArgs = new() {TriggerTime = DateTime.Now};
LastTriggerArguments = eventArgs;
LastTrigger = DateTime.Now;
TriggerCount++;
if (TrackHistory)
lock (EventArgumentsHistory)
{
if (EventArgumentsHistory.Count == 20)
EventArgumentsHistory.Dequeue();
EventArgumentsHistory.Enqueue(eventArgs);
}
OnEventTriggered();
}
internal virtual void OnEventTriggered()
{
EventTriggered?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc />
[DataModelProperty(Name = "Last trigger", Description = "The time at which the event last triggered")]
public DateTime LastTrigger { get; private set; }
/// <inheritdoc />
[DataModelProperty(Name = "Time since trigger", Description = "The time that has passed since the last trigger")]
public TimeSpan TimeSinceLastTrigger => DateTime.Now - LastTrigger;
/// <inheritdoc />
[DataModelProperty(Description = "The total amount of times this event has triggered since the module was activated")]
public int TriggerCount { get; private set; }
/// <inheritdoc />
[DataModelIgnore]
public Type ArgumentsType => typeof(DataModelEventArgs);
/// <inheritdoc />
[DataModelIgnore]
public string TriggerPastParticiple => "triggered";
/// <inheritdoc />
[DataModelIgnore]
public bool TrackHistory
{
get => _trackHistory;
set
{
EventArgumentsHistory.Clear();
_trackHistory = value;
}
}
/// <inheritdoc />
[DataModelIgnore]
public DataModelEventArgs? LastEventArgumentsUntyped => LastTriggerArguments;
/// <inheritdoc />
[DataModelIgnore]
public List<DataModelEventArgs> EventArgumentsHistoryUntyped => EventArgumentsHistory.ToList();
/// <inheritdoc />
public event EventHandler? EventTriggered;
/// <inheritdoc />
public void Reset()
{
TriggerCount = 0;
EventArgumentsHistory.Clear();
}
/// <inheritdoc />
public void Update()
{
}
}
@@ -1,17 +1,16 @@
using System;
using Artemis.Core.Modules;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents the base class for data model events that contain event data
/// </summary>
public class DataModelEventArgs
{
/// <summary>
/// Represents the base class for data model events that contain event data
/// Gets the time at which the event with these arguments was triggered
/// </summary>
public class DataModelEventArgs
{
/// <summary>
/// Gets the time at which the event with these arguments was triggered
/// </summary>
[DataModelIgnore]
public DateTime TriggerTime { get; internal set; }
}
[DataModelIgnore]
public DateTime TriggerTime { get; internal set; }
}
@@ -6,388 +6,388 @@ using System.Reflection;
using Artemis.Core.Modules;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a path that points to a property in data model
/// </summary>
public class DataModelPath : IStorageModel, IDisposable
{
private readonly LinkedList<DataModelPathSegment> _segments;
private Expression<Func<object, object>>? _accessorLambda;
private bool _disposed;
/// <summary>
/// Represents a path that points to a property in data model
/// Creates a new instance of the <see cref="DataModelPath" /> class pointing directly to the target
/// </summary>
public class DataModelPath : IStorageModel, IDisposable
/// <param name="target">The target at which this path starts</param>
public DataModelPath(DataModel target)
{
private readonly LinkedList<DataModelPathSegment> _segments;
private Expression<Func<object, object>>? _accessorLambda;
private bool _disposed;
Target = target ?? throw new ArgumentNullException(nameof(target));
Path = "";
Entity = new DataModelPathEntity();
/// <summary>
/// Creates a new instance of the <see cref="DataModelPath" /> class pointing directly to the target
/// </summary>
/// <param name="target">The target at which this path starts</param>
public DataModelPath(DataModel target)
{
Target = target ?? throw new ArgumentNullException(nameof(target));
Path = "";
Entity = new DataModelPathEntity();
_segments = new LinkedList<DataModelPathSegment>();
_segments = new LinkedList<DataModelPathSegment>();
Save();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelPath" /> class pointing to the provided path
/// </summary>
/// <param name="target">The target at which this path starts</param>
/// <param name="path">A point-separated path</param>
public DataModelPath(DataModel target, string path)
{
Target = target ?? throw new ArgumentNullException(nameof(target));
Path = path ?? throw new ArgumentNullException(nameof(path));
Entity = new DataModelPathEntity();
_segments = new LinkedList<DataModelPathSegment>();
Save();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelPath" /> class based on an existing path
/// </summary>
/// <param name="dataModelPath">The path to base the new instance on</param>
public DataModelPath(DataModelPath dataModelPath)
{
if (dataModelPath == null)
throw new ArgumentNullException(nameof(dataModelPath));
Target = dataModelPath.Target;
Path = dataModelPath.Path;
Entity = new DataModelPathEntity();
_segments = new LinkedList<DataModelPathSegment>();
Save();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelPath" /> class based on a <see cref="DataModelPathEntity" />
/// </summary>
/// <param name="entity"></param>
public DataModelPath(DataModelPathEntity entity)
{
Path = entity.Path;
Entity = entity;
_segments = new LinkedList<DataModelPathSegment>();
Load();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Gets the data model at which this path starts
/// </summary>
public DataModel? Target { get; private set; }
/// <summary>
/// Gets the data model ID of the <see cref="Target" /> if it is a <see cref="DataModel" />
/// </summary>
public string? DataModelId => Target?.Module.Id;
/// <summary>
/// Gets the point-separated path associated with this <see cref="DataModelPath" />
/// </summary>
public string Path { get; private set; }
/// <summary>
/// Gets a boolean indicating whether all <see cref="Segments" /> are valid
/// </summary>
public bool IsValid => Segments.Any() && Segments.All(p => p.Type != DataModelPathSegmentType.Invalid);
/// <summary>
/// Gets a read-only list of all segments of this path
/// </summary>
public IReadOnlyCollection<DataModelPathSegment> Segments => _segments.ToList().AsReadOnly();
/// <summary>
/// Gets the entity used for persistent storage
/// </summary>
public DataModelPathEntity Entity { get; }
internal Func<object, object>? Accessor { get; private set; }
/// <summary>
/// Gets the current value of the path
/// </summary>
public object? GetValue()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
if (_accessorLambda == null || Target == null)
return null;
// If the accessor has not yet been compiled do it now that it's first required
if (Accessor == null)
Accessor = _accessorLambda.Compile();
return Accessor(Target);
}
/// <summary>
/// Gets the property info of the property this path points to
/// </summary>
/// <returns>If static, the property info. If dynamic, <c>null</c></returns>
public PropertyInfo? GetPropertyInfo()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
return Segments.LastOrDefault()?.GetPropertyInfo();
}
/// <summary>
/// Gets the type of the property this path points to
/// </summary>
/// <returns>If possible, the property type</returns>
public Type? GetPropertyType()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
return Segments.LastOrDefault()?.GetPropertyType();
}
/// <summary>
/// Gets the property description of the property this path points to
/// </summary>
/// <returns>If found, the data model property description</returns>
public DataModelPropertyAttribute? GetPropertyDescription()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
return Segments.LastOrDefault()?.GetPropertyDescription();
}
/// <inheritdoc />
public override string ToString()
{
return string.IsNullOrWhiteSpace(Path) ? "this" : Path;
}
/// <summary>
/// Occurs whenever the path becomes invalid
/// </summary>
public event EventHandler? PathInvalidated;
/// <summary>
/// Occurs whenever the path becomes valid
/// </summary>
public event EventHandler? PathValidated;
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_disposed = true;
DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded;
DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved;
Invalidate();
}
}
/// <summary>
/// Invokes the <see cref="PathInvalidated" /> event
/// </summary>
protected virtual void OnPathValidated()
{
PathValidated?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Invokes the <see cref="PathValidated" /> event
/// </summary>
protected virtual void OnPathInvalidated()
{
PathInvalidated?.Invoke(this, EventArgs.Empty);
}
internal void Invalidate()
{
Target?.RemoveDataModelPath(this);
foreach (DataModelPathSegment dataModelPathSegment in _segments)
dataModelPathSegment.Dispose();
_segments.Clear();
_accessorLambda = null;
Accessor = null;
OnPathInvalidated();
}
internal void Initialize()
{
if (Target == null)
return;
Target.AddDataModelPath(this);
DataModelPathSegment startSegment = new(this, "target", "target");
startSegment.Node = _segments.AddFirst(startSegment);
// On an empty path don't bother processing segments
if (!string.IsNullOrWhiteSpace(Path))
{
string[] segments = Path.Split(".");
for (int index = 0; index < segments.Length; index++)
{
string identifier = segments[index];
LinkedListNode<DataModelPathSegment> node = _segments.AddLast(
new DataModelPathSegment(this, identifier, string.Join('.', segments.Take(index + 1)))
);
node.Value.Node = node;
}
}
ParameterExpression parameter = Expression.Parameter(typeof(object), "t");
Expression? expression = Expression.Convert(parameter, Target.GetType());
Expression? nullCondition = null;
MethodInfo equals = typeof(object).GetMethod("Equals", BindingFlags.Static | BindingFlags.Public)!;
foreach (DataModelPathSegment segment in _segments)
{
BinaryExpression notNull;
try
{
notNull = Expression.NotEqual(expression, Expression.Default(expression.Type));
}
catch (InvalidOperationException)
{
notNull = Expression.NotEqual(
Expression.Call(
null,
equals,
Expression.Convert(expression, typeof(object)),
Expression.Convert(Expression.Default(expression.Type), typeof(object))),
Expression.Constant(true));
}
nullCondition = nullCondition != null ? Expression.AndAlso(nullCondition, notNull) : notNull;
expression = segment.Initialize(parameter, expression, nullCondition);
if (expression == null)
return;
}
if (nullCondition == null)
return;
_accessorLambda = Expression.Lambda<Func<object, object>>(
// Wrap with a null check
Expression.Condition(
nullCondition,
Expression.Convert(expression, typeof(object)),
Expression.Convert(Expression.Default(expression.Type), typeof(object))
),
parameter
);
if (IsValid)
OnPathValidated();
}
private void SubscribeToDataModelStore()
{
DataModelStore.DataModelAdded += DataModelStoreOnDataModelAdded;
DataModelStore.DataModelRemoved += DataModelStoreOnDataModelRemoved;
}
private void DataModelStoreOnDataModelAdded(object? sender, DataModelStoreEvent e)
{
if (e.Registration.DataModel.Module.Id != Entity.DataModelId)
return;
Invalidate();
Target = e.Registration.DataModel;
Initialize();
}
private void DataModelStoreOnDataModelRemoved(object? sender, DataModelStoreEvent e)
{
if (e.Registration.DataModel.Module.Id != Entity.DataModelId)
return;
Invalidate();
Target = null;
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#region Storage
/// <inheritdoc />
public void Load()
{
Path = Entity.Path;
if (Target == null && Entity.DataModelId != null)
Target = DataModelStore.Get(Entity.DataModelId)?.DataModel;
}
/// <inheritdoc />
public void Save()
{
// Do not save an invalid state
if (!IsValid)
return;
Entity.Path = Path;
Entity.DataModelId = DataModelId;
}
#region Equality members
/// <inheritdoc cref="Equals(object)"/>>
protected bool Equals(DataModelPath other)
{
return ReferenceEquals(Target, other.Target) && Path == other.Path;
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((DataModelPath) obj);
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(Target, Path);
}
#endregion
#endregion
Save();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelPath" /> class pointing to the provided path
/// </summary>
/// <param name="target">The target at which this path starts</param>
/// <param name="path">A point-separated path</param>
public DataModelPath(DataModel target, string path)
{
Target = target ?? throw new ArgumentNullException(nameof(target));
Path = path ?? throw new ArgumentNullException(nameof(path));
Entity = new DataModelPathEntity();
_segments = new LinkedList<DataModelPathSegment>();
Save();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelPath" /> class based on an existing path
/// </summary>
/// <param name="dataModelPath">The path to base the new instance on</param>
public DataModelPath(DataModelPath dataModelPath)
{
if (dataModelPath == null)
throw new ArgumentNullException(nameof(dataModelPath));
Target = dataModelPath.Target;
Path = dataModelPath.Path;
Entity = new DataModelPathEntity();
_segments = new LinkedList<DataModelPathSegment>();
Save();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Creates a new instance of the <see cref="DataModelPath" /> class based on a <see cref="DataModelPathEntity" />
/// </summary>
/// <param name="entity"></param>
public DataModelPath(DataModelPathEntity entity)
{
Path = entity.Path;
Entity = entity;
_segments = new LinkedList<DataModelPathSegment>();
Load();
Initialize();
SubscribeToDataModelStore();
}
/// <summary>
/// Gets the data model at which this path starts
/// </summary>
public DataModel? Target { get; private set; }
/// <summary>
/// Gets the data model ID of the <see cref="Target" /> if it is a <see cref="DataModel" />
/// </summary>
public string? DataModelId => Target?.Module.Id;
/// <summary>
/// Gets the point-separated path associated with this <see cref="DataModelPath" />
/// </summary>
public string Path { get; private set; }
/// <summary>
/// Gets a boolean indicating whether all <see cref="Segments" /> are valid
/// </summary>
public bool IsValid => Segments.Any() && Segments.All(p => p.Type != DataModelPathSegmentType.Invalid);
/// <summary>
/// Gets a read-only list of all segments of this path
/// </summary>
public IReadOnlyCollection<DataModelPathSegment> Segments => _segments.ToList().AsReadOnly();
/// <summary>
/// Gets the entity used for persistent storage
/// </summary>
public DataModelPathEntity Entity { get; }
internal Func<object, object>? Accessor { get; private set; }
/// <summary>
/// Gets the current value of the path
/// </summary>
public object? GetValue()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
if (_accessorLambda == null || Target == null)
return null;
// If the accessor has not yet been compiled do it now that it's first required
if (Accessor == null)
Accessor = _accessorLambda.Compile();
return Accessor(Target);
}
/// <summary>
/// Gets the property info of the property this path points to
/// </summary>
/// <returns>If static, the property info. If dynamic, <c>null</c></returns>
public PropertyInfo? GetPropertyInfo()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
return Segments.LastOrDefault()?.GetPropertyInfo();
}
/// <summary>
/// Gets the type of the property this path points to
/// </summary>
/// <returns>If possible, the property type</returns>
public Type? GetPropertyType()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
return Segments.LastOrDefault()?.GetPropertyType();
}
/// <summary>
/// Gets the property description of the property this path points to
/// </summary>
/// <returns>If found, the data model property description</returns>
public DataModelPropertyAttribute? GetPropertyDescription()
{
if (_disposed)
throw new ObjectDisposedException("DataModelPath");
return Segments.LastOrDefault()?.GetPropertyDescription();
}
/// <inheritdoc />
public override string ToString()
{
return string.IsNullOrWhiteSpace(Path) ? "this" : Path;
}
/// <summary>
/// Occurs whenever the path becomes invalid
/// </summary>
public event EventHandler? PathInvalidated;
/// <summary>
/// Occurs whenever the path becomes valid
/// </summary>
public event EventHandler? PathValidated;
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_disposed = true;
DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded;
DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved;
Invalidate();
}
}
/// <summary>
/// Invokes the <see cref="PathInvalidated" /> event
/// </summary>
protected virtual void OnPathValidated()
{
PathValidated?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Invokes the <see cref="PathValidated" /> event
/// </summary>
protected virtual void OnPathInvalidated()
{
PathInvalidated?.Invoke(this, EventArgs.Empty);
}
internal void Invalidate()
{
Target?.RemoveDataModelPath(this);
foreach (DataModelPathSegment dataModelPathSegment in _segments)
dataModelPathSegment.Dispose();
_segments.Clear();
_accessorLambda = null;
Accessor = null;
OnPathInvalidated();
}
internal void Initialize()
{
if (Target == null)
return;
Target.AddDataModelPath(this);
DataModelPathSegment startSegment = new(this, "target", "target");
startSegment.Node = _segments.AddFirst(startSegment);
// On an empty path don't bother processing segments
if (!string.IsNullOrWhiteSpace(Path))
{
string[] segments = Path.Split(".");
for (int index = 0; index < segments.Length; index++)
{
string identifier = segments[index];
LinkedListNode<DataModelPathSegment> node = _segments.AddLast(
new DataModelPathSegment(this, identifier, string.Join('.', segments.Take(index + 1)))
);
node.Value.Node = node;
}
}
ParameterExpression parameter = Expression.Parameter(typeof(object), "t");
Expression? expression = Expression.Convert(parameter, Target.GetType());
Expression? nullCondition = null;
MethodInfo equals = typeof(object).GetMethod("Equals", BindingFlags.Static | BindingFlags.Public)!;
foreach (DataModelPathSegment segment in _segments)
{
BinaryExpression notNull;
try
{
notNull = Expression.NotEqual(expression, Expression.Default(expression.Type));
}
catch (InvalidOperationException)
{
notNull = Expression.NotEqual(
Expression.Call(
null,
equals,
Expression.Convert(expression, typeof(object)),
Expression.Convert(Expression.Default(expression.Type), typeof(object))),
Expression.Constant(true));
}
nullCondition = nullCondition != null ? Expression.AndAlso(nullCondition, notNull) : notNull;
expression = segment.Initialize(parameter, expression, nullCondition);
if (expression == null)
return;
}
if (nullCondition == null)
return;
_accessorLambda = Expression.Lambda<Func<object, object>>(
// Wrap with a null check
Expression.Condition(
nullCondition,
Expression.Convert(expression, typeof(object)),
Expression.Convert(Expression.Default(expression.Type), typeof(object))
),
parameter
);
if (IsValid)
OnPathValidated();
}
private void SubscribeToDataModelStore()
{
DataModelStore.DataModelAdded += DataModelStoreOnDataModelAdded;
DataModelStore.DataModelRemoved += DataModelStoreOnDataModelRemoved;
}
private void DataModelStoreOnDataModelAdded(object? sender, DataModelStoreEvent e)
{
if (e.Registration.DataModel.Module.Id != Entity.DataModelId)
return;
Invalidate();
Target = e.Registration.DataModel;
Initialize();
}
private void DataModelStoreOnDataModelRemoved(object? sender, DataModelStoreEvent e)
{
if (e.Registration.DataModel.Module.Id != Entity.DataModelId)
return;
Invalidate();
Target = null;
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#region Storage
/// <inheritdoc />
public void Load()
{
Path = Entity.Path;
if (Target == null && Entity.DataModelId != null)
Target = DataModelStore.Get(Entity.DataModelId)?.DataModel;
}
/// <inheritdoc />
public void Save()
{
// Do not save an invalid state
if (!IsValid)
return;
Entity.Path = Path;
Entity.DataModelId = DataModelId;
}
#region Equality members
/// <inheritdoc cref="Equals(object)" />
/// >
protected bool Equals(DataModelPath other)
{
return ReferenceEquals(Target, other.Target) && Path == other.Path;
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((DataModelPath) obj);
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(Target, Path);
}
#endregion
#endregion
}
@@ -6,307 +6,294 @@ using System.Reflection;
using Artemis.Core.Modules;
using Humanizer;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a segment of a data model path
/// </summary>
public class DataModelPathSegment : IDisposable
{
/// <summary>
/// Represents a segment of a data model path
/// </summary>
public class DataModelPathSegment : IDisposable
private Expression<Func<object, object>>? _accessorLambda;
private DataModel? _dynamicDataModel;
private DataModelPropertyAttribute? _dynamicDataModelAttribute;
private Type? _dynamicDataModelType;
private PropertyInfo? _property;
internal DataModelPathSegment(DataModelPath dataModelPath, string identifier, string path)
{
private Expression<Func<object, object>>? _accessorLambda;
private DataModel? _dynamicDataModel;
private Type? _dynamicDataModelType;
private DataModelPropertyAttribute? _dynamicDataModelAttribute;
private PropertyInfo? _property;
DataModelPath = dataModelPath;
Identifier = identifier;
Path = path;
IsStartSegment = !DataModelPath.Segments.Any();
}
internal DataModelPathSegment(DataModelPath dataModelPath, string identifier, string path)
/// <summary>
/// Gets the data model path this is a segment of
/// </summary>
public DataModelPath DataModelPath { get; }
/// <summary>
/// Gets the identifier that is associated with this segment
/// </summary>
public string Identifier { get; }
/// <summary>
/// Gets the path that leads to this segment
/// </summary>
public string Path { get; }
/// <summary>
/// Gets a boolean indicating whether this is the first segment in the path
/// </summary>
public bool IsStartSegment { get; }
/// <summary>
/// Gets the type of data model this segment of the path points to
/// </summary>
public DataModelPathSegmentType Type { get; private set; }
/// <summary>
/// Gets the previous segment in the path
/// </summary>
public DataModelPathSegment? Previous => Node?.Previous?.Value;
/// <summary>
/// Gets the next segment in the path
/// </summary>
public DataModelPathSegment? Next => Node?.Next?.Value;
internal Func<object, object>? Accessor { get; set; }
internal LinkedListNode<DataModelPathSegment>? Node { get; set; }
/// <summary>
/// Returns the current value of the path up to this segment
/// </summary>
/// <returns></returns>
public object? GetValue()
{
if (Type == DataModelPathSegmentType.Invalid || DataModelPath.Target == null || _accessorLambda == null)
return null;
// If the accessor has not yet been compiled do it now that it's first required
if (Accessor == null)
Accessor = _accessorLambda.Compile();
return Accessor(DataModelPath.Target);
}
/// <inheritdoc />
public override string ToString()
{
return $"[{Type}] {Path}";
}
/// <summary>
/// Gets the property info of the property this segment points to
/// </summary>
/// <returns>If static, the property info. If dynamic, <c>null</c></returns>
public PropertyInfo? GetPropertyInfo()
{
// Dynamic types have no property and therefore no property info
if (Type == DataModelPathSegmentType.Dynamic)
return null;
// The start segment has none either because it is the datamodel
if (IsStartSegment)
return null;
// If this is not the first segment in a path, the property is located on the previous segment
return Previous?.GetPropertyType()?.GetProperties().FirstOrDefault(p => p.Name == Identifier);
}
/// <summary>
/// Gets the property description of the property this segment points to
/// </summary>
/// <returns>If found, the data model property description</returns>
public DataModelPropertyAttribute? GetPropertyDescription()
{
// Dynamic types have a data model description
if (Type == DataModelPathSegmentType.Dynamic)
return _dynamicDataModelAttribute;
if (IsStartSegment && DataModelPath.Target != null)
return DataModelPath.Target.DataModelDescription;
if (IsStartSegment)
return null;
PropertyInfo? propertyInfo = GetPropertyInfo();
if (propertyInfo == null)
return null;
// Static types may have one as an attribute
DataModelPropertyAttribute? attribute = DataModelPath.Target?.GetPropertyDescription(propertyInfo);
if (attribute != null)
{
DataModelPath = dataModelPath;
Identifier = identifier;
Path = path;
IsStartSegment = !DataModelPath.Segments.Any();
if (string.IsNullOrWhiteSpace(attribute.Name))
attribute.Name = propertyInfo.Name.Humanize();
return attribute;
}
/// <summary>
/// Gets the data model path this is a segment of
/// </summary>
public DataModelPath DataModelPath { get; }
return new DataModelPropertyAttribute {Name = propertyInfo.Name.Humanize(), ResetsDepth = false};
}
/// <summary>
/// Gets the identifier that is associated with this segment
/// </summary>
public string Identifier { get; }
/// <summary>
/// Gets the type of the property this path points to
/// </summary>
/// <returns>If possible, the property type</returns>
public Type? GetPropertyType()
{
// The start segment type is always the target type
if (IsStartSegment)
return DataModelPath.Target?.GetType();
/// <summary>
/// Gets the path that leads to this segment
/// </summary>
public string Path { get; }
/// <summary>
/// Gets a boolean indicating whether this is the first segment in the path
/// </summary>
public bool IsStartSegment { get; }
/// <summary>
/// Gets the type of data model this segment of the path points to
/// </summary>
public DataModelPathSegmentType Type { get; private set; }
/// <summary>
/// Gets the previous segment in the path
/// </summary>
public DataModelPathSegment? Previous => Node?.Previous?.Value;
/// <summary>
/// Gets the next segment in the path
/// </summary>
public DataModelPathSegment? Next => Node?.Next?.Value;
internal Func<object, object>? Accessor { get; set; }
internal LinkedListNode<DataModelPathSegment>? Node { get; set; }
/// <summary>
/// Returns the current value of the path up to this segment
/// </summary>
/// <returns></returns>
public object? GetValue()
// Prefer basing the type on the property info
PropertyInfo? propertyInfo = GetPropertyInfo();
Type? type = propertyInfo?.PropertyType;
// Property info is not available on dynamic paths though, so fall back on the current value
if (propertyInfo == null)
{
if (Type == DataModelPathSegmentType.Invalid || DataModelPath.Target == null || _accessorLambda == null)
return null;
// If the accessor has not yet been compiled do it now that it's first required
if (Accessor == null)
Accessor = _accessorLambda.Compile();
return Accessor(DataModelPath.Target);
object? currentValue = GetValue();
if (currentValue != null)
type = currentValue.GetType();
}
/// <inheritdoc />
public override string ToString()
return type;
}
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
return $"[{Type}] {Path}";
}
/// <summary>
/// Gets the property info of the property this segment points to
/// </summary>
/// <returns>If static, the property info. If dynamic, <c>null</c></returns>
public PropertyInfo? GetPropertyInfo()
{
// Dynamic types have no property and therefore no property info
if (Type == DataModelPathSegmentType.Dynamic)
return null;
// The start segment has none either because it is the datamodel
if (IsStartSegment)
return null;
// If this is not the first segment in a path, the property is located on the previous segment
return Previous?.GetPropertyType()?.GetProperties().FirstOrDefault(p => p.Name == Identifier);
}
/// <summary>
/// Gets the property description of the property this segment points to
/// </summary>
/// <returns>If found, the data model property description</returns>
public DataModelPropertyAttribute? GetPropertyDescription()
{
// Dynamic types have a data model description
if (Type == DataModelPathSegmentType.Dynamic)
return _dynamicDataModelAttribute;
if (IsStartSegment && DataModelPath.Target != null)
return DataModelPath.Target.DataModelDescription;
if (IsStartSegment)
return null;
PropertyInfo? propertyInfo = GetPropertyInfo();
if (propertyInfo == null)
return null;
// Static types may have one as an attribute
DataModelPropertyAttribute? attribute = DataModelPath.Target?.GetPropertyDescription(propertyInfo);
if (attribute != null)
if (_dynamicDataModel != null)
{
if (string.IsNullOrWhiteSpace(attribute.Name))
attribute.Name = propertyInfo.Name.Humanize();
return attribute;
_dynamicDataModel.DynamicChildAdded -= DynamicChildOnDynamicChildAdded;
_dynamicDataModel.DynamicChildRemoved -= DynamicChildOnDynamicChildRemoved;
}
return new DataModelPropertyAttribute {Name = propertyInfo.Name.Humanize(), ResetsDepth = false};
Type = DataModelPathSegmentType.Invalid;
_accessorLambda = null;
Accessor = null;
}
}
/// <summary>
/// Gets the type of the property this path points to
/// </summary>
/// <returns>If possible, the property type</returns>
public Type? GetPropertyType()
internal Expression? Initialize(ParameterExpression parameter, Expression expression, Expression nullCondition)
{
if (IsStartSegment)
{
// The start segment type is always the target type
if (IsStartSegment)
return DataModelPath.Target?.GetType();
// Prefer basing the type on the property info
PropertyInfo? propertyInfo = GetPropertyInfo();
Type? type = propertyInfo?.PropertyType;
// Property info is not available on dynamic paths though, so fall back on the current value
if (propertyInfo == null)
{
object? currentValue = GetValue();
if (currentValue != null)
type = currentValue.GetType();
}
return type;
}
internal Expression? Initialize(ParameterExpression parameter, Expression expression, Expression nullCondition)
{
if (IsStartSegment)
{
Type = DataModelPathSegmentType.Static;
return CreateExpression(parameter, expression, nullCondition);
}
Type? previousType = Previous?.GetPropertyType();
if (previousType == null)
{
Type = DataModelPathSegmentType.Invalid;
return CreateExpression(parameter, expression, nullCondition);
}
// Prefer static since that's faster
DetermineStaticType(previousType);
// If no static type could be found, check if this is a data model and if so, look for a dynamic type
if (Type == DataModelPathSegmentType.Invalid && typeof(DataModel).IsAssignableFrom(previousType))
{
_dynamicDataModel = Previous?.GetValue() as DataModel;
// Cannot determine a dynamic type on a null data model, leave the segment invalid
if (_dynamicDataModel == null)
return CreateExpression(parameter, expression, nullCondition);
// If a dynamic data model is found the use that
bool hasDynamicChild = _dynamicDataModel.DynamicChildren.TryGetValue(Identifier, out DynamicChild? dynamicChild);
if (hasDynamicChild && dynamicChild?.BaseValue != null)
DetermineDynamicType(dynamicChild.BaseValue, dynamicChild.Attribute);
_dynamicDataModel.DynamicChildAdded += DynamicChildOnDynamicChildAdded;
_dynamicDataModel.DynamicChildRemoved += DynamicChildOnDynamicChildRemoved;
}
Type = DataModelPathSegmentType.Static;
return CreateExpression(parameter, expression, nullCondition);
}
private Expression? CreateExpression(ParameterExpression parameter, Expression expression, Expression nullCondition)
Type? previousType = Previous?.GetPropertyType();
if (previousType == null)
{
if (Type == DataModelPathSegmentType.Invalid)
{
_accessorLambda = null;
Accessor = null;
return null;
}
Type = DataModelPathSegmentType.Invalid;
return CreateExpression(parameter, expression, nullCondition);
}
Expression accessorExpression;
// A start segment just accesses the target
if (IsStartSegment)
accessorExpression = expression;
// A static segment just needs to access the property or filed
else if (Type == DataModelPathSegmentType.Static)
{
accessorExpression = _property != null
? Expression.Property(expression, _property)
: Expression.PropertyOrField(expression, Identifier);
}
// A dynamic segment calls the generic method DataModel.DynamicChild<T> and provides the identifier as an argument
else
{
accessorExpression = Expression.Call(
expression,
nameof(DataModel.GetDynamicChildValue),
_dynamicDataModelType != null ? new[] {_dynamicDataModelType} : null,
Expression.Constant(Identifier)
);
}
// Prefer static since that's faster
DetermineStaticType(previousType);
_accessorLambda = Expression.Lambda<Func<object, object>>(
// Wrap with a null check
Expression.Condition(
nullCondition,
Expression.Convert(accessorExpression, typeof(object)),
Expression.Convert(Expression.Default(accessorExpression.Type), typeof(object))
),
parameter
);
// If no static type could be found, check if this is a data model and if so, look for a dynamic type
if (Type == DataModelPathSegmentType.Invalid && typeof(DataModel).IsAssignableFrom(previousType))
{
_dynamicDataModel = Previous?.GetValue() as DataModel;
// Cannot determine a dynamic type on a null data model, leave the segment invalid
if (_dynamicDataModel == null)
return CreateExpression(parameter, expression, nullCondition);
// If a dynamic data model is found the use that
bool hasDynamicChild = _dynamicDataModel.DynamicChildren.TryGetValue(Identifier, out DynamicChild? dynamicChild);
if (hasDynamicChild && dynamicChild?.BaseValue != null)
DetermineDynamicType(dynamicChild.BaseValue, dynamicChild.Attribute);
_dynamicDataModel.DynamicChildAdded += DynamicChildOnDynamicChildAdded;
_dynamicDataModel.DynamicChildRemoved += DynamicChildOnDynamicChildRemoved;
}
return CreateExpression(parameter, expression, nullCondition);
}
private Expression? CreateExpression(ParameterExpression parameter, Expression expression, Expression nullCondition)
{
if (Type == DataModelPathSegmentType.Invalid)
{
_accessorLambda = null;
Accessor = null;
return accessorExpression;
return null;
}
private void DetermineDynamicType(object dynamicDataModel, DataModelPropertyAttribute attribute)
Expression accessorExpression;
// A start segment just accesses the target
if (IsStartSegment)
accessorExpression = expression;
// A static segment just needs to access the property or filed
else if (Type == DataModelPathSegmentType.Static)
accessorExpression = _property != null
? Expression.Property(expression, _property)
: Expression.PropertyOrField(expression, Identifier);
// A dynamic segment calls the generic method DataModel.DynamicChild<T> and provides the identifier as an argument
else
accessorExpression = Expression.Call(
expression,
nameof(DataModel.GetDynamicChildValue),
_dynamicDataModelType != null ? new[] {_dynamicDataModelType} : null,
Expression.Constant(Identifier)
);
_accessorLambda = Expression.Lambda<Func<object, object>>(
// Wrap with a null check
Expression.Condition(
nullCondition,
Expression.Convert(accessorExpression, typeof(object)),
Expression.Convert(Expression.Default(accessorExpression.Type), typeof(object))
),
parameter
);
Accessor = null;
return accessorExpression;
}
private void DetermineDynamicType(object dynamicDataModel, DataModelPropertyAttribute attribute)
{
Type = DataModelPathSegmentType.Dynamic;
_dynamicDataModelType = dynamicDataModel.GetType();
_dynamicDataModelAttribute = attribute;
}
private void DetermineStaticType(Type previousType)
{
// Situations in which AmbiguousMatchException occurs ...
//
// ...derived type declares a property that hides an inherited property with the same name, by using the new modifier
_property = previousType.GetProperties(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(p => p.Name == Identifier);
Type = _property == null ? DataModelPathSegmentType.Invalid : DataModelPathSegmentType.Static;
}
private void DynamicChildOnDynamicChildAdded(object? sender, DynamicDataModelChildEventArgs e)
{
if (e.Key == Identifier)
{
Type = DataModelPathSegmentType.Dynamic;
_dynamicDataModelType = dynamicDataModel.GetType();
_dynamicDataModelAttribute = attribute;
DataModelPath.Invalidate();
DataModelPath.Initialize();
}
}
private void DetermineStaticType(Type previousType)
{
// Situations in which AmbiguousMatchException occurs ...
//
// ...derived type declares a property that hides an inherited property with the same name, by using the new modifier
_property = previousType.GetProperties(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(p => p.Name == Identifier);
Type = _property == null ? DataModelPathSegmentType.Invalid : DataModelPathSegmentType.Static;
}
private void DynamicChildOnDynamicChildRemoved(object? sender, DynamicDataModelChildEventArgs e)
{
if (e.DynamicChild.BaseValue == _dynamicDataModel)
DataModelPath.Invalidate();
}
#region IDisposable
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (_dynamicDataModel != null)
{
_dynamicDataModel.DynamicChildAdded -= DynamicChildOnDynamicChildAdded;
_dynamicDataModel.DynamicChildRemoved -= DynamicChildOnDynamicChildRemoved;
}
Type = DataModelPathSegmentType.Invalid;
_accessorLambda = null;
Accessor = null;
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
#region Event handlers
private void DynamicChildOnDynamicChildAdded(object? sender, DynamicDataModelChildEventArgs e)
{
if (e.Key == Identifier)
{
DataModelPath.Invalidate();
DataModelPath.Initialize();
}
}
private void DynamicChildOnDynamicChildRemoved(object? sender, DynamicDataModelChildEventArgs e)
{
if (e.DynamicChild.BaseValue == _dynamicDataModel)
DataModelPath.Invalidate();
}
#endregion
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
@@ -1,23 +1,22 @@
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a type of data model path
/// </summary>
public enum DataModelPathSegmentType
{
/// <summary>
/// Represents a type of data model path
/// Represents an invalid data model type that points to a missing data model
/// </summary>
public enum DataModelPathSegmentType
{
/// <summary>
/// Represents an invalid data model type that points to a missing data model
/// </summary>
Invalid,
Invalid,
/// <summary>
/// Represents a static data model type that points to a data model defined in code
/// </summary>
Static,
/// <summary>
/// Represents a static data model type that points to a data model defined in code
/// </summary>
Static,
/// <summary>
/// Represents a static data model type that points to a data model defined at runtime
/// </summary>
Dynamic
}
/// <summary>
/// Represents a static data model type that points to a data model defined at runtime
/// </summary>
Dynamic
}
@@ -1,69 +1,68 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents an event that is part of a data model
/// </summary>
public interface IDataModelEvent
{
/// <summary>
/// Represents an event that is part of a data model
/// Gets the last time the event was triggered
/// </summary>
public interface IDataModelEvent
{
/// <summary>
/// Gets the last time the event was triggered
/// </summary>
DateTime LastTrigger { get; }
DateTime LastTrigger { get; }
/// <summary>
/// Gets the time that has passed since the last trigger
/// </summary>
TimeSpan TimeSinceLastTrigger { get; }
/// <summary>
/// Gets the amount of times the event was triggered
/// </summary>
int TriggerCount { get; }
/// <summary>
/// Gets the time that has passed since the last trigger
/// </summary>
TimeSpan TimeSinceLastTrigger { get; }
/// <summary>
/// Gets the type of arguments this event contains
/// </summary>
Type ArgumentsType { get; }
/// <summary>
/// Gets the amount of times the event was triggered
/// </summary>
int TriggerCount { get; }
/// <summary>
/// Gets the past participle for this event shown in the UI
/// </summary>
string TriggerPastParticiple { get; }
/// <summary>
/// Gets the type of arguments this event contains
/// </summary>
Type ArgumentsType { get; }
/// <summary>
/// Gets or sets a boolean indicating whether the last 20 events should be tracked
/// <para>Note: setting this to <see langword="false" /> will clear the current history</para>
/// </summary>
bool TrackHistory { get; set; }
/// <summary>
/// Gets the past participle for this event shown in the UI
/// </summary>
string TriggerPastParticiple { get; }
/// <summary>
/// Gets the event arguments of the last time the event was triggered by its base type
/// </summary>
public DataModelEventArgs? LastEventArgumentsUntyped { get; }
/// <summary>
/// Gets or sets a boolean indicating whether the last 20 events should be tracked
/// <para>Note: setting this to <see langword="false" /> will clear the current history</para>
/// </summary>
bool TrackHistory { get; set; }
/// <summary>
/// Gets a list of the last 20 event arguments by their base type.
/// <para>Always empty if <see cref="TrackHistory" /> is <see langword="false" /></para>
/// </summary>
public List<DataModelEventArgs> EventArgumentsHistoryUntyped { get; }
/// <summary>
/// Gets the event arguments of the last time the event was triggered by its base type
/// </summary>
public DataModelEventArgs? LastEventArgumentsUntyped { get; }
/// <summary>
/// Fires when the event is triggered
/// </summary>
event EventHandler EventTriggered;
/// <summary>
/// Gets a list of the last 20 event arguments by their base type.
/// <para>Always empty if <see cref="TrackHistory" /> is <see langword="false" /></para>
/// </summary>
public List<DataModelEventArgs> EventArgumentsHistoryUntyped { get; }
/// <summary>
/// Resets the trigger count and history of this data model event
/// </summary>
void Reset();
/// <summary>
/// Fires when the event is triggered
/// </summary>
event EventHandler EventTriggered;
/// <summary>
/// Updates the event, not required for standard events but included in case your custom event needs to update every
/// tick
/// </summary>
void Update();
}
/// <summary>
/// Resets the trigger count and history of this data model event
/// </summary>
void Reset();
/// <summary>
/// Updates the event, not required for standard events but included in case your custom event needs to update every
/// tick
/// </summary>
void Update();
}
+316 -317
View File
@@ -1,360 +1,359 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Artemis.Core.LayerEffects;
using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Entities.Profile.Abstract;
using SkiaSharp;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a folder in a <see cref="Profile" />
/// </summary>
public sealed class Folder : RenderProfileElement
{
private bool _isExpanded;
/// <summary>
/// Represents a folder in a <see cref="Profile" />
/// Creates a new instance of the <see cref="Folder" /> class and adds itself to the child collection of the provided
/// <paramref name="parent" />
/// </summary>
public sealed class Folder : RenderProfileElement
/// <param name="parent">The parent of the folder</param>
/// <param name="name">The name of the folder</param>
public Folder(ProfileElement parent, string name) : base(parent, parent.Profile)
{
private bool _isExpanded;
FolderEntity = new FolderEntity();
EntityId = Guid.NewGuid();
/// <summary>
/// Creates a new instance of the <see cref="Folder" /> class and adds itself to the child collection of the provided
/// <paramref name="parent" />
/// </summary>
/// <param name="parent">The parent of the folder</param>
/// <param name="name">The name of the folder</param>
public Folder(ProfileElement parent, string name) : base(parent, parent.Profile)
Profile = Parent.Profile;
Name = name;
}
/// <summary>
/// Creates a new instance of the <see cref="Folder" /> class based on the provided folder entity
/// </summary>
/// <param name="profile">The profile the folder belongs to</param>
/// <param name="parent">The parent of the folder</param>
/// <param name="folderEntity">The entity of the folder</param>
public Folder(Profile profile, ProfileElement parent, FolderEntity folderEntity) : base(parent, parent.Profile)
{
FolderEntity = folderEntity;
EntityId = folderEntity.Id;
Profile = profile;
Name = folderEntity.Name;
IsExpanded = folderEntity.IsExpanded;
Suspended = folderEntity.Suspended;
Order = folderEntity.Order;
Load();
}
/// <summary>
/// Gets a boolean indicating whether this folder is at the root of the profile tree
/// </summary>
public bool IsRootFolder => Parent == Profile;
/// <summary>
/// Gets or sets a boolean indicating whether this folder is expanded
/// </summary>
public bool IsExpanded
{
get => _isExpanded;
set => SetAndNotify(ref _isExpanded, value);
}
/// <summary>
/// Gets the folder entity this folder uses for persistent storage
/// </summary>
public FolderEntity FolderEntity { get; internal set; }
/// <inheritdoc />
public override bool ShouldBeEnabled => !Suspended && DisplayConditionMet && !Timeline.IsFinished;
internal override RenderElementEntity RenderElementEntity => FolderEntity;
/// <inheritdoc />
public override List<ILayerProperty> GetAllLayerProperties()
{
List<ILayerProperty> result = new();
foreach (BaseLayerEffect layerEffect in LayerEffects)
{
FolderEntity = new FolderEntity();
EntityId = Guid.NewGuid();
Profile = Parent.Profile;
Name = name;
if (layerEffect.BaseProperties != null)
result.AddRange(layerEffect.BaseProperties.GetAllLayerProperties());
}
/// <summary>
/// Creates a new instance of the <see cref="Folder" /> class based on the provided folder entity
/// </summary>
/// <param name="profile">The profile the folder belongs to</param>
/// <param name="parent">The parent of the folder</param>
/// <param name="folderEntity">The entity of the folder</param>
public Folder(Profile profile, ProfileElement parent, FolderEntity folderEntity) : base(parent, parent.Profile)
return result;
}
/// <inheritdoc />
public override void Update(double deltaTime)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
if (Timeline.IsOverridden)
{
FolderEntity = folderEntity;
EntityId = folderEntity.Id;
Profile = profile;
Name = folderEntity.Name;
IsExpanded = folderEntity.IsExpanded;
Suspended = folderEntity.Suspended;
Order = folderEntity.Order;
Load();
Timeline.ClearOverride();
return;
}
/// <summary>
/// Gets a boolean indicating whether this folder is at the root of the profile tree
/// </summary>
public bool IsRootFolder => Parent == Profile;
/// <summary>
/// Gets or sets a boolean indicating whether this folder is expanded
/// </summary>
public bool IsExpanded
{
get => _isExpanded;
set => SetAndNotify(ref _isExpanded, value);
}
/// <summary>
/// Gets the folder entity this folder uses for persistent storage
/// </summary>
public FolderEntity FolderEntity { get; internal set; }
/// <inheritdoc />
public override bool ShouldBeEnabled => !Suspended && DisplayConditionMet && !Timeline.IsFinished;
internal override RenderElementEntity RenderElementEntity => FolderEntity;
/// <inheritdoc />
public override List<ILayerProperty> GetAllLayerProperties()
{
List<ILayerProperty> result = new();
foreach (BaseLayerEffect layerEffect in LayerEffects)
{
if (layerEffect.BaseProperties != null)
result.AddRange(layerEffect.BaseProperties.GetAllLayerProperties());
}
return result;
}
/// <inheritdoc />
public override void Update(double deltaTime)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
if (Timeline.IsOverridden)
{
Timeline.ClearOverride();
return;
}
UpdateDisplayCondition();
UpdateTimeline(deltaTime);
if (ShouldBeEnabled)
Enable();
else if (Timeline.IsFinished)
Disable();
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
baseLayerEffect.InternalUpdate(Timeline);
foreach (ProfileElement child in Children)
child.Update(deltaTime);
}
/// <inheritdoc />
public override void Reset()
{
UpdateDisplayCondition();
if (DisplayConditionMet)
Timeline.JumpToStart();
else
Timeline.JumpToEnd();
foreach (ProfileElement child in Children)
child.Reset();
}
/// <inheritdoc />
public override void AddChild(ProfileElement child, int? order = null)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
base.AddChild(child, order);
CalculateRenderProperties();
}
/// <inheritdoc />
public override void RemoveChild(ProfileElement child)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
base.RemoveChild(child);
CalculateRenderProperties();
}
/// <summary>
/// Creates a deep copy of the folder
/// </summary>
/// <returns>The newly created copy</returns>
public Folder CreateCopy()
{
if (Parent == null)
throw new ArtemisCoreException("Cannot create a copy of a folder without a parent");
FolderEntity entityCopy = CoreJson.DeserializeObject<FolderEntity>(CoreJson.SerializeObject(FolderEntity, true), true)!;
entityCopy.Id = Guid.NewGuid();
entityCopy.Name += " - Copy";
// TODO Children
return new Folder(Profile, Parent, entityCopy);
}
/// <inheritdoc />
public override string ToString()
{
return $"[Folder] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}";
}
#region Rendering
/// <inheritdoc />
public override void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
// Ensure the folder is ready
if (!Enabled || Path == null)
return;
// No point rendering if all children are disabled
if (!Children.Any(c => c is RenderProfileElement {Enabled: true}))
return;
// If the editor focus is on this folder, discard further focus for children to effectively focus the entire folder and all descendants
if (editorFocus == this)
editorFocus = null;
SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low};
try
{
SKRectI rendererBounds = SKRectI.Create(0, 0, Bounds.Width, Bounds.Height);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
{
if (!baseLayerEffect.Suspended)
baseLayerEffect.InternalPreProcess(canvas, rendererBounds, layerPaint);
}
// No point rendering if the alpha was set to zero by one of the effects
if (layerPaint.Color.Alpha == 0)
return;
canvas.SaveLayer(layerPaint);
canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y);
// Iterate the children in reverse because the first layer must be rendered last to end up on top
for (int index = Children.Count - 1; index > -1; index--)
Children[index].Render(canvas, new SKPointI(Bounds.Left, Bounds.Top), editorFocus);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
{
if (!baseLayerEffect.Suspended)
baseLayerEffect.InternalPostProcess(canvas, rendererBounds, layerPaint);
}
}
finally
{
canvas.Restore();
layerPaint.DisposeSelfAndProperties();
}
Timeline.ClearDelta();
}
#endregion
/// <inheritdoc />
public override void Enable()
{
// No checks here, effects will do their own checks to ensure they never enable twice
// Also not enabling children, they'll enable themselves during their own Update
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
baseLayerEffect.InternalEnable();
Enabled = true;
}
/// <inheritdoc />
public override void Disable()
{
// No checks here, effects will do their own checks to ensure they never disable twice
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
baseLayerEffect.InternalDisable();
// Disabling children since their Update won't get called with their parent disabled
foreach (ProfileElement profileElement in Children)
{
if (profileElement is RenderProfileElement renderProfileElement)
renderProfileElement.Disable();
}
Enabled = false;
}
/// <inheritdoc />
public override void OverrideTimelineAndApply(TimeSpan position)
{
DisplayCondition.OverrideTimeline(position);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
baseLayerEffect.InternalUpdate(Timeline); ;
}
/// <summary>
/// Occurs when a property affecting the rendering properties of this folder has been updated
/// </summary>
public event EventHandler? RenderPropertiesUpdated;
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
Disposed = true;
UpdateDisplayCondition();
UpdateTimeline(deltaTime);
if (ShouldBeEnabled)
Enable();
else if (Timeline.IsFinished)
Disable();
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
base.Dispose(disposing);
}
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
baseLayerEffect.InternalUpdate(Timeline);
internal void CalculateRenderProperties()
foreach (ProfileElement child in Children)
child.Update(deltaTime);
}
/// <inheritdoc />
public override void Reset()
{
UpdateDisplayCondition();
if (DisplayConditionMet)
Timeline.JumpToStart();
else
Timeline.JumpToEnd();
foreach (ProfileElement child in Children)
child.Reset();
}
/// <inheritdoc />
public override void AddChild(ProfileElement child, int? order = null)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
base.AddChild(child, order);
CalculateRenderProperties();
}
/// <inheritdoc />
public override void RemoveChild(ProfileElement child)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
base.RemoveChild(child);
CalculateRenderProperties();
}
/// <summary>
/// Creates a deep copy of the folder
/// </summary>
/// <returns>The newly created copy</returns>
public Folder CreateCopy()
{
if (Parent == null)
throw new ArtemisCoreException("Cannot create a copy of a folder without a parent");
FolderEntity entityCopy = CoreJson.DeserializeObject<FolderEntity>(CoreJson.SerializeObject(FolderEntity, true), true)!;
entityCopy.Id = Guid.NewGuid();
entityCopy.Name += " - Copy";
// TODO Children
return new Folder(Profile, Parent, entityCopy);
}
/// <inheritdoc />
public override string ToString()
{
return $"[Folder] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}";
}
#region Rendering
/// <inheritdoc />
public override void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
// Ensure the folder is ready
if (!Enabled || Path == null)
return;
// No point rendering if all children are disabled
if (!Children.Any(c => c is RenderProfileElement {Enabled: true}))
return;
// If the editor focus is on this folder, discard further focus for children to effectively focus the entire folder and all descendants
if (editorFocus == this)
editorFocus = null;
SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low};
try
{
if (Disposed)
throw new ObjectDisposedException("Folder");
SKPath path = new() {FillType = SKPathFillType.Winding};
foreach (ProfileElement child in Children)
SKRectI rendererBounds = SKRectI.Create(0, 0, Bounds.Width, Bounds.Height);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
{
if (child is RenderProfileElement effectChild && effectChild.Path != null)
path.AddPath(effectChild.Path);
if (!baseLayerEffect.Suspended)
baseLayerEffect.InternalPreProcess(canvas, rendererBounds, layerPaint);
}
Path = path;
// No point rendering if the alpha was set to zero by one of the effects
if (layerPaint.Color.Alpha == 0)
return;
// Folder render properties are based on child paths and thus require an update
if (Parent is Folder folder)
folder.CalculateRenderProperties();
canvas.SaveLayer(layerPaint);
canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y);
OnRenderPropertiesUpdated();
// Iterate the children in reverse because the first layer must be rendered last to end up on top
for (int index = Children.Count - 1; index > -1; index--)
Children[index].Render(canvas, new SKPointI(Bounds.Left, Bounds.Top), editorFocus);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
{
if (!baseLayerEffect.Suspended)
baseLayerEffect.InternalPostProcess(canvas, rendererBounds, layerPaint);
}
}
internal override void Load()
finally
{
Reset();
// Load child folders
foreach (FolderEntity childFolder in Profile.ProfileEntity.Folders.Where(f => f.ParentId == EntityId))
ChildrenList.Add(new Folder(Profile, this, childFolder));
// Load child layers
foreach (LayerEntity childLayer in Profile.ProfileEntity.Layers.Where(f => f.ParentId == EntityId))
ChildrenList.Add(new Layer(Profile, this, childLayer));
// Ensure order integrity, should be unnecessary but no one is perfect specially me
ChildrenList.Sort((a, b) => a.Order.CompareTo(b.Order));
for (int index = 0; index < ChildrenList.Count; index++)
ChildrenList[index].Order = index + 1;
LoadRenderElement();
canvas.Restore();
layerPaint.DisposeSelfAndProperties();
}
internal override void Save()
Timeline.ClearDelta();
}
#endregion
/// <inheritdoc />
public override void Enable()
{
// No checks here, effects will do their own checks to ensure they never enable twice
// Also not enabling children, they'll enable themselves during their own Update
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
baseLayerEffect.InternalEnable();
Enabled = true;
}
/// <inheritdoc />
public override void Disable()
{
// No checks here, effects will do their own checks to ensure they never disable twice
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
baseLayerEffect.InternalDisable();
// Disabling children since their Update won't get called with their parent disabled
foreach (ProfileElement profileElement in Children)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
FolderEntity.Id = EntityId;
FolderEntity.ParentId = Parent?.EntityId ?? new Guid();
FolderEntity.Order = Order;
FolderEntity.Name = Name;
FolderEntity.IsExpanded = IsExpanded;
FolderEntity.Suspended = Suspended;
FolderEntity.ProfileId = Profile.EntityId;
SaveRenderElement();
if (profileElement is RenderProfileElement renderProfileElement)
renderProfileElement.Disable();
}
private void OnRenderPropertiesUpdated()
Enabled = false;
}
/// <inheritdoc />
public override void OverrideTimelineAndApply(TimeSpan position)
{
DisplayCondition.OverrideTimeline(position);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
baseLayerEffect.InternalUpdate(Timeline);
;
}
/// <summary>
/// Occurs when a property affecting the rendering properties of this folder has been updated
/// </summary>
public event EventHandler? RenderPropertiesUpdated;
#region Overrides of BreakableModel
/// <inheritdoc />
public override IEnumerable<IBreakableModel> GetBrokenHierarchy()
{
return LayerEffects.Where(e => e.BrokenState != null);
}
#endregion
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
Disposed = true;
Disable();
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
base.Dispose(disposing);
}
internal void CalculateRenderProperties()
{
if (Disposed)
throw new ObjectDisposedException("Folder");
SKPath path = new() {FillType = SKPathFillType.Winding};
foreach (ProfileElement child in Children)
{
RenderPropertiesUpdated?.Invoke(this, EventArgs.Empty);
if (child is RenderProfileElement effectChild && effectChild.Path != null)
path.AddPath(effectChild.Path);
}
#region Overrides of BreakableModel
Path = path;
/// <inheritdoc />
public override IEnumerable<IBreakableModel> GetBrokenHierarchy()
{
return LayerEffects.Where(e => e.BrokenState != null);
}
// Folder render properties are based on child paths and thus require an update
if (Parent is Folder folder)
folder.CalculateRenderProperties();
#endregion
OnRenderPropertiesUpdated();
}
internal override void Load()
{
Reset();
// Load child folders
foreach (FolderEntity childFolder in Profile.ProfileEntity.Folders.Where(f => f.ParentId == EntityId))
ChildrenList.Add(new Folder(Profile, this, childFolder));
// Load child layers
foreach (LayerEntity childLayer in Profile.ProfileEntity.Layers.Where(f => f.ParentId == EntityId))
ChildrenList.Add(new Layer(Profile, this, childLayer));
// Ensure order integrity, should be unnecessary but no one is perfect specially me
ChildrenList.Sort((a, b) => a.Order.CompareTo(b.Order));
for (int index = 0; index < ChildrenList.Count; index++)
ChildrenList[index].Order = index + 1;
LoadRenderElement();
}
internal override void Save()
{
if (Disposed)
throw new ObjectDisposedException("Folder");
FolderEntity.Id = EntityId;
FolderEntity.ParentId = Parent?.EntityId ?? new Guid();
FolderEntity.Order = Order;
FolderEntity.Name = Name;
FolderEntity.IsExpanded = IsExpanded;
FolderEntity.Suspended = Suspended;
FolderEntity.ProfileId = Profile.EntityId;
SaveRenderElement();
}
private void OnRenderPropertiesUpdated()
{
RenderPropertiesUpdated?.Invoke(this, EventArgs.Empty);
}
}
File diff suppressed because it is too large. Load diff
+188 -187
View File
@@ -6,194 +6,195 @@ using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Entities.Profile.AdaptionHints;
using RGB.NET.Core;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents an adapter that adapts a layer to a certain set of devices using <see cref="IAdaptionHint" />s
/// </summary>
public class LayerAdapter : IStorageModel
{
/// <summary>
/// Represents an adapter that adapts a layer to a certain set of devices using <see cref="IAdaptionHint" />s
/// </summary>
public class LayerAdapter : IStorageModel
private readonly List<IAdaptionHint> _adaptionHints;
internal LayerAdapter(Layer layer)
{
private readonly List<IAdaptionHint> _adaptionHints;
internal LayerAdapter(Layer layer)
{
_adaptionHints = new List<IAdaptionHint>();
Layer = layer;
AdaptionHints = new ReadOnlyCollection<IAdaptionHint>(_adaptionHints);
}
/// <summary>
/// Gets the layer this adapter can adapt
/// </summary>
public Layer Layer { get; }
/// <summary>
/// Gets or sets a list containing the adaption hints used by this adapter
/// </summary>
public ReadOnlyCollection<IAdaptionHint> AdaptionHints { get; set; }
/// <summary>
/// Modifies the layer, adapting it to the provided <paramref name="devices" />
/// </summary>
/// <param name="devices">The devices to adapt the layer to</param>
public void Adapt(List<ArtemisDevice> devices)
{
// Use adaption hints if provided
if (AdaptionHints.Any())
{
foreach (IAdaptionHint adaptionHint in AdaptionHints)
adaptionHint.Apply(Layer, devices);
}
// If there are no hints, try to find matching LEDs anyway
else
{
List<ArtemisLed> availableLeds = devices.SelectMany(d => d.Leds).ToList();
List<ArtemisLed> usedLeds = new();
foreach (LedEntity ledEntity in Layer.LayerEntity.Leds)
{
// TODO: If this is a keyboard LED and the layouts don't match, convert it before looking for it on the devices
LedId ledId = Enum.Parse<LedId>(ledEntity.LedName);
ArtemisLed? led = availableLeds.FirstOrDefault(l => l.RgbLed.Id == ledId);
if (led != null)
{
availableLeds.Remove(led);
usedLeds.Add(led);
}
}
Layer.AddLeds(usedLeds);
}
}
/// <summary>
/// Automatically determine hints for this layer
/// </summary>
public List<IAdaptionHint> DetermineHints(IEnumerable<ArtemisDevice> devices)
{
List<IAdaptionHint> newHints = new();
if (devices.All(DoesLayerCoverDevice))
{
DeviceAdaptionHint hint = new() {DeviceType = RGBDeviceType.All};
Add(hint);
newHints.Add(hint);
}
else
{
// Any fully covered device will add a device adaption hint for that type
foreach (IGrouping<ArtemisDevice, ArtemisLed> deviceLeds in Layer.Leds.GroupBy(l => l.Device))
{
ArtemisDevice device = deviceLeds.Key;
// If there is already an adaption hint for this type, don't add another
if (AdaptionHints.Any(h => h is DeviceAdaptionHint d && d.DeviceType == device.DeviceType))
continue;
if (DoesLayerCoverDevice(device))
{
DeviceAdaptionHint hint = new() {DeviceType = device.DeviceType};
Add(hint);
newHints.Add(hint);
}
}
// Any fully covered category will add a category adaption hint for its category
foreach (DeviceCategory deviceCategory in Enum.GetValues<DeviceCategory>())
{
if (AdaptionHints.Any(h => h is CategoryAdaptionHint c && c.Category == deviceCategory))
continue;
List<ArtemisDevice> categoryDevices = devices.Where(d => d.Categories.Contains(deviceCategory)).ToList();
if (categoryDevices.Any() && categoryDevices.All(DoesLayerCoverDevice))
{
CategoryAdaptionHint hint = new() {Category = deviceCategory};
Add(hint);
newHints.Add(hint);
}
}
}
return newHints;
}
private bool DoesLayerCoverDevice(ArtemisDevice device)
{
return device.Leds.All(l => Layer.Leds.Contains(l));
}
/// <summary>
/// Adds an adaption hint to the adapter.
/// </summary>
/// <param name="adaptionHint">The adaption hint to add.</param>
public void Add(IAdaptionHint adaptionHint)
{
if (_adaptionHints.Contains(adaptionHint))
return;
_adaptionHints.Add(adaptionHint);
AdapterHintAdded?.Invoke(this, new LayerAdapterHintEventArgs(adaptionHint));
}
/// <summary>
/// Removes the first occurrence of a specific adaption hint from the adapter.
/// </summary>
/// <param name="adaptionHint">The adaption hint to remove.</param>
public void Remove(IAdaptionHint adaptionHint)
{
if (_adaptionHints.Remove(adaptionHint))
AdapterHintRemoved?.Invoke(this, new LayerAdapterHintEventArgs(adaptionHint));
}
/// <summary>
/// Removes all adaption hints from the adapter.
/// </summary>
public void Clear()
{
while (_adaptionHints.Any())
Remove(_adaptionHints.First());
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
_adaptionHints.Clear();
// Kind of meh.
// This leaves the adapter responsible for finding the right hint for the right entity, but it's gotta be done somewhere..
foreach (IAdaptionHintEntity hintEntity in Layer.LayerEntity.AdaptionHints)
switch (hintEntity)
{
case DeviceAdaptionHintEntity entity:
Add(new DeviceAdaptionHint(entity));
break;
case CategoryAdaptionHintEntity entity:
Add(new CategoryAdaptionHint(entity));
break;
case KeyboardSectionAdaptionHintEntity entity:
Add(new KeyboardSectionAdaptionHint(entity));
break;
}
}
/// <inheritdoc />
public void Save()
{
Layer.LayerEntity.AdaptionHints.Clear();
foreach (IAdaptionHint adaptionHint in AdaptionHints)
Layer.LayerEntity.AdaptionHints.Add(adaptionHint.GetEntry());
}
#endregion
/// <summary>
/// Occurs whenever a new adapter hint is added to the adapter.
/// </summary>
public event EventHandler<LayerAdapterHintEventArgs>? AdapterHintAdded;
/// <summary>
/// Occurs whenever an adapter hint is removed from the adapter.
/// </summary>
public event EventHandler<LayerAdapterHintEventArgs>? AdapterHintRemoved;
_adaptionHints = new List<IAdaptionHint>();
Layer = layer;
AdaptionHints = new ReadOnlyCollection<IAdaptionHint>(_adaptionHints);
}
/// <summary>
/// Gets the layer this adapter can adapt
/// </summary>
public Layer Layer { get; }
/// <summary>
/// Gets or sets a list containing the adaption hints used by this adapter
/// </summary>
public ReadOnlyCollection<IAdaptionHint> AdaptionHints { get; set; }
/// <summary>
/// Modifies the layer, adapting it to the provided <paramref name="devices" />
/// </summary>
/// <param name="devices">The devices to adapt the layer to</param>
public void Adapt(List<ArtemisDevice> devices)
{
// Use adaption hints if provided
if (AdaptionHints.Any())
{
foreach (IAdaptionHint adaptionHint in AdaptionHints)
adaptionHint.Apply(Layer, devices);
}
// If there are no hints, try to find matching LEDs anyway
else
{
List<ArtemisLed> availableLeds = devices.SelectMany(d => d.Leds).ToList();
List<ArtemisLed> usedLeds = new();
foreach (LedEntity ledEntity in Layer.LayerEntity.Leds)
{
// TODO: If this is a keyboard LED and the layouts don't match, convert it before looking for it on the devices
LedId ledId = Enum.Parse<LedId>(ledEntity.LedName);
ArtemisLed? led = availableLeds.FirstOrDefault(l => l.RgbLed.Id == ledId);
if (led != null)
{
availableLeds.Remove(led);
usedLeds.Add(led);
}
}
Layer.AddLeds(usedLeds);
}
}
/// <summary>
/// Automatically determine hints for this layer
/// </summary>
public List<IAdaptionHint> DetermineHints(IEnumerable<ArtemisDevice> devices)
{
List<IAdaptionHint> newHints = new();
if (devices.All(DoesLayerCoverDevice))
{
DeviceAdaptionHint hint = new() {DeviceType = RGBDeviceType.All};
Add(hint);
newHints.Add(hint);
}
else
{
// Any fully covered device will add a device adaption hint for that type
foreach (IGrouping<ArtemisDevice, ArtemisLed> deviceLeds in Layer.Leds.GroupBy(l => l.Device))
{
ArtemisDevice device = deviceLeds.Key;
// If there is already an adaption hint for this type, don't add another
if (AdaptionHints.Any(h => h is DeviceAdaptionHint d && d.DeviceType == device.DeviceType))
continue;
if (DoesLayerCoverDevice(device))
{
DeviceAdaptionHint hint = new() {DeviceType = device.DeviceType};
Add(hint);
newHints.Add(hint);
}
}
// Any fully covered category will add a category adaption hint for its category
foreach (DeviceCategory deviceCategory in Enum.GetValues<DeviceCategory>())
{
if (AdaptionHints.Any(h => h is CategoryAdaptionHint c && c.Category == deviceCategory))
continue;
List<ArtemisDevice> categoryDevices = devices.Where(d => d.Categories.Contains(deviceCategory)).ToList();
if (categoryDevices.Any() && categoryDevices.All(DoesLayerCoverDevice))
{
CategoryAdaptionHint hint = new() {Category = deviceCategory};
Add(hint);
newHints.Add(hint);
}
}
}
return newHints;
}
/// <summary>
/// Adds an adaption hint to the adapter.
/// </summary>
/// <param name="adaptionHint">The adaption hint to add.</param>
public void Add(IAdaptionHint adaptionHint)
{
if (_adaptionHints.Contains(adaptionHint))
return;
_adaptionHints.Add(adaptionHint);
AdapterHintAdded?.Invoke(this, new LayerAdapterHintEventArgs(adaptionHint));
}
/// <summary>
/// Removes the first occurrence of a specific adaption hint from the adapter.
/// </summary>
/// <param name="adaptionHint">The adaption hint to remove.</param>
public void Remove(IAdaptionHint adaptionHint)
{
if (_adaptionHints.Remove(adaptionHint))
AdapterHintRemoved?.Invoke(this, new LayerAdapterHintEventArgs(adaptionHint));
}
/// <summary>
/// Removes all adaption hints from the adapter.
/// </summary>
public void Clear()
{
while (_adaptionHints.Any())
Remove(_adaptionHints.First());
}
/// <summary>
/// Occurs whenever a new adapter hint is added to the adapter.
/// </summary>
public event EventHandler<LayerAdapterHintEventArgs>? AdapterHintAdded;
/// <summary>
/// Occurs whenever an adapter hint is removed from the adapter.
/// </summary>
public event EventHandler<LayerAdapterHintEventArgs>? AdapterHintRemoved;
private bool DoesLayerCoverDevice(ArtemisDevice device)
{
return device.Leds.All(l => Layer.Leds.Contains(l));
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
_adaptionHints.Clear();
// Kind of meh.
// This leaves the adapter responsible for finding the right hint for the right entity, but it's gotta be done somewhere..
foreach (IAdaptionHintEntity hintEntity in Layer.LayerEntity.AdaptionHints)
{
switch (hintEntity)
{
case DeviceAdaptionHintEntity entity:
Add(new DeviceAdaptionHint(entity));
break;
case CategoryAdaptionHintEntity entity:
Add(new CategoryAdaptionHint(entity));
break;
case KeyboardSectionAdaptionHintEntity entity:
Add(new KeyboardSectionAdaptionHint(entity));
break;
}
}
}
/// <inheritdoc />
public void Save()
{
Layer.LayerEntity.AdaptionHints.Clear();
foreach (IAdaptionHint adaptionHint in AdaptionHints)
Layer.LayerEntity.AdaptionHints.Add(adaptionHint.GetEntry());
}
#endregion
}
@@ -1,37 +1,36 @@
using Artemis.Core.LayerBrushes;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// A reference to a <see cref="LayerBrushDescriptor" />
/// </summary>
public class LayerBrushReference
{
/// <summary>
/// A reference to a <see cref="LayerBrushDescriptor" />
/// Creates a new instance of the <see cref="LayerBrushReference" /> class
/// </summary>
public class LayerBrushReference
public LayerBrushReference()
{
/// <summary>
/// Creates a new instance of the <see cref="LayerBrushReference" /> class
/// </summary>
public LayerBrushReference()
{
}
/// <summary>
/// Creates a new instance of the <see cref="LayerBrushReference" /> class
/// </summary>
/// <param name="descriptor">The descriptor to point the new reference at</param>
public LayerBrushReference(LayerBrushDescriptor descriptor)
{
LayerBrushProviderId = descriptor.Provider.Id;
BrushType = descriptor.LayerBrushType.Name;
}
/// <summary>
/// The ID of the layer brush provided the brush was provided by
/// </summary>
public string? LayerBrushProviderId { get; set; }
/// <summary>
/// The full type name of the brush descriptor
/// </summary>
public string? BrushType { get; set; }
}
/// <summary>
/// Creates a new instance of the <see cref="LayerBrushReference" /> class
/// </summary>
/// <param name="descriptor">The descriptor to point the new reference at</param>
public LayerBrushReference(LayerBrushDescriptor descriptor)
{
LayerBrushProviderId = descriptor.Provider.Id;
BrushType = descriptor.LayerBrushType.Name;
}
/// <summary>
/// The ID of the layer brush provided the brush was provided by
/// </summary>
public string? LayerBrushProviderId { get; set; }
/// <summary>
/// The full type name of the brush descriptor
/// </summary>
public string? BrushType { get; set; }
}
@@ -1,25 +1,24 @@
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a property group on a layer
/// <para>
/// Note: You cannot initialize property groups yourself. If properly placed and annotated, the Artemis core will
/// initialize these for you.
/// </para>
/// </summary>
public abstract class LayerEffectPropertyGroup : LayerPropertyGroup
{
/// <summary>
/// Represents a property group on a layer
/// <para>
/// Note: You cannot initialize property groups yourself. If properly placed and annotated, the Artemis core will
/// initialize these for you.
/// </para>
/// Whether or not this layer effect is enabled
/// </summary>
public abstract class LayerEffectPropertyGroup : LayerPropertyGroup
{
/// <summary>
/// Whether or not this layer effect is enabled
/// </summary>
[PropertyDescription(Name = "Enabled", Description = "Whether or not this layer effect is enabled")]
public BoolLayerProperty IsEnabled { get; set; } = null!;
[PropertyDescription(Name = "Enabled", Description = "Whether or not this layer effect is enabled")]
public BoolLayerProperty IsEnabled { get; set; } = null!;
internal void InitializeIsEnabled()
{
IsEnabled.DefaultValue = true;
if (!IsEnabled.IsLoadedFromStorage)
IsEnabled.SetCurrentValue(true);
}
internal void InitializeIsEnabled()
{
IsEnabled.DefaultValue = true;
if (!IsEnabled.IsLoadedFromStorage)
IsEnabled.SetCurrentValue(true);
}
}
@@ -2,52 +2,51 @@
#pragma warning disable 8618
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents the general properties of a layer
/// </summary>
public class LayerGeneralProperties : LayerPropertyGroup
{
/// <summary>
/// Represents the general properties of a layer
/// The type of brush to use for this layer
/// </summary>
public class LayerGeneralProperties : LayerPropertyGroup
[PropertyDescription(Name = "Brush type", Description = "The type of brush to use for this layer")]
public LayerBrushReferenceLayerProperty BrushReference { get; set; }
/// <summary>
/// The type of shape to draw in this layer
/// </summary>
[PropertyDescription(Name = "Shape type", Description = "The type of shape to draw in this layer")]
public EnumLayerProperty<LayerShapeType> ShapeType { get; set; }
/// <summary>
/// How to blend this layer into the resulting image
/// </summary>
[PropertyDescription(Name = "Blend mode", Description = "How to blend this layer into the resulting image")]
public EnumLayerProperty<SKBlendMode> BlendMode { get; set; }
/// <summary>
/// How the transformation properties are applied to the layer
/// </summary>
[PropertyDescription(Name = "Transform mode", Description = "How the transformation properties are applied to the layer")]
public EnumLayerProperty<LayerTransformMode> TransformMode { get; set; }
/// <inheritdoc />
protected override void PopulateDefaults()
{
/// <summary>
/// The type of brush to use for this layer
/// </summary>
[PropertyDescription(Name = "Brush type", Description = "The type of brush to use for this layer")]
public LayerBrushReferenceLayerProperty BrushReference { get; set; }
/// <summary>
/// The type of shape to draw in this layer
/// </summary>
[PropertyDescription(Name = "Shape type", Description = "The type of shape to draw in this layer")]
public EnumLayerProperty<LayerShapeType> ShapeType { get; set; }
ShapeType.DefaultValue = LayerShapeType.Rectangle;
BlendMode.DefaultValue = SKBlendMode.SrcOver;
}
/// <summary>
/// How to blend this layer into the resulting image
/// </summary>
[PropertyDescription(Name = "Blend mode", Description = "How to blend this layer into the resulting image")]
public EnumLayerProperty<SKBlendMode> BlendMode { get; set; }
/// <inheritdoc />
protected override void EnableProperties()
{
}
/// <summary>
/// How the transformation properties are applied to the layer
/// </summary>
[PropertyDescription(Name = "Transform mode", Description = "How the transformation properties are applied to the layer")]
public EnumLayerProperty<LayerTransformMode> TransformMode { get; set; }
/// <inheritdoc />
protected override void PopulateDefaults()
{
ShapeType.DefaultValue = LayerShapeType.Rectangle;
BlendMode.DefaultValue = SKBlendMode.SrcOver;
}
/// <inheritdoc />
protected override void EnableProperties()
{
}
/// <inheritdoc />
protected override void DisableProperties()
{
}
/// <inheritdoc />
protected override void DisableProperties()
{
}
}
@@ -1,11 +1,10 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents an attribute that marks a layer property to be ignored
/// </summary>
public class LayerPropertyIgnoreAttribute : Attribute
{
/// <summary>
/// Represents an attribute that marks a layer property to be ignored
/// </summary>
public class LayerPropertyIgnoreAttribute : Attribute
{
}
}
@@ -1,55 +1,54 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a description attribute used to decorate layer properties
/// </summary>
public class PropertyDescriptionAttribute : Attribute
{
/// <summary>
/// Represents a description attribute used to decorate layer properties
/// The identifier of this property used for storage, if not set one will be generated property name in code
/// </summary>
public class PropertyDescriptionAttribute : Attribute
{
/// <summary>
/// The identifier of this property used for storage, if not set one will be generated property name in code
/// </summary>
public string? Identifier { get; set; }
public string? Identifier { get; set; }
/// <summary>
/// The user-friendly name for this property, shown in the UI
/// </summary>
public string? Name { get; set; }
/// <summary>
/// The user-friendly name for this property, shown in the UI
/// </summary>
public string? Name { get; set; }
/// <summary>
/// The user-friendly description for this property, shown in the UI
/// </summary>
public string? Description { get; set; }
/// <summary>
/// The user-friendly description for this property, shown in the UI
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Input prefix to show before input elements in the UI
/// </summary>
public string? InputPrefix { get; set; }
/// <summary>
/// Input prefix to show before input elements in the UI
/// </summary>
public string? InputPrefix { get; set; }
/// <summary>
/// Input affix to show behind input elements in the UI
/// </summary>
public string? InputAffix { get; set; }
/// <summary>
/// Input affix to show behind input elements in the UI
/// </summary>
public string? InputAffix { get; set; }
/// <summary>
/// The input drag step size, used in the UI
/// </summary>
public float InputStepSize { get; set; }
/// <summary>
/// The input drag step size, used in the UI
/// </summary>
public float InputStepSize { get; set; }
/// <summary>
/// Minimum input value, only enforced in the UI
/// </summary>
public object? MinInputValue { get; set; }
/// <summary>
/// Minimum input value, only enforced in the UI
/// </summary>
public object? MinInputValue { get; set; }
/// <summary>
/// Maximum input value, only enforced in the UI
/// </summary>
public object? MaxInputValue { get; set; }
/// <summary>
/// Maximum input value, only enforced in the UI
/// </summary>
public object? MaxInputValue { get; set; }
/// <summary>
/// Whether or not keyframes are always disabled
/// </summary>
public bool DisableKeyframes { get; set; }
}
/// <summary>
/// Whether or not keyframes are always disabled
/// </summary>
public bool DisableKeyframes { get; set; }
}
@@ -1,25 +1,25 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a description attribute used to decorate layer property groups
/// </summary>
public class PropertyGroupDescriptionAttribute : Attribute
{
/// <summary>
/// Represents a description attribute used to decorate layer property groups
/// The identifier of this property group used for storage, if not set one will be generated based on the group name in
/// code
/// </summary>
public class PropertyGroupDescriptionAttribute : Attribute
{
/// <summary>
/// The identifier of this property group used for storage, if not set one will be generated based on the group name in code
/// </summary>
public string? Identifier { get; set; }
public string? Identifier { get; set; }
/// <summary>
/// The user-friendly name for this property group, shown in the UI.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// The user-friendly description for this property group, shown in the UI.
/// </summary>
public string? Description { get; set; }
}
/// <summary>
/// The user-friendly name for this property group, shown in the UI.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// The user-friendly description for this property group, shown in the UI.
/// </summary>
public string? Description { get; set; }
}
@@ -1,63 +1,62 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a range between two single-precision floating point numbers
/// </summary>
public readonly struct FloatRange
{
private readonly Random _rand;
/// <summary>
/// Represents a range between two single-precision floating point numbers
/// Creates a new instance of the <see cref="FloatRange" /> class
/// </summary>
public readonly struct FloatRange
/// <param name="start">The start value of the range</param>
/// <param name="end">The end value of the range</param>
public FloatRange(float start, float end)
{
private readonly Random _rand;
Start = start;
End = end;
/// <summary>
/// Creates a new instance of the <see cref="FloatRange" /> class
/// </summary>
/// <param name="start">The start value of the range</param>
/// <param name="end">The end value of the range</param>
public FloatRange(float start, float end)
{
Start = start;
End = end;
_rand = new Random();
}
_rand = new Random();
}
/// <summary>
/// Gets the start value of the range
/// </summary>
public float Start { get; }
/// <summary>
/// Gets the start value of the range
/// </summary>
public float Start { get; }
/// <summary>
/// Gets the end value of the range
/// </summary>
public float End { get; }
/// <summary>
/// Gets the end value of the range
/// </summary>
public float End { get; }
/// <summary>
/// Determines whether the given value is in this range
/// </summary>
/// <param name="value">The value to check</param>
/// <param name="inclusive">
/// Whether the value may be equal to <see cref="Start" /> or <see cref="End" />
/// <para>Defaults to <see langword="true" /></para>
/// </param>
/// <returns></returns>
public bool IsInRange(float value, bool inclusive = true)
{
if (inclusive)
return value >= Start && value <= End;
return value > Start && value < End;
}
/// <summary>
/// Determines whether the given value is in this range
/// </summary>
/// <param name="value">The value to check</param>
/// <param name="inclusive">
/// Whether the value may be equal to <see cref="Start" /> or <see cref="End" />
/// <para>Defaults to <see langword="true" /></para>
/// </param>
/// <returns></returns>
public bool IsInRange(float value, bool inclusive = true)
{
if (inclusive)
return value >= Start && value <= End;
return value > Start && value < End;
}
/// <summary>
/// Returns a pseudo-random value between <see cref="Start" /> and <see cref="End" />
/// </summary>
/// <param name="inclusive">Whether the value may be equal to <see cref="Start" /></param>
/// <returns>The pseudo-random value</returns>
public float GetRandomValue(bool inclusive = true)
{
if (inclusive)
return _rand.Next((int) (Start * 100), (int) (End * 100)) / 100f;
return _rand.Next((int) (Start * 100) + 1, (int) (End * 100)) / 100f;
}
/// <summary>
/// Returns a pseudo-random value between <see cref="Start" /> and <see cref="End" />
/// </summary>
/// <param name="inclusive">Whether the value may be equal to <see cref="Start" /></param>
/// <returns>The pseudo-random value</returns>
public float GetRandomValue(bool inclusive = true)
{
if (inclusive)
return _rand.Next((int) (Start * 100), (int) (End * 100)) / 100f;
return _rand.Next((int) (Start * 100) + 1, (int) (End * 100)) / 100f;
}
}
@@ -2,152 +2,151 @@
using System.Collections.ObjectModel;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a property on a layer. Properties are saved in storage and can optionally be modified from the UI.
/// <para>
/// Note: You cannot initialize layer properties yourself. If properly placed and annotated, the Artemis core will
/// initialize these for you.
/// </para>
/// </summary>
public interface ILayerProperty : IStorageModel, IDisposable
{
/// <summary>
/// Represents a property on a layer. Properties are saved in storage and can optionally be modified from the UI.
/// Gets the description attribute applied to this property
/// </summary>
PropertyDescriptionAttribute PropertyDescription { get; }
/// <summary>
/// Gets the profile element (such as layer or folder) this property is applied to
/// </summary>
RenderProfileElement ProfileElement { get; }
/// <summary>
/// The parent group of this layer property, set after construction
/// </summary>
LayerPropertyGroup LayerPropertyGroup { get; }
/// <summary>
/// Gets or sets whether the property is hidden in the UI
/// </summary>
public bool IsHidden { get; set; }
/// <summary>
/// Gets the data binding of this property
/// </summary>
IDataBinding BaseDataBinding { get; }
/// <summary>
/// Gets a boolean indicating whether the layer has any data binding properties
/// </summary>
public bool HasDataBinding { get; }
/// <summary>
/// Gets a boolean indicating whether data bindings are supported on this type of property
/// </summary>
public bool DataBindingsSupported { get; }
/// <summary>
/// Gets the unique path of the property on the render element
/// </summary>
string Path { get; }
/// <summary>
/// Gets a read-only list of all the keyframes on this layer property
/// </summary>
ReadOnlyCollection<ILayerPropertyKeyframe> UntypedKeyframes { get; }
/// <summary>
/// Gets the type of the property
/// </summary>
Type PropertyType { get; }
/// <summary>
/// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied
/// </summary>
bool IsLoadedFromStorage { get; }
/// <summary>
/// Initializes the layer property
/// <para>
/// Note: You cannot initialize layer properties yourself. If properly placed and annotated, the Artemis core will
/// initialize these for you.
/// Note: This isn't done in the constructor to keep it parameterless which is easier for implementations of
/// <see cref="LayerProperty{T}" />
/// </para>
/// </summary>
public interface ILayerProperty : IStorageModel, IDisposable
{
/// <summary>
/// Gets the description attribute applied to this property
/// </summary>
PropertyDescriptionAttribute PropertyDescription { get; }
void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description);
/// <summary>
/// Gets the profile element (such as layer or folder) this property is applied to
/// </summary>
RenderProfileElement ProfileElement { get; }
/// <summary>
/// Attempts to create a keyframe for this property from the provided entity
/// </summary>
/// <param name="keyframeEntity">The entity representing the keyframe to create</param>
/// <returns>If succeeded the resulting keyframe, otherwise <see langword="null" /></returns>
ILayerPropertyKeyframe? CreateKeyframeFromEntity(KeyframeEntity keyframeEntity);
/// <summary>
/// The parent group of this layer property, set after construction
/// </summary>
LayerPropertyGroup LayerPropertyGroup { get; }
/// <summary>
/// Overrides the property value with the default value
/// </summary>
void ApplyDefaultValue();
/// <summary>
/// Gets or sets whether the property is hidden in the UI
/// </summary>
public bool IsHidden { get; set; }
/// <summary>
/// Gets the data binding of this property
/// </summary>
IDataBinding BaseDataBinding { get; }
/// <summary>
/// Gets a boolean indicating whether the layer has any data binding properties
/// </summary>
public bool HasDataBinding { get; }
/// <summary>
/// Gets a boolean indicating whether data bindings are supported on this type of property
/// </summary>
public bool DataBindingsSupported { get; }
/// <summary>
/// Gets the unique path of the property on the render element
/// </summary>
string Path { get; }
/// <summary>
/// Gets a read-only list of all the keyframes on this layer property
/// </summary>
ReadOnlyCollection<ILayerPropertyKeyframe> UntypedKeyframes { get; }
/// <summary>
/// Gets the type of the property
/// </summary>
Type PropertyType { get; }
/// <summary>
/// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied
/// </summary>
bool IsLoadedFromStorage { get; }
/// <summary>
/// Initializes the layer property
/// <para>
/// Note: This isn't done in the constructor to keep it parameterless which is easier for implementations of
/// <see cref="LayerProperty{T}" />
/// </para>
/// </summary>
void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description);
/// <summary>
/// Attempts to create a keyframe for this property from the provided entity
/// </summary>
/// <param name="keyframeEntity">The entity representing the keyframe to create</param>
/// <returns>If succeeded the resulting keyframe, otherwise <see langword="null" /></returns>
ILayerPropertyKeyframe? CreateKeyframeFromEntity(KeyframeEntity keyframeEntity);
/// <summary>
/// Overrides the property value with the default value
/// </summary>
void ApplyDefaultValue();
/// <summary>
/// Updates the layer properties internal state
/// </summary>
/// <param name="timeline">The timeline to apply to the property</param>
void Update(Timeline timeline);
/// <summary>
/// Updates the layer properties internal state
/// </summary>
/// <param name="timeline">The timeline to apply to the property</param>
void Update(Timeline timeline);
/// <summary>
/// Updates just the data binding instead of the entire layer
/// </summary>
void UpdateDataBinding();
/// <summary>
/// Updates just the data binding instead of the entire layer
/// </summary>
void UpdateDataBinding();
/// <summary>
/// Removes a keyframe from the layer property without knowing it's type.
/// <para>Prefer <see cref="LayerProperty{T}.RemoveKeyframe"/>.</para>
/// </summary>
/// <param name="keyframe"></param>
void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe);
/// <summary>
/// Removes a keyframe from the layer property without knowing it's type.
/// <para>Prefer <see cref="LayerProperty{T}.RemoveKeyframe" />.</para>
/// </summary>
/// <param name="keyframe"></param>
void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe);
/// <summary>
/// Adds a keyframe to the layer property without knowing it's type.
/// <para>Prefer <see cref="LayerProperty{T}.AddKeyframe"/>.</para>
/// </summary>
/// <param name="keyframe"></param>
void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe);
/// <summary>
/// Adds a keyframe to the layer property without knowing it's type.
/// <para>Prefer <see cref="LayerProperty{T}.AddKeyframe" />.</para>
/// </summary>
/// <param name="keyframe"></param>
void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe);
/// <summary>
/// Occurs when the layer property is disposed
/// </summary>
public event EventHandler Disposed;
/// <summary>
/// Occurs when the layer property is disposed
/// </summary>
public event EventHandler Disposed;
/// <summary>
/// Occurs once every frame when the layer property is updated
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? Updated;
/// <summary>
/// Occurs once every frame when the layer property is updated
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? Updated;
/// <summary>
/// Occurs when the current value of the layer property was updated by some form of input
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? CurrentValueSet;
/// <summary>
/// Occurs when the current value of the layer property was updated by some form of input
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? CurrentValueSet;
/// <summary>
/// Occurs when the visibility value of the layer property was updated
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? VisibilityChanged;
/// <summary>
/// Occurs when the visibility value of the layer property was updated
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? VisibilityChanged;
/// <summary>
/// Occurs when keyframes are enabled/disabled
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? KeyframesToggled;
/// <summary>
/// Occurs when keyframes are enabled/disabled
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? KeyframesToggled;
/// <summary>
/// Occurs when a new keyframe was added to the layer property
/// </summary>
public event EventHandler<LayerPropertyKeyframeEventArgs>? KeyframeAdded;
/// <summary>
/// Occurs when a new keyframe was added to the layer property
/// </summary>
public event EventHandler<LayerPropertyKeyframeEventArgs>? KeyframeAdded;
/// <summary>
/// Occurs when a keyframe was removed from the layer property
/// </summary>
public event EventHandler<LayerPropertyKeyframeEventArgs>? KeyframeRemoved;
}
/// <summary>
/// Occurs when a keyframe was removed from the layer property
/// </summary>
public event EventHandler<LayerPropertyKeyframeEventArgs>? KeyframeRemoved;
}
@@ -2,43 +2,42 @@
using System.ComponentModel;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a keyframe on a <see cref="ILayerProperty" /> containing a value and a timestamp
/// </summary>
public interface ILayerPropertyKeyframe : INotifyPropertyChanged
{
/// <summary>
/// Represents a keyframe on a <see cref="ILayerProperty" /> containing a value and a timestamp
/// Gets an untyped reference to the layer property of this keyframe
/// </summary>
public interface ILayerPropertyKeyframe : INotifyPropertyChanged
{
/// <summary>
/// Gets an untyped reference to the layer property of this keyframe
/// </summary>
ILayerProperty UntypedLayerProperty { get; }
ILayerProperty UntypedLayerProperty { get; }
/// <summary>
/// Gets or sets the position of this keyframe in the timeline
/// </summary>
TimeSpan Position { get; set; }
/// <summary>
/// Gets or sets the position of this keyframe in the timeline
/// </summary>
TimeSpan Position { get; set; }
/// <summary>
/// Gets or sets the easing function applied on the value of the keyframe
/// </summary>
Easings.Functions EasingFunction { get; set; }
/// <summary>
/// Gets or sets the easing function applied on the value of the keyframe
/// </summary>
Easings.Functions EasingFunction { get; set; }
/// <summary>
/// Gets the entity this keyframe uses for persistent storage
/// </summary>
KeyframeEntity GetKeyframeEntity();
/// <summary>
/// Gets the entity this keyframe uses for persistent storage
/// </summary>
KeyframeEntity GetKeyframeEntity();
/// <summary>
/// Removes the keyframe from the layer property
/// </summary>
void Remove();
/// <summary>
/// Removes the keyframe from the layer property
/// </summary>
void Remove();
/// <summary>
/// Creates a copy of this keyframe.
/// <para>Note: The copied keyframe is not added to the layer property.</para>
/// </summary>
/// <returns>The resulting copy</returns>
ILayerPropertyKeyframe CreateCopy();
}
/// <summary>
/// Creates a copy of this keyframe.
/// <para>Note: The copied keyframe is not added to the layer property.</para>
/// </summary>
/// <returns>The resulting copy</returns>
ILayerPropertyKeyframe CreateCopy();
}
@@ -1,63 +1,62 @@
using System;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a range between two signed integers
/// </summary>
public readonly struct IntRange
{
private readonly Random _rand;
/// <summary>
/// Represents a range between two signed integers
/// Creates a new instance of the <see cref="IntRange" /> class
/// </summary>
public readonly struct IntRange
/// <param name="start">The start value of the range</param>
/// <param name="end">The end value of the range</param>
public IntRange(int start, int end)
{
private readonly Random _rand;
/// <summary>
/// Creates a new instance of the <see cref="IntRange" /> class
/// </summary>
/// <param name="start">The start value of the range</param>
/// <param name="end">The end value of the range</param>
public IntRange(int start, int end)
{
Start = start;
End = end;
Start = start;
End = end;
_rand = new Random();
}
_rand = new Random();
}
/// <summary>
/// Gets the start value of the range
/// </summary>
public int Start { get; }
/// <summary>
/// Gets the start value of the range
/// </summary>
public int Start { get; }
/// <summary>
/// Gets the end value of the range
/// </summary>
public int End { get; }
/// <summary>
/// Gets the end value of the range
/// </summary>
public int End { get; }
/// <summary>
/// Determines whether the given value is in this range
/// </summary>
/// <param name="value">The value to check</param>
/// <param name="inclusive">
/// Whether the value may be equal to <see cref="Start" /> or <see cref="End" />
/// <para>Defaults to <see langword="true" /></para>
/// </param>
/// <returns></returns>
public bool IsInRange(int value, bool inclusive = true)
{
if (inclusive)
return value >= Start && value <= End;
return value > Start && value < End;
}
/// <summary>
/// Determines whether the given value is in this range
/// </summary>
/// <param name="value">The value to check</param>
/// <param name="inclusive">
/// Whether the value may be equal to <see cref="Start" /> or <see cref="End" />
/// <para>Defaults to <see langword="true" /></para>
/// </param>
/// <returns></returns>
public bool IsInRange(int value, bool inclusive = true)
{
if (inclusive)
return value >= Start && value <= End;
return value > Start && value < End;
}
/// <summary>
/// Returns a pseudo-random value between <see cref="Start" /> and <see cref="End" />
/// </summary>
/// <param name="inclusive">Whether the value may be equal to <see cref="Start" /></param>
/// <returns>The pseudo-random value</returns>
public int GetRandomValue(bool inclusive = true)
{
if (inclusive)
return _rand.Next(Start, End + 1);
return _rand.Next(Start + 1, End);
}
/// <summary>
/// Returns a pseudo-random value between <see cref="Start" /> and <see cref="End" />
/// </summary>
/// <param name="inclusive">Whether the value may be equal to <see cref="Start" /></param>
/// <returns>The pseudo-random value</returns>
public int GetRandomValue(bool inclusive = true)
{
if (inclusive)
return _rand.Next(Start, End + 1);
return _rand.Next(Start + 1, End);
}
}
File diff suppressed because it is too large. Load diff
@@ -1,90 +1,89 @@
using System;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a keyframe on a <see cref="LayerProperty{T}" /> containing a value and a timestamp
/// </summary>
public class LayerPropertyKeyframe<T> : CorePropertyChanged, ILayerPropertyKeyframe
{
private LayerProperty<T> _layerProperty;
private TimeSpan _position;
private T _value;
/// <summary>
/// Represents a keyframe on a <see cref="LayerProperty{T}" /> containing a value and a timestamp
/// Creates a new instance of the <see cref="LayerPropertyKeyframe{T}" /> class
/// </summary>
public class LayerPropertyKeyframe<T> : CorePropertyChanged, ILayerPropertyKeyframe
/// <param name="value">The value of the keyframe</param>
/// <param name="position">The position of this keyframe in the timeline</param>
/// <param name="easingFunction">The easing function applied on the value of the keyframe</param>
/// <param name="layerProperty">The layer property this keyframe is applied to</param>
public LayerPropertyKeyframe(T value, TimeSpan position, Easings.Functions easingFunction, LayerProperty<T> layerProperty)
{
private LayerProperty<T> _layerProperty;
private TimeSpan _position;
private T _value;
_position = position;
_layerProperty = layerProperty;
_value = value;
/// <summary>
/// Creates a new instance of the <see cref="LayerPropertyKeyframe{T}" /> class
/// </summary>
/// <param name="value">The value of the keyframe</param>
/// <param name="position">The position of this keyframe in the timeline</param>
/// <param name="easingFunction">The easing function applied on the value of the keyframe</param>
/// <param name="layerProperty">The layer property this keyframe is applied to</param>
public LayerPropertyKeyframe(T value, TimeSpan position, Easings.Functions easingFunction, LayerProperty<T> layerProperty)
EasingFunction = easingFunction;
}
/// <summary>
/// The layer property this keyframe is applied to
/// </summary>
public LayerProperty<T> LayerProperty
{
get => _layerProperty;
internal set => SetAndNotify(ref _layerProperty, value);
}
/// <summary>
/// The value of this keyframe
/// </summary>
public T Value
{
get => _value;
set => SetAndNotify(ref _value, value);
}
/// <inheritdoc />
public ILayerProperty UntypedLayerProperty => LayerProperty;
/// <inheritdoc />
public TimeSpan Position
{
get => _position;
set
{
_position = position;
_layerProperty = layerProperty;
_value = value;
EasingFunction = easingFunction;
}
/// <summary>
/// The layer property this keyframe is applied to
/// </summary>
public LayerProperty<T> LayerProperty
{
get => _layerProperty;
internal set => SetAndNotify(ref _layerProperty, value);
}
/// <summary>
/// The value of this keyframe
/// </summary>
public T Value
{
get => _value;
set => SetAndNotify(ref _value, value);
}
/// <inheritdoc />
public ILayerProperty UntypedLayerProperty => LayerProperty;
/// <inheritdoc />
public TimeSpan Position
{
get => _position;
set
{
SetAndNotify(ref _position, value);
LayerProperty.SortKeyframes();
LayerProperty.ReapplyUpdate();
}
}
/// <inheritdoc />
public Easings.Functions EasingFunction { get; set; }
/// <inheritdoc />
public KeyframeEntity GetKeyframeEntity()
{
return new KeyframeEntity
{
Value = CoreJson.SerializeObject(Value),
Position = Position,
EasingFunction = (int) EasingFunction
};
}
/// <inheritdoc />
public void Remove()
{
LayerProperty.RemoveKeyframe(this);
}
/// <inheritdoc />
public ILayerPropertyKeyframe CreateCopy()
{
return new LayerPropertyKeyframe<T>(Value, Position, EasingFunction, LayerProperty);
SetAndNotify(ref _position, value);
LayerProperty.SortKeyframes();
LayerProperty.ReapplyUpdate();
}
}
/// <inheritdoc />
public Easings.Functions EasingFunction { get; set; }
/// <inheritdoc />
public KeyframeEntity GetKeyframeEntity()
{
return new KeyframeEntity
{
Value = CoreJson.SerializeObject(Value),
Position = Position,
EasingFunction = (int) EasingFunction
};
}
/// <inheritdoc />
public void Remove()
{
LayerProperty.RemoveKeyframe(this);
}
/// <inheritdoc />
public ILayerPropertyKeyframe CreateCopy()
{
return new LayerPropertyKeyframe<T>(Value, Position, EasingFunction, LayerProperty);
}
}
@@ -79,7 +79,8 @@ public sealed class LayerPropertyPreview<T> : IDisposable
}
Property.SetCurrentValue(OriginalValue, Time);
return !Equals(OriginalValue, PreviewValue); ;
return !Equals(OriginalValue, PreviewValue);
;
}
/// <summary>
@@ -3,346 +3,343 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.Storage.Entities.Profile;
using Humanizer;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a property group on a layer
/// <para>
/// Note: You cannot initialize property groups yourself. If properly placed and annotated, the Artemis core will
/// initialize these for you.
/// </para>
/// </summary>
public abstract class LayerPropertyGroup : IDisposable
{
private readonly List<ILayerProperty> _layerProperties;
private readonly List<LayerPropertyGroup> _layerPropertyGroups;
private bool _disposed;
private bool _isHidden;
/// <summary>
/// Represents a property group on a layer
/// <para>
/// Note: You cannot initialize property groups yourself. If properly placed and annotated, the Artemis core will
/// initialize these for you.
/// </para>
/// A base constructor for a <see cref="LayerPropertyGroup" />
/// </summary>
public abstract class LayerPropertyGroup : IDisposable
protected LayerPropertyGroup()
{
private readonly List<ILayerProperty> _layerProperties;
private readonly List<LayerPropertyGroup> _layerPropertyGroups;
private bool _disposed;
private bool _isHidden;
// These are set right after construction to keep the constructor (and inherited constructs) clean
ProfileElement = null!;
GroupDescription = null!;
Path = "";
/// <summary>
/// A base constructor for a <see cref="LayerPropertyGroup" />
/// </summary>
protected LayerPropertyGroup()
_layerProperties = new List<ILayerProperty>();
_layerPropertyGroups = new List<LayerPropertyGroup>();
LayerProperties = new ReadOnlyCollection<ILayerProperty>(_layerProperties);
LayerPropertyGroups = new ReadOnlyCollection<LayerPropertyGroup>(_layerPropertyGroups);
}
/// <summary>
/// Gets the profile element (such as layer or folder) this group is associated with
/// </summary>
public RenderProfileElement ProfileElement { get; private set; }
/// <summary>
/// Gets the description of this group
/// </summary>
public PropertyGroupDescriptionAttribute GroupDescription { get; private set; }
/// <summary>
/// The parent group of this group
/// </summary>
[LayerPropertyIgnore] // Ignore the parent when selecting child groups
public LayerPropertyGroup? Parent { get; internal set; }
/// <summary>
/// Gets the unique path of the property on the render element
/// </summary>
public string Path { get; private set; }
/// <summary>
/// Gets whether this property groups properties are all initialized
/// </summary>
public bool PropertiesInitialized { get; private set; }
/// <summary>
/// Gets or sets whether the property is hidden in the UI
/// </summary>
public bool IsHidden
{
get => _isHidden;
set
{
// These are set right after construction to keep the constructor (and inherited constructs) clean
ProfileElement = null!;
GroupDescription = null!;
Path = "";
_layerProperties = new List<ILayerProperty>();
_layerPropertyGroups = new List<LayerPropertyGroup>();
LayerProperties = new ReadOnlyCollection<ILayerProperty>(_layerProperties);
LayerPropertyGroups = new ReadOnlyCollection<LayerPropertyGroup>(_layerPropertyGroups);
}
/// <summary>
/// Gets the profile element (such as layer or folder) this group is associated with
/// </summary>
public RenderProfileElement ProfileElement { get; private set; }
/// <summary>
/// Gets the description of this group
/// </summary>
public PropertyGroupDescriptionAttribute GroupDescription { get; private set; }
/// <summary>
/// The parent group of this group
/// </summary>
[LayerPropertyIgnore] // Ignore the parent when selecting child groups
public LayerPropertyGroup? Parent { get; internal set; }
/// <summary>
/// Gets the unique path of the property on the render element
/// </summary>
public string Path { get; private set; }
/// <summary>
/// Gets whether this property groups properties are all initialized
/// </summary>
public bool PropertiesInitialized { get; private set; }
/// <summary>
/// Gets or sets whether the property is hidden in the UI
/// </summary>
public bool IsHidden
{
get => _isHidden;
set
{
_isHidden = value;
OnVisibilityChanged();
}
}
/// <summary>
/// Gets the entity this property group uses for persistent storage
/// </summary>
public PropertyGroupEntity? PropertyGroupEntity { get; internal set; }
/// <summary>
/// A list of all layer properties in this group
/// </summary>
public ReadOnlyCollection<ILayerProperty> LayerProperties { get; }
/// <summary>
/// A list of al child groups in this group
/// </summary>
public ReadOnlyCollection<LayerPropertyGroup> LayerPropertyGroups { get; }
/// <summary>
/// Recursively gets all layer properties on this group and any subgroups
/// </summary>
public IReadOnlyCollection<ILayerProperty> GetAllLayerProperties()
{
if (_disposed)
throw new ObjectDisposedException("LayerPropertyGroup");
if (!PropertiesInitialized)
return new List<ILayerProperty>();
List<ILayerProperty> result = new(LayerProperties);
foreach (LayerPropertyGroup layerPropertyGroup in LayerPropertyGroups)
result.AddRange(layerPropertyGroup.GetAllLayerProperties());
return result.AsReadOnly();
}
/// <summary>
/// Applies the default value to all layer properties
/// </summary>
public void ResetAllLayerProperties()
{
foreach (ILayerProperty layerProperty in GetAllLayerProperties())
layerProperty.ApplyDefaultValue();
}
/// <summary>
/// Occurs when the property group has initialized all its children
/// </summary>
public event EventHandler? PropertyGroupInitialized;
/// <summary>
/// Occurs when one of the current value of one of the layer properties in this group changes by some form of input
/// <para>Note: Will not trigger on properties in child groups</para>
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? LayerPropertyOnCurrentValueSet;
/// <summary>
/// Occurs when the <see cref="IsHidden" /> value of the layer property was updated
/// </summary>
public event EventHandler? VisibilityChanged;
/// <summary>
/// Called before property group is activated to allow you to populate <see cref="LayerProperty{T}.DefaultValue" /> on
/// the properties you want
/// </summary>
protected abstract void PopulateDefaults();
/// <summary>
/// Called when the property group is activated
/// </summary>
protected abstract void EnableProperties();
/// <summary>
/// Called when the property group is deactivated (either the profile unloaded or the related brush/effect was removed)
/// </summary>
protected abstract void DisableProperties();
/// <summary>
/// Called when the property group and all its layer properties have been initialized
/// </summary>
protected virtual void OnPropertyGroupInitialized()
{
PropertyGroupInitialized?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_disposed = true;
DisableProperties();
foreach (ILayerProperty layerProperty in _layerProperties)
layerProperty.Dispose();
foreach (LayerPropertyGroup layerPropertyGroup in _layerPropertyGroups)
layerPropertyGroup.Dispose();
}
}
internal void Initialize(RenderProfileElement profileElement, LayerPropertyGroup? parent, PropertyGroupDescriptionAttribute groupDescription, PropertyGroupEntity? propertyGroupEntity)
{
if (groupDescription.Identifier == null)
throw new ArtemisCoreException("Can't initialize a property group without an identifier");
// Doubt this will happen but let's make sure
if (PropertiesInitialized)
throw new ArtemisCoreException("Layer property group already initialized, wut");
ProfileElement = profileElement;
Parent = parent;
GroupDescription = groupDescription;
PropertyGroupEntity = propertyGroupEntity ?? new PropertyGroupEntity {Identifier = groupDescription.Identifier};
Path = parent != null ? parent.Path + "." + groupDescription.Identifier : groupDescription.Identifier;
// Get all properties implementing ILayerProperty or LayerPropertyGroup
foreach (PropertyInfo propertyInfo in GetType().GetProperties())
{
if (Attribute.IsDefined(propertyInfo, typeof(LayerPropertyIgnoreAttribute)))
continue;
if (typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType))
{
PropertyDescriptionAttribute? propertyDescription =
(PropertyDescriptionAttribute?) Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyDescriptionAttribute));
InitializeProperty(propertyInfo, propertyDescription ?? new PropertyDescriptionAttribute());
}
else if (typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType))
{
PropertyGroupDescriptionAttribute? propertyGroupDescription =
(PropertyGroupDescriptionAttribute?) Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyGroupDescriptionAttribute));
InitializeChildGroup(propertyInfo, propertyGroupDescription ?? new PropertyGroupDescriptionAttribute());
}
}
// Request the property group to populate defaults
PopulateDefaults();
// Load the layer properties after defaults have been applied
foreach (ILayerProperty layerProperty in _layerProperties)
layerProperty.Load();
EnableProperties();
PropertiesInitialized = true;
OnPropertyGroupInitialized();
}
internal void ApplyToEntity()
{
if (!PropertiesInitialized || PropertyGroupEntity == null)
return;
foreach (ILayerProperty layerProperty in LayerProperties)
layerProperty.Save();
PropertyGroupEntity.PropertyGroups.Clear();
foreach (LayerPropertyGroup layerPropertyGroup in LayerPropertyGroups)
{
layerPropertyGroup.ApplyToEntity();
PropertyGroupEntity.PropertyGroups.Add(layerPropertyGroup.PropertyGroupEntity);
}
}
internal void Update(Timeline timeline)
{
foreach (ILayerProperty layerProperty in LayerProperties)
layerProperty.Update(timeline);
foreach (LayerPropertyGroup layerPropertyGroup in LayerPropertyGroups)
layerPropertyGroup.Update(timeline);
}
internal void MoveLayerProperty(ILayerProperty layerProperty, int index)
{
if (!_layerProperties.Contains(layerProperty))
return;
_layerProperties.Remove(layerProperty);
_layerProperties.Insert(index, layerProperty);
}
internal virtual void OnVisibilityChanged()
{
VisibilityChanged?.Invoke(this, EventArgs.Empty);
}
internal virtual void OnLayerPropertyOnCurrentValueSet(LayerPropertyEventArgs e)
{
Parent?.OnLayerPropertyOnCurrentValueSet(e);
LayerPropertyOnCurrentValueSet?.Invoke(this, e);
}
private void InitializeProperty(PropertyInfo propertyInfo, PropertyDescriptionAttribute propertyDescription)
{
// Ensure the description has an identifier and name, if not this is a good point to set it based on the property info
if (string.IsNullOrWhiteSpace(propertyDescription.Identifier))
propertyDescription.Identifier = propertyInfo.Name;
if (string.IsNullOrWhiteSpace(propertyDescription.Name))
propertyDescription.Name = propertyInfo.Name.Humanize();
if (!typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType))
throw new ArtemisPluginException($"Property with PropertyDescription attribute must be of type ILayerProperty: {propertyDescription.Identifier}");
if (Activator.CreateInstance(propertyInfo.PropertyType, true) is not ILayerProperty instance)
throw new ArtemisPluginException($"Failed to create instance of layer property: {propertyDescription.Identifier}");
PropertyEntity entity = GetPropertyEntity(propertyDescription.Identifier, out bool fromStorage);
instance.Initialize(ProfileElement, this, entity, fromStorage, propertyDescription);
propertyInfo.SetValue(this, instance);
_layerProperties.Add(instance);
}
private void InitializeChildGroup(PropertyInfo propertyInfo, PropertyGroupDescriptionAttribute propertyGroupDescription)
{
// Ensure the description has an identifier and name name, if not this is a good point to set it based on the property info
if (string.IsNullOrWhiteSpace(propertyGroupDescription.Identifier))
propertyGroupDescription.Identifier = propertyInfo.Name;
if (string.IsNullOrWhiteSpace(propertyGroupDescription.Name))
propertyGroupDescription.Name = propertyInfo.Name.Humanize();
if (!typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType))
throw new ArtemisPluginException($"Property with PropertyGroupDescription attribute must be of type LayerPropertyGroup: {propertyGroupDescription.Identifier}");
if (!(Activator.CreateInstance(propertyInfo.PropertyType) is LayerPropertyGroup instance))
throw new ArtemisPluginException($"Failed to create instance of layer property group: {propertyGroupDescription.Identifier}");
PropertyGroupEntity entity = GetPropertyGroupEntity(propertyGroupDescription.Identifier);
instance.Initialize(ProfileElement, this, propertyGroupDescription, entity);
propertyInfo.SetValue(this, instance);
_layerPropertyGroups.Add(instance);
}
private PropertyEntity GetPropertyEntity(string identifier, out bool fromStorage)
{
if (PropertyGroupEntity == null)
throw new ArtemisCoreException($"Can't execute {nameof(GetPropertyEntity)} without {nameof(PropertyGroupEntity)} being setup");
PropertyEntity? entity = PropertyGroupEntity.Properties.FirstOrDefault(p => p.Identifier == identifier);
fromStorage = entity != null;
if (entity == null)
{
entity = new PropertyEntity {Identifier = identifier};
PropertyGroupEntity.Properties.Add(entity);
}
return entity;
}
private PropertyGroupEntity GetPropertyGroupEntity(string identifier)
{
if (PropertyGroupEntity == null)
throw new ArtemisCoreException($"Can't execute {nameof(GetPropertyGroupEntity)} without {nameof(PropertyGroupEntity)} being setup");
return PropertyGroupEntity.PropertyGroups.FirstOrDefault(g => g.Identifier == identifier) ?? new PropertyGroupEntity() {Identifier = identifier};
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
_isHidden = value;
OnVisibilityChanged();
}
}
/// <summary>
/// Gets the entity this property group uses for persistent storage
/// </summary>
public PropertyGroupEntity? PropertyGroupEntity { get; internal set; }
/// <summary>
/// A list of all layer properties in this group
/// </summary>
public ReadOnlyCollection<ILayerProperty> LayerProperties { get; }
/// <summary>
/// A list of al child groups in this group
/// </summary>
public ReadOnlyCollection<LayerPropertyGroup> LayerPropertyGroups { get; }
/// <summary>
/// Recursively gets all layer properties on this group and any subgroups
/// </summary>
public IReadOnlyCollection<ILayerProperty> GetAllLayerProperties()
{
if (_disposed)
throw new ObjectDisposedException("LayerPropertyGroup");
if (!PropertiesInitialized)
return new List<ILayerProperty>();
List<ILayerProperty> result = new(LayerProperties);
foreach (LayerPropertyGroup layerPropertyGroup in LayerPropertyGroups)
result.AddRange(layerPropertyGroup.GetAllLayerProperties());
return result.AsReadOnly();
}
/// <summary>
/// Applies the default value to all layer properties
/// </summary>
public void ResetAllLayerProperties()
{
foreach (ILayerProperty layerProperty in GetAllLayerProperties())
layerProperty.ApplyDefaultValue();
}
/// <summary>
/// Occurs when the property group has initialized all its children
/// </summary>
public event EventHandler? PropertyGroupInitialized;
/// <summary>
/// Occurs when one of the current value of one of the layer properties in this group changes by some form of input
/// <para>Note: Will not trigger on properties in child groups</para>
/// </summary>
public event EventHandler<LayerPropertyEventArgs>? LayerPropertyOnCurrentValueSet;
/// <summary>
/// Occurs when the <see cref="IsHidden" /> value of the layer property was updated
/// </summary>
public event EventHandler? VisibilityChanged;
/// <summary>
/// Called before property group is activated to allow you to populate <see cref="LayerProperty{T}.DefaultValue" /> on
/// the properties you want
/// </summary>
protected abstract void PopulateDefaults();
/// <summary>
/// Called when the property group is activated
/// </summary>
protected abstract void EnableProperties();
/// <summary>
/// Called when the property group is deactivated (either the profile unloaded or the related brush/effect was removed)
/// </summary>
protected abstract void DisableProperties();
/// <summary>
/// Called when the property group and all its layer properties have been initialized
/// </summary>
protected virtual void OnPropertyGroupInitialized()
{
PropertyGroupInitialized?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_disposed = true;
DisableProperties();
foreach (ILayerProperty layerProperty in _layerProperties)
layerProperty.Dispose();
foreach (LayerPropertyGroup layerPropertyGroup in _layerPropertyGroups)
layerPropertyGroup.Dispose();
}
}
internal void Initialize(RenderProfileElement profileElement, LayerPropertyGroup? parent, PropertyGroupDescriptionAttribute groupDescription, PropertyGroupEntity? propertyGroupEntity)
{
if (groupDescription.Identifier == null)
throw new ArtemisCoreException("Can't initialize a property group without an identifier");
// Doubt this will happen but let's make sure
if (PropertiesInitialized)
throw new ArtemisCoreException("Layer property group already initialized, wut");
ProfileElement = profileElement;
Parent = parent;
GroupDescription = groupDescription;
PropertyGroupEntity = propertyGroupEntity ?? new PropertyGroupEntity {Identifier = groupDescription.Identifier};
Path = parent != null ? parent.Path + "." + groupDescription.Identifier : groupDescription.Identifier;
// Get all properties implementing ILayerProperty or LayerPropertyGroup
foreach (PropertyInfo propertyInfo in GetType().GetProperties())
{
if (Attribute.IsDefined(propertyInfo, typeof(LayerPropertyIgnoreAttribute)))
continue;
if (typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType))
{
PropertyDescriptionAttribute? propertyDescription =
(PropertyDescriptionAttribute?) Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyDescriptionAttribute));
InitializeProperty(propertyInfo, propertyDescription ?? new PropertyDescriptionAttribute());
}
else if (typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType))
{
PropertyGroupDescriptionAttribute? propertyGroupDescription =
(PropertyGroupDescriptionAttribute?) Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyGroupDescriptionAttribute));
InitializeChildGroup(propertyInfo, propertyGroupDescription ?? new PropertyGroupDescriptionAttribute());
}
}
// Request the property group to populate defaults
PopulateDefaults();
// Load the layer properties after defaults have been applied
foreach (ILayerProperty layerProperty in _layerProperties)
layerProperty.Load();
EnableProperties();
PropertiesInitialized = true;
OnPropertyGroupInitialized();
}
internal void ApplyToEntity()
{
if (!PropertiesInitialized || PropertyGroupEntity == null)
return;
foreach (ILayerProperty layerProperty in LayerProperties)
layerProperty.Save();
PropertyGroupEntity.PropertyGroups.Clear();
foreach (LayerPropertyGroup layerPropertyGroup in LayerPropertyGroups)
{
layerPropertyGroup.ApplyToEntity();
PropertyGroupEntity.PropertyGroups.Add(layerPropertyGroup.PropertyGroupEntity);
}
}
internal void Update(Timeline timeline)
{
foreach (ILayerProperty layerProperty in LayerProperties)
layerProperty.Update(timeline);
foreach (LayerPropertyGroup layerPropertyGroup in LayerPropertyGroups)
layerPropertyGroup.Update(timeline);
}
internal void MoveLayerProperty(ILayerProperty layerProperty, int index)
{
if (!_layerProperties.Contains(layerProperty))
return;
_layerProperties.Remove(layerProperty);
_layerProperties.Insert(index, layerProperty);
}
internal virtual void OnVisibilityChanged()
{
VisibilityChanged?.Invoke(this, EventArgs.Empty);
}
internal virtual void OnLayerPropertyOnCurrentValueSet(LayerPropertyEventArgs e)
{
Parent?.OnLayerPropertyOnCurrentValueSet(e);
LayerPropertyOnCurrentValueSet?.Invoke(this, e);
}
private void InitializeProperty(PropertyInfo propertyInfo, PropertyDescriptionAttribute propertyDescription)
{
// Ensure the description has an identifier and name, if not this is a good point to set it based on the property info
if (string.IsNullOrWhiteSpace(propertyDescription.Identifier))
propertyDescription.Identifier = propertyInfo.Name;
if (string.IsNullOrWhiteSpace(propertyDescription.Name))
propertyDescription.Name = propertyInfo.Name.Humanize();
if (!typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType))
throw new ArtemisPluginException($"Property with PropertyDescription attribute must be of type ILayerProperty: {propertyDescription.Identifier}");
if (Activator.CreateInstance(propertyInfo.PropertyType, true) is not ILayerProperty instance)
throw new ArtemisPluginException($"Failed to create instance of layer property: {propertyDescription.Identifier}");
PropertyEntity entity = GetPropertyEntity(propertyDescription.Identifier, out bool fromStorage);
instance.Initialize(ProfileElement, this, entity, fromStorage, propertyDescription);
propertyInfo.SetValue(this, instance);
_layerProperties.Add(instance);
}
private void InitializeChildGroup(PropertyInfo propertyInfo, PropertyGroupDescriptionAttribute propertyGroupDescription)
{
// Ensure the description has an identifier and name name, if not this is a good point to set it based on the property info
if (string.IsNullOrWhiteSpace(propertyGroupDescription.Identifier))
propertyGroupDescription.Identifier = propertyInfo.Name;
if (string.IsNullOrWhiteSpace(propertyGroupDescription.Name))
propertyGroupDescription.Name = propertyInfo.Name.Humanize();
if (!typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType))
throw new ArtemisPluginException($"Property with PropertyGroupDescription attribute must be of type LayerPropertyGroup: {propertyGroupDescription.Identifier}");
if (!(Activator.CreateInstance(propertyInfo.PropertyType) is LayerPropertyGroup instance))
throw new ArtemisPluginException($"Failed to create instance of layer property group: {propertyGroupDescription.Identifier}");
PropertyGroupEntity entity = GetPropertyGroupEntity(propertyGroupDescription.Identifier);
instance.Initialize(ProfileElement, this, propertyGroupDescription, entity);
propertyInfo.SetValue(this, instance);
_layerPropertyGroups.Add(instance);
}
private PropertyEntity GetPropertyEntity(string identifier, out bool fromStorage)
{
if (PropertyGroupEntity == null)
throw new ArtemisCoreException($"Can't execute {nameof(GetPropertyEntity)} without {nameof(PropertyGroupEntity)} being setup");
PropertyEntity? entity = PropertyGroupEntity.Properties.FirstOrDefault(p => p.Identifier == identifier);
fromStorage = entity != null;
if (entity == null)
{
entity = new PropertyEntity {Identifier = identifier};
PropertyGroupEntity.Properties.Add(entity);
}
return entity;
}
private PropertyGroupEntity GetPropertyGroupEntity(string identifier)
{
if (PropertyGroupEntity == null)
throw new ArtemisCoreException($"Can't execute {nameof(GetPropertyGroupEntity)} without {nameof(PropertyGroupEntity)} being setup");
return PropertyGroupEntity.PropertyGroups.FirstOrDefault(g => g.Identifier == identifier) ?? new PropertyGroupEntity {Identifier = identifier};
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
@@ -1,22 +1,21 @@
using SkiaSharp;
namespace Artemis.Core
{
/// <summary>
/// Represents an ellipse layer shape
/// </summary>
public class EllipseShape : LayerShape
{
internal EllipseShape(Layer layer) : base(layer)
{
}
namespace Artemis.Core;
/// <inheritdoc />
public override void CalculateRenderProperties()
{
SKPath path = new();
path.AddOval(SKRect.Create(Layer.Bounds.Width, Layer.Bounds.Height));
Path = path;
}
/// <summary>
/// Represents an ellipse layer shape
/// </summary>
public class EllipseShape : LayerShape
{
internal EllipseShape(Layer layer) : base(layer)
{
}
/// <inheritdoc />
public override void CalculateRenderProperties()
{
SKPath path = new();
path.AddOval(SKRect.Create(Layer.Bounds.Width, Layer.Bounds.Height));
Path = path;
}
}
@@ -1,30 +1,29 @@
using SkiaSharp;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents the shape of a layer
/// </summary>
public abstract class LayerShape
{
/// <summary>
/// Represents the shape of a layer
/// </summary>
public abstract class LayerShape
internal LayerShape(Layer layer)
{
internal LayerShape(Layer layer)
{
Layer = layer;
}
/// <summary>
/// The layer this shape is attached to
/// </summary>
public Layer Layer { get; set; }
/// <summary>
/// Gets a the path outlining the shape
/// </summary>
public SKPath? Path { get; protected set; }
/// <summary>
/// Calculates the <see cref="Path" />
/// </summary>
public abstract void CalculateRenderProperties();
Layer = layer;
}
/// <summary>
/// The layer this shape is attached to
/// </summary>
public Layer Layer { get; set; }
/// <summary>
/// Gets a the path outlining the shape
/// </summary>
public SKPath? Path { get; protected set; }
/// <summary>
/// Calculates the <see cref="Path" />
/// </summary>
public abstract void CalculateRenderProperties();
}
@@ -1,22 +1,21 @@
using SkiaSharp;
namespace Artemis.Core
{
/// <summary>
/// Represents a rectangular layer shape
/// </summary>
public class RectangleShape : LayerShape
{
internal RectangleShape(Layer layer) : base(layer)
{
}
namespace Artemis.Core;
/// <inheritdoc />
public override void CalculateRenderProperties()
{
SKPath path = new();
path.AddRect(SKRect.Create(Layer.Bounds.Width, Layer.Bounds.Height));
Path = path;
}
/// <summary>
/// Represents a rectangular layer shape
/// </summary>
public class RectangleShape : LayerShape
{
internal RectangleShape(Layer layer) : base(layer)
{
}
/// <inheritdoc />
public override void CalculateRenderProperties()
{
SKPath path = new();
path.AddRect(SKRect.Create(Layer.Bounds.Width, Layer.Bounds.Height));
Path = path;
}
}
@@ -2,60 +2,59 @@
#pragma warning disable 8618
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents the transform properties of a layer
/// </summary>
public class LayerTransformProperties : LayerPropertyGroup
{
/// <summary>
/// Represents the transform properties of a layer
/// The point at which the shape is attached to its position
/// </summary>
public class LayerTransformProperties : LayerPropertyGroup
[PropertyDescription(Description = "The point at which the shape is attached to its position", InputAffix = "%", InputStepSize = 0.001f)]
public SKPointLayerProperty AnchorPoint { get; set; }
/// <summary>
/// The position of the shape
/// </summary>
[PropertyDescription(Description = "The position of the shape", InputAffix = "%", InputStepSize = 0.001f)]
public SKPointLayerProperty Position { get; set; }
/// <summary>
/// The scale of the shape
/// </summary>
[PropertyDescription(Description = "The scale of the shape", InputAffix = "%", MinInputValue = 0f)]
public SKSizeLayerProperty Scale { get; set; }
/// <summary>
/// The rotation of the shape in degree
/// </summary>
[PropertyDescription(Description = "The rotation of the shape in degrees", InputAffix = "°", InputStepSize = 0.5f)]
public FloatLayerProperty Rotation { get; set; }
/// <summary>
/// The opacity of the shape
/// </summary>
[PropertyDescription(Description = "The opacity of the shape", InputAffix = "%", MinInputValue = 0f, MaxInputValue = 100f, InputStepSize = 0.1f)]
public FloatLayerProperty Opacity { get; set; }
/// <inheritdoc />
protected override void PopulateDefaults()
{
/// <summary>
/// The point at which the shape is attached to its position
/// </summary>
[PropertyDescription(Description = "The point at which the shape is attached to its position", InputAffix = "%", InputStepSize = 0.001f)]
public SKPointLayerProperty AnchorPoint { get; set; }
Scale.DefaultValue = new SKSize(100, 100);
AnchorPoint.DefaultValue = new SKPoint(0.5f, 0.5f);
Position.DefaultValue = new SKPoint(0.5f, 0.5f);
Opacity.DefaultValue = 100;
}
/// <summary>
/// The position of the shape
/// </summary>
[PropertyDescription(Description = "The position of the shape", InputAffix = "%", InputStepSize = 0.001f)]
public SKPointLayerProperty Position { get; set; }
/// <inheritdoc />
protected override void EnableProperties()
{
}
/// <summary>
/// The scale of the shape
/// </summary>
[PropertyDescription(Description = "The scale of the shape", InputAffix = "%", MinInputValue = 0f)]
public SKSizeLayerProperty Scale { get; set; }
/// <summary>
/// The rotation of the shape in degree
/// </summary>
[PropertyDescription(Description = "The rotation of the shape in degrees", InputAffix = "°", InputStepSize = 0.5f)]
public FloatLayerProperty Rotation { get; set; }
/// <summary>
/// The opacity of the shape
/// </summary>
[PropertyDescription(Description = "The opacity of the shape", InputAffix = "%", MinInputValue = 0f, MaxInputValue = 100f, InputStepSize = 0.1f)]
public FloatLayerProperty Opacity { get; set; }
/// <inheritdoc />
protected override void PopulateDefaults()
{
Scale.DefaultValue = new SKSize(100, 100);
AnchorPoint.DefaultValue = new SKPoint(0.5f, 0.5f);
Position.DefaultValue = new SKPoint(0.5f, 0.5f);
Opacity.DefaultValue = 100;
}
/// <inheritdoc />
protected override void EnableProperties()
{
}
/// <inheritdoc />
protected override void DisableProperties()
{
}
/// <inheritdoc />
protected override void DisableProperties()
{
}
}
+258 -260
View File
@@ -6,301 +6,299 @@ using Artemis.Core.ScriptingProviders;
using Artemis.Storage.Entities.Profile;
using SkiaSharp;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a profile containing folders and layers
/// </summary>
public sealed class Profile : ProfileElement
{
/// <summary>
/// Represents a profile containing folders and layers
/// </summary>
public sealed class Profile : ProfileElement
private readonly object _lock = new();
private readonly ObservableCollection<ScriptConfiguration> _scriptConfigurations;
private readonly ObservableCollection<ProfileScript> _scripts;
private bool _isFreshImport;
private ProfileElement? _lastSelectedProfileElement;
internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!)
{
private readonly object _lock = new();
private bool _isFreshImport;
private ProfileElement? _lastSelectedProfileElement;
private readonly ObservableCollection<ProfileScript> _scripts;
private readonly ObservableCollection<ScriptConfiguration> _scriptConfigurations;
_scripts = new ObservableCollection<ProfileScript>();
_scriptConfigurations = new ObservableCollection<ScriptConfiguration>();
internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!)
{
_scripts = new ObservableCollection<ProfileScript>();
_scriptConfigurations = new ObservableCollection<ScriptConfiguration>();
Configuration = configuration;
Profile = this;
ProfileEntity = profileEntity;
EntityId = profileEntity.Id;
Configuration = configuration;
Profile = this;
ProfileEntity = profileEntity;
EntityId = profileEntity.Id;
Exceptions = new List<Exception>();
Scripts = new ReadOnlyObservableCollection<ProfileScript>(_scripts);
ScriptConfigurations = new ReadOnlyObservableCollection<ScriptConfiguration>(_scriptConfigurations);
Exceptions = new List<Exception>();
Scripts = new ReadOnlyObservableCollection<ProfileScript>(_scripts);
ScriptConfigurations = new ReadOnlyObservableCollection<ScriptConfiguration>(_scriptConfigurations);
Load();
}
Load();
}
/// <summary>
/// Gets the profile configuration of this profile
/// </summary>
public ProfileConfiguration Configuration { get; }
/// <summary>
/// Gets the profile configuration of this profile
/// </summary>
public ProfileConfiguration Configuration { get; }
/// <summary>
/// Gets a collection of all active scripts assigned to this profile
/// </summary>
public ReadOnlyObservableCollection<ProfileScript> Scripts { get; }
/// <summary>
/// Gets a collection of all active scripts assigned to this profile
/// </summary>
public ReadOnlyObservableCollection<ProfileScript> Scripts { get; }
/// <summary>
/// Gets a collection of all script configurations assigned to this profile
/// </summary>
public ReadOnlyObservableCollection<ScriptConfiguration> ScriptConfigurations { get; }
/// <summary>
/// Gets a collection of all script configurations assigned to this profile
/// </summary>
public ReadOnlyObservableCollection<ScriptConfiguration> ScriptConfigurations { get; }
/// <summary>
/// Gets or sets a boolean indicating whether this profile is freshly imported i.e. no changes have been made to it
/// since import
/// <para>
/// Note: As long as this is <see langword="true" />, profile adaption will be performed on load and any surface
/// changes
/// </para>
/// </summary>
public bool IsFreshImport
{
get => _isFreshImport;
set => SetAndNotify(ref _isFreshImport, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether this profile is freshly imported i.e. no changes have been made to it
/// since import
/// <para>
/// Note: As long as this is <see langword="true" />, profile adaption will be performed on load and any surface
/// changes
/// </para>
/// </summary>
public bool IsFreshImport
{
get => _isFreshImport;
set => SetAndNotify(ref _isFreshImport, value);
}
/// <summary>
/// Gets or sets the last selected profile element of this profile
/// </summary>
public ProfileElement? LastSelectedProfileElement
{
get => _lastSelectedProfileElement;
set => SetAndNotify(ref _lastSelectedProfileElement, value);
}
/// <summary>
/// Gets or sets the last selected profile element of this profile
/// </summary>
public ProfileElement? LastSelectedProfileElement
{
get => _lastSelectedProfileElement;
set => SetAndNotify(ref _lastSelectedProfileElement, value);
}
/// <summary>
/// Gets the profile entity this profile uses for persistent storage
/// </summary>
public ProfileEntity ProfileEntity { get; internal set; }
/// <summary>
/// Gets the profile entity this profile uses for persistent storage
/// </summary>
public ProfileEntity ProfileEntity { get; internal set; }
internal List<Exception> Exceptions { get; }
internal List<Exception> Exceptions { get; }
/// <inheritdoc />
public override void Update(double deltaTime)
{
lock (_lock)
{
if (Disposed)
throw new ObjectDisposedException("Profile");
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileUpdating(deltaTime);
foreach (ProfileElement profileElement in Children)
profileElement.Update(deltaTime);
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileUpdated(deltaTime);
}
}
/// <inheritdoc />
public override void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus)
{
lock (_lock)
{
if (Disposed)
throw new ObjectDisposedException("Profile");
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileRendering(canvas, canvas.LocalClipBounds);
foreach (ProfileElement profileElement in Children)
profileElement.Render(canvas, basePosition, editorFocus);
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileRendered(canvas, canvas.LocalClipBounds);
if (!Exceptions.Any())
return;
List<Exception> exceptions = new(Exceptions);
Exceptions.Clear();
throw new AggregateException($"One or more exceptions while rendering profile {Name}", exceptions);
}
}
/// <inheritdoc />
public override void Reset()
{
foreach (ProfileElement child in Children)
child.Reset();
}
/// <summary>
/// Retrieves the root folder of this profile
/// </summary>
/// <returns>The root folder of the profile</returns>
/// <exception cref="ObjectDisposedException"></exception>
public Folder GetRootFolder()
/// <inheritdoc />
public override void Update(double deltaTime)
{
lock (_lock)
{
if (Disposed)
throw new ObjectDisposedException("Profile");
return (Folder) Children.Single();
}
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileUpdating(deltaTime);
/// <inheritdoc />
public override string ToString()
{
return $"[Profile] {nameof(Name)}: {Name}";
}
foreach (ProfileElement profileElement in Children)
profileElement.Update(deltaTime);
/// <summary>
/// Populates all the LEDs on the elements in this profile
/// </summary>
/// <param name="devices">The devices to use while populating LEDs</param>
public void PopulateLeds(IEnumerable<ArtemisDevice> devices)
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileUpdated(deltaTime);
}
}
/// <inheritdoc />
public override void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus)
{
lock (_lock)
{
if (Disposed)
throw new ObjectDisposedException("Profile");
foreach (Layer layer in GetAllLayers())
layer.PopulateLeds(devices);
}
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileRendering(canvas, canvas.LocalClipBounds);
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (!disposing)
foreach (ProfileElement profileElement in Children)
profileElement.Render(canvas, basePosition, editorFocus);
foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileRendered(canvas, canvas.LocalClipBounds);
if (!Exceptions.Any())
return;
while (Scripts.Count > 0)
RemoveScript(Scripts[0]);
List<Exception> exceptions = new(Exceptions);
Exceptions.Clear();
throw new AggregateException($"One or more exceptions while rendering profile {Name}", exceptions);
}
}
/// <inheritdoc />
public override void Reset()
{
foreach (ProfileElement child in Children)
child.Reset();
}
/// <summary>
/// Retrieves the root folder of this profile
/// </summary>
/// <returns>The root folder of the profile</returns>
/// <exception cref="ObjectDisposedException"></exception>
public Folder GetRootFolder()
{
if (Disposed)
throw new ObjectDisposedException("Profile");
return (Folder) Children.Single();
}
/// <inheritdoc />
public override string ToString()
{
return $"[Profile] {nameof(Name)}: {Name}";
}
/// <summary>
/// Populates all the LEDs on the elements in this profile
/// </summary>
/// <param name="devices">The devices to use while populating LEDs</param>
public void PopulateLeds(IEnumerable<ArtemisDevice> devices)
{
if (Disposed)
throw new ObjectDisposedException("Profile");
foreach (Layer layer in GetAllLayers())
layer.PopulateLeds(devices);
}
#region Overrides of BreakableModel
/// <inheritdoc />
public override IEnumerable<IBreakableModel> GetBrokenHierarchy()
{
return GetAllRenderElements().SelectMany(folders => folders.GetBrokenHierarchy());
}
#endregion
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (!disposing)
return;
while (Scripts.Count > 0)
RemoveScript(Scripts[0]);
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
ChildrenList.Clear();
Disposed = true;
}
internal override void Load()
{
if (Disposed)
throw new ObjectDisposedException("Profile");
Name = Configuration.Name;
IsFreshImport = ProfileEntity.IsFreshImport;
lock (ChildrenList)
{
// Remove the old profile tree
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
ChildrenList.Clear();
Disposed = true;
}
internal override void Load()
{
if (Disposed)
throw new ObjectDisposedException("Profile");
Name = Configuration.Name;
IsFreshImport = ProfileEntity.IsFreshImport;
lock (ChildrenList)
{
// Remove the old profile tree
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
ChildrenList.Clear();
// Populate the profile starting at the root, the rest is populated recursively
FolderEntity? rootFolder = ProfileEntity.Folders.FirstOrDefault(f => f.ParentId == EntityId);
if (rootFolder == null)
AddChild(new Folder(this, "Root folder"));
else
AddChild(new Folder(this, this, rootFolder));
}
List<RenderProfileElement> renderElements = GetAllRenderElements();
if (ProfileEntity.LastSelectedProfileElement != Guid.Empty)
LastSelectedProfileElement = renderElements.FirstOrDefault(f => f.EntityId == ProfileEntity.LastSelectedProfileElement);
// Populate the profile starting at the root, the rest is populated recursively
FolderEntity? rootFolder = ProfileEntity.Folders.FirstOrDefault(f => f.ParentId == EntityId);
if (rootFolder == null)
AddChild(new Folder(this, "Root folder"));
else
LastSelectedProfileElement = null;
while (_scriptConfigurations.Any())
RemoveScriptConfiguration(_scriptConfigurations[0]);
foreach (ScriptConfiguration scriptConfiguration in ProfileEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e)))
AddScriptConfiguration(scriptConfiguration);
// Load node scripts last since they may rely on the profile structure being in place
foreach (RenderProfileElement renderProfileElement in renderElements)
renderProfileElement.LoadNodeScript();
AddChild(new Folder(this, this, rootFolder));
}
/// <summary>
/// Removes a script configuration from the profile, if the configuration has an active script it is also removed.
/// </summary>
internal void RemoveScriptConfiguration(ScriptConfiguration scriptConfiguration)
List<RenderProfileElement> renderElements = GetAllRenderElements();
if (ProfileEntity.LastSelectedProfileElement != Guid.Empty)
LastSelectedProfileElement = renderElements.FirstOrDefault(f => f.EntityId == ProfileEntity.LastSelectedProfileElement);
else
LastSelectedProfileElement = null;
while (_scriptConfigurations.Any())
RemoveScriptConfiguration(_scriptConfigurations[0]);
foreach (ScriptConfiguration scriptConfiguration in ProfileEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e)))
AddScriptConfiguration(scriptConfiguration);
// Load node scripts last since they may rely on the profile structure being in place
foreach (RenderProfileElement renderProfileElement in renderElements)
renderProfileElement.LoadNodeScript();
}
/// <summary>
/// Removes a script configuration from the profile, if the configuration has an active script it is also removed.
/// </summary>
internal void RemoveScriptConfiguration(ScriptConfiguration scriptConfiguration)
{
if (!_scriptConfigurations.Contains(scriptConfiguration))
return;
Script? script = scriptConfiguration.Script;
if (script != null)
RemoveScript((ProfileScript) script);
_scriptConfigurations.Remove(scriptConfiguration);
}
/// <summary>
/// Adds a script configuration to the profile but does not instantiate it's script.
/// </summary>
internal void AddScriptConfiguration(ScriptConfiguration scriptConfiguration)
{
if (!_scriptConfigurations.Contains(scriptConfiguration))
_scriptConfigurations.Add(scriptConfiguration);
}
/// <summary>
/// Adds a script that has a script configuration belonging to this profile.
/// </summary>
internal void AddScript(ProfileScript script)
{
if (!_scriptConfigurations.Contains(script.ScriptConfiguration))
throw new ArtemisCoreException("Cannot add a script to a profile whose script configuration doesn't belong to the same profile.");
if (!_scripts.Contains(script))
_scripts.Add(script);
}
/// <summary>
/// Removes a script from the profile and disposes it.
/// </summary>
internal void RemoveScript(ProfileScript script)
{
_scripts.Remove(script);
script.Dispose();
}
internal override void Save()
{
if (Disposed)
throw new ObjectDisposedException("Profile");
ProfileEntity.Id = EntityId;
ProfileEntity.Name = Configuration.Name;
ProfileEntity.IsFreshImport = IsFreshImport;
ProfileEntity.LastSelectedProfileElement = LastSelectedProfileElement?.EntityId ?? Guid.Empty;
foreach (ProfileElement profileElement in Children)
profileElement.Save();
ProfileEntity.Folders.Clear();
ProfileEntity.Folders.AddRange(GetAllFolders().Select(f => f.FolderEntity));
ProfileEntity.Layers.Clear();
ProfileEntity.Layers.AddRange(GetAllLayers().Select(f => f.LayerEntity));
ProfileEntity.ScriptConfigurations.Clear();
foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations)
{
if (!_scriptConfigurations.Contains(scriptConfiguration))
return;
Script? script = scriptConfiguration.Script;
if (script != null)
RemoveScript((ProfileScript) script);
_scriptConfigurations.Remove(scriptConfiguration);
scriptConfiguration.Save();
ProfileEntity.ScriptConfigurations.Add(scriptConfiguration.Entity);
}
/// <summary>
/// Adds a script configuration to the profile but does not instantiate it's script.
/// </summary>
internal void AddScriptConfiguration(ScriptConfiguration scriptConfiguration)
{
if (!_scriptConfigurations.Contains(scriptConfiguration))
_scriptConfigurations.Add(scriptConfiguration);
}
/// <summary>
/// Adds a script that has a script configuration belonging to this profile.
/// </summary>
internal void AddScript(ProfileScript script)
{
if (!_scriptConfigurations.Contains(script.ScriptConfiguration))
throw new ArtemisCoreException("Cannot add a script to a profile whose script configuration doesn't belong to the same profile.");
if (!_scripts.Contains(script))
_scripts.Add(script);
}
/// <summary>
/// Removes a script from the profile and disposes it.
/// </summary>
internal void RemoveScript(ProfileScript script)
{
_scripts.Remove(script);
script.Dispose();
}
internal override void Save()
{
if (Disposed)
throw new ObjectDisposedException("Profile");
ProfileEntity.Id = EntityId;
ProfileEntity.Name = Configuration.Name;
ProfileEntity.IsFreshImport = IsFreshImport;
ProfileEntity.LastSelectedProfileElement = LastSelectedProfileElement?.EntityId ?? Guid.Empty;
foreach (ProfileElement profileElement in Children)
profileElement.Save();
ProfileEntity.Folders.Clear();
ProfileEntity.Folders.AddRange(GetAllFolders().Select(f => f.FolderEntity));
ProfileEntity.Layers.Clear();
ProfileEntity.Layers.AddRange(GetAllLayers().Select(f => f.LayerEntity));
ProfileEntity.ScriptConfigurations.Clear();
foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations)
{
scriptConfiguration.Save();
ProfileEntity.ScriptConfigurations.Add(scriptConfiguration.Entity);
}
}
#region Overrides of BreakableModel
/// <inheritdoc />
public override IEnumerable<IBreakableModel> GetBrokenHierarchy()
{
return GetAllRenderElements().SelectMany(folders => folders.GetBrokenHierarchy());
}
#endregion
}
}
+199 -201
View File
@@ -3,212 +3,210 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a category containing <see cref="ProfileConfigurations" />
/// </summary>
public class ProfileCategory : CorePropertyChanged, IStorageModel
{
private readonly List<ProfileConfiguration> _profileConfigurations = new();
private bool _isCollapsed;
private bool _isSuspended;
private string _name;
private int _order;
/// <summary>
/// Creates a new instance of the <see cref="ProfileCategory" /> class
/// </summary>
/// <param name="name">The name of the category</param>
internal ProfileCategory(string name)
{
_name = name;
Entity = new ProfileCategoryEntity();
ProfileConfigurations = new ReadOnlyCollection<ProfileConfiguration>(_profileConfigurations);
}
internal ProfileCategory(ProfileCategoryEntity entity)
{
_name = null!;
Entity = entity;
ProfileConfigurations = new ReadOnlyCollection<ProfileConfiguration>(_profileConfigurations);
Load();
}
/// <summary>
/// Gets or sets the name of the profile category
/// </summary>
public string Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// The order in which this category appears in the update loop and sidebar
/// </summary>
public int Order
{
get => _order;
set => SetAndNotify(ref _order, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the category is collapsed or not
/// <para>Note: Has no implications other than inside the UI</para>
/// </summary>
public bool IsCollapsed
{
get => _isCollapsed;
set => SetAndNotify(ref _isCollapsed, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether this category is suspended, disabling all its profiles
/// </summary>
public bool IsSuspended
{
get => _isSuspended;
set => SetAndNotify(ref _isSuspended, value);
}
/// <summary>
/// Gets a read only collection of the profiles inside this category
/// </summary>
public ReadOnlyCollection<ProfileConfiguration> ProfileConfigurations { get; }
/// <summary>
/// Gets the unique ID of this category
/// </summary>
public Guid EntityId => Entity.Id;
internal ProfileCategoryEntity Entity { get; }
/// <summary>
/// Adds a profile configuration to this category
/// </summary>
public void AddProfileConfiguration(ProfileConfiguration configuration, int? targetIndex)
{
// Removing the original will shift every item in the list forwards, keep that in mind with the target index
if (configuration.Category == this && targetIndex != null && targetIndex.Value > _profileConfigurations.IndexOf(configuration))
targetIndex -= 1;
configuration.Category.RemoveProfileConfiguration(configuration);
if (targetIndex != null)
{
targetIndex = Math.Clamp(targetIndex.Value, 0, _profileConfigurations.Count);
_profileConfigurations.Insert(targetIndex.Value, configuration);
}
else
{
_profileConfigurations.Add(configuration);
}
configuration.Category = this;
for (int index = 0; index < _profileConfigurations.Count; index++)
_profileConfigurations[index].Order = index;
OnProfileConfigurationAdded(new ProfileConfigurationEventArgs(configuration));
}
/// <inheritdoc />
public override string ToString()
{
return $"[ProfileCategory] {Order} {nameof(Name)}: {Name}, {nameof(IsSuspended)}: {IsSuspended}";
}
/// <summary>
/// Occurs when a profile configuration is added to this <see cref="ProfileCategory" />
/// </summary>
public event EventHandler<ProfileConfigurationEventArgs>? ProfileConfigurationAdded;
/// <summary>
/// Occurs when a profile configuration is removed from this <see cref="ProfileCategory" />
/// </summary>
public event EventHandler<ProfileConfigurationEventArgs>? ProfileConfigurationRemoved;
/// <summary>
/// Invokes the <see cref="ProfileConfigurationAdded" /> event
/// </summary>
protected virtual void OnProfileConfigurationAdded(ProfileConfigurationEventArgs e)
{
ProfileConfigurationAdded?.Invoke(this, e);
}
/// <summary>
/// Invokes the <see cref="ProfileConfigurationRemoved" /> event
/// </summary>
protected virtual void OnProfileConfigurationRemoved(ProfileConfigurationEventArgs e)
{
ProfileConfigurationRemoved?.Invoke(this, e);
}
internal void RemoveProfileConfiguration(ProfileConfiguration configuration)
{
if (!_profileConfigurations.Remove(configuration))
return;
for (int index = 0; index < _profileConfigurations.Count; index++)
_profileConfigurations[index].Order = index;
OnProfileConfigurationRemoved(new ProfileConfigurationEventArgs(configuration));
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
Name = Entity.Name;
IsCollapsed = Entity.IsCollapsed;
IsSuspended = Entity.IsSuspended;
Order = Entity.Order;
_profileConfigurations.Clear();
foreach (ProfileConfigurationEntity entityProfileConfiguration in Entity.ProfileConfigurations)
_profileConfigurations.Add(new ProfileConfiguration(this, entityProfileConfiguration));
}
/// <inheritdoc />
public void Save()
{
Entity.Name = Name;
Entity.IsCollapsed = IsCollapsed;
Entity.IsSuspended = IsSuspended;
Entity.Order = Order;
Entity.ProfileConfigurations.Clear();
foreach (ProfileConfiguration profileConfiguration in ProfileConfigurations)
{
profileConfiguration.Save();
Entity.ProfileConfigurations.Add(profileConfiguration.Entity);
}
}
#endregion
}
/// <summary>
/// Represents a name of one of the default categories
/// </summary>
public enum DefaultCategoryName
{
/// <summary>
/// Represents a category containing <see cref="ProfileConfigurations" />
/// The category used by profiles tied to games
/// </summary>
public class ProfileCategory : CorePropertyChanged, IStorageModel
{
private readonly List<ProfileConfiguration> _profileConfigurations = new();
private bool _isCollapsed;
private bool _isSuspended;
private string _name;
private int _order;
/// <summary>
/// Creates a new instance of the <see cref="ProfileCategory" /> class
/// </summary>
/// <param name="name">The name of the category</param>
internal ProfileCategory(string name)
{
_name = name;
Entity = new ProfileCategoryEntity();
ProfileConfigurations = new ReadOnlyCollection<ProfileConfiguration>(_profileConfigurations);
}
internal ProfileCategory(ProfileCategoryEntity entity)
{
_name = null!;
Entity = entity;
ProfileConfigurations = new ReadOnlyCollection<ProfileConfiguration>(_profileConfigurations);
Load();
}
/// <summary>
/// Gets or sets the name of the profile category
/// </summary>
public string Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// The order in which this category appears in the update loop and sidebar
/// </summary>
public int Order
{
get => _order;
set => SetAndNotify(ref _order, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the category is collapsed or not
/// <para>Note: Has no implications other than inside the UI</para>
/// </summary>
public bool IsCollapsed
{
get => _isCollapsed;
set => SetAndNotify(ref _isCollapsed, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether this category is suspended, disabling all its profiles
/// </summary>
public bool IsSuspended
{
get => _isSuspended;
set => SetAndNotify(ref _isSuspended, value);
}
/// <summary>
/// Gets a read only collection of the profiles inside this category
/// </summary>
public ReadOnlyCollection<ProfileConfiguration> ProfileConfigurations { get; }
/// <summary>
/// Gets the unique ID of this category
/// </summary>
public Guid EntityId => Entity.Id;
internal ProfileCategoryEntity Entity { get; }
/// <summary>
/// Adds a profile configuration to this category
/// </summary>
public void AddProfileConfiguration(ProfileConfiguration configuration, int? targetIndex)
{
// Removing the original will shift every item in the list forwards, keep that in mind with the target index
if (configuration.Category == this && targetIndex != null && targetIndex.Value > _profileConfigurations.IndexOf(configuration))
targetIndex -= 1;
configuration.Category.RemoveProfileConfiguration(configuration);
if (targetIndex != null)
{
targetIndex = Math.Clamp(targetIndex.Value, 0, _profileConfigurations.Count);
_profileConfigurations.Insert(targetIndex.Value, configuration);
}
else
_profileConfigurations.Add(configuration);
configuration.Category = this;
for (int index = 0; index < _profileConfigurations.Count; index++)
_profileConfigurations[index].Order = index;
OnProfileConfigurationAdded(new ProfileConfigurationEventArgs(configuration));
}
/// <inheritdoc />
public override string ToString()
{
return $"[ProfileCategory] {Order} {nameof(Name)}: {Name}, {nameof(IsSuspended)}: {IsSuspended}";
}
internal void RemoveProfileConfiguration(ProfileConfiguration configuration)
{
if (!_profileConfigurations.Remove(configuration))
return;
for (int index = 0; index < _profileConfigurations.Count; index++)
_profileConfigurations[index].Order = index;
OnProfileConfigurationRemoved(new ProfileConfigurationEventArgs(configuration));
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
Name = Entity.Name;
IsCollapsed = Entity.IsCollapsed;
IsSuspended = Entity.IsSuspended;
Order = Entity.Order;
_profileConfigurations.Clear();
foreach (ProfileConfigurationEntity entityProfileConfiguration in Entity.ProfileConfigurations)
_profileConfigurations.Add(new ProfileConfiguration(this, entityProfileConfiguration));
}
/// <inheritdoc />
public void Save()
{
Entity.Name = Name;
Entity.IsCollapsed = IsCollapsed;
Entity.IsSuspended = IsSuspended;
Entity.Order = Order;
Entity.ProfileConfigurations.Clear();
foreach (ProfileConfiguration profileConfiguration in ProfileConfigurations)
{
profileConfiguration.Save();
Entity.ProfileConfigurations.Add(profileConfiguration.Entity);
}
}
#endregion
#region Events
/// <summary>
/// Occurs when a profile configuration is added to this <see cref="ProfileCategory" />
/// </summary>
public event EventHandler<ProfileConfigurationEventArgs>? ProfileConfigurationAdded;
/// <summary>
/// Occurs when a profile configuration is removed from this <see cref="ProfileCategory" />
/// </summary>
public event EventHandler<ProfileConfigurationEventArgs>? ProfileConfigurationRemoved;
/// <summary>
/// Invokes the <see cref="ProfileConfigurationAdded" /> event
/// </summary>
protected virtual void OnProfileConfigurationAdded(ProfileConfigurationEventArgs e)
{
ProfileConfigurationAdded?.Invoke(this, e);
}
/// <summary>
/// Invokes the <see cref="ProfileConfigurationRemoved" /> event
/// </summary>
protected virtual void OnProfileConfigurationRemoved(ProfileConfigurationEventArgs e)
{
ProfileConfigurationRemoved?.Invoke(this, e);
}
#endregion
}
Games,
/// <summary>
/// Represents a name of one of the default categories
/// The category used by profiles tied to applications
/// </summary>
public enum DefaultCategoryName
{
/// <summary>
/// The category used by profiles tied to games
/// </summary>
Games,
Applications,
/// <summary>
/// The category used by profiles tied to applications
/// </summary>
Applications,
/// <summary>
/// The category used by general profiles
/// </summary>
General
}
/// <summary>
/// The category used by general profiles
/// </summary>
General
}
+370 -372
View File
@@ -2,381 +2,379 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text.RegularExpressions;
using SkiaSharp;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents an element of a <see cref="Profile" />
/// </summary>
public abstract class ProfileElement : BreakableModel, IDisposable
{
/// <summary>
/// Represents an element of a <see cref="Profile" />
/// </summary>
public abstract class ProfileElement : BreakableModel, IDisposable
internal readonly List<ProfileElement> ChildrenList;
private Guid _entityId;
private string? _name;
private int _order;
private ProfileElement? _parent;
private Profile _profile;
private bool _suspended;
internal ProfileElement(Profile profile)
{
internal readonly List<ProfileElement> ChildrenList;
private Guid _entityId;
private string? _name;
private int _order;
private ProfileElement? _parent;
private Profile _profile;
private bool _suspended;
internal ProfileElement(Profile profile)
{
_profile = profile;
ChildrenList = new List<ProfileElement>();
Children = new ReadOnlyCollection<ProfileElement>(ChildrenList);
}
/// <summary>
/// Gets the unique ID of this profile element
/// </summary>
public Guid EntityId
{
get => _entityId;
internal set => SetAndNotify(ref _entityId, value);
}
/// <summary>
/// Gets the profile this element belongs to
/// </summary>
public Profile Profile
{
get => _profile;
internal set => SetAndNotify(ref _profile, value);
}
/// <summary>
/// Gets the parent of this element
/// </summary>
public ProfileElement? Parent
{
get => _parent;
internal set => SetAndNotify(ref _parent, value);
}
/// <summary>
/// The element's children
/// </summary>
public ReadOnlyCollection<ProfileElement> Children { get; }
/// <summary>
/// The order in which this element appears in the update loop and editor
/// </summary>
public int Order
{
get => _order;
internal set => SetAndNotify(ref _order, value);
}
/// <summary>
/// The name which appears in the editor
/// </summary>
public string? Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// Gets or sets the suspended state, if suspended the element is skipped in render and update
/// </summary>
public bool Suspended
{
get => _suspended;
set => SetAndNotify(ref _suspended, value);
}
/// <summary>
/// Gets a boolean indicating whether the profile element is disposed
/// </summary>
public bool Disposed { get; protected set; }
#region Overrides of BreakableModel
/// <inheritdoc />
public override string BrokenDisplayName => Name ?? GetType().Name;
#endregion
/// <summary>
/// Updates the element
/// </summary>
/// <param name="deltaTime"></param>
public abstract void Update(double deltaTime);
/// <summary>
/// Renders the element
/// </summary>
/// <param name="canvas">The canvas to render upon.</param>
/// <param name="basePosition">The base position to use to translate relative positions to absolute positions.</param>
/// <param name="editorFocus">An optional element to focus on while rendering (other elements will not render).</param>
public abstract void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus);
/// <summary>
/// Resets the internal state of the element
/// </summary>
public abstract void Reset();
/// <inheritdoc />
public override string ToString()
{
return $"{nameof(EntityId)}: {EntityId}, {nameof(Order)}: {Order}, {nameof(Name)}: {Name}";
}
/// <summary>
/// Occurs when a child was added to the <see cref="Children" /> list
/// </summary>
public event EventHandler<ProfileElementEventArgs>? ChildAdded;
/// <summary>
/// Occurs when a child was removed from the <see cref="Children" /> list
/// </summary>
public event EventHandler<ProfileElementEventArgs>? ChildRemoved;
/// <summary>
/// Occurs when a child was added to the <see cref="Children" /> list of this element or any of it's descendents.
/// </summary>
public event EventHandler<ProfileElementEventArgs>? DescendentAdded;
/// <summary>
/// Occurs when a child was removed from the <see cref="Children" /> list of this element or any of it's descendents.
/// </summary>
public event EventHandler<ProfileElementEventArgs>? DescendentRemoved;
/// <summary>
/// Invokes the <see cref="ChildAdded" /> event
/// </summary>
protected virtual void OnChildAdded(ProfileElement child)
{
ChildAdded?.Invoke(this, new ProfileElementEventArgs(child));
}
/// <summary>
/// Invokes the <see cref="ChildRemoved" /> event
/// </summary>
protected virtual void OnChildRemoved(ProfileElement child)
{
ChildRemoved?.Invoke(this, new ProfileElementEventArgs(child));
}
/// <summary>
/// Invokes the <see cref="DescendentAdded" /> event
/// </summary>
protected virtual void OnDescendentAdded(ProfileElement child)
{
DescendentAdded?.Invoke(this, new ProfileElementEventArgs(child));
Parent?.OnDescendentAdded(child);
}
/// <summary>
/// Invokes the <see cref="DescendentRemoved" /> event
/// </summary>
protected virtual void OnDescendentRemoved(ProfileElement child)
{
DescendentRemoved?.Invoke(this, new ProfileElementEventArgs(child));
Parent?.OnDescendentRemoved(child);
}
/// <summary>
/// Disposes the profile element
/// </summary>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#region Hierarchy
/// <summary>
/// Adds a profile element to the <see cref="Children" /> collection, optionally at the given position (0-based)
/// </summary>
/// <param name="child">The profile element to add</param>
/// <param name="order">The order where to place the child (0-based), defaults to the end of the collection</param>
public virtual void AddChild(ProfileElement child, int? order = null)
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
ProfileElement? current = this;
while (current != null)
{
if (ReferenceEquals(child, this))
throw new ArtemisCoreException("Cannot make an element a child of itself");
current = current.Parent;
}
lock (ChildrenList)
{
if (ChildrenList.Contains(child))
return;
// Add to the end of the list
if (order == null)
{
ChildrenList.Add(child);
}
// Insert at the given index
else
{
if (order < 0)
order = 0;
if (order > ChildrenList.Count)
order = ChildrenList.Count;
ChildrenList.Insert(order.Value, child);
}
child.Parent = this;
StreamlineOrder();
}
OnChildAdded(child);
OnDescendentAdded(child);
}
/// <summary>
/// Removes a profile element from the <see cref="Children" /> collection
/// </summary>
/// <param name="child">The profile element to remove</param>
public virtual void RemoveChild(ProfileElement child)
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
lock (ChildrenList)
{
ChildrenList.Remove(child);
StreamlineOrder();
child.Parent = null;
}
OnChildRemoved(child);
OnDescendentRemoved(child);
}
private void StreamlineOrder()
{
for (int index = 0; index < ChildrenList.Count; index++)
ChildrenList[index].Order = index + 1;
}
/// <summary>
/// Returns a flattened list of all child render elements
/// </summary>
/// <returns></returns>
public List<RenderProfileElement> GetAllRenderElements()
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
List<RenderProfileElement> elements = new();
foreach (RenderProfileElement childElement in Children.Where(c => c is RenderProfileElement).Cast<RenderProfileElement>())
{
// Add all folders in this element
elements.Add(childElement);
// Add all folders in folders inside this element
elements.AddRange(childElement.GetAllRenderElements());
}
return elements;
}
/// <summary>
/// Returns a flattened list of all child folders
/// </summary>
/// <returns></returns>
public List<Folder> GetAllFolders()
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
List<Folder> folders = new();
foreach (Folder childFolder in Children.Where(c => c is Folder).Cast<Folder>())
{
// Add all folders in this element
folders.Add(childFolder);
// Add all folders in folders inside this element
folders.AddRange(childFolder.GetAllFolders());
}
return folders;
}
/// <summary>
/// Returns a flattened list of all child layers
/// </summary>
/// <returns></returns>
public List<Layer> GetAllLayers()
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
List<Layer> layers = new();
// Add all layers in this element
layers.AddRange(Children.Where(c => c is Layer).Cast<Layer>());
// Add all layers in folders inside this element
foreach (Folder childFolder in Children.Where(c => c is Folder).Cast<Folder>())
layers.AddRange(childFolder.GetAllLayers());
return layers;
}
/// <summary>
/// Returns a name for a new layer according to any other layers with a default name similar to creating new folders in
/// Explorer
/// </summary>
/// <returns>The resulting name i.e. <c>New layer</c> or <c>New layer (2)</c></returns>
public string GetNewLayerName(string baseName = "New layer")
{
if (!Children.Any(c => c is Layer && c.Name == baseName))
return baseName;
int current = 2;
while (true)
{
if (Children.Where(c => c is Layer).All(c => c.Name != $"{baseName} ({current})"))
return $"{baseName} ({current})";
current++;
}
}
/// <summary>
/// Returns a name for a new folder according to any other folders with a default name similar to creating new folders
/// in Explorer
/// </summary>
/// <returns>The resulting name i.e. <c>New folder</c> or <c>New folder (2)</c></returns>
public string GetNewFolderName(string baseName = "New folder")
{
if (!Children.Any(c => c is Folder && c.Name == baseName))
return baseName;
int current = 2;
while (true)
{
if (Children.Where(c => c is Folder).All(c => c.Name != $"{baseName} ({current})"))
return $"{baseName} ({current})";
current++;
}
}
#endregion
#region Storage
internal abstract void Load();
internal abstract void Save();
#endregion
_profile = profile;
ChildrenList = new List<ProfileElement>();
Children = new ReadOnlyCollection<ProfileElement>(ChildrenList);
}
/// <summary>
/// Gets the unique ID of this profile element
/// </summary>
public Guid EntityId
{
get => _entityId;
internal set => SetAndNotify(ref _entityId, value);
}
/// <summary>
/// Gets the profile this element belongs to
/// </summary>
public Profile Profile
{
get => _profile;
internal set => SetAndNotify(ref _profile, value);
}
/// <summary>
/// Gets the parent of this element
/// </summary>
public ProfileElement? Parent
{
get => _parent;
internal set => SetAndNotify(ref _parent, value);
}
/// <summary>
/// The element's children
/// </summary>
public ReadOnlyCollection<ProfileElement> Children { get; }
/// <summary>
/// The order in which this element appears in the update loop and editor
/// </summary>
public int Order
{
get => _order;
internal set => SetAndNotify(ref _order, value);
}
/// <summary>
/// The name which appears in the editor
/// </summary>
public string? Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// Gets or sets the suspended state, if suspended the element is skipped in render and update
/// </summary>
public bool Suspended
{
get => _suspended;
set => SetAndNotify(ref _suspended, value);
}
/// <summary>
/// Gets a boolean indicating whether the profile element is disposed
/// </summary>
public bool Disposed { get; protected set; }
#region Overrides of BreakableModel
/// <inheritdoc />
public override string BrokenDisplayName => Name ?? GetType().Name;
#endregion
/// <summary>
/// Updates the element
/// </summary>
/// <param name="deltaTime"></param>
public abstract void Update(double deltaTime);
/// <summary>
/// Renders the element
/// </summary>
/// <param name="canvas">The canvas to render upon.</param>
/// <param name="basePosition">The base position to use to translate relative positions to absolute positions.</param>
/// <param name="editorFocus">An optional element to focus on while rendering (other elements will not render).</param>
public abstract void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus);
/// <summary>
/// Resets the internal state of the element
/// </summary>
public abstract void Reset();
/// <inheritdoc />
public override string ToString()
{
return $"{nameof(EntityId)}: {EntityId}, {nameof(Order)}: {Order}, {nameof(Name)}: {Name}";
}
/// <summary>
/// Occurs when a child was added to the <see cref="Children" /> list
/// </summary>
public event EventHandler<ProfileElementEventArgs>? ChildAdded;
/// <summary>
/// Occurs when a child was removed from the <see cref="Children" /> list
/// </summary>
public event EventHandler<ProfileElementEventArgs>? ChildRemoved;
/// <summary>
/// Occurs when a child was added to the <see cref="Children" /> list of this element or any of it's descendents.
/// </summary>
public event EventHandler<ProfileElementEventArgs>? DescendentAdded;
/// <summary>
/// Occurs when a child was removed from the <see cref="Children" /> list of this element or any of it's descendents.
/// </summary>
public event EventHandler<ProfileElementEventArgs>? DescendentRemoved;
/// <summary>
/// Invokes the <see cref="ChildAdded" /> event
/// </summary>
protected virtual void OnChildAdded(ProfileElement child)
{
ChildAdded?.Invoke(this, new ProfileElementEventArgs(child));
}
/// <summary>
/// Invokes the <see cref="ChildRemoved" /> event
/// </summary>
protected virtual void OnChildRemoved(ProfileElement child)
{
ChildRemoved?.Invoke(this, new ProfileElementEventArgs(child));
}
/// <summary>
/// Invokes the <see cref="DescendentAdded" /> event
/// </summary>
protected virtual void OnDescendentAdded(ProfileElement child)
{
DescendentAdded?.Invoke(this, new ProfileElementEventArgs(child));
Parent?.OnDescendentAdded(child);
}
/// <summary>
/// Invokes the <see cref="DescendentRemoved" /> event
/// </summary>
protected virtual void OnDescendentRemoved(ProfileElement child)
{
DescendentRemoved?.Invoke(this, new ProfileElementEventArgs(child));
Parent?.OnDescendentRemoved(child);
}
/// <summary>
/// Disposes the profile element
/// </summary>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#region Hierarchy
/// <summary>
/// Adds a profile element to the <see cref="Children" /> collection, optionally at the given position (0-based)
/// </summary>
/// <param name="child">The profile element to add</param>
/// <param name="order">The order where to place the child (0-based), defaults to the end of the collection</param>
public virtual void AddChild(ProfileElement child, int? order = null)
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
ProfileElement? current = this;
while (current != null)
{
if (ReferenceEquals(child, this))
throw new ArtemisCoreException("Cannot make an element a child of itself");
current = current.Parent;
}
lock (ChildrenList)
{
if (ChildrenList.Contains(child))
return;
// Add to the end of the list
if (order == null)
{
ChildrenList.Add(child);
}
// Insert at the given index
else
{
if (order < 0)
order = 0;
if (order > ChildrenList.Count)
order = ChildrenList.Count;
ChildrenList.Insert(order.Value, child);
}
child.Parent = this;
StreamlineOrder();
}
OnChildAdded(child);
OnDescendentAdded(child);
}
/// <summary>
/// Removes a profile element from the <see cref="Children" /> collection
/// </summary>
/// <param name="child">The profile element to remove</param>
public virtual void RemoveChild(ProfileElement child)
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
lock (ChildrenList)
{
ChildrenList.Remove(child);
StreamlineOrder();
child.Parent = null;
}
OnChildRemoved(child);
OnDescendentRemoved(child);
}
private void StreamlineOrder()
{
for (int index = 0; index < ChildrenList.Count; index++)
ChildrenList[index].Order = index + 1;
}
/// <summary>
/// Returns a flattened list of all child render elements
/// </summary>
/// <returns></returns>
public List<RenderProfileElement> GetAllRenderElements()
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
List<RenderProfileElement> elements = new();
foreach (RenderProfileElement childElement in Children.Where(c => c is RenderProfileElement).Cast<RenderProfileElement>())
{
// Add all folders in this element
elements.Add(childElement);
// Add all folders in folders inside this element
elements.AddRange(childElement.GetAllRenderElements());
}
return elements;
}
/// <summary>
/// Returns a flattened list of all child folders
/// </summary>
/// <returns></returns>
public List<Folder> GetAllFolders()
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
List<Folder> folders = new();
foreach (Folder childFolder in Children.Where(c => c is Folder).Cast<Folder>())
{
// Add all folders in this element
folders.Add(childFolder);
// Add all folders in folders inside this element
folders.AddRange(childFolder.GetAllFolders());
}
return folders;
}
/// <summary>
/// Returns a flattened list of all child layers
/// </summary>
/// <returns></returns>
public List<Layer> GetAllLayers()
{
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
List<Layer> layers = new();
// Add all layers in this element
layers.AddRange(Children.Where(c => c is Layer).Cast<Layer>());
// Add all layers in folders inside this element
foreach (Folder childFolder in Children.Where(c => c is Folder).Cast<Folder>())
layers.AddRange(childFolder.GetAllLayers());
return layers;
}
/// <summary>
/// Returns a name for a new layer according to any other layers with a default name similar to creating new folders in
/// Explorer
/// </summary>
/// <returns>The resulting name i.e. <c>New layer</c> or <c>New layer (2)</c></returns>
public string GetNewLayerName(string baseName = "New layer")
{
if (!Children.Any(c => c is Layer && c.Name == baseName))
return baseName;
int current = 2;
while (true)
{
if (Children.Where(c => c is Layer).All(c => c.Name != $"{baseName} ({current})"))
return $"{baseName} ({current})";
current++;
}
}
/// <summary>
/// Returns a name for a new folder according to any other folders with a default name similar to creating new folders
/// in Explorer
/// </summary>
/// <returns>The resulting name i.e. <c>New folder</c> or <c>New folder (2)</c></returns>
public string GetNewFolderName(string baseName = "New folder")
{
if (!Children.Any(c => c is Folder && c.Name == baseName))
return baseName;
int current = 2;
while (true)
{
if (Children.Where(c => c is Folder).All(c => c.Name != $"{baseName} ({current})"))
return $"{baseName} ({current})";
current++;
}
}
#endregion
#region Storage
internal abstract void Load();
internal abstract void Save();
#endregion
}
@@ -1,18 +1,17 @@
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// An enum defining the right side type of a profile entity
/// </summary>
public enum ProfileRightSideType
{
/// <summary>
/// An enum defining the right side type of a profile entity
/// A static right side value
/// </summary>
public enum ProfileRightSideType
{
/// <summary>
/// A static right side value
/// </summary>
Static,
Static,
/// <summary>
/// A dynamic right side value based on a path in a data model
/// </summary>
Dynamic
}
/// <summary>
/// A dynamic right side value based on a path in a data model
/// </summary>
Dynamic
}
@@ -182,7 +182,7 @@ public abstract class RenderProfileElement : ProfileElement
get => _bounds;
private set => SetAndNotify(ref _bounds, value);
}
#endregion
#region State
@@ -256,7 +256,7 @@ public abstract class RenderProfileElement : ProfileElement
foreach (BaseLayerEffect baseLayerEffect in _layerEffects)
baseLayerEffect.Dispose();
_layerEffects.Clear();
foreach (LayerEffectEntity layerEffectEntity in RenderElementEntity.LayerEffects.OrderBy(e => e.Order))
LoadLayerEffect(layerEffectEntity);
}
@@ -276,7 +276,7 @@ public abstract class RenderProfileElement : ProfileElement
descriptor = PlaceholderLayerEffectDescriptor.Create(layerEffectEntity.ProviderId);
layerEffect = descriptor.CreateInstance(this, layerEffectEntity);
}
_layerEffects.Add(layerEffect);
}
@@ -285,7 +285,7 @@ public abstract class RenderProfileElement : ProfileElement
int index = _layerEffects.IndexOf(layerEffect);
if (index == -1)
return;
LayerEffectDescriptor descriptor = PlaceholderLayerEffectDescriptor.Create(layerEffect.ProviderId);
BaseLayerEffect placeholder = descriptor.CreateInstance(this, layerEffect.LayerEffectEntity);
_layerEffects[index] = placeholder;
@@ -298,17 +298,17 @@ public abstract class RenderProfileElement : ProfileElement
int index = _layerEffects.IndexOf(placeholder);
if (index == -1)
return;
LayerEffectDescriptor? descriptor = LayerEffectStore.Get(placeholder.OriginalEntity.ProviderId, placeholder.PlaceholderFor)?.LayerEffectDescriptor;
if (descriptor == null)
throw new ArtemisCoreException("Can't replace a placeholder effect because the real effect isn't available.");
BaseLayerEffect layerEffect = descriptor.CreateInstance(this, placeholder.OriginalEntity);
_layerEffects[index] = layerEffect;
placeholder.Dispose();
OnLayerEffectsUpdated();
}
private void OrderEffects()
{
int index = 0;
@@ -320,7 +320,7 @@ public abstract class RenderProfileElement : ProfileElement
_layerEffects.Sort((a, b) => a.Order.CompareTo(b.Order));
}
private void LayerEffectStoreOnLayerEffectRemoved(object? sender, LayerEffectStoreEvent e)
{
// Find effects that just got disabled and replace them with placeholders
@@ -328,7 +328,7 @@ public abstract class RenderProfileElement : ProfileElement
if (!affectedLayerEffects.Any())
return;
foreach (BaseLayerEffect baseLayerEffect in affectedLayerEffects)
ReplaceLayerEffectWithPlaceholder(baseLayerEffect);
OnLayerEffectsUpdated();
@@ -344,7 +344,7 @@ public abstract class RenderProfileElement : ProfileElement
if (!affectedPlaceholders.Any())
return;
foreach (PlaceholderLayerEffect placeholderLayerEffect in affectedPlaceholders)
ReplacePlaceholderWithLayerEffect(placeholderLayerEffect);
OnLayerEffectsUpdated();
@@ -393,4 +393,4 @@ public abstract class RenderProfileElement : ProfileElement
}
#endregion
}
}
+89 -90
View File
@@ -1,118 +1,117 @@
using System;
using SkiaSharp;
namespace Artemis.Core
namespace Artemis.Core;
internal class Renderer : IDisposable
{
internal class Renderer : IDisposable
private bool _disposed;
private SKRect _lastBounds;
private GRContext? _lastGraphicsContext;
private SKRect _lastParentBounds;
private bool _valid;
public SKSurface? Surface { get; private set; }
public SKPaint? Paint { get; private set; }
public SKPath? Path { get; private set; }
public SKPoint TargetLocation { get; private set; }
public bool IsOpen { get; private set; }
/// <summary>
/// Opens the render context using the dimensions of the provided path
/// </summary>
public void Open(SKPath path, Folder? parent)
{
private bool _valid;
private bool _disposed;
private SKRect _lastBounds;
private SKRect _lastParentBounds;
private GRContext? _lastGraphicsContext;
public SKSurface? Surface { get; private set; }
public SKPaint? Paint { get; private set; }
public SKPath? Path { get; private set; }
public SKPoint TargetLocation { get; private set; }
if (_disposed)
throw new ObjectDisposedException("Renderer");
public bool IsOpen { get; private set; }
if (IsOpen)
throw new ArtemisCoreException("Cannot open render context because it is already open");
/// <summary>
/// Opens the render context using the dimensions of the provided path
/// </summary>
public void Open(SKPath path, Folder? parent)
if (path.Bounds != _lastBounds || (parent != null && parent.Bounds != _lastParentBounds) || _lastGraphicsContext != Constants.ManagedGraphicsContext?.GraphicsContext)
Invalidate();
if (!_valid || Surface == null)
{
if (_disposed)
throw new ObjectDisposedException("Renderer");
SKRect pathBounds = path.Bounds;
int width = (int) pathBounds.Width;
int height = (int) pathBounds.Height;
if (IsOpen)
throw new ArtemisCoreException("Cannot open render context because it is already open");
SKImageInfo imageInfo = new(width, height);
if (Constants.ManagedGraphicsContext?.GraphicsContext == null)
Surface = SKSurface.Create(imageInfo);
else
Surface = SKSurface.Create(Constants.ManagedGraphicsContext.GraphicsContext, true, imageInfo);
if (path.Bounds != _lastBounds || (parent != null && parent.Bounds != _lastParentBounds) || _lastGraphicsContext != Constants.ManagedGraphicsContext?.GraphicsContext)
Invalidate();
Path = new SKPath(path);
Path.Transform(SKMatrix.CreateTranslation(pathBounds.Left * -1, pathBounds.Top * -1));
if (!_valid || Surface == null)
{
SKRect pathBounds = path.Bounds;
int width = (int) pathBounds.Width;
int height = (int) pathBounds.Height;
TargetLocation = new SKPoint(pathBounds.Location.X, pathBounds.Location.Y);
if (parent != null)
TargetLocation -= parent.Bounds.Location;
SKImageInfo imageInfo = new(width, height);
if (Constants.ManagedGraphicsContext?.GraphicsContext == null)
Surface = SKSurface.Create(imageInfo);
else
Surface = SKSurface.Create(Constants.ManagedGraphicsContext.GraphicsContext, true, imageInfo);
Surface.Canvas.ClipPath(Path);
Path = new SKPath(path);
Path.Transform(SKMatrix.CreateTranslation(pathBounds.Left * -1, pathBounds.Top * -1));
TargetLocation = new SKPoint(pathBounds.Location.X, pathBounds.Location.Y);
if (parent != null)
TargetLocation -= parent.Bounds.Location;
Surface.Canvas.ClipPath(Path);
_lastParentBounds = parent?.Bounds ?? new SKRect();
_lastBounds = path.Bounds;
_lastGraphicsContext = Constants.ManagedGraphicsContext?.GraphicsContext;
_valid = true;
}
Paint = new SKPaint();
Surface.Canvas.Clear();
Surface.Canvas.Save();
IsOpen = true;
_lastParentBounds = parent?.Bounds ?? new SKRect();
_lastBounds = path.Bounds;
_lastGraphicsContext = Constants.ManagedGraphicsContext?.GraphicsContext;
_valid = true;
}
public void Close()
{
if (_disposed)
throw new ObjectDisposedException("Renderer");
Paint = new SKPaint();
Surface?.Canvas.Restore();
Surface.Canvas.Clear();
Surface.Canvas.Save();
// Looks like every part of the paint needs to be disposed :(
Paint?.ColorFilter?.Dispose();
Paint?.ImageFilter?.Dispose();
Paint?.MaskFilter?.Dispose();
Paint?.PathEffect?.Dispose();
Paint?.Dispose();
IsOpen = true;
}
Paint = null;
public void Close()
{
if (_disposed)
throw new ObjectDisposedException("Renderer");
IsOpen = false;
}
Surface?.Canvas.Restore();
public void Invalidate()
{
if (_disposed)
throw new ObjectDisposedException("Renderer");
// Looks like every part of the paint needs to be disposed :(
Paint?.ColorFilter?.Dispose();
Paint?.ImageFilter?.Dispose();
Paint?.MaskFilter?.Dispose();
Paint?.PathEffect?.Dispose();
Paint?.Dispose();
_valid = false;
}
Paint = null;
public void Dispose()
{
if (IsOpen)
Close();
IsOpen = false;
}
Surface?.Dispose();
Paint?.Dispose();
Path?.Dispose();
public void Invalidate()
{
if (_disposed)
throw new ObjectDisposedException("Renderer");
Surface = null;
Paint = null;
Path = null;
_valid = false;
}
_disposed = true;
}
~Renderer()
{
if (IsOpen)
Close();
}
~Renderer()
{
if (IsOpen)
Close();
}
public void Dispose()
{
if (IsOpen)
Close();
Surface?.Dispose();
Paint?.Dispose();
Path?.Dispose();
Surface = null;
Paint = null;
Path = null;
_disposed = true;
}
}
+1 -5
View File
@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core;
@@ -67,7 +64,6 @@ public class Timeline : CorePropertyChanged, IStorageModel
}
/// <summary>
/// Gets a boolean indicating whether the timeline has finished its run
/// </summary>
@@ -391,4 +387,4 @@ internal enum TimelineSegment
Start,
Main,
End
}
}
@@ -1,65 +1,63 @@
using Artemis.Core.Services;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a key or combination of keys that changes the suspension status of a <see cref="ProfileConfiguration" />
/// </summary>
public class Hotkey : CorePropertyChanged, IStorageModel
{
/// <summary>
/// Represents a key or combination of keys that changes the suspension status of a <see cref="ProfileConfiguration"/>
/// Creates a new instance of <see cref="Hotkey" />
/// </summary>
public class Hotkey : CorePropertyChanged, IStorageModel
public Hotkey()
{
/// <summary>
/// Creates a new instance of <see cref="Hotkey" />
/// </summary>
public Hotkey()
{
Entity = new ProfileConfigurationHotkeyEntity();
}
internal Hotkey(ProfileConfigurationHotkeyEntity entity)
{
Entity = entity;
Load();
}
internal ProfileConfigurationHotkeyEntity Entity { get; }
/// <summary>
/// Gets or sets the <see cref="KeyboardKey" /> of the hotkey
/// </summary>
public KeyboardKey? Key { get; set; }
/// <summary>
/// Gets or sets the <see cref="KeyboardModifierKey" />s of the hotkey
/// </summary>
public KeyboardModifierKey? Modifiers { get; set; }
/// <summary>
/// Determines whether the provided <see cref="ArtemisKeyboardKeyEventArgs" /> match the hotkey
/// </summary>
/// <returns><see langword="true" /> if the event args match the hotkey; otherwise <see langword="false" /></returns>
public bool MatchesEventArgs(ArtemisKeyboardKeyEventArgs eventArgs)
{
return eventArgs.Key == Key && eventArgs.Modifiers == Modifiers;
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
Key = (KeyboardKey?) Entity.Key;
Modifiers = (KeyboardModifierKey?) Entity.Modifiers;
}
/// <inheritdoc />
public void Save()
{
Entity.Key = (int?) Key;
Entity.Modifiers = (int?) Modifiers;
}
#endregion
Entity = new ProfileConfigurationHotkeyEntity();
}
internal Hotkey(ProfileConfigurationHotkeyEntity entity)
{
Entity = entity;
Load();
}
/// <summary>
/// Gets or sets the <see cref="KeyboardKey" /> of the hotkey
/// </summary>
public KeyboardKey? Key { get; set; }
/// <summary>
/// Gets or sets the <see cref="KeyboardModifierKey" />s of the hotkey
/// </summary>
public KeyboardModifierKey? Modifiers { get; set; }
internal ProfileConfigurationHotkeyEntity Entity { get; }
/// <summary>
/// Determines whether the provided <see cref="ArtemisKeyboardKeyEventArgs" /> match the hotkey
/// </summary>
/// <returns><see langword="true" /> if the event args match the hotkey; otherwise <see langword="false" /></returns>
public bool MatchesEventArgs(ArtemisKeyboardKeyEventArgs eventArgs)
{
return eventArgs.Key == Key && eventArgs.Modifiers == Modifiers;
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
Key = (KeyboardKey?) Entity.Key;
Modifiers = (KeyboardModifierKey?) Entity.Modifiers;
}
/// <inheritdoc />
public void Save()
{
Entity.Key = (int?) Key;
Entity.Modifiers = (int?) Modifiers;
}
#endregion
}
@@ -4,376 +4,373 @@ using System.Linq;
using Artemis.Core.Modules;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents the configuration of a profile, contained in a <see cref="ProfileCategory" />
/// </summary>
public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable
{
private ActivationBehaviour _activationBehaviour;
private bool _activationConditionMet;
private ProfileCategory _category;
private Hotkey? _disableHotkey;
private bool _disposed;
private Hotkey? _enableHotkey;
private ProfileConfigurationHotkeyMode _hotkeyMode;
private bool _isBeingEdited;
private bool _isMissingModule;
private bool _isSuspended;
private Module? _module;
private string _name;
private int _order;
private Profile? _profile;
internal ProfileConfiguration(ProfileCategory category, string name, string icon)
{
_name = name;
_category = category;
Entity = new ProfileConfigurationEntity();
Icon = new ProfileConfigurationIcon(Entity);
Icon.SetIconByName(icon);
ActivationCondition = new NodeScript<bool>("Activate profile", "Whether or not the profile should be active", this);
}
internal ProfileConfiguration(ProfileCategory category, ProfileConfigurationEntity entity)
{
// Will be loaded from the entity
_name = null!;
_category = category;
Entity = entity;
Icon = new ProfileConfigurationIcon(Entity);
ActivationCondition = new NodeScript<bool>("Activate profile", "Whether or not the profile should be active", this);
Load();
}
/// <summary>
/// Gets or sets the name of this profile configuration
/// </summary>
public string Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// The order in which this profile 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 this profile is suspended, disabling it regardless of the
/// <see cref="ActivationCondition" />
/// </summary>
public bool IsSuspended
{
get => _isSuspended;
set => SetAndNotify(ref _isSuspended, value);
}
/// <summary>
/// Gets a boolean indicating whether this profile configuration is missing any modules
/// </summary>
public bool IsMissingModule
{
get => _isMissingModule;
private set => SetAndNotify(ref _isMissingModule, value);
}
/// <summary>
/// Gets or sets the category of this profile configuration
/// </summary>
public ProfileCategory Category
{
get => _category;
internal set => SetAndNotify(ref _category, value);
}
/// <summary>
/// Gets or sets the <see cref="ProfileConfigurationHotkeyMode" /> used to determine hotkey behaviour
/// </summary>
public ProfileConfigurationHotkeyMode HotkeyMode
{
get => _hotkeyMode;
set => SetAndNotify(ref _hotkeyMode, value);
}
/// <summary>
/// Gets or sets the hotkey used to enable or toggle the profile
/// </summary>
public Hotkey? EnableHotkey
{
get => _enableHotkey;
set => SetAndNotify(ref _enableHotkey, value);
}
/// <summary>
/// Gets or sets the hotkey used to disable the profile
/// </summary>
public Hotkey? DisableHotkey
{
get => _disableHotkey;
set => SetAndNotify(ref _disableHotkey, value);
}
/// <summary>
/// Gets or sets the behaviour of when this profile is activated
/// </summary>
public ActivationBehaviour ActivationBehaviour
{
get => _activationBehaviour;
set => SetAndNotify(ref _activationBehaviour, value);
}
/// <summary>
/// Gets a boolean indicating whether the activation conditions where met during the last <see cref="Update" /> call
/// </summary>
public bool ActivationConditionMet
{
get => _activationConditionMet;
private set => SetAndNotify(ref _activationConditionMet, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether this profile configuration is being edited
/// </summary>
public bool IsBeingEdited
{
get => _isBeingEdited;
set => SetAndNotify(ref _isBeingEdited, value);
}
/// <summary>
/// Gets the profile of this profile configuration
/// </summary>
public Profile? Profile
{
get => _profile;
internal set => SetAndNotify(ref _profile, value);
}
/// <summary>
/// Gets or sets the module this profile uses
/// </summary>
public Module? Module
{
get => _module;
set
{
SetAndNotify(ref _module, value);
IsMissingModule = false;
}
}
/// <summary>
/// Gets the icon configuration
/// </summary>
public ProfileConfigurationIcon Icon { get; }
/// <summary>
/// Gets the data model condition that must evaluate to <see langword="true" /> for this profile to be activated
/// alongside any activation requirements of the <see cref="Module" />, if set
/// </summary>
public NodeScript<bool> ActivationCondition { get; }
/// <summary>
/// Gets the entity used by this profile config
/// </summary>
public ProfileConfigurationEntity Entity { get; }
/// <summary>
/// Gets the ID of the profile of this profile configuration
/// </summary>
public Guid ProfileId => Entity.ProfileId;
#region Overrides of BreakableModel
/// <inheritdoc />
public override string BrokenDisplayName => "Profile Configuration";
#endregion
/// <summary>
/// Updates this configurations activation condition status
/// </summary>
public void Update()
{
if (_disposed)
throw new ObjectDisposedException("ProfileConfiguration");
if (!ActivationCondition.ExitNodeConnected)
{
ActivationConditionMet = true;
}
else
{
ActivationCondition.Run();
ActivationConditionMet = ActivationCondition.Result;
}
}
/// <summary>
/// Determines whether the profile of this configuration should be active
/// </summary>
/// <param name="includeActivationCondition">Whether or not to take activation conditions into consideration</param>
public bool ShouldBeActive(bool includeActivationCondition)
{
if (_disposed)
throw new ObjectDisposedException("ProfileConfiguration");
if (IsBeingEdited)
return true;
if (Category.IsSuspended || IsSuspended || IsMissingModule)
return false;
if (includeActivationCondition)
return ActivationConditionMet && (Module == null || Module.IsActivated);
return Module == null || Module.IsActivated;
}
/// <inheritdoc />
public override string ToString()
{
return $"[ProfileConfiguration] {nameof(Name)}: {Name}";
}
internal void LoadModules(List<Module> enabledModules)
{
if (_disposed)
throw new ObjectDisposedException("ProfileConfiguration");
Module = enabledModules.FirstOrDefault(m => m.Id == Entity.ModuleId);
IsMissingModule = Module == null && Entity.ModuleId != null;
}
/// <inheritdoc />
public void Dispose()
{
_disposed = true;
ActivationCondition.Dispose();
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
if (_disposed)
throw new ObjectDisposedException("ProfileConfiguration");
Name = Entity.Name;
IsSuspended = Entity.IsSuspended;
ActivationBehaviour = (ActivationBehaviour) Entity.ActivationBehaviour;
HotkeyMode = (ProfileConfigurationHotkeyMode) Entity.HotkeyMode;
Order = Entity.Order;
Icon.Load();
if (Entity.ActivationCondition != null)
ActivationCondition.LoadFromEntity(Entity.ActivationCondition);
EnableHotkey = Entity.EnableHotkey != null ? new Hotkey(Entity.EnableHotkey) : null;
DisableHotkey = Entity.DisableHotkey != null ? new Hotkey(Entity.DisableHotkey) : null;
}
/// <inheritdoc />
public void Save()
{
if (_disposed)
throw new ObjectDisposedException("ProfileConfiguration");
Entity.Name = Name;
Entity.IsSuspended = IsSuspended;
Entity.ActivationBehaviour = (int) ActivationBehaviour;
Entity.HotkeyMode = (int) HotkeyMode;
Entity.ProfileCategoryId = Category.Entity.Id;
Entity.Order = Order;
Icon.Save();
ActivationCondition.Save();
Entity.ActivationCondition = ActivationCondition.Entity;
EnableHotkey?.Save();
Entity.EnableHotkey = EnableHotkey?.Entity;
DisableHotkey?.Save();
Entity.DisableHotkey = DisableHotkey?.Entity;
if (!IsMissingModule)
Entity.ModuleId = Module?.Id;
}
#endregion
}
/// <summary>
/// Represents a type of behaviour when this profile is activated
/// </summary>
public enum ActivationBehaviour
{
/// <summary>
/// Represents the configuration of a profile, contained in a <see cref="ProfileCategory" />
/// Do nothing to other profiles
/// </summary>
public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable
{
private bool _disposed;
private string _name;
private int _order;
private bool _isSuspended;
private bool _isMissingModule;
private ProfileCategory _category;
private ProfileConfigurationHotkeyMode _hotkeyMode;
private Hotkey? _enableHotkey;
private Hotkey? _disableHotkey;
private ActivationBehaviour _activationBehaviour;
private bool _activationConditionMet;
private bool _isBeingEdited;
private Profile? _profile;
private Module? _module;
internal ProfileConfiguration(ProfileCategory category, string name, string icon)
{
_name = name;
_category = category;
Entity = new ProfileConfigurationEntity();
Icon = new ProfileConfigurationIcon(Entity);
Icon.SetIconByName(icon);
ActivationCondition = new NodeScript<bool>("Activate profile", "Whether or not the profile should be active", this);
}
internal ProfileConfiguration(ProfileCategory category, ProfileConfigurationEntity entity)
{
// Will be loaded from the entity
_name = null!;
_category = category;
Entity = entity;
Icon = new ProfileConfigurationIcon(Entity);
ActivationCondition = new NodeScript<bool>("Activate profile", "Whether or not the profile should be active", this);
Load();
}
/// <summary>
/// Gets or sets the name of this profile configuration
/// </summary>
public string Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// The order in which this profile 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 this profile is suspended, disabling it regardless of the
/// <see cref="ActivationCondition" />
/// </summary>
public bool IsSuspended
{
get => _isSuspended;
set => SetAndNotify(ref _isSuspended, value);
}
/// <summary>
/// Gets a boolean indicating whether this profile configuration is missing any modules
/// </summary>
public bool IsMissingModule
{
get => _isMissingModule;
private set => SetAndNotify(ref _isMissingModule, value);
}
/// <summary>
/// Gets or sets the category of this profile configuration
/// </summary>
public ProfileCategory Category
{
get => _category;
internal set => SetAndNotify(ref _category, value);
}
/// <summary>
/// Gets or sets the <see cref="ProfileConfigurationHotkeyMode" /> used to determine hotkey behaviour
/// </summary>
public ProfileConfigurationHotkeyMode HotkeyMode
{
get => _hotkeyMode;
set => SetAndNotify(ref _hotkeyMode, value);
}
/// <summary>
/// Gets or sets the hotkey used to enable or toggle the profile
/// </summary>
public Hotkey? EnableHotkey
{
get => _enableHotkey;
set => SetAndNotify(ref _enableHotkey, value);
}
/// <summary>
/// Gets or sets the hotkey used to disable the profile
/// </summary>
public Hotkey? DisableHotkey
{
get => _disableHotkey;
set => SetAndNotify(ref _disableHotkey, value);
}
/// <summary>
/// Gets or sets the behaviour of when this profile is activated
/// </summary>
public ActivationBehaviour ActivationBehaviour
{
get => _activationBehaviour;
set => SetAndNotify(ref _activationBehaviour, value);
}
/// <summary>
/// Gets a boolean indicating whether the activation conditions where met during the last <see cref="Update" /> call
/// </summary>
public bool ActivationConditionMet
{
get => _activationConditionMet;
private set => SetAndNotify(ref _activationConditionMet, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether this profile configuration is being edited
/// </summary>
public bool IsBeingEdited
{
get => _isBeingEdited;
set => SetAndNotify(ref _isBeingEdited, value);
}
/// <summary>
/// Gets the profile of this profile configuration
/// </summary>
public Profile? Profile
{
get => _profile;
internal set => SetAndNotify(ref _profile, value);
}
/// <summary>
/// Gets or sets the module this profile uses
/// </summary>
public Module? Module
{
get => _module;
set
{
SetAndNotify(ref _module, value);
IsMissingModule = false;
}
}
/// <summary>
/// Gets the icon configuration
/// </summary>
public ProfileConfigurationIcon Icon { get; }
/// <summary>
/// Gets the data model condition that must evaluate to <see langword="true" /> for this profile to be activated
/// alongside any activation requirements of the <see cref="Module" />, if set
/// </summary>
public NodeScript<bool> ActivationCondition { get; }
/// <summary>
/// Gets the entity used by this profile config
/// </summary>
public ProfileConfigurationEntity Entity { get; }
/// <summary>
/// Gets the ID of the profile of this profile configuration
/// </summary>
public Guid ProfileId => Entity.ProfileId;
/// <summary>
/// Updates this configurations activation condition status
/// </summary>
public void Update()
{
if (_disposed)
throw new ObjectDisposedException("ProfileConfiguration");
if (!ActivationCondition.ExitNodeConnected)
ActivationConditionMet = true;
else
{
ActivationCondition.Run();
ActivationConditionMet = ActivationCondition.Result;
}
}
/// <summary>
/// Determines whether the profile of this configuration should be active
/// </summary>
/// <param name="includeActivationCondition">Whether or not to take activation conditions into consideration</param>
public bool ShouldBeActive(bool includeActivationCondition)
{
if (_disposed)
throw new ObjectDisposedException("ProfileConfiguration");
if (IsBeingEdited)
return true;
if (Category.IsSuspended || IsSuspended || IsMissingModule)
return false;
if (includeActivationCondition)
return ActivationConditionMet && (Module == null || Module.IsActivated);
return Module == null || Module.IsActivated;
}
/// <inheritdoc />
public override string ToString()
{
return $"[ProfileConfiguration] {nameof(Name)}: {Name}";
}
internal void LoadModules(List<Module> enabledModules)
{
if (_disposed)
throw new ObjectDisposedException("ProfileConfiguration");
Module = enabledModules.FirstOrDefault(m => m.Id == Entity.ModuleId);
IsMissingModule = Module == null && Entity.ModuleId != null;
}
#region IDisposable
/// <inheritdoc />
public void Dispose()
{
_disposed = true;
ActivationCondition.Dispose();
}
#endregion
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
if (_disposed)
throw new ObjectDisposedException("ProfileConfiguration");
Name = Entity.Name;
IsSuspended = Entity.IsSuspended;
ActivationBehaviour = (ActivationBehaviour) Entity.ActivationBehaviour;
HotkeyMode = (ProfileConfigurationHotkeyMode) Entity.HotkeyMode;
Order = Entity.Order;
Icon.Load();
if (Entity.ActivationCondition != null)
ActivationCondition.LoadFromEntity(Entity.ActivationCondition);
EnableHotkey = Entity.EnableHotkey != null ? new Hotkey(Entity.EnableHotkey) : null;
DisableHotkey = Entity.DisableHotkey != null ? new Hotkey(Entity.DisableHotkey) : null;
}
/// <inheritdoc />
public void Save()
{
if (_disposed)
throw new ObjectDisposedException("ProfileConfiguration");
Entity.Name = Name;
Entity.IsSuspended = IsSuspended;
Entity.ActivationBehaviour = (int) ActivationBehaviour;
Entity.HotkeyMode = (int) HotkeyMode;
Entity.ProfileCategoryId = Category.Entity.Id;
Entity.Order = Order;
Icon.Save();
ActivationCondition.Save();
Entity.ActivationCondition = ActivationCondition.Entity;
EnableHotkey?.Save();
Entity.EnableHotkey = EnableHotkey?.Entity;
DisableHotkey?.Save();
Entity.DisableHotkey = DisableHotkey?.Entity;
if (!IsMissingModule)
Entity.ModuleId = Module?.Id;
}
#endregion
#region Overrides of BreakableModel
/// <inheritdoc />
public override string BrokenDisplayName => "Profile Configuration";
#endregion
}
None,
/// <summary>
/// Represents a type of behaviour when this profile is activated
/// Disable all other profiles
/// </summary>
public enum ActivationBehaviour
{
/// <summary>
/// Do nothing to other profiles
/// </summary>
None,
/// <summary>
/// Disable all other profiles
/// </summary>
DisableOthers,
/// <summary>
/// Disable all other profiles below this one
/// </summary>
DisableOthersBelow,
/// <summary>
/// Disable all other profiles above this one
/// </summary>
DisableOthersAbove,
/// <summary>
/// Disable all other profiles in the same category
/// </summary>
DisableOthersInCategory,
/// <summary>
/// Disable all other profiles below this one in the same category
/// </summary>
DisableOthersBelowInCategory,
/// <summary>
/// Disable all other profiles above this one in the same category
/// </summary>
DisableOthersAboveInCategory
}
DisableOthers,
/// <summary>
/// Represents a hotkey mode for a profile configuration
/// Disable all other profiles below this one
/// </summary>
public enum ProfileConfigurationHotkeyMode
{
/// <summary>
/// Use no hotkeys
/// </summary>
None,
DisableOthersBelow,
/// <summary>
/// Toggle the profile with one hotkey
/// </summary>
Toggle,
/// <summary>
/// Disable all other profiles above this one
/// </summary>
DisableOthersAbove,
/// <summary>
/// Enable and disable the profile with two separate hotkeys
/// </summary>
EnableDisable
}
/// <summary>
/// Disable all other profiles in the same category
/// </summary>
DisableOthersInCategory,
/// <summary>
/// Disable all other profiles below this one in the same category
/// </summary>
DisableOthersBelowInCategory,
/// <summary>
/// Disable all other profiles above this one in the same category
/// </summary>
DisableOthersAboveInCategory
}
/// <summary>
/// Represents a hotkey mode for a profile configuration
/// </summary>
public enum ProfileConfigurationHotkeyMode
{
/// <summary>
/// Use no hotkeys
/// </summary>
None,
/// <summary>
/// Toggle the profile with one hotkey
/// </summary>
Toggle,
/// <summary>
/// Enable and disable the profile with two separate hotkeys
/// </summary>
EnableDisable
}
@@ -4,34 +4,33 @@ using Artemis.Core.JsonConverters;
using Artemis.Storage.Entities.Profile;
using Newtonsoft.Json;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// A model that can be used to serialize a profile configuration, it's profile and it's icon
/// </summary>
public class ProfileConfigurationExportModel : IDisposable
{
/// <summary>
/// A model that can be used to serialize a profile configuration, it's profile and it's icon
/// Gets or sets the storage entity of the profile configuration
/// </summary>
public class ProfileConfigurationExportModel : IDisposable
public ProfileConfigurationEntity? ProfileConfigurationEntity { get; set; }
/// <summary>
/// Gets or sets the storage entity of the profile
/// </summary>
[JsonProperty(Required = Required.Always)]
public ProfileEntity ProfileEntity { get; set; } = null!;
/// <summary>
/// Gets or sets a stream containing the profile image
/// </summary>
[JsonConverter(typeof(StreamConverter))]
public Stream? ProfileImage { get; set; }
/// <inheritdoc />
public void Dispose()
{
/// <summary>
/// Gets or sets the storage entity of the profile configuration
/// </summary>
public ProfileConfigurationEntity? ProfileConfigurationEntity { get; set; }
/// <summary>
/// Gets or sets the storage entity of the profile
/// </summary>
[JsonProperty(Required = Required.Always)]
public ProfileEntity ProfileEntity { get; set; } = null!;
/// <summary>
/// Gets or sets a stream containing the profile image
/// </summary>
[JsonConverter(typeof(StreamConverter))]
public Stream? ProfileImage { get; set; }
/// <inheritdoc />
public void Dispose()
{
ProfileImage?.Dispose();
}
ProfileImage?.Dispose();
}
}
@@ -3,168 +3,167 @@ using System.ComponentModel;
using System.IO;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents the icon of a <see cref="ProfileConfiguration" />
/// </summary>
public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel
{
private readonly ProfileConfigurationEntity _entity;
private bool _fill;
private string? _iconName;
private Stream? _iconStream;
private ProfileConfigurationIconType _iconType;
private string? _originalFileName;
internal ProfileConfigurationIcon(ProfileConfigurationEntity entity)
{
_entity = entity;
}
/// <summary>
/// Gets the type of icon this profile configuration uses
/// </summary>
public ProfileConfigurationIconType IconType
{
get => _iconType;
private set => SetAndNotify(ref _iconType, value);
}
/// <summary>
/// Gets the name of the icon if <see cref="IconType" /> is <see cref="ProfileConfigurationIconType.MaterialIcon" />
/// </summary>
public string? IconName
{
get => _iconName;
private set => SetAndNotify(ref _iconName, value);
}
/// <summary>
/// Gets the original file name of the icon (if applicable)
/// </summary>
public string? OriginalFileName
{
get => _originalFileName;
private set => SetAndNotify(ref _originalFileName, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether or not this icon should be filled.
/// </summary>
public bool Fill
{
get => _fill;
set => SetAndNotify(ref _fill, value);
}
/// <summary>
/// Updates the <see cref="IconName" /> to the provided value and changes the <see cref="IconType" /> is
/// <see cref="ProfileConfigurationIconType.MaterialIcon" />
/// </summary>
/// <param name="iconName">The name of the icon</param>
public void SetIconByName(string iconName)
{
if (iconName == null) throw new ArgumentNullException(nameof(iconName));
_iconStream?.Dispose();
IconName = iconName;
OriginalFileName = null;
IconType = ProfileConfigurationIconType.MaterialIcon;
OnIconUpdated();
}
/// <summary>
/// Updates the stream returned by <see cref="GetIconStream" /> to the provided stream
/// </summary>
/// <param name="originalFileName">The original file name backing the stream, should include the extension</param>
/// <param name="stream">The stream to copy</param>
public void SetIconByStream(string originalFileName, Stream stream)
{
if (originalFileName == null) throw new ArgumentNullException(nameof(originalFileName));
if (stream == null) throw new ArgumentNullException(nameof(stream));
_iconStream?.Dispose();
_iconStream = new MemoryStream();
stream.Seek(0, SeekOrigin.Begin);
stream.CopyTo(_iconStream);
_iconStream.Seek(0, SeekOrigin.Begin);
IconName = null;
OriginalFileName = originalFileName;
IconType = ProfileConfigurationIconType.BitmapImage;
OnIconUpdated();
}
/// <summary>
/// Creates a copy of the stream containing the icon
/// </summary>
/// <returns>A stream containing the icon</returns>
public Stream? GetIconStream()
{
if (_iconStream == null)
return null;
MemoryStream stream = new();
_iconStream.CopyTo(stream);
stream.Seek(0, SeekOrigin.Begin);
_iconStream.Seek(0, SeekOrigin.Begin);
return stream;
}
/// <summary>
/// Occurs when the icon was updated
/// </summary>
public event EventHandler? IconUpdated;
/// <summary>
/// Invokes the <see cref="IconUpdated" /> event
/// </summary>
protected virtual void OnIconUpdated()
{
IconUpdated?.Invoke(this, EventArgs.Empty);
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
IconType = (ProfileConfigurationIconType) _entity.IconType;
Fill = _entity.IconFill;
if (IconType != ProfileConfigurationIconType.MaterialIcon)
return;
IconName = _entity.MaterialIcon;
OnIconUpdated();
}
/// <inheritdoc />
public void Save()
{
_entity.IconType = (int) IconType;
_entity.MaterialIcon = IconType == ProfileConfigurationIconType.MaterialIcon ? IconName : null;
_entity.IconFill = Fill;
}
#endregion
}
/// <summary>
/// Represents a type of profile icon
/// </summary>
public enum ProfileConfigurationIconType
{
/// <summary>
/// Represents the icon of a <see cref="ProfileConfiguration" />
/// An icon picked from the Material Design Icons collection
/// </summary>
public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel
{
private readonly ProfileConfigurationEntity _entity;
private Stream? _iconStream;
private ProfileConfigurationIconType _iconType;
private string? _iconName;
private string? _originalFileName;
private bool _fill;
internal ProfileConfigurationIcon(ProfileConfigurationEntity entity)
{
_entity = entity;
}
/// <summary>
/// Gets the type of icon this profile configuration uses
/// </summary>
public ProfileConfigurationIconType IconType
{
get => _iconType;
private set => SetAndNotify(ref _iconType, value);
}
/// <summary>
/// Gets the name of the icon if <see cref="IconType" /> is <see cref="ProfileConfigurationIconType.MaterialIcon" />
/// </summary>
public string? IconName
{
get => _iconName;
private set => SetAndNotify(ref _iconName, value);
}
/// <summary>
/// Gets the original file name of the icon (if applicable)
/// </summary>
public string? OriginalFileName
{
get => _originalFileName;
private set => SetAndNotify(ref _originalFileName, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether or not this icon should be filled.
/// </summary>
public bool Fill
{
get => _fill;
set => SetAndNotify(ref _fill, value);
}
/// <summary>
/// Updates the <see cref="IconName" /> to the provided value and changes the <see cref="IconType" /> is
/// <see cref="ProfileConfigurationIconType.MaterialIcon" />
/// </summary>
/// <param name="iconName">The name of the icon</param>
public void SetIconByName(string iconName)
{
if (iconName == null) throw new ArgumentNullException(nameof(iconName));
_iconStream?.Dispose();
IconName = iconName;
OriginalFileName = null;
IconType = ProfileConfigurationIconType.MaterialIcon;
OnIconUpdated();
}
/// <summary>
/// Updates the stream returned by <see cref="GetIconStream" /> to the provided stream
/// </summary>
/// <param name="originalFileName">The original file name backing the stream, should include the extension</param>
/// <param name="stream">The stream to copy</param>
public void SetIconByStream(string originalFileName, Stream stream)
{
if (originalFileName == null) throw new ArgumentNullException(nameof(originalFileName));
if (stream == null) throw new ArgumentNullException(nameof(stream));
_iconStream?.Dispose();
_iconStream = new MemoryStream();
stream.Seek(0, SeekOrigin.Begin);
stream.CopyTo(_iconStream);
_iconStream.Seek(0, SeekOrigin.Begin);
IconName = null;
OriginalFileName = originalFileName;
IconType = ProfileConfigurationIconType.BitmapImage;
OnIconUpdated();
}
/// <summary>
/// Creates a copy of the stream containing the icon
/// </summary>
/// <returns>A stream containing the icon</returns>
public Stream? GetIconStream()
{
if (_iconStream == null)
return null;
MemoryStream stream = new();
_iconStream.CopyTo(stream);
stream.Seek(0, SeekOrigin.Begin);
_iconStream.Seek(0, SeekOrigin.Begin);
return stream;
}
/// <summary>
/// Occurs when the icon was updated
/// </summary>
public event EventHandler? IconUpdated;
/// <summary>
/// Invokes the <see cref="IconUpdated" /> event
/// </summary>
protected virtual void OnIconUpdated()
{
IconUpdated?.Invoke(this, EventArgs.Empty);
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
IconType = (ProfileConfigurationIconType) _entity.IconType;
Fill = _entity.IconFill;
if (IconType != ProfileConfigurationIconType.MaterialIcon)
return;
IconName = _entity.MaterialIcon;
OnIconUpdated();
}
/// <inheritdoc />
public void Save()
{
_entity.IconType = (int) IconType;
_entity.MaterialIcon = IconType == ProfileConfigurationIconType.MaterialIcon ? IconName : null;
_entity.IconFill = Fill;
}
#endregion
}
[Description("Material Design Icon")] MaterialIcon,
/// <summary>
/// Represents a type of profile icon
/// A bitmap image icon
/// </summary>
public enum ProfileConfigurationIconType
{
/// <summary>
/// An icon picked from the Material Design Icons collection
/// </summary>
[Description("Material Design Icon")] MaterialIcon,
/// <summary>
/// A bitmap image icon
/// </summary>
[Description("Bitmap Image")] BitmapImage,
}
[Description("Bitmap Image")] BitmapImage
}
File diff suppressed because it is too large. Load diff
@@ -1,33 +1,32 @@
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a device input identifier used by a specific <see cref="Services.InputProvider" /> to identify the
/// device
/// </summary>
public class ArtemisDeviceInputIdentifier
{
/// <summary>
/// Represents a device input identifier used by a specific <see cref="Services.InputProvider" /> to identify the
/// device
/// Creates a new instance of the <see cref="ArtemisDeviceInputIdentifier" /> class
/// </summary>
public class ArtemisDeviceInputIdentifier
/// <param name="inputProvider">
/// The full type and namespace of the <see cref="Services.InputProvider" /> this identifier is
/// used by
/// </param>
/// <param name="identifier">A value used to identify the device</param>
internal ArtemisDeviceInputIdentifier(string inputProvider, object identifier)
{
/// <summary>
/// Creates a new instance of the <see cref="ArtemisDeviceInputIdentifier" /> class
/// </summary>
/// <param name="inputProvider">
/// The full type and namespace of the <see cref="Services.InputProvider" /> this identifier is
/// used by
/// </param>
/// <param name="identifier">A value used to identify the device</param>
internal ArtemisDeviceInputIdentifier(string inputProvider, object identifier)
{
InputProvider = inputProvider;
Identifier = identifier;
}
/// <summary>
/// Gets or sets the full type and namespace of the <see cref="Services.InputProvider" /> this identifier is used by
/// </summary>
public string InputProvider { get; set; }
/// <summary>
/// Gets or sets a value used to identify the device
/// </summary>
public object Identifier { get; set; }
InputProvider = inputProvider;
Identifier = identifier;
}
/// <summary>
/// Gets or sets the full type and namespace of the <see cref="Services.InputProvider" /> this identifier is used by
/// </summary>
public string InputProvider { get; set; }
/// <summary>
/// Gets or sets a value used to identify the device
/// </summary>
public object Identifier { get; set; }
}
+61 -62
View File
@@ -1,76 +1,75 @@
using RGB.NET.Core;
using SkiaSharp;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents an RGB LED contained in an <see cref="ArtemisDevice" />
/// </summary>
public class ArtemisLed : CorePropertyChanged
{
/// <summary>
/// Represents an RGB LED contained in an <see cref="ArtemisDevice" />
/// </summary>
public class ArtemisLed : CorePropertyChanged
private SKRect _absoluteRectangle;
private SKRect _rectangle;
internal ArtemisLed(Led led, ArtemisDevice device)
{
private SKRect _absoluteRectangle;
private SKRect _rectangle;
RgbLed = led;
Device = device;
CalculateRectangles();
}
internal ArtemisLed(Led led, ArtemisDevice device)
{
RgbLed = led;
Device = device;
CalculateRectangles();
}
/// <summary>
/// Gets the RGB.NET LED backing this Artemis LED
/// </summary>
public Led RgbLed { get; }
/// <summary>
/// Gets the RGB.NET LED backing this Artemis LED
/// </summary>
public Led RgbLed { get; }
/// <summary>
/// Gets the device that contains this LED
/// </summary>
public ArtemisDevice Device { get; }
/// <summary>
/// Gets the device that contains this LED
/// </summary>
public ArtemisDevice Device { get; }
/// <summary>
/// Gets the rectangle covering the LED positioned relative to the<see cref="Device" />
/// </summary>
public SKRect Rectangle
{
get => _rectangle;
private set => SetAndNotify(ref _rectangle, value);
}
/// <summary>
/// Gets the rectangle covering the LED positioned relative to the<see cref="Device" />
/// </summary>
public SKRect Rectangle
{
get => _rectangle;
private set => SetAndNotify(ref _rectangle, value);
}
/// <summary>
/// Gets the rectangle covering the LED
/// </summary>
public SKRect AbsoluteRectangle
{
get => _absoluteRectangle;
private set => SetAndNotify(ref _absoluteRectangle, value);
}
/// <summary>
/// Gets the rectangle covering the LED
/// </summary>
public SKRect AbsoluteRectangle
{
get => _absoluteRectangle;
private set => SetAndNotify(ref _absoluteRectangle, value);
}
/// <summary>
/// Gets the layout applied to this LED
/// </summary>
public ArtemisLedLayout? Layout { get; internal set; }
/// <summary>
/// Gets the layout applied to this LED
/// </summary>
public ArtemisLedLayout? Layout { get; internal set; }
/// <inheritdoc />
public override string ToString()
{
return RgbLed.ToString();
}
/// <inheritdoc />
public override string ToString()
{
return RgbLed.ToString();
}
internal void CalculateRectangles()
{
Rectangle = Utilities.CreateScaleCompatibleRect(
RgbLed.Boundary.Location.X,
RgbLed.Boundary.Location.Y,
RgbLed.Boundary.Size.Width,
RgbLed.Boundary.Size.Height
);
AbsoluteRectangle = Utilities.CreateScaleCompatibleRect(
RgbLed.AbsoluteBoundary.Location.X,
RgbLed.AbsoluteBoundary.Location.Y,
RgbLed.AbsoluteBoundary.Size.Width,
RgbLed.AbsoluteBoundary.Size.Height
);
}
internal void CalculateRectangles()
{
Rectangle = Utilities.CreateScaleCompatibleRect(
RgbLed.Boundary.Location.X,
RgbLed.Boundary.Location.Y,
RgbLed.Boundary.Size.Width,
RgbLed.Boundary.Size.Height
);
AbsoluteRectangle = Utilities.CreateScaleCompatibleRect(
RgbLed.AbsoluteBoundary.Location.X,
RgbLed.AbsoluteBoundary.Location.Y,
RgbLed.AbsoluteBoundary.Size.Width,
RgbLed.AbsoluteBoundary.Size.Height
);
}
}
@@ -1,41 +1,40 @@
// ReSharper disable InconsistentNaming
namespace Artemis.Core
namespace Artemis.Core;
// Copied from RGB.NET to avoid needing to reference RGB.NET
/// <summary>
/// Represents a physical layout type for a keyboard
/// </summary>
public enum KeyboardLayoutType
{
// Copied from RGB.NET to avoid needing to reference RGB.NET
/// <summary>
/// Represents a physical layout type for a keyboard
/// An unknown layout type
/// </summary>
public enum KeyboardLayoutType
{
/// <summary>
/// An unknown layout type
/// </summary>
Unknown = 0,
Unknown = 0,
/// <summary>
/// The ANSI layout type, often used in the US (uses a short enter)
/// </summary>
ANSI = 1,
/// <summary>
/// The ANSI layout type, often used in the US (uses a short enter)
/// </summary>
ANSI = 1,
/// <summary>
/// The ISO layout type, often used in the EU (uses a tall enter)
/// </summary>
ISO = 2,
/// <summary>
/// The ISO layout type, often used in the EU (uses a tall enter)
/// </summary>
ISO = 2,
/// <summary>
/// The JIS layout type, often used in Japan (based on ISO)
/// </summary>
JIS = 3,
/// <summary>
/// The JIS layout type, often used in Japan (based on ISO)
/// </summary>
JIS = 3,
/// <summary>
/// The ABNT layout type, often used in Brazil/Portugal (based on ISO)
/// </summary>
ABNT = 4,
/// <summary>
/// The ABNT layout type, often used in Brazil/Portugal (based on ISO)
/// </summary>
ABNT = 4,
/// <summary>
/// The KS layout type, often used in South Korea
/// </summary>
KS = 5
}
/// <summary>
/// The KS layout type, often used in South Korea
/// </summary>
KS = 5
}
@@ -5,244 +5,243 @@ using System.Linq;
using RGB.NET.Core;
using RGB.NET.Layout;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a device layout decorated with extra Artemis-specific data
/// </summary>
public class ArtemisLayout
{
/// <summary>
/// Represents a device layout decorated with extra Artemis-specific data
/// Creates a new instance of the <see cref="ArtemisLayout" /> class
/// </summary>
public class ArtemisLayout
/// <param name="filePath">The path of the layout XML file</param>
/// <param name="source">The source from where this layout is being loaded</param>
public ArtemisLayout(string filePath, LayoutSource source)
{
/// <summary>
/// Creates a new instance of the <see cref="ArtemisLayout" /> class
/// </summary>
/// <param name="filePath">The path of the layout XML file</param>
/// <param name="source">The source from where this layout is being loaded</param>
public ArtemisLayout(string filePath, LayoutSource source)
{
FilePath = filePath;
Source = source;
Leds = new List<ArtemisLedLayout>();
FilePath = filePath;
Source = source;
Leds = new List<ArtemisLedLayout>();
LoadLayout();
}
/// <summary>
/// Gets the file path the layout was (attempted to be) loaded from
/// </summary>
public string FilePath { get; }
/// <summary>
/// Gets the source from where this layout was loaded
/// </summary>
public LayoutSource Source { get; }
/// <summary>
/// Gets the device this layout is applied to
/// </summary>
public ArtemisDevice? Device { get; private set; }
/// <summary>
/// Gets a boolean indicating whether a valid layout was loaded
/// </summary>
public bool IsValid { get; private set; }
/// <summary>
/// Gets the image of the device
/// </summary>
public Uri? Image { get; private set; }
/// <summary>
/// Gets a list of LEDs this layout contains
/// </summary>
public List<ArtemisLedLayout> Leds { get; }
/// <summary>
/// Gets the RGB.NET device layout
/// </summary>
public DeviceLayout RgbLayout { get; private set; } = null!;
/// <summary>
/// Gets the custom layout data embedded in the RGB.NET layout
/// </summary>
public LayoutCustomDeviceData LayoutCustomDeviceData { get; private set; } = null!;
/// <summary>
/// Applies the layout to the provided device
/// </summary>
public void ApplyTo(IRGBDevice device, bool createMissingLeds = false, bool removeExcessiveLeds = false)
{
device.Size = new Size(MathF.Round(RgbLayout.Width), MathF.Round(RgbLayout.Height));
device.DeviceInfo.LayoutMetadata = RgbLayout.CustomData;
HashSet<LedId> ledIds = new();
foreach (ILedLayout layoutLed in RgbLayout.Leds)
{
if (Enum.TryParse(layoutLed.Id, true, out LedId ledId))
{
ledIds.Add(ledId);
Led? led = device[ledId];
if (led == null && createMissingLeds)
led = device.AddLed(ledId, new Point(), new Size());
if (led != null)
{
led.Location = new Point(layoutLed.X, layoutLed.Y);
led.Size = new Size(layoutLed.Width, layoutLed.Height);
led.Shape = layoutLed.Shape;
led.ShapeData = layoutLed.ShapeData;
led.LayoutMetadata = layoutLed.CustomData;
}
}
}
if (removeExcessiveLeds)
{
List<LedId> ledsToRemove = device.Select(led => led.Id).Where(id => !ledIds.Contains(id)).ToList();
foreach (LedId led in ledsToRemove)
device.RemoveLed(led);
}
List<Led> deviceLeds = device.ToList();
foreach (Led led in deviceLeds)
{
float x = led.Location.X;
float y = led.Location.Y;
// Try to move the LED if it falls outside the boundaries of the layout
if (led.Location.X + led.Size.Width > device.Size.Width)
x -= led.Location.X + led.Size.Width - device.Size.Width;
if (led.Location.Y + led.Size.Height > device.Size.Height)
y -= led.Location.Y + led.Size.Height - device.Size.Height;
// If not possible because it's too large we'll have to drop it to avoid rendering issues
if (x < 0 || y < 0)
device.RemoveLed(led.Id);
else
led.Location = new Point(x, y);
}
}
internal void ApplyDevice(ArtemisDevice artemisDevice)
{
Device = artemisDevice;
foreach (ArtemisLedLayout artemisLedLayout in Leds)
artemisLedLayout.ApplyDevice(Device);
}
private void LoadLayout()
{
DeviceLayout? deviceLayout = DeviceLayout.Load(FilePath, typeof(LayoutCustomDeviceData), typeof(LayoutCustomLedData));
if (deviceLayout != null)
{
RgbLayout = deviceLayout;
IsValid = true;
}
else
{
RgbLayout = new DeviceLayout();
IsValid = false;
}
if (IsValid)
Leds.AddRange(RgbLayout.Leds.Select(l => new ArtemisLedLayout(this, l)));
LayoutCustomDeviceData = (LayoutCustomDeviceData?) RgbLayout.CustomData ?? new LayoutCustomDeviceData();
ApplyCustomDeviceData();
}
private void ApplyCustomDeviceData()
{
if (!IsValid)
{
Image = null;
return;
}
Uri layoutDirectory = new(Path.GetDirectoryName(FilePath)! + "/", UriKind.Absolute);
if (LayoutCustomDeviceData.DeviceImage != null)
Image = new Uri(layoutDirectory, new Uri(LayoutCustomDeviceData.DeviceImage, UriKind.Relative));
else
Image = null;
}
internal static ArtemisLayout? GetDefaultLayout(ArtemisDevice device)
{
string layoutFolder = Path.Combine(Constants.ApplicationFolder, "DefaultLayouts", "Artemis");
if (device.DeviceType == RGBDeviceType.Keyboard)
{
// XL layout is defined by its programmable macro keys
if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_Programmable1 && l.RgbLed.Id <= LedId.Keyboard_Programmable32))
{
if (device.PhysicalLayout == KeyboardLayoutType.ANSI)
return new ArtemisLayout(Path.Combine(layoutFolder, "Keyboard", "Artemis XL keyboard-ANSI.xml"), LayoutSource.Default);
return new ArtemisLayout(Path.Combine(layoutFolder, "Keyboard", "Artemis XL keyboard-ISO.xml"), LayoutSource.Default);
}
// L layout is defined by its numpad
if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_NumLock && l.RgbLed.Id <= LedId.Keyboard_NumPeriodAndDelete))
{
if (device.PhysicalLayout == KeyboardLayoutType.ANSI)
return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard","Artemis L keyboard-ANSI.xml"), LayoutSource.Default);
return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard","Artemis L keyboard-ISO.xml"), LayoutSource.Default);
}
// No numpad will result in TKL
if (device.PhysicalLayout == KeyboardLayoutType.ANSI)
return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard","Artemis TKL keyboard-ANSI.xml"), LayoutSource.Default);
return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard","Artemis TKL keyboard-ISO.xml"), LayoutSource.Default);
}
// if (device.DeviceType == RGBDeviceType.Mouse)
// {
// if (device.Leds.Count == 1)
// {
// if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo))
// return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "1 LED mouse logo.xml"), LayoutSource.Default);
// return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "1 LED mouse.xml"), LayoutSource.Default);
// }
// if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo))
// return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "4 LED mouse logo.xml"), LayoutSource.Default);
// return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "4 LED mouse.xml"), LayoutSource.Default);
// }
if (device.DeviceType == RGBDeviceType.Headset)
{
if (device.Leds.Count == 1)
return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 1 LED headset.xml"), LayoutSource.Default);
if (device.Leds.Count == 2)
return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 2 LED headset.xml"), LayoutSource.Default);
return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 4 LED headset.xml"), LayoutSource.Default);
}
return null;
}
LoadLayout();
}
/// <summary>
/// Represents a source from where a layout came
/// Gets the file path the layout was (attempted to be) loaded from
/// </summary>
public enum LayoutSource
public string FilePath { get; }
/// <summary>
/// Gets the source from where this layout was loaded
/// </summary>
public LayoutSource Source { get; }
/// <summary>
/// Gets the device this layout is applied to
/// </summary>
public ArtemisDevice? Device { get; private set; }
/// <summary>
/// Gets a boolean indicating whether a valid layout was loaded
/// </summary>
public bool IsValid { get; private set; }
/// <summary>
/// Gets the image of the device
/// </summary>
public Uri? Image { get; private set; }
/// <summary>
/// Gets a list of LEDs this layout contains
/// </summary>
public List<ArtemisLedLayout> Leds { get; }
/// <summary>
/// Gets the RGB.NET device layout
/// </summary>
public DeviceLayout RgbLayout { get; private set; } = null!;
/// <summary>
/// Gets the custom layout data embedded in the RGB.NET layout
/// </summary>
public LayoutCustomDeviceData LayoutCustomDeviceData { get; private set; } = null!;
/// <summary>
/// Applies the layout to the provided device
/// </summary>
public void ApplyTo(IRGBDevice device, bool createMissingLeds = false, bool removeExcessiveLeds = false)
{
/// <summary>
/// A layout loaded from config
/// </summary>
Configured,
device.Size = new Size(MathF.Round(RgbLayout.Width), MathF.Round(RgbLayout.Height));
device.DeviceInfo.LayoutMetadata = RgbLayout.CustomData;
/// <summary>
/// A layout loaded from the user layout folder
/// </summary>
User,
HashSet<LedId> ledIds = new();
foreach (ILedLayout layoutLed in RgbLayout.Leds)
{
if (Enum.TryParse(layoutLed.Id, true, out LedId ledId))
{
ledIds.Add(ledId);
/// <summary>
/// A layout loaded from the plugin folder
/// </summary>
Plugin,
Led? led = device[ledId];
if (led == null && createMissingLeds)
led = device.AddLed(ledId, new Point(), new Size());
/// <summary>
/// A default layout loaded as a fallback option
/// </summary>
Default
if (led != null)
{
led.Location = new Point(layoutLed.X, layoutLed.Y);
led.Size = new Size(layoutLed.Width, layoutLed.Height);
led.Shape = layoutLed.Shape;
led.ShapeData = layoutLed.ShapeData;
led.LayoutMetadata = layoutLed.CustomData;
}
}
}
if (removeExcessiveLeds)
{
List<LedId> ledsToRemove = device.Select(led => led.Id).Where(id => !ledIds.Contains(id)).ToList();
foreach (LedId led in ledsToRemove)
device.RemoveLed(led);
}
List<Led> deviceLeds = device.ToList();
foreach (Led led in deviceLeds)
{
float x = led.Location.X;
float y = led.Location.Y;
// Try to move the LED if it falls outside the boundaries of the layout
if (led.Location.X + led.Size.Width > device.Size.Width)
x -= led.Location.X + led.Size.Width - device.Size.Width;
if (led.Location.Y + led.Size.Height > device.Size.Height)
y -= led.Location.Y + led.Size.Height - device.Size.Height;
// If not possible because it's too large we'll have to drop it to avoid rendering issues
if (x < 0 || y < 0)
device.RemoveLed(led.Id);
else
led.Location = new Point(x, y);
}
}
internal void ApplyDevice(ArtemisDevice artemisDevice)
{
Device = artemisDevice;
foreach (ArtemisLedLayout artemisLedLayout in Leds)
artemisLedLayout.ApplyDevice(Device);
}
internal static ArtemisLayout? GetDefaultLayout(ArtemisDevice device)
{
string layoutFolder = Path.Combine(Constants.ApplicationFolder, "DefaultLayouts", "Artemis");
if (device.DeviceType == RGBDeviceType.Keyboard)
{
// XL layout is defined by its programmable macro keys
if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_Programmable1 && l.RgbLed.Id <= LedId.Keyboard_Programmable32))
{
if (device.PhysicalLayout == KeyboardLayoutType.ANSI)
return new ArtemisLayout(Path.Combine(layoutFolder, "Keyboard", "Artemis XL keyboard-ANSI.xml"), LayoutSource.Default);
return new ArtemisLayout(Path.Combine(layoutFolder, "Keyboard", "Artemis XL keyboard-ISO.xml"), LayoutSource.Default);
}
// L layout is defined by its numpad
if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_NumLock && l.RgbLed.Id <= LedId.Keyboard_NumPeriodAndDelete))
{
if (device.PhysicalLayout == KeyboardLayoutType.ANSI)
return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis L keyboard-ANSI.xml"), LayoutSource.Default);
return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis L keyboard-ISO.xml"), LayoutSource.Default);
}
// No numpad will result in TKL
if (device.PhysicalLayout == KeyboardLayoutType.ANSI)
return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis TKL keyboard-ANSI.xml"), LayoutSource.Default);
return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis TKL keyboard-ISO.xml"), LayoutSource.Default);
}
// if (device.DeviceType == RGBDeviceType.Mouse)
// {
// if (device.Leds.Count == 1)
// {
// if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo))
// return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "1 LED mouse logo.xml"), LayoutSource.Default);
// return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "1 LED mouse.xml"), LayoutSource.Default);
// }
// if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo))
// return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "4 LED mouse logo.xml"), LayoutSource.Default);
// return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "4 LED mouse.xml"), LayoutSource.Default);
// }
if (device.DeviceType == RGBDeviceType.Headset)
{
if (device.Leds.Count == 1)
return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 1 LED headset.xml"), LayoutSource.Default);
if (device.Leds.Count == 2)
return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 2 LED headset.xml"), LayoutSource.Default);
return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 4 LED headset.xml"), LayoutSource.Default);
}
return null;
}
private void LoadLayout()
{
DeviceLayout? deviceLayout = DeviceLayout.Load(FilePath, typeof(LayoutCustomDeviceData), typeof(LayoutCustomLedData));
if (deviceLayout != null)
{
RgbLayout = deviceLayout;
IsValid = true;
}
else
{
RgbLayout = new DeviceLayout();
IsValid = false;
}
if (IsValid)
Leds.AddRange(RgbLayout.Leds.Select(l => new ArtemisLedLayout(this, l)));
LayoutCustomDeviceData = (LayoutCustomDeviceData?) RgbLayout.CustomData ?? new LayoutCustomDeviceData();
ApplyCustomDeviceData();
}
private void ApplyCustomDeviceData()
{
if (!IsValid)
{
Image = null;
return;
}
Uri layoutDirectory = new(Path.GetDirectoryName(FilePath)! + "/", UriKind.Absolute);
if (LayoutCustomDeviceData.DeviceImage != null)
Image = new Uri(layoutDirectory, new Uri(LayoutCustomDeviceData.DeviceImage, UriKind.Relative));
else
Image = null;
}
}
/// <summary>
/// Represents a source from where a layout came
/// </summary>
public enum LayoutSource
{
/// <summary>
/// A layout loaded from config
/// </summary>
Configured,
/// <summary>
/// A layout loaded from the user layout folder
/// </summary>
User,
/// <summary>
/// A layout loaded from the plugin folder
/// </summary>
Plugin,
/// <summary>
/// A default layout loaded as a fallback option
/// </summary>
Default
}
@@ -3,72 +3,71 @@ using System.IO;
using System.Linq;
using RGB.NET.Layout;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a LED layout decorated with extra Artemis-specific data
/// </summary>
public class ArtemisLedLayout
{
/// <summary>
/// Represents a LED layout decorated with extra Artemis-specific data
/// </summary>
public class ArtemisLedLayout
internal ArtemisLedLayout(ArtemisLayout deviceLayout, ILedLayout led)
{
internal ArtemisLedLayout(ArtemisLayout deviceLayout, ILedLayout led)
{
DeviceLayout = deviceLayout;
RgbLayout = led;
LayoutCustomLedData = (LayoutCustomLedData?) led.CustomData ?? new LayoutCustomLedData();
}
DeviceLayout = deviceLayout;
RgbLayout = led;
LayoutCustomLedData = (LayoutCustomLedData?) led.CustomData ?? new LayoutCustomLedData();
}
/// <summary>
/// Gets the device layout of this LED layout
/// </summary>
public ArtemisLayout DeviceLayout { get; }
/// <summary>
/// Gets the device layout of this LED layout
/// </summary>
public ArtemisLayout DeviceLayout { get; }
/// <summary>
/// Gets the RGB.NET LED Layout of this LED layout
/// </summary>
public ILedLayout RgbLayout { get; }
/// <summary>
/// Gets the RGB.NET LED Layout of this LED layout
/// </summary>
public ILedLayout RgbLayout { get; }
/// <summary>
/// Gets the LED this layout is applied to
/// </summary>
public ArtemisLed? Led { get; protected set; }
/// <summary>
/// Gets the LED this layout is applied to
/// </summary>
public ArtemisLed? Led { get; protected set; }
/// <summary>
/// Gets the name of the logical layout this LED belongs to
/// </summary>
public string? LogicalName { get; private set; }
/// <summary>
/// Gets the name of the logical layout this LED belongs to
/// </summary>
public string? LogicalName { get; private set; }
/// <summary>
/// Gets the image of the LED
/// </summary>
public Uri? Image { get; private set; }
/// <summary>
/// Gets the image of the LED
/// </summary>
public Uri? Image { get; private set; }
/// <summary>
/// Gets the custom layout data embedded in the RGB.NET layout
/// </summary>
public LayoutCustomLedData LayoutCustomLedData { get; }
/// <summary>
/// Gets the custom layout data embedded in the RGB.NET layout
/// </summary>
public LayoutCustomLedData LayoutCustomLedData { get; }
internal void ApplyDevice(ArtemisDevice device)
{
Led = device.Leds.FirstOrDefault(d => d.RgbLed.Id.ToString() == RgbLayout.Id);
if (Led != null)
Led.Layout = this;
internal void ApplyDevice(ArtemisDevice device)
{
Led = device.Leds.FirstOrDefault(d => d.RgbLed.Id.ToString() == RgbLayout.Id);
if (Led != null)
Led.Layout = this;
ApplyCustomLedData(device);
}
ApplyCustomLedData(device);
}
private void ApplyCustomLedData(ArtemisDevice artemisDevice)
{
if (LayoutCustomLedData.LogicalLayouts == null || !LayoutCustomLedData.LogicalLayouts.Any())
return;
private void ApplyCustomLedData(ArtemisDevice artemisDevice)
{
if (LayoutCustomLedData.LogicalLayouts == null || !LayoutCustomLedData.LogicalLayouts.Any())
return;
// Prefer a matching layout or else a default layout (that has no name)
LayoutCustomLedDataLogicalLayout logicalLayout = LayoutCustomLedData.LogicalLayouts
.OrderBy(l => l.Name == artemisDevice.LogicalLayout)
.ThenBy(l => l.Name == null)
.First();
// Prefer a matching layout or else a default layout (that has no name)
LayoutCustomLedDataLogicalLayout logicalLayout = LayoutCustomLedData.LogicalLayouts
.OrderBy(l => l.Name == artemisDevice.LogicalLayout)
.ThenBy(l => l.Name == null)
.First();
LogicalName = logicalLayout.Name;
Image = new Uri(Path.Combine(Path.GetDirectoryName(DeviceLayout.FilePath)!, logicalLayout.Image!), UriKind.Absolute);
}
LogicalName = logicalLayout.Name;
Image = new Uri(Path.Combine(Path.GetDirectoryName(DeviceLayout.FilePath)!, logicalLayout.Image!), UriKind.Absolute);
}
}
@@ -1,15 +1,15 @@
using System.Xml.Serialization;
#pragma warning disable 1591
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents extra Artemis-specific information stored in RGB.NET layouts
/// </summary>
[XmlRoot("CustomData")]
public class LayoutCustomDeviceData
{
/// <summary>
/// Represents extra Artemis-specific information stored in RGB.NET layouts
/// </summary>
[XmlRoot("CustomData")]
public class LayoutCustomDeviceData
{
[XmlElement("DeviceImage")]
public string? DeviceImage { get; set; }
}
[XmlElement("DeviceImage")]
public string? DeviceImage { get; set; }
}
@@ -1,29 +1,29 @@
using System.Collections.Generic;
using System.Xml.Serialization;
#pragma warning disable 1591
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents extra Artemis-specific information stored in RGB.NET layouts
/// </summary>
[XmlRoot("CustomData")]
public class LayoutCustomLedData
{
/// <summary>
/// Represents extra Artemis-specific information stored in RGB.NET layouts
/// </summary>
[XmlRoot("CustomData")]
public class LayoutCustomLedData
{
[XmlArray("LogicalLayouts")]
public List<LayoutCustomLedDataLogicalLayout>? LogicalLayouts { get; set; }
}
[XmlArray("LogicalLayouts")]
public List<LayoutCustomLedDataLogicalLayout>? LogicalLayouts { get; set; }
}
/// <summary>
/// Represents extra Artemis-specific information stored in RGB.NET layouts
/// </summary>
[XmlType("LogicalLayout")]
public class LayoutCustomLedDataLogicalLayout
{
[XmlAttribute("Name")]
public string? Name { get; set; }
/// <summary>
/// Represents extra Artemis-specific information stored in RGB.NET layouts
/// </summary>
[XmlType("LogicalLayout")]
public class LayoutCustomLedDataLogicalLayout
{
[XmlAttribute("Name")]
public string? Name { get; set; }
[XmlAttribute("Image")]
public string? Image { get; set; }
}
[XmlAttribute("Image")]
public string? Image { get; set; }
}