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

Merge branch 'feature/events' into master

This commit is contained in:
Robert Beekman 2020-11-05 21:12:56 +01:00 committed by GitHub
commit 143dea4c4c
111 changed files with 3997 additions and 2153 deletions

View File

@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cconditions_005Cwrappers/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cdatabindings_005Cmodes/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cdatabindings_005Cmodes_005Cconditional/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cdatabindings_005Cmodes_005Cdirect/@EntryIndexedValue">True</s:Boolean>

View File

@ -0,0 +1,19 @@
using SkiaSharp;
namespace Artemis.Core.DefaultTypes
{
internal class SKColorDesaturateModifierType : DataBindingModifierType<SKColor, float>
{
public override string Name => "Desaturate";
public override string Icon => "ImageMinus";
public override string Description => "Desaturates the color by the amount in percent";
public override SKColor Apply(SKColor currentValue, float parameterValue)
{
// TODO: Not so straightforward ^^
currentValue.ToHsl(out float h, out float s, out float l);
s *= (parameterValue * -1 + 100f) / 100f;
return SKColor.FromHsl(h, s, l);
}
}
}

View File

@ -0,0 +1,18 @@
using SkiaSharp;
namespace Artemis.Core.DefaultTypes
{
internal class SKColorSaturateModifierType : DataBindingModifierType<SKColor, float>
{
public override string Name => "Saturate";
public override string Icon => "ImagePlus";
public override string Description => "Saturates the color by the amount in percent";
public override SKColor Apply(SKColor currentValue, float parameterValue)
{
// TODO: Not so straightforward ^^
currentValue.ToHsl(out float h, out float s, out float l);
return SKColor.FromHsl(h, parameterValue, l);
}
}
}

View File

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

View File

@ -8,7 +8,7 @@
/// <summary>
/// Performs an update on the model
/// </summary>
/// <param name="deltaTime">The delta time in seconds</param>
void Update(double deltaTime);
/// <param name="timeline">The timeline to apply during update</param>
void Update(Timeline timeline);
}
}

View File

@ -7,6 +7,12 @@ namespace Artemis.Core
/// </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>
@ -20,24 +26,32 @@ namespace Artemis.Core
// TODO: Can we avoid boxing/unboxing?
TLeftSide leftSide;
if (leftSideValue != null)
leftSide = (TLeftSide) Convert.ChangeType(leftSideValue, typeof(TLeftSide));
{
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)
rightSide = (TRightSide) Convert.ChangeType(rightSideValue, typeof(TRightSide));
{
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!);
}
/// <inheritdoc />
public override Type LeftSideType => typeof(TLeftSide);
/// <inheritdoc />
public override Type RightSideType => typeof(TRightSide);
}
/// <summary>
@ -45,6 +59,14 @@ namespace Artemis.Core
/// </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>
@ -57,19 +79,18 @@ namespace Artemis.Core
// TODO: Can we avoid boxing/unboxing?
TLeftSide leftSide;
if (leftSideValue != null)
leftSide = (TLeftSide) Convert.ChangeType(leftSideValue, typeof(TLeftSide));
{
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!);
}
/// <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;
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Storage.Entities.Profile.Abstract;
namespace Artemis.Core
@ -36,6 +37,8 @@ namespace Artemis.Core
_children.Insert(index.Value, dataModelConditionPart);
else
_children.Add(dataModelConditionPart);
OnChildAdded();
}
}
@ -49,9 +52,19 @@ namespace Artemis.Core
{
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>
@ -71,7 +84,7 @@ namespace Artemis.Core
#region IDisposable
/// <summary>
/// Disposed the condition part
/// Disposed the condition part
/// </summary>
protected virtual void Dispose(bool disposing)
{
@ -88,5 +101,22 @@ namespace Artemis.Core
}
#endregion
#region Events
public event EventHandler ChildAdded;
public event EventHandler ChildRemoved;
protected virtual void OnChildAdded()
{
ChildAdded?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnChildRemoved()
{
ChildRemoved?.Invoke(this, EventArgs.Empty);
}
#endregion
}
}

View File

@ -1,6 +1,4 @@
using System;
using System.IO;
using Artemis.Core.DataModelExpansions;
using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Conditions;
using Newtonsoft.Json;
@ -11,20 +9,19 @@ namespace Artemis.Core
/// 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 DataModelConditionPredicate : DataModelConditionPart
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>
public DataModelConditionPredicate(DataModelConditionPart parent, ProfileRightSideType predicateType)
/// <param name="entity">A new empty entity</param>
protected DataModelConditionPredicate(DataModelConditionPart parent, ProfileRightSideType predicateType, DataModelConditionPredicateEntity entity)
{
Parent = parent;
Entity = entity;
PredicateType = predicateType;
Entity = new DataModelConditionPredicateEntity();
Initialize();
}
internal DataModelConditionPredicate(DataModelConditionPart parent, DataModelConditionPredicateEntity entity)
@ -32,8 +29,6 @@ namespace Artemis.Core
Parent = parent;
Entity = entity;
PredicateType = (ProfileRightSideType) entity.PredicateType;
Initialize();
}
/// <summary>
@ -44,31 +39,117 @@ namespace Artemis.Core
/// <summary>
/// Gets the operator
/// </summary>
public BaseConditionOperator? Operator { get; private set; }
public BaseConditionOperator? Operator { get; protected set; }
/// <summary>
/// Gets the path of the left property
/// </summary>
public DataModelPath? LeftPath { get; private set; }
public DataModelPath? LeftPath { get; protected set; }
/// <summary>
/// Gets the path of the right property
/// </summary>
public DataModelPath? RightPath { get; private set; }
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; private set; }
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 (LeftPath != null && LeftPath.IsValid)
{
// Use the left side type so JSON.NET has a better idea what to do
Type leftSideType = LeftPath.GetPropertyType()!;
object? rightSideValue;
try
{
rightSideValue = JsonConvert.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);
}
else
{
// Hope for the best...
UpdateRightSideStatic(JsonConvert.DeserializeObject(Entity.RightStaticValue));
}
}
catch (JsonReaderException e)
{
DeserializationLogger.LogPredicateDeserializationFailure(this, e);
}
}
protected abstract void InitializeLeftPath();
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 void UpdateLeftSide(DataModelPath? path)
public virtual void UpdateLeftSide(DataModelPath? path)
{
if (path != null && !path.IsValid)
throw new ArtemisCoreException("Cannot update left side of predicate to an invalid path");
@ -113,6 +194,7 @@ namespace Artemis.Core
RightStaticValue = staticValue;
return;
}
// If the operator does not support a right side, always set it to null
if (Operator.RightSideType == null)
{
@ -121,13 +203,14 @@ namespace Artemis.Core
}
// If not null ensure the types match and if not, convert it
if (staticValue != null && staticValue.GetType() == Operator.RightSideType)
Type? preferredType = GetPreferredRightSideType();
if (staticValue != null && staticValue.GetType() == preferredType || preferredType == null)
RightStaticValue = staticValue;
else if (staticValue != null)
RightStaticValue = Convert.ChangeType(staticValue, Operator.RightSideType);
RightStaticValue = Convert.ChangeType(staticValue, preferredType);
// If null create a default instance for value types or simply make it null for reference types
else if (Operator.RightSideType.IsValueType)
RightStaticValue = Activator.CreateInstance(Operator.RightSideType);
else if (preferredType.IsValueType)
RightStaticValue = Activator.CreateInstance(preferredType);
else
RightStaticValue = null;
}
@ -161,137 +244,10 @@ namespace Artemis.Core
ValidateRightSide();
}
/// <inheritdoc />
public override bool Evaluate()
{
if (Operator == null || LeftPath == null || !LeftPath.IsValid)
return false;
// 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 />
public override string ToString()
{
if (PredicateType == ProfileRightSideType.Dynamic)
return $"[Dynamic] {LeftPath} {Operator.Description} {RightPath}";
return $"[Static] {LeftPath} {Operator.Description} {RightStaticValue}";
}
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
ConditionOperatorStore.ConditionOperatorAdded -= ConditionOperatorStoreOnConditionOperatorAdded;
ConditionOperatorStore.ConditionOperatorRemoved -= ConditionOperatorStoreOnConditionOperatorRemoved;
LeftPath?.Dispose();
RightPath?.Dispose();
base.Dispose(disposing);
}
/// <inheritdoc />
internal override bool EvaluateObject(object target)
{
return false;
}
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 = JsonConvert.SerializeObject(RightStaticValue);
if (Operator != null)
{
Entity.OperatorPluginGuid = Operator.PluginInfo.Guid;
Entity.OperatorType = Operator.GetType().Name;
}
}
internal void Initialize()
{
ConditionOperatorStore.ConditionOperatorAdded += ConditionOperatorStoreOnConditionOperatorAdded;
ConditionOperatorStore.ConditionOperatorRemoved += ConditionOperatorStoreOnConditionOperatorRemoved;
// Left side
if (Entity.LeftPath != null)
LeftPath = new DataModelPath(null, Entity.LeftPath);
// 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 && Entity.RightPath != null)
RightPath = new DataModelPath(null, Entity.RightPath);
// Right side static
else if (PredicateType == ProfileRightSideType.Static && Entity.RightStaticValue != null)
try
{
if (LeftPath != null && LeftPath.IsValid)
{
// Use the left side type so JSON.NET has a better idea what to do
Type leftSideType = LeftPath.GetPropertyType()!;
object? rightSideValue;
try
{
rightSideValue = JsonConvert.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);
}
else
{
// Hope for the best...
UpdateRightSideStatic(JsonConvert.DeserializeObject(Entity.RightStaticValue));
}
}
catch (JsonReaderException)
{
// ignored
// TODO: Some logging would be nice
}
}
internal override DataModelConditionPartEntity GetEntity()
{
return Entity;
}
/// <summary>
/// Determines the best type to use for the right side op this predicate
/// </summary>
public abstract Type? GetPreferredRightSideType();
private void ValidateOperator()
{
@ -327,6 +283,76 @@ namespace Artemis.Core
}
}
#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 = JsonConvert.SerializeObject(RightStaticValue);
if (Operator != null)
{
Entity.OperatorPluginGuid = Operator.PluginInfo.Guid;
Entity.OperatorType = Operator.GetType().Name;
}
}
#endregion
#region Event handlers
private void ConditionOperatorStoreOnConditionOperatorAdded(object? sender, ConditionOperatorStoreEvent e)

View File

@ -0,0 +1,241 @@
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? _event;
private bool _eventTriggered;
/// <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; }
public Type? EventArgumentType { get; set; }
internal DataModelConditionEventEntity Entity { get; set; }
/// <inheritdoc />
public override bool Evaluate()
{
if (_disposed)
throw new ObjectDisposedException("DataModelConditionEvent");
// Ensure the event has not been replaced
if (EventPath?.GetValue() is IDataModelEvent dataModelEvent && _event != dataModelEvent)
SubscribeToDataModelEvent(dataModelEvent);
// Only evaluate to true once every time the event has been triggered
if (!_eventTriggered)
return false;
_eventTriggered = false;
// If there is a child (root group), it must evaluate to true whenever the event triggered
if (Children.Any())
return Children[0].EvaluateObject(_event.LastEventArgumentsUntyped);
// If there are no children, we always evaluate to true whenever the event triggered
return true;
}
private void SubscribeToDataModelEvent(IDataModelEvent dataModelEvent)
{
if (_event != null)
_event.EventTriggered -= OnEventTriggered;
_event = dataModelEvent;
if (_event != null)
_event.EventTriggered += OnEventTriggered;
}
/// <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();
// 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;
}
}
private Type? GetEventArgumentType()
{
if (EventPath == null || !EventPath.IsValid)
return null;
// 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);
}
#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;
// Ensure the list path is valid and points to a list
DataModelPath eventPath = new DataModelPath(null, Entity.EventPath);
// Can't check this on an invalid list, if it becomes valid later lets hope for the best
if (eventPath.IsValid && !PointsToEvent(eventPath))
return;
EventPath = eventPath;
SubscribeToEventPath();
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));
}
}
private bool PointsToEvent(DataModelPath dataModelPath)
{
Type? type = dataModelPath.GetPropertyType();
if (type == null)
return false;
return typeof(IDataModelEvent).IsAssignableFrom(type);
}
private void SubscribeToEventPath()
{
if (EventPath == null) return;
EventPath.PathValidated += EventPathOnPathValidated;
EventPath.PathInvalidated += EventPathOnPathInvalidated;
}
#region Event handlers
private void OnEventTriggered(object? sender, EventArgs e)
{
_eventTriggered = true;
}
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
}
}

View File

@ -0,0 +1,161 @@
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
protected override void InitializeLeftPath()
{
if (Entity.LeftPath != null)
LeftPath = DataModelConditionEvent.EventArgumentType != null
? new DataModelPath(EventPredicateWrapperDataModel.Create(DataModelConditionEvent.EventArgumentType), Entity.LeftPath)
: null;
}
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
}
}

View File

@ -0,0 +1,63 @@
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
protected override void InitializeLeftPath()
{
if (Entity.LeftPath != null)
LeftPath = new DataModelPath(null, Entity.LeftPath);
}
protected override void InitializeRightPath()
{
if (PredicateType == ProfileRightSideType.Dynamic && Entity.RightPath != null)
RightPath = new DataModelPath(null, Entity.RightPath);
}
#endregion
}
}

View File

@ -1,4 +1,6 @@
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;
@ -21,6 +23,8 @@ namespace Artemis.Core
{
Parent = parent;
Entity = new DataModelConditionGroupEntity();
ChildAdded += OnChildrenChanged;
ChildRemoved += OnChildrenChanged;
}
/// <summary>
@ -40,11 +44,19 @@ namespace Artemis.Core
AddChild(new DataModelConditionGroup(this, groupEntity));
else if (childEntity is DataModelConditionListEntity listEntity)
AddChild(new DataModelConditionList(this, listEntity));
else if (childEntity is DataModelConditionPredicateEntity predicateEntity)
AddChild(new DataModelConditionPredicate(this, predicateEntity));
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>
@ -52,6 +64,11 @@ namespace Artemis.Core
/// </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 />
@ -67,16 +84,26 @@ namespace Artemis.Core
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)
{
switch (BooleanOperator)
{
case BooleanOperator.And:
return Children.All(c => c.Evaluate());
return targets.All(c => c.Evaluate());
case BooleanOperator.Or:
return Children.Any(c => c.Evaluate());
return targets.Any(c => c.Evaluate());
case BooleanOperator.AndNot:
return Children.All(c => !c.Evaluate());
return targets.All(c => !c.Evaluate());
case BooleanOperator.OrNot:
return Children.Any(c => !c.Evaluate());
return targets.Any(c => !c.Evaluate());
default:
throw new ArgumentOutOfRangeException();
}
@ -133,6 +160,11 @@ namespace Artemis.Core
{
return Entity;
}
private void OnChildrenChanged(object? sender, EventArgs e)
{
ContainsEvents = Children.Any(c => c is DataModelConditionEvent);
}
}
/// <summary>

View File

@ -180,7 +180,7 @@ namespace Artemis.Core
DataModelPath listPath = new DataModelPath(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 && !listPath.PointsToList)
if (listPath.IsValid && !PointsToList(listPath))
return;
ListPath = listPath;
@ -208,6 +208,12 @@ namespace Artemis.Core
}
}
private bool PointsToList(DataModelPath dataModelPath)
{
Type? type = dataModelPath.GetPropertyType();
return type?.IsGenericEnumerable() ?? false;
}
private void SubscribeToListPath()
{
if (ListPath == null) return;

View File

@ -1,16 +1,13 @@
using System;
using System.Reflection;
using Artemis.Core.DataModelExpansions;
using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Entities.Profile.Conditions;
using Newtonsoft.Json;
namespace Artemis.Core
{
/// <summary>
/// A predicate like evaluated inside a <see cref="DataModelConditionList" />
/// </summary>
public class DataModelConditionListPredicate : DataModelConditionPart
public class DataModelConditionListPredicate : DataModelConditionPredicate
{
/// <summary>
/// Creates a new instance of the <see cref="DataModelConditionListPredicate" /> class
@ -18,138 +15,104 @@ namespace Artemis.Core
/// <param name="parent"></param>
/// <param name="predicateType"></param>
public DataModelConditionListPredicate(DataModelConditionPart parent, ProfileRightSideType predicateType)
: base(parent, predicateType, new DataModelConditionListPredicateEntity())
{
Parent = parent;
PredicateType = predicateType;
Entity = new DataModelConditionListPredicateEntity();
DataModelConditionList = null!;
ApplyParentList();
Initialize();
}
internal DataModelConditionListPredicate(DataModelConditionPart parent, DataModelConditionListPredicateEntity entity)
: base(parent, entity)
{
Parent = parent;
Entity = entity;
PredicateType = (ProfileRightSideType) entity.PredicateType;
DataModelConditionList = null!;
ApplyParentList();
Initialize();
}
/// <summary>
/// Gets or sets the predicate type
/// </summary>
public ProfileRightSideType PredicateType { get; set; }
/// <summary>
/// Gets the operator
/// </summary>
public BaseConditionOperator? Operator { get; private set; }
/// <summary>
/// Gets the data model condition list this predicate belongs to
/// </summary>
public DataModelConditionList DataModelConditionList { get; private set; }
/// <summary>
/// Gets the path of the left property
/// </summary>
public DataModelPath? LeftPath { get; private set; }
/// <summary>
/// Gets the path of the right property
/// </summary>
public DataModelPath? RightPath { get; private set; }
/// <summary>
/// Gets the right static value, only used it <see cref="PredicateType" /> is
/// <see cref="ProfileRightSideType.Static" />
/// </summary>
public object? RightStaticValue { get; private set; }
internal DataModelConditionListPredicateEntity Entity { get; set; }
/// <summary>
/// Updates the left side of the predicate
/// </summary>
/// <param name="path">The path pointing to the left side value inside the list</param>
public void UpdateLeftSide(DataModelPath? path)
private void ApplyParentList()
{
if (DataModelConditionList.IsPrimitiveList)
throw new ArtemisCoreException("Cannot apply a left side to a predicate inside a primitive list");
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 using a path and makes the predicate dynamic
/// </summary>
/// <param name="path">The path pointing to the right side value</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;
SetStaticValue(staticValue);
}
/// <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)
DataModelConditionPart? current = Parent;
while (current != null)
{
Operator = null;
return;
if (current is DataModelConditionList parentList)
{
DataModelConditionList = parentList;
return;
}
current = current.Parent;
}
// No need to clear compiled expressions, without a left data model they are already null
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();
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
protected override void InitializeLeftPath()
{
if (Entity.LeftPath != null)
LeftPath = DataModelConditionList.ListType != null
? new DataModelPath(ListPredicateWrapperDataModel.Create(DataModelConditionList.ListType), Entity.LeftPath)
: null;
}
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), 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>
@ -158,45 +121,6 @@ namespace Artemis.Core
return false;
}
/// <summary>
/// Determines whether the provided path is contained inside the list
/// </summary>
/// <param name="path">The path to evaluate</param>
public bool ListContainsInnerPath(string path)
{
if (DataModelConditionList.ListType == null)
return false;
string[] parts = path.Split('.');
Type current = DataModelConditionList.ListType;
foreach (string part in parts)
{
PropertyInfo? property = current.GetProperty(part);
current = property?.PropertyType;
if (property == null)
return false;
}
return true;
}
#region IDisposable
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
ConditionOperatorStore.ConditionOperatorAdded -= ConditionOperatorStoreOnConditionOperatorAdded;
ConditionOperatorStore.ConditionOperatorRemoved -= ConditionOperatorStoreOnConditionOperatorRemoved;
LeftPath?.Dispose();
RightPath?.Dispose();
base.Dispose(disposing);
}
#endregion
internal override bool EvaluateObject(object target)
{
if (Operator == null || LeftPath == null || !LeftPath.IsValid)
@ -227,226 +151,6 @@ namespace Artemis.Core
return false;
}
internal Type GetTypeAtInnerPath(string path)
{
if (!ListContainsInnerPath(path))
return null;
string[] parts = path.Split('.');
Type current = DataModelConditionList.ListType;
Type result = null;
foreach (string part in parts)
{
PropertyInfo? property = current.GetProperty(part);
current = property.PropertyType;
result = property.PropertyType;
}
return result;
}
internal override void Save()
{
Entity.PredicateType = (int) PredicateType;
LeftPath?.Save();
Entity.LeftPath = LeftPath?.Entity;
RightPath?.Save();
Entity.RightPath = RightPath?.Entity;
Entity.RightStaticValue = JsonConvert.SerializeObject(RightStaticValue);
if (Operator != null)
{
Entity.OperatorPluginGuid = Operator.PluginInfo.Guid;
Entity.OperatorType = Operator.GetType().Name;
}
}
internal override DataModelConditionPartEntity GetEntity()
{
return Entity;
}
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 void Initialize()
{
ConditionOperatorStore.ConditionOperatorAdded += ConditionOperatorStoreOnConditionOperatorAdded;
ConditionOperatorStore.ConditionOperatorRemoved += ConditionOperatorStoreOnConditionOperatorRemoved;
// Left side
if (Entity.LeftPath != null)
{
LeftPath = DataModelConditionList.ListType != null
? new DataModelPath(ListPredicateWrapperDataModel.Create(DataModelConditionList.ListType), Entity.LeftPath)
: null;
}
// 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 && Entity.RightPath != null)
{
// Right side dynamic inside the list
// TODO: Come up with a general wrapper solution because this will clash with events
if (Entity.RightPath.DataModelGuid == Constants.CorePluginInfo.Guid)
{
RightPath = DataModelConditionList.ListType != null
? new DataModelPath(ListPredicateWrapperDataModel.Create(DataModelConditionList.ListType), Entity.RightPath)
: null;
}
// Right side dynamic
else
RightPath = new DataModelPath(null, Entity.RightPath);
}
// Right side static
else if (PredicateType == ProfileRightSideType.Static && Entity.RightStaticValue != null)
try
{
if (LeftPath != null && LeftPath.IsValid)
{
// Use the left side type so JSON.NET has a better idea what to do
Type leftSideType = LeftPath.GetPropertyType()!;
object? rightSideValue;
try
{
rightSideValue = JsonConvert.DeserializeObject(Entity.RightStaticValue, leftSideType);
}
// If deserialization fails, use the type's default
catch (JsonSerializationException e)
{
DeserializationLogger.LogListPredicateDeserializationFailure(this, e);
rightSideValue = Activator.CreateInstance(leftSideType);
}
UpdateRightSideStatic(rightSideValue);
}
else
{
// Hope for the best... we must infer the type from JSON now
UpdateRightSideStatic(JsonConvert.DeserializeObject(Entity.RightStaticValue));
}
}
catch (JsonException e)
{
DeserializationLogger.LogListPredicateDeserializationFailure(this, e);
}
}
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);
}
}
private void SetStaticValue(object? staticValue)
{
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
if (staticValue != null && staticValue.GetType() == Operator.RightSideType)
RightStaticValue = staticValue;
else if (staticValue != null)
RightStaticValue = Convert.ChangeType(staticValue, Operator.RightSideType);
// If null create a default instance for value types or simply make it null for reference types
else if (Operator.RightSideType.IsValueType)
RightStaticValue = Activator.CreateInstance(Operator.RightSideType);
else
RightStaticValue = null;
}
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 Event handlers
private void ConditionOperatorStoreOnConditionOperatorAdded(object sender, ConditionOperatorStoreEvent e)
{
BaseConditionOperator conditionOperator = e.Registration.ConditionOperator;
if (Entity.OperatorPluginGuid == conditionOperator.PluginInfo.Guid && Entity.OperatorType == conditionOperator.GetType().Name)
UpdateOperator(conditionOperator);
}
private void ConditionOperatorStoreOnConditionOperatorRemoved(object sender, ConditionOperatorStoreEvent e)
{
if (e.Registration.ConditionOperator != Operator)
return;
Operator = null;
}
#endregion
}
}

View File

@ -0,0 +1,37 @@
using System;
using Artemis.Core.DataModelExpansions;
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()
{
PluginInfo = Constants.CorePluginInfo;
}
[DataModelIgnore]
public object? UntypedArguments { get; 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;
}
}
}

View File

@ -9,6 +9,9 @@ namespace Artemis.Core
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()
@ -19,6 +22,9 @@ namespace Artemis.Core
[DataModelIgnore]
public object? UntypedValue { get; set; }
/// <summary>
/// Creates a new instance of the <see cref="ListPredicateWrapperDataModel"/> class
/// </summary>
public static ListPredicateWrapperDataModel Create(Type type)
{
object? instance = Activator.CreateInstance(typeof(ListPredicateWrapperDataModel<>).MakeGenericType(type));

View File

@ -137,16 +137,26 @@ namespace Artemis.Core
/// <summary>
/// Updates the smoothing progress of the data binding
/// </summary>
/// <param name="deltaTime">The time in seconds that passed since the last update</param>
public void Update(double deltaTime)
/// <param name="timeline">The timeline to apply during update</param>
public void Update(Timeline timeline)
{
UpdateWithDelta(timeline.Delta);
}
/// <summary>
/// Updates the smoothing progress of the data binding
/// </summary>
/// <param name="delta">The delta to apply during update</param>
public void UpdateWithDelta(TimeSpan delta)
{
if (_disposed)
throw new ObjectDisposedException("DataBinding");
// Data bindings cannot go back in time like brushes
deltaTime = Math.Max(0, deltaTime);
if (delta < TimeSpan.Zero)
delta = TimeSpan.Zero;
_easingProgress = _easingProgress.Add(TimeSpan.FromSeconds(deltaTime));
_easingProgress = _easingProgress.Add(delta);
if (_easingProgress > EasingTime)
_easingProgress = EasingTime;
}

View File

@ -200,7 +200,7 @@ namespace Artemis.Core
ParameterPath = new DataModelPath(null, Entity.ParameterPath);
}
// Static parameter
else if (ParameterType == ProfileRightSideType.Static && Entity.ParameterStaticValue != null && ParameterStaticValue == null)
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();

View File

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

View File

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

View File

@ -1,5 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
@ -111,18 +110,6 @@ namespace Artemis.Core
/// </summary>
public IReadOnlyCollection<DataModelPathSegment> Segments => _segments.ToList().AsReadOnly();
/// <summary>
/// Gets a boolean indicating whether this data model path points to a list
/// </summary>
public bool PointsToList
{
get
{
Type? type = GetPropertyType();
return type?.IsGenericEnumerable() ?? false;
}
}
internal DataModelPathEntity Entity { get; }
internal Func<object, object>? Accessor { get; private set; }
@ -293,6 +280,13 @@ namespace Artemis.Core
Entity.Path = Path;
Entity.DataModelGuid = DataModelGuid;
Entity.WrapperType = Target switch
{
ListPredicateWrapperDataModel _ => PathWrapperType.List,
EventPredicateWrapperDataModel _ => PathWrapperType.Event,
_ => PathWrapperType.None
};
}
#endregion

View File

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core
{
internal interface IDataModelEvent
{
/// <summary>
/// Gets the last time the event was triggered
/// </summary>
DateTime LastTrigger { get; }
/// <summary>
/// Gets the amount of times the event was triggered
/// </summary>
int TriggerCount { get; }
/// <summary>
/// Gets the type of arguments this event contains
/// </summary>
Type ArgumentsType { get; }
/// <summary>
/// Gets or sets a boolean indicating whether the last 20 events should be tracked
/// <para>Note: setting this to <see langword="false" /> will clear the current history</para>
/// </summary>
bool TrackHistory { get; set; }
/// <summary>
/// Gets the event arguments of the last time the event was triggered by its base type
/// </summary>
public DataModelEventArgs? LastEventArgumentsUntyped { get; }
/// <summary>
/// Gets a list of the last 20 event arguments by their base type.
/// <para>Always empty if <see cref="TrackHistory" /> is <see langword="false" /></para>
/// </summary>
public List<DataModelEventArgs> EventArgumentsHistoryUntyped { get; }
/// <summary>
/// Fires when the event is triggered
/// </summary>
event EventHandler EventTriggered;
/// <summary>
/// Resets the trigger count and history of this data model event
/// </summary>
void Reset();
}
}

View File

@ -30,19 +30,16 @@ namespace Artemis.Core
Profile = Parent.Profile;
Name = name;
Enabled = true;
DisplayContinuously = true;
_layerEffects = new List<BaseLayerEffect>();
_expandedPropertyGroups = new List<string>();
ApplyRenderElementDefaults();
Parent.AddChild(this);
}
internal Folder(Profile profile, ProfileElement parent, FolderEntity folderEntity)
{
FolderEntity = folderEntity;
EntityId = folderEntity.Id;
Profile = profile;
@ -53,125 +50,195 @@ namespace Artemis.Core
_layerEffects = new List<BaseLayerEffect>();
_expandedPropertyGroups = new List<string>();
Load();
}
/// <summary>
/// Gets a boolean indicating whether this folder is at the root of the profile tree
/// </summary>
public bool IsRootFolder => Parent == Profile;
/// <summary>
/// Gets the longest timeline of all this folders children
/// </summary>
public Timeline LongestChildTimeline { get; private set; }
internal FolderEntity FolderEntity { get; set; }
internal override RenderElementEntity RenderElementEntity => FolderEntity;
/// <inheritdoc />
public override List<ILayerProperty> GetAllLayerProperties()
{
List<ILayerProperty> result = new List<ILayerProperty>();
foreach (BaseLayerEffect layerEffect in LayerEffects)
{
if (layerEffect.BaseProperties != null)
result.AddRange(layerEffect.BaseProperties.GetAllLayerProperties());
}
return result;
}
internal override RenderElementEntity RenderElementEntity => FolderEntity;
public bool IsRootFolder => Parent == Profile;
public override void Update(double deltaTime)
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Folder");
if (!Enabled)
return;
// Disable data bindings during an override
bool wasApplyingDataBindings = ApplyDataBindingsEnabled;
ApplyDataBindingsEnabled = false;
UpdateDisplayCondition();
// Update the layer timeline, this will give us a new delta time which could be negative in case the main segment wrapped back
// to it's start
UpdateTimeline(deltaTime);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled))
{
baseLayerEffect.BaseProperties?.Update(deltaTime);
baseLayerEffect.Update(deltaTime);
}
// Iterate the children in reverse because that's how they must be rendered too
for (int index = Children.Count - 1; index > -1; index--)
{
ProfileElement profileElement = Children[index];
profileElement.Update(deltaTime);
}
// Restore the old data bindings enabled state
ApplyDataBindingsEnabled = wasApplyingDataBindings;
foreach (ProfileElement child in Children)
child.Update(deltaTime);
}
protected internal override void UpdateTimelineLength()
/// <inheritdoc />
public override void Reset()
{
TimelineLength = !Children.Any() ? TimeSpan.Zero : Children.OfType<RenderProfileElement>().Max(c => c.TimelineLength);
if (StartSegmentLength + MainSegmentLength + EndSegmentLength > TimelineLength)
TimelineLength = StartSegmentLength + MainSegmentLength + EndSegmentLength;
DisplayConditionMet = false;
Timeline.JumpToStart();
if (Parent is RenderProfileElement parent)
parent.UpdateTimelineLength();
foreach (ProfileElement child in Children)
child.Reset();
}
public override void OverrideProgress(TimeSpan timeOverride, bool stickToMainSegment)
/// <inheritdoc />
public override void AddChild(ProfileElement child, int? order = null)
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Folder");
if (!Enabled)
base.AddChild(child, order);
CalculateRenderProperties();
}
/// <inheritdoc />
public override void RemoveChild(ProfileElement child)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
base.RemoveChild(child);
CalculateRenderProperties();
}
public override string ToString()
{
return $"[Folder] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}";
}
public void CalculateRenderProperties()
{
if (Disposed)
throw new ObjectDisposedException("Folder");
SKPath path = new SKPath {FillType = SKPathFillType.Winding};
foreach (ProfileElement child in Children)
if (child is RenderProfileElement effectChild && effectChild.Path != null)
path.AddPath(effectChild.Path);
Path = path;
// Folder render properties are based on child paths and thus require an update
if (Parent is Folder folder)
folder.CalculateRenderProperties();
OnRenderPropertiesUpdated();
}
protected override void Dispose(bool disposing)
{
Disposed = true;
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
_folderBitmap?.Dispose();
base.Dispose(disposing);
}
internal override void Load()
{
_expandedPropertyGroups.AddRange(FolderEntity.ExpandedPropertyGroups);
// Load child folders
foreach (FolderEntity childFolder in Profile.ProfileEntity.Folders.Where(f => f.ParentId == EntityId))
ChildrenList.Add(new Folder(Profile, this, childFolder));
// Load child layers
foreach (LayerEntity childLayer in Profile.ProfileEntity.Layers.Where(f => f.ParentId == EntityId))
ChildrenList.Add(new Layer(Profile, this, childLayer));
// Ensure order integrity, should be unnecessary but no one is perfect specially me
ChildrenList = ChildrenList.OrderBy(c => c.Order).ToList();
for (int index = 0; index < ChildrenList.Count; index++)
ChildrenList[index].Order = index + 1;
LoadRenderElement();
}
internal override void Save()
{
if (Disposed)
throw new ObjectDisposedException("Folder");
FolderEntity.Id = EntityId;
FolderEntity.ParentId = Parent?.EntityId ?? new Guid();
FolderEntity.Order = Order;
FolderEntity.Name = Name;
FolderEntity.Enabled = Enabled;
FolderEntity.ProfileId = Profile.EntityId;
FolderEntity.ExpandedPropertyGroups.Clear();
FolderEntity.ExpandedPropertyGroups.AddRange(_expandedPropertyGroups);
SaveRenderElement();
}
#region Rendering
public override void Render(SKCanvas canvas, SKImageInfo canvasInfo)
{
if (Disposed)
throw new ObjectDisposedException("Folder");
if (!Enabled || !Children.Any(c => c.Enabled))
return;
TimeSpan beginTime = TimelinePosition;
// Ensure the folder is ready
if (Path == null)
return;
if (stickToMainSegment)
// No point rendering if none of the children are going to render
if (!Children.Any(c => c is RenderProfileElement renderElement && !renderElement.Timeline.IsFinished))
return;
lock (Timeline)
{
if (!DisplayContinuously)
{
TimeSpan position = timeOverride + StartSegmentLength;
if (position > StartSegmentLength + EndSegmentLength)
TimelinePosition = StartSegmentLength + EndSegmentLength;
}
else
{
double progress = timeOverride.TotalMilliseconds % MainSegmentLength.TotalMilliseconds;
if (progress > 0)
TimelinePosition = TimeSpan.FromMilliseconds(progress) + StartSegmentLength;
else
TimelinePosition = StartSegmentLength;
}
}
else
TimelinePosition = timeOverride;
double delta = (TimelinePosition - beginTime).TotalSeconds;
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled))
{
baseLayerEffect.BaseProperties?.Update(delta);
baseLayerEffect.Update(delta);
RenderFolder(Timeline, canvas, canvasInfo);
Timeline.ClearDelta();
}
}
public override void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
private void PrepareForRender(Timeline timeline)
{
if (_disposed)
throw new ObjectDisposedException("Folder");
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled))
{
baseLayerEffect.BaseProperties?.Update(timeline);
baseLayerEffect.Update(timeline.Delta.TotalSeconds);
}
}
if (Path == null || !Enabled || !Children.Any(c => c.Enabled))
return;
// No need to render if at the end of the timeline
if (TimelinePosition > TimelineLength)
return;
private void RenderFolder(Timeline timeline, SKCanvas canvas, SKImageInfo canvasInfo)
{
PrepareForRender(timeline);
if (_folderBitmap == null)
{
_folderBitmap = new SKBitmap(new SKImageInfo((int) Path.Bounds.Width, (int) Path.Bounds.Height));
}
else if (_folderBitmap.Info.Width != (int) Path.Bounds.Width || _folderBitmap.Info.Height != (int) Path.Bounds.Height)
{
_folderBitmap.Dispose();
@ -207,7 +274,7 @@ namespace Artemis.Core
{
folderCanvas.Save();
ProfileElement profileElement = Children[index];
profileElement.Render(deltaTime, folderCanvas, _folderBitmap.Info);
profileElement.Render(folderCanvas, _folderBitmap.Info);
folderCanvas.Restore();
}
@ -225,102 +292,7 @@ namespace Artemis.Core
canvas.Restore();
}
/// <inheritdoc />
public override void AddChild(ProfileElement child, int? order = null)
{
if (_disposed)
throw new ObjectDisposedException("Folder");
base.AddChild(child, order);
UpdateTimelineLength();
CalculateRenderProperties();
}
/// <inheritdoc />
public override void RemoveChild(ProfileElement child)
{
if (_disposed)
throw new ObjectDisposedException("Folder");
base.RemoveChild(child);
UpdateTimelineLength();
CalculateRenderProperties();
}
public override string ToString()
{
return $"[Folder] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}";
}
public void CalculateRenderProperties()
{
if (_disposed)
throw new ObjectDisposedException("Folder");
SKPath path = new SKPath {FillType = SKPathFillType.Winding};
foreach (ProfileElement child in Children)
{
if (child is RenderProfileElement effectChild && effectChild.Path != null)
path.AddPath(effectChild.Path);
}
Path = path;
// Folder render properties are based on child paths and thus require an update
if (Parent is Folder folder)
folder.CalculateRenderProperties();
OnRenderPropertiesUpdated();
}
protected override void Dispose(bool disposing)
{
_disposed = true;
foreach (ProfileElement profileElement in Children)
profileElement.Dispose();
_folderBitmap?.Dispose();
base.Dispose(disposing);
}
internal override void Load()
{
_expandedPropertyGroups.AddRange(FolderEntity.ExpandedPropertyGroups);
// Load child folders
foreach (FolderEntity childFolder in Profile.ProfileEntity.Folders.Where(f => f.ParentId == EntityId))
ChildrenList.Add(new Folder(Profile, this, childFolder));
// Load child layers
foreach (LayerEntity childLayer in Profile.ProfileEntity.Layers.Where(f => f.ParentId == EntityId))
ChildrenList.Add(new Layer(Profile, this, childLayer));
// Ensure order integrity, should be unnecessary but no one is perfect specially me
ChildrenList = ChildrenList.OrderBy(c => c.Order).ToList();
for (int index = 0; index < ChildrenList.Count; index++)
ChildrenList[index].Order = index + 1;
LoadRenderElement();
}
internal override void Save()
{
if (_disposed)
throw new ObjectDisposedException("Folder");
FolderEntity.Id = EntityId;
FolderEntity.ParentId = Parent?.EntityId ?? new Guid();
FolderEntity.Order = Order;
FolderEntity.Name = Name;
FolderEntity.Enabled = Enabled;
FolderEntity.ProfileId = Profile.EntityId;
FolderEntity.ExpandedPropertyGroups.Clear();
FolderEntity.ExpandedPropertyGroups.AddRange(_expandedPropertyGroups);
SaveRenderElement();
}
#endregion
#region Events

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
@ -38,7 +37,6 @@ namespace Artemis.Core
Profile = Parent.Profile;
Name = name;
Enabled = true;
DisplayContinuously = true;
General = new LayerGeneralProperties();
Transform = new LayerTransformProperties();
@ -47,14 +45,14 @@ namespace Artemis.Core
_expandedPropertyGroups = new List<string>();
Initialize();
ApplyRenderElementDefaults();
Parent.AddChild(this);
}
internal Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity)
{
LayerEntity = layerEntity;
EntityId = layerEntity.Id;
Profile = profile;
Parent = parent;
General = new LayerGeneralProperties();
@ -68,27 +66,6 @@ namespace Artemis.Core
Initialize();
}
internal LayerEntity LayerEntity { get; set; }
/// <inheritdoc />
public override List<ILayerProperty> GetAllLayerProperties()
{
List<ILayerProperty> result = new List<ILayerProperty>();
result.AddRange(General.GetAllLayerProperties());
result.AddRange(Transform.GetAllLayerProperties());
if (LayerBrush?.BaseProperties != null)
result.AddRange(LayerBrush.BaseProperties.GetAllLayerProperties());
foreach (BaseLayerEffect layerEffect in LayerEffects)
{
if (layerEffect.BaseProperties != null)
result.AddRange(layerEffect.BaseProperties.GetAllLayerProperties());
}
return result;
}
internal override RenderElementEntity RenderElementEntity => LayerEntity;
/// <summary>
/// A collection of all the LEDs this layer is assigned to.
/// </summary>
@ -131,6 +108,25 @@ namespace Artemis.Core
internal set => SetAndNotify(ref _layerBrush, value);
}
internal LayerEntity LayerEntity { get; set; }
internal override RenderElementEntity RenderElementEntity => LayerEntity;
/// <inheritdoc />
public override List<ILayerProperty> GetAllLayerProperties()
{
List<ILayerProperty> result = new List<ILayerProperty>();
result.AddRange(General.GetAllLayerProperties());
result.AddRange(Transform.GetAllLayerProperties());
if (LayerBrush?.BaseProperties != null)
result.AddRange(LayerBrush.BaseProperties.GetAllLayerProperties());
foreach (BaseLayerEffect layerEffect in LayerEffects)
if (layerEffect.BaseProperties != null)
result.AddRange(layerEffect.BaseProperties.GetAllLayerProperties());
return result;
}
/// <inheritdoc />
public override string ToString()
{
@ -142,7 +138,7 @@ namespace Artemis.Core
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
_disposed = true;
Disposed = true;
// Brush first in case it depends on any of the other disposables during it's own disposal
_layerBrush?.Dispose();
@ -195,7 +191,7 @@ namespace Artemis.Core
internal override void Save()
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Layer");
// Properties
@ -258,94 +254,30 @@ namespace Artemis.Core
/// <inheritdoc />
public override void Update(double deltaTime)
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Layer");
if (!Enabled || LayerBrush?.BaseProperties == null || !LayerBrush.BaseProperties.PropertiesInitialized)
if (!Enabled)
return;
// Ensure the layer must still be displayed
UpdateDisplayCondition();
// Update the layer timeline, this will give us a new delta time which could be negative in case the main segment wrapped back
// to it's start
UpdateTimeline(deltaTime);
// No point updating further than this if the layer is not going to be rendered
if (TimelinePosition > TimelineLength)
return;
General.Update(deltaTime);
Transform.Update(deltaTime);
LayerBrush.BaseProperties?.Update(deltaTime);
LayerBrush.Update(deltaTime);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled))
{
baseLayerEffect.BaseProperties?.Update(deltaTime);
baseLayerEffect.Update(deltaTime);
}
}
protected internal override void UpdateTimelineLength()
{
TimelineLength = StartSegmentLength + MainSegmentLength + EndSegmentLength;
}
public override void OverrideProgress(TimeSpan timeOverride, bool stickToMainSegment)
{
if (_disposed)
throw new ObjectDisposedException("Layer");
if (!Enabled || LayerBrush?.BaseProperties == null || !LayerBrush.BaseProperties.PropertiesInitialized)
return;
// Disable data bindings during an override
bool wasApplyingDataBindings = ApplyDataBindingsEnabled;
ApplyDataBindingsEnabled = false;
TimeSpan beginTime = TimelinePosition;
if (stickToMainSegment)
{
if (!DisplayContinuously)
TimelinePosition = StartSegmentLength + timeOverride;
else
{
double progress = timeOverride.TotalMilliseconds % MainSegmentLength.TotalMilliseconds;
if (progress > 0)
TimelinePosition = TimeSpan.FromMilliseconds(progress) + StartSegmentLength;
else
TimelinePosition = StartSegmentLength;
}
}
else
TimelinePosition = timeOverride;
double delta = (TimelinePosition - beginTime).TotalSeconds;
General.Update(delta);
Transform.Update(delta);
LayerBrush.BaseProperties?.Update(delta);
LayerBrush.Update(delta);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled))
{
baseLayerEffect.BaseProperties?.Update(delta);
baseLayerEffect.Update(delta);
}
// Restore the old data bindings enabled state
ApplyDataBindingsEnabled = wasApplyingDataBindings;
}
/// <inheritdoc />
public override void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
public override void Reset()
{
if (_disposed)
DisplayConditionMet = false;
Timeline.JumpToStart();
}
/// <inheritdoc />
public override void Render(SKCanvas canvas, SKImageInfo canvasInfo)
{
if (Disposed)
throw new ObjectDisposedException("Layer");
if (!Enabled || TimelinePosition > TimelineLength)
if (!Enabled)
return;
// Ensure the layer is ready
@ -355,8 +287,40 @@ namespace Artemis.Core
if (LayerBrush?.BaseProperties?.PropertiesInitialized == false || LayerBrush?.BrushType != LayerBrushType.Regular)
return;
lock (Timeline)
{
RenderLayer(Timeline, canvas);
foreach (Timeline extraTimeline in Timeline.ExtraTimelines)
RenderLayer(extraTimeline, canvas);
Timeline.ClearDelta();
}
}
private void PrepareForRender(Timeline timeline)
{
General.Update(timeline);
Transform.Update(timeline);
LayerBrush.BaseProperties?.Update(timeline);
LayerBrush.Update(timeline.Delta.TotalSeconds);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled))
{
baseLayerEffect.BaseProperties?.Update(timeline);
baseLayerEffect.Update(timeline.Delta.TotalSeconds);
}
}
private void RenderLayer(Timeline timeline, SKCanvas canvas)
{
if (timeline.IsFinished)
return;
PrepareForRender(timeline);
if (_layerBitmap == null)
{
_layerBitmap = new SKBitmap(new SKImageInfo((int) Path.Bounds.Width, (int) Path.Bounds.Height));
}
else if (_layerBitmap.Info.Width != (int) Path.Bounds.Width || _layerBitmap.Info.Height != (int) Path.Bounds.Height)
{
_layerBitmap.Dispose();
@ -365,11 +329,7 @@ namespace Artemis.Core
using SKPath layerPath = new SKPath(Path);
using SKCanvas layerCanvas = new SKCanvas(_layerBitmap);
using SKPaint layerPaint = new SKPaint
{
FilterQuality = SKFilterQuality.Low,
Color = new SKColor(0, 0, 0, (byte) (Transform.Opacity.CurrentValue * 2.55f))
};
using SKPaint layerPaint = new SKPaint {FilterQuality = SKFilterQuality.Low};
layerCanvas.Clear();
layerPath.Transform(SKMatrix.MakeTranslation(layerPath.Bounds.Left * -1, layerPath.Bounds.Top * -1));
@ -388,7 +348,11 @@ namespace Artemis.Core
else if (General.ResizeMode.CurrentValue == LayerResizeMode.Clip)
ClipRender(layerCanvas, _layerBitmap.Info, layerPaint, layerPath);
using SKPaint canvasPaint = new SKPaint { BlendMode = General.BlendMode.CurrentValue };
using SKPaint canvasPaint = new SKPaint
{
BlendMode = General.BlendMode.CurrentValue,
Color = new SKColor(0, 0, 0, (byte) (Transform.Opacity.CurrentValue * 2.55f))
};
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.Enabled))
baseLayerEffect.PostProcess(layerCanvas, _layerBitmap.Info, layerPath, canvasPaint);
@ -401,7 +365,7 @@ namespace Artemis.Core
(canvasPath.Bounds.Left - targetLocation.X) * -1,
(canvasPath.Bounds.Top - targetLocation.Y) * -1)
);
canvas.ClipPath(canvasPath);
// canvas.ClipPath(canvasPath);
canvas.DrawBitmap(_layerBitmap, targetLocation, canvasPaint);
}
@ -471,11 +435,13 @@ namespace Artemis.Core
internal void CalculateRenderProperties()
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Layer");
if (!Leds.Any())
{
Path = new SKPath();
}
else
{
SKPath path = new SKPath {FillType = SKPathFillType.Winding};
@ -498,7 +464,7 @@ namespace Artemis.Core
internal SKPoint GetLayerAnchorPosition(SKPath layerPath, bool zeroBased = false)
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Layer");
SKPoint positionProperty = Transform.Position.CurrentValue;
@ -522,7 +488,7 @@ namespace Artemis.Core
/// <param name="path"></param>
public void IncludePathInTranslation(SKPath path, bool zeroBased)
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Layer");
SKSize sizeProperty = Transform.Scale.CurrentValue;
@ -548,13 +514,51 @@ namespace Artemis.Core
}
}
/// <summary>
/// Creates a transformation matrix that applies the current transformation settings
/// </summary>
/// <param name="zeroBased">
/// If true, treats the layer as if it is located at 0,0 instead of its actual position on the
/// surface
/// </param>
/// <returns>The transformation matrix containing the current transformation settings</returns>
public SKMatrix GetTransformMatrix(bool zeroBased)
{
if (Disposed)
throw new ObjectDisposedException("Layer");
SKSize sizeProperty = Transform.Scale.CurrentValue;
float rotationProperty = Transform.Rotation.CurrentValue;
SKPoint anchorPosition = GetLayerAnchorPosition(Path, zeroBased);
SKPoint anchorProperty = Transform.AnchorPoint.CurrentValue;
// Translation originates from the unscaled center of the shape and is tied to the anchor
float x = anchorPosition.X - (zeroBased ? Bounds.MidX - Bounds.Left : Bounds.MidX) - anchorProperty.X * Bounds.Width;
float y = anchorPosition.Y - (zeroBased ? Bounds.MidY - Bounds.Top : Bounds.MidY) - anchorProperty.Y * Bounds.Height;
if (General.ResizeMode == LayerResizeMode.Normal)
{
SKMatrix transform = SKMatrix.MakeTranslation(x, y);
transform = transform.PostConcat(SKMatrix.MakeRotationDegrees(rotationProperty, anchorPosition.X, anchorPosition.Y));
transform = transform.PostConcat(SKMatrix.MakeScale(sizeProperty.Width / 100f, sizeProperty.Height / 100f, anchorPosition.X, anchorPosition.Y));
return transform;
}
else
{
SKMatrix transform = SKMatrix.MakeTranslation(x, y);
transform = transform.PostConcat(SKMatrix.MakeRotationDegrees(rotationProperty * -1, anchorPosition.X, anchorPosition.Y));
return transform;
}
}
/// <summary>
/// Excludes the provided path from the translations applied to the layer by applying translations that cancel the
/// layer translations out
/// </summary>
public void ExcludePathFromTranslation(SKPath path, bool zeroBased)
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Layer");
SKSize sizeProperty = Transform.Scale.CurrentValue;
@ -590,7 +594,7 @@ namespace Artemis.Core
/// <returns>The number of transformations applied</returns>
public int ExcludeCanvasFromTranslation(SKCanvas canvas, bool zeroBased)
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Layer");
SKSize sizeProperty = Transform.Scale.CurrentValue;
@ -630,7 +634,7 @@ namespace Artemis.Core
/// <param name="led">The LED to add</param>
public void AddLed(ArtemisLed led)
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Layer");
_leds.Add(led);
@ -643,7 +647,7 @@ namespace Artemis.Core
/// <param name="leds">The LEDs to add</param>
public void AddLeds(IEnumerable<ArtemisLed> leds)
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Layer");
_leds.AddRange(leds);
@ -656,7 +660,7 @@ namespace Artemis.Core
/// <param name="led">The LED to remove</param>
public void RemoveLed(ArtemisLed led)
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Layer");
_leds.Remove(led);
@ -668,7 +672,7 @@ namespace Artemis.Core
/// </summary>
public void ClearLeds()
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Layer");
_leds.Clear();
@ -677,7 +681,7 @@ namespace Artemis.Core
internal void PopulateLeds(ArtemisSurface surface)
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Layer");
List<ArtemisLed> leds = new List<ArtemisLed>();

View File

@ -11,7 +11,7 @@ namespace Artemis.Core
/// initialize these for you.
/// </para>
/// </summary>
public interface ILayerProperty : IStorageModel, IUpdateModel, IDisposable
public interface ILayerProperty : IStorageModel, IDisposable
{
/// <summary>
/// Gets the description attribute applied to this property
@ -36,5 +36,11 @@ namespace Artemis.Core
/// Returns a list off all data binding registrations
/// </summary>
List<IDataBindingRegistration> GetAllDataBindingRegistrations();
/// <summary>
/// Updates the layer properties internal state
/// </summary>
/// <param name="timeline">The timeline to apply to the property</param>
void Update(Timeline timeline);
}
}

View File

@ -34,20 +34,16 @@ namespace Artemis.Core
/// <inheritdoc />
public string Path { get; private set; }
/// <summary>
/// Updates the property, applying keyframes and data bindings to the current value
/// </summary>
public void Update(double deltaTime)
/// <inheritdoc />
public void Update(Timeline timeline)
{
if (_disposed)
throw new ObjectDisposedException("LayerProperty");
CurrentValue = BaseValue;
if (ProfileElement.ApplyKeyframesEnabled)
UpdateKeyframes();
if (ProfileElement.ApplyDataBindingsEnabled)
UpdateDataBindings(deltaTime);
UpdateKeyframes(timeline);
UpdateDataBindings(timeline);
OnUpdated();
}
@ -125,8 +121,7 @@ namespace Artemis.Core
return;
_baseValue = value;
Update(0);
OnCurrentValueSet();
ReapplyUpdate();
}
}
@ -169,8 +164,7 @@ namespace Artemis.Core
// Force an update so that the base value is applied to the current value and
// keyframes/data bindings are applied using the new base value
Update(0);
OnCurrentValueSet();
ReapplyUpdate();
}
/// <summary>
@ -185,6 +179,13 @@ namespace Artemis.Core
CurrentValue = DefaultValue;
}
private void ReapplyUpdate()
{
ProfileElement.Timeline.ClearDelta();
Update(ProfileElement.Timeline);
OnCurrentValueSet();
}
#endregion
#region Keyframes
@ -294,13 +295,13 @@ namespace Artemis.Core
_keyframes = _keyframes.OrderBy(k => k.Position).ToList();
}
private void UpdateKeyframes()
private void UpdateKeyframes(Timeline timeline)
{
if (!KeyframesSupported || !KeyframesEnabled)
return;
// The current keyframe is the last keyframe before the current time
CurrentKeyframe = _keyframes.LastOrDefault(k => k.Position <= ProfileElement.TimelinePosition);
CurrentKeyframe = _keyframes.LastOrDefault(k => k.Position <= timeline.Position);
// Keyframes are sorted by position so we can safely assume the next keyframe's position is after the current
int nextIndex = _keyframes.IndexOf(CurrentKeyframe) + 1;
NextKeyframe = _keyframes.Count > nextIndex ? _keyframes[nextIndex] : null;
@ -314,7 +315,7 @@ namespace Artemis.Core
else
{
TimeSpan timeDiff = NextKeyframe.Position - CurrentKeyframe.Position;
float keyframeProgress = (float) ((ProfileElement.TimelinePosition - CurrentKeyframe.Position).TotalMilliseconds / timeDiff.TotalMilliseconds);
float keyframeProgress = (float) ((timeline.Position - CurrentKeyframe.Position).TotalMilliseconds / timeDiff.TotalMilliseconds);
float keyframeProgressEased = (float) Easings.Interpolate(keyframeProgress, CurrentKeyframe.EasingFunction);
UpdateCurrentValue(keyframeProgress, keyframeProgressEased);
}
@ -416,11 +417,15 @@ namespace Artemis.Core
OnDataBindingDisabled(new LayerPropertyEventArgs<T>(dataBinding.LayerProperty));
}
private void UpdateDataBindings(double deltaTime)
private void UpdateDataBindings(Timeline timeline)
{
// To avoid data bindings applying at non-regular updating (during editing) only update when not overriden
if (timeline.IsOverridden)
return;
foreach (IDataBinding dataBinding in _dataBindings)
{
dataBinding.Update(deltaTime);
dataBinding.Update(timeline);
dataBinding.Apply();
}
}
@ -452,7 +457,6 @@ namespace Artemis.Core
Entity = entity ?? throw new ArgumentNullException(nameof(entity));
PropertyDescription = description ?? throw new ArgumentNullException(nameof(description));
IsLoadedFromStorage = fromStorage;
LayerPropertyGroup.PropertyGroupUpdating += (sender, args) => Update(args.DeltaTime);
}
/// <inheritdoc />
@ -485,12 +489,10 @@ namespace Artemis.Core
_keyframes.Clear();
try
{
_keyframes.AddRange(Entity.KeyframeEntities.Select(k => new LayerPropertyKeyframe<T>(
JsonConvert.DeserializeObject<T>(k.Value),
k.Position,
(Easings.Functions) k.EasingFunction,
this
)));
_keyframes.AddRange(
Entity.KeyframeEntities.Where(k => k.Position <= ProfileElement.Timeline.Length)
.Select(k => new LayerPropertyKeyframe<T>(JsonConvert.DeserializeObject<T>(k.Value), k.Position, (Easings.Functions) k.EasingFunction, this))
);
}
catch (JsonException e)
{

View File

@ -198,11 +198,12 @@ namespace Artemis.Core
layerPropertyGroup.ApplyToEntity();
}
internal void Update(double deltaTime)
internal void Update(Timeline timeline)
{
// Since at this point we don't know what properties the group has without using reflection,
// let properties subscribe to the update event and update themselves
OnPropertyGroupUpdating(new LayerPropertyGroupUpdatingEventArgs(deltaTime));
foreach (ILayerProperty layerProperty in LayerProperties)
layerProperty.Update(timeline);
foreach (LayerPropertyGroup layerPropertyGroup in LayerPropertyGroups)
layerPropertyGroup.Update(timeline);
}
private void InitializeProperty(PropertyInfo propertyInfo, PropertyDescriptionAttribute propertyDescription)
@ -266,8 +267,6 @@ namespace Artemis.Core
#region Events
internal event EventHandler<LayerPropertyGroupUpdatingEventArgs> PropertyGroupUpdating;
/// <summary>
/// Occurs when the property group has initialized all its children
/// </summary>
@ -284,11 +283,6 @@ namespace Artemis.Core
/// </summary>
public event EventHandler VisibilityChanged;
internal virtual void OnPropertyGroupUpdating(LayerPropertyGroupUpdatingEventArgs e)
{
PropertyGroupUpdating?.Invoke(this, e);
}
internal virtual void OnVisibilityChanged()
{
VisibilityChanged?.Invoke(this, EventArgs.Empty);

View File

@ -56,7 +56,7 @@ namespace Artemis.Core
{
lock (this)
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Profile");
if (!IsActivated)
throw new ArtemisCoreException($"Cannot update inactive profile: {this}");
@ -66,23 +66,30 @@ namespace Artemis.Core
}
}
public override void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
public override void Render(SKCanvas canvas, SKImageInfo canvasInfo)
{
lock (this)
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Profile");
if (!IsActivated)
throw new ArtemisCoreException($"Cannot render inactive profile: {this}");
foreach (ProfileElement profileElement in Children)
profileElement.Render(deltaTime, canvas, canvasInfo);
profileElement.Render(canvas, canvasInfo);
}
}
/// <inheritdoc />
public override void Reset()
{
foreach (ProfileElement child in Children)
child.Reset();
}
public Folder GetRootFolder()
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Profile");
return (Folder) Children.Single();
@ -90,7 +97,7 @@ namespace Artemis.Core
internal override void Load()
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Profile");
Name = ProfileEntity.Name;
@ -130,12 +137,12 @@ namespace Artemis.Core
ChildrenList.Clear();
IsActivated = false;
_disposed = true;
Disposed = true;
}
internal override void Save()
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Profile");
ProfileEntity.Id = EntityId;
@ -157,7 +164,7 @@ namespace Artemis.Core
{
lock (this)
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Profile");
if (IsActivated)
return;
@ -170,7 +177,7 @@ namespace Artemis.Core
internal void PopulateLeds(ArtemisSurface surface)
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException("Profile");
foreach (Layer layer in GetAllLayers())

View File

@ -9,13 +9,13 @@ namespace Artemis.Core
{
public abstract class ProfileElement : PropertyChangedBase, IDisposable
{
protected bool _disposed;
private bool _enabled;
private Guid _entityId;
private string _name;
private int _order;
private ProfileElement _parent;
private Profile _profile;
protected bool Disposed;
protected List<ProfileElement> ChildrenList;
protected ProfileElement()
@ -91,7 +91,12 @@ namespace Artemis.Core
/// <summary>
/// Renders the element
/// </summary>
public abstract void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo);
public abstract void Render(SKCanvas canvas, SKImageInfo canvasInfo);
/// <summary>
/// Resets the internal state of the element
/// </summary>
public abstract void Reset();
/// <inheritdoc />
public override string ToString()
@ -108,7 +113,7 @@ namespace Artemis.Core
/// <param name="order">The order where to place the child (1-based), defaults to the end of the collection</param>
public virtual void AddChild(ProfileElement child, int? order = null)
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
lock (ChildrenList)
@ -152,7 +157,7 @@ namespace Artemis.Core
/// <param name="child">The profile element to remove</param>
public virtual void RemoveChild(ProfileElement child)
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
lock (ChildrenList)
@ -175,7 +180,7 @@ namespace Artemis.Core
/// <returns></returns>
public List<Folder> GetAllFolders()
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
List<Folder> folders = new List<Folder>();
@ -196,7 +201,7 @@ namespace Artemis.Core
/// <returns></returns>
public List<Layer> GetAllLayers()
{
if (_disposed)
if (Disposed)
throw new ObjectDisposedException(GetType().Name);
List<Layer> layers = new List<Layer>();

View File

@ -15,8 +15,7 @@ namespace Artemis.Core
{
protected RenderProfileElement()
{
ApplyDataBindingsEnabled = true;
ApplyKeyframesEnabled = true;
Timeline = new Timeline();
LayerEffectStore.LayerEffectAdded += LayerEffectStoreOnLayerEffectAdded;
LayerEffectStore.LayerEffectRemoved += LayerEffectStoreOnLayerEffectRemoved;
@ -24,49 +23,21 @@ namespace Artemis.Core
public abstract List<ILayerProperty> GetAllLayerProperties();
#region IDisposable
protected override void Dispose(bool disposing)
{
LayerEffectStore.LayerEffectAdded -= LayerEffectStoreOnLayerEffectAdded;
LayerEffectStore.LayerEffectRemoved -= LayerEffectStoreOnLayerEffectRemoved;
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
baseLayerEffect.Dispose();
base.Dispose(disposing);
}
#endregion
internal void ApplyRenderElementDefaults()
{
MainSegmentLength = TimeSpan.FromSeconds(5);
}
internal void LoadRenderElement()
{
StartSegmentLength = RenderElementEntity.StartSegmentLength;
MainSegmentLength = RenderElementEntity.MainSegmentLength;
EndSegmentLength = RenderElementEntity.EndSegmentLength;
DisplayContinuously = RenderElementEntity.DisplayContinuously;
AlwaysFinishTimeline = RenderElementEntity.AlwaysFinishTimeline;
DisplayCondition = RenderElementEntity.DisplayCondition != null
? new DataModelConditionGroup(null, RenderElementEntity.DisplayCondition)
: new DataModelConditionGroup(null);
Timeline = RenderElementEntity.Timeline != null
? new Timeline(RenderElementEntity.Timeline)
: new Timeline();
ActivateEffects();
}
internal void SaveRenderElement()
{
RenderElementEntity.StartSegmentLength = StartSegmentLength;
RenderElementEntity.MainSegmentLength = MainSegmentLength;
RenderElementEntity.EndSegmentLength = EndSegmentLength;
RenderElementEntity.DisplayContinuously = DisplayContinuously;
RenderElementEntity.AlwaysFinishTimeline = AlwaysFinishTimeline;
RenderElementEntity.LayerEffects.Clear();
foreach (BaseLayerEffect layerEffect in LayerEffects)
{
@ -87,8 +58,48 @@ namespace Artemis.Core
// Conditions
RenderElementEntity.DisplayCondition = DisplayCondition?.Entity;
DisplayCondition?.Save();
// Timeline
RenderElementEntity.Timeline = Timeline?.Entity;
Timeline?.Save();
}
#region Timeline
/// <summary>
/// Gets the timeline associated with this render element
/// </summary>
public Timeline Timeline { get; private set; }
/// <summary>
/// Updates the <see cref="Timeline"/> according to the provided <paramref name="deltaTime"/> and current display condition status
/// </summary>
public void UpdateTimeline(double deltaTime)
{
// The play mode dictates whether to stick to the main segment unless the display conditions contains events
bool stickToMainSegment = Timeline.PlayMode == TimelinePlayMode.Repeat && DisplayConditionMet;
if (DisplayCondition != null && DisplayCondition.ContainsEvents)
stickToMainSegment = false;
Timeline.Update(TimeSpan.FromSeconds(deltaTime), stickToMainSegment);
}
#endregion
#region IDisposable
protected override void Dispose(bool disposing)
{
LayerEffectStore.LayerEffectAdded -= LayerEffectStoreOnLayerEffectAdded;
LayerEffectStore.LayerEffectRemoved -= LayerEffectStoreOnLayerEffectRemoved;
foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
baseLayerEffect.Dispose();
base.Dispose(disposing);
}
#endregion
#region Properties
private SKPath _path;
@ -141,126 +152,6 @@ namespace Artemis.Core
#endregion
#region Timeline
private TimeSpan _startSegmentLength;
private TimeSpan _mainSegmentLength;
private TimeSpan _endSegmentLength;
private bool _displayContinuously;
private bool _alwaysFinishTimeline;
/// <summary>
/// Gets or sets the length of the start segment
/// </summary>
public TimeSpan StartSegmentLength
{
get => _startSegmentLength;
set
{
if (!SetAndNotify(ref _startSegmentLength, value)) return;
UpdateTimelineLength();
if (Parent is RenderProfileElement renderElement)
renderElement.UpdateTimelineLength();
}
}
/// <summary>
/// Gets or sets the length of the main segment
/// </summary>
public TimeSpan MainSegmentLength
{
get => _mainSegmentLength;
set
{
if (!SetAndNotify(ref _mainSegmentLength, value)) return;
UpdateTimelineLength();
if (Parent is RenderProfileElement renderElement)
renderElement.UpdateTimelineLength();
}
}
/// <summary>
/// Gets or sets the length of the end segment
/// </summary>
public TimeSpan EndSegmentLength
{
get => _endSegmentLength;
set
{
if (!SetAndNotify(ref _endSegmentLength, value)) return;
UpdateTimelineLength();
if (Parent is RenderProfileElement renderElement)
renderElement.UpdateTimelineLength();
}
}
/// <summary>
/// Gets the current timeline position
/// </summary>
public TimeSpan TimelinePosition
{
get => _timelinePosition;
protected set => SetAndNotify(ref _timelinePosition, value);
}
/// <summary>
/// Gets or sets whether main timeline should repeat itself as long as display conditions are met
/// </summary>
public bool DisplayContinuously
{
get => _displayContinuously;
set => SetAndNotify(ref _displayContinuously, value);
}
/// <summary>
/// Gets or sets whether the timeline should finish when conditions are no longer met
/// </summary>
public bool AlwaysFinishTimeline
{
get => _alwaysFinishTimeline;
set => SetAndNotify(ref _alwaysFinishTimeline, value);
}
/// <summary>
/// Gets the max length of this element and any of its children
/// </summary>
public TimeSpan TimelineLength { get; protected set; }
protected double UpdateTimeline(double deltaTime)
{
TimeSpan oldPosition = _timelinePosition;
TimeSpan deltaTimeSpan = TimeSpan.FromSeconds(deltaTime);
TimeSpan mainSegmentEnd = StartSegmentLength + MainSegmentLength;
TimelinePosition += deltaTimeSpan;
// Manage segments while the condition is met
if (DisplayConditionMet)
{
// If we are at the end of the main timeline, wrap around back to the beginning
if (DisplayContinuously && TimelinePosition >= mainSegmentEnd)
TimelinePosition = StartSegmentLength;
}
else
{
// Skip to the last segment if conditions are no longer met
if (!AlwaysFinishTimeline && TimelinePosition < mainSegmentEnd)
TimelinePosition = mainSegmentEnd;
}
return (TimelinePosition - oldPosition).TotalSeconds;
}
protected internal abstract void UpdateTimelineLength();
/// <summary>
/// Overrides the progress of the element
/// </summary>
/// <param name="timeOverride"></param>
/// <param name="stickToMainSegment"></param>
public abstract void OverrideProgress(TimeSpan timeOverride, bool stickToMainSegment);
#endregion
#region Effect management
protected List<BaseLayerEffect> _layerEffects;
@ -392,11 +283,10 @@ namespace Artemis.Core
public bool DisplayConditionMet
{
get => _displayConditionMet;
private set => SetAndNotify(ref _displayConditionMet, value);
protected set => SetAndNotify(ref _displayConditionMet, value);
}
private DataModelConditionGroup _displayCondition;
private TimeSpan _timelinePosition;
private bool _displayConditionMet;
/// <summary>
@ -409,20 +299,46 @@ namespace Artemis.Core
}
/// <summary>
/// Gets or sets whether keyframes should be applied when this profile element updates
/// Evaluates the display conditions on this element and applies any required changes to the <see cref="Timeline"/>
/// </summary>
public bool ApplyKeyframesEnabled { get; set; }
/// <summary>
/// Gets or sets whether data bindings should be applied when this profile element updates
/// </summary>
public bool ApplyDataBindingsEnabled { get; set; }
public void UpdateDisplayCondition()
{
bool conditionMet = DisplayCondition == null || DisplayCondition.Evaluate();
if (conditionMet && !DisplayConditionMet)
TimelinePosition = TimeSpan.Zero;
if (DisplayCondition == null)
{
DisplayConditionMet = true;
return;
}
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 && DisplayConditionMet && Timeline.StopMode == TimelineStopMode.SkipToEnd)
Timeline.JumpToEndSegment();
}
else if (conditionMet)
{
// 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 = conditionMet;
}

View File

@ -0,0 +1,493 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Artemis.Storage.Entities.Profile;
using Stylet;
namespace Artemis.Core
{
/// <summary>
/// Represents a timeline used by profile elements
/// </summary>
public class Timeline : PropertyChangedBase, IStorageModel
{
/// <summary>
/// Creates a new instance of the <see cref="Timeline" /> class
/// </summary>
public Timeline()
{
Entity = new TimelineEntity();
_extraTimelines = new List<Timeline>();
MainSegmentLength = TimeSpan.FromSeconds(5);
Save();
}
internal Timeline(TimelineEntity entity)
{
Entity = entity;
_extraTimelines = new List<Timeline>();
Load();
}
private Timeline(Timeline parent)
{
Parent = parent;
}
#region Extra timelines
/// <summary>
/// Adds an extra timeline to this timeline
/// </summary>
public void AddExtraTimeline()
{
_extraTimelines.Add(new Timeline(this));
}
/// <summary>
/// Removes all extra timelines from this timeline
/// </summary>
public void ClearExtraTimelines()
{
_extraTimelines.Clear();
}
#endregion
#region Properties
private TimeSpan _position;
private TimeSpan _lastDelta;
private TimeLineEventOverlapMode _eventOverlapMode;
private TimelinePlayMode _playMode;
private TimelineStopMode _stopMode;
private readonly List<Timeline> _extraTimelines;
private TimeSpan _startSegmentLength;
private TimeSpan _mainSegmentLength;
private TimeSpan _endSegmentLength;
/// <summary>
/// Gets the parent this timeline is an extra timeline of
/// </summary>
public Timeline? Parent { get; }
/// <summary>
/// Gets the current position of the timeline
/// </summary>
public TimeSpan Position
{
get => _position;
private set => SetAndNotify(ref _position, value);
}
/// <summary>
/// Gets the cumulative delta of all calls to <see cref="Update" /> that took place after the last call to <see cref="ClearDelta"/>
/// <para>
/// Note: If this is an extra timeline <see cref="Delta" /> is always equal to <see cref="DeltaToParent" />
/// </para>
/// </summary>
public TimeSpan Delta
{
get => Parent == null ? _lastDelta : DeltaToParent;
private set => SetAndNotify(ref _lastDelta, value);
}
/// <summary>
/// Gets the delta to this timeline's <see cref="Parent" />
/// </summary>
public TimeSpan DeltaToParent => Parent != null ? Position - Parent.Position : TimeSpan.Zero;
/// <summary>
/// Gets or sets the mode in which the render element starts its timeline when display conditions are met
/// </summary>
public TimelinePlayMode PlayMode
{
get => _playMode;
set => SetAndNotify(ref _playMode, value);
}
/// <summary>
/// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met
/// </summary>
public TimelineStopMode StopMode
{
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>
public ReadOnlyCollection<Timeline> ExtraTimelines => _extraTimelines.AsReadOnly();
/// <summary>
/// Gets a boolean indicating whether the timeline has finished its run
/// </summary>
public bool IsFinished => Position > Length || Length == TimeSpan.Zero;
/// <summary>
/// Gets a boolean indicating whether the timeline progress has been overridden
/// </summary>
public bool IsOverridden { get; private set; }
#region Segments
/// <summary>
/// Gets the total length of this timeline
/// </summary>
public TimeSpan Length => StartSegmentLength + MainSegmentLength + EndSegmentLength;
/// <summary>
/// Gets or sets the length of the start segment
/// </summary>
public TimeSpan StartSegmentLength
{
get => _startSegmentLength;
set
{
if (SetAndNotify(ref _startSegmentLength, value))
NotifySegmentShiftAt(TimelineSegment.Start, false);
}
}
/// <summary>
/// Gets or sets the length of the main segment
/// </summary>
public TimeSpan MainSegmentLength
{
get => _mainSegmentLength;
set
{
if (SetAndNotify(ref _mainSegmentLength, value))
NotifySegmentShiftAt(TimelineSegment.Main, false);
}
}
/// <summary>
/// Gets or sets the length of the end segment
/// </summary>
public TimeSpan EndSegmentLength
{
get => _endSegmentLength;
set
{
if (SetAndNotify(ref _endSegmentLength, value))
NotifySegmentShiftAt(TimelineSegment.End, false);
}
}
/// <summary>
/// Gets or sets the start position of the main segment
/// </summary>
public TimeSpan MainSegmentStartPosition
{
get => StartSegmentEndPosition;
set
{
StartSegmentEndPosition = value;
NotifySegmentShiftAt(TimelineSegment.Main, true);
}
}
/// <summary>
/// Gets or sets the end position of the end segment
/// </summary>
public TimeSpan EndSegmentStartPosition
{
get => MainSegmentEndPosition;
set
{
MainSegmentEndPosition = value;
NotifySegmentShiftAt(TimelineSegment.End, true);
}
}
/// <summary>
/// Gets or sets the end position of the start segment
/// </summary>
public TimeSpan StartSegmentEndPosition
{
get => StartSegmentLength;
set
{
StartSegmentLength = value;
NotifySegmentShiftAt(TimelineSegment.Start, false);
}
}
/// <summary>
/// Gets or sets the end position of the main segment
/// </summary>
public TimeSpan MainSegmentEndPosition
{
get => StartSegmentEndPosition + MainSegmentLength;
set
{
MainSegmentLength = value - StartSegmentEndPosition >= TimeSpan.Zero ? value - StartSegmentEndPosition : TimeSpan.Zero;
NotifySegmentShiftAt(TimelineSegment.Main, false);
}
}
/// <summary>
/// Gets or sets the end position of the end segment
/// </summary>
public TimeSpan EndSegmentEndPosition
{
get => MainSegmentEndPosition + EndSegmentLength;
set
{
EndSegmentLength = value - MainSegmentEndPosition >= TimeSpan.Zero ? value - MainSegmentEndPosition : TimeSpan.Zero;
NotifySegmentShiftAt(TimelineSegment.End, false);
}
}
internal TimelineEntity Entity { get; set; }
/// <summary>
/// Notifies the right segments in a way that I don't have to think about it
/// </summary>
/// <param name="segment">The segment that was updated</param>
/// <param name="startUpdated">Whether the start point of the <paramref name="segment" /> was updated</param>
private void NotifySegmentShiftAt(TimelineSegment segment, bool startUpdated)
{
if (segment <= TimelineSegment.End)
{
if (startUpdated || segment < TimelineSegment.End)
NotifyOfPropertyChange(nameof(EndSegmentStartPosition));
NotifyOfPropertyChange(nameof(EndSegmentEndPosition));
}
if (segment <= TimelineSegment.Main)
{
if (startUpdated || segment < TimelineSegment.Main)
NotifyOfPropertyChange(nameof(MainSegmentStartPosition));
NotifyOfPropertyChange(nameof(MainSegmentEndPosition));
}
if (segment <= TimelineSegment.Start) NotifyOfPropertyChange(nameof(StartSegmentEndPosition));
NotifyOfPropertyChange(nameof(Length));
}
#endregion
#endregion
#region Updating
/// <summary>
/// Updates the timeline, applying the provided <paramref name="delta" /> to the <see cref="Position" />
/// </summary>
/// <param name="delta">The amount of time to apply to the position</param>
/// <param name="stickToMainSegment">Whether to stick to the main segment, wrapping around if needed</param>
public void Update(TimeSpan delta, bool stickToMainSegment)
{
lock (this)
{
Delta += delta;
Position += delta;
IsOverridden = false;
if (stickToMainSegment && Position >= MainSegmentStartPosition)
{
// If the main segment has no length, simply stick to the start of the segment
if (MainSegmentLength == TimeSpan.Zero)
Position = MainSegmentStartPosition;
// Ensure wrapping back around retains the delta time
else
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
}
foreach (Timeline extraTimeline in _extraTimelines)
extraTimeline.Update(delta, false);
}
}
/// <summary>
/// Moves the position of the timeline backwards to the very start of the timeline
/// </summary>
public void JumpToStart()
{
lock (this)
{
if (Position == TimeSpan.Zero)
return;
Delta = TimeSpan.Zero - Position;
Position = TimeSpan.Zero;
_extraTimelines.Clear();
}
}
/// <summary>
/// Moves the position of the timeline forwards to the beginning of the end segment
/// </summary>
public void JumpToEndSegment()
{
lock (this)
{
if (Position >= EndSegmentStartPosition)
return;
Delta = EndSegmentStartPosition - Position;
Position = EndSegmentStartPosition;
_extraTimelines.Clear();
}
}
/// <summary>
/// Moves the position of the timeline forwards to the very end of the timeline
/// </summary>
public void JumpToEnd()
{
lock (this)
{
if (Position >= EndSegmentEndPosition)
return;
Delta = EndSegmentEndPosition - Position;
Position = EndSegmentEndPosition;
_extraTimelines.Clear();
}
}
/// <summary>
/// Overrides the <see cref="Position" /> to the specified time and clears any extra time lines
/// </summary>
/// <param name="position">The position to set the timeline to</param>
/// <param name="stickToMainSegment">Whether to stick to the main segment, wrapping around if needed</param>
public void Override(TimeSpan position, bool stickToMainSegment)
{
lock (this)
{
Delta += position - Position;
Position = position;
IsOverridden = true;
if (stickToMainSegment && Position >= MainSegmentStartPosition)
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
_extraTimelines.Clear();
}
}
/// <summary>
/// Sets the <see cref="Delta" /> to <see cref="TimeSpan.Zero" />
/// </summary>
public void ClearDelta()
{
lock (this)
{
Delta = TimeSpan.Zero;
}
}
#endregion
#region Storage
/// <inheritdoc />
public void Load()
{
StartSegmentLength = Entity.StartSegmentLength;
MainSegmentLength = Entity.MainSegmentLength;
EndSegmentLength = Entity.EndSegmentLength;
PlayMode = (TimelinePlayMode) Entity.PlayMode;
StopMode = (TimelineStopMode) Entity.StopMode;
EventOverlapMode = (TimeLineEventOverlapMode) Entity.EventOverlapMode;
}
/// <inheritdoc />
public void Save()
{
Entity.StartSegmentLength = StartSegmentLength;
Entity.MainSegmentLength = MainSegmentLength;
Entity.EndSegmentLength = EndSegmentLength;
Entity.PlayMode = (int) PlayMode;
Entity.StopMode = (int) StopMode;
Entity.EventOverlapMode = (int) EventOverlapMode;
}
#endregion
public override string ToString()
{
return $"Progress: {Position}/{Length} - delta: {Delta}";
}
}
internal enum TimelineSegment
{
Start,
Main,
End
}
/// <summary>
/// Represents a mode for render elements to start their timeline when display conditions are met
/// </summary>
public enum TimelinePlayMode
{
/// <summary>
/// Continue repeating the main segment of the timeline while the condition is met
/// </summary>
Repeat,
/// <summary>
/// Only play the timeline once when the condition is met
/// </summary>
Once
}
/// <summary>
/// Represents a mode for render elements to stop their timeline when display conditions are no longer met
/// </summary>
public enum TimelineStopMode
{
/// <summary>
/// When conditions are no longer met, finish the the current run of the main timeline
/// </summary>
Finish,
/// <summary>
/// When conditions are no longer met, skip to the end segment of the timeline
/// </summary>
SkipToEnd
}
/// <summary>
/// Represents a mode for render elements to start their timeline when display conditions events are fired
/// </summary>
public enum TimeLineEventOverlapMode
{
/// <summary>
/// Stop the current run and restart the timeline
/// </summary>
Restart,
/// <summary>
/// Ignore subsequent event fires until the timeline finishes
/// </summary>
Ignore,
/// <summary>
/// Play another copy of the timeline on top of the current run
/// </summary>
Copy
}
}

View File

@ -81,8 +81,9 @@ namespace Artemis.Core.Modules
internal override void InternalDisablePlugin()
{
DataModel = null;
Deactivate(true);
base.InternalDisablePlugin();
DataModel = null;
}
}
@ -112,7 +113,7 @@ namespace Artemis.Core.Modules
/// <summary>
/// Gets the currently active profile
/// </summary>
public Profile ActiveProfile { get; private set; }
public Profile? ActiveProfile { get; private set; }
/// <summary>
/// Disables updating the profile, rendering does continue
@ -174,7 +175,7 @@ namespace Artemis.Core.Modules
lock (this)
{
// Render the profile
ActiveProfile?.Render(deltaTime, canvas, canvasInfo);
ActiveProfile?.Render(canvas, canvasInfo);
}
ProfileRendered(deltaTime, surface, canvas, canvasInfo);

View File

@ -43,7 +43,7 @@ namespace Artemis.Core
/// The action to call every time the interval has passed. The delta time parameter represents the
/// time passed since the last update in seconds
/// </param>
/// <returns>The resulting plugin update registration</returns>
/// <returns>The resulting plugin update registration which can be used to stop the update</returns>
public PluginUpdateRegistration AddTimedUpdate(TimeSpan interval, Action<double> action)
{
if (action == null)

View File

@ -1,47 +1,55 @@
{
"$type": "Artemis.Storage.Entities.Profile.ProfileEntity, Artemis.Storage",
"Id": "eb4f487b-475b-408f-a84f-733412d41b44",
"Id": "824a235d-da46-4c82-a16b-13efe347f492",
"PluginGuid": "0de2991a-d7b8-4f61-ae4e-6623849215b5",
"Name": "Intro animation",
"Name": "Intro animation - Imported",
"IsActive": true,
"Folders": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.FolderEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.FolderEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": [
{
"$type": "Artemis.Storage.Entities.Profile.FolderEntity, Artemis.Storage",
"Id": "cc21b67c-3485-4dc6-b2af-105fda42a915",
"ParentId": "eb4f487b-475b-408f-a84f-733412d41b44",
"ParentId": "824a235d-da46-4c82-a16b-13efe347f492",
"Order": 1,
"Name": "Root folder",
"Enabled": true,
"Profile": null,
"ProfileId": "eb4f487b-475b-408f-a84f-733412d41b44",
"StartSegmentLength": "00:00:00",
"MainSegmentLength": "00:00:05",
"EndSegmentLength": "00:00:00",
"DisplayContinuously": true,
"AlwaysFinishTimeline": false,
"ProfileId": "824a235d-da46-4c82-a16b-13efe347f492",
"LayerEffects": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.LayerEffectEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.LayerEffectEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"PropertyEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"ExpandedPropertyGroups": {
"$type": "System.Collections.Generic.List`1[[System.String, System.Private.CoreLib]], System.Private.CoreLib",
"$values": []
},
"DisplayCondition": {
"$type": "Artemis.Storage.Entities.Profile.Conditions.DataModelConditionGroupEntity, Artemis.Storage",
"BooleanOperator": 0,
"Children": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.Abstract.DataModelConditionPartEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
"Timeline": {
"$type": "Artemis.Storage.Entities.Profile.TimelineEntity, Artemis.Storage",
"StartSegmentLength": "00:00:00",
"MainSegmentLength": "00:00:05",
"EndSegmentLength": "00:00:00",
"PlayMode": 0,
"StopMode": 0,
"EventOverlapMode": 0
}
}
]
},
"Layers": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.LayerEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.LayerEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": [
{
"$type": "Artemis.Storage.Entities.Profile.LayerEntity, Artemis.Storage",
@ -51,25 +59,17 @@
"Name": "Noise",
"Enabled": true,
"Leds": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.LedEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.LedEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"Profile": null,
"ProfileId": "eb4f487b-475b-408f-a84f-733412d41b44",
"StartSegmentLength": "00:00:00",
"MainSegmentLength": "00:00:05",
"EndSegmentLength": "00:00:00",
"DisplayContinuously": false,
"AlwaysFinishTimeline": false,
"ProfileId": "824a235d-da46-4c82-a16b-13efe347f492",
"LayerEffects": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.LayerEffectEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.LayerEffectEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"PropertyEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": [
{
"$type": "Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage",
@ -78,8 +78,11 @@
"Value": "1",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -90,8 +93,11 @@
"Value": "1",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -102,8 +108,11 @@
"Value": "3",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -114,8 +123,11 @@
"Value": "{\"BrushPluginGuid\":\"61cbbf01-8d69-4ede-a972-f3f269da66d9\",\"BrushType\":\"NoiseBrush\"}",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -126,8 +138,11 @@
"Value": "{\"IsEmpty\":true,\"Length\":0.0,\"LengthSquared\":0.0,\"X\":0.0,\"Y\":0.0}",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -138,8 +153,11 @@
"Value": "{\"IsEmpty\":true,\"Length\":0.0,\"LengthSquared\":0.0,\"X\":0.0,\"Y\":0.0}",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -150,8 +168,7 @@
"Value": "{\"IsEmpty\":false,\"Width\":500.0,\"Height\":500.0}",
"KeyframesEnabled": true,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": [
{
"$type": "Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage",
@ -168,6 +185,10 @@
"EasingFunction": 0
}
]
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
{
@ -177,8 +198,11 @@
"Value": "-45.0",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -189,9 +213,15 @@
"Value": "100.0",
"KeyframesEnabled": true,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": [
{
"$type": "Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage",
"Position": "00:00:03.2500000",
"Timeline": 0,
"Value": "100.0",
"EasingFunction": 0
},
{
"$type": "Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage",
"Position": "00:00:04",
@ -207,6 +237,10 @@
"EasingFunction": 0
}
]
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
{
@ -216,8 +250,11 @@
"Value": "1",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -228,8 +265,11 @@
"Value": "\"#ff009688\"",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -240,8 +280,11 @@
"Value": "\"#ff00ffe7\"",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -249,12 +292,14 @@
"$type": "Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage",
"PluginGuid": "61cbbf01-8d69-4ede-a972-f3f269da66d9",
"Path": "LayerBrush.GradientColor",
"Value":
"{\"Stops\":[{\"Color\":\"#ff0b4a40\",\"Position\":0.0},{\"Color\":\"#ff00897c\",\"Position\":0.242},{\"Color\":\"#ffffffff\",\"Position\":1.0},{\"Color\":\"#ff00ffe6\",\"Position\":0.67391306}],\"Rotation\":0.0}",
"Value": "{\"Stops\":[{\"Color\":\"#ff0b4a40\",\"Position\":0.0},{\"Color\":\"#ff00897c\",\"Position\":0.242},{\"Color\":\"#ffffffff\",\"Position\":1.0},{\"Color\":\"#ff00ffe6\",\"Position\":0.67391306}]}",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -265,8 +310,11 @@
"Value": "{\"IsEmpty\":false,\"Width\":44.9,\"Height\":31.0}",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -277,8 +325,11 @@
"Value": "228.5",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -289,8 +340,11 @@
"Value": "{\"IsEmpty\":true,\"Length\":0.0,\"LengthSquared\":0.0,\"X\":0.0,\"Y\":0.0}",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -301,8 +355,11 @@
"Value": "25.0",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
}
@ -311,8 +368,25 @@
"ExpandedPropertyGroups": {
"$type": "System.Collections.Generic.List`1[[System.String, System.Private.CoreLib]], System.Private.CoreLib",
"$values": [
"LayerBrush"
"Transform"
]
},
"DisplayCondition": {
"$type": "Artemis.Storage.Entities.Profile.Conditions.DataModelConditionGroupEntity, Artemis.Storage",
"BooleanOperator": 0,
"Children": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.Abstract.DataModelConditionPartEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
"Timeline": {
"$type": "Artemis.Storage.Entities.Profile.TimelineEntity, Artemis.Storage",
"StartSegmentLength": "00:00:00",
"MainSegmentLength": "00:00:05",
"EndSegmentLength": "00:00:00",
"PlayMode": 0,
"StopMode": 0,
"EventOverlapMode": 0
}
},
{
@ -323,25 +397,17 @@
"Name": "Exploison",
"Enabled": true,
"Leds": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.LedEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.LedEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"Profile": null,
"ProfileId": "eb4f487b-475b-408f-a84f-733412d41b44",
"StartSegmentLength": "00:00:00",
"MainSegmentLength": "00:00:03",
"EndSegmentLength": "00:00:00",
"DisplayContinuously": false,
"AlwaysFinishTimeline": false,
"ProfileId": "824a235d-da46-4c82-a16b-13efe347f492",
"LayerEffects": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.LayerEffectEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.LayerEffectEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"PropertyEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": [
{
"$type": "Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage",
@ -350,8 +416,11 @@
"Value": "0",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -362,8 +431,11 @@
"Value": "0",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -374,8 +446,11 @@
"Value": "3",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -386,8 +461,11 @@
"Value": "{\"BrushPluginGuid\":\"92a9d6ba-6f7a-4937-94d5-c1d715b4141a\",\"BrushType\":\"ColorBrush\"}",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -398,8 +476,11 @@
"Value": "{\"IsEmpty\":true,\"Length\":0.0,\"LengthSquared\":0.0,\"X\":0.0,\"Y\":0.0}",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -410,8 +491,11 @@
"Value": "{\"IsEmpty\":true,\"Length\":0.0,\"LengthSquared\":0.0,\"X\":0.0,\"Y\":0.0}",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -422,8 +506,7 @@
"Value": "{\"IsEmpty\":false,\"Width\":110.03,\"Height\":340.37}",
"KeyframesEnabled": true,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": [
{
"$type": "Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage",
@ -440,6 +523,10 @@
"EasingFunction": 0
}
]
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
{
@ -449,8 +536,11 @@
"Value": "0.0",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -461,8 +551,11 @@
"Value": "100.0",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -473,8 +566,11 @@
"Value": "2",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -485,8 +581,11 @@
"Value": "0",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -497,8 +596,11 @@
"Value": "\"#ffff0000\"",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -506,12 +608,14 @@
"$type": "Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage",
"PluginGuid": "92a9d6ba-6f7a-4937-94d5-c1d715b4141a",
"Path": "LayerBrush.Colors",
"Value":
"{\"Stops\":[{\"Color\":\"#00ff0000\",\"Position\":0.0},{\"Color\":\"#ffff8800\",\"Position\":0.492},{\"Color\":\"#ffedff00\",\"Position\":0.905},{\"Color\":\"#00ff0000\",\"Position\":1.0}],\"Rotation\":0.0}",
"Value": "{\"Stops\":[{\"Color\":\"#00ff0000\",\"Position\":0.0},{\"Color\":\"#ffff8800\",\"Position\":0.492},{\"Color\":\"#ffedff00\",\"Position\":0.905},{\"Color\":\"#00ff0000\",\"Position\":1.0}]}",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -522,8 +626,11 @@
"Value": "0",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -534,8 +641,11 @@
"Value": "0.0",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -546,8 +656,11 @@
"Value": "{\"IsEmpty\":true,\"Length\":0.0,\"LengthSquared\":0.0,\"X\":0.0,\"Y\":0.0}",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -558,8 +671,11 @@
"Value": "0",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
}
@ -568,35 +684,44 @@
"ExpandedPropertyGroups": {
"$type": "System.Collections.Generic.List`1[[System.String, System.Private.CoreLib]], System.Private.CoreLib",
"$values": []
},
"DisplayCondition": {
"$type": "Artemis.Storage.Entities.Profile.Conditions.DataModelConditionGroupEntity, Artemis.Storage",
"BooleanOperator": 0,
"Children": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.Abstract.DataModelConditionPartEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
"Timeline": {
"$type": "Artemis.Storage.Entities.Profile.TimelineEntity, Artemis.Storage",
"StartSegmentLength": "00:00:00",
"MainSegmentLength": "00:00:02.8500000",
"EndSegmentLength": "00:00:00",
"PlayMode": 1,
"StopMode": 0,
"EventOverlapMode": 0
}
},
{
"$type": "Artemis.Storage.Entities.Profile.LayerEntity, Artemis.Storage",
"Id": "f046f56f-a236-4ed6-bbd9-b5a4731878cf",
"ParentId": "cc21b67c-3485-4dc6-b2af-105fda42a915",
"Order": 2,
"Order": 3,
"Name": "Background",
"Enabled": true,
"Leds": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.LedEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.LedEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"Profile": null,
"ProfileId": "eb4f487b-475b-408f-a84f-733412d41b44",
"StartSegmentLength": "00:00:00",
"MainSegmentLength": "00:00:03",
"EndSegmentLength": "00:00:00",
"DisplayContinuously": false,
"AlwaysFinishTimeline": false,
"ProfileId": "824a235d-da46-4c82-a16b-13efe347f492",
"LayerEffects": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.LayerEffectEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.LayerEffectEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"PropertyEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": [
{
"$type": "Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage",
@ -605,8 +730,11 @@
"Value": "1",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -617,8 +745,11 @@
"Value": "0",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -629,8 +760,11 @@
"Value": "3",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -641,8 +775,11 @@
"Value": "{\"BrushPluginGuid\":\"92a9d6ba-6f7a-4937-94d5-c1d715b4141a\",\"BrushType\":\"ColorBrush\"}",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -653,8 +790,11 @@
"Value": "{\"IsEmpty\":true,\"Length\":0.0,\"LengthSquared\":0.0,\"X\":0.0,\"Y\":0.0}",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -665,8 +805,11 @@
"Value": "{\"IsEmpty\":true,\"Length\":0.0,\"LengthSquared\":0.0,\"X\":0.0,\"Y\":0.0}",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -677,8 +820,11 @@
"Value": "{\"IsEmpty\":false,\"Width\":100.0,\"Height\":100.0}",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -689,8 +835,11 @@
"Value": "0.0",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -701,8 +850,11 @@
"Value": "100.0",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -713,8 +865,11 @@
"Value": "0",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -725,8 +880,11 @@
"Value": "0",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -737,8 +895,11 @@
"Value": "\"#ff000000\"",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -746,12 +907,14 @@
"$type": "Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage",
"PluginGuid": "92a9d6ba-6f7a-4937-94d5-c1d715b4141a",
"Path": "LayerBrush.Colors",
"Value":
"{\"Stops\":[{\"Color\":\"#ffff0000\",\"Position\":0.0},{\"Color\":\"#ffff8800\",\"Position\":0.125},{\"Color\":\"#ffedff00\",\"Position\":0.25},{\"Color\":\"#ff65ff00\",\"Position\":0.375},{\"Color\":\"#ff00ff22\",\"Position\":0.5},{\"Color\":\"#ff00ffaa\",\"Position\":0.625},{\"Color\":\"#ff00cbff\",\"Position\":0.75},{\"Color\":\"#ff0043ff\",\"Position\":0.875},{\"Color\":\"#ffff0000\",\"Position\":1.0}],\"Rotation\":0.0}",
"Value": "{\"Stops\":[{\"Color\":\"#ffff0000\",\"Position\":0.0},{\"Color\":\"#ffff8800\",\"Position\":0.125},{\"Color\":\"#ffedff00\",\"Position\":0.25},{\"Color\":\"#ff65ff00\",\"Position\":0.375},{\"Color\":\"#ff00ff22\",\"Position\":0.5},{\"Color\":\"#ff00ffaa\",\"Position\":0.625},{\"Color\":\"#ff00cbff\",\"Position\":0.75},{\"Color\":\"#ff0043ff\",\"Position\":0.875},{\"Color\":\"#ffff0000\",\"Position\":1.0}]}",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -762,8 +925,11 @@
"Value": "0",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -774,8 +940,11 @@
"Value": "0.0",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -786,8 +955,11 @@
"Value": "{\"IsEmpty\":true,\"Length\":0.0,\"LengthSquared\":0.0,\"X\":0.0,\"Y\":0.0}",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
@ -798,8 +970,11 @@
"Value": "0",
"KeyframesEnabled": false,
"KeyframeEntities": {
"$type":
"System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
},
"DataBindingEntities": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.DataBindings.DataBindingEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
}
@ -810,6 +985,23 @@
"$values": [
"LayerBrush"
]
},
"DisplayCondition": {
"$type": "Artemis.Storage.Entities.Profile.Conditions.DataModelConditionGroupEntity, Artemis.Storage",
"BooleanOperator": 0,
"Children": {
"$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.Abstract.DataModelConditionPartEntity, Artemis.Storage]], System.Private.CoreLib",
"$values": []
}
},
"Timeline": {
"$type": "Artemis.Storage.Entities.Profile.TimelineEntity, Artemis.Storage",
"StartSegmentLength": "00:00:00",
"MainSegmentLength": "00:00:05",
"EndSegmentLength": "00:00:00",
"PlayMode": 0,
"StopMode": 0,
"EventOverlapMode": 0
}
}
]

View File

@ -123,7 +123,7 @@ namespace Artemis.Core.Services
_introAnimation.Render(args.DeltaTime, args.Canvas, _rgbService.BitmapBrush.Bitmap.Info);
}
TimeSpan introLength = _introAnimation.AnimationProfile.GetAllLayers().Max(l => l.TimelineLength);
TimeSpan introLength = _introAnimation.AnimationProfile.GetAllLayers().Max(l => l.Timeline.Length);
// Stop rendering after the profile finishes (take 1 second extra in case of slow updates)
Task.Run(async () =>
@ -146,7 +146,7 @@ namespace Artemis.Core.Services
{
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
Converters = new List<JsonConverter> {new SKColorConverter()}
Converters = new List<JsonConverter> {new SKColorConverter(), new ForgivingIntConverter()}
};
}

View File

@ -221,8 +221,7 @@ namespace Artemis.Core.Services
Type pluginType = pluginTypes.Single();
try
{
IParameter[] parameters = new IParameter[]
{
IParameter[] parameters = {
new Parameter("PluginInfo", pluginInfo, false)
};
pluginInfo.Kernel = new ChildKernel(_kernel);

View File

@ -72,6 +72,8 @@ namespace Artemis.Core.Services
// Colors
RegisterModifierType(Constants.CorePluginInfo, new SKColorSumModifierType());
RegisterModifierType(Constants.CorePluginInfo, new SKColorSaturateModifierType());
RegisterModifierType(Constants.CorePluginInfo, new SKColorDesaturateModifierType());
RegisterModifierType(Constants.CorePluginInfo, new SKColorBrightenModifierType());
RegisterModifierType(Constants.CorePluginInfo, new SKColorDarkenModifierType());
RegisterModifierType(Constants.CorePluginInfo, new SKColorRotateHueModifierType());

View File

@ -49,6 +49,11 @@ namespace Artemis.Core.Services
/// <param name="profileModule"></param>
void ActivateLastProfile(ProfileModule profileModule);
/// <summary>
/// Reloads the currently active profile on the provided profile module
/// </summary>
void ReloadProfile(ProfileModule module);
/// <summary>
/// Asynchronously activates the last profile of the given profile module using a fade animation
/// </summary>

View File

@ -82,6 +82,19 @@ namespace Artemis.Core.Services
return profile;
}
public void ReloadProfile(ProfileModule module)
{
if (module.ActiveProfile == null)
return;
ProfileEntity entity = _profileRepository.Get(module.ActiveProfile.EntityId);
Profile profile = new Profile(module, entity);
InstantiateProfile(profile);
module.ChangeActiveProfile(null, _surfaceService.ActiveSurface);
module.ChangeActiveProfile(profile, _surfaceService.ActiveSurface);
}
public async Task<Profile> ActivateProfileAnimated(ProfileDescriptor profileDescriptor)
{
if (profileDescriptor.ProfileModule.ActiveProfile?.EntityId == profileDescriptor.Id)

View File

@ -24,17 +24,6 @@ namespace Artemis.Core
);
}
public static void LogListPredicateDeserializationFailure(DataModelConditionListPredicate dataModelConditionPredicate, JsonException exception)
{
_logger.Warning(
exception,
"Failed to deserialize display condition list predicate {list} => {left} {operator} {right}",
dataModelConditionPredicate.Entity.LeftPath?.Path,
dataModelConditionPredicate.Entity.OperatorType,
dataModelConditionPredicate.Entity.RightPath?.Path
);
}
public static void LogModifierDeserializationFailure(string modifierName, JsonSerializationException exception)
{
_logger.Warning(exception, "Failed to deserialize static parameter for modifier {modifierName}", modifierName);

View File

@ -32,7 +32,7 @@ namespace Artemis.Core
return;
AnimationProfile.Update(deltaTime);
AnimationProfile.Render(deltaTime, canvas, bitmapInfo);
AnimationProfile.Render(canvas, bitmapInfo);
}
private void CreateIntroProfile()

View File

@ -6,16 +6,11 @@ namespace Artemis.Storage.Entities.Profile.Abstract
{
public abstract class RenderElementEntity
{
public TimeSpan StartSegmentLength { get; set; }
public TimeSpan MainSegmentLength { get; set; }
public TimeSpan EndSegmentLength { get; set; }
public bool DisplayContinuously { get; set; }
public bool AlwaysFinishTimeline { get; set; }
public List<LayerEffectEntity> LayerEffects { get; set; }
public List<PropertyEntity> PropertyEntities { get; set; }
public List<string> ExpandedPropertyGroups { get; set; }
public DataModelConditionGroupEntity DisplayCondition { get; set; }
public TimelineEntity Timeline { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
using Artemis.Storage.Entities.Profile.Abstract;
namespace Artemis.Storage.Entities.Profile.Conditions
{
public class DataModelConditionEventEntity : DataModelConditionPartEntity
{
public DataModelConditionEventEntity()
{
Children = new List<DataModelConditionPartEntity>();
}
public DataModelPathEntity EventPath { get; set; }
}
}

View File

@ -0,0 +1,6 @@
namespace Artemis.Storage.Entities.Profile.Conditions
{
public class DataModelConditionEventPredicateEntity : DataModelConditionPredicateEntity
{
}
}

View File

@ -0,0 +1,6 @@
namespace Artemis.Storage.Entities.Profile.Conditions
{
public class DataModelConditionGeneralPredicateEntity : DataModelConditionPredicateEntity
{
}
}

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using Artemis.Storage.Entities.Profile.Abstract;
namespace Artemis.Storage.Entities.Profile.Conditions

View File

@ -1,20 +1,6 @@
using System;
using Artemis.Storage.Entities.Profile.Abstract;
namespace Artemis.Storage.Entities.Profile.Conditions
namespace Artemis.Storage.Entities.Profile.Conditions
{
public class DataModelConditionListPredicateEntity : DataModelConditionPartEntity
public class DataModelConditionListPredicateEntity : DataModelConditionPredicateEntity
{
public int PredicateType { get; set; }
public DataModelPathEntity LeftPath { get; set; }
public DataModelPathEntity RightPath { get; set; }
// Stored as a string to be able to control serialization and deserialization ourselves
public string RightStaticValue { get; set; }
public string OperatorType { get; set; }
public Guid? OperatorPluginGuid { get; set; }
}
}

View File

@ -3,7 +3,7 @@ using Artemis.Storage.Entities.Profile.Abstract;
namespace Artemis.Storage.Entities.Profile.Conditions
{
public class DataModelConditionPredicateEntity : DataModelConditionPartEntity
public abstract class DataModelConditionPredicateEntity : DataModelConditionPartEntity
{
public int PredicateType { get; set; }
public DataModelPathEntity LeftPath { get; set; }
@ -14,6 +14,5 @@ namespace Artemis.Storage.Entities.Profile.Conditions
// Stored as a string to be able to control serialization and deserialization ourselves
public string RightStaticValue { get; set; }
}
}

View File

@ -6,5 +6,14 @@ namespace Artemis.Storage.Entities.Profile
{
public string Path { get; set; }
public Guid? DataModelGuid { get; set; }
public PathWrapperType WrapperType { get; set; }
}
public enum PathWrapperType
{
None,
List,
Event
}
}

View File

@ -0,0 +1,15 @@
using System;
namespace Artemis.Storage.Entities.Profile
{
public class TimelineEntity
{
public TimeSpan StartSegmentLength { get; set; }
public TimeSpan MainSegmentLength { get; set; }
public TimeSpan EndSegmentLength { get; set; }
public int PlayMode { get; set; }
public int StopMode { get; set; }
public int EventOverlapMode { get; set; }
}
}

View File

@ -13,31 +13,32 @@ namespace Artemis.Storage.Migrations
public void Apply(LiteRepository repository)
{
List<ProfileEntity> profiles = repository.Query<ProfileEntity>().ToList();
foreach (ProfileEntity profileEntity in profiles)
{
foreach (FolderEntity folder in profileEntity.Folders.Where(f => f.MainSegmentLength == TimeSpan.Zero))
{
if (folder.PropertyEntities.Any(p => p.KeyframeEntities.Any()))
folder.MainSegmentLength = folder.PropertyEntities.Where(p => p.KeyframeEntities.Any()).Max(p => p.KeyframeEntities.Max(k => k.Position));
if (folder.MainSegmentLength == TimeSpan.Zero)
folder.MainSegmentLength = TimeSpan.FromSeconds(5);
folder.DisplayContinuously = true;
}
foreach (LayerEntity layer in profileEntity.Layers.Where(l => l.MainSegmentLength == TimeSpan.Zero))
{
if (layer.PropertyEntities.Any(p => p.KeyframeEntities.Any()))
layer.MainSegmentLength = layer.PropertyEntities.Where(p => p.KeyframeEntities.Any()).Max(p => p.KeyframeEntities.Max(k => k.Position));
if (layer.MainSegmentLength == TimeSpan.Zero)
layer.MainSegmentLength = TimeSpan.FromSeconds(5);
layer.DisplayContinuously = true;
}
repository.Update(profileEntity);
}
// Lesson for next time: Use BsonDocuments in migrations
// List<ProfileEntity> profiles = repository.Query<ProfileEntity>().ToList();
// foreach (ProfileEntity profileEntity in profiles)
// {
// foreach (FolderEntity folder in profileEntity.Folders.Where(f => f.MainSegmentLength == TimeSpan.Zero))
// {
// if (folder.PropertyEntities.Any(p => p.KeyframeEntities.Any()))
// folder.MainSegmentLength = folder.PropertyEntities.Where(p => p.KeyframeEntities.Any()).Max(p => p.KeyframeEntities.Max(k => k.Position));
// if (folder.MainSegmentLength == TimeSpan.Zero)
// folder.MainSegmentLength = TimeSpan.FromSeconds(5);
//
// folder.PlayMode = 0;
// }
//
// foreach (LayerEntity layer in profileEntity.Layers.Where(l => l.MainSegmentLength == TimeSpan.Zero))
// {
// if (layer.PropertyEntities.Any(p => p.KeyframeEntities.Any()))
// layer.MainSegmentLength = layer.PropertyEntities.Where(p => p.KeyframeEntities.Any()).Max(p => p.KeyframeEntities.Max(k => k.Position));
// if (layer.MainSegmentLength == TimeSpan.Zero)
// layer.MainSegmentLength = TimeSpan.FromSeconds(5);
//
// layer.PlayMode = 0;
// }
//
// repository.Update(profileEntity);
// }
}
}
}

View File

@ -0,0 +1,49 @@
using System;
using Artemis.Storage.Migrations.Interfaces;
using LiteDB;
namespace Artemis.Storage.Migrations
{
public class M6PredicateAbstraction : IStorageMigration
{
public int UserVersion => 6;
public void Apply(LiteRepository repository)
{
ILiteCollection<BsonDocument> collection = repository.Database.GetCollection("ProfileEntity");
foreach (BsonDocument bsonDocument in collection.FindAll())
{
foreach (BsonValue bsonLayer in bsonDocument["Layers"].AsArray)
Migrate(bsonLayer);
foreach (BsonValue bsonLayer in bsonDocument["Folders"].AsArray)
Migrate(bsonLayer);
collection.Update(bsonDocument);
}
}
private void Migrate(BsonValue bsonValue)
{
if (bsonValue.IsArray)
{
foreach (BsonValue child in bsonValue.AsArray)
Migrate(child);
return;
}
if (bsonValue.IsDocument)
{
// See if the document has a type
if (bsonValue.AsDocument.TryGetValue("_type", out BsonValue typeValue))
{
if (typeValue.AsString == "Artemis.Storage.Entities.Profile.Conditions.DataModelConditionPredicateEntity, Artemis.Storage")
bsonValue.AsDocument["_type"] = "Artemis.Storage.Entities.Profile.Conditions.DataModelConditionGeneralPredicateEntity, Artemis.Storage";
}
foreach (BsonValue documentValue in bsonValue.AsDocument.Values)
Migrate(documentValue);
}
}
}
}

View File

@ -1,2 +1,15 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=behaviors/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=controls/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=converters/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=datamodelvisualization/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=datamodelvisualization_005Cshared/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=dependencyproperties/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=events/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=exceptions/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=extensions/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=propertyinput/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cdatamodelvisualization/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cdialog/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cinterfaces/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=utilities/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -97,7 +97,11 @@ namespace Artemis.UI.Shared.Input
public bool IsDataModelViewModelOpen
{
get => _isDataModelViewModelOpen;
set => SetAndNotify(ref _isDataModelViewModelOpen, value);
set
{
if (!SetAndNotify(ref _isDataModelViewModelOpen, value)) return;
if (value) UpdateDataModelVisualization();
}
}
public DataModelPath DataModelPath
@ -127,6 +131,8 @@ namespace Artemis.UI.Shared.Input
}
}
public bool LoadEventChildren { get; set; } = true;
public void ChangeDataModel(DataModelPropertiesViewModel dataModel)
{
if (DataModelViewModel != null)
@ -198,9 +204,14 @@ namespace Artemis.UI.Shared.Input
if (!IsDataModelViewModelOpen)
return;
DataModelViewModel.Update(_dataModelUIService);
UpdateDataModelVisualization();
}
private void UpdateDataModelVisualization()
{
DataModelViewModel.Update(_dataModelUIService, new DataModelUpdateConfiguration(LoadEventChildren));
foreach (DataModelPropertiesViewModel extraDataModelViewModel in ExtraDataModelViewModels)
extraDataModelViewModel.Update(_dataModelUIService);
extraDataModelViewModel.Update(_dataModelUIService, new DataModelUpdateConfiguration(LoadEventChildren));
}
#endregion

View File

@ -16,12 +16,21 @@
<ResourceDictionary Source="pack://application:,,,/Artemis.UI.Shared;component/ResourceDictionaries/DataModelConditions.xaml" />
<ResourceDictionary>
<shared:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
<Style x:Key="DataModelConditionButtonCornerToggle" BasedOn="{StaticResource DataModelConditionButton}" TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding DisplaySwitchButton}" Value="True">
<Setter Property="materialDesign:ButtonAssist.CornerRadius" Value="0 2 2 0" />
</DataTrigger>
<DataTrigger Binding="{Binding DisplaySwitchButton}" Value="False">
<Setter Property="materialDesign:ButtonAssist.CornerRadius" Value="2" />
</DataTrigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid Margin="3 -4">
<Grid>
<StackPanel Orientation="Horizontal" Visibility="{Binding InputViewModel, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Inverted}">
<Button Style="{StaticResource MaterialDesignFlatDarkBgButton}"
Background="{Binding SwitchButtonBrush}"
@ -31,15 +40,14 @@
Width="22"
FontSize="12"
materialDesign:ButtonAssist.CornerRadius="2 0 0 2"
Margin="0 0 -3 0"
Margin="3 0 -3 0"
HorizontalAlignment="Left"
ToolTip="Switch to data model value"
Command="{s:Action SwitchToDynamic}"
Visibility="{Binding DisplaySwitchButton, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
<materialDesign:PackIcon Kind="SwapHorizontal" />
</Button>
<Button Style="{StaticResource DataModelConditionButton}"
materialDesign:ButtonAssist.CornerRadius="0 2 2 0"
<Button Style="{StaticResource DataModelConditionButtonCornerToggle}"
Background="{Binding ButtonBrush}"
BorderBrush="{Binding ButtonBrush}"
Command="{s:Action ActivateInputViewModel}"
@ -63,6 +71,7 @@
Visibility="{Binding InputViewModel, Converter={StaticResource NullToVisibilityConverter}}"
CornerRadius="3"
Padding="3"
Margin="0 -3"
HorizontalAlignment="Left"
MinWidth="140">
<ContentControl s:View.Model="{Binding InputViewModel}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" />

View File

@ -69,7 +69,7 @@ namespace Artemis.UI.Shared.Input
public Type TargetType
{
get => _targetType;
set => SetAndNotify(ref _targetType, value);
private set => SetAndNotify(ref _targetType, value);
}
public DataModelPropertyAttribute TargetDescription

View File

@ -0,0 +1,59 @@
using System;
using System.Linq;
using Artemis.Core;
using Artemis.Core.DataModelExpansions;
using Artemis.UI.Shared.Services;
namespace Artemis.UI.Shared
{
public class DataModelEventViewModel : DataModelVisualizationViewModel
{
private Type _displayValueType;
internal DataModelEventViewModel(DataModel dataModel, DataModelVisualizationViewModel parent, DataModelPath dataModelPath) : base(dataModel, parent, dataModelPath)
{
}
public Type DisplayValueType
{
get => _displayValueType;
set => SetAndNotify(ref _displayValueType, value);
}
public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration configuration)
{
DisplayValueType = DataModelPath?.GetPropertyType();
if (configuration != null)
{
if (configuration.CreateEventChildren)
PopulateProperties(dataModelUIService, configuration);
else if (Children.Any())
Children.Clear();
}
// Only update children if the parent is expanded
if (Parent != null && !Parent.IsRootViewModel && !Parent.IsVisualizationExpanded)
return;
foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in Children)
dataModelVisualizationViewModel.Update(dataModelUIService, configuration);
}
public override object GetCurrentValue()
{
return null;
}
/// <inheritdoc />
public override string ToString()
{
return DisplayPath ?? Path;
}
internal override int GetChildDepth()
{
return PropertyDescription != null && !PropertyDescription.ResetsDepth ? Depth + 1 : 1;
}
}
}

View File

@ -15,8 +15,6 @@ namespace Artemis.UI.Shared
{
DataModel = ListPredicateWrapperDataModel.Create(listType);
ListType = listType;
IsRootViewModel = false;
}
public int Index
@ -41,17 +39,17 @@ namespace Artemis.UI.Shared
public override string DisplayPath => null;
public override void Update(IDataModelUIService dataModelUIService)
public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration configuration)
{
((ListPredicateWrapperDataModel) DataModel).UntypedValue = DisplayValue;
PopulateProperties(dataModelUIService);
PopulateProperties(dataModelUIService, configuration);
if (DisplayViewModel == null)
return;
if (IsVisualizationExpanded && !DisplayViewModel.IsVisualizationExpanded)
DisplayViewModel.IsVisualizationExpanded = IsVisualizationExpanded;
DisplayViewModel.Update(dataModelUIService);
DisplayViewModel.Update(dataModelUIService, null);
}
public override object GetCurrentValue()

View File

@ -39,7 +39,7 @@ namespace Artemis.UI.Shared
return DisplayValue;
}
public override void Update(IDataModelUIService dataModelUIService)
public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration configuration)
{
// Display value gets updated by parent, don't do anything if it is null
if (DisplayValue == null)

View File

@ -45,31 +45,7 @@ namespace Artemis.UI.Shared
public BindableCollection<DataModelVisualizationViewModel> ListChildren { get; set; }
public DataModelPropertiesViewModel GetListTypeViewModel(IDataModelUIService dataModelUIService)
{
Type listType = DataModelPath.GetPropertyType()?.GetGenericEnumerableType();
if (listType == null)
return null;
// Create a property VM describing the type of the list
DataModelVisualizationViewModel viewModel = CreateListChild(dataModelUIService, listType);
viewModel.Update(dataModelUIService);
// Put an empty value into the list type property view model
if (viewModel is DataModelListPropertiesViewModel dataModelListClassViewModel) return dataModelListClassViewModel;
if (viewModel is DataModelListPropertyViewModel dataModelListPropertyViewModel)
{
dataModelListPropertyViewModel.DisplayValue = Activator.CreateInstance(dataModelListPropertyViewModel.ListType);
DataModelPropertiesViewModel wrapper = new DataModelPropertiesViewModel(null, null, null);
wrapper.Children.Add(dataModelListPropertyViewModel);
return wrapper;
}
return null;
}
public override void Update(IDataModelUIService dataModelUIService)
public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration configuration)
{
if (Parent != null && !Parent.IsVisualizationExpanded)
return;
@ -107,7 +83,7 @@ namespace Artemis.UI.Shared
dataModelListPropertyViewModel.Index = index;
}
child.Update(dataModelUIService);
child.Update(dataModelUIService, configuration);
index++;
}

View File

@ -7,8 +7,8 @@ namespace Artemis.UI.Shared
{
public class DataModelPropertiesViewModel : DataModelVisualizationViewModel
{
private Type _displayValueType;
private object _displayValue;
private Type _displayValueType;
internal DataModelPropertiesViewModel(DataModel dataModel, DataModelVisualizationViewModel parent, DataModelPath dataModelPath) : base(dataModel, parent, dataModelPath)
{
@ -26,7 +26,7 @@ namespace Artemis.UI.Shared
set => SetAndNotify(ref _displayValue, value);
}
public override void Update(IDataModelUIService dataModelUIService)
public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration configuration)
{
DisplayValueType = DataModelPath?.GetPropertyType();
@ -38,21 +38,21 @@ namespace Artemis.UI.Shared
DisplayValue = null;
// Always populate properties
PopulateProperties(dataModelUIService);
PopulateProperties(dataModelUIService, configuration);
// Only update children if the parent is expanded
if (Parent != null && !Parent.IsVisualizationExpanded && !Parent.IsRootViewModel)
if (Parent != null && !Parent.IsRootViewModel && !Parent.IsVisualizationExpanded)
return;
foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in Children)
dataModelVisualizationViewModel.Update(dataModelUIService);
dataModelVisualizationViewModel.Update(dataModelUIService, configuration);
}
public override object GetCurrentValue()
{
if (Parent == null)
return null;
return Parent.IsRootViewModel ? DataModel : base.GetCurrentValue();
if (Parent == null || Parent.IsRootViewModel || IsRootViewModel)
return DataModel;
return base.GetCurrentValue();
}
/// <inheritdoc />

View File

@ -33,7 +33,7 @@ namespace Artemis.UI.Shared
set => SetAndNotify(ref _displayViewModel, value);
}
public override void Update(IDataModelUIService dataModelUIService)
public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration configuration)
{
if (Parent != null && !Parent.IsVisualizationExpanded && !Parent.IsRootViewModel)
return;

View File

@ -1,5 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
@ -29,7 +28,7 @@ namespace Artemis.UI.Shared
Children = new BindableCollection<DataModelVisualizationViewModel>();
IsMatchingFilteredTypes = true;
if (dataModel == null && parent == null && dataModelPath == null)
if (parent == null)
IsRootViewModel = true;
else
PropertyDescription = DataModelPath?.GetPropertyDescription() ?? DataModel.DataModelDescription;
@ -88,8 +87,9 @@ namespace Artemis.UI.Shared
/// <summary>
/// Updates the datamodel and if in an parent, any children
/// </summary>
/// <param name="dataModelUIService"></param>
public abstract void Update(IDataModelUIService dataModelUIService);
/// <param name="dataModelUIService">The data model UI service used during update</param>
/// <param name="configuration">The configuration to apply while updating</param>
public abstract void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration configuration);
public virtual object GetCurrentValue()
{
@ -137,57 +137,23 @@ namespace Artemis.UI.Shared
if (looseMatch)
IsMatchingFilteredTypes = filteredTypes.Any(t => t.IsCastableFrom(type) ||
t == typeof(Enum) && type.IsEnum ||
t == typeof(IEnumerable<>) && type.IsGenericEnumerable());
t == typeof(IEnumerable<>) && type.IsGenericEnumerable() ||
type.IsGenericType && t == type.GetGenericTypeDefinition());
else
IsMatchingFilteredTypes = filteredTypes.Any(t => t == type || t == typeof(Enum) && type.IsEnum);
}
public DataModelVisualizationViewModel GetChildByPath(Guid dataModelGuid, string propertyPath)
{
if (!IsRootViewModel)
{
if (DataModel.PluginInfo.Guid != dataModelGuid)
return null;
if (propertyPath == null)
return null;
if (Path != null && Path.StartsWith(propertyPath, StringComparison.OrdinalIgnoreCase))
return null;
}
// Ensure children are populated by requesting an update
if (!IsVisualizationExpanded)
{
IsVisualizationExpanded = true;
RequestUpdate();
IsVisualizationExpanded = false;
}
foreach (DataModelVisualizationViewModel child in Children)
{
// Try the child itself first
if (child.Path == propertyPath)
return child;
// Try a child on the child next, this will go recursive
DataModelVisualizationViewModel match = child.GetChildByPath(dataModelGuid, propertyPath);
if (match != null)
return match;
}
return null;
}
internal virtual int GetChildDepth()
{
return 0;
}
internal void PopulateProperties(IDataModelUIService dataModelUIService)
internal void PopulateProperties(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration dataModelUpdateConfiguration)
{
if (IsRootViewModel)
if (IsRootViewModel && DataModel == null)
return;
Type modelType = Parent == null || Parent.IsRootViewModel ? DataModel.GetType() : DataModelPath.GetPropertyType();
Type modelType = IsRootViewModel ? DataModel.GetType() : DataModelPath.GetPropertyType();
// Add missing static children
foreach (PropertyInfo propertyInfo in modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance).OrderBy(t => t.MetadataToken))
@ -219,7 +185,6 @@ namespace Artemis.UI.Shared
// Add missing dynamic children
object value = Parent == null || Parent.IsRootViewModel ? DataModel : DataModelPath.GetValue();
if (value is DataModel dataModel)
{
foreach (KeyValuePair<string, DataModel> kvp in dataModel.DynamicDataModels)
{
string childPath = AppendToPath(kvp.Key);
@ -230,7 +195,6 @@ namespace Artemis.UI.Shared
if (child != null)
Children.Add(child);
}
}
// Remove dynamic children that have been removed from the data model
List<DataModelVisualizationViewModel> toRemoveDynamic = Children.Where(c => !c.DataModelPath.IsValid).ToList();
@ -266,6 +230,8 @@ namespace Artemis.UI.Shared
return new DataModelPropertyViewModel(DataModel, this, dataModelPath) {Depth = depth};
if (propertyType.IsGenericEnumerable())
return new DataModelListViewModel(DataModel, this, dataModelPath) {Depth = depth};
if (propertyType == typeof(DataModelEvent) || propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(DataModelEvent<>))
return new DataModelEventViewModel(DataModel, this, dataModelPath) {Depth = depth};
// For other value types create a child view model
if (propertyType.IsClass || propertyType.IsStruct())
return new DataModelPropertiesViewModel(DataModel, this, dataModelPath) {Depth = depth};
@ -295,4 +261,14 @@ namespace Artemis.UI.Shared
#endregion
}
public class DataModelUpdateConfiguration
{
public bool CreateEventChildren { get; }
public DataModelUpdateConfiguration(bool createEventChildren)
{
CreateEventChildren = createEventChildren;
}
}
}

View File

@ -0,0 +1,29 @@
using System.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services;
namespace Artemis.UI.Shared
{
public static class DataModelWrapperExtensions
{
public static DataModelPropertiesViewModel CreateViewModel(this EventPredicateWrapperDataModel wrapper, IDataModelUIService dataModelUIService, DataModelUpdateConfiguration configuration)
{
DataModelPropertiesViewModel viewModel = new DataModelPropertiesViewModel(wrapper, null, new DataModelPath(wrapper));
viewModel.Update(dataModelUIService, configuration);
viewModel.UpdateRequested += (sender, args) => viewModel.Update(dataModelUIService, configuration);
viewModel.Children.First().IsVisualizationExpanded = true;
return viewModel;
}
public static DataModelPropertiesViewModel CreateViewModel(this ListPredicateWrapperDataModel wrapper, IDataModelUIService dataModelUIService, DataModelUpdateConfiguration configuration)
{
DataModelPropertiesViewModel viewModel = new DataModelPropertiesViewModel(wrapper, null, new DataModelPath(wrapper));
viewModel.Update(dataModelUIService, configuration);
viewModel.UpdateRequested += (sender, args) => viewModel.Update(dataModelUIService, configuration);
viewModel.Children.First().IsVisualizationExpanded = true;
return viewModel;
}
}
}

View File

@ -39,6 +39,12 @@
<TextBlock Text="{Binding PropertyDescription.Name}" ToolTip="{Binding PropertyDescription.Description}" ToolTipService.ShowOnDisabled="True" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type dataModel:DataModelEventViewModel}">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="LightningBolt" VerticalAlignment="Center" Margin="0 0 5 0" />
<TextBlock Text="{Binding PropertyDescription.Name}" ToolTip="Trigger layer on event" ToolTipService.ShowOnDisabled="True" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type dataModel:DataModelPropertyViewModel}">
<Grid>
<!-- Value description -->
@ -73,6 +79,12 @@
<TextBlock Text="{Binding PropertyDescription.Name}" ToolTip="{Binding PropertyDescription.Description}" ToolTipService.ShowOnDisabled="True" VerticalAlignment="Center"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type dataModel:DataModelEventViewModel}">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="LightningBolt" VerticalAlignment="Center" Margin="0 0 5 0" />
<TextBlock Text="{Binding PropertyDescription.Name}" ToolTip="{Binding PropertyDescription.Description}" ToolTipService.ShowOnDisabled="True" VerticalAlignment="Center"/>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type dataModel:DataModelPropertyViewModel}">
<Grid ToolTip="{Binding PropertyDescription.Description}">
<Grid.ColumnDefinitions>

View File

@ -37,8 +37,8 @@ namespace Artemis.UI.Shared.Services
viewModel.Children.Add(new DataModelPropertiesViewModel(dataModelExpansion, viewModel, new DataModelPath(dataModelExpansion)));
// Update to populate children
viewModel.Update(this);
viewModel.UpdateRequested += (sender, args) => viewModel.Update(this);
viewModel.Update(this, null);
viewModel.UpdateRequested += (sender, args) => viewModel.Update(this, null);
return viewModel;
}
@ -67,8 +67,8 @@ namespace Artemis.UI.Shared.Services
viewModel.Children.Add(new DataModelPropertiesViewModel(dataModel, viewModel, null));
// Update to populate children
viewModel.Update(this);
viewModel.UpdateRequested += (sender, args) => viewModel.Update(this);
viewModel.Update(this, null);
viewModel.UpdateRequested += (sender, args) => viewModel.Update(this, null);
return viewModel;
}
@ -220,8 +220,7 @@ namespace Artemis.UI.Shared.Services
if (initialValue != null && initialValue.GetType() != registration.SupportedType)
initialValue = Convert.ChangeType(initialValue, registration.SupportedType);
IParameter[] parameters = new IParameter[]
{
IParameter[] parameters = {
new ConstructorArgument("targetDescription", description),
new ConstructorArgument("initialValue", initialValue)
};

View File

@ -57,8 +57,7 @@ namespace Artemis.UI.Shared.Services
public async Task<bool> ShowConfirmDialog(string header, string text, string confirmText = "Confirm", string cancelText = "Cancel")
{
IParameter[] arguments =
{
IParameter[] arguments = {
new ConstructorArgument("header", header),
new ConstructorArgument("text", text),
new ConstructorArgument("confirmText", confirmText.ToUpper()),
@ -70,8 +69,7 @@ namespace Artemis.UI.Shared.Services
public async Task<bool> ShowConfirmDialogAt(string identifier, string header, string text, string confirmText = "Confirm", string cancelText = "Cancel")
{
IParameter[] arguments =
{
IParameter[] arguments = {
new ConstructorArgument("header", header),
new ConstructorArgument("text", text),
new ConstructorArgument("confirmText", confirmText.ToUpper()),

View File

@ -15,6 +15,7 @@ namespace Artemis.UI.Shared.Services
internal class ProfileEditorService : IProfileEditorService
{
private readonly ILogger _logger;
private readonly ICoreService _coreService;
private readonly IProfileService _profileService;
private readonly List<PropertyInputRegistration> _registeredPropertyEditors;
private readonly object _selectedProfileElementLock = new object();
@ -22,16 +23,23 @@ namespace Artemis.UI.Shared.Services
private TimeSpan _currentTime;
private int _pixelsPerSecond;
public ProfileEditorService(IProfileService profileService, IKernel kernel, ILogger logger)
public ProfileEditorService(IProfileService profileService, IKernel kernel, ILogger logger, ICoreService coreService)
{
_profileService = profileService;
_logger = logger;
_coreService = coreService;
_registeredPropertyEditors = new List<PropertyInputRegistration>();
Kernel = kernel;
PixelsPerSecond = 100;
}
private void CoreServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e)
{
_coreService.FrameRendered -= CoreServiceOnFrameRendered;
Execute.PostToUIThread(OnProfilePreviewUpdated);
}
public IKernel Kernel { get; }
public ReadOnlyCollection<PropertyInputRegistration> RegisteredPropertyEditors => _registeredPropertyEditors.AsReadOnly();
public Profile SelectedProfile { get; private set; }
@ -44,8 +52,8 @@ namespace Artemis.UI.Shared.Services
set
{
if (_currentTime.Equals(value)) return;
if (SelectedProfileElement != null && value > SelectedProfileElement.TimelineLength)
_currentTime = SelectedProfileElement.TimelineLength;
if (SelectedProfileElement != null && value > SelectedProfileElement.Timeline.Length)
_currentTime = SelectedProfileElement.Timeline.Length;
else
_currentTime = value;
UpdateProfilePreview();
@ -142,11 +150,11 @@ namespace Artemis.UI.Shared.Services
// Stick to the main segment for any element that is not currently selected
foreach (Folder folder in SelectedProfile.GetAllFolders())
folder.OverrideProgress(CurrentTime, folder != SelectedProfileElement);
folder.Timeline.Override(CurrentTime, folder != SelectedProfileElement && folder.Timeline.PlayMode == TimelinePlayMode.Repeat);
foreach (Layer layer in SelectedProfile.GetAllLayers())
layer.OverrideProgress(CurrentTime, layer != SelectedProfileElement);
layer.Timeline.Override(CurrentTime, layer != SelectedProfileElement && layer.Timeline.PlayMode == TimelinePlayMode.Repeat);
OnProfilePreviewUpdated();
_coreService.FrameRendered += CoreServiceOnFrameRendered;
}
public bool UndoUpdateProfile()
@ -224,24 +232,23 @@ namespace Artemis.UI.Shared.Services
if (snapToSegments)
{
// Snap to the end of the start segment
if (Math.Abs(time.TotalMilliseconds - SelectedProfileElement.StartSegmentLength.TotalMilliseconds) < tolerance.TotalMilliseconds)
return SelectedProfileElement.StartSegmentLength;
if (Math.Abs(time.TotalMilliseconds - SelectedProfileElement.Timeline.StartSegmentEndPosition.TotalMilliseconds) < tolerance.TotalMilliseconds)
return SelectedProfileElement.Timeline.StartSegmentEndPosition;
// Snap to the end of the main segment
TimeSpan mainSegmentEnd = SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength;
if (Math.Abs(time.TotalMilliseconds - mainSegmentEnd.TotalMilliseconds) < tolerance.TotalMilliseconds)
return mainSegmentEnd;
if (Math.Abs(time.TotalMilliseconds - SelectedProfileElement.Timeline.MainSegmentEndPosition.TotalMilliseconds) < tolerance.TotalMilliseconds)
return SelectedProfileElement.Timeline.MainSegmentEndPosition;
// Snap to the end of the end segment (end of the timeline)
if (Math.Abs(time.TotalMilliseconds - SelectedProfileElement.TimelineLength.TotalMilliseconds) < tolerance.TotalMilliseconds)
return SelectedProfileElement.TimelineLength;
if (Math.Abs(time.TotalMilliseconds - SelectedProfileElement.Timeline.EndSegmentEndPosition.TotalMilliseconds) < tolerance.TotalMilliseconds)
return SelectedProfileElement.Timeline.EndSegmentEndPosition;
}
if (snapToCurrentTime)
{
// Snap to the current time
if (Math.Abs(time.TotalMilliseconds - CurrentTime.TotalMilliseconds) < tolerance.TotalMilliseconds)
return SelectedProfileElement.StartSegmentLength;
return CurrentTime;
}
if (snapTimes != null)

View File

@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Cconditions_005Cpredicate/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -0,0 +1,19 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace Artemis.UI.Converters
{
public class ComparisonConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value?.Equals(parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value?.Equals(true) == true ? parameter : Binding.DoNothing;
}
}
}

View File

@ -65,10 +65,12 @@ namespace Artemis.UI.Ninject.Factories
public interface IDataModelConditionsVmFactory : IVmFactory
{
DataModelConditionGroupViewModel DataModelConditionGroupViewModel(DataModelConditionGroup dataModelConditionGroup, bool isListGroup);
DataModelConditionGroupViewModel DataModelConditionGroupViewModel(DataModelConditionGroup dataModelConditionGroup, ConditionGroupType groupType);
DataModelConditionListViewModel DataModelConditionListViewModel(DataModelConditionList dataModelConditionList);
DataModelConditionPredicateViewModel DataModelConditionPredicateViewModel(DataModelConditionPredicate dataModelConditionPredicate);
DataModelConditionEventViewModel DataModelConditionEventViewModel(DataModelConditionEvent dataModelConditionEvent);
DataModelConditionGeneralPredicateViewModel DataModelConditionGeneralPredicateViewModel(DataModelConditionGeneralPredicate dataModelConditionGeneralPredicate);
DataModelConditionListPredicateViewModel DataModelConditionListPredicateViewModel(DataModelConditionListPredicate dataModelConditionListPredicate);
DataModelConditionEventPredicateViewModel DataModelConditionEventPredicateViewModel(DataModelConditionEventPredicate dataModelConditionEventPredicate);
}
public interface ILayerPropertyVmFactory : IVmFactory

View File

@ -13,12 +13,11 @@ using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.Conditions
{
public class DataModelConditionPredicateViewModel : DataModelConditionViewModel, IDisposable
public abstract class DataModelConditionPredicateViewModel : DataModelConditionViewModel, IDisposable
{
private readonly IConditionOperatorService _conditionOperatorService;
private readonly IDataModelUIService _dataModelUIService;
private readonly IProfileEditorService _profileEditorService;
private DataModelDynamicViewModel _leftSideSelectionViewModel;
private BindableCollection<BaseConditionOperator> _operators;
private DataModelStaticViewModel _rightSideInputViewModel;
private DataModelDynamicViewModel _rightSideSelectionViewModel;
@ -42,26 +41,11 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
Operators = new BindableCollection<BaseConditionOperator>();
ShowDataModelValues = settingsService.GetSetting<bool>("ProfileEditor.ShowDataModelValues");
Initialize();
}
public DataModelConditionPredicate DataModelConditionPredicate => (DataModelConditionPredicate) Model;
public PluginSetting<bool> ShowDataModelValues { get; }
public BindableCollection<BaseConditionOperator> Operators
{
get => _operators;
set => SetAndNotify(ref _operators, value);
}
public DataModelDynamicViewModel LeftSideSelectionViewModel
{
get => _leftSideSelectionViewModel;
set => SetAndNotify(ref _leftSideSelectionViewModel, value);
}
public BaseConditionOperator SelectedOperator
{
get => _selectedOperator;
@ -81,6 +65,9 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
}
public DelegateCommand SelectOperatorCommand { get; }
public BindableCollection<BaseConditionOperator> Operators { get; }
protected SolidColorBrush LeftSideColor { get; set; }
public override void Delete()
{
@ -88,15 +75,16 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
_profileEditorService.UpdateSelectedProfileElement();
}
public void Initialize()
public virtual void Initialize()
{
LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule());
LeftSideSelectionViewModel.PropertySelected += LeftSideOnPropertySelected;
if (LeftSideColor != null)
LeftSideSelectionViewModel.ButtonBrush = LeftSideColor;
// Determine which types are currently supported
IReadOnlyCollection<DataModelVisualizationRegistration> editors = _dataModelUIService.RegisteredDataModelEditors;
_supportedInputTypes = editors.Select(e => e.SupportedType).ToList();
_supportedInputTypes.AddRange(editors.Where(e => e.CompatibleConversionTypes != null).SelectMany(e => e.CompatibleConversionTypes));
_supportedInputTypes.Add(typeof(IEnumerable<>));
_supportedInputTypes = GetSupportedInputTypes();
Update();
}
@ -105,13 +93,17 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
LeftSideSelectionViewModel.FilterTypes = _supportedInputTypes.ToArray();
LeftSideSelectionViewModel.ChangeDataModelPath(DataModelConditionPredicate.LeftPath);
Type leftSideType = LeftSideSelectionViewModel.DataModelPath?.GetPropertyType();
Type leftSideType = GetLeftSideType();
// Get the supported operators
Operators.Clear();
Operators.AddRange(_conditionOperatorService.GetConditionOperatorsForType(leftSideType ?? typeof(object), ConditionParameterSide.Left));
if (DataModelConditionPredicate.Operator == null)
DataModelConditionPredicate.UpdateOperator(Operators.FirstOrDefault(o => o.SupportsType(leftSideType ?? typeof(object), ConditionParameterSide.Left)));
DataModelConditionPredicate.UpdateOperator(Operators.FirstOrDefault());
// The core doesn't care about best matches so if there is a new preferred operator, use that instead
else if (!Operators.Contains(DataModelConditionPredicate.Operator))
DataModelConditionPredicate.UpdateOperator(Operators.FirstOrDefault(o => o.Description == DataModelConditionPredicate.Operator.Description) ?? Operators.FirstOrDefault());
SelectedOperator = DataModelConditionPredicate.Operator;
// Without a selected operator or one that supports a right side, leave the right side input empty
@ -136,25 +128,26 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
{
DisposeRightSideDynamicViewModel();
if (RightSideInputViewModel == null)
CreateRightSideInputViewModel(SelectedOperator.RightSideType);
CreateRightSideInputViewModel();
if (SelectedOperator.RightSideType.IsValueType && DataModelConditionPredicate.RightStaticValue == null)
RightSideInputViewModel.Value = SelectedOperator.RightSideType.GetDefault();
Type preferredType = DataModelConditionPredicate.GetPreferredRightSideType();
// Ensure the right static value is never null when the preferred type is a value type
if (preferredType.IsValueType && DataModelConditionPredicate.RightStaticValue == null)
RightSideInputViewModel.Value = preferredType.GetDefault();
else
RightSideInputViewModel.Value = DataModelConditionPredicate.RightStaticValue;
if (RightSideInputViewModel.TargetType != SelectedOperator.RightSideType)
RightSideInputViewModel.UpdateTargetType(SelectedOperator.RightSideType);
if (RightSideInputViewModel.TargetType != preferredType)
RightSideInputViewModel.UpdateTargetType(preferredType);
}
}
public void ApplyLeftSide()
{
if (LeftSideSelectionViewModel.DataModelPath.GetPropertyType().IsGenericEnumerable())
{
if (Parent is DataModelConditionGroupViewModel groupViewModel)
groupViewModel.ConvertToConditionList(this);
Type newType = LeftSideSelectionViewModel.DataModelPath.GetPropertyType();
bool converted = ConvertIfRequired(newType);
if (converted)
return;
}
DataModelConditionPredicate.UpdateLeftSide(LeftSideSelectionViewModel.DataModelPath);
_profileEditorService.UpdateSelectedProfileElement();
@ -187,12 +180,20 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
Update();
}
protected abstract List<Type> GetSupportedInputTypes();
protected abstract Type GetLeftSideType();
protected virtual List<DataModelPropertiesViewModel> GetExtraRightSideDataModelViewModels()
{
return null;
}
private void ExecuteSelectOperatorCommand(object context)
{
if (!(context is BaseConditionOperator DataModelConditionOperator))
if (!(context is BaseConditionOperator dataModelConditionOperator))
return;
SelectedOperator = DataModelConditionOperator;
SelectedOperator = dataModelConditionOperator;
ApplyOperator();
}
@ -222,11 +223,16 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
RightSideSelectionViewModel.DisplaySwitchButton = true;
RightSideSelectionViewModel.PropertySelected += RightSideOnPropertySelected;
RightSideSelectionViewModel.SwitchToStaticRequested += RightSideSelectionViewModelOnSwitchToStaticRequested;
List<DataModelPropertiesViewModel> extra = GetExtraRightSideDataModelViewModels();
if (extra != null)
RightSideSelectionViewModel.ExtraDataModelViewModels.AddRange(extra);
}
private void CreateRightSideInputViewModel(Type leftSideType)
private void CreateRightSideInputViewModel()
{
RightSideInputViewModel = _dataModelUIService.GetStaticInputViewModel(leftSideType, LeftSideSelectionViewModel.DataModelPath?.GetPropertyDescription());
Type preferredType = DataModelConditionPredicate.GetPreferredRightSideType();
RightSideInputViewModel = _dataModelUIService.GetStaticInputViewModel(preferredType, LeftSideSelectionViewModel.DataModelPath?.GetPropertyDescription());
RightSideInputViewModel.ButtonBrush = (SolidColorBrush) Application.Current.FindResource("PrimaryHueMidBrush");
RightSideInputViewModel.DisplaySwitchButton = true;
RightSideInputViewModel.ValueUpdated += RightSideOnValueEntered;
@ -278,7 +284,6 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
Update();
}
private void RightSideInputViewModelOnSwitchToDynamicRequested(object? sender, EventArgs e)
{
DataModelConditionPredicate.PredicateType = ProfileRightSideType.Dynamic;

View File

@ -1,10 +1,14 @@
using Artemis.Core;
using System;
using Artemis.Core;
using Artemis.UI.Shared.Input;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract
{
public abstract class DataModelConditionViewModel : Conductor<DataModelConditionViewModel>.Collection.AllActive
{
private DataModelDynamicViewModel _leftSideSelectionViewModel;
protected DataModelConditionViewModel(DataModelConditionPart model)
{
Model = model;
@ -12,6 +16,12 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract
public DataModelConditionPart Model { get; }
public DataModelDynamicViewModel LeftSideSelectionViewModel
{
get => _leftSideSelectionViewModel;
set => SetAndNotify(ref _leftSideSelectionViewModel, value);
}
public abstract void Update();
public virtual void Delete()
@ -19,5 +29,29 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract
Model.Parent.RemoveChild(Model);
((DataModelConditionViewModel) Parent).Update();
}
protected bool ConvertIfRequired(Type newType)
{
if (newType == null)
return false;
if (!(Parent is DataModelConditionGroupViewModel groupViewModel))
return false;
// List
if (newType.IsGenericEnumerable())
{
if (this is DataModelConditionListViewModel)
return false;
groupViewModel.ConvertToConditionList(this);
return true;
}
// Predicate
if (this is DataModelConditionPredicateViewModel)
return false;
groupViewModel.ConvertToPredicate(this);
return true;
}
}
}

View File

@ -0,0 +1,63 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.Conditions.DataModelConditionEventView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.Conditions"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Artemis.UI;component/ResourceDictionaries/DataModelConditions.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Grid.Row="0"
Grid.Column="0"
ToolTip="Delete the event trigger"
Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Left"
Foreground="#E74C4C"
Width="25"
Height="25"
Command="{s:Action Delete}">
<materialDesign:PackIcon Kind="Close" Width="18" Height="18" />
</Button>
<!-- Left side, the list this predicate is targeting -->
<ContentControl Grid.Row="0"
Grid.Column="1"
s:View.Model="{Binding LeftSideSelectionViewModel}"
VerticalContentAlignment="Stretch"
HorizontalContentAlignment="Stretch"
IsTabStop="False" />
<TextBlock Grid.Row="0"
Grid.Column="2"
VerticalAlignment="Center"
Margin="5 0 0 0">
triggered
</TextBlock>
<ItemsControl Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" ItemsSource="{Binding Items}" Margin="0 3 0 0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl s:View.Model="{Binding}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" IsTabStop="False" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</UserControl>

View File

@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Media;
using Artemis.Core;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
namespace Artemis.UI.Screens.ProfileEditor.Conditions
{
public class DataModelConditionEventViewModel : DataModelConditionViewModel, IDisposable
{
private readonly IDataModelConditionsVmFactory _dataModelConditionsVmFactory;
private readonly IDataModelUIService _dataModelUIService;
private readonly IProfileEditorService _profileEditorService;
public DataModelConditionEventViewModel(DataModelConditionEvent dataModelConditionEvent,
IProfileEditorService profileEditorService,
IDataModelUIService dataModelUIService,
IDataModelConditionsVmFactory dataModelConditionsVmFactory) : base(dataModelConditionEvent)
{
_profileEditorService = profileEditorService;
_dataModelUIService = dataModelUIService;
_dataModelConditionsVmFactory = dataModelConditionsVmFactory;
}
public DataModelConditionEvent DataModelConditionEvent => (DataModelConditionEvent) Model;
public void Initialize()
{
LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule());
LeftSideSelectionViewModel.PropertySelected += LeftSideSelectionViewModelOnPropertySelected;
LeftSideSelectionViewModel.LoadEventChildren = false;
IReadOnlyCollection<DataModelVisualizationRegistration> editors = _dataModelUIService.RegisteredDataModelEditors;
List<Type> supportedInputTypes = new List<Type> {typeof(DataModelEvent), typeof(DataModelEvent<>)};
LeftSideSelectionViewModel.FilterTypes = supportedInputTypes.ToArray();
LeftSideSelectionViewModel.ButtonBrush = new SolidColorBrush(Color.FromRgb(185, 164, 10));
LeftSideSelectionViewModel.Placeholder = "Select an event";
Update();
}
public override void Update()
{
LeftSideSelectionViewModel.ChangeDataModelPath(DataModelConditionEvent.EventPath);
// Remove VMs of effects no longer applied on the layer
Items.RemoveRange(Items.Where(c => !DataModelConditionEvent.Children.Contains(c.Model)).ToList());
if (DataModelConditionEvent.EventPath == null || !DataModelConditionEvent.EventPath.IsValid)
return;
List<DataModelConditionViewModel> viewModels = new List<DataModelConditionViewModel>();
foreach (DataModelConditionPart childModel in Model.Children)
{
if (Items.Any(c => c.Model == childModel))
continue;
if (!(childModel is DataModelConditionGroup dataModelConditionGroup))
continue;
DataModelConditionGroupViewModel viewModel = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(dataModelConditionGroup, ConditionGroupType.Event);
viewModel.IsRootGroup = true;
viewModels.Add(viewModel);
}
if (viewModels.Any())
Items.AddRange(viewModels);
foreach (DataModelConditionViewModel childViewModel in Items)
childViewModel.Update();
}
public void ApplyEvent()
{
DataModelConditionEvent.UpdateEvent(LeftSideSelectionViewModel.DataModelPath);
_profileEditorService.UpdateSelectedProfileElement();
Update();
}
protected override void OnInitialActivate()
{
Initialize();
base.OnInitialActivate();
}
#region Event handlers
private void LeftSideSelectionViewModelOnPropertySelected(object? sender, DataModelInputDynamicEventArgs e)
{
ApplyEvent();
}
#endregion
#region IDisposable
public void Dispose()
{
LeftSideSelectionViewModel.Dispose();
LeftSideSelectionViewModel.PropertySelected -= LeftSideSelectionViewModelOnPropertySelected;
}
#endregion
}
}

View File

@ -17,6 +17,7 @@
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Artemis.UI;component/ResourceDictionaries/DataModelConditions.xaml" />
</ResourceDictionary.MergedDictionaries>
<Converters:InverseBooleanConverter x:Key="InverseBooleanConverter" />
<utilities:BindingProxy x:Key="DataContextProxy" Data="{Binding}" />
</ResourceDictionary>
</UserControl.Resources>
@ -45,13 +46,24 @@
</Button>
<Button Grid.Row="0"
Grid.Column="1"
ToolTip="Change the operator of the group, determining which conditions should match"
Style="{StaticResource DataModelConditionButtonLeftClickMenu}"
Background="#E74C4C"
BorderBrush="#E74C4C"
Margin="3 1"
Content="{Binding SelectedBooleanOperator}"
Visibility="{Binding DisplayBooleanOperator, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
Visibility="{Binding DisplayBooleanOperator, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"
IsEnabled="{Binding IsEventGroup, Converter={StaticResource InverseBooleanConverter}}"
ToolTipService.ShowOnDisabled="True">
<Button.ToolTip>
<StackPanel>
<TextBlock Visibility="{Binding IsEventGroup, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}">
Change the operator of the group, determining which conditions should match
</TextBlock>
<TextBlock Visibility="{Binding IsEventGroup, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
The operator of the root group cannot be changed for event-based conditions
</TextBlock>
</StackPanel>
</Button.ToolTip>
<Button.ContextMenu>
<ContextMenu>
<MenuItem Header="And"
@ -98,12 +110,20 @@
</Button.Style>
<Button.ContextMenu>
<ContextMenu>
<MenuItem Header="Add condition" ToolTip="A condition that compares with another value" Command="{s:Action AddCondition}">
<MenuItem Header="Add condition" ToolTip="A condition that evaluates the state of a property in the data model" Command="{s:Action AddCondition}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Equal" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Add group" ToolTip="A group can contain conditions and other groups" Command="{s:Action AddGroup}">
<MenuItem Header="Add event condition"
ToolTip="An event condition that responds to data model events"
Command="{s:Action AddEventCondition}"
Visibility="{Binding Data.CanAddEventCondition, Source={StaticResource DataContextProxy}, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="LightningBolt" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Add group" ToolTip="A group for conditions and other groups" Command="{s:Action AddGroup}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="CodeParentheses" />
</MenuItem.Icon>
@ -113,7 +133,7 @@
<materialDesign:PackIcon Kind="Add" Width="18" Height="18" />
</Button>
<ItemsControl Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" ItemsSource="{Binding Items, IsAsync=True}">
<ItemsControl Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" ItemsSource="{Binding Items, IsAsync=True}" Margin="0 3 0 0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl s:View.Model="{Binding}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" IsTabStop="False" />

View File

@ -19,14 +19,15 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
private readonly IProfileEditorService _profileEditorService;
private bool _isInitialized;
private bool _isRootGroup;
private bool _isEventGroup;
public DataModelConditionGroupViewModel(DataModelConditionGroup dataModelConditionGroup,
bool isListGroup,
ConditionGroupType groupType,
IProfileEditorService profileEditorService,
IDataModelConditionsVmFactory dataModelConditionsVmFactory)
: base(dataModelConditionGroup)
{
IsListGroup = isListGroup;
GroupType = groupType;
_profileEditorService = profileEditorService;
_dataModelConditionsVmFactory = dataModelConditionsVmFactory;
@ -39,13 +40,25 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
});
}
public bool IsListGroup { get; }
public ConditionGroupType GroupType { get; }
public DataModelConditionGroup DataModelConditionGroup => (DataModelConditionGroup) Model;
public bool IsRootGroup
{
get => _isRootGroup;
set => SetAndNotify(ref _isRootGroup, value);
set
{
if (!SetAndNotify(ref _isRootGroup, value)) return;
NotifyOfPropertyChange(nameof(CanAddEventCondition));
}
}
public bool CanAddEventCondition => IsRootGroup && GroupType == ConditionGroupType.General;
public bool IsEventGroup
{
get => _isEventGroup;
set => SetAndNotify(ref _isEventGroup, value);
}
public bool IsInitialized
@ -68,10 +81,37 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
public void AddCondition()
{
if (!IsListGroup)
DataModelConditionGroup.AddChild(new DataModelConditionPredicate(DataModelConditionGroup, ProfileRightSideType.Dynamic));
else
DataModelConditionGroup.AddChild(new DataModelConditionListPredicate(DataModelConditionGroup, ProfileRightSideType.Dynamic));
switch (GroupType)
{
case ConditionGroupType.General:
DataModelConditionGroup.AddChild(new DataModelConditionGeneralPredicate(DataModelConditionGroup, ProfileRightSideType.Dynamic));
break;
case ConditionGroupType.List:
DataModelConditionGroup.AddChild(new DataModelConditionListPredicate(DataModelConditionGroup, ProfileRightSideType.Dynamic));
break;
case ConditionGroupType.Event:
DataModelConditionGroup.AddChild(new DataModelConditionEventPredicate(DataModelConditionGroup, ProfileRightSideType.Dynamic));
break;
default:
throw new ArgumentOutOfRangeException();
}
Update();
_profileEditorService.UpdateSelectedProfileElement();
}
public void AddEventCondition()
{
if (!CanAddEventCondition)
return;
// Find a good spot for the event, behind the last existing event
int index = 0;
DataModelConditionPart existing = DataModelConditionGroup.Children.LastOrDefault(c => c is DataModelConditionEvent);
if (existing != null)
index = DataModelConditionGroup.Children.IndexOf(existing) + 1;
DataModelConditionGroup.AddChild(new DataModelConditionEvent(DataModelConditionGroup), index);
Update();
_profileEditorService.UpdateSelectedProfileElement();
@ -88,11 +128,9 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
public override void Update()
{
NotifyOfPropertyChange(nameof(SelectedBooleanOperator));
// Remove VMs of effects no longer applied on the layer
Items.RemoveRange(Items.Where(c => !DataModelConditionGroup.Children.Contains(c.Model)).ToList());
List<DataModelConditionViewModel> viewModels = new List<DataModelConditionViewModel>();
foreach (DataModelConditionPart childModel in Model.Children)
{
if (Items.Any(c => c.Model == childModel))
@ -100,39 +138,43 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
switch (childModel)
{
case DataModelConditionGroup DataModelConditionGroup:
viewModels.Add(_dataModelConditionsVmFactory.DataModelConditionGroupViewModel(DataModelConditionGroup, IsListGroup));
case DataModelConditionGroup dataModelConditionGroup:
Items.Add(_dataModelConditionsVmFactory.DataModelConditionGroupViewModel(dataModelConditionGroup, GroupType));
break;
case DataModelConditionList DataModelConditionListPredicate:
viewModels.Add(_dataModelConditionsVmFactory.DataModelConditionListViewModel(DataModelConditionListPredicate));
case DataModelConditionList dataModelConditionList:
Items.Add(_dataModelConditionsVmFactory.DataModelConditionListViewModel(dataModelConditionList));
break;
case DataModelConditionPredicate DataModelConditionPredicate:
if (!IsListGroup)
viewModels.Add(_dataModelConditionsVmFactory.DataModelConditionPredicateViewModel(DataModelConditionPredicate));
case DataModelConditionEvent dataModelConditionEvent:
Items.Add(_dataModelConditionsVmFactory.DataModelConditionEventViewModel(dataModelConditionEvent));
break;
case DataModelConditionListPredicate DataModelConditionListPredicate:
if (IsListGroup)
viewModels.Add(_dataModelConditionsVmFactory.DataModelConditionListPredicateViewModel(DataModelConditionListPredicate));
case DataModelConditionGeneralPredicate dataModelConditionGeneralPredicate:
Items.Add(_dataModelConditionsVmFactory.DataModelConditionGeneralPredicateViewModel(dataModelConditionGeneralPredicate));
break;
case DataModelConditionListPredicate dataModelConditionListPredicate:
Items.Add(_dataModelConditionsVmFactory.DataModelConditionListPredicateViewModel(dataModelConditionListPredicate));
break;
case DataModelConditionEventPredicate dataModelConditionEventPredicate:
Items.Add(_dataModelConditionsVmFactory.DataModelConditionEventPredicateViewModel(dataModelConditionEventPredicate));
break;
}
}
if (viewModels.Any())
Items.AddRange(viewModels);
// Ensure the items are in the same order as on the model
((BindableCollection<DataModelConditionViewModel>) Items).Sort(i => Model.Children.IndexOf(i.Model));
foreach (DataModelConditionViewModel childViewModel in Items)
childViewModel.Update();
if (IsRootGroup && Parent is DisplayConditionsViewModel displayConditionsViewModel)
displayConditionsViewModel.DisplayStartHint = !Items.Any();
IsEventGroup = Items.Any(i => i is DataModelConditionEventViewModel);
if (IsEventGroup)
{
if (DataModelConditionGroup.BooleanOperator != BooleanOperator.And)
SelectBooleanOperator("And");
}
OnUpdated();
}
public void ConvertToConditionList(DataModelConditionPredicateViewModel predicateViewModel)
public void ConvertToConditionList(DataModelConditionViewModel predicateViewModel)
{
// Store the old index and remove the old predicate
int index = DataModelConditionGroup.Children.IndexOf(predicateViewModel.Model);
@ -147,15 +189,15 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
Update();
}
public void ConvertToPredicate(DataModelConditionListViewModel listViewModel)
public void ConvertToPredicate(DataModelConditionViewModel listViewModel)
{
// Store the old index and remove the old predicate
int index = DataModelConditionGroup.Children.IndexOf(listViewModel.Model);
DataModelConditionGroup.RemoveChild(listViewModel.Model);
// Insert a list in the same position
DataModelConditionPredicate predicate = new DataModelConditionPredicate(DataModelConditionGroup, ProfileRightSideType.Dynamic);
predicate.UpdateLeftSide(listViewModel.TargetSelectionViewModel.DataModelPath);
DataModelConditionGeneralPredicate predicate = new DataModelConditionGeneralPredicate(DataModelConditionGroup, ProfileRightSideType.Dynamic);
predicate.UpdateLeftSide(listViewModel.LeftSideSelectionViewModel.DataModelPath);
DataModelConditionGroup.AddChild(predicate, index);
// Update to switch the VMs
@ -169,4 +211,11 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
Updated?.Invoke(this, EventArgs.Empty);
}
}
public enum ConditionGroupType
{
General,
List,
Event
}
}

View File

@ -1,320 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Exceptions;
using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Input;
using Artemis.UI.Shared.Services;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.Conditions
{
public class DataModelConditionListPredicateViewModel : DataModelConditionViewModel, IDisposable
{
private readonly IConditionOperatorService _conditionOperatorService;
private readonly IDataModelUIService _dataModelUIService;
private readonly IProfileEditorService _profileEditorService;
private bool _isPrimitiveList;
private DataModelDynamicViewModel _leftSideSelectionViewModel;
private BindableCollection<BaseConditionOperator> _operators;
private DataModelStaticViewModel _rightSideInputViewModel;
private DataModelDynamicViewModel _rightSideSelectionViewModel;
private BaseConditionOperator _selectedOperator;
private List<Type> _supportedInputTypes;
public DataModelConditionListPredicateViewModel(
DataModelConditionListPredicate dataModelConditionListPredicate,
IProfileEditorService profileEditorService,
IDataModelUIService dataModelUIService,
IConditionOperatorService conditionOperatorService) : base(dataModelConditionListPredicate)
{
_profileEditorService = profileEditorService;
_dataModelUIService = dataModelUIService;
_conditionOperatorService = conditionOperatorService;
_supportedInputTypes = new List<Type>();
SelectOperatorCommand = new DelegateCommand(ExecuteSelectOperatorCommand);
Operators = new BindableCollection<BaseConditionOperator>();
Initialize();
}
public DataModelConditionListPredicate DataModelConditionListPredicate => (DataModelConditionListPredicate) Model;
public BindableCollection<BaseConditionOperator> Operators
{
get => _operators;
set => SetAndNotify(ref _operators, value);
}
public DataModelDynamicViewModel LeftSideSelectionViewModel
{
get => _leftSideSelectionViewModel;
set => SetAndNotify(ref _leftSideSelectionViewModel, value);
}
public DataModelDynamicViewModel RightSideSelectionViewModel
{
get => _rightSideSelectionViewModel;
set => SetAndNotify(ref _rightSideSelectionViewModel, value);
}
public DataModelStaticViewModel RightSideInputViewModel
{
get => _rightSideInputViewModel;
set => SetAndNotify(ref _rightSideInputViewModel, value);
}
public BaseConditionOperator SelectedOperator
{
get => _selectedOperator;
set => SetAndNotify(ref _selectedOperator, value);
}
public DelegateCommand SelectOperatorCommand { get; }
public override void Delete()
{
base.Delete();
_profileEditorService.UpdateSelectedProfileElement();
}
public void Initialize()
{
DataModelPropertiesViewModel listDataModel = GetListDataModel();
if (listDataModel.Children.Count == 1 && listDataModel.Children.First() is DataModelListPropertyViewModel)
_isPrimitiveList = true;
else
_isPrimitiveList = false;
// Get the data models
if (!_isPrimitiveList)
{
LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule());
LeftSideSelectionViewModel.ChangeDataModel(listDataModel);
LeftSideSelectionViewModel.PropertySelected += LeftSideOnPropertySelected;
}
// Determine which types are currently supported
IReadOnlyCollection<DataModelVisualizationRegistration> editors = _dataModelUIService.RegisteredDataModelEditors;
_supportedInputTypes = editors.Select(e => e.SupportedType).ToList();
_supportedInputTypes.AddRange(editors.Where(e => e.CompatibleConversionTypes != null).SelectMany(e => e.CompatibleConversionTypes));
Update();
}
public override void Update()
{
Guid? listDataModelGuid = DataModelConditionListPredicate.DataModelConditionList.ListPath?.DataModelGuid;
if (listDataModelGuid == null)
return;
if (!_isPrimitiveList)
{
// Lists use a different color
LeftSideSelectionViewModel.ButtonBrush = new SolidColorBrush(Color.FromRgb(71, 108, 188));
LeftSideSelectionViewModel.FilterTypes = _supportedInputTypes.ToArray();
LeftSideSelectionViewModel.ChangeDataModelPath(DataModelConditionListPredicate.LeftPath);
}
Type leftSideType = _isPrimitiveList
? DataModelConditionListPredicate.DataModelConditionList.ListType
: LeftSideSelectionViewModel.DataModelPath?.GetPropertyType();
// Get the supported operators
Operators.Clear();
Operators.AddRange(_conditionOperatorService.GetConditionOperatorsForType(leftSideType ?? typeof(object), ConditionParameterSide.Left));
if (DataModelConditionListPredicate.Operator == null)
DataModelConditionListPredicate.UpdateOperator(Operators.FirstOrDefault(o => o.SupportsType(leftSideType ?? typeof(object), ConditionParameterSide.Left)));
SelectedOperator = DataModelConditionListPredicate.Operator;
// Without a selected operator or one that supports a right side, leave the right side input empty
if (SelectedOperator == null || SelectedOperator.RightSideType == null)
{
DisposeRightSideStaticViewModel();
DisposeRightSideDynamicViewModel();
return;
}
// Ensure the right side has the proper VM
if (DataModelConditionListPredicate.PredicateType == ProfileRightSideType.Dynamic)
{
DisposeRightSideStaticViewModel();
if (RightSideSelectionViewModel == null)
CreateRightSideSelectionViewModel();
RightSideSelectionViewModel.ChangeDataModelPath(DataModelConditionListPredicate.RightPath);
RightSideSelectionViewModel.FilterTypes = new[] {SelectedOperator.RightSideType};
}
else
{
DisposeRightSideDynamicViewModel();
if (RightSideInputViewModel == null)
CreateRightSideInputViewModel(SelectedOperator.RightSideType);
if (SelectedOperator.RightSideType.IsValueType && DataModelConditionListPredicate.RightStaticValue == null)
RightSideInputViewModel.Value = SelectedOperator.RightSideType.GetDefault();
else
RightSideInputViewModel.Value = DataModelConditionListPredicate.RightStaticValue;
if (RightSideInputViewModel.TargetType != SelectedOperator.RightSideType)
RightSideInputViewModel.UpdateTargetType(SelectedOperator.RightSideType);
}
}
public void ApplyLeftSide()
{
DataModelConditionListPredicate.UpdateLeftSide(LeftSideSelectionViewModel.DataModelPath);
_profileEditorService.UpdateSelectedProfileElement();
SelectedOperator = DataModelConditionListPredicate.Operator;
Update();
}
public void ApplyRightSideDynamic()
{
DataModelConditionListPredicate.UpdateRightSideDynamic(RightSideSelectionViewModel.DataModelPath);
_profileEditorService.UpdateSelectedProfileElement();
Update();
}
public void ApplyRightSideStatic(object value)
{
DataModelConditionListPredicate.UpdateRightSideStatic(value);
_profileEditorService.UpdateSelectedProfileElement();
Update();
}
public void ApplyOperator()
{
DataModelConditionListPredicate.UpdateOperator(SelectedOperator);
_profileEditorService.UpdateSelectedProfileElement();
Update();
}
private DataModelPropertiesViewModel GetListDataModel()
{
if (DataModelConditionListPredicate.DataModelConditionList.ListPath?.DataModelGuid == null)
throw new ArtemisUIException("Failed to retrieve the list data model VM for this list predicate because it has no list path");
DataModelPropertiesViewModel dataModel = _dataModelUIService.GetPluginDataModelVisualization(_profileEditorService.GetCurrentModule(), true);
DataModelListViewModel listDataModel = (DataModelListViewModel) dataModel.GetChildByPath(
DataModelConditionListPredicate.DataModelConditionList.ListPath.DataModelGuid.Value,
DataModelConditionListPredicate.DataModelConditionList.ListPath.Path
);
return listDataModel.GetListTypeViewModel(_dataModelUIService);
}
private void ExecuteSelectOperatorCommand(object context)
{
if (!(context is BaseConditionOperator dataModelConditionOperator))
return;
SelectedOperator = dataModelConditionOperator;
ApplyOperator();
}
#region IDisposable
public void Dispose()
{
if (!_isPrimitiveList)
{
LeftSideSelectionViewModel.PropertySelected -= LeftSideOnPropertySelected;
LeftSideSelectionViewModel.Dispose();
}
DisposeRightSideStaticViewModel();
DisposeRightSideDynamicViewModel();
}
#endregion
#region Event handlers
private void LeftSideOnPropertySelected(object sender, DataModelInputDynamicEventArgs e)
{
ApplyLeftSide();
}
private void RightSideOnPropertySelected(object sender, DataModelInputDynamicEventArgs e)
{
ApplyRightSideDynamic();
}
private void RightSideOnValueEntered(object sender, DataModelInputStaticEventArgs e)
{
ApplyRightSideStatic(e.Value);
}
private void RightSideSelectionViewModelOnSwitchToStaticRequested(object sender, EventArgs e)
{
DataModelConditionListPredicate.PredicateType = ProfileRightSideType.Static;
Update();
}
private void RightSideInputViewModelOnSwitchToDynamicRequested(object? sender, EventArgs e)
{
DataModelConditionListPredicate.PredicateType = ProfileRightSideType.Dynamic;
Update();
}
#endregion
#region View model management
private void CreateRightSideSelectionViewModel()
{
RightSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule());
RightSideSelectionViewModel.ButtonBrush = (SolidColorBrush) Application.Current.FindResource("PrimaryHueMidBrush");
RightSideSelectionViewModel.DisplaySwitchButton = true;
RightSideSelectionViewModel.PropertySelected += RightSideOnPropertySelected;
RightSideSelectionViewModel.SwitchToStaticRequested += RightSideSelectionViewModelOnSwitchToStaticRequested;
// Add an extra data model to the selection VM to allow self-referencing the current item
// The safe cast prevents adding this extra VM on primitive lists where they serve no purpose
if (GetListDataModel()?.Children?.FirstOrDefault() is DataModelPropertiesViewModel listValue)
RightSideSelectionViewModel.ExtraDataModelViewModels.Add(listValue);
}
private void CreateRightSideInputViewModel(Type leftSideType)
{
RightSideInputViewModel = _dataModelUIService.GetStaticInputViewModel(leftSideType, LeftSideSelectionViewModel.DataModelPath?.GetPropertyDescription());
RightSideInputViewModel.ButtonBrush = (SolidColorBrush) Application.Current.FindResource("PrimaryHueMidBrush");
RightSideInputViewModel.DisplaySwitchButton = true;
RightSideInputViewModel.ValueUpdated += RightSideOnValueEntered;
RightSideInputViewModel.SwitchToDynamicRequested += RightSideInputViewModelOnSwitchToDynamicRequested;
}
private void DisposeRightSideStaticViewModel()
{
if (RightSideInputViewModel == null)
return;
RightSideInputViewModel.ValueUpdated -= RightSideOnValueEntered;
RightSideInputViewModel.SwitchToDynamicRequested -= RightSideInputViewModelOnSwitchToDynamicRequested;
RightSideInputViewModel.Dispose();
RightSideInputViewModel = null;
}
private void DisposeRightSideDynamicViewModel()
{
if (RightSideSelectionViewModel == null)
return;
RightSideSelectionViewModel.PropertySelected -= RightSideOnPropertySelected;
RightSideSelectionViewModel.SwitchToStaticRequested -= RightSideSelectionViewModelOnSwitchToStaticRequested;
RightSideSelectionViewModel.Dispose();
RightSideSelectionViewModel = null;
}
#endregion
}
}

View File

@ -16,7 +16,7 @@
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid Margin="0 3">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
@ -41,7 +41,7 @@
<!-- Left side, the list this predicate is targeting -->
<ContentControl Grid.Row="0"
Grid.Column="1"
s:View.Model="{Binding TargetSelectionViewModel}"
s:View.Model="{Binding LeftSideSelectionViewModel}"
VerticalContentAlignment="Stretch"
HorizontalContentAlignment="Stretch"
IsTabStop="False" />

View File

@ -1,5 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Media;
@ -7,7 +6,6 @@ using Artemis.Core;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Input;
using Artemis.UI.Shared.Services;
using Humanizer;
@ -18,7 +16,6 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
private readonly IDataModelConditionsVmFactory _dataModelConditionsVmFactory;
private readonly IDataModelUIService _dataModelUIService;
private readonly IProfileEditorService _profileEditorService;
private DataModelDynamicViewModel _targetSelectionViewModel;
public DataModelConditionListViewModel(
DataModelConditionList dataModelConditionList,
@ -29,26 +26,12 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
_profileEditorService = profileEditorService;
_dataModelUIService = dataModelUIService;
_dataModelConditionsVmFactory = dataModelConditionsVmFactory;
Initialize();
}
public DataModelDynamicViewModel TargetSelectionViewModel
{
get => _targetSelectionViewModel;
set => SetAndNotify(ref _targetSelectionViewModel, value);
}
public DataModelConditionList DataModelConditionList => (DataModelConditionList) Model;
public string SelectedListOperator => DataModelConditionList.ListOperator.Humanize();
public void Dispose()
{
TargetSelectionViewModel.Dispose();
TargetSelectionViewModel.PropertySelected -= TargetSelectionViewModelOnPropertySelected;
}
public void SelectListOperator(string type)
{
ListOperator enumValue = Enum.Parse<ListOperator>(type);
@ -60,7 +43,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
public void AddCondition()
{
DataModelConditionList.AddChild(new DataModelConditionPredicate(DataModelConditionList, ProfileRightSideType.Dynamic));
DataModelConditionList.AddChild(new DataModelConditionGeneralPredicate(DataModelConditionList, ProfileRightSideType.Dynamic));
Update();
_profileEditorService.UpdateSelectedProfileElement();
@ -82,30 +65,29 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
public void Initialize()
{
TargetSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule());
TargetSelectionViewModel.PropertySelected += TargetSelectionViewModelOnPropertySelected;
LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule());
LeftSideSelectionViewModel.PropertySelected += LeftSideSelectionViewModelOnPropertySelected;
IReadOnlyCollection<DataModelVisualizationRegistration> editors = _dataModelUIService.RegisteredDataModelEditors;
List<Type> supportedInputTypes = editors.Select(e => e.SupportedType).ToList();
supportedInputTypes.AddRange(editors.Where(e => e.CompatibleConversionTypes != null).SelectMany(e => e.CompatibleConversionTypes));
supportedInputTypes.Add(typeof(IEnumerable<>));
TargetSelectionViewModel.FilterTypes = supportedInputTypes.ToArray();
TargetSelectionViewModel.ButtonBrush = new SolidColorBrush(Color.FromRgb(71, 108, 188));
TargetSelectionViewModel.Placeholder = "Select a list";
LeftSideSelectionViewModel.FilterTypes = supportedInputTypes.ToArray();
LeftSideSelectionViewModel.ButtonBrush = new SolidColorBrush(Color.FromRgb(71, 108, 188));
LeftSideSelectionViewModel.Placeholder = "Select a list";
Update();
}
public void ApplyList()
{
if (!TargetSelectionViewModel.DataModelPath.GetPropertyType().IsGenericEnumerable())
{
if (Parent is DataModelConditionGroupViewModel groupViewModel)
groupViewModel.ConvertToPredicate(this);
Type newType = LeftSideSelectionViewModel.DataModelPath.GetPropertyType();
bool converted = ConvertIfRequired(newType);
if (converted)
return;
}
DataModelConditionList.UpdateList(TargetSelectionViewModel.DataModelPath);
DataModelConditionList.UpdateList(LeftSideSelectionViewModel.DataModelPath);
_profileEditorService.UpdateSelectedProfileElement();
Update();
@ -113,7 +95,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
public override void Update()
{
TargetSelectionViewModel.ChangeDataModelPath(DataModelConditionList.ListPath);
LeftSideSelectionViewModel.ChangeDataModelPath(DataModelConditionList.ListPath);
NotifyOfPropertyChange(nameof(SelectedListOperator));
// Remove VMs of effects no longer applied on the layer
@ -130,7 +112,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
if (!(childModel is DataModelConditionGroup dataModelConditionGroup))
continue;
DataModelConditionGroupViewModel viewModel = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(dataModelConditionGroup, true);
DataModelConditionGroupViewModel viewModel = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(dataModelConditionGroup, ConditionGroupType.List);
viewModel.IsRootGroup = true;
viewModels.Add(viewModel);
}
@ -142,9 +124,21 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
childViewModel.Update();
}
private void TargetSelectionViewModelOnPropertySelected(object? sender, DataModelInputDynamicEventArgs e)
protected override void OnInitialActivate()
{
Initialize();
base.OnInitialActivate();
}
private void LeftSideSelectionViewModelOnPropertySelected(object? sender, DataModelInputDynamicEventArgs e)
{
ApplyList();
}
public void Dispose()
{
LeftSideSelectionViewModel.Dispose();
LeftSideSelectionViewModel.PropertySelected -= LeftSideSelectionViewModelOnPropertySelected;
}
}
}

View File

@ -0,0 +1,94 @@
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:utilities="clr-namespace:Artemis.UI.Utilities"
xmlns:DataModelConditions="clr-namespace:Artemis.UI.Screens.ProfileEditor.Conditions"
x:Class="Artemis.UI.Screens.ProfileEditor.Conditions.DataModelConditionEventPredicateView"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance IsDesignTimeCreatable=False, Type={x:Type DataModelConditions:DataModelConditionEventPredicateViewModel}}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Artemis.UI;component/ResourceDictionaries/DataModelConditions.xaml" />
<ResourceDictionary>
<utilities:BindingProxy x:Key="DataContextProxy" Data="{Binding}" />
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid Margin="0 0 0 3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button Grid.Row="0"
Grid.Column="0"
ToolTip="Delete the predicate"
Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Left"
Foreground="#E74C4C"
Width="25"
Height="25"
Command="{s:Action Delete}">
<materialDesign:PackIcon Kind="Close" Width="18" Height="18" />
</Button>
<!-- Left side -->
<ContentControl Grid.Row="0"
Grid.Column="1"
s:View.Model="{Binding LeftSideSelectionViewModel}"
VerticalContentAlignment="Stretch"
HorizontalContentAlignment="Stretch"
IsTabStop="False" />
<!-- Operator -->
<Button Grid.Row="0"
Grid.Column="2"
Style="{StaticResource DataModelConditionButtonLeftClickMenu}"
Background="#7B7B7B"
BorderBrush="#7B7B7B"
Content="{Binding SelectedOperator.Description}"
Click="PropertyButton_OnClick">
<Button.ContextMenu>
<ContextMenu ItemsSource="{Binding Operators}">
<ContextMenu.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="{Binding Icon}" VerticalAlignment="Center" Margin="0 0 15 0" />
<TextBlock Text="{Binding Description}" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
</ContextMenu.ItemTemplate>
<ContextMenu.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource MaterialDesignMenuItem}">
<Setter Property="Command" Value="{Binding Data.SelectOperatorCommand, Source={StaticResource DataContextProxy}}" />
<Setter Property="CommandParameter" Value="{Binding}" />
<Setter Property="CommandTarget" Value="{Binding}" />
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Button.ContextMenu>
</Button>
<!-- Right side, either a selection or an input -->
<ContentControl Grid.Row="0"
Grid.Column="3"
s:View.Model="{Binding RightSideSelectionViewModel}"
VerticalContentAlignment="Stretch"
HorizontalContentAlignment="Stretch"
IsTabStop="False" />
<ContentControl Grid.Row="0"
Grid.Column="3"
s:View.Model="{Binding RightSideInputViewModel}"
VerticalContentAlignment="Stretch"
HorizontalContentAlignment="Stretch"
IsTabStop="False" />
</Grid>
</UserControl>

View File

@ -4,11 +4,11 @@ using System.Windows.Controls;
namespace Artemis.UI.Screens.ProfileEditor.Conditions
{
/// <summary>
/// Interaction logic for DataModelConditionPredicateView.xaml
/// Interaction logic for DataModelConditionEventPredicateView.xaml
/// </summary>
public partial class DataModelConditionPredicateView : UserControl
public partial class DataModelConditionEventPredicateView : UserControl
{
public DataModelConditionPredicateView()
public DataModelConditionEventPredicateView()
{
InitializeComponent();
}

View File

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Media;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
namespace Artemis.UI.Screens.ProfileEditor.Conditions
{
public class DataModelConditionEventPredicateViewModel : DataModelConditionPredicateViewModel
{
private readonly IDataModelUIService _dataModelUIService;
public DataModelConditionEventPredicateViewModel(DataModelConditionEventPredicate dataModelConditionEventPredicate,
IProfileEditorService profileEditorService,
IDataModelUIService dataModelUIService,
IConditionOperatorService conditionOperatorService,
ISettingsService settingsService)
: base(dataModelConditionEventPredicate, profileEditorService, dataModelUIService, conditionOperatorService, settingsService)
{
_dataModelUIService = dataModelUIService;
LeftSideColor = new SolidColorBrush(Color.FromRgb(185, 164, 10));
}
public DataModelConditionEventPredicate DataModelConditionEventPredicate => (DataModelConditionEventPredicate) Model;
public override void Initialize()
{
base.Initialize();
DataModelPropertiesViewModel eventDataModel = GetEventDataModel();
LeftSideSelectionViewModel.ChangeDataModel(eventDataModel);
}
protected override void OnInitialActivate()
{
base.OnInitialActivate();
Initialize();
}
protected override List<Type> GetSupportedInputTypes()
{
IReadOnlyCollection<DataModelVisualizationRegistration> editors = _dataModelUIService.RegisteredDataModelEditors;
List<Type> supportedInputTypes = editors.Select(e => e.SupportedType).ToList();
supportedInputTypes.AddRange(editors.Where(e => e.CompatibleConversionTypes != null).SelectMany(e => e.CompatibleConversionTypes));
return supportedInputTypes;
}
protected override Type GetLeftSideType()
{
return LeftSideSelectionViewModel.DataModelPath?.GetPropertyType();
}
protected override List<DataModelPropertiesViewModel> GetExtraRightSideDataModelViewModels()
{
// Extra data models are expected to not have an empty root, so lets return the child
return GetEventDataModel().Children.Cast<DataModelPropertiesViewModel>().ToList();
}
private DataModelPropertiesViewModel GetEventDataModel()
{
EventPredicateWrapperDataModel wrapper = EventPredicateWrapperDataModel.Create(
DataModelConditionEventPredicate.DataModelConditionEvent.EventArgumentType
);
return wrapper.CreateViewModel(_dataModelUIService, new DataModelUpdateConfiguration(false));
}
}
}

View File

@ -7,10 +7,10 @@
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:utilities="clr-namespace:Artemis.UI.Utilities"
xmlns:conditions="clr-namespace:Artemis.UI.Screens.ProfileEditor.Conditions"
x:Class="Artemis.UI.Screens.ProfileEditor.Conditions.DataModelConditionPredicateView"
x:Class="Artemis.UI.Screens.ProfileEditor.Conditions.DataModelConditionGeneralPredicateView"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance IsDesignTimeCreatable=False, Type={x:Type conditions:DataModelConditionPredicateViewModel}}">
d:DataContext="{d:DesignInstance IsDesignTimeCreatable=False, Type={x:Type conditions:DataModelConditionGeneralPredicateViewModel}}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
@ -21,7 +21,7 @@
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid Margin="0 3">
<Grid Margin="0 0 0 3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />

View File

@ -0,0 +1,26 @@
using System.Windows;
using System.Windows.Controls;
namespace Artemis.UI.Screens.ProfileEditor.Conditions
{
/// <summary>
/// Interaction logic for DataModelConditionGeneralPredicateView.xaml
/// </summary>
public partial class DataModelConditionGeneralPredicateView : UserControl
{
public DataModelConditionGeneralPredicateView()
{
InitializeComponent();
}
private void PropertyButton_OnClick(object sender, RoutedEventArgs e)
{
// DataContext is not set when using left button, I don't know why but there it is
if (sender is Button button && button.ContextMenu != null)
{
button.ContextMenu.DataContext = button.DataContext;
button.ContextMenu.IsOpen = true;
}
}
}
}

View File

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
namespace Artemis.UI.Screens.ProfileEditor.Conditions
{
public class DataModelConditionGeneralPredicateViewModel : DataModelConditionPredicateViewModel
{
private readonly IDataModelUIService _dataModelUIService;
public DataModelConditionGeneralPredicateViewModel(DataModelConditionGeneralPredicate dataModelConditionGeneralPredicate,
IProfileEditorService profileEditorService,
IDataModelUIService dataModelUIService,
IConditionOperatorService conditionOperatorService,
ISettingsService settingsService)
: base(dataModelConditionGeneralPredicate, profileEditorService, dataModelUIService, conditionOperatorService, settingsService)
{
_dataModelUIService = dataModelUIService;
}
protected override void OnInitialActivate()
{
Initialize();
}
protected override List<Type> GetSupportedInputTypes()
{
IReadOnlyCollection<DataModelVisualizationRegistration> editors = _dataModelUIService.RegisteredDataModelEditors;
List<Type> supportedInputTypes = editors.Select(e => e.SupportedType).ToList();
supportedInputTypes.AddRange(editors.Where(e => e.CompatibleConversionTypes != null).SelectMany(e => e.CompatibleConversionTypes));
supportedInputTypes.Add(typeof(IEnumerable<>));
return supportedInputTypes;
}
protected override Type GetLeftSideType()
{
return LeftSideSelectionViewModel.DataModelPath?.GetPropertyType();
}
}
}

View File

@ -5,7 +5,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:utilities="clr-namespace:Artemis.UI.Utilities"
xmlns:DataModelConditions="clr-namespace:Artemis.UI.Screens.ProfileEditor.Conditions"
x:Class="Artemis.UI.Screens.ProfileEditor.Conditions.DataModelConditionListPredicateView"
@ -17,21 +16,12 @@
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Artemis.UI;component/ResourceDictionaries/DataModelConditions.xaml" />
<ResourceDictionary>
<converters:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
<utilities:BindingProxy x:Key="DataContextProxy" Data="{Binding}" />
<DataTemplate x:Key="DataModelDataTemplate">
<Control x:Name="TemplateControl" Focusable="False" Template="{StaticResource DataModelSelectionTemplate}" />
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Data.ShowDataModelValues.Value, Source={StaticResource DataContextProxy}}" Value="True">
<Setter TargetName="TemplateControl" Property="Template" Value="{StaticResource DataModelSelectionTemplateWithValues}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid Margin="0 3">
<Grid Margin="0 0 0 3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
@ -50,7 +40,7 @@
<materialDesign:PackIcon Kind="Close" Width="18" Height="18" />
</Button>
<!-- Left side, may be null for primitive lists -->
<!-- Left side, always a property -->
<ContentControl Grid.Row="0"
Grid.Column="1"
s:View.Model="{Binding LeftSideSelectionViewModel}"

View File

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Media;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
namespace Artemis.UI.Screens.ProfileEditor.Conditions
{
public class DataModelConditionListPredicateViewModel : DataModelConditionPredicateViewModel
{
private readonly IDataModelUIService _dataModelUIService;
public DataModelConditionListPredicateViewModel(DataModelConditionListPredicate dataModelConditionListPredicate,
IProfileEditorService profileEditorService,
IDataModelUIService dataModelUIService,
IConditionOperatorService conditionOperatorService,
ISettingsService settingsService)
: base(dataModelConditionListPredicate, profileEditorService, dataModelUIService, conditionOperatorService, settingsService)
{
_dataModelUIService = dataModelUIService;
LeftSideColor = new SolidColorBrush(Color.FromRgb(71, 108, 188));
}
public DataModelConditionListPredicate DataModelConditionListPredicate => (DataModelConditionListPredicate) Model;
public override void Initialize()
{
base.Initialize();
DataModelPropertiesViewModel listDataModel = GetListDataModel();
LeftSideSelectionViewModel.ChangeDataModel(listDataModel);
// If this is a primitive list the user doesn't have much to choose, so preselect the list item for them
if (DataModelConditionListPredicate.DataModelConditionList.IsPrimitiveList && DataModelConditionListPredicate.LeftPath == null)
{
DataModelConditionListPredicate.UpdateLeftSide(listDataModel.Children.FirstOrDefault()?.DataModelPath);
Update();
}
}
protected override void OnInitialActivate()
{
base.OnInitialActivate();
Initialize();
}
protected override List<Type> GetSupportedInputTypes()
{
IReadOnlyCollection<DataModelVisualizationRegistration> editors = _dataModelUIService.RegisteredDataModelEditors;
List<Type> supportedInputTypes = editors.Select(e => e.SupportedType).ToList();
supportedInputTypes.AddRange(editors.Where(e => e.CompatibleConversionTypes != null).SelectMany(e => e.CompatibleConversionTypes));
return supportedInputTypes;
}
protected override Type GetLeftSideType()
{
return DataModelConditionListPredicate.DataModelConditionList.IsPrimitiveList
? DataModelConditionListPredicate.DataModelConditionList.ListType
: LeftSideSelectionViewModel.DataModelPath?.GetPropertyType();
}
protected override List<DataModelPropertiesViewModel> GetExtraRightSideDataModelViewModels()
{
if (GetListDataModel()?.Children?.FirstOrDefault() is DataModelPropertiesViewModel listValue)
return new List<DataModelPropertiesViewModel> {listValue};
return null;
}
private DataModelPropertiesViewModel GetListDataModel()
{
ListPredicateWrapperDataModel wrapper = ListPredicateWrapperDataModel.Create(
DataModelConditionListPredicate.DataModelConditionList.ListType
);
return wrapper.CreateViewModel(_dataModelUIService, new DataModelUpdateConfiguration(true));
}
}
}

View File

@ -7,12 +7,14 @@
xmlns:s="https://github.com/canton7/Stylet"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:displayConditions="clr-namespace:Artemis.UI.Screens.ProfileEditor.DisplayConditions"
xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core"
x:Class="Artemis.UI.Screens.ProfileEditor.DisplayConditions.DisplayConditionsView"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance {x:Type displayConditions:DisplayConditionsViewModel}}">
<UserControl.Resources>
<converters:InverseBooleanConverter x:Key="InverseBooleanConverter" />
<converters:ComparisonConverter x:Key="ComparisonConverter" />
</UserControl.Resources>
<Grid Margin="10">
@ -56,9 +58,9 @@
</materialDesign:Card>
</Grid>
<Grid Grid.Row="3">
<Grid Grid.Row="3" Visibility="{Binding IsEventCondition, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}">
<Grid.RowDefinitions>
<RowDefinition Height="20" />
<RowDefinition Height="18" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
@ -67,19 +69,15 @@
</Grid.ColumnDefinitions>
<!-- Play mode -->
<StackPanel Grid.Column="0" Orientation="Horizontal">
<materialDesign:PackIcon Kind="PlayOutline" VerticalAlignment="Center" />
<TextBlock Text="Play mode" VerticalAlignment="Center">
<TextBlock.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-30">
<TextBlock>
Configure how the layer should act while the conditions above are met
</TextBlock>
</ToolTip>
</TextBlock.ToolTip>
</TextBlock>
</StackPanel>
<TextBlock Grid.Column="0" Text="Play mode">
<TextBlock.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-30">
<TextBlock>
Configure how the layer should act while the conditions above are met
</TextBlock>
</ToolTip>
</TextBlock.ToolTip>
</TextBlock>
<materialDesign:ColorZone Grid.Row="1" Grid.Column="0" Mode="Standard" CornerRadius="3" Margin="0 0 2 0">
<Grid>
<Grid.ColumnDefinitions>
@ -98,10 +96,10 @@
</TextBlock>
</ToolTip>
</RadioButton.ToolTip>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Repeat" VerticalAlignment="Center" />
<TextBlock FontSize="12" VerticalAlignment="Center">REPEAT</TextBlock>
</StackPanel>
<TextBlock VerticalAlignment="Center" FontSize="12">
<materialDesign:PackIcon Kind="Repeat" VerticalAlignment="Center" Margin="-3 0 0 -3" />
REPEAT
</TextBlock>
</RadioButton>
<RadioButton Grid.Column="1"
Style="{StaticResource MaterialDesignTabRadioButton}"
@ -115,27 +113,24 @@
</TextBlock>
</ToolTip>
</RadioButton.ToolTip>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="StopwatchOutline" VerticalAlignment="Center" />
<TextBlock FontSize="12" VerticalAlignment="Center">ONCE</TextBlock>
</StackPanel>
<TextBlock VerticalAlignment="Center" FontSize="12">
<materialDesign:PackIcon Kind="StopwatchOutline" VerticalAlignment="Center" Margin="-3 0 0 -3" />
ONCE
</TextBlock>
</RadioButton>
</Grid>
</materialDesign:ColorZone>
<!-- Stop mode -->
<StackPanel Grid.Column="1" Orientation="Horizontal">
<materialDesign:PackIcon Kind="Stop" VerticalAlignment="Center" />
<TextBlock Text="Stop mode" VerticalAlignment="Center">
<TextBlock.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-30">
<TextBlock>
Configure how the layer should act when the conditions above are no longer met
</TextBlock>
</ToolTip>
</TextBlock.ToolTip>
</TextBlock>
</StackPanel>
<TextBlock Grid.Row="0" Grid.Column="1" Text="Stop mode">
<TextBlock.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-30">
<TextBlock>
Configure how the layer should act when the conditions above are no longer met
</TextBlock>
</ToolTip>
</TextBlock.ToolTip>
</TextBlock>
<materialDesign:ColorZone Grid.Row="1" Grid.Column="1" Mode="Standard" CornerRadius="3" Margin="2 0 0 0" HorizontalAlignment="Stretch">
<Grid>
<Grid.ColumnDefinitions>
@ -154,10 +149,10 @@
</TextBlock>
</ToolTip>
</RadioButton.ToolTip>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="PlayOutline" VerticalAlignment="Center" />
<TextBlock FontSize="12" VerticalAlignment="Center">FINISH</TextBlock>
</StackPanel>
<TextBlock VerticalAlignment="Center" FontSize="12">
<materialDesign:PackIcon Kind="PlayOutline" VerticalAlignment="Center" Margin="-3 0 0 -3" />
FINISH
</TextBlock>
</RadioButton>
<RadioButton Grid.Column="1"
Style="{StaticResource MaterialDesignTabRadioButton}"
@ -171,10 +166,82 @@
</TextBlock>
</ToolTip>
</RadioButton.ToolTip>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="SkipNextOutline" VerticalAlignment="Center" />
<TextBlock FontSize="12" VerticalAlignment="Center">SKIP TO END</TextBlock>
</StackPanel>
<TextBlock VerticalAlignment="Center" FontSize="12">
<materialDesign:PackIcon Kind="SkipNextOutline" VerticalAlignment="Center" Margin="-3 0 0 -3" />
SKIP TO END
</TextBlock>
</RadioButton>
</Grid>
</materialDesign:ColorZone>
</Grid>
<Grid Grid.Row="3" Visibility="{Binding IsEventCondition, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
<Grid.RowDefinitions>
<RowDefinition Height="18" />
<RowDefinition />
</Grid.RowDefinitions>
<!-- Trigger mode -->
<TextBlock Grid.Column="0" Text="Rapid trigger mode">
<TextBlock.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-30">
<TextBlock>
Configure how the layer should act when the event(s) trigger before the timeline finishes
</TextBlock>
</ToolTip>
</TextBlock.ToolTip>
</TextBlock>
<materialDesign:ColorZone Grid.Row="1" Grid.Column="0" Mode="Standard" CornerRadius="3">
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<RadioButton Grid.Column="0"
Style="{StaticResource MaterialDesignTabRadioButton}"
IsChecked="{Binding Path=RenderProfileElement.Timeline.EventOverlapMode, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static core:TimeLineEventOverlapMode.Restart}}">
<TextBlock VerticalAlignment="Center" FontSize="12">
<materialDesign:PackIcon Kind="Repeat" VerticalAlignment="Center" Margin="-3 0 0 -3" />
RESTART
</TextBlock>
<RadioButton.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-40">
<TextBlock>
Stop the current run and restart the timeline
</TextBlock>
</ToolTip>
</RadioButton.ToolTip>
</RadioButton>
<RadioButton Grid.Column="1"
Style="{StaticResource MaterialDesignTabRadioButton}"
IsChecked="{Binding Path=RenderProfileElement.Timeline.EventOverlapMode, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static core:TimeLineEventOverlapMode.Ignore}}">
<TextBlock VerticalAlignment="Center" FontSize="12">
<materialDesign:PackIcon Kind="EarHearingOff" VerticalAlignment="Center" Margin="-3 0 0 -3" />
IGNORE
</TextBlock>
<RadioButton.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-40">
<TextBlock>
Ignore subsequent event fires until the timeline finishes
</TextBlock>
</ToolTip>
</RadioButton.ToolTip>
</RadioButton>
<RadioButton Grid.Column="2"
Style="{StaticResource MaterialDesignTabRadioButton}"
IsChecked="{Binding Path=RenderProfileElement.Timeline.EventOverlapMode, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static core:TimeLineEventOverlapMode.Copy}}">
<TextBlock VerticalAlignment="Center" FontSize="12">
<materialDesign:PackIcon Kind="ContentCopy" VerticalAlignment="Center" Margin="-3 0 0 -3" />
COPY
</TextBlock>
<RadioButton.ToolTip>
<ToolTip Placement="Center" VerticalOffset="-40">
<TextBlock>
Play another copy of the timeline on top of the current run
</TextBlock>
</ToolTip>
</RadioButton.ToolTip>
</RadioButton>
</Grid>
</materialDesign:ColorZone>

View File

@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using Artemis.Core;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.Conditions;
@ -14,6 +15,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions
private readonly IProfileEditorService _profileEditorService;
private RenderProfileElement _renderProfileElement;
private bool _displayStartHint;
private bool _isEventCondition;
public DisplayConditionsViewModel(IProfileEditorService profileEditorService, IDataModelConditionsVmFactory dataModelConditionsVmFactory)
{
@ -27,6 +29,12 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions
set => SetAndNotify(ref _displayStartHint, value);
}
public bool IsEventCondition
{
get => _isEventCondition;
set => SetAndNotify(ref _isEventCondition, value);
}
public RenderProfileElement RenderProfileElement
{
get => _renderProfileElement;
@ -35,22 +43,24 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions
public bool DisplayContinuously
{
get => RenderProfileElement?.DisplayContinuously ?? false;
get => RenderProfileElement?.Timeline.PlayMode == TimelinePlayMode.Repeat;
set
{
if (RenderProfileElement == null || RenderProfileElement.DisplayContinuously == value) return;
RenderProfileElement.DisplayContinuously = value;
TimelinePlayMode playMode = value ? TimelinePlayMode.Repeat : TimelinePlayMode.Once;
if (RenderProfileElement == null || RenderProfileElement?.Timeline.PlayMode == playMode) return;
RenderProfileElement.Timeline.PlayMode = playMode;
_profileEditorService.UpdateSelectedProfileElement();
}
}
public bool AlwaysFinishTimeline
{
get => RenderProfileElement?.AlwaysFinishTimeline ?? false;
get => RenderProfileElement?.Timeline.StopMode == TimelineStopMode.Finish;
set
{
if (RenderProfileElement == null || RenderProfileElement.AlwaysFinishTimeline == value) return;
RenderProfileElement.AlwaysFinishTimeline = value;
TimelineStopMode stopMode = value ? TimelineStopMode.Finish : TimelineStopMode.SkipToEnd;
if (RenderProfileElement == null || RenderProfileElement?.Timeline.StopMode == stopMode) return;
RenderProfileElement.Timeline.StopMode = stopMode;
_profileEditorService.UpdateSelectedProfileElement();
}
}
@ -71,7 +81,14 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions
private void ProfileEditorServiceOnProfileElementSelected(object sender, RenderProfileElementEventArgs e)
{
if (RenderProfileElement != null)
{
RenderProfileElement.DisplayCondition.ChildAdded -= DisplayConditionOnChildrenModified;
RenderProfileElement.DisplayCondition.ChildRemoved -= DisplayConditionOnChildrenModified;
}
RenderProfileElement = e.RenderProfileElement;
NotifyOfPropertyChange(nameof(DisplayContinuously));
NotifyOfPropertyChange(nameof(AlwaysFinishTimeline));
NotifyOfPropertyChange(nameof(ConditionBehaviourEnabled));
@ -86,12 +103,21 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions
if (e.RenderProfileElement.DisplayCondition == null)
e.RenderProfileElement.DisplayCondition = new DataModelConditionGroup(null);
ActiveItem = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(e.RenderProfileElement.DisplayCondition, false);
ActiveItem = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(e.RenderProfileElement.DisplayCondition, ConditionGroupType.General);
ActiveItem.IsRootGroup = true;
ActiveItem.Update();
// Only show the intro to conditions once, and only if the layer has no conditions
DisplayStartHint = !ActiveItem.Items.Any();
DisplayStartHint = !RenderProfileElement.DisplayCondition.Children.Any();
IsEventCondition = RenderProfileElement.DisplayCondition.Children.Any(c => c is DataModelConditionEvent);
RenderProfileElement.DisplayCondition.ChildAdded += DisplayConditionOnChildrenModified;
RenderProfileElement.DisplayCondition.ChildRemoved += DisplayConditionOnChildrenModified;
}
private void DisplayConditionOnChildrenModified(object? sender, EventArgs e)
{
DisplayStartHint = !RenderProfileElement.DisplayCondition.Children.Any();
IsEventCondition = RenderProfileElement.DisplayCondition.Children.Any(c => c is DataModelConditionEvent);
}
}
}

View File

@ -5,18 +5,13 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:dd="urn:gong-wpf-dragdrop" xmlns:Converters="clr-namespace:Artemis.UI.Converters"
xmlns:utilities="clr-namespace:Artemis.UI.Utilities"
xmlns:dd="urn:gong-wpf-dragdrop"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Artemis.UI;component/ResourceDictionaries/DataModelConditions.xaml" />
<ResourceDictionary>
<Converters:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
<utilities:BindingProxy x:Key="DataContextProxy" Data="{Binding}" />
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
@ -27,43 +22,16 @@
</Grid.RowDefinitions>
<Button Grid.Row="0"
Style="{StaticResource DataModelConditionButtonLeftClickMenu}"
Style="{StaticResource DataModelConditionButton}"
Background="{StaticResource PrimaryHueMidBrush}"
BorderBrush="{StaticResource PrimaryHueMidBrush}"
HorizontalAlignment="Right">
HorizontalAlignment="Right"
Command="{s:Action AddCondition}">
ADD CONDITION
<Button.ContextMenu>
<ContextMenu>
<MenuItem Header="Add static condition"
ToolTip="A condition that compares with a static input"
Command="{s:Action AddCondition}"
CommandParameter="Static">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="FormTextarea" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Add dynamic condition"
ToolTip="A condition that compares with a data model property"
Command="{s:Action AddCondition}"
CommandParameter="Dynamic">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Link" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Add list condition"
ToolTip="A condition that evaluates on items in a list"
Command="{s:Action AddCondition}"
CommandParameter="List">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="FormatListBulleted" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</Button.ContextMenu>
</Button>
<ListBox Grid.Row="1"
ItemsSource="{Binding ConditionViewModels}"
ItemsSource="{Binding Items}"
materialDesign:RippleAssist.IsDisabled="True"
dd:DragDrop.IsDragSource="True"
dd:DragDrop.IsDropTarget="True"

View File

@ -10,7 +10,7 @@ using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.ConditionalDataBinding
{
public class ConditionalDataBindingModeViewModel<TLayerProperty, TProperty> : Screen, IDataBindingModeViewModel
public class ConditionalDataBindingModeViewModel<TLayerProperty, TProperty> : Conductor<DataBindingConditionViewModel<TLayerProperty, TProperty>>.Collection.AllActive, IDataBindingModeViewModel
{
private readonly IDataBindingsVmFactory _dataBindingsVmFactory;
private readonly IProfileEditorService _profileEditorService;
@ -24,18 +24,83 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.Conditio
_dataBindingsVmFactory = dataBindingsVmFactory;
ConditionalDataBinding = conditionalDataBinding;
ConditionViewModels = new BindableCollection<DataBindingConditionViewModel<TLayerProperty, TProperty>>();
Initialize();
}
public ConditionalDataBinding<TLayerProperty, TProperty> ConditionalDataBinding { get; }
public BindableCollection<DataBindingConditionViewModel<TLayerProperty, TProperty>> ConditionViewModels { get; }
public void AddCondition()
{
DataBindingCondition<TLayerProperty, TProperty> condition = ConditionalDataBinding.AddCondition();
// Find the VM of the new condition
DataBindingConditionViewModel<TLayerProperty, TProperty> viewModel = Items.First(c => c.DataBindingCondition == condition);
viewModel.ActiveItem.AddCondition();
_profileEditorService.UpdateSelectedProfileElement();
}
protected override void OnInitialActivate()
{
base.OnInitialActivate();
Initialize();
}
private void UpdateItems()
{
_updating = true;
// Remove old VMs
List<DataBindingConditionViewModel<TLayerProperty, TProperty>> toRemove = Items.Where(c => !ConditionalDataBinding.Conditions.Contains(c.DataBindingCondition)).ToList();
foreach (DataBindingConditionViewModel<TLayerProperty, TProperty> dataBindingConditionViewModel in toRemove)
{
Items.Remove(dataBindingConditionViewModel);
dataBindingConditionViewModel.Dispose();
}
// Add missing VMs
foreach (DataBindingCondition<TLayerProperty, TProperty> condition in ConditionalDataBinding.Conditions)
if (Items.All(c => c.DataBindingCondition != condition))
Items.Add(_dataBindingsVmFactory.DataBindingConditionViewModel(condition));
// Fix order
((BindableCollection<DataBindingConditionViewModel<TLayerProperty, TProperty>>) Items).Sort(c => c.DataBindingCondition.Order);
_updating = false;
}
private void Initialize()
{
ConditionalDataBinding.ConditionsUpdated += ConditionalDataBindingOnConditionsUpdated;
Items.CollectionChanged += ItemsOnCollectionChanged;
UpdateItems();
}
private void ItemsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (_updating || e.Action != NotifyCollectionChangedAction.Add)
return;
for (int index = 0; index < Items.Count; index++)
{
DataBindingConditionViewModel<TLayerProperty, TProperty> conditionViewModel = Items[index];
conditionViewModel.DataBindingCondition.Order = index + 1;
}
ConditionalDataBinding.ApplyOrder();
_profileEditorService.UpdateSelectedProfileElement();
}
private void ConditionalDataBindingOnConditionsUpdated(object sender, EventArgs e)
{
UpdateItems();
}
public bool SupportsTestValue => false;
public void Update()
{
UpdateConditionViewModels();
UpdateItems();
}
public object GetTestValue()
@ -49,64 +114,11 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.Conditio
{
ConditionalDataBinding.ConditionsUpdated -= ConditionalDataBindingOnConditionsUpdated;
foreach (DataBindingConditionViewModel<TLayerProperty, TProperty> conditionViewModel in ConditionViewModels)
foreach (DataBindingConditionViewModel<TLayerProperty, TProperty> conditionViewModel in Items)
conditionViewModel.Dispose();
ConditionViewModels.Clear();
Items.Clear();
}
#endregion
private void UpdateConditionViewModels()
{
_updating = true;
// Remove old VMs
List<DataBindingConditionViewModel<TLayerProperty, TProperty>> toRemove = ConditionViewModels.Where(c => !ConditionalDataBinding.Conditions.Contains(c.DataBindingCondition)).ToList();
foreach (DataBindingConditionViewModel<TLayerProperty, TProperty> dataBindingConditionViewModel in toRemove)
{
ConditionViewModels.Remove(dataBindingConditionViewModel);
dataBindingConditionViewModel.Dispose();
}
// Add missing VMs
foreach (DataBindingCondition<TLayerProperty, TProperty> condition in ConditionalDataBinding.Conditions)
{
if (ConditionViewModels.All(c => c.DataBindingCondition != condition))
ConditionViewModels.Add(_dataBindingsVmFactory.DataBindingConditionViewModel(condition));
}
// Fix order
ConditionViewModels.Sort(c => c.DataBindingCondition.Order);
_updating = false;
}
private void Initialize()
{
ConditionalDataBinding.ConditionsUpdated += ConditionalDataBindingOnConditionsUpdated;
ConditionViewModels.CollectionChanged += ConditionViewModelsOnCollectionChanged;
UpdateConditionViewModels();
}
private void ConditionViewModelsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (_updating || e.Action != NotifyCollectionChangedAction.Add)
return;
for (int index = 0; index < ConditionViewModels.Count; index++)
{
DataBindingConditionViewModel<TLayerProperty, TProperty> conditionViewModel = ConditionViewModels[index];
conditionViewModel.DataBindingCondition.Order = index + 1;
}
ConditionalDataBinding.ApplyOrder();
_profileEditorService.UpdateSelectedProfileElement();
}
private void ConditionalDataBindingOnConditionsUpdated(object sender, EventArgs e)
{
UpdateConditionViewModels();
}
}
}

View File

@ -18,7 +18,7 @@
IsTabStop="False" />
<ContentControl Grid.Row="1"
Margin="26 5 0 0"
Margin="26 2 0 0"
s:View.Model="{Binding ValueViewModel}"
VerticalContentAlignment="Stretch"
HorizontalContentAlignment="Stretch"

View File

@ -13,6 +13,8 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.Conditio
public class DataBindingConditionViewModel<TLayerProperty, TProperty> : Conductor<DataModelConditionGroupViewModel>, IDisposable
{
private readonly IProfileEditorService _profileEditorService;
private readonly IDataModelConditionsVmFactory _dataModelConditionsVmFactory;
private readonly IDataModelUIService _dataModelUIService;
public DataBindingConditionViewModel(DataBindingCondition<TLayerProperty, TProperty> dataBindingCondition,
IProfileEditorService profileEditorService,
@ -20,22 +22,28 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.Conditio
IDataModelUIService dataModelUIService)
{
_profileEditorService = profileEditorService;
_dataModelConditionsVmFactory = dataModelConditionsVmFactory;
_dataModelUIService = dataModelUIService;
DataBindingCondition = dataBindingCondition;
ActiveItem = dataModelConditionsVmFactory.DataModelConditionGroupViewModel(DataBindingCondition.Condition, false);
ActiveItem.IsRootGroup = true;
ActiveItem.Update();
ActiveItem.Updated += ActiveItemOnUpdated;
ValueViewModel = dataModelUIService.GetStaticInputViewModel(typeof(TProperty), null);
ValueViewModel.ValueUpdated += ValueViewModelOnValueUpdated;
ValueViewModel.Value = DataBindingCondition.Value;
}
public DataBindingCondition<TLayerProperty, TProperty> DataBindingCondition { get; }
public DataModelStaticViewModel ValueViewModel { get; set; }
protected override void OnInitialActivate()
{
base.OnInitialActivate();
ActiveItem = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(DataBindingCondition.Condition, ConditionGroupType.General);
ActiveItem.IsRootGroup = true;
ActiveItem.Update();
ActiveItem.Updated += ActiveItemOnUpdated;
ValueViewModel = _dataModelUIService.GetStaticInputViewModel(typeof(TProperty), null);
ValueViewModel.ValueUpdated += ValueViewModelOnValueUpdated;
ValueViewModel.Value = DataBindingCondition.Value;
}
public void Dispose()
{
ValueViewModel.Dispose();

View File

@ -44,7 +44,11 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings
EasingViewModels = new BindableCollection<TimelineEasingViewModel>();
TestInputValue = dataModelUIService.GetDataModelDisplayViewModel(typeof(TProperty), null, true);
TestResultValue = dataModelUIService.GetDataModelDisplayViewModel(typeof(TProperty), null, true);
}
protected override void OnInitialActivate()
{
base.OnInitialActivate();
Initialize();
}
@ -237,7 +241,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings
}
// While playing in preview data bindings aren't updated
Registration.DataBinding.Update(0.04);
Registration.DataBinding.UpdateWithDelta(TimeSpan.FromMilliseconds(40));
if (ActiveItem.SupportsTestValue)
{

View File

@ -140,7 +140,12 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDa
if (DynamicSelectionViewModel != null)
DynamicSelectionViewModel.ChangeDataModelPath(Modifier.ParameterPath);
else if (StaticInputViewModel != null)
{
// Ensure the right static value is never null when the preferred type is a value type
StaticInputViewModel.Value = Modifier.ParameterStaticValue;
if (SelectedModifierType.ParameterType.IsValueType && StaticInputViewModel.Value == null)
StaticInputViewModel.Value = SelectedModifierType.ParameterType.GetDefault();
}
}
private void ExecuteSelectModifierTypeCommand(object context)

View File

@ -567,7 +567,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties
{
if (Repeating && RepeatTimeline)
{
if (newTime > SelectedProfileElement.TimelineLength)
if (newTime > SelectedProfileElement.Timeline.Length)
newTime = TimeSpan.Zero;
}
else if (Repeating && RepeatSegment)
@ -575,9 +575,9 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties
if (newTime > GetCurrentSegmentEnd())
newTime = GetCurrentSegmentStart();
}
else if (newTime > SelectedProfileElement.TimelineLength)
else if (newTime > SelectedProfileElement.Timeline.Length)
{
newTime = SelectedProfileElement.TimelineLength;
newTime = SelectedProfileElement.Timeline.Length;
Pause();
}
}

View File

@ -148,8 +148,8 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
{
if (position < TimeSpan.Zero)
LayerPropertyKeyframe.Position = TimeSpan.Zero;
else if (position > _profileEditorService.SelectedProfileElement.TimelineLength)
LayerPropertyKeyframe.Position = _profileEditorService.SelectedProfileElement.TimelineLength;
else if (position > _profileEditorService.SelectedProfileElement.Timeline.Length)
LayerPropertyKeyframe.Position = _profileEditorService.SelectedProfileElement.Timeline.Length;
else
LayerPropertyKeyframe.Position = position;
@ -170,7 +170,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
);
// If possible, shift the keyframe to the right by 11 pixels
TimeSpan desiredPosition = newKeyframe.Position + TimeSpan.FromMilliseconds(1000f / _profileEditorService.PixelsPerSecond * 11);
if (desiredPosition <= newKeyframe.LayerProperty.ProfileElement.TimelineLength)
if (desiredPosition <= newKeyframe.LayerProperty.ProfileElement.Timeline.Length)
newKeyframe.Position = desiredPosition;
// Otherwise if possible shift it to the left by 11 pixels
else

View File

@ -56,7 +56,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
start ??= TimeSpan.Zero;
end ??= TimeSpan.MaxValue;
List<LayerPropertyKeyframe<T>> toShift = LayerProperty.Keyframes.Where(k => k.Position >= start && k.Position < end).ToList();
List<LayerPropertyKeyframe<T>> toShift = LayerProperty.Keyframes.Where(k => k.Position > start && k.Position < end).ToList();
foreach (LayerPropertyKeyframe<T> keyframe in toShift)
keyframe.Position += amount;

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