mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-11 19:36:28 +00:00
Code style - Use file scoped namespaces
Code style - Ran code cleanup
This commit is contained in:
1 parent
e68e16df4d
commit
f6090dc296
691 files changed
+37301
-38119
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
+6
-7
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
+40
-41
@@ -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; }
|
||||
}
|
||||
+18
-18
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
Reference in new issue
Block a user