mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-11 11:26:27 +00:00
Merge branch 'feature/visual-scripting' into feature/avalonia
This commit is contained in:
commit
4ef7c83c74
366 files changed
+11986
-10866
No files matched your search
@@ -1,82 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a condition operator that performs a boolean operation
|
||||
/// <para>
|
||||
/// To implement your own condition operator, inherit <see cref="ConditionOperator{TLeftSide, TRightSide}" /> or
|
||||
/// <see cref="ConditionOperator{TLeftSide}" />
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public abstract class BaseConditionOperator
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the description of this logical operator
|
||||
/// </summary>
|
||||
public abstract string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the icon of this logical operator
|
||||
/// </summary>
|
||||
public abstract string Icon { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin this condition operator belongs to
|
||||
/// <para>Note: Not set until after registering</para>
|
||||
/// </summary>
|
||||
public Plugin? Plugin { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the left side type of this condition operator
|
||||
/// </summary>
|
||||
public abstract Type LeftSideType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the right side type of this condition operator. May be null if the operator does not support a right side
|
||||
/// </summary>
|
||||
public abstract Type? RightSideType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the given type is supported by the operator
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check for, must be either the same or be castable to the target type</param>
|
||||
/// <param name="side">Which side of the operator to check, left or right</param>
|
||||
public bool SupportsType(Type type, ConditionParameterSide side)
|
||||
{
|
||||
if (type == null)
|
||||
return true;
|
||||
if (side == ConditionParameterSide.Left)
|
||||
return LeftSideType.IsCastableFrom(type);
|
||||
return RightSideType != null && RightSideType.IsCastableFrom(type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the condition with the input types being provided as objects
|
||||
/// <para>
|
||||
/// This leaves the caller responsible for the types matching <see cref="LeftSideType" /> and
|
||||
/// <see cref="RightSideType" />
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="leftSideValue">The left side value, type should match <see cref="LeftSideType" /></param>
|
||||
/// <param name="rightSideValue">The right side value, type should match <see cref="RightSideType" /></param>
|
||||
/// <returns>The result of the boolean condition's evaluation</returns>
|
||||
internal abstract bool InternalEvaluate(object? leftSideValue, object? rightSideValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a side of a condition parameter
|
||||
/// </summary>
|
||||
public enum ConditionParameterSide
|
||||
{
|
||||
/// <summary>
|
||||
/// The left side of a condition parameter
|
||||
/// </summary>
|
||||
Left,
|
||||
|
||||
/// <summary>
|
||||
/// The right side of a condition parameter
|
||||
/// </summary>
|
||||
Right
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a condition operator that performs a boolean operation using a left- and right-side
|
||||
/// </summary>
|
||||
public abstract class ConditionOperator<TLeftSide, TRightSide> : BaseConditionOperator
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override Type LeftSideType => typeof(TLeftSide);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Type RightSideType => typeof(TRightSide);
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the operator on a and b
|
||||
/// </summary>
|
||||
/// <param name="a">The parameter on the left side of the expression</param>
|
||||
/// <param name="b">The parameter on the right side of the expression</param>
|
||||
public abstract bool Evaluate(TLeftSide a, TRightSide b);
|
||||
|
||||
/// <inheritdoc />
|
||||
internal override bool InternalEvaluate(object? leftSideValue, object? rightSideValue)
|
||||
{
|
||||
// TODO: Can we avoid boxing/unboxing?
|
||||
TLeftSide leftSide;
|
||||
if (leftSideValue != null)
|
||||
{
|
||||
if (leftSideValue.GetType() != typeof(TLeftSide) && leftSideValue is IConvertible)
|
||||
leftSide = (TLeftSide) Convert.ChangeType(leftSideValue, typeof(TLeftSide));
|
||||
else
|
||||
leftSide = (TLeftSide) leftSideValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
leftSide = default;
|
||||
}
|
||||
|
||||
TRightSide rightSide;
|
||||
if (rightSideValue != null)
|
||||
{
|
||||
if (rightSideValue.GetType() != typeof(TRightSide) && leftSideValue is IConvertible)
|
||||
rightSide = (TRightSide) Convert.ChangeType(rightSideValue, typeof(TRightSide));
|
||||
else
|
||||
rightSide = (TRightSide) rightSideValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
rightSide = default;
|
||||
}
|
||||
|
||||
return Evaluate(leftSide!, rightSide!);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a condition operator that performs a boolean operation using only a left side
|
||||
/// </summary>
|
||||
public abstract class ConditionOperator<TLeftSide> : BaseConditionOperator
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override Type LeftSideType => typeof(TLeftSide);
|
||||
|
||||
/// <summary>
|
||||
/// Always <c>null</c>, not applicable to this type of condition operator
|
||||
/// </summary>
|
||||
public override Type? RightSideType => null;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the operator on a and b
|
||||
/// </summary>
|
||||
/// <param name="a">The parameter on the left side of the expression</param>
|
||||
public abstract bool Evaluate(TLeftSide a);
|
||||
|
||||
/// <inheritdoc />
|
||||
internal override bool InternalEvaluate(object? leftSideValue, object? rightSideValue)
|
||||
{
|
||||
// TODO: Can we avoid boxing/unboxing?
|
||||
TLeftSide leftSide;
|
||||
if (leftSideValue != null)
|
||||
{
|
||||
if (leftSideValue.GetType() != typeof(TLeftSide) && leftSideValue is IConvertible)
|
||||
leftSide = (TLeftSide) Convert.ChangeType(leftSideValue, typeof(TLeftSide));
|
||||
else
|
||||
leftSide = (TLeftSide) leftSideValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
leftSide = default;
|
||||
}
|
||||
|
||||
return Evaluate(leftSide!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Artemis.Storage.Entities.Profile.Abstract;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// An abstract class for display condition parts
|
||||
/// </summary>
|
||||
public abstract class DataModelConditionPart : IDisposable
|
||||
{
|
||||
private readonly List<DataModelConditionPart> _children = new();
|
||||
|
||||
protected DataModelConditionPart()
|
||||
{
|
||||
Children = new ReadOnlyCollection<DataModelConditionPart>(_children);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parent of this part
|
||||
/// </summary>
|
||||
public DataModelConditionPart? Parent { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the children of this part
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<DataModelConditionPart> Children { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a child to the display condition part's <see cref="Children" /> collection
|
||||
/// </summary>
|
||||
/// <param name="dataModelConditionPart"></param>
|
||||
/// <param name="index">An optional index at which to insert the condition</param>
|
||||
public void AddChild(DataModelConditionPart dataModelConditionPart, int? index = null)
|
||||
{
|
||||
if (!_children.Contains(dataModelConditionPart))
|
||||
{
|
||||
dataModelConditionPart.Parent = this;
|
||||
if (index != null)
|
||||
_children.Insert(index.Value, dataModelConditionPart);
|
||||
else
|
||||
_children.Add(dataModelConditionPart);
|
||||
|
||||
OnChildAdded();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a child from the display condition part's <see cref="Children" /> collection
|
||||
/// </summary>
|
||||
/// <param name="dataModelConditionPart">The child to remove</param>
|
||||
public void RemoveChild(DataModelConditionPart dataModelConditionPart)
|
||||
{
|
||||
if (_children.Contains(dataModelConditionPart))
|
||||
{
|
||||
dataModelConditionPart.Parent = null;
|
||||
_children.Remove(dataModelConditionPart);
|
||||
OnChildRemoved();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all children. You monster.
|
||||
/// </summary>
|
||||
public void ClearChildren()
|
||||
{
|
||||
while (Children.Any())
|
||||
RemoveChild(Children[0]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the condition part on the data model
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public abstract bool Evaluate();
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the condition part on the given target (currently only for lists)
|
||||
/// </summary>
|
||||
/// <param name="target"></param>
|
||||
/// <returns></returns>
|
||||
internal abstract bool EvaluateObject(object? target);
|
||||
|
||||
internal abstract void Save();
|
||||
internal abstract DataModelConditionPartEntity GetEntity();
|
||||
|
||||
#region IDisposable
|
||||
|
||||
/// <summary>
|
||||
/// Disposed the condition part
|
||||
/// </summary>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a child-condition was added
|
||||
/// </summary>
|
||||
public event EventHandler? ChildAdded;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a child-condition was removed
|
||||
/// </summary>
|
||||
public event EventHandler? ChildRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Invokers the <see cref="ChildAdded" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnChildAdded()
|
||||
{
|
||||
ChildAdded?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokers the <see cref="ChildRemoved" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnChildRemoved()
|
||||
{
|
||||
ChildRemoved?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,411 +0,0 @@
|
||||
using System;
|
||||
using Artemis.Storage.Entities.Profile.Abstract;
|
||||
using Artemis.Storage.Entities.Profile.Conditions;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A predicate in a data model condition using either two data model values or one data model value and a
|
||||
/// static value
|
||||
/// </summary>
|
||||
public abstract class DataModelConditionPredicate : DataModelConditionPart
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="DataModelConditionPredicate" /> class
|
||||
/// </summary>
|
||||
/// <param name="parent"></param>
|
||||
/// <param name="predicateType"></param>
|
||||
/// <param name="entity">A new empty entity</param>
|
||||
protected DataModelConditionPredicate(DataModelConditionPart parent, ProfileRightSideType predicateType, DataModelConditionPredicateEntity entity)
|
||||
{
|
||||
Parent = parent;
|
||||
Entity = entity;
|
||||
PredicateType = predicateType;
|
||||
}
|
||||
|
||||
internal DataModelConditionPredicate(DataModelConditionPart parent, DataModelConditionPredicateEntity entity)
|
||||
{
|
||||
Parent = parent;
|
||||
Entity = entity;
|
||||
PredicateType = (ProfileRightSideType) entity.PredicateType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the predicate type
|
||||
/// </summary>
|
||||
public ProfileRightSideType PredicateType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the operator
|
||||
/// </summary>
|
||||
public BaseConditionOperator? Operator { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path of the left property
|
||||
/// </summary>
|
||||
public DataModelPath? LeftPath { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path of the right property
|
||||
/// </summary>
|
||||
public DataModelPath? RightPath { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the right static value, only used it <see cref="PredicateType" /> is
|
||||
/// <see cref="ProfileRightSideType.Static" />
|
||||
/// </summary>
|
||||
public object? RightStaticValue { get; protected set; }
|
||||
|
||||
internal DataModelConditionPredicateEntity Entity { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
if (PredicateType == ProfileRightSideType.Dynamic)
|
||||
return $"[Dynamic] {LeftPath} {Operator?.Description} {RightPath}";
|
||||
return $"[Static] {LeftPath} {Operator?.Description} {RightStaticValue}";
|
||||
}
|
||||
|
||||
#region IDisposable
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
ConditionOperatorStore.ConditionOperatorAdded -= ConditionOperatorStoreOnConditionOperatorAdded;
|
||||
ConditionOperatorStore.ConditionOperatorRemoved -= ConditionOperatorStoreOnConditionOperatorRemoved;
|
||||
|
||||
LeftPath?.Dispose();
|
||||
RightPath?.Dispose();
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Initialization
|
||||
|
||||
internal void Initialize()
|
||||
{
|
||||
ConditionOperatorStore.ConditionOperatorAdded += ConditionOperatorStoreOnConditionOperatorAdded;
|
||||
ConditionOperatorStore.ConditionOperatorRemoved += ConditionOperatorStoreOnConditionOperatorRemoved;
|
||||
|
||||
InitializeLeftPath();
|
||||
|
||||
// Operator
|
||||
if (Entity.OperatorPluginGuid != null)
|
||||
{
|
||||
BaseConditionOperator? conditionOperator = ConditionOperatorStore.Get(Entity.OperatorPluginGuid.Value, Entity.OperatorType)?.ConditionOperator;
|
||||
if (conditionOperator != null)
|
||||
UpdateOperator(conditionOperator);
|
||||
}
|
||||
|
||||
// Right side dynamic
|
||||
if (PredicateType == ProfileRightSideType.Dynamic)
|
||||
InitializeRightPath();
|
||||
// Right side static
|
||||
else if (PredicateType == ProfileRightSideType.Static && Entity.RightStaticValue != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// If the left path is not valid we cannot reliably set up the right side because the type is unknown
|
||||
// Because of that wait for it to validate first
|
||||
if (LeftPath != null && !LeftPath.IsValid)
|
||||
{
|
||||
LeftPath.PathValidated += InitializeRightSideStatic;
|
||||
return;
|
||||
}
|
||||
if (LeftPath == null)
|
||||
return;
|
||||
|
||||
// Use the left side type so JSON.NET has a better idea what to do
|
||||
Type leftSideType = LeftPath.GetPropertyType()!;
|
||||
object? rightSideValue;
|
||||
|
||||
try
|
||||
{
|
||||
rightSideValue = CoreJson.DeserializeObject(Entity.RightStaticValue, leftSideType);
|
||||
}
|
||||
// If deserialization fails, use the type's default
|
||||
catch (JsonSerializationException e)
|
||||
{
|
||||
DeserializationLogger.LogPredicateDeserializationFailure(this, e);
|
||||
rightSideValue = Activator.CreateInstance(leftSideType);
|
||||
}
|
||||
|
||||
UpdateRightSideStatic(rightSideValue);
|
||||
}
|
||||
catch (JsonReaderException e)
|
||||
{
|
||||
DeserializationLogger.LogPredicateDeserializationFailure(this, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeRightSideStatic(object? sender, EventArgs args)
|
||||
{
|
||||
if (LeftPath == null)
|
||||
return;
|
||||
|
||||
LeftPath.PathValidated -= InitializeRightSideStatic;
|
||||
|
||||
// Use the left side type so JSON.NET has a better idea what to do
|
||||
Type leftSideType = LeftPath.GetPropertyType()!;
|
||||
object? rightSideValue;
|
||||
|
||||
try
|
||||
{
|
||||
rightSideValue = CoreJson.DeserializeObject(Entity.RightStaticValue, leftSideType);
|
||||
}
|
||||
// If deserialization fails, use the type's default
|
||||
catch (JsonSerializationException e)
|
||||
{
|
||||
DeserializationLogger.LogPredicateDeserializationFailure(this, e);
|
||||
rightSideValue = Activator.CreateInstance(leftSideType);
|
||||
}
|
||||
|
||||
UpdateRightSideStatic(rightSideValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the left path of this condition predicate
|
||||
/// </summary>
|
||||
protected abstract void InitializeLeftPath();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the right path of this condition predicate
|
||||
/// </summary>
|
||||
protected abstract void InitializeRightPath();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Modification
|
||||
|
||||
/// <summary>
|
||||
/// Updates the left side of the predicate
|
||||
/// </summary>
|
||||
/// <param name="path">The path pointing to the left side value inside the data model</param>
|
||||
public virtual void UpdateLeftSide(DataModelPath? path)
|
||||
{
|
||||
if (path != null && !path.IsValid)
|
||||
throw new ArtemisCoreException("Cannot update left side of predicate to an invalid path");
|
||||
|
||||
LeftPath?.Dispose();
|
||||
LeftPath = path != null ? new DataModelPath(path) : null;
|
||||
|
||||
ValidateOperator();
|
||||
ValidateRightSide();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the right side of the predicate, makes the predicate dynamic and re-compiles the expression
|
||||
/// </summary>
|
||||
/// <param name="path">The path pointing to the right side value inside the data model</param>
|
||||
public void UpdateRightSideDynamic(DataModelPath? path)
|
||||
{
|
||||
if (path != null && !path.IsValid)
|
||||
throw new ArtemisCoreException("Cannot update right side of predicate to an invalid path");
|
||||
if (Operator != null && path != null && !Operator.SupportsType(path.GetPropertyType()!, ConditionParameterSide.Right))
|
||||
throw new ArtemisCoreException($"Selected operator does not support right side of type {path.GetPropertyType()!.Name}");
|
||||
|
||||
RightPath?.Dispose();
|
||||
RightPath = path != null ? new DataModelPath(path) : null;
|
||||
|
||||
PredicateType = ProfileRightSideType.Dynamic;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the right side of the predicate, makes the predicate static and re-compiles the expression
|
||||
/// </summary>
|
||||
/// <param name="staticValue">The right side value to use</param>
|
||||
public void UpdateRightSideStatic(object? staticValue)
|
||||
{
|
||||
PredicateType = ProfileRightSideType.Static;
|
||||
RightPath?.Dispose();
|
||||
RightPath = null;
|
||||
|
||||
// If the operator is null simply apply the value, any validation will wait
|
||||
if (Operator == null)
|
||||
{
|
||||
RightStaticValue = staticValue;
|
||||
return;
|
||||
}
|
||||
|
||||
// If the operator does not support a right side, always set it to null
|
||||
if (Operator.RightSideType == null)
|
||||
{
|
||||
RightStaticValue = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// If not null ensure the types match and if not, convert it
|
||||
Type? preferredType = GetPreferredRightSideType();
|
||||
if (staticValue != null && staticValue.GetType() == preferredType || preferredType == null)
|
||||
RightStaticValue = staticValue;
|
||||
else if (staticValue != null)
|
||||
RightStaticValue = Convert.ChangeType(staticValue, preferredType);
|
||||
// If null create a default instance for value types or simply make it null for reference types
|
||||
else if (preferredType.IsValueType)
|
||||
RightStaticValue = Activator.CreateInstance(preferredType);
|
||||
else
|
||||
RightStaticValue = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the operator of the predicate and re-compiles the expression
|
||||
/// </summary>
|
||||
/// <param name="conditionOperator"></param>
|
||||
public void UpdateOperator(BaseConditionOperator? conditionOperator)
|
||||
{
|
||||
if (conditionOperator == null)
|
||||
{
|
||||
Operator = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// No need to check for compatibility without a left side, when left site does get set it will be checked again
|
||||
if (LeftPath == null || !LeftPath.IsValid)
|
||||
{
|
||||
Operator = conditionOperator;
|
||||
return;
|
||||
}
|
||||
|
||||
Type leftType = LeftPath.GetPropertyType()!;
|
||||
// Left side can't go empty so enforce a match
|
||||
if (!conditionOperator.SupportsType(leftType, ConditionParameterSide.Left))
|
||||
throw new ArtemisCoreException($"Cannot apply operator {conditionOperator.GetType().Name} to this predicate because " +
|
||||
$"it does not support left side type {leftType.Name}");
|
||||
|
||||
Operator = conditionOperator;
|
||||
ValidateRightSide();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the best type to use for the right side op this predicate
|
||||
/// </summary>
|
||||
public abstract Type? GetPreferredRightSideType();
|
||||
|
||||
private void ValidateOperator()
|
||||
{
|
||||
if (LeftPath == null || !LeftPath.IsValid || Operator == null)
|
||||
return;
|
||||
|
||||
Type leftType = LeftPath.GetPropertyType()!;
|
||||
if (!Operator.SupportsType(leftType, ConditionParameterSide.Left))
|
||||
Operator = null;
|
||||
}
|
||||
|
||||
private void ValidateRightSide()
|
||||
{
|
||||
if (Operator == null)
|
||||
return;
|
||||
|
||||
if (PredicateType == ProfileRightSideType.Dynamic)
|
||||
{
|
||||
if (RightPath == null || !RightPath.IsValid)
|
||||
return;
|
||||
|
||||
Type rightSideType = RightPath.GetPropertyType()!;
|
||||
if (!Operator.SupportsType(rightSideType, ConditionParameterSide.Right))
|
||||
UpdateRightSideDynamic(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (RightStaticValue == null)
|
||||
return;
|
||||
|
||||
if (!Operator.SupportsType(RightStaticValue.GetType(), ConditionParameterSide.Right))
|
||||
UpdateRightSideStatic(null);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Evaluation
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Evaluate()
|
||||
{
|
||||
if (Operator == null || LeftPath == null || !LeftPath.IsValid)
|
||||
return false;
|
||||
|
||||
// If the operator does not support a right side, immediately evaluate with null
|
||||
if (Operator.RightSideType == null)
|
||||
return Operator.InternalEvaluate(LeftPath.GetValue(), null);
|
||||
|
||||
// Compare with a static value
|
||||
if (PredicateType == ProfileRightSideType.Static)
|
||||
{
|
||||
object? leftSideValue = LeftPath.GetValue();
|
||||
if (leftSideValue != null && leftSideValue.GetType().IsValueType && RightStaticValue == null)
|
||||
return false;
|
||||
|
||||
return Operator.InternalEvaluate(leftSideValue, RightStaticValue);
|
||||
}
|
||||
|
||||
if (RightPath == null || !RightPath.IsValid)
|
||||
return false;
|
||||
|
||||
// Compare with dynamic values
|
||||
return Operator.InternalEvaluate(LeftPath.GetValue(), RightPath.GetValue());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
internal override bool EvaluateObject(object? target)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Storage
|
||||
|
||||
internal override DataModelConditionPartEntity GetEntity()
|
||||
{
|
||||
return Entity;
|
||||
}
|
||||
|
||||
internal override void Save()
|
||||
{
|
||||
// Don't save an invalid state
|
||||
if (LeftPath != null && !LeftPath.IsValid || RightPath != null && !RightPath.IsValid)
|
||||
return;
|
||||
|
||||
Entity.PredicateType = (int) PredicateType;
|
||||
|
||||
LeftPath?.Save();
|
||||
Entity.LeftPath = LeftPath?.Entity;
|
||||
RightPath?.Save();
|
||||
Entity.RightPath = RightPath?.Entity;
|
||||
|
||||
Entity.RightStaticValue = CoreJson.SerializeObject(RightStaticValue);
|
||||
|
||||
if (Operator?.Plugin != null)
|
||||
{
|
||||
Entity.OperatorPluginGuid = Operator.Plugin.Guid;
|
||||
Entity.OperatorType = Operator.GetType().Name;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event handlers
|
||||
|
||||
private void ConditionOperatorStoreOnConditionOperatorAdded(object? sender, ConditionOperatorStoreEvent e)
|
||||
{
|
||||
BaseConditionOperator conditionOperator = e.Registration.ConditionOperator;
|
||||
if (Entity.OperatorPluginGuid == conditionOperator.Plugin!.Guid && Entity.OperatorType == conditionOperator.GetType().Name)
|
||||
UpdateOperator(conditionOperator);
|
||||
}
|
||||
|
||||
private void ConditionOperatorStoreOnConditionOperatorRemoved(object? sender, ConditionOperatorStoreEvent e)
|
||||
{
|
||||
if (e.Registration.ConditionOperator != Operator)
|
||||
return;
|
||||
|
||||
Operator = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Artemis.Storage.Entities.Profile.Abstract;
|
||||
using Artemis.Storage.Entities.Profile.Conditions;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A condition that evaluates to true when an event is triggered
|
||||
/// </summary>
|
||||
public class DataModelConditionEvent : DataModelConditionPart
|
||||
{
|
||||
private bool _disposed;
|
||||
private bool _reinitializing;
|
||||
private IDataModelEvent? _valueChangedEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="DataModelConditionEvent" /> class
|
||||
/// </summary>
|
||||
/// <param name="parent"></param>
|
||||
public DataModelConditionEvent(DataModelConditionPart parent)
|
||||
{
|
||||
Parent = parent;
|
||||
Entity = new DataModelConditionEventEntity();
|
||||
|
||||
Initialize();
|
||||
}
|
||||
|
||||
internal DataModelConditionEvent(DataModelConditionPart parent, DataModelConditionEventEntity entity)
|
||||
{
|
||||
Parent = parent;
|
||||
Entity = entity;
|
||||
|
||||
Initialize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path of the event property
|
||||
/// </summary>
|
||||
public DataModelPath? EventPath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last time the event this condition is applied to was triggered
|
||||
/// </summary>
|
||||
public DateTime LastTrigger { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of argument the event provides
|
||||
/// </summary>
|
||||
public Type? EventArgumentType { get; private set; }
|
||||
|
||||
internal DataModelConditionEventEntity Entity { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Evaluate()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DataModelConditionEvent");
|
||||
|
||||
IDataModelEvent? dataModelEvent = GetDataModelEvent();
|
||||
if (dataModelEvent == null)
|
||||
return false;
|
||||
dataModelEvent.Update();
|
||||
|
||||
// Only evaluate to true once every time the event has been triggered since the last evaluation
|
||||
if (dataModelEvent.LastTrigger <= LastTrigger)
|
||||
return false;
|
||||
|
||||
LastTrigger = DateTime.Now;
|
||||
|
||||
// If there is a child (root group), it must evaluate to true whenever the event triggered
|
||||
if (Children.Any())
|
||||
return Children[0].EvaluateObject(dataModelEvent.LastEventArgumentsUntyped);
|
||||
|
||||
// If there are no children, we always evaluate to true whenever the event triggered
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the event the condition is triggered by
|
||||
/// </summary>
|
||||
public void UpdateEvent(DataModelPath? path)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DataModelConditionEvent");
|
||||
|
||||
if (path != null && !path.IsValid)
|
||||
throw new ArtemisCoreException("Cannot update event to an invalid path");
|
||||
|
||||
EventPath?.Dispose();
|
||||
EventPath = path != null ? new DataModelPath(path) : null;
|
||||
SubscribeToEventPath();
|
||||
CreateValueChangedEventIfNeeded();
|
||||
|
||||
// Remove the old root group that was tied to the old data model
|
||||
ClearChildren();
|
||||
|
||||
if (EventPath != null)
|
||||
{
|
||||
EventArgumentType = GetEventArgumentType();
|
||||
// Create a new root group
|
||||
AddChild(new DataModelConditionGroup(this));
|
||||
}
|
||||
else
|
||||
{
|
||||
EventArgumentType = null;
|
||||
}
|
||||
|
||||
LastTrigger = GetDataModelEvent()?.LastTrigger ?? DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="IDataModelEvent" /> this <see cref="DataModelConditionEvent" /> is triggered by
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IDataModelEvent" /> this <see cref="DataModelConditionEvent" /> is triggered by</returns>
|
||||
public IDataModelEvent? GetDataModelEvent()
|
||||
{
|
||||
if (_valueChangedEvent != null)
|
||||
return _valueChangedEvent;
|
||||
return EventPath?.GetValue() as IDataModelEvent;
|
||||
}
|
||||
|
||||
#region IDisposable
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
_disposed = true;
|
||||
|
||||
EventPath?.Dispose();
|
||||
|
||||
foreach (DataModelConditionPart child in Children)
|
||||
child.Dispose();
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
internal override bool EvaluateObject(object? target)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
internal override void Save()
|
||||
{
|
||||
// Don't save an invalid state
|
||||
if (EventPath != null && !EventPath.IsValid)
|
||||
return;
|
||||
|
||||
// Target list
|
||||
EventPath?.Save();
|
||||
Entity.EventPath = EventPath?.Entity;
|
||||
|
||||
// Children
|
||||
Entity.Children.Clear();
|
||||
Entity.Children.AddRange(Children.Select(c => c.GetEntity()));
|
||||
foreach (DataModelConditionPart child in Children)
|
||||
child.Save();
|
||||
}
|
||||
|
||||
internal override DataModelConditionPartEntity GetEntity()
|
||||
{
|
||||
return Entity;
|
||||
}
|
||||
|
||||
internal void Initialize()
|
||||
{
|
||||
ClearChildren();
|
||||
|
||||
if (Entity.EventPath == null)
|
||||
return;
|
||||
|
||||
DataModelPath eventPath = new(null, Entity.EventPath);
|
||||
EventPath = eventPath;
|
||||
SubscribeToEventPath();
|
||||
CreateValueChangedEventIfNeeded();
|
||||
|
||||
EventArgumentType = GetEventArgumentType();
|
||||
// There should only be one child and it should be a group
|
||||
if (Entity.Children.FirstOrDefault() is DataModelConditionGroupEntity rootGroup)
|
||||
{
|
||||
AddChild(new DataModelConditionGroup(this, rootGroup));
|
||||
}
|
||||
else
|
||||
{
|
||||
Entity.Children.Clear();
|
||||
AddChild(new DataModelConditionGroup(this));
|
||||
}
|
||||
|
||||
LastTrigger = GetDataModelEvent()?.LastTrigger ?? DateTime.Now;
|
||||
}
|
||||
|
||||
private Type? GetEventArgumentType()
|
||||
{
|
||||
if (EventPath == null || !EventPath.IsValid)
|
||||
return null;
|
||||
|
||||
if (_valueChangedEvent != null)
|
||||
return _valueChangedEvent.ArgumentsType;
|
||||
|
||||
// Cannot rely on EventPath.GetValue() because part of the path might be null
|
||||
Type eventType = EventPath.GetPropertyType()!;
|
||||
return eventType.IsGenericType ? eventType.GetGenericArguments()[0] : typeof(DataModelEventArgs);
|
||||
}
|
||||
|
||||
private void SubscribeToEventPath()
|
||||
{
|
||||
if (EventPath == null) return;
|
||||
EventPath.PathValidated += EventPathOnPathValidated;
|
||||
EventPath.PathInvalidated += EventPathOnPathInvalidated;
|
||||
}
|
||||
|
||||
private void CreateValueChangedEventIfNeeded()
|
||||
{
|
||||
Type? propertyType = EventPath?.GetPropertyType();
|
||||
if (propertyType == null)
|
||||
return;
|
||||
|
||||
if (!typeof(IDataModelEvent).IsAssignableFrom(propertyType))
|
||||
{
|
||||
IDataModelEvent? instance = (IDataModelEvent?) Activator.CreateInstance(typeof(DataModelValueChangedEvent<>).MakeGenericType(propertyType), EventPath);
|
||||
_valueChangedEvent = instance ?? throw new ArtemisCoreException("Failed to create a DataModelValueChangedEvent for a property changed data model event");
|
||||
}
|
||||
else
|
||||
{
|
||||
_valueChangedEvent = null;
|
||||
}
|
||||
}
|
||||
|
||||
#region Event handlers
|
||||
|
||||
private void EventPathOnPathValidated(object? sender, EventArgs e)
|
||||
{
|
||||
if (_reinitializing)
|
||||
return;
|
||||
|
||||
_reinitializing = true;
|
||||
EventPath?.Dispose();
|
||||
Initialize();
|
||||
_reinitializing = false;
|
||||
}
|
||||
|
||||
private void EventPathOnPathInvalidated(object? sender, EventArgs e)
|
||||
{
|
||||
if (_reinitializing)
|
||||
return;
|
||||
|
||||
_reinitializing = true;
|
||||
EventPath?.Dispose();
|
||||
Initialize();
|
||||
_reinitializing = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
using System;
|
||||
using Artemis.Storage.Entities.Profile;
|
||||
using Artemis.Storage.Entities.Profile.Conditions;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A predicate like evaluated inside a <see cref="DataModelConditionEvent" />
|
||||
/// </summary>
|
||||
public class DataModelConditionEventPredicate : DataModelConditionPredicate
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="DataModelConditionEventPredicate" /> class
|
||||
/// </summary>
|
||||
/// <param name="parent"></param>
|
||||
/// <param name="predicateType"></param>
|
||||
public DataModelConditionEventPredicate(DataModelConditionPart parent, ProfileRightSideType predicateType)
|
||||
: base(parent, predicateType, new DataModelConditionEventPredicateEntity())
|
||||
{
|
||||
DataModelConditionEvent = null!;
|
||||
ApplyParentEvent();
|
||||
Initialize();
|
||||
}
|
||||
|
||||
internal DataModelConditionEventPredicate(DataModelConditionPart parent, DataModelConditionEventPredicateEntity entity)
|
||||
: base(parent, entity)
|
||||
{
|
||||
DataModelConditionEvent = null!;
|
||||
ApplyParentEvent();
|
||||
Initialize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data model condition event this predicate belongs to
|
||||
/// </summary>
|
||||
public DataModelConditionEvent DataModelConditionEvent { get; private set; }
|
||||
|
||||
private void ApplyParentEvent()
|
||||
{
|
||||
DataModelConditionPart? current = Parent;
|
||||
while (current != null)
|
||||
{
|
||||
if (current is DataModelConditionEvent parentEvent)
|
||||
{
|
||||
DataModelConditionEvent = parentEvent;
|
||||
return;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
if (DataModelConditionEvent == null)
|
||||
throw new ArtemisCoreException("This data model condition event predicate does not belong to a data model condition event");
|
||||
}
|
||||
|
||||
private object? GetEventPathValue(DataModelPath path, object? target)
|
||||
{
|
||||
lock (path)
|
||||
{
|
||||
if (!(path.Target is EventPredicateWrapperDataModel wrapper))
|
||||
throw new ArtemisCoreException("Data model condition event predicate has a path with an invalid target");
|
||||
|
||||
wrapper.UntypedArguments = target;
|
||||
return path.GetValue();
|
||||
}
|
||||
}
|
||||
|
||||
#region Initialization
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void InitializeLeftPath()
|
||||
{
|
||||
if (Entity.LeftPath != null)
|
||||
LeftPath = DataModelConditionEvent.EventArgumentType != null
|
||||
? new DataModelPath(EventPredicateWrapperDataModel.Create(DataModelConditionEvent.EventArgumentType), Entity.LeftPath)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void InitializeRightPath()
|
||||
{
|
||||
if (PredicateType == ProfileRightSideType.Dynamic && Entity.RightPath != null)
|
||||
{
|
||||
// Right side dynamic using event arguments
|
||||
if (Entity.RightPath.WrapperType == PathWrapperType.Event)
|
||||
{
|
||||
RightPath = DataModelConditionEvent.EventArgumentType != null
|
||||
? new DataModelPath(EventPredicateWrapperDataModel.Create(DataModelConditionEvent.EventArgumentType), Entity.RightPath)
|
||||
: null;
|
||||
}
|
||||
// Right side dynamic
|
||||
else
|
||||
RightPath = new DataModelPath(null, Entity.RightPath);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Modification
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Type? GetPreferredRightSideType()
|
||||
{
|
||||
Type? preferredType = Operator?.RightSideType;
|
||||
Type? leftSideType = LeftPath?.GetPropertyType();
|
||||
if (preferredType == null)
|
||||
return null;
|
||||
|
||||
if (leftSideType != null && preferredType.IsAssignableFrom(leftSideType))
|
||||
preferredType = leftSideType;
|
||||
|
||||
return preferredType;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Evaluation
|
||||
|
||||
/// <summary>
|
||||
/// Not supported for event predicates, always returns <c>false</c>
|
||||
/// </summary>
|
||||
public override bool Evaluate()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
internal override bool EvaluateObject(object? target)
|
||||
{
|
||||
if (Operator == null || LeftPath == null || !LeftPath.IsValid)
|
||||
return false;
|
||||
|
||||
// If the operator does not support a right side, immediately evaluate with null
|
||||
if (Operator.RightSideType == null)
|
||||
return Operator.InternalEvaluate(GetEventPathValue(LeftPath, target), null);
|
||||
|
||||
// Compare with a static value
|
||||
if (PredicateType == ProfileRightSideType.Static)
|
||||
{
|
||||
object? leftSideValue = GetEventPathValue(LeftPath, target);
|
||||
if (leftSideValue != null && leftSideValue.GetType().IsValueType && RightStaticValue == null)
|
||||
return false;
|
||||
|
||||
return Operator.InternalEvaluate(leftSideValue, RightStaticValue);
|
||||
}
|
||||
|
||||
if (RightPath == null || !RightPath.IsValid)
|
||||
return false;
|
||||
|
||||
// Compare with dynamic values
|
||||
if (PredicateType == ProfileRightSideType.Dynamic)
|
||||
{
|
||||
// If the path targets a property inside the event, evaluate on the event path value instead of the right path value
|
||||
if (RightPath.Target is EventPredicateWrapperDataModel)
|
||||
return Operator.InternalEvaluate(GetEventPathValue(LeftPath, target), GetEventPathValue(RightPath, target));
|
||||
return Operator.InternalEvaluate(GetEventPathValue(LeftPath, target), RightPath.GetValue());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
using System;
|
||||
using Artemis.Storage.Entities.Profile.Conditions;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A predicate in a data model condition using either two data model values or one data model value and a
|
||||
/// static value
|
||||
/// </summary>
|
||||
public class DataModelConditionGeneralPredicate : DataModelConditionPredicate
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="DataModelConditionGeneralPredicate" /> class
|
||||
/// </summary>
|
||||
/// <param name="parent"></param>
|
||||
/// <param name="predicateType"></param>
|
||||
public DataModelConditionGeneralPredicate(DataModelConditionPart parent, ProfileRightSideType predicateType)
|
||||
: base(parent, predicateType, new DataModelConditionGeneralPredicateEntity())
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
internal DataModelConditionGeneralPredicate(DataModelConditionPart parent, DataModelConditionGeneralPredicateEntity entity)
|
||||
: base(parent, entity)
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
#region Modification
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Type? GetPreferredRightSideType()
|
||||
{
|
||||
Type? preferredType = Operator?.RightSideType;
|
||||
Type? leftSideType = LeftPath?.GetPropertyType();
|
||||
if (preferredType == null)
|
||||
return null;
|
||||
|
||||
if (leftSideType != null && preferredType.IsAssignableFrom(leftSideType))
|
||||
preferredType = leftSideType;
|
||||
|
||||
return preferredType;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Initialization
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void InitializeLeftPath()
|
||||
{
|
||||
if (Entity.LeftPath != null)
|
||||
LeftPath = new DataModelPath(null, Entity.LeftPath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void InitializeRightPath()
|
||||
{
|
||||
if (PredicateType == ProfileRightSideType.Dynamic && Entity.RightPath != null)
|
||||
RightPath = new DataModelPath(null, Entity.RightPath);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Artemis.Storage.Entities.Profile.Abstract;
|
||||
using Artemis.Storage.Entities.Profile.Conditions;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A group containing zero to many <see cref="DataModelConditionPart" />s which it evaluates using a boolean specific
|
||||
/// operator
|
||||
/// </summary>
|
||||
public class DataModelConditionGroup : DataModelConditionPart
|
||||
{
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="DataModelConditionGroup" /> class
|
||||
/// </summary>
|
||||
/// <param name="parent"></param>
|
||||
public DataModelConditionGroup(DataModelConditionPart? parent)
|
||||
{
|
||||
Parent = parent;
|
||||
Entity = new DataModelConditionGroupEntity();
|
||||
ChildAdded += OnChildrenChanged;
|
||||
ChildRemoved += OnChildrenChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="DataModelConditionGroup" /> class
|
||||
/// </summary>
|
||||
/// <param name="parent"></param>
|
||||
/// <param name="entity"></param>
|
||||
public DataModelConditionGroup(DataModelConditionPart? parent, DataModelConditionGroupEntity entity)
|
||||
{
|
||||
Parent = parent;
|
||||
Entity = entity;
|
||||
BooleanOperator = (BooleanOperator) Entity.BooleanOperator;
|
||||
|
||||
foreach (DataModelConditionPartEntity childEntity in Entity.Children)
|
||||
{
|
||||
if (childEntity is DataModelConditionGroupEntity groupEntity)
|
||||
AddChild(new DataModelConditionGroup(this, groupEntity));
|
||||
else if (childEntity is DataModelConditionListEntity listEntity)
|
||||
AddChild(new DataModelConditionList(this, listEntity));
|
||||
else if (childEntity is DataModelConditionEventEntity eventEntity)
|
||||
AddChild(new DataModelConditionEvent(this, eventEntity));
|
||||
else if (childEntity is DataModelConditionGeneralPredicateEntity predicateEntity)
|
||||
AddChild(new DataModelConditionGeneralPredicate(this, predicateEntity));
|
||||
else if (childEntity is DataModelConditionListPredicateEntity listPredicateEntity)
|
||||
AddChild(new DataModelConditionListPredicate(this, listPredicateEntity));
|
||||
else if (childEntity is DataModelConditionEventPredicateEntity eventPredicateEntity)
|
||||
AddChild(new DataModelConditionEventPredicate(this, eventPredicateEntity));
|
||||
}
|
||||
|
||||
ContainsEvents = Children.Any(c => c is DataModelConditionEvent);
|
||||
ChildAdded += OnChildrenChanged;
|
||||
ChildRemoved += OnChildrenChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the boolean operator of this group
|
||||
/// </summary>
|
||||
public BooleanOperator BooleanOperator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this group contains any events
|
||||
/// </summary>
|
||||
public bool ContainsEvents { get; private set; }
|
||||
|
||||
internal DataModelConditionGroupEntity Entity { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Evaluate()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DataModelConditionGroup");
|
||||
|
||||
// Empty groups are always true
|
||||
if (Children.Count == 0)
|
||||
return true;
|
||||
// Groups with only one child ignore the boolean operator
|
||||
if (Children.Count == 1)
|
||||
return Children[0].Evaluate();
|
||||
|
||||
if (ContainsEvents)
|
||||
{
|
||||
bool eventTriggered = Children.Where(c => c is DataModelConditionEvent).Any(c => c.Evaluate());
|
||||
return eventTriggered && EvaluateWithOperator(Children.Where(c => !(c is DataModelConditionEvent)));
|
||||
}
|
||||
return EvaluateWithOperator(Children);
|
||||
}
|
||||
|
||||
private bool EvaluateWithOperator(IEnumerable<DataModelConditionPart> targets)
|
||||
{
|
||||
return BooleanOperator switch
|
||||
{
|
||||
BooleanOperator.And => targets.All(c => c.Evaluate()),
|
||||
BooleanOperator.Or => targets.Any(c => c.Evaluate()),
|
||||
BooleanOperator.AndNot => targets.All(c => !c.Evaluate()),
|
||||
BooleanOperator.OrNot => targets.Any(c => !c.Evaluate()),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
#region IDisposable
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
_disposed = true;
|
||||
foreach (DataModelConditionPart child in Children)
|
||||
child.Dispose();
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <inheritdoc />
|
||||
internal override bool EvaluateObject(object? target)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DataModelConditionGroup");
|
||||
|
||||
// Empty groups are always true
|
||||
if (Children.Count == 0)
|
||||
return true;
|
||||
// Groups with only one child ignore the boolean operator
|
||||
if (Children.Count == 1)
|
||||
return Children[0].EvaluateObject(target);
|
||||
|
||||
return BooleanOperator switch
|
||||
{
|
||||
BooleanOperator.And => Children.All(c => c.EvaluateObject(target)),
|
||||
BooleanOperator.Or => Children.Any(c => c.EvaluateObject(target)),
|
||||
BooleanOperator.AndNot => Children.All(c => !c.EvaluateObject(target)),
|
||||
BooleanOperator.OrNot => Children.Any(c => !c.EvaluateObject(target)),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
internal override void Save()
|
||||
{
|
||||
Entity.BooleanOperator = (int) BooleanOperator;
|
||||
|
||||
Entity.Children.Clear();
|
||||
Entity.Children.AddRange(Children.Select(c => c.GetEntity()));
|
||||
foreach (DataModelConditionPart child in Children)
|
||||
child.Save();
|
||||
}
|
||||
|
||||
internal override DataModelConditionPartEntity GetEntity()
|
||||
{
|
||||
return Entity;
|
||||
}
|
||||
|
||||
private void OnChildrenChanged(object? sender, EventArgs e)
|
||||
{
|
||||
ContainsEvents = Children.Any(c => c is DataModelConditionEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a boolean operator
|
||||
/// </summary>
|
||||
public enum BooleanOperator
|
||||
{
|
||||
/// <summary>
|
||||
/// All the conditions in the group should evaluate to true
|
||||
/// </summary>
|
||||
And,
|
||||
|
||||
/// <summary>
|
||||
/// Any of the conditions in the group should evaluate to true
|
||||
/// </summary>
|
||||
Or,
|
||||
|
||||
/// <summary>
|
||||
/// All the conditions in the group should evaluate to false
|
||||
/// </summary>
|
||||
AndNot,
|
||||
|
||||
/// <summary>
|
||||
/// Any of the conditions in the group should evaluate to false
|
||||
/// </summary>
|
||||
OrNot
|
||||
}
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Artemis.Storage.Entities.Profile.Abstract;
|
||||
using Artemis.Storage.Entities.Profile.Conditions;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A condition that evaluates one or more predicates inside a list
|
||||
/// </summary>
|
||||
public class DataModelConditionList : DataModelConditionPart
|
||||
{
|
||||
private bool _disposed;
|
||||
private bool _reinitializing;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="DataModelConditionList" /> class
|
||||
/// </summary>
|
||||
/// <param name="parent"></param>
|
||||
public DataModelConditionList(DataModelConditionPart parent)
|
||||
{
|
||||
Parent = parent;
|
||||
Entity = new DataModelConditionListEntity();
|
||||
|
||||
Initialize();
|
||||
}
|
||||
|
||||
internal DataModelConditionList(DataModelConditionPart parent, DataModelConditionListEntity entity)
|
||||
{
|
||||
Parent = parent;
|
||||
Entity = entity;
|
||||
ListOperator = (ListOperator) entity.ListOperator;
|
||||
|
||||
Initialize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list operator
|
||||
/// </summary>
|
||||
public ListOperator ListOperator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path of the list property
|
||||
/// </summary>
|
||||
public DataModelPath? ListPath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the content of the list this predicate is evaluated on
|
||||
/// </summary>
|
||||
public Type? ListType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the list contains primitives
|
||||
/// </summary>
|
||||
public bool IsPrimitiveList { get; set; }
|
||||
|
||||
internal DataModelConditionListEntity Entity { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Evaluate()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DataModelConditionList");
|
||||
|
||||
if (ListPath == null || !ListPath.IsValid)
|
||||
return false;
|
||||
|
||||
return EvaluateObject(ListPath.GetValue());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the list the predicate is evaluated on
|
||||
/// </summary>
|
||||
/// <param name="path">The path pointing to the list inside the list</param>
|
||||
public void UpdateList(DataModelPath? path)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DataModelConditionList");
|
||||
|
||||
if (path != null && !path.IsValid)
|
||||
throw new ArtemisCoreException("Cannot update list to an invalid path");
|
||||
|
||||
ListPath?.Dispose();
|
||||
ListPath = path != null ? new DataModelPath(path) : null;
|
||||
SubscribeToListPath();
|
||||
|
||||
// Remove the old root group that was tied to the old data model
|
||||
while (Children.Any())
|
||||
RemoveChild(Children[0]);
|
||||
|
||||
if (ListPath != null)
|
||||
{
|
||||
Type listType = ListPath.GetPropertyType()!;
|
||||
ListType = listType.GetGenericEnumerableType();
|
||||
IsPrimitiveList = ListType == null || ListType.IsPrimitive || ListType.IsEnum || ListType == typeof(string);
|
||||
|
||||
// Create a new root group
|
||||
AddChild(new DataModelConditionGroup(this));
|
||||
}
|
||||
else
|
||||
{
|
||||
ListType = null;
|
||||
}
|
||||
}
|
||||
|
||||
#region IDisposable
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
_disposed = true;
|
||||
|
||||
ListPath?.Dispose();
|
||||
|
||||
foreach (DataModelConditionPart child in Children)
|
||||
child.Dispose();
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
internal override bool EvaluateObject(object? target)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DataModelConditionList");
|
||||
|
||||
if (!Children.Any())
|
||||
return false;
|
||||
if (!(target is IEnumerable enumerable))
|
||||
return false;
|
||||
|
||||
IEnumerable<object> objectList = enumerable.Cast<object>();
|
||||
return ListOperator switch
|
||||
{
|
||||
ListOperator.Any => objectList.Any(o => Children[0].EvaluateObject(o)),
|
||||
ListOperator.All => objectList.All(o => Children[0].EvaluateObject(o)),
|
||||
ListOperator.None => objectList.Any(o => !Children[0].EvaluateObject(o)),
|
||||
ListOperator.Count => false,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
internal override void Save()
|
||||
{
|
||||
// Don't save an invalid state
|
||||
if (ListPath != null && !ListPath.IsValid)
|
||||
return;
|
||||
|
||||
// Target list
|
||||
ListPath?.Save();
|
||||
Entity.ListPath = ListPath?.Entity;
|
||||
|
||||
// Operator
|
||||
Entity.ListOperator = (int) ListOperator;
|
||||
|
||||
// Children
|
||||
Entity.Children.Clear();
|
||||
Entity.Children.AddRange(Children.Select(c => c.GetEntity()));
|
||||
foreach (DataModelConditionPart child in Children)
|
||||
child.Save();
|
||||
}
|
||||
|
||||
internal override DataModelConditionPartEntity GetEntity()
|
||||
{
|
||||
return Entity;
|
||||
}
|
||||
|
||||
internal void Initialize()
|
||||
{
|
||||
while (Children.Any())
|
||||
RemoveChild(Children[0]);
|
||||
|
||||
if (Entity.ListPath == null)
|
||||
return;
|
||||
|
||||
// Ensure the list path is valid and points to a list
|
||||
DataModelPath listPath = new(null, Entity.ListPath);
|
||||
Type listType = listPath.GetPropertyType()!;
|
||||
// Can't check this on an invalid list, if it becomes valid later lets hope for the best
|
||||
if (listPath.IsValid && !PointsToList(listPath))
|
||||
return;
|
||||
|
||||
ListPath = listPath;
|
||||
SubscribeToListPath();
|
||||
if (ListPath.IsValid)
|
||||
{
|
||||
ListType = listType.GetGenericEnumerableType();
|
||||
IsPrimitiveList = ListType == null || ListType.IsPrimitive || ListType.IsEnum || ListType == typeof(string);
|
||||
}
|
||||
else
|
||||
{
|
||||
ListType = null;
|
||||
IsPrimitiveList = false;
|
||||
}
|
||||
|
||||
// There should only be one child and it should be a group
|
||||
if (Entity.Children.FirstOrDefault() is DataModelConditionGroupEntity rootGroup)
|
||||
{
|
||||
AddChild(new DataModelConditionGroup(this, rootGroup));
|
||||
}
|
||||
else
|
||||
{
|
||||
Entity.Children.Clear();
|
||||
AddChild(new DataModelConditionGroup(this));
|
||||
}
|
||||
}
|
||||
|
||||
private bool PointsToList(DataModelPath dataModelPath)
|
||||
{
|
||||
Type? type = dataModelPath.GetPropertyType();
|
||||
return type?.IsGenericEnumerable() ?? false;
|
||||
}
|
||||
|
||||
private void SubscribeToListPath()
|
||||
{
|
||||
if (ListPath == null) return;
|
||||
ListPath.PathValidated += ListPathOnPathValidated;
|
||||
ListPath.PathInvalidated += ListPathOnPathInvalidated;
|
||||
}
|
||||
|
||||
#region Event handlers
|
||||
|
||||
private void ListPathOnPathValidated(object? sender, EventArgs e)
|
||||
{
|
||||
if (_reinitializing)
|
||||
return;
|
||||
|
||||
_reinitializing = true;
|
||||
ListPath?.Dispose();
|
||||
Initialize();
|
||||
_reinitializing = false;
|
||||
}
|
||||
|
||||
private void ListPathOnPathInvalidated(object? sender, EventArgs e)
|
||||
{
|
||||
if (_reinitializing)
|
||||
return;
|
||||
|
||||
_reinitializing = true;
|
||||
ListPath?.Dispose();
|
||||
Initialize();
|
||||
_reinitializing = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a list operator
|
||||
/// </summary>
|
||||
public enum ListOperator
|
||||
{
|
||||
/// <summary>
|
||||
/// Any of the list items should evaluate to true
|
||||
/// </summary>
|
||||
Any,
|
||||
|
||||
/// <summary>
|
||||
/// All of the list items should evaluate to true
|
||||
/// </summary>
|
||||
All,
|
||||
|
||||
/// <summary>
|
||||
/// None of the list items should evaluate to true
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// A specific amount of the list items should evaluate to true
|
||||
/// </summary>
|
||||
Count
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
using System;
|
||||
using Artemis.Core.Modules;
|
||||
using Artemis.Storage.Entities.Profile;
|
||||
using Artemis.Storage.Entities.Profile.Conditions;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A predicate like evaluated inside a <see cref="DataModelConditionList" />
|
||||
/// </summary>
|
||||
public class DataModelConditionListPredicate : DataModelConditionPredicate
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="DataModelConditionListPredicate" /> class
|
||||
/// </summary>
|
||||
/// <param name="parent"></param>
|
||||
/// <param name="predicateType"></param>
|
||||
public DataModelConditionListPredicate(DataModelConditionPart parent, ProfileRightSideType predicateType)
|
||||
: base(parent, predicateType, new DataModelConditionListPredicateEntity())
|
||||
{
|
||||
DataModelConditionList = null!;
|
||||
ApplyParentList();
|
||||
Initialize();
|
||||
}
|
||||
|
||||
internal DataModelConditionListPredicate(DataModelConditionPart parent, DataModelConditionListPredicateEntity entity)
|
||||
: base(parent, entity)
|
||||
{
|
||||
DataModelConditionList = null!;
|
||||
ApplyParentList();
|
||||
Initialize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data model condition list this predicate belongs to
|
||||
/// </summary>
|
||||
public DataModelConditionList DataModelConditionList { get; private set; }
|
||||
|
||||
private void ApplyParentList()
|
||||
{
|
||||
DataModelConditionPart? current = Parent;
|
||||
while (current != null)
|
||||
{
|
||||
if (current is DataModelConditionList parentList)
|
||||
{
|
||||
DataModelConditionList = parentList;
|
||||
return;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
if (DataModelConditionList == null)
|
||||
throw new ArtemisCoreException("This data model condition list predicate does not belong to a data model condition list");
|
||||
}
|
||||
|
||||
private object? GetListPathValue(DataModelPath path, object? target)
|
||||
{
|
||||
if (!(path.Target is ListPredicateWrapperDataModel wrapper))
|
||||
throw new ArtemisCoreException("Data model condition list predicate has a path with an invalid target");
|
||||
|
||||
wrapper.UntypedValue = target;
|
||||
return path.GetValue();
|
||||
}
|
||||
|
||||
#region Initialization
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void InitializeLeftPath()
|
||||
{
|
||||
if (Entity.LeftPath != null)
|
||||
LeftPath = DataModelConditionList.ListType != null
|
||||
? new DataModelPath(ListPredicateWrapperDataModel.Create(
|
||||
DataModelConditionList.ListType,
|
||||
DataModelConditionList.ListPath?.GetPropertyDescription()?.ListItemName
|
||||
), Entity.LeftPath)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void InitializeRightPath()
|
||||
{
|
||||
if (PredicateType == ProfileRightSideType.Dynamic && Entity.RightPath != null)
|
||||
{
|
||||
// Right side dynamic inside the list
|
||||
if (Entity.RightPath.WrapperType == PathWrapperType.List)
|
||||
{
|
||||
RightPath = DataModelConditionList.ListType != null
|
||||
? new DataModelPath(ListPredicateWrapperDataModel.Create(
|
||||
DataModelConditionList.ListType,
|
||||
DataModelConditionList.ListPath?.GetPropertyDescription()?.ListItemName
|
||||
), Entity.RightPath)
|
||||
: null;
|
||||
}
|
||||
// Right side dynamic
|
||||
else
|
||||
RightPath = new DataModelPath(null, Entity.RightPath);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Modification
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Type? GetPreferredRightSideType()
|
||||
{
|
||||
Type? preferredType = Operator?.RightSideType;
|
||||
Type? leftSideType = DataModelConditionList.IsPrimitiveList
|
||||
? DataModelConditionList.ListType
|
||||
: LeftPath?.GetPropertyType();
|
||||
if (preferredType == null)
|
||||
return null;
|
||||
|
||||
if (leftSideType != null && preferredType.IsAssignableFrom(leftSideType))
|
||||
preferredType = leftSideType;
|
||||
|
||||
return preferredType;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Evaluation
|
||||
|
||||
/// <summary>
|
||||
/// Not supported for list predicates, always returns <c>false</c>
|
||||
/// </summary>
|
||||
public override bool Evaluate()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
internal override bool EvaluateObject(object? target)
|
||||
{
|
||||
if (Operator == null || LeftPath == null || !LeftPath.IsValid)
|
||||
return false;
|
||||
|
||||
// Compare with a static value
|
||||
if (PredicateType == ProfileRightSideType.Static)
|
||||
{
|
||||
object? leftSideValue = GetListPathValue(LeftPath, target);
|
||||
if (leftSideValue != null && leftSideValue.GetType().IsValueType && RightStaticValue == null)
|
||||
return false;
|
||||
|
||||
return Operator.InternalEvaluate(leftSideValue, RightStaticValue);
|
||||
}
|
||||
|
||||
if (RightPath == null || !RightPath.IsValid)
|
||||
return false;
|
||||
|
||||
// Compare with dynamic values
|
||||
if (PredicateType == ProfileRightSideType.Dynamic)
|
||||
{
|
||||
// If the path targets a property inside the list, evaluate on the list path value instead of the right path value
|
||||
if (RightPath.Target is ListPredicateWrapperDataModel)
|
||||
return Operator.InternalEvaluate(GetListPathValue(LeftPath, target), GetListPathValue(RightPath, target));
|
||||
return Operator.InternalEvaluate(GetListPathValue(LeftPath, target), RightPath.GetValue());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Artemis.Core.Internal;
|
||||
using Artemis.Storage.Entities.Profile.Abstract;
|
||||
using Artemis.Storage.Entities.Profile.Conditions;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a condition that is based on a <see cref="DataModelEvent" />
|
||||
/// </summary>
|
||||
public class EventCondition : CorePropertyChanged, INodeScriptCondition
|
||||
{
|
||||
private readonly string _displayName;
|
||||
private readonly EventConditionEntity _entity;
|
||||
private EventDefaultNode? _eventNode;
|
||||
private TimeLineEventOverlapMode _eventOverlapMode;
|
||||
private DataModelPath? _eventPath;
|
||||
private DateTime _lastProcessedTrigger;
|
||||
private NodeScript<bool>? _script;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="EventCondition" /> class
|
||||
/// </summary>
|
||||
public EventCondition(ProfileElement profileElement)
|
||||
{
|
||||
_entity = new EventConditionEntity();
|
||||
_displayName = profileElement.GetType().Name;
|
||||
|
||||
ProfileElement = profileElement;
|
||||
}
|
||||
|
||||
internal EventCondition(EventConditionEntity entity, ProfileElement profileElement)
|
||||
{
|
||||
_entity = entity;
|
||||
_displayName = profileElement.GetType().Name;
|
||||
|
||||
ProfileElement = profileElement;
|
||||
Load();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the script that drives the event condition
|
||||
/// </summary>
|
||||
public NodeScript<bool>? Script
|
||||
{
|
||||
get => _script;
|
||||
set => SetAndNotify(ref _script, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the event that drives this event condition
|
||||
/// </summary>
|
||||
public DataModelPath? EventPath
|
||||
{
|
||||
get => _eventPath;
|
||||
set => SetAndNotify(ref _eventPath, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets how the condition behaves when events trigger before the timeline finishes
|
||||
/// </summary>
|
||||
public TimeLineEventOverlapMode EventOverlapMode
|
||||
{
|
||||
get => _eventOverlapMode;
|
||||
set => SetAndNotify(ref _eventOverlapMode, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the event node, applying the selected event
|
||||
/// </summary>
|
||||
public void UpdateEventNode()
|
||||
{
|
||||
if (Script == null || EventPath?.GetValue() is not IDataModelEvent dataModelEvent)
|
||||
return;
|
||||
|
||||
if (Script.Nodes.FirstOrDefault(n => n is EventDefaultNode) is EventDefaultNode existing)
|
||||
{
|
||||
existing.UpdateDataModelEvent(dataModelEvent);
|
||||
_eventNode = existing;
|
||||
}
|
||||
else
|
||||
{
|
||||
_eventNode = new EventDefaultNode {X = -300};
|
||||
_eventNode.UpdateDataModelEvent(dataModelEvent);
|
||||
}
|
||||
|
||||
if (_eventNode.Pins.Any() && !Script.Nodes.Contains(_eventNode))
|
||||
Script.AddNode(_eventNode);
|
||||
else
|
||||
Script.RemoveNode(_eventNode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the <see cref="Script" /> with a new empty node script
|
||||
/// </summary>
|
||||
public void CreateEmptyNodeScript()
|
||||
{
|
||||
Script?.Dispose();
|
||||
Script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile);
|
||||
UpdateEventNode();
|
||||
}
|
||||
|
||||
private bool Evaluate()
|
||||
{
|
||||
if (EventPath?.GetValue() is not IDataModelEvent dataModelEvent || dataModelEvent.LastTrigger <= _lastProcessedTrigger)
|
||||
return false;
|
||||
|
||||
_lastProcessedTrigger = dataModelEvent.LastTrigger;
|
||||
|
||||
if (Script == null)
|
||||
return true;
|
||||
|
||||
Script.Run();
|
||||
return Script.Result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IConditionEntity Entity => _entity;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ProfileElement ProfileElement { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsMet { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Update()
|
||||
{
|
||||
if (EventOverlapMode == TimeLineEventOverlapMode.Toggle)
|
||||
{
|
||||
if (Evaluate())
|
||||
IsMet = !IsMet;
|
||||
}
|
||||
else
|
||||
{
|
||||
IsMet = Evaluate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ApplyToTimeline(bool isMet, bool wasMet, Timeline timeline)
|
||||
{
|
||||
if (!isMet)
|
||||
{
|
||||
if (EventOverlapMode == TimeLineEventOverlapMode.Toggle)
|
||||
timeline.JumpToEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
// Event overlap mode doesn't apply in this case
|
||||
if (timeline.IsFinished)
|
||||
{
|
||||
timeline.JumpToStart();
|
||||
return;
|
||||
}
|
||||
|
||||
// If the timeline was already running, look at the event overlap mode
|
||||
if (EventOverlapMode == TimeLineEventOverlapMode.Restart)
|
||||
timeline.JumpToStart();
|
||||
else if (EventOverlapMode == TimeLineEventOverlapMode.Copy)
|
||||
timeline.AddExtraTimeline();
|
||||
else if (EventOverlapMode == TimeLineEventOverlapMode.Toggle && !wasMet)
|
||||
timeline.JumpToStart();
|
||||
|
||||
// The remaining overlap mode is 'ignore' which requires no further action
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Script?.Dispose();
|
||||
EventPath?.Dispose();
|
||||
}
|
||||
|
||||
#region Storage
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Load()
|
||||
{
|
||||
EventOverlapMode = (TimeLineEventOverlapMode) _entity.EventOverlapMode;
|
||||
if (_entity.Script != null)
|
||||
Script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", _entity.Script, ProfileElement.Profile);
|
||||
if (_entity.EventPath != null)
|
||||
EventPath = new DataModelPath(_entity.EventPath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Save()
|
||||
{
|
||||
_entity.EventOverlapMode = (int) EventOverlapMode;
|
||||
Script?.Save();
|
||||
_entity.Script = Script?.Entity;
|
||||
EventPath?.Save();
|
||||
_entity.EventPath = EventPath?.Entity;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void LoadNodeScript()
|
||||
{
|
||||
if (Script == null)
|
||||
return;
|
||||
|
||||
Script.Load();
|
||||
UpdateEventNode();
|
||||
Script.LoadConnections();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using Artemis.Storage.Entities.Profile.Abstract;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a condition applied to a <see cref="ProfileElement" />
|
||||
/// </summary>
|
||||
public interface ICondition : IDisposable, IStorageModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the entity used to store this condition
|
||||
/// </summary>
|
||||
public IConditionEntity Entity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the profile element this condition applies to
|
||||
/// </summary>
|
||||
public ProfileElement ProfileElement { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean indicating whether the condition is currently met
|
||||
/// </summary>
|
||||
|
||||
bool IsMet { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Updates the condition
|
||||
/// </summary>
|
||||
void Update();
|
||||
|
||||
/// <summary>
|
||||
/// Applies the display condition to the provided timeline
|
||||
/// </summary>
|
||||
/// <param name="isMet"></param>
|
||||
/// <param name="wasMet"></param>
|
||||
/// <param name="timeline">The timeline to apply the display condition to</param>
|
||||
void ApplyToTimeline(bool isMet, bool wasMet, Timeline timeline);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a condition applied to a <see cref="ProfileElement" /> using a <see cref="INodeScript" />
|
||||
/// </summary>
|
||||
public interface INodeScriptCondition : ICondition
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads the node script this node script condition uses
|
||||
/// </summary>
|
||||
void LoadNodeScript();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using Artemis.Storage.Entities.Profile.Abstract;
|
||||
using Artemis.Storage.Entities.Profile.Conditions;
|
||||
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="StaticCondition" /> class
|
||||
/// </summary>
|
||||
public StaticCondition(ProfileElement 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, ProfileElement 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 ProfileElement ProfileElement { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsMet { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Update()
|
||||
{
|
||||
if (!Script.ExitNodeConnected)
|
||||
{
|
||||
IsMet = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Script.Run();
|
||||
IsMet = Script.Result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ApplyToTimeline(bool isMet, bool wasMet, Timeline timeline)
|
||||
{
|
||||
if (isMet && !wasMet && timeline.IsFinished)
|
||||
timeline.JumpToStart();
|
||||
else if (!isMet && wasMet && timeline.StopMode == TimelineStopMode.SkipToEnd)
|
||||
timeline.JumpToEndSegment();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Script.Dispose();
|
||||
}
|
||||
|
||||
#region Storage
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Load()
|
||||
{
|
||||
Script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", _entity.Script, ProfileElement.Profile);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Save()
|
||||
{
|
||||
Script.Save();
|
||||
_entity.Script = Script.Entity;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void LoadNodeScript()
|
||||
{
|
||||
Script.Load();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using System;
|
||||
using Artemis.Core.Modules;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
internal class EventPredicateWrapperDataModel<T> : EventPredicateWrapperDataModel
|
||||
{
|
||||
[DataModelProperty(Name = "Event arguments", Description = "The arguments provided when the event triggers")]
|
||||
public T Arguments => (UntypedArguments is T typedArguments ? typedArguments : default)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a datamodel that wraps the event arguments of an event
|
||||
/// </summary>
|
||||
public abstract class EventPredicateWrapperDataModel : DataModel
|
||||
{
|
||||
internal EventPredicateWrapperDataModel()
|
||||
{
|
||||
Module = Constants.CorePluginFeature;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last arguments of this event as an object
|
||||
/// </summary>
|
||||
[DataModelIgnore]
|
||||
public object? UntypedArguments { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="EventPredicateWrapperDataModel" /> class
|
||||
/// </summary>
|
||||
public static EventPredicateWrapperDataModel Create(Type type)
|
||||
{
|
||||
object? instance = Activator.CreateInstance(typeof(EventPredicateWrapperDataModel<>).MakeGenericType(type));
|
||||
if (instance == null)
|
||||
throw new ArtemisCoreException($"Failed to create an instance of EventPredicateWrapperDataModel<T> for type {type.Name}");
|
||||
|
||||
return (EventPredicateWrapperDataModel) instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Artemis.Core.Modules;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
internal class ListPredicateWrapperDataModel<T> : ListPredicateWrapperDataModel
|
||||
{
|
||||
public T Value => (UntypedValue is T typedValue ? typedValue : default)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a datamodel that wraps a value in a list
|
||||
/// </summary>
|
||||
public abstract class ListPredicateWrapperDataModel : DataModel
|
||||
{
|
||||
internal ListPredicateWrapperDataModel()
|
||||
{
|
||||
Module = Constants.CorePluginFeature;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of this list as an object
|
||||
/// </summary>
|
||||
[DataModelIgnore]
|
||||
public object? UntypedValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the list item
|
||||
/// </summary>
|
||||
[DataModelIgnore]
|
||||
public string? ItemName { get; set; }
|
||||
|
||||
#region Overrides of DataModel
|
||||
|
||||
/// <inheritdoc />
|
||||
public override DataModelPropertyAttribute? GetPropertyDescription(PropertyInfo propertyInfo)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(ItemName))
|
||||
return new DataModelPropertyAttribute {Name = ItemName};
|
||||
return base.GetPropertyDescription(propertyInfo);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="ListPredicateWrapperDataModel" /> class
|
||||
/// </summary>
|
||||
public static ListPredicateWrapperDataModel Create(Type type, string? name = null)
|
||||
{
|
||||
ListPredicateWrapperDataModel? instance = Activator.CreateInstance(typeof(ListPredicateWrapperDataModel<>).MakeGenericType(type)) as ListPredicateWrapperDataModel;
|
||||
if (instance == null)
|
||||
throw new ArtemisCoreException($"Failed to create an instance of ListPredicateWrapperDataModel<T> for type {type.Name}");
|
||||
|
||||
instance.ItemName = name;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Artemis.Storage.Entities.Profile.DataBindings;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class DataBinding<TLayerProperty, TProperty> : IDataBinding
|
||||
public class DataBinding<TLayerProperty> : IDataBinding
|
||||
{
|
||||
private TProperty _currentValue = default!;
|
||||
private readonly List<IDataBindingProperty> _properties = new();
|
||||
private bool _disposed;
|
||||
private TimeSpan _easingProgress;
|
||||
private TProperty _lastAppliedValue = default!;
|
||||
private TProperty _previousValue = default!;
|
||||
private bool _reapplyValue;
|
||||
private bool _isEnabled;
|
||||
private DataBindingNodeScript<TLayerProperty> _script;
|
||||
|
||||
internal DataBinding(DataBindingRegistration<TLayerProperty, TProperty> dataBindingRegistration)
|
||||
internal DataBinding(LayerProperty<TLayerProperty> layerProperty)
|
||||
{
|
||||
LayerProperty = dataBindingRegistration.LayerProperty;
|
||||
LayerProperty = layerProperty;
|
||||
|
||||
Entity = new DataBindingEntity();
|
||||
_script = new DataBindingNodeScript<TLayerProperty>(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile);
|
||||
|
||||
ApplyRegistration(dataBindingRegistration);
|
||||
Save();
|
||||
ApplyDataBindingMode();
|
||||
}
|
||||
|
||||
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();
|
||||
ApplyDataBindingMode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data binding registration this data binding is based upon
|
||||
/// </summary>
|
||||
public DataBindingRegistration<TLayerProperty, TProperty>? Registration { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the layer property this data binding targets
|
||||
/// </summary>
|
||||
public LayerProperty<TLayerProperty> LayerProperty { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the converter used to apply this data binding to the <see cref="LayerProperty" />
|
||||
/// Gets the script used to populate the data binding
|
||||
/// </summary>
|
||||
public DataBindingConverter<TLayerProperty, TProperty>? Converter { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data binding mode
|
||||
/// </summary>
|
||||
public IDataBindingMode<TLayerProperty, TProperty>? DataBindingMode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the easing time of the data binding
|
||||
/// </summary>
|
||||
public TimeSpan EasingTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets ors ets the easing function of the data binding
|
||||
/// </summary>
|
||||
public Easings.Functions EasingFunction { get; set; }
|
||||
public INodeScript Script => _script;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data binding entity this data binding uses for persistent storage
|
||||
@@ -69,38 +51,52 @@ namespace Artemis.Core
|
||||
public DataBindingEntity Entity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value of the data binding
|
||||
/// Updates the pending values of this data binding
|
||||
/// </summary>
|
||||
/// <param name="baseValue">The base value of the property the data binding is applied to</param>
|
||||
/// <returns></returns>
|
||||
public TProperty GetValue(TProperty baseValue)
|
||||
public void Update()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DataBinding");
|
||||
|
||||
if (Converter == null || DataBindingMode == null)
|
||||
return baseValue;
|
||||
if (!IsEnabled)
|
||||
return;
|
||||
|
||||
TProperty value = DataBindingMode.GetValue(baseValue);
|
||||
// TODO: Update the 'base value' node
|
||||
|
||||
// If no easing is to be applied simple return whatever the current value is
|
||||
if (EasingTime == TimeSpan.Zero || !Converter.SupportsInterpolate)
|
||||
return value;
|
||||
|
||||
// If the value changed, update the current and previous values used for easing
|
||||
if (!Equals(value, _currentValue))
|
||||
ResetEasing(value);
|
||||
|
||||
// Apply interpolation between the previous and current value
|
||||
return GetInterpolatedValue();
|
||||
Script.Run();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the type of the target property of this data binding
|
||||
/// Registers a data binding property so that is available to the data binding system
|
||||
/// </summary>
|
||||
public Type? GetTargetType()
|
||||
/// <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)
|
||||
{
|
||||
return Registration?.Getter.Method.ReturnType;
|
||||
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>
|
||||
@@ -115,103 +111,80 @@ namespace Artemis.Core
|
||||
if (disposing)
|
||||
{
|
||||
_disposed = true;
|
||||
_isEnabled = false;
|
||||
|
||||
if (Registration != null)
|
||||
Registration.DataBinding = null;
|
||||
DataBindingMode?.Dispose();
|
||||
Script.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetEasing(TProperty value)
|
||||
{
|
||||
_previousValue = GetInterpolatedValue();
|
||||
_currentValue = value;
|
||||
_easingProgress = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
private void ApplyRegistration(DataBindingRegistration<TLayerProperty, TProperty> dataBindingRegistration)
|
||||
{
|
||||
if (dataBindingRegistration == null)
|
||||
throw new ArgumentNullException(nameof(dataBindingRegistration));
|
||||
|
||||
dataBindingRegistration.DataBinding = this;
|
||||
Converter = dataBindingRegistration.Converter;
|
||||
Registration = dataBindingRegistration;
|
||||
|
||||
if (GetTargetType()!.IsValueType)
|
||||
{
|
||||
if (_currentValue == null)
|
||||
_currentValue = default!;
|
||||
if (_previousValue == null)
|
||||
_previousValue = default!;
|
||||
}
|
||||
|
||||
Converter?.Initialize(this);
|
||||
}
|
||||
|
||||
private TProperty GetInterpolatedValue()
|
||||
{
|
||||
if (_easingProgress == EasingTime || Converter == null || !Converter.SupportsInterpolate)
|
||||
return _currentValue;
|
||||
|
||||
double easingAmount = _easingProgress.TotalSeconds / EasingTime.TotalSeconds;
|
||||
return Converter.Interpolate(_previousValue, _currentValue, Easings.Interpolate(easingAmount, EasingFunction));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the smoothing progress of the data binding
|
||||
/// Invokes the <see cref="DataBindingPropertyRegistered" /> event
|
||||
/// </summary>
|
||||
/// <param name="timeline">The timeline to apply during update</param>
|
||||
public void Update(Timeline timeline)
|
||||
protected virtual void OnDataBindingPropertyRegistered()
|
||||
{
|
||||
// Don't update data bindings if there is no delta, otherwise this creates an inconsistency between
|
||||
// data bindings with easing and data bindings without easing (the ones with easing will seemingly not update)
|
||||
if (timeline.Delta == TimeSpan.Zero || timeline.IsOverridden)
|
||||
return;
|
||||
DataBindingPropertyRegistered?.Invoke(this, new DataBindingEventArgs(this));
|
||||
}
|
||||
|
||||
UpdateWithDelta(timeline.Delta);
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="DataBindingDisabled" /> 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 void UpdateWithDelta(TimeSpan delta)
|
||||
public ILayerProperty BaseLayerProperty => LayerProperty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DataBinding");
|
||||
get => _isEnabled;
|
||||
set
|
||||
{
|
||||
_isEnabled = value;
|
||||
|
||||
// Data bindings cannot go back in time like brushes
|
||||
if (delta < TimeSpan.Zero)
|
||||
delta = TimeSpan.Zero;
|
||||
|
||||
_easingProgress = _easingProgress.Add(delta);
|
||||
if (_easingProgress > EasingTime)
|
||||
_easingProgress = EasingTime;
|
||||
|
||||
// Tell Apply() to apply a new value next call
|
||||
_reapplyValue = false;
|
||||
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 (Converter == null)
|
||||
if (!IsEnabled)
|
||||
return;
|
||||
|
||||
// If Update() has not been called, reapply the previous value
|
||||
if (_reapplyValue)
|
||||
{
|
||||
Converter.ApplyValue(_lastAppliedValue);
|
||||
return;
|
||||
}
|
||||
|
||||
TProperty converterValue = Converter.GetValue();
|
||||
TProperty value = GetValue(converterValue);
|
||||
Converter.ApplyValue(value);
|
||||
|
||||
_lastAppliedValue = value;
|
||||
_reapplyValue = true;
|
||||
_script.DataBindingExitNode.ApplyToDataBinding();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -221,56 +194,18 @@ namespace Artemis.Core
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#region Mode management
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<DataBindingEventArgs>? DataBindingPropertyRegistered;
|
||||
|
||||
/// <summary>
|
||||
/// Changes the data binding mode of the data binding to the specified <paramref name="dataBindingMode" />
|
||||
/// </summary>
|
||||
public void ChangeDataBindingMode(DataBindingModeType dataBindingMode)
|
||||
{
|
||||
switch (dataBindingMode)
|
||||
{
|
||||
case DataBindingModeType.Direct:
|
||||
Entity.DataBindingMode = new DirectDataBindingEntity();
|
||||
break;
|
||||
case DataBindingModeType.Conditional:
|
||||
Entity.DataBindingMode = new ConditionalDataBindingEntity();
|
||||
break;
|
||||
default:
|
||||
Entity.DataBindingMode = null;
|
||||
break;
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<DataBindingEventArgs>? DataBindingPropertiesCleared;
|
||||
|
||||
ApplyDataBindingMode();
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<DataBindingEventArgs>? DataBindingEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the current data binding mode with one based on the provided data binding mode entity
|
||||
/// </summary>
|
||||
/// <param name="dataBindingModeEntity">The data binding mode entity to base the new data binding mode upon</param>
|
||||
public void ApplyDataBindingEntity(IDataBindingModeEntity dataBindingModeEntity)
|
||||
{
|
||||
Entity.DataBindingMode = dataBindingModeEntity;
|
||||
ApplyDataBindingMode();
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<DataBindingEventArgs>? DataBindingDisabled;
|
||||
|
||||
private void ApplyDataBindingMode()
|
||||
{
|
||||
DataBindingMode?.Dispose();
|
||||
DataBindingMode = null;
|
||||
|
||||
switch (Entity.DataBindingMode)
|
||||
{
|
||||
case DirectDataBindingEntity directDataBindingEntity:
|
||||
DataBindingMode = new DirectDataBinding<TLayerProperty, TProperty>(this, directDataBindingEntity);
|
||||
break;
|
||||
case ConditionalDataBindingEntity conditionalDataBindingEntity:
|
||||
DataBindingMode = new ConditionalDataBinding<TLayerProperty, TProperty>(this, conditionalDataBindingEntity);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Storage
|
||||
|
||||
@@ -280,15 +215,16 @@ namespace Artemis.Core
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DataBinding");
|
||||
|
||||
// General
|
||||
DataBindingRegistration<TLayerProperty, TProperty>? registration = LayerProperty.GetDataBindingRegistration<TProperty>(Entity.Identifier);
|
||||
if (registration != null)
|
||||
ApplyRegistration(registration);
|
||||
IsEnabled = Entity.IsEnabled;
|
||||
}
|
||||
|
||||
EasingTime = Entity.EasingTime;
|
||||
EasingFunction = (Easings.Functions) Entity.EasingFunction;
|
||||
|
||||
DataBindingMode?.Load();
|
||||
/// <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 />
|
||||
@@ -297,40 +233,11 @@ namespace Artemis.Core
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DataBinding");
|
||||
|
||||
if (!LayerProperty.Entity.DataBindingEntities.Contains(Entity))
|
||||
LayerProperty.Entity.DataBindingEntities.Add(Entity);
|
||||
|
||||
// Don't save an invalid state
|
||||
if (Registration != null)
|
||||
Entity.Identifier = Registration.DisplayName;
|
||||
|
||||
Entity.EasingTime = EasingTime;
|
||||
Entity.EasingFunction = (int) EasingFunction;
|
||||
|
||||
DataBindingMode?.Save();
|
||||
_script.Save();
|
||||
Entity.IsEnabled = IsEnabled;
|
||||
Entity.NodeScript = _script.Entity.Nodes.Any() ? _script.Entity : null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A mode that determines how the data binding is applied to the layer property
|
||||
/// </summary>
|
||||
public enum DataBindingModeType
|
||||
{
|
||||
/// <summary>
|
||||
/// Disables the data binding
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the layer property value with the data binding value
|
||||
/// </summary>
|
||||
Direct,
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the layer property value with the data binding value
|
||||
/// </summary>
|
||||
Conditional
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a data binding converter that acts as the bridge between a
|
||||
/// <see cref="DataBinding{TLayerProperty, TProperty}" /> and a <see cref="LayerProperty{T}" />
|
||||
/// </summary>
|
||||
public abstract class DataBindingConverter<TLayerProperty, TProperty> : IDataBindingConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the data binding this converter is applied to
|
||||
/// </summary>
|
||||
public DataBinding<TLayerProperty, TProperty>? DataBinding { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether or not this data binding converter supports the <see cref="Sum" /> method
|
||||
/// </summary>
|
||||
public bool SupportsSum { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether or not this data binding converter supports the <see cref="Interpolate" /> method
|
||||
/// </summary>
|
||||
public bool SupportsInterpolate { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the sum of <paramref name="a" /> and <paramref name="b" />
|
||||
/// </summary>
|
||||
public abstract TProperty Sum(TProperty a, TProperty b);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the the interpolated value between <paramref name="a" /> and <paramref name="b" /> on a scale (generally)
|
||||
/// between <c>0.0</c> and <c>1.0</c> defined by the <paramref name="progress" />
|
||||
/// <para>Note: The progress may go be negative or go beyond <c>1.0</c> depending on the easing method used</para>
|
||||
/// </summary>
|
||||
/// <param name="a">The value to interpolate away from</param>
|
||||
/// <param name="b">The value to interpolate towards</param>
|
||||
/// <param name="progress">The progress of the interpolation between 0.0 and 1.0</param>
|
||||
/// <returns></returns>
|
||||
public abstract TProperty Interpolate(TProperty a, TProperty b, double progress);
|
||||
|
||||
/// <summary>
|
||||
/// Applies the <paramref name="value" /> to the layer property
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
public virtual void ApplyValue(TProperty value)
|
||||
{
|
||||
if (DataBinding?.Registration == null)
|
||||
throw new ArtemisCoreException("Data binding converter is not yet initialized");
|
||||
DataBinding.Registration.Setter(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current base value of the data binding
|
||||
/// </summary>
|
||||
public virtual TProperty GetValue()
|
||||
{
|
||||
if (DataBinding?.Registration == null)
|
||||
throw new ArtemisCoreException("Data binding converter is not yet initialized");
|
||||
return DataBinding.Registration.Getter();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the provided object to a type of <typeparamref name="TProperty" />
|
||||
/// </summary>
|
||||
public virtual TProperty ConvertFromObject(object? source)
|
||||
{
|
||||
return (TProperty) Convert.ChangeType(source, typeof(TProperty))!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the data binding converter has been initialized and the <see cref="DataBinding" /> is available
|
||||
/// </summary>
|
||||
protected virtual void OnInitialized()
|
||||
{
|
||||
}
|
||||
|
||||
internal void Initialize(DataBinding<TLayerProperty, TProperty> dataBinding)
|
||||
{
|
||||
if (dataBinding.Registration == null)
|
||||
throw new ArtemisCoreException("Cannot initialize a data binding converter for a data binding without a registration");
|
||||
|
||||
DataBinding = dataBinding;
|
||||
OnInitialized();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Type SupportedType => typeof(TProperty);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class DataBindingProperty<TProperty> : IDataBindingProperty
|
||||
{
|
||||
internal DataBindingProperty(Func<TProperty> getter, Action<TProperty?> setter, string displayName)
|
||||
{
|
||||
Getter = getter ?? throw new ArgumentNullException(nameof(getter));
|
||||
Setter = setter ?? throw new ArgumentNullException(nameof(setter));
|
||||
DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the function to call to get the value of the property
|
||||
/// </summary>
|
||||
public Func<TProperty> Getter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the action to call to set the value of the property
|
||||
/// </summary>
|
||||
public Action<TProperty?> Setter { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DisplayName { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Type ValueType => typeof(TProperty);
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? GetValue()
|
||||
{
|
||||
return Getter();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetValue(object? value)
|
||||
{
|
||||
if (value is TProperty match)
|
||||
Setter(match);
|
||||
else
|
||||
throw new ArgumentException("Value must match the type of the data binding registration", nameof(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Artemis.Storage.Entities.Profile.DataBindings;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class DataBindingRegistration<TLayerProperty, TProperty> : IDataBindingRegistration
|
||||
{
|
||||
internal DataBindingRegistration(LayerProperty<TLayerProperty> layerProperty, DataBindingConverter<TLayerProperty, TProperty> converter,
|
||||
Func<TProperty> getter, Action<TProperty> setter, string displayName)
|
||||
{
|
||||
LayerProperty = layerProperty ?? throw new ArgumentNullException(nameof(layerProperty));
|
||||
Converter = converter ?? throw new ArgumentNullException(nameof(converter));
|
||||
Getter = getter ?? throw new ArgumentNullException(nameof(getter));
|
||||
Setter = setter ?? throw new ArgumentNullException(nameof(setter));
|
||||
DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the layer property this registration was made on
|
||||
/// </summary>
|
||||
public LayerProperty<TLayerProperty> LayerProperty { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the converter that's used by the data binding
|
||||
/// </summary>
|
||||
public DataBindingConverter<TLayerProperty, TProperty> Converter { get; }
|
||||
|
||||
/// <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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data binding created using this registration
|
||||
/// </summary>
|
||||
public DataBinding<TLayerProperty, TProperty>? DataBinding { get; internal set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDataBinding? GetDataBinding()
|
||||
{
|
||||
return DataBinding;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDataBinding? CreateDataBinding()
|
||||
{
|
||||
if (DataBinding != null)
|
||||
return DataBinding;
|
||||
|
||||
DataBindingEntity? dataBinding = LayerProperty.Entity.DataBindingEntities.FirstOrDefault(e => e.Identifier == DisplayName);
|
||||
if (dataBinding == null)
|
||||
return null;
|
||||
|
||||
DataBinding = new DataBinding<TLayerProperty, TProperty>(LayerProperty, dataBinding);
|
||||
return DataBinding;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearDataBinding()
|
||||
{
|
||||
if (DataBinding == null)
|
||||
return;
|
||||
|
||||
// The related entity is left behind, just in case the data binding is added back later
|
||||
LayerProperty.DisableDataBinding(DataBinding);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using Artemis.Core.Modules;
|
||||
|
||||
namespace Artemis.Core
|
||||
@@ -7,17 +8,51 @@ namespace Artemis.Core
|
||||
/// 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, IUpdateModel, IDisposable
|
||||
public interface IDataBinding : IStorageModel, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Updates the smoothing progress of the data binding and recalculates the value next <see cref="Apply" /> call
|
||||
/// Gets the layer property the data binding is applied to
|
||||
/// </summary>
|
||||
/// <param name="delta">The delta to apply during update</param>
|
||||
void UpdateWithDelta(TimeSpan delta);
|
||||
ILayerProperty BaseLayerProperty { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies the data binding to the layer property
|
||||
/// 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>
|
||||
/// 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>
|
||||
/// 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 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;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a data binding converter that acts as the bridge between a
|
||||
/// <see cref="DataBinding{TLayerProperty, TProperty}" /> and a <see cref="LayerProperty{T}" />
|
||||
/// </summary>
|
||||
public interface IDataBindingConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the type this converter supports
|
||||
/// </summary>
|
||||
public Type SupportedType { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a data binding registration
|
||||
/// </summary>
|
||||
public interface IDataBindingProperty
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the display name of the data binding registration
|
||||
/// </summary>
|
||||
string DisplayName { 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>
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a data binding registration
|
||||
/// </summary>
|
||||
public interface IDataBindingRegistration
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the display name of the data binding registration
|
||||
/// </summary>
|
||||
string DisplayName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the data binding applied using this registration
|
||||
/// </summary>
|
||||
public IDataBinding? GetDataBinding();
|
||||
|
||||
/// <summary>
|
||||
/// If found, creates a data binding from storage
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IDataBinding? CreateDataBinding();
|
||||
|
||||
/// <summary>
|
||||
/// If present, removes the current data binding
|
||||
/// </summary>
|
||||
void ClearDataBinding();
|
||||
}
|
||||
}
|
||||
-168
@@ -1,168 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Artemis.Storage.Entities.Profile.DataBindings;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a data binding mode that applies a value depending on conditions
|
||||
/// </summary>
|
||||
public class ConditionalDataBinding<TLayerProperty, TProperty> : IDataBindingMode<TLayerProperty, TProperty>
|
||||
{
|
||||
private readonly List<DataBindingCondition<TLayerProperty, TProperty>> _conditions = new();
|
||||
private bool _disposed;
|
||||
|
||||
internal ConditionalDataBinding(DataBinding<TLayerProperty, TProperty> dataBinding, ConditionalDataBindingEntity entity)
|
||||
{
|
||||
DataBinding = dataBinding;
|
||||
Entity = entity;
|
||||
Conditions = new ReadOnlyCollection<DataBindingCondition<TLayerProperty, TProperty>>(_conditions);
|
||||
Load();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of conditions applied to this data binding
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<DataBindingCondition<TLayerProperty, TProperty>> Conditions { get; }
|
||||
|
||||
internal ConditionalDataBindingEntity Entity { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DataBinding<TLayerProperty, TProperty> DataBinding { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public TProperty GetValue(TProperty baseValue)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("ConditionalDataBinding");
|
||||
|
||||
DataBindingCondition<TLayerProperty, TProperty>? condition = Conditions.FirstOrDefault(c => c.Evaluate());
|
||||
return condition == null ? baseValue : condition.Value;
|
||||
}
|
||||
|
||||
#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)
|
||||
{
|
||||
_disposed = true;
|
||||
|
||||
foreach (DataBindingCondition<TLayerProperty, TProperty> dataBindingCondition in Conditions)
|
||||
dataBindingCondition.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Values
|
||||
|
||||
/// <summary>
|
||||
/// Adds a condition to the conditional data binding's <see cref="Conditions" /> collection
|
||||
/// </summary>
|
||||
/// <returns>The newly created <see cref="DataBindingCondition{TLayerProperty,TProperty}" /></returns>
|
||||
public DataBindingCondition<TLayerProperty, TProperty> AddCondition()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("ConditionalDataBinding");
|
||||
|
||||
DataBindingCondition<TLayerProperty, TProperty> condition = new(this);
|
||||
_conditions.Add(condition);
|
||||
|
||||
ApplyOrder();
|
||||
OnConditionsUpdated();
|
||||
|
||||
return condition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a condition from the conditional data binding's <see cref="Conditions" /> collection and disposes it
|
||||
/// </summary>
|
||||
/// <param name="condition"></param>
|
||||
public void RemoveCondition(DataBindingCondition<TLayerProperty, TProperty> condition)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("ConditionalDataBinding");
|
||||
if (!_conditions.Contains(condition))
|
||||
return;
|
||||
|
||||
_conditions.Remove(condition);
|
||||
condition.Dispose();
|
||||
|
||||
ApplyOrder();
|
||||
OnConditionsUpdated();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the current order of conditions to the <see cref="Conditions" /> collection
|
||||
/// </summary>
|
||||
public void ApplyOrder()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("ConditionalDataBinding");
|
||||
|
||||
_conditions.Sort((a, b) => a.Order.CompareTo(b.Order));
|
||||
for (int index = 0; index < _conditions.Count; index++)
|
||||
{
|
||||
DataBindingCondition<TLayerProperty, TProperty> condition = _conditions[index];
|
||||
condition.Order = index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Storage
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Load()
|
||||
{
|
||||
foreach (DataBindingConditionEntity dataBindingConditionEntity in Entity.Values)
|
||||
_conditions.Add(new DataBindingCondition<TLayerProperty, TProperty>(this, dataBindingConditionEntity));
|
||||
|
||||
ApplyOrder();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Save()
|
||||
{
|
||||
Entity.Values.Clear();
|
||||
foreach (DataBindingCondition<TLayerProperty, TProperty> dataBindingCondition in Conditions)
|
||||
dataBindingCondition.Save();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a condition is added or removed
|
||||
/// </summary>
|
||||
public event EventHandler? ConditionsUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="ConditionsUpdated" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnConditionsUpdated()
|
||||
{
|
||||
ConditionsUpdated?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
-121
@@ -1,121 +0,0 @@
|
||||
using System;
|
||||
using Artemis.Storage.Entities.Profile.DataBindings;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class DataBindingCondition<TLayerProperty, TProperty> : IDataBindingCondition
|
||||
{
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="DataBindingCondition{TLayerProperty,TProperty}" /> class
|
||||
/// </summary>
|
||||
/// <param name="conditionalDataBinding">The conditional data binding this condition is applied too</param>
|
||||
internal DataBindingCondition(ConditionalDataBinding<TLayerProperty, TProperty> conditionalDataBinding)
|
||||
{
|
||||
ConditionalDataBinding = conditionalDataBinding ?? throw new ArgumentNullException(nameof(conditionalDataBinding));
|
||||
Order = conditionalDataBinding.Conditions.Count + 1;
|
||||
Condition = new DataModelConditionGroup(null);
|
||||
Value = default!;
|
||||
|
||||
Entity = new DataBindingConditionEntity();
|
||||
Save();
|
||||
}
|
||||
|
||||
internal DataBindingCondition(ConditionalDataBinding<TLayerProperty, TProperty> conditionalDataBinding, DataBindingConditionEntity entity)
|
||||
{
|
||||
ConditionalDataBinding = conditionalDataBinding ?? throw new ArgumentNullException(nameof(conditionalDataBinding));
|
||||
Entity = entity;
|
||||
Condition = null!;
|
||||
Value = default!;
|
||||
|
||||
Load();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the conditional data binding this condition is applied to
|
||||
/// </summary>
|
||||
public ConditionalDataBinding<TLayerProperty, TProperty> ConditionalDataBinding { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the position at which the modifier appears on the data binding
|
||||
/// </summary>
|
||||
public int Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value to be applied when the condition is met
|
||||
/// </summary>
|
||||
public TProperty Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the root group of the condition that must be met
|
||||
/// </summary>
|
||||
public DataModelConditionGroup Condition { get; private set; }
|
||||
|
||||
internal DataBindingConditionEntity Entity { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Evaluate()
|
||||
{
|
||||
return Condition.Evaluate();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Save()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DataBindingCondition");
|
||||
|
||||
if (!ConditionalDataBinding.Entity.Values.Contains(Entity))
|
||||
ConditionalDataBinding.Entity.Values.Add(Entity);
|
||||
|
||||
Entity.Condition = Condition.Entity;
|
||||
Condition.Save();
|
||||
|
||||
Entity.Value = CoreJson.SerializeObject(Value);
|
||||
Entity.Order = Order;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Load()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DataBindingCondition");
|
||||
|
||||
Condition = Entity.Condition != null
|
||||
? new DataModelConditionGroup(null, Entity.Condition)
|
||||
: new DataModelConditionGroup(null);
|
||||
|
||||
Value = (Entity.Value == null ? default : CoreJson.DeserializeObject<TProperty>(Entity.Value))!;
|
||||
Order = Entity.Order;
|
||||
}
|
||||
|
||||
#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)
|
||||
{
|
||||
_disposed = true;
|
||||
Condition.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a condition and a value inside a <see cref="ConditionalDataBinding{TLayerProperty,TProperty}" />
|
||||
/// </summary>
|
||||
public interface IDataBindingCondition : IStorageModel, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates the condition
|
||||
/// </summary>
|
||||
bool Evaluate();
|
||||
}
|
||||
}
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A modifier that changes the source value of a data binding in some way
|
||||
/// <para>
|
||||
/// To implement your own condition operator, inherit <see cref="DataBindingModifierType{TValue, TParameter}" /> or
|
||||
/// <see cref="DataBindingModifierType{TValue}" />
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public abstract class BaseDataBindingModifierType
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the plugin this data binding modifier belongs to
|
||||
/// <para>Note: Not set until after registering</para>
|
||||
/// </summary>
|
||||
public Plugin? Plugin { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value type of this modifier type
|
||||
/// </summary>
|
||||
public abstract Type ValueType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter type of this modifier type. May be null if the modifier type does not support a parameter
|
||||
/// </summary>
|
||||
public abstract Type? ParameterType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of this modifier
|
||||
/// </summary>
|
||||
public abstract string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the icon of this modifier
|
||||
/// </summary>
|
||||
public abstract string? Icon { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description of this modifier
|
||||
/// </summary>
|
||||
public virtual string? Description => null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the category of this modifier
|
||||
/// </summary>
|
||||
public virtual string? Category => null;
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the given type is supported by the modifier
|
||||
/// </summary>
|
||||
/// <param name="type">The type to check for, must be either the same or be castable to the target type</param>
|
||||
/// <param name="part">Which part of the modifier to check, the value or the parameter</param>
|
||||
public bool SupportsType(Type type, ModifierTypePart part)
|
||||
{
|
||||
if (type == null)
|
||||
return true;
|
||||
if (part == ModifierTypePart.Value)
|
||||
return ValueType.IsCastableFrom(type);
|
||||
return ParameterType != null && ParameterType.IsCastableFrom(type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the modifier to the provided current value
|
||||
/// <para>
|
||||
/// This leaves the caller responsible for the types matching <see cref="ValueType" /> and
|
||||
/// <see cref="ParameterType" />
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="currentValue">The current value before modification, type should match <see cref="ValueType" /></param>
|
||||
/// <param name="parameterValue">
|
||||
/// The parameter to use for the modification, type should match <see cref="ParameterType" />
|
||||
/// </param>
|
||||
/// <returns>The modified value, with a type of <see cref="ValueType" /></returns>
|
||||
internal abstract object? InternalApply(object? currentValue, object? parameterValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a part of a modifier type
|
||||
/// </summary>
|
||||
public enum ModifierTypePart
|
||||
{
|
||||
/// <summary>
|
||||
/// The value part of a modifier, backed by <see cref="DataBindingModifierType{TValue}.ValueType" />
|
||||
/// </summary>
|
||||
Value,
|
||||
|
||||
/// <summary>
|
||||
/// The parameter part of a modifier, backed by <see cref="DataBindingModifierType{TValue,TParameter}.ParameterType" />
|
||||
/// </summary>
|
||||
Parameter
|
||||
}
|
||||
}
|
||||
@@ -1,324 +0,0 @@
|
||||
using System;
|
||||
using Artemis.Storage.Entities.Profile.DataBindings;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class DataBindingModifier<TLayerProperty, TProperty> : IDataBindingModifier
|
||||
{
|
||||
private bool _disposed;
|
||||
|
||||
internal DataBindingModifier(DirectDataBinding<TLayerProperty, TProperty> directDataBinding, ProfileRightSideType parameterType)
|
||||
{
|
||||
DirectDataBinding = directDataBinding ?? throw new ArgumentNullException(nameof(directDataBinding));
|
||||
Order = directDataBinding.Modifiers.Count + 1;
|
||||
ParameterType = parameterType;
|
||||
Entity = new DataBindingModifierEntity();
|
||||
Initialize();
|
||||
Save();
|
||||
}
|
||||
|
||||
internal DataBindingModifier(DirectDataBinding<TLayerProperty, TProperty> directDataBinding, DataBindingModifierEntity entity)
|
||||
{
|
||||
DirectDataBinding = directDataBinding ?? throw new ArgumentNullException(nameof(directDataBinding));
|
||||
Entity = entity;
|
||||
Load();
|
||||
Initialize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of modifier that is being applied
|
||||
/// </summary>
|
||||
public BaseDataBindingModifierType? ModifierType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the direct data binding this modifier is applied to
|
||||
/// </summary>
|
||||
public DirectDataBinding<TLayerProperty, TProperty> DirectDataBinding { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the parameter, can either be dynamic (based on a data model value) or static
|
||||
/// </summary>
|
||||
public ProfileRightSideType ParameterType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the position at which the modifier appears on the data binding
|
||||
/// </summary>
|
||||
public int Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path of the parameter property
|
||||
/// </summary>
|
||||
public DataModelPath? ParameterPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter static value, only used it <see cref="ParameterType" /> is
|
||||
/// <see cref="ProfileRightSideType.Static" />
|
||||
/// </summary>
|
||||
public object? ParameterStaticValue { get; private set; }
|
||||
|
||||
internal DataBindingModifierEntity Entity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies the modifier to the provided value
|
||||
/// </summary>
|
||||
/// <param name="currentValue">The value to apply the modifier to, should be of the same type as the data binding target</param>
|
||||
/// <returns>The modified value</returns>
|
||||
public object? Apply(object? currentValue)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DataBindingModifier");
|
||||
|
||||
if (ModifierType == null)
|
||||
return currentValue;
|
||||
|
||||
if (ModifierType.ParameterType == null)
|
||||
return ModifierType.InternalApply(currentValue, null);
|
||||
|
||||
if (ParameterType == ProfileRightSideType.Dynamic && ParameterPath != null && ParameterPath.IsValid)
|
||||
{
|
||||
object? value = ParameterPath.GetValue();
|
||||
return ModifierType.InternalApply(currentValue, value);
|
||||
}
|
||||
|
||||
if (ParameterType == ProfileRightSideType.Static)
|
||||
return ModifierType.InternalApply(currentValue, ParameterStaticValue);
|
||||
|
||||
return currentValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the modifier type of the modifier and re-compiles the expression
|
||||
/// </summary>
|
||||
/// <param name="modifierType"></param>
|
||||
public void UpdateModifierType(BaseDataBindingModifierType? modifierType)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DataBindingModifier");
|
||||
|
||||
if (modifierType == null)
|
||||
{
|
||||
ModifierType = null;
|
||||
return;
|
||||
}
|
||||
|
||||
Type? targetType = DirectDataBinding.DataBinding.GetTargetType();
|
||||
if (targetType != null && !modifierType.SupportsType(targetType, ModifierTypePart.Value))
|
||||
throw new ArtemisCoreException($"Cannot apply modifier type {modifierType.GetType().Name} to this modifier because " +
|
||||
$"it does not support this data binding's type {targetType.Name}");
|
||||
|
||||
ModifierType = modifierType;
|
||||
ValidateParameter();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the parameter of the modifier and makes the modifier dynamic
|
||||
/// </summary>
|
||||
/// <param name="path">The path pointing to the parameter</param>
|
||||
public void UpdateParameterDynamic(DataModelPath? path)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DataBindingModifier");
|
||||
|
||||
if (path != null && !path.IsValid)
|
||||
throw new ArtemisCoreException("Cannot update parameter of data binding modifier to an invalid path");
|
||||
|
||||
ParameterPath?.Dispose();
|
||||
ParameterPath = path != null ? new DataModelPath(path) : null;
|
||||
|
||||
ParameterType = ProfileRightSideType.Dynamic;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the parameter of the modifier, makes the modifier static and re-compiles the expression
|
||||
/// </summary>
|
||||
/// <param name="staticValue">The static value to use as a parameter</param>
|
||||
public void UpdateParameterStatic(object? staticValue)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DataBindingModifier");
|
||||
|
||||
ParameterType = ProfileRightSideType.Static;
|
||||
ParameterPath?.Dispose();
|
||||
ParameterPath = null;
|
||||
|
||||
Type? parameterType = ModifierType?.ParameterType ?? DirectDataBinding.DataBinding.GetTargetType();
|
||||
|
||||
// If not null ensure the types match and if not, convert it
|
||||
if (parameterType == null || staticValue != null && staticValue.GetType() == parameterType)
|
||||
ParameterStaticValue = staticValue;
|
||||
else if (staticValue != null)
|
||||
ParameterStaticValue = Convert.ChangeType(staticValue, parameterType);
|
||||
// If null create a default instance for value types or simply make it null for reference types
|
||||
else if (parameterType.IsValueType)
|
||||
ParameterStaticValue = Activator.CreateInstance(parameterType);
|
||||
else
|
||||
ParameterStaticValue = null;
|
||||
}
|
||||
|
||||
private void ValidateParameter()
|
||||
{
|
||||
if (ModifierType == null)
|
||||
return;
|
||||
|
||||
if (ParameterType == ProfileRightSideType.Dynamic)
|
||||
{
|
||||
if (ParameterPath == null || !ParameterPath.IsValid)
|
||||
return;
|
||||
|
||||
Type parameterType = ParameterPath.GetPropertyType()!;
|
||||
if (!ModifierType.SupportsType(parameterType, ModifierTypePart.Parameter))
|
||||
UpdateParameterDynamic(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ParameterStaticValue == null)
|
||||
return;
|
||||
|
||||
if (!ModifierType.SupportsType(ParameterStaticValue.GetType(), ModifierTypePart.Parameter))
|
||||
UpdateParameterStatic(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
DataBindingModifierTypeStore.DataBindingModifierAdded += DataBindingModifierTypeStoreOnDataBindingModifierAdded;
|
||||
DataBindingModifierTypeStore.DataBindingModifierRemoved += DataBindingModifierTypeStoreOnDataBindingModifierRemoved;
|
||||
|
||||
// Modifier type
|
||||
if (Entity.ModifierTypePluginGuid != null && ModifierType == null)
|
||||
{
|
||||
BaseDataBindingModifierType? modifierType = DataBindingModifierTypeStore.Get(Entity.ModifierTypePluginGuid.Value, Entity.ModifierType)?.DataBindingModifierType;
|
||||
if (modifierType != null)
|
||||
UpdateModifierType(modifierType);
|
||||
}
|
||||
|
||||
// Dynamic parameter
|
||||
if (ParameterType == ProfileRightSideType.Dynamic && Entity.ParameterPath != null)
|
||||
{
|
||||
ParameterPath = new DataModelPath(null, Entity.ParameterPath);
|
||||
}
|
||||
// Static parameter
|
||||
else if (ParameterType == ProfileRightSideType.Static && Entity.ParameterStaticValue != null)
|
||||
{
|
||||
// Use the target type so JSON.NET has a better idea what to do
|
||||
Type? parameterType = ModifierType?.ParameterType ?? DirectDataBinding.DataBinding.GetTargetType();
|
||||
object? staticValue = null;
|
||||
|
||||
try
|
||||
{
|
||||
staticValue = parameterType != null
|
||||
? CoreJson.DeserializeObject(Entity.ParameterStaticValue, parameterType)
|
||||
: CoreJson.DeserializeObject(Entity.ParameterStaticValue);
|
||||
}
|
||||
// If deserialization fails, use the type's default
|
||||
catch (JsonSerializationException e)
|
||||
{
|
||||
DeserializationLogger.LogModifierDeserializationFailure(GetType().Name, e);
|
||||
if (parameterType != null)
|
||||
staticValue = Activator.CreateInstance(parameterType);
|
||||
}
|
||||
|
||||
UpdateParameterStatic(staticValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Save()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DataBindingModifier");
|
||||
|
||||
// Don't save an invalid state
|
||||
if (ParameterPath != null && !ParameterPath.IsValid)
|
||||
return;
|
||||
|
||||
if (!DirectDataBinding.Entity.Modifiers.Contains(Entity))
|
||||
DirectDataBinding.Entity.Modifiers.Add(Entity);
|
||||
|
||||
// Modifier
|
||||
if (ModifierType?.Plugin != null)
|
||||
{
|
||||
Entity.ModifierType = ModifierType.GetType().Name;
|
||||
Entity.ModifierTypePluginGuid = ModifierType.Plugin.Guid;
|
||||
}
|
||||
|
||||
// General
|
||||
Entity.Order = Order;
|
||||
Entity.ParameterType = (int) ParameterType;
|
||||
|
||||
// Parameter
|
||||
ParameterPath?.Save();
|
||||
Entity.ParameterPath = ParameterPath?.Entity;
|
||||
|
||||
Entity.ParameterStaticValue = CoreJson.SerializeObject(ParameterStaticValue);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Load()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DataBindingModifier");
|
||||
|
||||
// Modifier type is done during Initialize
|
||||
|
||||
// General
|
||||
Order = Entity.Order;
|
||||
ParameterType = (ProfileRightSideType) Entity.ParameterType;
|
||||
|
||||
// Parameter is done during initialize
|
||||
}
|
||||
|
||||
#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)
|
||||
{
|
||||
_disposed = true;
|
||||
|
||||
DataBindingModifierTypeStore.DataBindingModifierAdded -= DataBindingModifierTypeStoreOnDataBindingModifierAdded;
|
||||
DataBindingModifierTypeStore.DataBindingModifierRemoved -= DataBindingModifierTypeStoreOnDataBindingModifierRemoved;
|
||||
|
||||
ParameterPath?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event handlers
|
||||
|
||||
private void DataBindingModifierTypeStoreOnDataBindingModifierAdded(object? sender, DataBindingModifierTypeStoreEvent e)
|
||||
{
|
||||
if (ModifierType != null)
|
||||
return;
|
||||
|
||||
BaseDataBindingModifierType modifierType = e.TypeRegistration.DataBindingModifierType;
|
||||
if (modifierType.Plugin!.Guid == Entity.ModifierTypePluginGuid && modifierType.GetType().Name == Entity.ModifierType)
|
||||
UpdateModifierType(modifierType);
|
||||
}
|
||||
|
||||
private void DataBindingModifierTypeStoreOnDataBindingModifierRemoved(object? sender, DataBindingModifierTypeStoreEvent e)
|
||||
{
|
||||
if (e.TypeRegistration.DataBindingModifierType == ModifierType)
|
||||
UpdateModifierType(null);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A modifier that changes the source value of a data binding in some way using a parameter
|
||||
/// </summary>
|
||||
public abstract class DataBindingModifierType<TValue, TParameter> : BaseDataBindingModifierType
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override Type ValueType => typeof(TValue);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Type ParameterType => typeof(TParameter);
|
||||
|
||||
/// <summary>
|
||||
/// Called whenever the modifier must apply to a specific value
|
||||
/// </summary>
|
||||
/// <param name="currentValue">
|
||||
/// The current value before modification
|
||||
/// </param>
|
||||
/// <param name="parameterValue">
|
||||
/// The parameter to use for the modification
|
||||
/// </param>
|
||||
/// <returns>The modified value></returns>
|
||||
public abstract TValue Apply(TValue currentValue, TParameter parameterValue);
|
||||
|
||||
/// <inheritdoc />
|
||||
internal override object? InternalApply(object? currentValue, object? parameterValue)
|
||||
{
|
||||
// TODO: Can we avoid boxing/unboxing?
|
||||
TValue current;
|
||||
if (currentValue != null)
|
||||
current = (TValue) Convert.ChangeType(currentValue, typeof(TValue));
|
||||
else
|
||||
current = default;
|
||||
|
||||
TParameter parameter;
|
||||
if (parameterValue != null)
|
||||
parameter = (TParameter) Convert.ChangeType(parameterValue, typeof(TParameter));
|
||||
else
|
||||
parameter = default;
|
||||
|
||||
return Apply(current!, parameter!);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A modifier that changes the source value of a data binding in some way
|
||||
/// </summary>
|
||||
public abstract class DataBindingModifierType<TValue> : BaseDataBindingModifierType
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override Type ValueType => typeof(TValue);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Type? ParameterType => null;
|
||||
|
||||
/// <summary>
|
||||
/// Called whenever the modifier must apply to a specific value
|
||||
/// </summary>
|
||||
/// <param name="currentValue">
|
||||
/// The current value before modification
|
||||
/// </param>
|
||||
/// <returns>The modified value</returns>
|
||||
public abstract TValue Apply(TValue currentValue);
|
||||
|
||||
/// <inheritdoc />
|
||||
internal override object? InternalApply(object? currentValue, object? parameterValue)
|
||||
{
|
||||
// TODO: Can we avoid boxing/unboxing?
|
||||
TValue current;
|
||||
if (currentValue != null)
|
||||
current = (TValue) Convert.ChangeType(currentValue, typeof(TValue));
|
||||
else
|
||||
current = default;
|
||||
|
||||
return Apply(current!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Modifies a data model value in a way defined by the modifier type
|
||||
/// </summary>
|
||||
public interface IDataBindingModifier : IStorageModel, IDisposable
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using Artemis.Storage.Entities.Profile.DataBindings;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a data binding mode that directly applies a data model value to a data binding
|
||||
/// </summary>
|
||||
public class DirectDataBinding<TLayerProperty, TProperty> : IDataBindingMode<TLayerProperty, TProperty>
|
||||
{
|
||||
private readonly List<DataBindingModifier<TLayerProperty, TProperty>> _modifiers = new();
|
||||
private bool _disposed;
|
||||
|
||||
internal DirectDataBinding(DataBinding<TLayerProperty, TProperty> dataBinding, DirectDataBindingEntity entity)
|
||||
{
|
||||
DataBinding = dataBinding;
|
||||
Entity = entity;
|
||||
Modifiers = new ReadOnlyCollection<DataBindingModifier<TLayerProperty, TProperty>>(_modifiers);
|
||||
Load();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path of the source property
|
||||
/// </summary>
|
||||
public DataModelPath? SourcePath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of modifiers applied to this data binding
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<DataBindingModifier<TLayerProperty, TProperty>> Modifiers { get; }
|
||||
|
||||
internal DirectDataBindingEntity Entity { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DataBinding<TLayerProperty, TProperty> DataBinding { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public TProperty GetValue(TProperty baseValue)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DirectDataBinding");
|
||||
|
||||
if (SourcePath == null || !SourcePath.IsValid || DataBinding.Converter == null)
|
||||
return baseValue;
|
||||
|
||||
object? dataBindingValue = SourcePath.GetValue();
|
||||
foreach (DataBindingModifier<TLayerProperty, TProperty> dataBindingModifier in Modifiers)
|
||||
dataBindingValue = dataBindingModifier.Apply(dataBindingValue);
|
||||
|
||||
return DataBinding.Converter.ConvertFromObject(dataBindingValue);
|
||||
}
|
||||
|
||||
#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)
|
||||
{
|
||||
_disposed = true;
|
||||
|
||||
foreach (DataBindingModifier<TLayerProperty, TProperty> dataBindingModifier in Modifiers)
|
||||
dataBindingModifier.Dispose();
|
||||
|
||||
SourcePath?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Storage
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Load()
|
||||
{
|
||||
// Source
|
||||
if (Entity.SourcePath != null)
|
||||
SourcePath = new DataModelPath(null, Entity.SourcePath);
|
||||
|
||||
// Modifiers
|
||||
foreach (DataBindingModifierEntity dataBindingModifierEntity in Entity.Modifiers)
|
||||
_modifiers.Add(new DataBindingModifier<TLayerProperty, TProperty>(this, dataBindingModifierEntity));
|
||||
|
||||
ApplyOrder();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Save()
|
||||
{
|
||||
// Don't save an invalid state
|
||||
if (SourcePath != null && !SourcePath.IsValid)
|
||||
return;
|
||||
|
||||
SourcePath?.Save();
|
||||
Entity.SourcePath = SourcePath?.Entity;
|
||||
|
||||
// Modifiers
|
||||
Entity.Modifiers.Clear();
|
||||
foreach (DataBindingModifier<TLayerProperty, TProperty> dataBindingModifier in Modifiers)
|
||||
dataBindingModifier.Save();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Source
|
||||
|
||||
/// <summary>
|
||||
/// Returns the type of the source property of this data binding
|
||||
/// </summary>
|
||||
public Type? GetSourceType()
|
||||
{
|
||||
return SourcePath?.GetPropertyType();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the source of the data binding
|
||||
/// </summary>
|
||||
/// <param name="path">The path pointing to the source</param>
|
||||
public void UpdateSource(DataModelPath? path)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DirectDataBinding");
|
||||
|
||||
if (path != null && !path.IsValid)
|
||||
throw new ArtemisCoreException("Cannot update source of data binding to an invalid path");
|
||||
|
||||
SourcePath?.Dispose();
|
||||
SourcePath = path != null ? new DataModelPath(path) : null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Modifiers
|
||||
|
||||
/// <summary>
|
||||
/// Adds a modifier to the direct data binding's <see cref="Modifiers" /> collection
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the parameter, can either be dynamic (based on a data model value) or static</param>
|
||||
public DataBindingModifier<TLayerProperty, TProperty> AddModifier(ProfileRightSideType type)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DirectDataBinding");
|
||||
|
||||
DataBindingModifier<TLayerProperty, TProperty> modifier = new(this, type);
|
||||
_modifiers.Add(modifier);
|
||||
|
||||
ApplyOrder();
|
||||
OnModifiersUpdated();
|
||||
|
||||
return modifier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a modifier from the direct data binding's <see cref="Modifiers" /> collection and disposes it
|
||||
/// </summary>
|
||||
public void RemoveModifier(DataBindingModifier<TLayerProperty, TProperty> modifier)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("DirectDataBinding");
|
||||
if (!_modifiers.Contains(modifier))
|
||||
return;
|
||||
|
||||
_modifiers.Remove(modifier);
|
||||
modifier.Dispose();
|
||||
|
||||
ApplyOrder();
|
||||
OnModifiersUpdated();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the current order of conditions to the <see cref="Modifiers" /> collection
|
||||
/// </summary>
|
||||
public void ApplyOrder()
|
||||
{
|
||||
_modifiers.Sort((a, b) => a.Order.CompareTo(b.Order));
|
||||
for (int index = 0; index < _modifiers.Count; index++)
|
||||
{
|
||||
DataBindingModifier<TLayerProperty, TProperty> modifier = _modifiers[index];
|
||||
modifier.Order = index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a modifier is added or removed
|
||||
/// </summary>
|
||||
public event EventHandler? ModifiersUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="ModifiersUpdated" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnModifiersUpdated()
|
||||
{
|
||||
ModifiersUpdated?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a data binding mode
|
||||
/// </summary>
|
||||
public interface IDataBindingMode<TLayerProperty, TProperty> : IStorageModel, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the data binding this mode is applied to
|
||||
/// </summary>
|
||||
DataBinding<TLayerProperty, TProperty> DataBinding { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value of the data binding
|
||||
/// </summary>
|
||||
/// <param name="baseValue">The base value of the property the data binding is applied to</param>
|
||||
/// <returns></returns>
|
||||
TProperty GetValue(TProperty baseValue);
|
||||
}
|
||||
}
|
||||
@@ -72,9 +72,12 @@ namespace Artemis.Core
|
||||
SubscribeToDataModelStore();
|
||||
}
|
||||
|
||||
internal DataModelPath(DataModel? target, DataModelPathEntity entity)
|
||||
/// <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)
|
||||
{
|
||||
Target = target;
|
||||
Path = entity.Path;
|
||||
Entity = entity;
|
||||
|
||||
@@ -110,7 +113,10 @@ namespace Artemis.Core
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<DataModelPathSegment> Segments => _segments.ToList().AsReadOnly();
|
||||
|
||||
internal DataModelPathEntity Entity { get; }
|
||||
/// <summary>
|
||||
/// Gets the entity used for persistent storage
|
||||
/// </summary>
|
||||
public DataModelPathEntity Entity { get; }
|
||||
|
||||
internal Func<object, object>? Accessor { get; private set; }
|
||||
|
||||
@@ -173,6 +179,52 @@ namespace Artemis.Core
|
||||
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);
|
||||
@@ -262,27 +314,24 @@ namespace Artemis.Core
|
||||
DataModelStore.DataModelAdded += DataModelStoreOnDataModelAdded;
|
||||
DataModelStore.DataModelRemoved += DataModelStoreOnDataModelRemoved;
|
||||
}
|
||||
|
||||
#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)
|
||||
private void DataModelStoreOnDataModelAdded(object? sender, DataModelStoreEvent e)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_disposed = true;
|
||||
if (e.Registration.DataModel.Module.Id != Entity.DataModelId)
|
||||
return;
|
||||
|
||||
DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded;
|
||||
DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved;
|
||||
Invalidate();
|
||||
Target = e.Registration.DataModel;
|
||||
Initialize();
|
||||
}
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
private void DataModelStoreOnDataModelRemoved(object? sender, DataModelStoreEvent e)
|
||||
{
|
||||
if (e.Registration.DataModel.Module.Id != Entity.DataModelId)
|
||||
return;
|
||||
|
||||
Invalidate();
|
||||
Target = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -292,8 +341,6 @@ namespace Artemis.Core
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Storage
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -314,68 +361,33 @@ namespace Artemis.Core
|
||||
|
||||
Entity.Path = Path;
|
||||
Entity.DataModelId = DataModelId;
|
||||
}
|
||||
|
||||
Entity.WrapperType = Target switch
|
||||
{
|
||||
ListPredicateWrapperDataModel _ => PathWrapperType.List,
|
||||
EventPredicateWrapperDataModel _ => PathWrapperType.Event,
|
||||
_ => PathWrapperType.None
|
||||
};
|
||||
#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() != this.GetType()) return false;
|
||||
return Equals((DataModelPath) obj);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Target, Path);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event handlers
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
/// <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>
|
||||
/// 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);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -295,7 +295,10 @@ namespace Artemis.Core
|
||||
private void DynamicChildOnDynamicChildAdded(object? sender, DynamicDataModelChildEventArgs e)
|
||||
{
|
||||
if (e.Key == Identifier)
|
||||
{
|
||||
DataModelPath.Invalidate();
|
||||
DataModelPath.Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
private void DynamicChildOnDynamicChildRemoved(object? sender, DynamicDataModelChildEventArgs e)
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.Collections.Generic;
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a data model event that can trigger <see cref="DataModelConditionEvent" />s.
|
||||
/// Represents an event that is part of a data model
|
||||
/// </summary>
|
||||
public interface IDataModelEvent
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Artemis.Storage.Entities.Profile;
|
||||
|
||||
namespace Artemis.Core
|
||||
@@ -23,6 +22,26 @@ namespace Artemis.Core
|
||||
/// </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 layer
|
||||
/// </summary>
|
||||
@@ -47,11 +66,6 @@ namespace Artemis.Core
|
||||
/// </summary>
|
||||
void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description, string path);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list off all data binding registrations
|
||||
/// </summary>
|
||||
List<IDataBindingRegistration> GetAllDataBindingRegistrations();
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to load and add the provided keyframe entity to the layer property
|
||||
/// </summary>
|
||||
@@ -70,6 +84,12 @@ namespace Artemis.Core
|
||||
/// <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>
|
||||
/// Occurs when the layer property is disposed
|
||||
/// </summary>
|
||||
@@ -104,25 +124,5 @@ namespace Artemis.Core
|
||||
/// Occurs when a keyframe was removed from the layer property
|
||||
/// </summary>
|
||||
public event EventHandler<LayerPropertyEventArgs>? KeyframeRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a data binding property has been added
|
||||
/// </summary>
|
||||
public event EventHandler<LayerPropertyEventArgs>? DataBindingPropertyRegistered;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when all data binding properties have been removed
|
||||
/// </summary>
|
||||
public event EventHandler<LayerPropertyEventArgs>? DataBindingPropertiesCleared;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a data binding has been enabled
|
||||
/// </summary>
|
||||
public event EventHandler<LayerPropertyEventArgs>? DataBindingEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a data binding has been disabled
|
||||
/// </summary>
|
||||
public event EventHandler<LayerPropertyEventArgs>? DataBindingDisabled;
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ namespace Artemis.Core
|
||||
Path = null!;
|
||||
Entity = null!;
|
||||
PropertyDescription = null!;
|
||||
DataBinding = null!;
|
||||
|
||||
CurrentValue = default!;
|
||||
DefaultValue = default!;
|
||||
@@ -58,93 +59,10 @@ namespace Artemis.Core
|
||||
{
|
||||
_disposed = true;
|
||||
|
||||
foreach (IDataBinding dataBinding in _dataBindings)
|
||||
dataBinding.Dispose();
|
||||
|
||||
DataBinding.Dispose();
|
||||
Disposed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="Updated" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnUpdated()
|
||||
{
|
||||
Updated?.Invoke(this, new LayerPropertyEventArgs(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="CurrentValueSet" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnCurrentValueSet()
|
||||
{
|
||||
CurrentValueSet?.Invoke(this, new LayerPropertyEventArgs(this));
|
||||
LayerPropertyGroup.OnLayerPropertyOnCurrentValueSet(new LayerPropertyEventArgs(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="VisibilityChanged" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnVisibilityChanged()
|
||||
{
|
||||
VisibilityChanged?.Invoke(this, new LayerPropertyEventArgs(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="KeyframesToggled" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnKeyframesToggled()
|
||||
{
|
||||
KeyframesToggled?.Invoke(this, new LayerPropertyEventArgs(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="KeyframeAdded" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnKeyframeAdded()
|
||||
{
|
||||
KeyframeAdded?.Invoke(this, new LayerPropertyEventArgs(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="KeyframeRemoved" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnKeyframeRemoved()
|
||||
{
|
||||
KeyframeRemoved?.Invoke(this, new LayerPropertyEventArgs(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="DataBindingPropertyRegistered" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnDataBindingPropertyRegistered()
|
||||
{
|
||||
DataBindingPropertyRegistered?.Invoke(this, new LayerPropertyEventArgs(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="DataBindingDisabled" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnDataBindingPropertiesCleared()
|
||||
{
|
||||
DataBindingPropertiesCleared?.Invoke(this, new LayerPropertyEventArgs(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="DataBindingEnabled" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnDataBindingEnabled(LayerPropertyEventArgs e)
|
||||
{
|
||||
DataBindingEnabled?.Invoke(this, e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="DataBindingDisabled" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnDataBindingDisabled(LayerPropertyEventArgs e)
|
||||
{
|
||||
DataBindingDisabled?.Invoke(this, e);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public PropertyDescriptionAttribute PropertyDescription { get; internal set; }
|
||||
|
||||
@@ -163,7 +81,16 @@ namespace Artemis.Core
|
||||
CurrentValue = BaseValue;
|
||||
|
||||
UpdateKeyframes(timeline);
|
||||
UpdateDataBindings(timeline);
|
||||
UpdateDataBinding();
|
||||
|
||||
// UpdateDataBinding called OnUpdated()
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UpdateDataBinding()
|
||||
{
|
||||
DataBinding.Update();
|
||||
DataBinding.Apply();
|
||||
|
||||
OnUpdated();
|
||||
}
|
||||
@@ -175,46 +102,11 @@ namespace Artemis.Core
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler? Disposed;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<LayerPropertyEventArgs>? Updated;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<LayerPropertyEventArgs>? CurrentValueSet;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<LayerPropertyEventArgs>? VisibilityChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<LayerPropertyEventArgs>? KeyframesToggled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<LayerPropertyEventArgs>? KeyframeAdded;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<LayerPropertyEventArgs>? KeyframeRemoved;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<LayerPropertyEventArgs>? DataBindingPropertyRegistered;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<LayerPropertyEventArgs>? DataBindingPropertiesCleared;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<LayerPropertyEventArgs>? DataBindingEnabled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<LayerPropertyEventArgs>? DataBindingDisabled;
|
||||
|
||||
#region Hierarchy
|
||||
|
||||
private bool _isHidden;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the property is hidden in the UI
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public bool IsHidden
|
||||
{
|
||||
get => _isHidden;
|
||||
@@ -481,137 +373,19 @@ namespace Artemis.Core
|
||||
|
||||
#region Data bindings
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
internal readonly List<IDataBindingRegistration> _dataBindingRegistrations = new();
|
||||
|
||||
internal readonly List<IDataBinding> _dataBindings = new();
|
||||
// ReSharper restore InconsistentNaming
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether data bindings are supported on this type of property
|
||||
/// Gets the data binding of this property
|
||||
/// </summary>
|
||||
public bool DataBindingsSupported { get; protected internal set; } = true;
|
||||
public DataBinding<T> DataBinding { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the layer has any active data bindings
|
||||
/// </summary>
|
||||
public bool HasDataBinding => GetAllDataBindingRegistrations().Any(r => r.GetDataBinding() != null);
|
||||
/// <inheritdoc />
|
||||
public bool DataBindingsSupported => DataBinding.Properties.Any();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a data binding registration by the display name used to register it
|
||||
/// <para>Note: The expression must exactly match the one used to register the data binding</para>
|
||||
/// </summary>
|
||||
public DataBindingRegistration<T, TProperty>? GetDataBindingRegistration<TProperty>(string identifier)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("LayerProperty");
|
||||
/// <inheritdoc />
|
||||
public IDataBinding BaseDataBinding => DataBinding;
|
||||
|
||||
IDataBindingRegistration? match = _dataBindingRegistrations.FirstOrDefault(r => r is DataBindingRegistration<T, TProperty> registration &&
|
||||
registration.DisplayName == identifier);
|
||||
return (DataBindingRegistration<T, TProperty>?) match;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list containing all data binding registrations of this layer property
|
||||
/// </summary>
|
||||
/// <returns>A list containing all data binding registrations of this layer property</returns>
|
||||
public List<IDataBindingRegistration> GetAllDataBindingRegistrations()
|
||||
{
|
||||
return _dataBindingRegistrations;
|
||||
}
|
||||
|
||||
/// <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="converter">The converter to use while applying the data binding</param>
|
||||
/// <param name="displayName">The display name of the data binding property</param>
|
||||
public DataBindingRegistration<T, TProperty> RegisterDataBindingProperty<TProperty>(Func<TProperty> getter, Action<TProperty> setter, DataBindingConverter<T, TProperty> converter,
|
||||
string displayName)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("LayerProperty");
|
||||
if (_dataBindingRegistrations.Any(d => d.DisplayName == displayName))
|
||||
throw new ArtemisCoreException($"A databinding property named '{displayName}' is already registered.");
|
||||
|
||||
DataBindingRegistration<T, TProperty> registration = new(this, converter, getter, setter, displayName);
|
||||
_dataBindingRegistrations.Add(registration);
|
||||
|
||||
// If not yet initialized, load the data binding related to the registration if available
|
||||
if (_isInitialized)
|
||||
{
|
||||
IDataBinding? dataBinding = registration.CreateDataBinding();
|
||||
if (dataBinding != null)
|
||||
_dataBindings.Add(dataBinding);
|
||||
}
|
||||
|
||||
OnDataBindingPropertyRegistered();
|
||||
return registration;
|
||||
}
|
||||
|
||||
/// <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");
|
||||
|
||||
foreach (IDataBindingRegistration dataBindingRegistration in _dataBindingRegistrations)
|
||||
dataBindingRegistration.ClearDataBinding();
|
||||
_dataBindingRegistrations.Clear();
|
||||
|
||||
OnDataBindingPropertiesCleared();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables a data binding for the provided <paramref name="dataBindingRegistration" />
|
||||
/// </summary>
|
||||
/// <returns>The newly created data binding</returns>
|
||||
public DataBinding<T, TProperty> EnableDataBinding<TProperty>(DataBindingRegistration<T, TProperty> dataBindingRegistration)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("LayerProperty");
|
||||
|
||||
if (dataBindingRegistration.LayerProperty != this)
|
||||
throw new ArtemisCoreException("Cannot enable a data binding using a data binding registration of a different layer property");
|
||||
if (dataBindingRegistration.DataBinding != null)
|
||||
throw new ArtemisCoreException("Provided data binding registration already has an enabled data binding");
|
||||
|
||||
DataBinding<T, TProperty> dataBinding = new(dataBindingRegistration);
|
||||
_dataBindings.Add(dataBinding);
|
||||
|
||||
OnDataBindingEnabled(new LayerPropertyEventArgs(dataBinding.LayerProperty));
|
||||
return dataBinding;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disables the provided data binding
|
||||
/// </summary>
|
||||
/// <param name="dataBinding">The data binding to remove</param>
|
||||
public void DisableDataBinding<TProperty>(DataBinding<T, TProperty> dataBinding)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("LayerProperty");
|
||||
|
||||
_dataBindings.Remove(dataBinding);
|
||||
|
||||
if (dataBinding.Registration != null)
|
||||
dataBinding.Registration.DataBinding = null;
|
||||
dataBinding.Dispose();
|
||||
OnDataBindingDisabled(new LayerPropertyEventArgs(dataBinding.LayerProperty));
|
||||
}
|
||||
|
||||
private void UpdateDataBindings(Timeline timeline)
|
||||
{
|
||||
foreach (IDataBinding dataBinding in _dataBindings)
|
||||
{
|
||||
dataBinding.Update(timeline);
|
||||
dataBinding.Apply();
|
||||
}
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public bool HasDataBinding => DataBinding.IsEnabled;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -695,6 +469,7 @@ namespace Artemis.Core
|
||||
Entity = entity ?? throw new ArgumentNullException(nameof(entity));
|
||||
PropertyDescription = description ?? throw new ArgumentNullException(nameof(description));
|
||||
IsLoadedFromStorage = fromStorage;
|
||||
DataBinding = Entity.DataBinding != null ? new DataBinding<T>(this, Entity.DataBinding) : new DataBinding<T>(this);
|
||||
|
||||
if (PropertyDescription.DisableKeyframes)
|
||||
KeyframesSupported = false;
|
||||
@@ -738,13 +513,7 @@ namespace Artemis.Core
|
||||
// ignored for now
|
||||
}
|
||||
|
||||
_dataBindings.Clear();
|
||||
foreach (IDataBindingRegistration dataBindingRegistration in _dataBindingRegistrations)
|
||||
{
|
||||
IDataBinding? dataBinding = dataBindingRegistration.CreateDataBinding();
|
||||
if (dataBinding != null)
|
||||
_dataBindings.Add(dataBinding);
|
||||
}
|
||||
DataBinding.Load();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -763,9 +532,8 @@ namespace Artemis.Core
|
||||
Entity.KeyframeEntities.Clear();
|
||||
Entity.KeyframeEntities.AddRange(Keyframes.Select(k => k.GetKeyframeEntity()));
|
||||
|
||||
Entity.DataBindingEntities.Clear();
|
||||
foreach (IDataBinding dataBinding in _dataBindings)
|
||||
dataBinding.Save();
|
||||
DataBinding.Save();
|
||||
Entity.DataBinding = DataBinding.Entity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -776,5 +544,79 @@ namespace Artemis.Core
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler? Disposed;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<LayerPropertyEventArgs>? Updated;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<LayerPropertyEventArgs>? CurrentValueSet;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<LayerPropertyEventArgs>? VisibilityChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<LayerPropertyEventArgs>? KeyframesToggled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<LayerPropertyEventArgs>? KeyframeAdded;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<LayerPropertyEventArgs>? KeyframeRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="Updated" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnUpdated()
|
||||
{
|
||||
Updated?.Invoke(this, new LayerPropertyEventArgs(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="CurrentValueSet" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnCurrentValueSet()
|
||||
{
|
||||
CurrentValueSet?.Invoke(this, new LayerPropertyEventArgs(this));
|
||||
LayerPropertyGroup.OnLayerPropertyOnCurrentValueSet(new LayerPropertyEventArgs(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="VisibilityChanged" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnVisibilityChanged()
|
||||
{
|
||||
VisibilityChanged?.Invoke(this, new LayerPropertyEventArgs(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="KeyframesToggled" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnKeyframesToggled()
|
||||
{
|
||||
KeyframesToggled?.Invoke(this, new LayerPropertyEventArgs(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="KeyframeAdded" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnKeyframeAdded()
|
||||
{
|
||||
KeyframeAdded?.Invoke(this, new LayerPropertyEventArgs(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="KeyframeRemoved" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnKeyframeRemoved()
|
||||
{
|
||||
KeyframeRemoved?.Invoke(this, new LayerPropertyEventArgs(this));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -206,12 +206,10 @@ namespace Artemis.Core
|
||||
}
|
||||
}
|
||||
|
||||
List<RenderProfileElement> renderElements = GetAllRenderElements();
|
||||
|
||||
if (ProfileEntity.LastSelectedProfileElement != Guid.Empty)
|
||||
{
|
||||
LastSelectedProfileElement = GetAllFolders().FirstOrDefault(f => f.EntityId == ProfileEntity.LastSelectedProfileElement);
|
||||
if (LastSelectedProfileElement == null)
|
||||
LastSelectedProfileElement = GetAllLayers().FirstOrDefault(f => f.EntityId == ProfileEntity.LastSelectedProfileElement);
|
||||
}
|
||||
LastSelectedProfileElement = renderElements.FirstOrDefault(f => f.EntityId == ProfileEntity.LastSelectedProfileElement);
|
||||
else
|
||||
LastSelectedProfileElement = null;
|
||||
|
||||
@@ -219,6 +217,10 @@ namespace Artemis.Core
|
||||
scriptConfiguration.Script?.Dispose();
|
||||
ScriptConfigurations.Clear();
|
||||
ScriptConfigurations.AddRange(ProfileEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e)));
|
||||
|
||||
// Load node scripts last since they may rely on the profile structure being in place
|
||||
foreach (RenderProfileElement renderProfileElement in renderElements)
|
||||
renderProfileElement.LoadNodeScript();
|
||||
}
|
||||
|
||||
internal override void Save()
|
||||
@@ -253,10 +255,7 @@ namespace Artemis.Core
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<IBreakableModel> GetBrokenHierarchy()
|
||||
{
|
||||
foreach (IBreakableModel breakableModel in GetAllFolders().SelectMany(folders => folders.GetBrokenHierarchy()))
|
||||
yield return breakableModel;
|
||||
foreach (IBreakableModel breakableModel in GetAllLayers().SelectMany(layer => layer.GetBrokenHierarchy()))
|
||||
yield return breakableModel;
|
||||
return GetAllRenderElements().SelectMany(folders => folders.GetBrokenHierarchy());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -91,6 +91,13 @@ namespace Artemis.Core
|
||||
/// </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>
|
||||
@@ -113,13 +120,6 @@ namespace Artemis.Core
|
||||
return $"{nameof(EntityId)}: {EntityId}, {nameof(Order)}: {Order}, {nameof(Name)}: {Name}";
|
||||
}
|
||||
|
||||
#region Overrides of BreakableModel
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string BrokenDisplayName => Name ?? GetType().Name;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Hierarchy
|
||||
|
||||
/// <summary>
|
||||
@@ -139,7 +139,9 @@ namespace Artemis.Core
|
||||
|
||||
// Add to the end of the list
|
||||
if (order == null)
|
||||
{
|
||||
ChildrenList.Add(child);
|
||||
}
|
||||
// Insert at the given index
|
||||
else
|
||||
{
|
||||
@@ -183,6 +185,27 @@ namespace Artemis.Core
|
||||
ChildrenList[index].Order = index;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@@ -232,27 +255,6 @@ namespace Artemis.Core
|
||||
internal abstract void Load();
|
||||
internal abstract void Save();
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the profile element
|
||||
/// </summary>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
@@ -284,5 +286,26 @@ namespace Artemis.Core
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the profile element
|
||||
/// </summary>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using Artemis.Core.LayerEffects.Placeholder;
|
||||
using Artemis.Core.Properties;
|
||||
using Artemis.Storage.Entities.Profile;
|
||||
using Artemis.Storage.Entities.Profile.Abstract;
|
||||
using Artemis.Storage.Entities.Profile.Conditions;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Artemis.Core
|
||||
@@ -60,21 +61,25 @@ namespace Artemis.Core
|
||||
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
|
||||
baseLayerEffect.Dispose();
|
||||
|
||||
DisplayCondition?.Dispose();
|
||||
if (DisplayCondition is IDisposable disposable)
|
||||
disposable.Dispose();
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
internal void LoadRenderElement()
|
||||
{
|
||||
DisplayCondition = RenderElementEntity.DisplayCondition != null
|
||||
? new DataModelConditionGroup(null, RenderElementEntity.DisplayCondition)
|
||||
: new DataModelConditionGroup(null);
|
||||
|
||||
Timeline = RenderElementEntity.Timeline != null
|
||||
? new Timeline(RenderElementEntity.Timeline)
|
||||
: new Timeline();
|
||||
|
||||
DisplayCondition = RenderElementEntity.DisplayCondition switch
|
||||
{
|
||||
StaticConditionEntity staticConditionEntity => new StaticCondition(staticConditionEntity, this),
|
||||
EventConditionEntity eventConditionEntity => new EventCondition(eventConditionEntity, this),
|
||||
_ => DisplayCondition
|
||||
};
|
||||
|
||||
ActivateEffects();
|
||||
}
|
||||
|
||||
@@ -97,15 +102,24 @@ namespace Artemis.Core
|
||||
layerEffect.BaseProperties?.ApplyToEntity();
|
||||
}
|
||||
|
||||
// Conditions
|
||||
RenderElementEntity.DisplayCondition = DisplayCondition?.Entity;
|
||||
// Condition
|
||||
DisplayCondition?.Save();
|
||||
RenderElementEntity.DisplayCondition = DisplayCondition?.Entity;
|
||||
|
||||
// Timeline
|
||||
RenderElementEntity.Timeline = Timeline?.Entity;
|
||||
Timeline?.Save();
|
||||
}
|
||||
|
||||
internal void LoadNodeScript()
|
||||
{
|
||||
if (DisplayCondition is INodeScriptCondition scriptCondition)
|
||||
scriptCondition.LoadNodeScript();
|
||||
|
||||
foreach (ILayerProperty layerProperty in GetAllLayerProperties())
|
||||
layerProperty.BaseDataBinding.LoadNodeScript();
|
||||
}
|
||||
|
||||
internal void OnLayerEffectsUpdated()
|
||||
{
|
||||
LayerEffectsUpdated?.Invoke(this, EventArgs.Empty);
|
||||
@@ -124,10 +138,10 @@ namespace Artemis.Core
|
||||
/// </summary>
|
||||
public void UpdateTimeline(double deltaTime)
|
||||
{
|
||||
// TODO: Move to conditions
|
||||
|
||||
// The play mode dictates whether to stick to the main segment unless the display conditions contains events
|
||||
bool stickToMainSegment = (Timeline.PlayMode == TimelinePlayMode.Repeat || Timeline.EventOverlapMode == TimeLineEventOverlapMode.Toggle) && DisplayConditionMet;
|
||||
if (DisplayCondition != null && DisplayCondition.ContainsEvents && Timeline.EventOverlapMode != TimeLineEventOverlapMode.Toggle)
|
||||
stickToMainSegment = false;
|
||||
bool stickToMainSegment = Timeline.PlayMode == TimelinePlayMode.Repeat && DisplayConditionMet;
|
||||
|
||||
Timeline.Update(TimeSpan.FromSeconds(deltaTime), stickToMainSegment);
|
||||
}
|
||||
@@ -355,19 +369,19 @@ namespace Artemis.Core
|
||||
protected set => SetAndNotify(ref _displayConditionMet, value);
|
||||
}
|
||||
|
||||
private DataModelConditionGroup? _displayCondition;
|
||||
private bool _displayConditionMet;
|
||||
private bool _toggledOnByEvent = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the root display condition group
|
||||
/// Gets the display condition used to determine whether this element is active or not
|
||||
/// </summary>
|
||||
public DataModelConditionGroup? DisplayCondition
|
||||
public ICondition? DisplayCondition
|
||||
{
|
||||
get => _displayCondition;
|
||||
set => SetAndNotify(ref _displayCondition, value);
|
||||
}
|
||||
|
||||
private ICondition? _displayCondition;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the display conditions on this element and applies any required changes to the <see cref="Timeline" />
|
||||
/// </summary>
|
||||
@@ -385,54 +399,9 @@ namespace Artemis.Core
|
||||
return;
|
||||
}
|
||||
|
||||
if (Timeline.EventOverlapMode != TimeLineEventOverlapMode.Toggle)
|
||||
_toggledOnByEvent = false;
|
||||
|
||||
bool conditionMet = DisplayCondition.Evaluate();
|
||||
if (Parent is RenderProfileElement parent && !parent.DisplayConditionMet)
|
||||
conditionMet = false;
|
||||
|
||||
if (!DisplayCondition.ContainsEvents)
|
||||
{
|
||||
// Regular conditions reset the timeline whenever their condition is met and was not met before that
|
||||
if (conditionMet && !DisplayConditionMet && Timeline.IsFinished)
|
||||
Timeline.JumpToStart();
|
||||
// If regular conditions are no longer met, jump to the end segment if stop mode requires it
|
||||
if (!conditionMet && Timeline.StopMode == TimelineStopMode.SkipToEnd)
|
||||
Timeline.JumpToEndSegment();
|
||||
}
|
||||
else if (conditionMet)
|
||||
{
|
||||
if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Toggle)
|
||||
{
|
||||
_toggledOnByEvent = !_toggledOnByEvent;
|
||||
if (_toggledOnByEvent)
|
||||
Timeline.JumpToStart();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Event conditions reset if the timeline finished
|
||||
if (Timeline.IsFinished)
|
||||
{
|
||||
Timeline.JumpToStart();
|
||||
}
|
||||
// and otherwise apply their overlap mode
|
||||
else
|
||||
{
|
||||
if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Restart)
|
||||
Timeline.JumpToStart();
|
||||
else if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Copy)
|
||||
Timeline.AddExtraTimeline();
|
||||
// The third option is ignore which is handled below:
|
||||
|
||||
// done
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DisplayConditionMet = Timeline.EventOverlapMode == TimeLineEventOverlapMode.Toggle
|
||||
? _toggledOnByEvent
|
||||
: conditionMet;
|
||||
DisplayCondition.Update();
|
||||
DisplayCondition.ApplyToTimeline(DisplayCondition.IsMet, DisplayConditionMet, Timeline);
|
||||
DisplayConditionMet = DisplayCondition.IsMet;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Artemis.Core
|
||||
public class Timeline : CorePropertyChanged, IStorageModel
|
||||
{
|
||||
private const int MaxExtraTimelines = 15;
|
||||
private readonly object _lock = new ();
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="Timeline" /> class
|
||||
@@ -82,7 +82,6 @@ namespace Artemis.Core
|
||||
|
||||
private TimeSpan _position;
|
||||
private TimeSpan _lastDelta;
|
||||
private TimeLineEventOverlapMode _eventOverlapMode;
|
||||
private TimelinePlayMode _playMode;
|
||||
private TimelineStopMode _stopMode;
|
||||
private readonly List<Timeline> _extraTimelines;
|
||||
@@ -139,17 +138,7 @@ namespace Artemis.Core
|
||||
get => _stopMode;
|
||||
set => SetAndNotify(ref _stopMode, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the mode in which the render element responds to display condition events being fired before the
|
||||
/// timeline finished
|
||||
/// </summary>
|
||||
public TimeLineEventOverlapMode EventOverlapMode
|
||||
{
|
||||
get => _eventOverlapMode;
|
||||
set => SetAndNotify(ref _eventOverlapMode, value);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of extra copies of the timeline applied to this timeline
|
||||
/// </summary>
|
||||
@@ -326,7 +315,7 @@ namespace Artemis.Core
|
||||
|
||||
IsOverridden = false;
|
||||
_lastOverridePosition = Position;
|
||||
|
||||
|
||||
if (stickToMainSegment && Position > MainSegmentEndPosition)
|
||||
{
|
||||
// If the main segment has no length, simply stick to the start of the segment
|
||||
@@ -445,7 +434,8 @@ namespace Artemis.Core
|
||||
EndSegmentLength = Entity.EndSegmentLength;
|
||||
PlayMode = (TimelinePlayMode) Entity.PlayMode;
|
||||
StopMode = (TimelineStopMode) Entity.StopMode;
|
||||
EventOverlapMode = (TimeLineEventOverlapMode) Entity.EventOverlapMode;
|
||||
|
||||
JumpToEnd();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -456,7 +446,6 @@ namespace Artemis.Core
|
||||
Entity.EndSegmentLength = EndSegmentLength;
|
||||
Entity.PlayMode = (int) PlayMode;
|
||||
Entity.StopMode = (int) StopMode;
|
||||
Entity.EventOverlapMode = (int) EventOverlapMode;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
Reference in new issue
Block a user