diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings index 06398867e..705d6dc5c 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -1,4 +1,5 @@  + True True True True diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorDesaturateModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorDesaturateModifierType.cs new file mode 100644 index 000000000..2c27d58d1 --- /dev/null +++ b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorDesaturateModifierType.cs @@ -0,0 +1,19 @@ +using SkiaSharp; + +namespace Artemis.Core.DefaultTypes +{ + internal class SKColorDesaturateModifierType : DataBindingModifierType + { + 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); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorSaturateModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorSaturateModifierType.cs new file mode 100644 index 000000000..daa5b0fc7 --- /dev/null +++ b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorSaturateModifierType.cs @@ -0,0 +1,18 @@ +using SkiaSharp; + +namespace Artemis.Core.DefaultTypes +{ + internal class SKColorSaturateModifierType : DataBindingModifierType + { + 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); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/JsonConverters/ForgivingIntConverter.cs b/src/Artemis.Core/JsonConverters/ForgivingIntConverter.cs new file mode 100644 index 000000000..77a09cb2b --- /dev/null +++ b/src/Artemis.Core/JsonConverters/ForgivingIntConverter.cs @@ -0,0 +1,31 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Artemis.Core.JsonConverters +{ + /// + /// An int converter that, if required, will round float values + /// + internal class ForgivingIntConverter : JsonConverter + { + 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(reader); + + if (jsonValue.Type == JTokenType.Float) + return (int) Math.Round(jsonValue.Value()); + if (jsonValue.Type == JTokenType.Integer) + return jsonValue.Value(); + + throw new FormatException(); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/IUpdateModel.cs b/src/Artemis.Core/Models/IUpdateModel.cs index 870079a7f..24c480c73 100644 --- a/src/Artemis.Core/Models/IUpdateModel.cs +++ b/src/Artemis.Core/Models/IUpdateModel.cs @@ -8,7 +8,7 @@ /// /// Performs an update on the model /// - /// The delta time in seconds - void Update(double deltaTime); + /// The timeline to apply during update + void Update(Timeline timeline); } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/Abstract/ConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Abstract/ConditionOperator.cs index 416f41912..2134cbaf9 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Abstract/ConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Abstract/ConditionOperator.cs @@ -7,37 +7,51 @@ namespace Artemis.Core /// public abstract class ConditionOperator : BaseConditionOperator { + /// + public override Type LeftSideType => typeof(TLeftSide); + + /// + public override Type RightSideType => typeof(TRightSide); + /// /// Evaluates the operator on a and b /// /// The parameter on the left side of the expression /// The parameter on the right side of the expression public abstract bool Evaluate(TLeftSide a, TRightSide b); - + /// internal override bool InternalEvaluate(object? leftSideValue, object? rightSideValue) { // 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!); } - - /// - public override Type LeftSideType => typeof(TLeftSide); - - /// - public override Type RightSideType => typeof(TRightSide); } /// @@ -45,6 +59,14 @@ namespace Artemis.Core /// public abstract class ConditionOperator : BaseConditionOperator { + /// + public override Type LeftSideType => typeof(TLeftSide); + + /// + /// Always null, not applicable to this type of condition operator + /// + public override Type? RightSideType => null; + /// /// Evaluates the operator on a and b /// @@ -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!); } - - /// - public override Type LeftSideType => typeof(TLeftSide); - - /// - /// Always null, not applicable to this type of condition operator - /// - public override Type? RightSideType => null; } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/Abstract/DataModelConditionPart.cs b/src/Artemis.Core/Models/Profile/Conditions/Abstract/DataModelConditionPart.cs index bca0ebc4d..c42ef85ae 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Abstract/DataModelConditionPart.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Abstract/DataModelConditionPart.cs @@ -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(); } } + /// + /// Removes all children. You monster. + /// + public void ClearChildren() + { + while (Children.Any()) + RemoveChild(Children[0]); + } + /// /// Evaluates the condition part on the data model /// @@ -71,7 +84,7 @@ namespace Artemis.Core #region IDisposable /// - /// Disposed the condition part + /// Disposed the condition part /// 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 } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionPredicate.cs b/src/Artemis.Core/Models/Profile/Conditions/Abstract/DataModelConditionPredicate.cs similarity index 86% rename from src/Artemis.Core/Models/Profile/Conditions/DataModelConditionPredicate.cs rename to src/Artemis.Core/Models/Profile/Conditions/Abstract/DataModelConditionPredicate.cs index db96b8c22..f99364cd2 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionPredicate.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Abstract/DataModelConditionPredicate.cs @@ -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 /// - public class DataModelConditionPredicate : DataModelConditionPart + public abstract class DataModelConditionPredicate : DataModelConditionPart { /// /// Creates a new instance of the class /// /// /// - public DataModelConditionPredicate(DataModelConditionPart parent, ProfileRightSideType predicateType) + /// A new empty entity + 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(); } /// @@ -44,31 +39,117 @@ namespace Artemis.Core /// /// Gets the operator /// - public BaseConditionOperator? Operator { get; private set; } + public BaseConditionOperator? Operator { get; protected set; } /// /// Gets the path of the left property /// - public DataModelPath? LeftPath { get; private set; } + public DataModelPath? LeftPath { get; protected set; } /// /// Gets the path of the right property /// - public DataModelPath? RightPath { get; private set; } + public DataModelPath? RightPath { get; protected set; } /// /// Gets the right static value, only used it is /// /// - public object? RightStaticValue { get; private set; } + public object? RightStaticValue { get; protected set; } internal DataModelConditionPredicateEntity Entity { get; set; } + /// + public override string ToString() + { + if (PredicateType == ProfileRightSideType.Dynamic) + return $"[Dynamic] {LeftPath} {Operator.Description} {RightPath}"; + return $"[Static] {LeftPath} {Operator.Description} {RightStaticValue}"; + } + + #region IDisposable + + /// + 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 + /// /// Updates the left side of the predicate /// /// The path pointing to the left side value inside the data model - 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(); } - /// - 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()); - } - - /// - public override string ToString() - { - if (PredicateType == ProfileRightSideType.Dynamic) - return $"[Dynamic] {LeftPath} {Operator.Description} {RightPath}"; - return $"[Static] {LeftPath} {Operator.Description} {RightStaticValue}"; - } - - /// - protected override void Dispose(bool disposing) - { - ConditionOperatorStore.ConditionOperatorAdded -= ConditionOperatorStoreOnConditionOperatorAdded; - ConditionOperatorStore.ConditionOperatorRemoved -= ConditionOperatorStoreOnConditionOperatorRemoved; - - LeftPath?.Dispose(); - RightPath?.Dispose(); - - base.Dispose(disposing); - } - - /// - 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; - } + /// + /// Determines the best type to use for the right side op this predicate + /// + public abstract Type? GetPreferredRightSideType(); private void ValidateOperator() { @@ -327,6 +283,76 @@ namespace Artemis.Core } } + #endregion + + #region Evaluation + + /// + 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()); + } + + /// + 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) diff --git a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionEvent.cs b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionEvent.cs new file mode 100644 index 000000000..c629ef276 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionEvent.cs @@ -0,0 +1,241 @@ +using System; +using System.Linq; +using Artemis.Storage.Entities.Profile.Abstract; +using Artemis.Storage.Entities.Profile.Conditions; + +namespace Artemis.Core +{ + /// + /// A condition that evaluates to true when an event is triggered + /// + public class DataModelConditionEvent : DataModelConditionPart + { + private bool _disposed; + private bool _reinitializing; + private IDataModelEvent? _event; + private bool _eventTriggered; + + /// + /// Creates a new instance of the class + /// + /// + public DataModelConditionEvent(DataModelConditionPart parent) + { + Parent = parent; + Entity = new DataModelConditionEventEntity(); + + Initialize(); + } + + internal DataModelConditionEvent(DataModelConditionPart parent, DataModelConditionEventEntity entity) + { + Parent = parent; + Entity = entity; + + Initialize(); + } + + /// + /// Gets the path of the event property + /// + public DataModelPath? EventPath { get; private set; } + + public Type? EventArgumentType { get; set; } + + internal DataModelConditionEventEntity Entity { get; set; } + + /// + 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; + } + + /// + /// Updates the event the condition is triggered by + /// + 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 + + /// + 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 + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionEventPredicate.cs b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionEventPredicate.cs new file mode 100644 index 000000000..4e5f38d02 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionEventPredicate.cs @@ -0,0 +1,161 @@ +using System; +using Artemis.Storage.Entities.Profile; +using Artemis.Storage.Entities.Profile.Conditions; + +namespace Artemis.Core +{ + /// + /// A predicate like evaluated inside a + /// + public class DataModelConditionEventPredicate : DataModelConditionPredicate + { + /// + /// Creates a new instance of the class + /// + /// + /// + 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(); + } + + /// + /// Gets the data model condition event this predicate belongs to + /// + 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 + + /// + 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 + + /// + /// Not supported for event predicates, always returns false + /// + 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 + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionGeneralPredicate.cs b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionGeneralPredicate.cs new file mode 100644 index 000000000..2f29d2f40 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionGeneralPredicate.cs @@ -0,0 +1,63 @@ +using System; +using Artemis.Storage.Entities.Profile.Conditions; + +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 + /// + public class DataModelConditionGeneralPredicate : DataModelConditionPredicate + { + /// + /// Creates a new instance of the class + /// + /// + /// + public DataModelConditionGeneralPredicate(DataModelConditionPart parent, ProfileRightSideType predicateType) + : base(parent, predicateType, new DataModelConditionGeneralPredicateEntity()) + { + Initialize(); + } + + internal DataModelConditionGeneralPredicate(DataModelConditionPart parent, DataModelConditionGeneralPredicateEntity entity) + : base(parent, entity) + { + Initialize(); + } + + #region Modification + + /// + 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 + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionGroup.cs b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionGroup.cs index c731176ca..2b44acb40 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionGroup.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionGroup.cs @@ -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; } /// @@ -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; } /// @@ -52,6 +64,11 @@ namespace Artemis.Core /// public BooleanOperator BooleanOperator { get; set; } + /// + /// Gets whether this group contains any events + /// + public bool ContainsEvents { get; private set; } + internal DataModelConditionGroupEntity Entity { get; set; } /// @@ -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 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); + } } /// diff --git a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionList.cs b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionList.cs index a3459f385..392e1ba51 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionList.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionList.cs @@ -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; diff --git a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionListPredicate.cs b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionListPredicate.cs index 601483aef..32316ec38 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionListPredicate.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionListPredicate.cs @@ -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 { /// /// A predicate like evaluated inside a /// - public class DataModelConditionListPredicate : DataModelConditionPart + public class DataModelConditionListPredicate : DataModelConditionPredicate { /// /// Creates a new instance of the class @@ -18,138 +15,104 @@ namespace Artemis.Core /// /// 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(); } - /// - /// Gets or sets the predicate type - /// - public ProfileRightSideType PredicateType { get; set; } - - /// - /// Gets the operator - /// - public BaseConditionOperator? Operator { get; private set; } - /// /// Gets the data model condition list this predicate belongs to /// public DataModelConditionList DataModelConditionList { get; private set; } - /// - /// Gets the path of the left property - /// - public DataModelPath? LeftPath { get; private set; } - - /// - /// Gets the path of the right property - /// - public DataModelPath? RightPath { get; private set; } - - /// - /// Gets the right static value, only used it is - /// - /// - public object? RightStaticValue { get; private set; } - - internal DataModelConditionListPredicateEntity Entity { get; set; } - - /// - /// Updates the left side of the predicate - /// - /// The path pointing to the left side value inside the list - 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(); - } - - /// - /// Updates the right side of the predicate using a path and makes the predicate dynamic - /// - /// The path pointing to the right side value - 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; - } - - /// - /// Updates the right side of the predicate, makes the predicate static and re-compiles the expression - /// - /// The right side value to use - public void UpdateRightSideStatic(object? staticValue) - { - PredicateType = ProfileRightSideType.Static; - RightPath?.Dispose(); - RightPath = null; - - SetStaticValue(staticValue); - } - - /// - /// Updates the operator of the predicate and re-compiles the expression - /// - /// - 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 + + /// + 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 + /// /// Not supported for list predicates, always returns false /// @@ -158,45 +121,6 @@ namespace Artemis.Core return false; } - /// - /// Determines whether the provided path is contained inside the list - /// - /// The path to evaluate - 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 - - /// - 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 } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/Wrappers/EventPredicateWrapperDataModel.cs b/src/Artemis.Core/Models/Profile/Conditions/Wrappers/EventPredicateWrapperDataModel.cs new file mode 100644 index 000000000..aee8d2adc --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/Wrappers/EventPredicateWrapperDataModel.cs @@ -0,0 +1,37 @@ +using System; +using Artemis.Core.DataModelExpansions; + +namespace Artemis.Core +{ + internal class EventPredicateWrapperDataModel : EventPredicateWrapperDataModel + { + [DataModelProperty(Name = "Event arguments", Description = "The arguments provided when the event triggers")] + public T Arguments => (UntypedArguments is T typedArguments ? typedArguments : default)!; + } + + /// + /// Represents a datamodel that wraps the event arguments of an event + /// + public abstract class EventPredicateWrapperDataModel : DataModel + { + internal EventPredicateWrapperDataModel() + { + PluginInfo = Constants.CorePluginInfo; + } + + [DataModelIgnore] + public object? UntypedArguments { get; set; } + + /// + /// Creates a new instance of the class + /// + 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 for type {type.Name}"); + + return (EventPredicateWrapperDataModel) instance; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/ListPredicateWrapperDataModel.cs b/src/Artemis.Core/Models/Profile/Conditions/Wrappers/ListPredicateWrapperDataModel.cs similarity index 81% rename from src/Artemis.Core/Models/Profile/Conditions/ListPredicateWrapperDataModel.cs rename to src/Artemis.Core/Models/Profile/Conditions/Wrappers/ListPredicateWrapperDataModel.cs index 7c436e8aa..1c8a3d458 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/ListPredicateWrapperDataModel.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Wrappers/ListPredicateWrapperDataModel.cs @@ -9,6 +9,9 @@ namespace Artemis.Core public T Value => (UntypedValue is T typedValue ? typedValue : default)!; } + /// + /// Represents a datamodel that wraps a value in a list + /// public abstract class ListPredicateWrapperDataModel : DataModel { internal ListPredicateWrapperDataModel() @@ -19,6 +22,9 @@ namespace Artemis.Core [DataModelIgnore] public object? UntypedValue { get; set; } + /// + /// Creates a new instance of the class + /// public static ListPredicateWrapperDataModel Create(Type type) { object? instance = Activator.CreateInstance(typeof(ListPredicateWrapperDataModel<>).MakeGenericType(type)); diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs index c1e86d9cf..f62920e14 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs @@ -60,9 +60,9 @@ namespace Artemis.Core /// Gets ors ets the easing function of the data binding /// public Easings.Functions EasingFunction { get; set; } - + internal DataBindingEntity Entity { get; } - + /// /// Gets the current value of the data binding /// @@ -137,16 +137,26 @@ namespace Artemis.Core /// /// Updates the smoothing progress of the data binding /// - /// The time in seconds that passed since the last update - public void Update(double deltaTime) + /// The timeline to apply during update + public void Update(Timeline timeline) + { + UpdateWithDelta(timeline.Delta); + } + + /// + /// Updates the smoothing progress of the data binding + /// + /// The delta to apply during update + 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; } diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Modes/Conditional/ConditionalDataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/Modes/Conditional/ConditionalDataBinding.cs index 7b4e26a5b..765e0927c 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/Modes/Conditional/ConditionalDataBinding.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/Modes/Conditional/ConditionalDataBinding.cs @@ -75,7 +75,7 @@ namespace Artemis.Core return condition; } - + /// /// Removes a condition from the conditional data binding's collection and disposes it /// diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Modes/Direct/DataBindingModifier.cs b/src/Artemis.Core/Models/Profile/DataBindings/Modes/Direct/DataBindingModifier.cs index 05d4c0aba..10732dfd3 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/Modes/Direct/DataBindingModifier.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/Modes/Direct/DataBindingModifier.cs @@ -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(); diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelEvent.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelEvent.cs new file mode 100644 index 000000000..c9e2c32d8 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelEvent.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Artemis.Core.DataModelExpansions; + +namespace Artemis.Core +{ + /// + /// Represents a data model event with event arguments of type + /// + public class DataModelEvent : IDataModelEvent where T : DataModelEventArgs + { + private bool _trackHistory; + + /// + /// Creates a new instance of the class with history tracking disabled + /// + public DataModelEvent() + { + } + + /// + /// Creates a new instance of the + /// + /// A boolean indicating whether the last 20 events should be tracked + public DataModelEvent(bool trackHistory) + { + _trackHistory = trackHistory; + } + + /// + [DataModelProperty(Name = "Last event trigger", Description = "The time at which the event last triggered")] + public DateTime LastTrigger { get; private set; } + + /// + /// Gets the event arguments of the last time the event was triggered + /// + [DataModelProperty(Description = "The arguments of the last time this event triggered")] + public T? LastEventArguments { get; private set; } + + /// + [DataModelProperty(Description = "The total amount of times this event has triggered since the module was activated")] + public int TriggerCount { get; private set; } + + /// + /// Gets a queue of the last 20 event arguments + /// Always empty if is + /// + [DataModelProperty(Description = "The arguments of the last time this event triggered")] + public Queue EventArgumentsHistory { get; } = new Queue(20); + + /// + /// Trigger the event with the given + /// + /// The event argument to pass to the event + 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); + } + + /// + [DataModelIgnore] + public Type ArgumentsType => typeof(T); + + /// + [DataModelIgnore] + public bool TrackHistory + { + get => _trackHistory; + set + { + EventArgumentsHistory.Clear(); + _trackHistory = value; + } + } + + /// + [DataModelIgnore] + public DataModelEventArgs? LastEventArgumentsUntyped => LastEventArguments; + + /// + [DataModelIgnore] + public List EventArgumentsHistoryUntyped => EventArgumentsHistory.Cast().ToList(); + + /// + public event EventHandler? EventTriggered; + + /// + public void Reset() + { + TriggerCount = 0; + EventArgumentsHistory.Clear(); + } + } + + /// + /// Represents a data model event without event arguments + /// + public class DataModelEvent : IDataModelEvent + { + private bool _trackHistory; + + /// + /// Creates a new instance of the class with history tracking disabled + /// + public DataModelEvent() + { + } + + /// + /// Creates a new instance of the + /// + /// A boolean indicating whether the last 20 events should be tracked + public DataModelEvent(bool trackHistory) + { + _trackHistory = trackHistory; + } + + /// + [DataModelProperty(Name = "Last event trigger", Description = "The time at which the event last triggered")] + public DateTime LastTrigger { get; private set; } + + /// + /// Gets the event arguments of the last time the event was triggered + /// + [DataModelProperty(Description = "The arguments of the last time this event triggered")] + public DataModelEventArgs? LastEventArguments { get; private set; } + + /// + [DataModelProperty(Description = "The total amount of times this event has triggered since the module was activated")] + public int TriggerCount { get; private set; } + + /// + /// Gets a queue of the last 20 event arguments + /// Always empty if is + /// + [DataModelProperty(Description = "The arguments of the last time this event triggered")] + public Queue EventArgumentsHistory { get; } = new Queue(20); + + /// + /// Trigger the event + /// + 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); + } + + /// + [DataModelIgnore] + public Type ArgumentsType => typeof(DataModelEventArgs); + + /// + [DataModelIgnore] + public bool TrackHistory + { + get => _trackHistory; + set + { + EventArgumentsHistory.Clear(); + _trackHistory = value; + } + } + + /// + [DataModelIgnore] + public DataModelEventArgs? LastEventArgumentsUntyped => LastEventArguments; + + /// + [DataModelIgnore] + public List EventArgumentsHistoryUntyped => EventArgumentsHistory.ToList(); + + /// + public event EventHandler? EventTriggered; + + /// + public void Reset() + { + TriggerCount = 0; + EventArgumentsHistory.Clear(); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelEventArgs.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelEventArgs.cs new file mode 100644 index 000000000..3e20a9b1c --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelEventArgs.cs @@ -0,0 +1,15 @@ +using System; + +namespace Artemis.Core +{ + /// + /// Represents the base class for data model events that contain event data + /// + public class DataModelEventArgs + { + /// + /// Gets the time at which the event with these arguments was triggered + /// + public DateTime TriggerTime { get; internal set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs index 64f62ed68..054257402 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs @@ -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 /// public IReadOnlyCollection Segments => _segments.ToList().AsReadOnly(); - /// - /// Gets a boolean indicating whether this data model path points to a list - /// - public bool PointsToList - { - get - { - Type? type = GetPropertyType(); - return type?.IsGenericEnumerable() ?? false; - } - } - internal DataModelPathEntity Entity { get; } internal Func? 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 diff --git a/src/Artemis.Core/Models/Profile/DataModel/IDataModelEvent.cs b/src/Artemis.Core/Models/Profile/DataModel/IDataModelEvent.cs new file mode 100644 index 000000000..36b9c3824 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataModel/IDataModelEvent.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; + +namespace Artemis.Core +{ + internal interface IDataModelEvent + { + /// + /// Gets the last time the event was triggered + /// + DateTime LastTrigger { get; } + + /// + /// Gets the amount of times the event was triggered + /// + int TriggerCount { get; } + + /// + /// Gets the type of arguments this event contains + /// + Type ArgumentsType { get; } + + /// + /// Gets or sets a boolean indicating whether the last 20 events should be tracked + /// Note: setting this to will clear the current history + /// + bool TrackHistory { get; set; } + + /// + /// Gets the event arguments of the last time the event was triggered by its base type + /// + public DataModelEventArgs? LastEventArgumentsUntyped { get; } + + /// + /// Gets a list of the last 20 event arguments by their base type. + /// Always empty if is + /// + public List EventArgumentsHistoryUntyped { get; } + + /// + /// Fires when the event is triggered + /// + event EventHandler EventTriggered; + + /// + /// Resets the trigger count and history of this data model event + /// + void Reset(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs index e615ba920..6c28c72a1 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -30,19 +30,16 @@ namespace Artemis.Core Profile = Parent.Profile; Name = name; Enabled = true; - DisplayContinuously = true; _layerEffects = new List(); _expandedPropertyGroups = new List(); - 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(); _expandedPropertyGroups = new List(); + Load(); } + /// + /// Gets a boolean indicating whether this folder is at the root of the profile tree + /// + public bool IsRootFolder => Parent == Profile; + + /// + /// Gets the longest timeline of all this folders children + /// + public Timeline LongestChildTimeline { get; private set; } + internal FolderEntity FolderEntity { get; set; } + internal override RenderElementEntity RenderElementEntity => FolderEntity; + /// public override List GetAllLayerProperties() { List result = new List(); 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() + /// + public override void Reset() { - TimelineLength = !Children.Any() ? TimeSpan.Zero : Children.OfType().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) + /// + public override void AddChild(ProfileElement child, int? order = null) { - if (_disposed) + if (Disposed) throw new ObjectDisposedException("Folder"); - if (!Enabled) + base.AddChild(child, order); + CalculateRenderProperties(); + } + + /// + 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(); } - /// - public override void AddChild(ProfileElement child, int? order = null) - { - if (_disposed) - throw new ObjectDisposedException("Folder"); - - base.AddChild(child, order); - UpdateTimelineLength(); - CalculateRenderProperties(); - } - - /// - 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 diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 8e0e66ab2..70da32cd7 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -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(); 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; } - - /// - public override List GetAllLayerProperties() - { - List result = new List(); - 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; - /// /// A collection of all the LEDs this layer is assigned to. /// @@ -131,6 +108,25 @@ namespace Artemis.Core internal set => SetAndNotify(ref _layerBrush, value); } + internal LayerEntity LayerEntity { get; set; } + + internal override RenderElementEntity RenderElementEntity => LayerEntity; + + /// + public override List GetAllLayerProperties() + { + List result = new List(); + 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; + } + /// public override string ToString() { @@ -142,7 +138,7 @@ namespace Artemis.Core /// 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 /// 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; } /// - public override void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo) + public override void Reset() { - if (_disposed) + DisplayConditionMet = false; + Timeline.JumpToStart(); + } + + /// + 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 /// 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 } } + /// + /// Creates a transformation matrix that applies the current transformation settings + /// + /// + /// If true, treats the layer as if it is located at 0,0 instead of its actual position on the + /// surface + /// + /// The transformation matrix containing the current transformation settings + 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; + } + } + /// /// Excludes the provided path from the translations applied to the layer by applying translations that cancel the /// layer translations out /// 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 /// The number of transformations applied 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 /// The LED to add public void AddLed(ArtemisLed led) { - if (_disposed) + if (Disposed) throw new ObjectDisposedException("Layer"); _leds.Add(led); @@ -643,7 +647,7 @@ namespace Artemis.Core /// The LEDs to add public void AddLeds(IEnumerable leds) { - if (_disposed) + if (Disposed) throw new ObjectDisposedException("Layer"); _leds.AddRange(leds); @@ -656,7 +660,7 @@ namespace Artemis.Core /// The LED to remove public void RemoveLed(ArtemisLed led) { - if (_disposed) + if (Disposed) throw new ObjectDisposedException("Layer"); _leds.Remove(led); @@ -668,7 +672,7 @@ namespace Artemis.Core /// 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 leds = new List(); diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs index aaa36796b..512b63213 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs @@ -11,7 +11,7 @@ namespace Artemis.Core /// initialize these for you. /// /// - public interface ILayerProperty : IStorageModel, IUpdateModel, IDisposable + public interface ILayerProperty : IStorageModel, IDisposable { /// /// Gets the description attribute applied to this property @@ -36,5 +36,11 @@ namespace Artemis.Core /// Returns a list off all data binding registrations /// List GetAllDataBindingRegistrations(); + + /// + /// Updates the layer properties internal state + /// + /// The timeline to apply to the property + void Update(Timeline timeline); } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 910ca1324..39a18a503 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -34,20 +34,16 @@ namespace Artemis.Core /// public string Path { get; private set; } - /// - /// Updates the property, applying keyframes and data bindings to the current value - /// - public void Update(double deltaTime) + /// + 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(); } /// @@ -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(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); } /// @@ -485,12 +489,10 @@ namespace Artemis.Core _keyframes.Clear(); try { - _keyframes.AddRange(Entity.KeyframeEntities.Select(k => new LayerPropertyKeyframe( - JsonConvert.DeserializeObject(k.Value), - k.Position, - (Easings.Functions) k.EasingFunction, - this - ))); + _keyframes.AddRange( + Entity.KeyframeEntities.Where(k => k.Position <= ProfileElement.Timeline.Length) + .Select(k => new LayerPropertyKeyframe(JsonConvert.DeserializeObject(k.Value), k.Position, (Easings.Functions) k.EasingFunction, this)) + ); } catch (JsonException e) { diff --git a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs index c127199ef..48afecab9 100644 --- a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs +++ b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs @@ -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 PropertyGroupUpdating; - /// /// Occurs when the property group has initialized all its children /// @@ -284,11 +283,6 @@ namespace Artemis.Core /// public event EventHandler VisibilityChanged; - internal virtual void OnPropertyGroupUpdating(LayerPropertyGroupUpdatingEventArgs e) - { - PropertyGroupUpdating?.Invoke(this, e); - } - internal virtual void OnVisibilityChanged() { VisibilityChanged?.Invoke(this, EventArgs.Empty); diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs index cc224c313..786ed8369 100644 --- a/src/Artemis.Core/Models/Profile/Profile.cs +++ b/src/Artemis.Core/Models/Profile/Profile.cs @@ -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); } } + /// + 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()) diff --git a/src/Artemis.Core/Models/Profile/ProfileElement.cs b/src/Artemis.Core/Models/Profile/ProfileElement.cs index 836d8c2e3..56b86b240 100644 --- a/src/Artemis.Core/Models/Profile/ProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/ProfileElement.cs @@ -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 ChildrenList; protected ProfileElement() @@ -91,7 +91,12 @@ namespace Artemis.Core /// /// Renders the element /// - public abstract void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo); + public abstract void Render(SKCanvas canvas, SKImageInfo canvasInfo); + + /// + /// Resets the internal state of the element + /// + public abstract void Reset(); /// public override string ToString() @@ -108,9 +113,9 @@ namespace Artemis.Core /// The order where to place the child (1-based), defaults to the end of the collection public virtual void AddChild(ProfileElement child, int? order = null) { - if (_disposed) + if (Disposed) throw new ObjectDisposedException(GetType().Name); - + lock (ChildrenList) { if (ChildrenList.Contains(child)) @@ -152,7 +157,7 @@ namespace Artemis.Core /// The profile element to remove public virtual void RemoveChild(ProfileElement child) { - if (_disposed) + if (Disposed) throw new ObjectDisposedException(GetType().Name); lock (ChildrenList) @@ -175,7 +180,7 @@ namespace Artemis.Core /// public List GetAllFolders() { - if (_disposed) + if (Disposed) throw new ObjectDisposedException(GetType().Name); List folders = new List(); @@ -196,7 +201,7 @@ namespace Artemis.Core /// public List GetAllLayers() { - if (_disposed) + if (Disposed) throw new ObjectDisposedException(GetType().Name); List layers = new List(); diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index a30f31c17..27fe3246d 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -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 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 + + /// + /// Gets the timeline associated with this render element + /// + public Timeline Timeline { get; private set; } + + /// + /// Updates the according to the provided and current display condition status + /// + 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; - - /// - /// Gets or sets the length of the start segment - /// - public TimeSpan StartSegmentLength - { - get => _startSegmentLength; - set - { - if (!SetAndNotify(ref _startSegmentLength, value)) return; - UpdateTimelineLength(); - if (Parent is RenderProfileElement renderElement) - renderElement.UpdateTimelineLength(); - } - } - - /// - /// Gets or sets the length of the main segment - /// - public TimeSpan MainSegmentLength - { - get => _mainSegmentLength; - set - { - if (!SetAndNotify(ref _mainSegmentLength, value)) return; - UpdateTimelineLength(); - if (Parent is RenderProfileElement renderElement) - renderElement.UpdateTimelineLength(); - } - } - - /// - /// Gets or sets the length of the end segment - /// - public TimeSpan EndSegmentLength - { - get => _endSegmentLength; - set - { - if (!SetAndNotify(ref _endSegmentLength, value)) return; - UpdateTimelineLength(); - if (Parent is RenderProfileElement renderElement) - renderElement.UpdateTimelineLength(); - } - } - - /// - /// Gets the current timeline position - /// - public TimeSpan TimelinePosition - { - get => _timelinePosition; - protected set => SetAndNotify(ref _timelinePosition, value); - } - - /// - /// Gets or sets whether main timeline should repeat itself as long as display conditions are met - /// - public bool DisplayContinuously - { - get => _displayContinuously; - set => SetAndNotify(ref _displayContinuously, value); - } - - /// - /// Gets or sets whether the timeline should finish when conditions are no longer met - /// - public bool AlwaysFinishTimeline - { - get => _alwaysFinishTimeline; - set => SetAndNotify(ref _alwaysFinishTimeline, value); - } - - /// - /// Gets the max length of this element and any of its children - /// - 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(); - - /// - /// Overrides the progress of the element - /// - /// - /// - public abstract void OverrideProgress(TimeSpan timeOverride, bool stickToMainSegment); - - #endregion - #region Effect management protected List _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; /// @@ -409,20 +299,46 @@ namespace Artemis.Core } /// - /// 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 /// - public bool ApplyKeyframesEnabled { get; set; } - - /// - /// Gets or sets whether data bindings should be applied when this profile element updates - /// - 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; } diff --git a/src/Artemis.Core/Models/Profile/Timeline.cs b/src/Artemis.Core/Models/Profile/Timeline.cs new file mode 100644 index 000000000..6397b34c9 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Timeline.cs @@ -0,0 +1,493 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Artemis.Storage.Entities.Profile; +using Stylet; + +namespace Artemis.Core +{ + /// + /// Represents a timeline used by profile elements + /// + public class Timeline : PropertyChangedBase, IStorageModel + { + /// + /// Creates a new instance of the class + /// + public Timeline() + { + Entity = new TimelineEntity(); + _extraTimelines = new List(); + MainSegmentLength = TimeSpan.FromSeconds(5); + + Save(); + } + + internal Timeline(TimelineEntity entity) + { + Entity = entity; + _extraTimelines = new List(); + + Load(); + } + + private Timeline(Timeline parent) + { + Parent = parent; + } + + #region Extra timelines + + /// + /// Adds an extra timeline to this timeline + /// + public void AddExtraTimeline() + { + _extraTimelines.Add(new Timeline(this)); + } + + /// + /// Removes all extra timelines from this timeline + /// + 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 _extraTimelines; + private TimeSpan _startSegmentLength; + private TimeSpan _mainSegmentLength; + private TimeSpan _endSegmentLength; + + /// + /// Gets the parent this timeline is an extra timeline of + /// + public Timeline? Parent { get; } + + /// + /// Gets the current position of the timeline + /// + public TimeSpan Position + { + get => _position; + private set => SetAndNotify(ref _position, value); + } + + /// + /// Gets the cumulative delta of all calls to that took place after the last call to + /// + /// Note: If this is an extra timeline is always equal to + /// + /// + public TimeSpan Delta + { + get => Parent == null ? _lastDelta : DeltaToParent; + private set => SetAndNotify(ref _lastDelta, value); + } + + /// + /// Gets the delta to this timeline's + /// + public TimeSpan DeltaToParent => Parent != null ? Position - Parent.Position : TimeSpan.Zero; + + /// + /// Gets or sets the mode in which the render element starts its timeline when display conditions are met + /// + public TimelinePlayMode PlayMode + { + get => _playMode; + set => SetAndNotify(ref _playMode, value); + } + + /// + /// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met + /// + public TimelineStopMode StopMode + { + get => _stopMode; + set => SetAndNotify(ref _stopMode, value); + } + + /// + /// Gets or sets the mode in which the render element responds to display condition events being fired before the + /// timeline finished + /// + public TimeLineEventOverlapMode EventOverlapMode + { + get => _eventOverlapMode; + set => SetAndNotify(ref _eventOverlapMode, value); + } + + /// + /// Gets a list of extra copies of the timeline applied to this timeline + /// + public ReadOnlyCollection ExtraTimelines => _extraTimelines.AsReadOnly(); + + /// + /// Gets a boolean indicating whether the timeline has finished its run + /// + public bool IsFinished => Position > Length || Length == TimeSpan.Zero; + + /// + /// Gets a boolean indicating whether the timeline progress has been overridden + /// + public bool IsOverridden { get; private set; } + + #region Segments + + /// + /// Gets the total length of this timeline + /// + public TimeSpan Length => StartSegmentLength + MainSegmentLength + EndSegmentLength; + + /// + /// Gets or sets the length of the start segment + /// + public TimeSpan StartSegmentLength + { + get => _startSegmentLength; + set + { + if (SetAndNotify(ref _startSegmentLength, value)) + NotifySegmentShiftAt(TimelineSegment.Start, false); + } + } + + /// + /// Gets or sets the length of the main segment + /// + public TimeSpan MainSegmentLength + { + get => _mainSegmentLength; + set + { + if (SetAndNotify(ref _mainSegmentLength, value)) + NotifySegmentShiftAt(TimelineSegment.Main, false); + } + } + + /// + /// Gets or sets the length of the end segment + /// + public TimeSpan EndSegmentLength + { + get => _endSegmentLength; + set + { + if (SetAndNotify(ref _endSegmentLength, value)) + NotifySegmentShiftAt(TimelineSegment.End, false); + } + } + + /// + /// Gets or sets the start position of the main segment + /// + public TimeSpan MainSegmentStartPosition + { + get => StartSegmentEndPosition; + set + { + StartSegmentEndPosition = value; + NotifySegmentShiftAt(TimelineSegment.Main, true); + } + } + + /// + /// Gets or sets the end position of the end segment + /// + public TimeSpan EndSegmentStartPosition + { + get => MainSegmentEndPosition; + set + { + MainSegmentEndPosition = value; + NotifySegmentShiftAt(TimelineSegment.End, true); + } + } + + /// + /// Gets or sets the end position of the start segment + /// + public TimeSpan StartSegmentEndPosition + { + get => StartSegmentLength; + set + { + StartSegmentLength = value; + NotifySegmentShiftAt(TimelineSegment.Start, false); + } + } + + /// + /// Gets or sets the end position of the main segment + /// + public TimeSpan MainSegmentEndPosition + { + get => StartSegmentEndPosition + MainSegmentLength; + set + { + MainSegmentLength = value - StartSegmentEndPosition >= TimeSpan.Zero ? value - StartSegmentEndPosition : TimeSpan.Zero; + NotifySegmentShiftAt(TimelineSegment.Main, false); + } + } + + /// + /// Gets or sets the end position of the end segment + /// + 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; } + + /// + /// Notifies the right segments in a way that I don't have to think about it + /// + /// The segment that was updated + /// Whether the start point of the was updated + 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 + + /// + /// Updates the timeline, applying the provided to the + /// + /// The amount of time to apply to the position + /// Whether to stick to the main segment, wrapping around if needed + 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); + } + } + + /// + /// Moves the position of the timeline backwards to the very start of the timeline + /// + public void JumpToStart() + { + lock (this) + { + if (Position == TimeSpan.Zero) + return; + + Delta = TimeSpan.Zero - Position; + Position = TimeSpan.Zero; + + _extraTimelines.Clear(); + } + } + + /// + /// Moves the position of the timeline forwards to the beginning of the end segment + /// + public void JumpToEndSegment() + { + lock (this) + { + if (Position >= EndSegmentStartPosition) + return; + + Delta = EndSegmentStartPosition - Position; + Position = EndSegmentStartPosition; + + _extraTimelines.Clear(); + } + } + + /// + /// Moves the position of the timeline forwards to the very end of the timeline + /// + public void JumpToEnd() + { + lock (this) + { + if (Position >= EndSegmentEndPosition) + return; + + Delta = EndSegmentEndPosition - Position; + Position = EndSegmentEndPosition; + + _extraTimelines.Clear(); + } + } + + /// + /// Overrides the to the specified time and clears any extra time lines + /// + /// The position to set the timeline to + /// Whether to stick to the main segment, wrapping around if needed + 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(); + } + } + + /// + /// Sets the to + /// + public void ClearDelta() + { + lock (this) + { + Delta = TimeSpan.Zero; + } + } + + #endregion + + #region Storage + + /// + public void Load() + { + StartSegmentLength = Entity.StartSegmentLength; + MainSegmentLength = Entity.MainSegmentLength; + EndSegmentLength = Entity.EndSegmentLength; + PlayMode = (TimelinePlayMode) Entity.PlayMode; + StopMode = (TimelineStopMode) Entity.StopMode; + EventOverlapMode = (TimeLineEventOverlapMode) Entity.EventOverlapMode; + } + + /// + 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 + } + + /// + /// Represents a mode for render elements to start their timeline when display conditions are met + /// + public enum TimelinePlayMode + { + /// + /// Continue repeating the main segment of the timeline while the condition is met + /// + Repeat, + + /// + /// Only play the timeline once when the condition is met + /// + Once + } + + /// + /// Represents a mode for render elements to stop their timeline when display conditions are no longer met + /// + public enum TimelineStopMode + { + /// + /// When conditions are no longer met, finish the the current run of the main timeline + /// + Finish, + + /// + /// When conditions are no longer met, skip to the end segment of the timeline + /// + SkipToEnd + } + + /// + /// Represents a mode for render elements to start their timeline when display conditions events are fired + /// + public enum TimeLineEventOverlapMode + { + /// + /// Stop the current run and restart the timeline + /// + Restart, + + /// + /// Ignore subsequent event fires until the timeline finishes + /// + Ignore, + + /// + /// Play another copy of the timeline on top of the current run + /// + Copy + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs b/src/Artemis.Core/Plugins/Modules/ProfileModule.cs index 8f1930609..d72015308 100644 --- a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs +++ b/src/Artemis.Core/Plugins/Modules/ProfileModule.cs @@ -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 /// /// Gets the currently active profile /// - public Profile ActiveProfile { get; private set; } + public Profile? ActiveProfile { get; private set; } /// /// 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); diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs index d3ba8b262..5abb34c91 100644 --- a/src/Artemis.Core/Plugins/Plugin.cs +++ b/src/Artemis.Core/Plugins/Plugin.cs @@ -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 /// - /// The resulting plugin update registration + /// The resulting plugin update registration which can be used to stop the update public PluginUpdateRegistration AddTimedUpdate(TimeSpan interval, Action action) { if (action == null) diff --git a/src/Artemis.Core/Resources/intro-profile.json b/src/Artemis.Core/Resources/intro-profile.json index abd185a69..972e89c7c 100644 --- a/src/Artemis.Core/Resources/intro-profile.json +++ b/src/Artemis.Core/Resources/intro-profile.json @@ -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 } } ] diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index 824649181..7901cb0a0 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -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 {new SKColorConverter()} + Converters = new List {new SKColorConverter(), new ForgivingIntConverter()} }; } diff --git a/src/Artemis.Core/Services/PluginService.cs b/src/Artemis.Core/Services/PluginService.cs index fa328b720..ec583c2d8 100644 --- a/src/Artemis.Core/Services/PluginService.cs +++ b/src/Artemis.Core/Services/PluginService.cs @@ -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); diff --git a/src/Artemis.Core/Services/Registration/DataBindingService.cs b/src/Artemis.Core/Services/Registration/DataBindingService.cs index 28218cf65..96dedfe50 100644 --- a/src/Artemis.Core/Services/Registration/DataBindingService.cs +++ b/src/Artemis.Core/Services/Registration/DataBindingService.cs @@ -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()); diff --git a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs index ac2bd2d04..d7a1d9787 100644 --- a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs +++ b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs @@ -49,6 +49,11 @@ namespace Artemis.Core.Services /// void ActivateLastProfile(ProfileModule profileModule); + /// + /// Reloads the currently active profile on the provided profile module + /// + void ReloadProfile(ProfileModule module); + /// /// Asynchronously activates the last profile of the given profile module using a fade animation /// diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index 32512f627..880ff1ab1 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -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 ActivateProfileAnimated(ProfileDescriptor profileDescriptor) { if (profileDescriptor.ProfileModule.ActiveProfile?.EntityId == profileDescriptor.Id) diff --git a/src/Artemis.Core/Utilities/DeserializationLogger.cs b/src/Artemis.Core/Utilities/DeserializationLogger.cs index e1c176283..de4424def 100644 --- a/src/Artemis.Core/Utilities/DeserializationLogger.cs +++ b/src/Artemis.Core/Utilities/DeserializationLogger.cs @@ -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); diff --git a/src/Artemis.Core/Utilities/IntroAnimation.cs b/src/Artemis.Core/Utilities/IntroAnimation.cs index 63008a8dc..3e4b90eb8 100644 --- a/src/Artemis.Core/Utilities/IntroAnimation.cs +++ b/src/Artemis.Core/Utilities/IntroAnimation.cs @@ -32,7 +32,7 @@ namespace Artemis.Core return; AnimationProfile.Update(deltaTime); - AnimationProfile.Render(deltaTime, canvas, bitmapInfo); + AnimationProfile.Render(canvas, bitmapInfo); } private void CreateIntroProfile() diff --git a/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs b/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs index 7e7556e36..d8f7ded16 100644 --- a/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs @@ -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 LayerEffects { get; set; } public List PropertyEntities { get; set; } public List ExpandedPropertyGroups { get; set; } public DataModelConditionGroupEntity DisplayCondition { get; set; } + public TimelineEntity Timeline { get; set; } } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionEventEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionEventEntity.cs new file mode 100644 index 000000000..141f5bb08 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionEventEntity.cs @@ -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(); + } + + public DataModelPathEntity EventPath { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionEventPredicateEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionEventPredicateEntity.cs new file mode 100644 index 000000000..fee134489 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionEventPredicateEntity.cs @@ -0,0 +1,6 @@ +namespace Artemis.Storage.Entities.Profile.Conditions +{ + public class DataModelConditionEventPredicateEntity : DataModelConditionPredicateEntity + { + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionGeneralPredicateEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionGeneralPredicateEntity.cs new file mode 100644 index 000000000..1a0fa93ea --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionGeneralPredicateEntity.cs @@ -0,0 +1,6 @@ +namespace Artemis.Storage.Entities.Profile.Conditions +{ + public class DataModelConditionGeneralPredicateEntity : DataModelConditionPredicateEntity + { + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionListEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionListEntity.cs index 8c8959ab4..fd812cc41 100644 --- a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionListEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionListEntity.cs @@ -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 diff --git a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionListPredicateEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionListPredicateEntity.cs index 513a9af43..cfc4b5372 100644 --- a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionListPredicateEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionListPredicateEntity.cs @@ -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; } - } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionPredicateEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionPredicateEntity.cs index 08ee41d25..c913a6a3c 100644 --- a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionPredicateEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionPredicateEntity.cs @@ -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; } - } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/DataModelPathEntity.cs b/src/Artemis.Storage/Entities/Profile/DataModelPathEntity.cs index b1e9a3241..b4a2b03b5 100644 --- a/src/Artemis.Storage/Entities/Profile/DataModelPathEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/DataModelPathEntity.cs @@ -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 } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/TimelineEntity.cs b/src/Artemis.Storage/Entities/Profile/TimelineEntity.cs new file mode 100644 index 000000000..6d17188c6 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/TimelineEntity.cs @@ -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; } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Migrations/M4ProfileSegments.cs b/src/Artemis.Storage/Migrations/M4ProfileSegments.cs index d5c85542e..c54e3dc0f 100644 --- a/src/Artemis.Storage/Migrations/M4ProfileSegments.cs +++ b/src/Artemis.Storage/Migrations/M4ProfileSegments.cs @@ -13,31 +13,32 @@ namespace Artemis.Storage.Migrations public void Apply(LiteRepository repository) { - List profiles = repository.Query().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 profiles = repository.Query().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); + // } } } } \ No newline at end of file diff --git a/src/Artemis.Storage/Migrations/M6PredicateAbstraction.cs b/src/Artemis.Storage/Migrations/M6PredicateAbstraction.cs new file mode 100644 index 000000000..339b42358 --- /dev/null +++ b/src/Artemis.Storage/Migrations/M6PredicateAbstraction.cs @@ -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 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); + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings index 6cbf8796d..1292ea906 100644 --- a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings +++ b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings @@ -1,2 +1,15 @@  + True + True + True + True + True + True + True + True + True + True + True + True + True True \ No newline at end of file diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs index a4502e62f..88f1a0933 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs @@ -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) @@ -197,10 +203,15 @@ namespace Artemis.UI.Shared.Input { if (!IsDataModelViewModelOpen) return; + + UpdateDataModelVisualization(); + } - DataModelViewModel.Update(_dataModelUIService); + private void UpdateDataModelVisualization() + { + DataModelViewModel.Update(_dataModelUIService, new DataModelUpdateConfiguration(LoadEventChildren)); foreach (DataModelPropertiesViewModel extraDataModelViewModel in ExtraDataModelViewModels) - extraDataModelViewModel.Update(_dataModelUIService); + extraDataModelViewModel.Update(_dataModelUIService, new DataModelUpdateConfiguration(LoadEventChildren)); } #endregion diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelStaticView.xaml b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelStaticView.xaml index 3caf803cf..a069d383a 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelStaticView.xaml +++ b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelStaticView.xaml @@ -16,12 +16,21 @@ + - - + - + + + + + + triggered + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionEventViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionEventViewModel.cs new file mode 100644 index 000000000..324dd7e7e --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionEventViewModel.cs @@ -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 editors = _dataModelUIService.RegisteredDataModelEditors; + List supportedInputTypes = new List {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 viewModels = new List(); + 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 + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionGroupView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionGroupView.xaml index 51db5e501..0e4f1b687 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionGroupView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionGroupView.xaml @@ -17,6 +17,7 @@ + @@ -45,13 +46,24 @@ - + diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionGroupViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionGroupViewModel.cs index 48dbb28b9..0af34024f 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionGroupViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionGroupViewModel.cs @@ -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 viewModels = new List(); 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) 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 + } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListPredicateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListPredicateViewModel.cs deleted file mode 100644 index 658ac8c92..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListPredicateViewModel.cs +++ /dev/null @@ -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 _operators; - private DataModelStaticViewModel _rightSideInputViewModel; - private DataModelDynamicViewModel _rightSideSelectionViewModel; - private BaseConditionOperator _selectedOperator; - - private List _supportedInputTypes; - - public DataModelConditionListPredicateViewModel( - DataModelConditionListPredicate dataModelConditionListPredicate, - IProfileEditorService profileEditorService, - IDataModelUIService dataModelUIService, - IConditionOperatorService conditionOperatorService) : base(dataModelConditionListPredicate) - { - _profileEditorService = profileEditorService; - _dataModelUIService = dataModelUIService; - _conditionOperatorService = conditionOperatorService; - _supportedInputTypes = new List(); - - SelectOperatorCommand = new DelegateCommand(ExecuteSelectOperatorCommand); - Operators = new BindableCollection(); - - Initialize(); - } - - public DataModelConditionListPredicate DataModelConditionListPredicate => (DataModelConditionListPredicate) Model; - - public BindableCollection 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 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 - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListView.xaml index 7ad1e3265..f19dd3a0c 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListView.xaml @@ -16,7 +16,7 @@ - + @@ -41,7 +41,7 @@ diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListViewModel.cs index ba875de53..c1da5d680 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListViewModel.cs @@ -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(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,38 +65,37 @@ 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 editors = _dataModelUIService.RegisteredDataModelEditors; List 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(); } - + 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; + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateView.xaml new file mode 100644 index 000000000..3fb1a1bd9 --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateView.xaml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionPredicateView.xaml.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateView.xaml.cs similarity index 75% rename from src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionPredicateView.xaml.cs rename to src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateView.xaml.cs index 0891cd5fc..5ce43cf28 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionPredicateView.xaml.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateView.xaml.cs @@ -4,11 +4,11 @@ using System.Windows.Controls; namespace Artemis.UI.Screens.ProfileEditor.Conditions { /// - /// Interaction logic for DataModelConditionPredicateView.xaml + /// Interaction logic for DataModelConditionEventPredicateView.xaml /// - public partial class DataModelConditionPredicateView : UserControl + public partial class DataModelConditionEventPredicateView : UserControl { - public DataModelConditionPredicateView() + public DataModelConditionEventPredicateView() { InitializeComponent(); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateViewModel.cs new file mode 100644 index 000000000..75326fef2 --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateViewModel.cs @@ -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 GetSupportedInputTypes() + { + IReadOnlyCollection editors = _dataModelUIService.RegisteredDataModelEditors; + List 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 GetExtraRightSideDataModelViewModels() + { + // Extra data models are expected to not have an empty root, so lets return the child + return GetEventDataModel().Children.Cast().ToList(); + } + + private DataModelPropertiesViewModel GetEventDataModel() + { + EventPredicateWrapperDataModel wrapper = EventPredicateWrapperDataModel.Create( + DataModelConditionEventPredicate.DataModelConditionEvent.EventArgumentType + ); + + return wrapper.CreateViewModel(_dataModelUIService, new DataModelUpdateConfiguration(false)); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionPredicateView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateView.xaml similarity index 97% rename from src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionPredicateView.xaml rename to src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateView.xaml index 8bdc6a14e..dfcac88d1 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionPredicateView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateView.xaml @@ -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}}"> @@ -21,7 +21,7 @@ - + diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateView.xaml.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateView.xaml.cs new file mode 100644 index 000000000..22411462b --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateView.xaml.cs @@ -0,0 +1,26 @@ +using System.Windows; +using System.Windows.Controls; + +namespace Artemis.UI.Screens.ProfileEditor.Conditions +{ + /// + /// Interaction logic for DataModelConditionGeneralPredicateView.xaml + /// + 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; + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateViewModel.cs new file mode 100644 index 000000000..5c7f781c5 --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateViewModel.cs @@ -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 GetSupportedInputTypes() + { + IReadOnlyCollection editors = _dataModelUIService.RegisteredDataModelEditors; + List 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(); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListPredicateView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateView.xaml similarity index 83% rename from src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListPredicateView.xaml rename to src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateView.xaml index 7b6ba3bb8..b22c824d3 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListPredicateView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateView.xaml @@ -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 @@ - - - - - - - - - - + @@ -50,7 +40,7 @@ - + (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 GetSupportedInputTypes() + { + IReadOnlyCollection editors = _dataModelUIService.RegisteredDataModelEditors; + List 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 GetExtraRightSideDataModelViewModels() + { + if (GetListDataModel()?.Children?.FirstOrDefault() is DataModelPropertiesViewModel listValue) + return new List {listValue}; + return null; + } + + private DataModelPropertiesViewModel GetListDataModel() + { + ListPredicateWrapperDataModel wrapper = ListPredicateWrapperDataModel.Create( + DataModelConditionListPredicate.DataModelConditionList.ListType + ); + + return wrapper.CreateViewModel(_dataModelUIService, new DataModelUpdateConfiguration(true)); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml index 838d58ddf..cc49f3d59 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml @@ -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}}"> + @@ -56,9 +58,9 @@ - + - + @@ -67,19 +69,15 @@ - - - - - - - Configure how the layer should act while the conditions above are met - - - - - - + + + + + Configure how the layer should act while the conditions above are met + + + + @@ -98,10 +96,10 @@ - - - REPEAT - + + + REPEAT + - - - ONCE - + + + ONCE + - - - - - - - Configure how the layer should act when the conditions above are no longer met - - - - - + + + + + Configure how the layer should act when the conditions above are no longer met + + + + @@ -154,10 +149,10 @@ - - - FINISH - + + + FINISH + - - - SKIP TO END - + + + SKIP TO END + + + + + + + + + + + + + + + + + + Configure how the layer should act when the event(s) trigger before the timeline finishes + + + + + + + + + + + + + + + RESTART + + + + + Stop the current run and restart the timeline + + + + + + + + IGNORE + + + + + Ignore subsequent event fires until the timeline finishes + + + + + + + + COPY + + + + + Play another copy of the timeline on top of the current run + + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs index b5f75e0c9..c4646dd37 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs @@ -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); } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeView.xaml index f28be532a..bed240696 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeView.xaml @@ -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"> - - - - @@ -27,43 +22,16 @@ : Screen, IDataBindingModeViewModel + public class ConditionalDataBindingModeViewModel : Conductor>.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>(); - - Initialize(); } public ConditionalDataBinding ConditionalDataBinding { get; } - public BindableCollection> ConditionViewModels { get; } + + public void AddCondition() + { + DataBindingCondition condition = ConditionalDataBinding.AddCondition(); + + // Find the VM of the new condition + DataBindingConditionViewModel 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> toRemove = Items.Where(c => !ConditionalDataBinding.Conditions.Contains(c.DataBindingCondition)).ToList(); + foreach (DataBindingConditionViewModel dataBindingConditionViewModel in toRemove) + { + Items.Remove(dataBindingConditionViewModel); + dataBindingConditionViewModel.Dispose(); + } + + // Add missing VMs + foreach (DataBindingCondition condition in ConditionalDataBinding.Conditions) + if (Items.All(c => c.DataBindingCondition != condition)) + Items.Add(_dataBindingsVmFactory.DataBindingConditionViewModel(condition)); + + // Fix order + ((BindableCollection>) 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 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 conditionViewModel in ConditionViewModels) + foreach (DataBindingConditionViewModel conditionViewModel in Items) conditionViewModel.Dispose(); - ConditionViewModels.Clear(); + Items.Clear(); } #endregion - - private void UpdateConditionViewModels() - { - _updating = true; - - // Remove old VMs - List> toRemove = ConditionViewModels.Where(c => !ConditionalDataBinding.Conditions.Contains(c.DataBindingCondition)).ToList(); - foreach (DataBindingConditionViewModel dataBindingConditionViewModel in toRemove) - { - ConditionViewModels.Remove(dataBindingConditionViewModel); - dataBindingConditionViewModel.Dispose(); - } - - // Add missing VMs - foreach (DataBindingCondition 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 conditionViewModel = ConditionViewModels[index]; - conditionViewModel.DataBindingCondition.Order = index + 1; - } - - ConditionalDataBinding.ApplyOrder(); - - _profileEditorService.UpdateSelectedProfileElement(); - } - - private void ConditionalDataBindingOnConditionsUpdated(object sender, EventArgs e) - { - UpdateConditionViewModels(); - } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/DataBindingConditionView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/DataBindingConditionView.xaml index 4b068ef90..f3e1833c6 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/DataBindingConditionView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/DataBindingConditionView.xaml @@ -18,7 +18,7 @@ IsTabStop="False" /> : Conductor, IDisposable { private readonly IProfileEditorService _profileEditorService; + private readonly IDataModelConditionsVmFactory _dataModelConditionsVmFactory; + private readonly IDataModelUIService _dataModelUIService; public DataBindingConditionViewModel(DataBindingCondition 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 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(); diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs index 153e9ca40..95d873b28 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs @@ -44,7 +44,11 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings EasingViewModels = new BindableCollection(); 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) { diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs index 81e4c9e12..bfe1f5a54 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs @@ -48,7 +48,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings // and creating the actual data bindings foreach (IDataBindingRegistration registration in registrations) Items.Add(_dataBindingsVmFactory.DataBindingViewModel(registration)); - + SelectedItemIndex = 0; } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierViewModel.cs index a77871291..65620f0e5 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierViewModel.cs @@ -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) diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs index 3ed17124f..41401ab7e 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs @@ -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(); } } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineKeyframeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineKeyframeViewModel.cs index 92e96040b..6c9b8e7d5 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineKeyframeViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineKeyframeViewModel.cs @@ -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 diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyViewModel.cs index 888d0db50..066b66cdb 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyViewModel.cs @@ -56,10 +56,10 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline start ??= TimeSpan.Zero; end ??= TimeSpan.MaxValue; - List> toShift = LayerProperty.Keyframes.Where(k => k.Position >= start && k.Position < end).ToList(); + List> toShift = LayerProperty.Keyframes.Where(k => k.Position > start && k.Position < end).ToList(); foreach (LayerPropertyKeyframe keyframe in toShift) keyframe.Position += amount; - + UpdateKeyframes(); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs index 578bbf7a2..974a64b66 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs @@ -48,7 +48,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline ProfileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged; ProfileEditorService.ProfileElementSelected += ProfileEditorServiceOnProfileElementSelected; if (ProfileEditorService.SelectedProfileElement != null) - ProfileEditorService.SelectedProfileElement.PropertyChanged += SelectedProfileElementOnPropertyChanged; + ProfileEditorService.SelectedProfileElement.Timeline.PropertyChanged += TimelineOnPropertyChanged; Update(); } @@ -104,15 +104,13 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline { if (Segment != SegmentViewModelType.Main) return false; - - return SelectedProfileElement?.DisplayContinuously ?? false; + return SelectedProfileElement?.Timeline.PlayMode == TimelinePlayMode.Repeat; } set { - if (Segment != SegmentViewModelType.Main) + if (Segment != SegmentViewModelType.Main || SelectedProfileElement == null) return; - - SelectedProfileElement.DisplayContinuously = value; + SelectedProfileElement.Timeline.PlayMode = value ? TimelinePlayMode.Repeat : TimelinePlayMode.Once; ProfileEditorService.UpdateSelectedProfileElement(); NotifyOfPropertyChange(nameof(RepeatSegment)); } @@ -158,32 +156,35 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline { if (SelectedProfileElement == null) { - SegmentLength = TimeSpan.Zero; SegmentStart = TimeSpan.Zero; SegmentEnd = TimeSpan.Zero; + SegmentLength = TimeSpan.Zero; SegmentStartPosition = 0; SegmentWidth = 0; SegmentEnabled = false; return; } + // It would be nice to do this differently if this patterns end up appearing in more places if (Segment == SegmentViewModelType.Start) { - SegmentLength = SelectedProfileElement.StartSegmentLength; SegmentStart = TimeSpan.Zero; + SegmentEnd = SelectedProfileElement.Timeline.StartSegmentEndPosition; + SegmentLength = SelectedProfileElement.Timeline.StartSegmentLength; } else if (Segment == SegmentViewModelType.Main) { - SegmentLength = SelectedProfileElement.MainSegmentLength; - SegmentStart = SelectedProfileElement.StartSegmentLength; + SegmentStart = SelectedProfileElement.Timeline.MainSegmentStartPosition; + SegmentEnd = SelectedProfileElement.Timeline.MainSegmentEndPosition; + SegmentLength = SelectedProfileElement.Timeline.MainSegmentLength; } else if (Segment == SegmentViewModelType.End) { - SegmentLength = SelectedProfileElement.EndSegmentLength; - SegmentStart = SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength; + SegmentStart = SelectedProfileElement.Timeline.EndSegmentStartPosition; + SegmentEnd = SelectedProfileElement.Timeline.EndSegmentEndPosition; + SegmentLength = SelectedProfileElement.Timeline.EndSegmentLength; } - SegmentEnd = SegmentStart + SegmentLength; SegmentStartPosition = SegmentStart.TotalSeconds * ProfileEditorService.PixelsPerSecond; SegmentWidth = SegmentLength.TotalSeconds * ProfileEditorService.PixelsPerSecond; SegmentEnabled = SegmentLength != TimeSpan.Zero; @@ -197,28 +198,25 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline public void DisableSegment() { - TimeSpan startSegmentEnd = SelectedProfileElement.StartSegmentLength; - TimeSpan mainSegmentEnd = SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength; - TimeSpan oldSegmentLength = SegmentLength; if (Segment == SegmentViewModelType.Start) { // Remove keyframes that fall in this segment - WipeKeyframes(null, startSegmentEnd); - SelectedProfileElement.StartSegmentLength = TimeSpan.Zero; + WipeKeyframes(null, SelectedProfileElement.Timeline.StartSegmentEndPosition); + SelectedProfileElement.Timeline.StartSegmentLength = TimeSpan.Zero; } else if (Segment == SegmentViewModelType.Main) { // Remove keyframes that fall in this segment - WipeKeyframes(startSegmentEnd, startSegmentEnd); - SelectedProfileElement.MainSegmentLength = TimeSpan.Zero; + WipeKeyframes(SelectedProfileElement.Timeline.MainSegmentStartPosition, SelectedProfileElement.Timeline.MainSegmentEndPosition); + SelectedProfileElement.Timeline.MainSegmentLength = TimeSpan.Zero; } else if (Segment == SegmentViewModelType.End) { // Remove keyframes that fall in this segment - WipeKeyframes(mainSegmentEnd, null); - SelectedProfileElement.EndSegmentLength = TimeSpan.Zero; + WipeKeyframes(SelectedProfileElement.Timeline.EndSegmentStartPosition, SelectedProfileElement.Timeline.EndSegmentEndPosition); + SelectedProfileElement.Timeline.EndSegmentLength = TimeSpan.Zero; } ShiftNextSegment(SegmentLength - oldSegmentLength); @@ -230,11 +228,11 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline { ShiftNextSegment(TimeSpan.FromSeconds(1)); if (Segment == SegmentViewModelType.Start) - SelectedProfileElement.StartSegmentLength = TimeSpan.FromSeconds(1); + SelectedProfileElement.Timeline.StartSegmentLength = TimeSpan.FromSeconds(1); else if (Segment == SegmentViewModelType.Main) - SelectedProfileElement.MainSegmentLength = TimeSpan.FromSeconds(1); + SelectedProfileElement.Timeline.MainSegmentLength = TimeSpan.FromSeconds(1); else if (Segment == SegmentViewModelType.End) - SelectedProfileElement.EndSegmentLength = TimeSpan.FromSeconds(1); + SelectedProfileElement.Timeline.EndSegmentLength = TimeSpan.FromSeconds(1); ProfileEditorService.UpdateSelectedProfileElement(); Update(); @@ -292,27 +290,24 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline public void UpdateLength(TimeSpan newTime) { - TimeSpan oldSegmentLength = SegmentLength; - if (Segment == SegmentViewModelType.Start) - { - if (newTime < TimeSpan.FromMilliseconds(100)) - newTime = TimeSpan.FromMilliseconds(100); - SelectedProfileElement.StartSegmentLength = newTime; - } - else if (Segment == SegmentViewModelType.Main) - { - if (newTime < SelectedProfileElement.StartSegmentLength + TimeSpan.FromMilliseconds(100)) - newTime = SelectedProfileElement.StartSegmentLength + TimeSpan.FromMilliseconds(100); - SelectedProfileElement.MainSegmentLength = newTime - SelectedProfileElement.StartSegmentLength; - } - else if (Segment == SegmentViewModelType.End) - { - if (newTime < SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength + TimeSpan.FromMilliseconds(100)) - newTime = SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength + TimeSpan.FromMilliseconds(100); - SelectedProfileElement.EndSegmentLength = newTime - SelectedProfileElement.StartSegmentLength - SelectedProfileElement.MainSegmentLength; - } + if (newTime < TimeSpan.FromMilliseconds(100)) + newTime = TimeSpan.FromMilliseconds(100); + + TimeSpan difference = newTime - SegmentEnd; + + if (difference > TimeSpan.Zero) + ShiftNextSegment(difference); + + if (Segment == SegmentViewModelType.Start) + SelectedProfileElement.Timeline.StartSegmentEndPosition = newTime; + else if (Segment == SegmentViewModelType.Main) + SelectedProfileElement.Timeline.MainSegmentEndPosition = newTime; + else if (Segment == SegmentViewModelType.End) + SelectedProfileElement.Timeline.EndSegmentEndPosition = newTime; + + if (difference < TimeSpan.Zero) + ShiftNextSegment(difference); - ShiftNextSegment(SegmentLength - oldSegmentLength); Update(); } @@ -325,7 +320,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline ProfileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged; ProfileEditorService.ProfileElementSelected -= ProfileEditorServiceOnProfileElementSelected; if (SelectedProfileElement != null) - SelectedProfileElement.PropertyChanged -= SelectedProfileElementOnPropertyChanged; + SelectedProfileElement.Timeline.PropertyChanged -= TimelineOnPropertyChanged; } #endregion @@ -335,20 +330,20 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline private void ProfileEditorServiceOnProfileElementSelected(object? sender, RenderProfileElementEventArgs e) { if (e.PreviousRenderProfileElement != null) - e.PreviousRenderProfileElement.PropertyChanged -= SelectedProfileElementOnPropertyChanged; + e.PreviousRenderProfileElement.Timeline.PropertyChanged -= TimelineOnPropertyChanged; if (e.RenderProfileElement != null) - e.RenderProfileElement.PropertyChanged += SelectedProfileElementOnPropertyChanged; + e.RenderProfileElement.Timeline.PropertyChanged += TimelineOnPropertyChanged; Update(); } - private void SelectedProfileElementOnPropertyChanged(object sender, PropertyChangedEventArgs e) + private void TimelineOnPropertyChanged(object sender, PropertyChangedEventArgs e) { - if (e.PropertyName == nameof(RenderProfileElement.StartSegmentLength) || - e.PropertyName == nameof(RenderProfileElement.MainSegmentLength) || - e.PropertyName == nameof(RenderProfileElement.EndSegmentLength)) + if (e.PropertyName == nameof(Core.Timeline.StartSegmentLength) || + e.PropertyName == nameof(Core.Timeline.MainSegmentLength) || + e.PropertyName == nameof(Core.Timeline.EndSegmentLength)) Update(); - else if (e.PropertyName == nameof(RenderProfileElement.DisplayContinuously)) + else if (e.PropertyName == nameof(Core.Timeline.PlayMode)) NotifyOfPropertyChange(nameof(RepeatSegment)); } @@ -361,15 +356,12 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline public void ShiftNextSegment(TimeSpan amount) { - TimeSpan segmentEnd = TimeSpan.Zero; if (Segment == SegmentViewModelType.Start) - segmentEnd = SelectedProfileElement.StartSegmentLength; + ShiftKeyframes(SelectedProfileElement.Timeline.StartSegmentEndPosition, null, amount); else if (Segment == SegmentViewModelType.Main) - segmentEnd = SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength; + ShiftKeyframes(SelectedProfileElement.Timeline.MainSegmentEndPosition, null, amount); else if (Segment == SegmentViewModelType.End) - segmentEnd = SelectedProfileElement.TimelineLength; - - ShiftKeyframes(segmentEnd, null, amount); + ShiftKeyframes(SelectedProfileElement.Timeline.EndSegmentEndPosition, null, amount); } private void WipeKeyframes(TimeSpan? start, TimeSpan? end) diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineViewModel.cs index 83c271cb4..5dd9a62b9 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineViewModel.cs @@ -29,7 +29,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline _profileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged; _profileEditorService.ProfileElementSelected += ProfileEditorServiceOnProfileElementSelected; if (_profileEditorService.SelectedProfileElement != null) - _profileEditorService.SelectedProfileElement.PropertyChanged += SelectedProfileElementOnPropertyChanged; + _profileEditorService.SelectedProfileElement.Timeline.PropertyChanged += TimelineOnPropertyChanged; Update(); } @@ -75,9 +75,12 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline TotalTimelineWidth = EndSegmentEndPosition; } - private void SelectedProfileElementOnPropertyChanged(object sender, PropertyChangedEventArgs e) + private void TimelineOnPropertyChanged(object sender, PropertyChangedEventArgs e) { - Update(); + if (e.PropertyName == nameof(Core.Timeline.StartSegmentLength) || + e.PropertyName == nameof(Core.Timeline.MainSegmentLength) || + e.PropertyName == nameof(Core.Timeline.EndSegmentLength)) + Update(); } private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e) @@ -88,9 +91,9 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline private void ProfileEditorServiceOnProfileElementSelected(object? sender, RenderProfileElementEventArgs e) { if (e.PreviousRenderProfileElement != null) - e.PreviousRenderProfileElement.PropertyChanged -= SelectedProfileElementOnPropertyChanged; + e.PreviousRenderProfileElement.Timeline.PropertyChanged -= TimelineOnPropertyChanged; if (e.RenderProfileElement != null) - e.RenderProfileElement.PropertyChanged += SelectedProfileElementOnPropertyChanged; + e.RenderProfileElement.Timeline.PropertyChanged += TimelineOnPropertyChanged; Update(); } @@ -184,13 +187,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline // If shift is held, snap to the current time // Take a tolerance of 5 pixels (half a keyframe width) if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) - { - float tolerance = 1000f / _profileEditorService.PixelsPerSecond * 5; - if (Math.Abs(_profileEditorService.CurrentTime.TotalMilliseconds - time.TotalMilliseconds) < tolerance) - time = _profileEditorService.CurrentTime; - else if (Math.Abs(_profileEditorService.SelectedProfileElement.StartSegmentLength.TotalMilliseconds - time.TotalMilliseconds) < tolerance) - time = _profileEditorService.SelectedProfileElement.StartSegmentLength; - } + time = _profileEditorService.SnapToTimeline(time, TimeSpan.FromMilliseconds(1000f / _profileEditorService.PixelsPerSecond * 5), false, true); return time; } @@ -351,7 +348,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline _profileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged; _profileEditorService.ProfileElementSelected -= ProfileEditorServiceOnProfileElementSelected; if (_profileEditorService.SelectedProfileElement != null) - _profileEditorService.SelectedProfileElement.PropertyChanged -= SelectedProfileElementOnPropertyChanged; + _profileEditorService.SelectedProfileElement.Timeline.PropertyChanged -= TimelineOnPropertyChanged; } #endregion diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs index f90b48300..36fb2e3ad 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs @@ -246,6 +246,7 @@ namespace Artemis.UI.Screens.ProfileEditor { LoadWorkspaceSettings(); Module.IsProfileUpdatingDisabled = true; + Module.ActiveProfile?.Reset(); Module.ActiveProfileChanged += ModuleOnActiveProfileChanged; LoadProfiles(); @@ -261,6 +262,7 @@ namespace Artemis.UI.Screens.ProfileEditor { SaveWorkspaceSettings(); Module.IsProfileUpdatingDisabled = false; + Module.ActiveProfile?.Reset(); Module.ActiveProfileChanged -= ModuleOnActiveProfileChanged; _profileEditorService.ChangeSelectedProfile(null); diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs index ddebeb6b9..977051f10 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLayerViewModel.cs @@ -125,25 +125,22 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization return; } - Execute.PostToUIThread(() => + Rect bounds = _layerEditorService.GetLayerBounds(Layer); + Geometry shapeGeometry = Geometry.Empty; + switch (Layer.LayerShape) { - Rect bounds = _layerEditorService.GetLayerBounds(Layer); - Geometry shapeGeometry = Geometry.Empty; - switch (Layer.LayerShape) - { - case EllipseShape _: - shapeGeometry = new EllipseGeometry(bounds); - break; - case RectangleShape _: - shapeGeometry = new RectangleGeometry(bounds); - break; - } + case EllipseShape _: + shapeGeometry = new EllipseGeometry(bounds); + break; + case RectangleShape _: + shapeGeometry = new RectangleGeometry(bounds); + break; + } - if (Layer.LayerBrush == null || Layer.LayerBrush.SupportsTransformation) - shapeGeometry.Transform = _layerEditorService.GetLayerTransformGroup(Layer); + if (Layer.LayerBrush == null || Layer.LayerBrush.SupportsTransformation) + shapeGeometry.Transform = _layerEditorService.GetLayerTransformGroup(Layer); - ShapeGeometry = shapeGeometry; - }); + ShapeGeometry = shapeGeometry; } private void CreateViewportRectangle() diff --git a/src/Artemis.UI/Screens/RootView.xaml b/src/Artemis.UI/Screens/RootView.xaml index 7cd6ac604..c4d5e45bb 100644 --- a/src/Artemis.UI/Screens/RootView.xaml +++ b/src/Artemis.UI/Screens/RootView.xaml @@ -55,9 +55,9 @@ DialogTheme="Inherit" SnackbarMessageQueue="{Binding MainMessageQueue}"> - + - + + + + + + + + + + + [] + + + + + + List item [] diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/DataModelDebugViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/DataModelDebugViewModel.cs index 2446f2666..8d7e501ac 100644 --- a/src/Artemis.UI/Screens/Settings/Debug/Tabs/DataModelDebugViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Debug/Tabs/DataModelDebugViewModel.cs @@ -99,7 +99,7 @@ namespace Artemis.UI.Screens.Settings.Debug.Tabs { lock (MainDataModel) { - MainDataModel.Update(_dataModelUIService); + MainDataModel.Update(_dataModelUIService, null); } } diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarView.xaml b/src/Artemis.UI/Screens/Sidebar/SidebarView.xaml index a2d0b5182..9da80b24c 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarView.xaml +++ b/src/Artemis.UI/Screens/Sidebar/SidebarView.xaml @@ -21,7 +21,7 @@ True True True - True \ No newline at end of file + True + True \ No newline at end of file