diff --git a/pointers.json b/pointers.json deleted file mode 100644 index 5d2c2f369..000000000 --- a/pointers.json +++ /dev/null @@ -1,75 +0,0 @@ -// Pointers used to auto-update. -// NOTE: If you're going to copy paste these for your own application, include a link to https://github.com/SpoinkyNL/Artemis -[ - { - "Game":"RocketLeague", - "GameVersion":"1.30", - "GameAddresses":[ - { - "Description":"Boost", - "BasePointer":{ - "value":23986356 - }, - "Offsets":[ - 196, - 24, - 904, - 1852, - 548 - ] - } - ] - }, - { - "Game":"WorldOfWarcraft", - "GameVersion":"7.0.3.22810", - "GameAddresses":[ - { - "Description":"ObjectManager", - "BasePointer":{ - "value":22511728 - }, - "Offsets":null - }, - { - "Description":"LocalPlayer", - "BasePointer":{ - "value":23715600 - }, - "Offsets":null - }, - { - "Description":"NameCache", - "BasePointer":{ - "value":22142184 - }, - "Offsets":null - }, - { - "Description":"TargetGuid", - "BasePointer":{ - "value":24758592 - }, - "Offsets":null - } - ] - }, - { - "Game":"Terraria", - "GameVersion":"1.3.4.4", - "GameAddresses":[ - { - "Description":"PlayerBase", - "BasePointer":{ - "value":3784824 - }, - "Offsets":[ - 640, - 1728, - 1652, - 60 - ] - } - ] - } -] diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index baa5c2106..99bdd68c0 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -37,6 +37,7 @@ + diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings index 77021d436..abb086d59 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -1,5 +1,8 @@  True + True + True + True True True True @@ -20,6 +23,7 @@ True True True + True True True True @@ -29,6 +33,10 @@ True True True + True + True True True + True + True True \ No newline at end of file diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index 0fa81409d..267ec0b76 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -32,7 +32,13 @@ namespace Artemis.Core /// /// The plugin info used by core components of Artemis /// - public static readonly PluginInfo CorePluginInfo = new PluginInfo {Guid = Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff"), Name = "Artemis Core"}; + public static readonly PluginInfo CorePluginInfo = new PluginInfo + { + Guid = Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff"), Name = "Artemis Core", Enabled = true + }; + + internal static readonly CorePlugin CorePlugin = new CorePlugin {PluginInfo = CorePluginInfo}; + internal static readonly EffectPlaceholderPlugin EffectPlaceholderPlugin = new EffectPlaceholderPlugin {PluginInfo = CorePluginInfo}; /// /// A read-only collection containing all primitive numeric types diff --git a/src/Artemis.Core/Events/LayerPropertyEventArgs.cs b/src/Artemis.Core/Events/LayerPropertyEventArgs.cs deleted file mode 100644 index c469947d0..000000000 --- a/src/Artemis.Core/Events/LayerPropertyEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace Artemis.Core -{ - public class LayerPropertyEventArgs : EventArgs - { - public LayerPropertyEventArgs(BaseLayerProperty layerProperty) - { - LayerProperty = layerProperty; - } - - public BaseLayerProperty LayerProperty { get; } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Events/PluginEventArgs.cs b/src/Artemis.Core/Events/Plugins/PluginEventArgs.cs similarity index 100% rename from src/Artemis.Core/Events/PluginEventArgs.cs rename to src/Artemis.Core/Events/Plugins/PluginEventArgs.cs diff --git a/src/Artemis.Core/Events/DataBindingPropertyUpdatedEvent.cs b/src/Artemis.Core/Events/Profiles/DataBindingPropertyUpdatedEvent.cs similarity index 100% rename from src/Artemis.Core/Events/DataBindingPropertyUpdatedEvent.cs rename to src/Artemis.Core/Events/Profiles/DataBindingPropertyUpdatedEvent.cs diff --git a/src/Artemis.Core/Events/Profiles/LayerPropertyEventArgs.cs b/src/Artemis.Core/Events/Profiles/LayerPropertyEventArgs.cs new file mode 100644 index 000000000..296de8310 --- /dev/null +++ b/src/Artemis.Core/Events/Profiles/LayerPropertyEventArgs.cs @@ -0,0 +1,24 @@ +using System; + +namespace Artemis.Core +{ + public class LayerPropertyEventArgs : EventArgs + { + public LayerPropertyEventArgs(LayerProperty layerProperty) + { + LayerProperty = layerProperty; + } + + public LayerProperty LayerProperty { get; } + } + + public class LayerPropertyEventArgs : EventArgs + { + public LayerPropertyEventArgs(ILayerProperty layerProperty) + { + LayerProperty = layerProperty; + } + + public ILayerProperty LayerProperty { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Events/LayerPropertyGroupUpdatingEventArgs.cs b/src/Artemis.Core/Events/Profiles/LayerPropertyGroupUpdatingEventArgs.cs similarity index 100% rename from src/Artemis.Core/Events/LayerPropertyGroupUpdatingEventArgs.cs rename to src/Artemis.Core/Events/Profiles/LayerPropertyGroupUpdatingEventArgs.cs diff --git a/src/Artemis.Core/Events/Stores/ConditionOperatorStoreEvent.cs b/src/Artemis.Core/Events/Stores/ConditionOperatorStoreEvent.cs new file mode 100644 index 000000000..fb81fa23f --- /dev/null +++ b/src/Artemis.Core/Events/Stores/ConditionOperatorStoreEvent.cs @@ -0,0 +1,12 @@ +namespace Artemis.Core +{ + internal class ConditionOperatorStoreEvent + { + public ConditionOperatorStoreEvent(ConditionOperatorRegistration registration) + { + Registration = registration; + } + + public ConditionOperatorRegistration Registration { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Events/Stores/DataBindingModifierTypeStoreEvent.cs b/src/Artemis.Core/Events/Stores/DataBindingModifierTypeStoreEvent.cs new file mode 100644 index 000000000..c954a7f60 --- /dev/null +++ b/src/Artemis.Core/Events/Stores/DataBindingModifierTypeStoreEvent.cs @@ -0,0 +1,12 @@ +namespace Artemis.Core +{ + internal class DataBindingModifierTypeStoreEvent + { + public DataBindingModifierTypeStoreEvent(DataBindingModifierTypeRegistration typeRegistration) + { + TypeRegistration = typeRegistration; + } + + public DataBindingModifierTypeRegistration TypeRegistration { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Events/Stores/DataModelStoreEvent.cs b/src/Artemis.Core/Events/Stores/DataModelStoreEvent.cs new file mode 100644 index 000000000..eb1132422 --- /dev/null +++ b/src/Artemis.Core/Events/Stores/DataModelStoreEvent.cs @@ -0,0 +1,12 @@ +namespace Artemis.Core +{ + internal class DataModelStoreEvent + { + public DataModelStoreEvent(DataModelRegistration registration) + { + Registration = registration; + } + + public DataModelRegistration Registration { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Events/Stores/LayerBrushStoreEvent.cs b/src/Artemis.Core/Events/Stores/LayerBrushStoreEvent.cs new file mode 100644 index 000000000..6d80a46fb --- /dev/null +++ b/src/Artemis.Core/Events/Stores/LayerBrushStoreEvent.cs @@ -0,0 +1,12 @@ +namespace Artemis.Core +{ + internal class LayerBrushStoreEvent + { + public LayerBrushStoreEvent(LayerBrushRegistration registration) + { + Registration = registration; + } + + public LayerBrushRegistration Registration { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Events/Stores/LayerEffectStoreEvent.cs b/src/Artemis.Core/Events/Stores/LayerEffectStoreEvent.cs new file mode 100644 index 000000000..8abcecb7f --- /dev/null +++ b/src/Artemis.Core/Events/Stores/LayerEffectStoreEvent.cs @@ -0,0 +1,12 @@ +namespace Artemis.Core +{ + internal class LayerEffectStoreEvent + { + public LayerEffectStoreEvent(LayerEffectRegistration registration) + { + Registration = registration; + } + + public LayerEffectRegistration Registration { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/IStorageModel.cs b/src/Artemis.Core/Models/IStorageModel.cs new file mode 100644 index 000000000..ce76eb0cd --- /dev/null +++ b/src/Artemis.Core/Models/IStorageModel.cs @@ -0,0 +1,18 @@ +namespace Artemis.Core +{ + /// + /// Represents a model that can be loaded and saved to persistent storage + /// + public interface IStorageModel + { + /// + /// Loads the model from its associated entity + /// + void Load(); + + /// + /// Saves the model to its associated entity + /// + void Save(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/IUpdateModel.cs b/src/Artemis.Core/Models/IUpdateModel.cs new file mode 100644 index 000000000..870079a7f --- /dev/null +++ b/src/Artemis.Core/Models/IUpdateModel.cs @@ -0,0 +1,14 @@ +namespace Artemis.Core +{ + /// + /// Represents a model that updates using a delta time + /// + public interface IUpdateModel + { + /// + /// Performs an update on the model + /// + /// The delta time in seconds + void Update(double deltaTime); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/Abstract/DisplayConditionPart.cs b/src/Artemis.Core/Models/Profile/Conditions/Abstract/DisplayConditionPart.cs index 2ae490f56..f751046d0 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Abstract/DisplayConditionPart.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Abstract/DisplayConditionPart.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Artemis.Core.Services; using Artemis.Storage.Entities.Profile.Abstract; @@ -7,7 +8,7 @@ namespace Artemis.Core /// /// An abstract class for display condition parts /// - public abstract class DisplayConditionPart + public abstract class DisplayConditionPart : IDisposable { private readonly List _children = new List(); @@ -60,8 +61,25 @@ namespace Artemis.Core /// public abstract bool EvaluateObject(object target); - internal abstract void Initialize(IDataModelService dataModelService); - internal abstract void ApplyToEntity(); + internal abstract void Save(); internal abstract DisplayConditionPartEntity GetEntity(); + + #region IDisposable + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/ConditionOperator.cs similarity index 61% rename from src/Artemis.Core/Models/Profile/Conditions/DisplayConditionOperator.cs rename to src/Artemis.Core/Models/Profile/Conditions/ConditionOperator.cs index cb592b5e5..53ca9bd79 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/ConditionOperator.cs @@ -2,18 +2,14 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using Artemis.Core.Services; namespace Artemis.Core { /// - /// A display condition operator is used by the conditions system to perform a specific boolean check + /// A condition operator is used by the conditions system to perform a specific boolean check /// - public abstract class DisplayConditionOperator + public abstract class ConditionOperator { - private IDataModelService _dataModelService; - private bool _registered; - /// /// Gets the plugin info this condition operator belongs to /// Note: Not set until after registering @@ -57,35 +53,5 @@ namespace Artemis.Core /// The parameter on the right side of the expression /// public abstract BinaryExpression CreateExpression(Expression leftSide, Expression rightSide); - - internal void Register(PluginInfo pluginInfo, IDataModelService dataModelService) - { - if (_registered) - return; - - PluginInfo = pluginInfo; - _dataModelService = dataModelService; - - if (PluginInfo != Constants.CorePluginInfo) - PluginInfo.Instance.PluginDisabled += InstanceOnPluginDisabled; - - _registered = true; - } - - internal void Unsubscribe() - { - if (!_registered) - return; - - if (PluginInfo != Constants.CorePluginInfo) - PluginInfo.Instance.PluginDisabled -= InstanceOnPluginDisabled; - _registered = false; - } - - private void InstanceOnPluginDisabled(object sender, EventArgs e) - { - // The service will call Unsubscribe - _dataModelService.RemoveConditionOperator(this); - } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionGroup.cs b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionGroup.cs index c93bb0612..055a40b0b 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionGroup.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionGroup.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using Artemis.Core.Services; using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.Conditions; @@ -12,6 +11,8 @@ namespace Artemis.Core /// public class DisplayConditionGroup : DisplayConditionPart { + private bool _disposed; + /// /// Creates a new instance of the class /// @@ -56,6 +57,9 @@ namespace Artemis.Core /// public override bool Evaluate() { + if (_disposed) + throw new ObjectDisposedException("DisplayConditionGroup"); + // Empty groups are always true if (Children.Count == 0) return true; @@ -81,6 +85,9 @@ namespace Artemis.Core /// public override bool EvaluateObject(object target) { + if (_disposed) + throw new ObjectDisposedException("DisplayConditionGroup"); + // Empty groups are always true if (Children.Count == 0) return true; @@ -98,26 +105,33 @@ namespace Artemis.Core }; } - internal override void ApplyToEntity() + internal override void Save() { Entity.BooleanOperator = (int) BooleanOperator; Entity.Children.Clear(); Entity.Children.AddRange(Children.Select(c => c.GetEntity())); foreach (var child in Children) - child.ApplyToEntity(); - } - - internal override void Initialize(IDataModelService dataModelService) - { - foreach (var child in Children) - child.Initialize(dataModelService); + child.Save(); } internal override DisplayConditionPartEntity GetEntity() { return Entity; } + + #region IDisposable + + protected override void Dispose(bool disposing) + { + _disposed = true; + foreach (var child in Children) + child.Dispose(); + + base.Dispose(disposing); + } + + #endregion } public enum BooleanOperator diff --git a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionList.cs b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionList.cs index 4bb1a587d..5ecbc607c 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionList.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionList.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Linq; using System.Linq.Expressions; using Artemis.Core.DataModelExpansions; -using Artemis.Core.Services; using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.Conditions; @@ -11,10 +10,14 @@ namespace Artemis.Core { public class DisplayConditionList : DisplayConditionPart { + private bool _disposed; + public DisplayConditionList(DisplayConditionPart parent) { Parent = parent; Entity = new DisplayConditionListEntity(); + + Initialize(); } public DisplayConditionList(DisplayConditionPart parent, DisplayConditionListEntity entity) @@ -22,6 +25,8 @@ namespace Artemis.Core Parent = parent; Entity = entity; ListOperator = (ListOperator) entity.ListOperator; + + Initialize(); } public DisplayConditionListEntity Entity { get; set; } @@ -34,6 +39,9 @@ namespace Artemis.Core public override bool Evaluate() { + if (_disposed) + throw new ObjectDisposedException("DisplayConditionList"); + if (CompiledListAccessor == null) return false; @@ -42,6 +50,9 @@ namespace Artemis.Core public override bool EvaluateObject(object target) { + if (_disposed) + throw new ObjectDisposedException("DisplayConditionList"); + if (!Children.Any()) return false; if (!(target is IList list)) @@ -60,6 +71,9 @@ namespace Artemis.Core public void UpdateList(DataModel dataModel, string path) { + if (_disposed) + throw new ObjectDisposedException("DisplayConditionList"); + if (dataModel != null && path == null) throw new ArtemisCoreException("If a data model is provided, a path is also required"); if (dataModel == null && path != null) @@ -90,6 +104,9 @@ namespace Artemis.Core public void CreateExpression() { + if (_disposed) + throw new ObjectDisposedException("DisplayConditionList"); + var parameter = Expression.Parameter(typeof(object), "listDataModel"); var accessor = ListPropertyPath.Split('.').Aggregate( Expression.Convert(parameter, ListDataModel.GetType()), @@ -98,12 +115,15 @@ namespace Artemis.Core var lambda = Expression.Lambda>(accessor, parameter); CompiledListAccessor = lambda.Compile(); } - - internal override void ApplyToEntity() + + internal override void Save() { // Target list - Entity.ListDataModelGuid = ListDataModel?.PluginInfo?.Guid; - Entity.ListPropertyPath = ListPropertyPath; + if (ListDataModel != null) + { + Entity.ListDataModelGuid = ListDataModel.PluginInfo.Guid; + Entity.ListPropertyPath = ListPropertyPath; + } // Operator Entity.ListOperator = (int) ListOperator; @@ -112,7 +132,7 @@ namespace Artemis.Core Entity.Children.Clear(); Entity.Children.AddRange(Children.Select(c => c.GetEntity())); foreach (var child in Children) - child.ApplyToEntity(); + child.Save(); } internal override DisplayConditionPartEntity GetEntity() @@ -120,13 +140,15 @@ namespace Artemis.Core return Entity; } - internal override void Initialize(IDataModelService dataModelService) + internal void Initialize() { + DataModelStore.DataModelAdded += DataModelStoreOnDataModelAdded; + DataModelStore.DataModelRemoved += DataModelStoreOnDataModelRemoved; if (Entity.ListDataModelGuid == null) return; // Get the data model and ensure the path is valid - var dataModel = dataModelService.GetPluginDataModelByGuid(Entity.ListDataModelGuid.Value); + var dataModel = DataModelStore.Get(Entity.ListDataModelGuid.Value)?.DataModel; if (dataModel == null || !dataModel.ContainsPath(Entity.ListPropertyPath)) return; @@ -143,9 +165,50 @@ namespace Artemis.Core Entity.Children.Clear(); AddChild(new DisplayConditionGroup(this)); } - - Children[0].Initialize(dataModelService); } + + + #region Event handlers + + private void DataModelStoreOnDataModelAdded(object sender, DataModelStoreEvent e) + { + var dataModel = e.Registration.DataModel; + if (dataModel.PluginInfo.Guid == Entity.ListDataModelGuid && dataModel.ContainsPath(Entity.ListPropertyPath)) + { + ListDataModel = dataModel; + ListPropertyPath = Entity.ListPropertyPath; + CreateExpression(); + } + } + + private void DataModelStoreOnDataModelRemoved(object sender, DataModelStoreEvent e) + { + if (ListDataModel != e.Registration.DataModel) + return; + + ListDataModel = null; + CompiledListAccessor = null; + } + + #endregion + + #region IDisposable + + /// + protected override void Dispose(bool disposing) + { + _disposed = true; + + DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded; + DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved; + + foreach (var child in Children) + child.Dispose(); + + base.Dispose(disposing); + } + + #endregion } public enum ListOperator diff --git a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionListPredicate.cs b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionListPredicate.cs index 6afee4a2f..ef93a992b 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionListPredicate.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionListPredicate.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Linq.Expressions; using Artemis.Core.DataModelExpansions; -using Artemis.Core.Services; using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.Conditions; using Newtonsoft.Json; @@ -18,6 +17,7 @@ namespace Artemis.Core Entity = new DisplayConditionListPredicateEntity(); ApplyParentList(); + Initialize(); } public DisplayConditionListPredicate(DisplayConditionPart parent, DisplayConditionListPredicateEntity entity) @@ -27,12 +27,13 @@ namespace Artemis.Core PredicateType = (ProfileRightSideType) entity.PredicateType; ApplyParentList(); + Initialize(); } public DisplayConditionListPredicateEntity Entity { get; set; } public ProfileRightSideType PredicateType { get; set; } - public DisplayConditionOperator Operator { get; private set; } + public ConditionOperator Operator { get; private set; } public Type ListType { get; private set; } public DataModel ListDataModel { get; private set; } @@ -120,9 +121,9 @@ namespace Artemis.Core CreateExpression(); } - public void UpdateOperator(DisplayConditionOperator displayConditionOperator) + public void UpdateOperator(ConditionOperator conditionOperator) { - if (displayConditionOperator == null) + if (conditionOperator == null) { Operator = null; return; @@ -130,13 +131,13 @@ namespace Artemis.Core if (LeftPropertyPath == null) { - Operator = displayConditionOperator; + Operator = conditionOperator; return; } var leftType = GetTypeAtInnerPath(LeftPropertyPath); - if (displayConditionOperator.SupportsType(leftType)) - Operator = displayConditionOperator; + if (conditionOperator.SupportsType(leftType)) + Operator = conditionOperator; CreateExpression(); } @@ -189,22 +190,31 @@ namespace Artemis.Core return result; } - internal override void ApplyToEntity() + internal override void Save() { Entity.PredicateType = (int) PredicateType; - Entity.ListDataModelGuid = ListDataModel?.PluginInfo?.Guid; - Entity.ListPropertyPath = ListPropertyPath; + if (ListDataModel != null) + { + Entity.ListDataModelGuid = ListDataModel.PluginInfo.Guid; + Entity.ListPropertyPath = ListPropertyPath; + } Entity.LeftPropertyPath = LeftPropertyPath; Entity.RightPropertyPath = RightPropertyPath; Entity.RightStaticValue = JsonConvert.SerializeObject(RightStaticValue); - Entity.OperatorPluginGuid = Operator?.PluginInfo?.Guid; - Entity.OperatorType = Operator?.GetType().Name; + if (Operator != null) + { + Entity.OperatorPluginGuid = Operator.PluginInfo.Guid; + Entity.OperatorType = Operator.GetType().Name; + } } - internal override void Initialize(IDataModelService dataModelService) + private void Initialize() { + ConditionOperatorStore.ConditionOperatorAdded += ConditionOperatorStoreOnConditionOperatorAdded; + ConditionOperatorStore.ConditionOperatorRemoved += ConditionOperatorStoreOnConditionOperatorRemoved; + // Left side if (Entity.LeftPropertyPath != null && ListContainsInnerPath(Entity.LeftPropertyPath)) UpdateLeftSide(Entity.LeftPropertyPath); @@ -212,7 +222,7 @@ namespace Artemis.Core // Operator if (Entity.OperatorPluginGuid != null) { - var conditionOperator = dataModelService.GetConditionOperator(Entity.OperatorPluginGuid.Value, Entity.OperatorType); + var conditionOperator = ConditionOperatorStore.Get(Entity.OperatorPluginGuid.Value, Entity.OperatorType)?.ConditionOperator; if (conditionOperator != null) UpdateOperator(conditionOperator); } @@ -221,7 +231,7 @@ namespace Artemis.Core if (PredicateType == ProfileRightSideType.Dynamic && Entity.RightPropertyPath != null) { if (ListContainsInnerPath(Entity.RightPropertyPath)) - UpdateLeftSide(Entity.LeftPropertyPath); + UpdateRightSideDynamic(Entity.RightPropertyPath); } // Right side static else if (PredicateType == ProfileRightSideType.Static && Entity.RightStaticValue != null) @@ -241,7 +251,7 @@ namespace Artemis.Core // If deserialization fails, use the type's default catch (JsonSerializationException e) { - dataModelService.LogListPredicateDeserializationFailure(this, e); + DeserializationLogger.LogListPredicateDeserializationFailure(this, e); rightSideValue = Activator.CreateInstance(leftSideType); } @@ -255,7 +265,7 @@ namespace Artemis.Core } catch (JsonException e) { - dataModelService.LogListPredicateDeserializationFailure(this, e); + DeserializationLogger.LogListPredicateDeserializationFailure(this, e); } } } @@ -383,5 +393,37 @@ namespace Artemis.Core Expression.Property ); } + + #region Event handlers + + private void ConditionOperatorStoreOnConditionOperatorAdded(object sender, ConditionOperatorStoreEvent e) + { + var 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; + CompiledListPredicate = null; + } + + #endregion + + #region IDisposable + + /// + protected override void Dispose(bool disposing) + { + ConditionOperatorStore.ConditionOperatorAdded -= ConditionOperatorStoreOnConditionOperatorAdded; + ConditionOperatorStore.ConditionOperatorRemoved -= ConditionOperatorStoreOnConditionOperatorRemoved; + + base.Dispose(disposing); + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs index 617de25f1..a86f57dc1 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/DisplayConditionPredicate.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Linq.Expressions; using Artemis.Core.DataModelExpansions; -using Artemis.Core.Services; using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.Conditions; using Newtonsoft.Json; @@ -25,6 +24,8 @@ namespace Artemis.Core Parent = parent; PredicateType = predicateType; Entity = new DisplayConditionPredicateEntity(); + + Initialize(); } /// @@ -37,6 +38,8 @@ namespace Artemis.Core Parent = parent; Entity = entity; PredicateType = (ProfileRightSideType) entity.PredicateType; + + Initialize(); } /// @@ -47,7 +50,7 @@ namespace Artemis.Core /// /// Gets the operator /// - public DisplayConditionOperator Operator { get; private set; } + public ConditionOperator Operator { get; private set; } /// /// Gets the currently used instance of the left data model @@ -175,11 +178,11 @@ namespace Artemis.Core /// /// Updates the operator of the predicate and re-compiles the expression /// - /// - public void UpdateOperator(DisplayConditionOperator displayConditionOperator) + /// + public void UpdateOperator(ConditionOperator conditionOperator) { // Calling CreateExpression will clear compiled expressions - if (displayConditionOperator == null) + if (conditionOperator == null) { Operator = null; CreateExpression(); @@ -189,18 +192,18 @@ namespace Artemis.Core // No need to clear compiled expressions, without a left data model they are already null if (LeftDataModel == null) { - Operator = displayConditionOperator; + Operator = conditionOperator; return; } var leftType = LeftDataModel.GetTypeAtPath(LeftPropertyPath); - if (!displayConditionOperator.SupportsType(leftType)) + if (!conditionOperator.SupportsType(leftType)) { - throw new ArtemisCoreException($"Cannot apply operator {displayConditionOperator.GetType().Name} to this predicate because " + + throw new ArtemisCoreException($"Cannot apply operator {conditionOperator.GetType().Name} to this predicate because " + $"it does not support left side type {leftType.Name}"); } - Operator = displayConditionOperator; + Operator = conditionOperator; CreateExpression(); } @@ -229,7 +232,7 @@ namespace Artemis.Core return $"[Static] {LeftPropertyPath} {Operator.Description} {RightStaticValue}"; } - internal override void ApplyToEntity() + internal override void Save() { Entity.PredicateType = (int) PredicateType; Entity.LeftDataModelGuid = LeftDataModel?.PluginInfo?.Guid; @@ -243,12 +246,17 @@ namespace Artemis.Core Entity.OperatorType = Operator?.GetType().Name; } - internal override void Initialize(IDataModelService dataModelService) + internal void Initialize() { + DataModelStore.DataModelAdded += DataModelStoreOnDataModelAdded; + DataModelStore.DataModelRemoved += DataModelStoreOnDataModelRemoved; + ConditionOperatorStore.ConditionOperatorAdded += ConditionOperatorStoreOnConditionOperatorAdded; + ConditionOperatorStore.ConditionOperatorRemoved += ConditionOperatorStoreOnConditionOperatorRemoved; + // Left side if (Entity.LeftDataModelGuid != null) { - var dataModel = dataModelService.GetPluginDataModelByGuid(Entity.LeftDataModelGuid.Value); + var dataModel = DataModelStore.Get(Entity.LeftDataModelGuid.Value)?.DataModel; if (dataModel != null && dataModel.ContainsPath(Entity.LeftPropertyPath)) UpdateLeftSide(dataModel, Entity.LeftPropertyPath); } @@ -256,7 +264,7 @@ namespace Artemis.Core // Operator if (Entity.OperatorPluginGuid != null) { - var conditionOperator = dataModelService.GetConditionOperator(Entity.OperatorPluginGuid.Value, Entity.OperatorType); + var conditionOperator = ConditionOperatorStore.Get(Entity.OperatorPluginGuid.Value, Entity.OperatorType)?.ConditionOperator; if (conditionOperator != null) UpdateOperator(conditionOperator); } @@ -264,7 +272,7 @@ namespace Artemis.Core // Right side dynamic if (PredicateType == ProfileRightSideType.Dynamic && Entity.RightDataModelGuid != null) { - var dataModel = dataModelService.GetPluginDataModelByGuid(Entity.RightDataModelGuid.Value); + var dataModel = DataModelStore.Get(Entity.RightDataModelGuid.Value)?.DataModel; if (dataModel != null && dataModel.ContainsPath(Entity.RightPropertyPath)) UpdateRightSide(dataModel, Entity.RightPropertyPath); } @@ -286,7 +294,7 @@ namespace Artemis.Core // If deserialization fails, use the type's default catch (JsonSerializationException e) { - dataModelService.LogPredicateDeserializationFailure(this, e); + DeserializationLogger.LogPredicateDeserializationFailure(this, e); rightSideValue = Activator.CreateInstance(leftSideType); } @@ -409,17 +417,60 @@ namespace Artemis.Core ); } - private Expression CreateListAccessor(DataModel dataModel, string path, ParameterExpression listParameter) - { - var listType = dataModel.GetListTypeInPath(path); - if (listType == null) - throw new ArtemisCoreException($"Cannot create a list accessor at path {path} because the path does not contain a list"); + #region Event handlers - path = dataModel.GetListInnerPath(path); - return path.Split('.').Aggregate( - Expression.Convert(listParameter, listType), // Cast to the appropriate type - Expression.Property - ); + private void DataModelStoreOnDataModelAdded(object sender, DataModelStoreEvent e) + { + var dataModel = e.Registration.DataModel; + if (dataModel.PluginInfo.Guid == Entity.LeftDataModelGuid && dataModel.ContainsPath(Entity.LeftPropertyPath)) + UpdateLeftSide(dataModel, Entity.LeftPropertyPath); + if (dataModel.PluginInfo.Guid == Entity.RightDataModelGuid && dataModel.ContainsPath(Entity.RightPropertyPath)) + UpdateRightSide(dataModel, Entity.RightPropertyPath); + } + + private void DataModelStoreOnDataModelRemoved(object sender, DataModelStoreEvent e) + { + if (LeftDataModel == e.Registration.DataModel) + { + CompiledDynamicPredicate = null; + LeftDataModel = null; + } + + if (RightDataModel == e.Registration.DataModel) + { + CompiledDynamicPredicate = null; + RightDataModel = null; + } + } + + private void ConditionOperatorStoreOnConditionOperatorAdded(object sender, ConditionOperatorStoreEvent e) + { + var 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; + CompiledStaticPredicate = null; + CompiledDynamicPredicate = null; + } + + #endregion + + /// + protected override void Dispose(bool disposing) + { + DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded; + DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved; + ConditionOperatorStore.ConditionOperatorAdded -= ConditionOperatorStoreOnConditionOperatorAdded; + ConditionOperatorStore.ConditionOperatorRemoved -= ConditionOperatorStoreOnConditionOperatorRemoved; + + base.Dispose(disposing); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/EqualsConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/EqualsConditionOperator.cs index 44388b7c1..87b60d484 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/EqualsConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/EqualsConditionOperator.cs @@ -4,7 +4,7 @@ using System.Linq.Expressions; namespace Artemis.Core { - internal class EqualsConditionOperator : DisplayConditionOperator + internal class EqualsConditionOperator : ConditionOperator { public override IReadOnlyCollection CompatibleTypes => new List {typeof(object)}; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/GreaterThanConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/GreaterThanConditionOperator.cs index 37dbccadb..6fa3287a5 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/GreaterThanConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/GreaterThanConditionOperator.cs @@ -4,7 +4,7 @@ using System.Linq.Expressions; namespace Artemis.Core { - internal class GreaterThanConditionOperator : DisplayConditionOperator + internal class GreaterThanConditionOperator : ConditionOperator { public override IReadOnlyCollection CompatibleTypes => Constants.NumberTypes; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/GreaterThanOrEqualConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/GreaterThanOrEqualConditionOperator.cs index 578c20aab..5bd914abb 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/GreaterThanOrEqualConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/GreaterThanOrEqualConditionOperator.cs @@ -4,7 +4,7 @@ using System.Linq.Expressions; namespace Artemis.Core { - internal class GreaterThanOrEqualConditionOperator : DisplayConditionOperator + internal class GreaterThanOrEqualConditionOperator : ConditionOperator { public override IReadOnlyCollection CompatibleTypes => Constants.NumberTypes; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/LessThanConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/LessThanConditionOperator.cs index 244b15c08..72567b9a9 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/LessThanConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/LessThanConditionOperator.cs @@ -4,7 +4,7 @@ using System.Linq.Expressions; namespace Artemis.Core { - internal class LessThanConditionOperator : DisplayConditionOperator + internal class LessThanConditionOperator : ConditionOperator { public override IReadOnlyCollection CompatibleTypes => Constants.NumberTypes; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/LessThanOrEqualConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/LessThanOrEqualConditionOperator.cs index e47b3f018..fd344dbf3 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/LessThanOrEqualConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/LessThanOrEqualConditionOperator.cs @@ -4,7 +4,7 @@ using System.Linq.Expressions; namespace Artemis.Core { - internal class LessThanOrEqualConditionOperator : DisplayConditionOperator + internal class LessThanOrEqualConditionOperator : ConditionOperator { public override IReadOnlyCollection CompatibleTypes => Constants.NumberTypes; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/NotEqualConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/NotEqualConditionOperator.cs index a1a9934be..4af94147c 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/NotEqualConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/NotEqualConditionOperator.cs @@ -4,7 +4,7 @@ using System.Linq.Expressions; namespace Artemis.Core { - internal class NotEqualConditionOperator : DisplayConditionOperator + internal class NotEqualConditionOperator : ConditionOperator { public override IReadOnlyCollection CompatibleTypes => new List {typeof(object)}; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringContainsConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringContainsConditionOperator.cs index 758e0400e..3ef231cfd 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringContainsConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringContainsConditionOperator.cs @@ -5,7 +5,7 @@ using System.Reflection; namespace Artemis.Core { - internal class StringContainsConditionOperator : DisplayConditionOperator + internal class StringContainsConditionOperator : ConditionOperator { private readonly MethodInfo _contains; private readonly MethodInfo _toLower; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEndsWithConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEndsWithConditionOperator.cs index 38ed72633..1903984e9 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEndsWithConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEndsWithConditionOperator.cs @@ -5,7 +5,7 @@ using System.Reflection; namespace Artemis.Core { - internal class StringEndsWithConditionOperator : DisplayConditionOperator + internal class StringEndsWithConditionOperator : ConditionOperator { private readonly MethodInfo _endsWith; private readonly MethodInfo _toLower; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEqualsConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEqualsConditionOperator.cs index 91a58ec91..cc0cf432c 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEqualsConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringEqualsConditionOperator.cs @@ -5,7 +5,7 @@ using System.Reflection; namespace Artemis.Core { - internal class StringEqualsConditionOperator : DisplayConditionOperator + internal class StringEqualsConditionOperator : ConditionOperator { private readonly MethodInfo _toLower; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotContainsConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotContainsConditionOperator.cs index 671555eb5..97fbac2eb 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotContainsConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotContainsConditionOperator.cs @@ -5,7 +5,7 @@ using System.Reflection; namespace Artemis.Core { - internal class StringNotContainsConditionOperator : DisplayConditionOperator + internal class StringNotContainsConditionOperator : ConditionOperator { private readonly MethodInfo _contains; private readonly MethodInfo _toLower; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotEqualConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotEqualConditionOperator.cs index c98d4bef3..33e14b4e1 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotEqualConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNotEqualConditionOperator.cs @@ -5,7 +5,7 @@ using System.Reflection; namespace Artemis.Core { - internal class StringNotEqualConditionOperator : DisplayConditionOperator + internal class StringNotEqualConditionOperator : ConditionOperator { private readonly MethodInfo _toLower; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNullConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNullConditionOperator.cs index bb0b99d42..6c51024ed 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNullConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringNullConditionOperator.cs @@ -4,7 +4,7 @@ using System.Linq.Expressions; namespace Artemis.Core { - internal class StringNullConditionOperator : DisplayConditionOperator + internal class StringNullConditionOperator : ConditionOperator { public StringNullConditionOperator() { diff --git a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringStartsWithConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringStartsWithConditionOperator.cs index f9f9fbf8e..4b3d9f39b 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/Operators/StringStartsWithConditionOperator.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/Operators/StringStartsWithConditionOperator.cs @@ -5,7 +5,7 @@ using System.Reflection; namespace Artemis.Core { - internal class StringStartsWithConditionOperator : DisplayConditionOperator + internal class StringStartsWithConditionOperator : ConditionOperator { private readonly MethodInfo _startsWith; private readonly MethodInfo _toLower; diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Converters/FloatDataBindingConverter.cs b/src/Artemis.Core/Models/Profile/DataBindings/Converters/FloatDataBindingConverter.cs index eb337ba54..ed77db48f 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/Converters/FloatDataBindingConverter.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/Converters/FloatDataBindingConverter.cs @@ -1,48 +1,51 @@ using System; +using SkiaSharp; namespace Artemis.Core { /// - public class FloatDataBindingConverter : DataBindingConverter + public class FloatDataBindingConverter : FloatDataBindingConverter { + } + + /// + /// The type of layer property this converter is applied to + public class FloatDataBindingConverter : DataBindingConverter + { + /// + /// Creates a new instance of the class + /// public FloatDataBindingConverter() { - SupportedType = typeof(float); SupportsSum = true; SupportsInterpolate = true; } /// - public override object Sum(object a, object b) + public override float Sum(float a, float b) { - return Convert.ToSingle(a) + Convert.ToSingle(b); + return a + b; } /// - public override object Interpolate(object a, object b, double progress) + public override float Interpolate(float a, float b, double progress) { - var floatA = Convert.ToSingle(a); - var floatB = Convert.ToSingle(b); - var diff = floatB - floatA; - return floatA + diff * progress; + var diff = b - a; + return (float) (a + diff * progress); } /// - public override void ApplyValue(object value) + public override void ApplyValue(float value) { - var floatValue = Convert.ToSingle(value); + if (ValueTypeSetExpression == null) + return; + if (DataBinding.LayerProperty.PropertyDescription.MaxInputValue is float max) - floatValue = Math.Min(floatValue, max); + value = Math.Min(value, max); if (DataBinding.LayerProperty.PropertyDescription.MinInputValue is float min) - floatValue = Math.Max(floatValue, min); + value = Math.Max(value, min); - ValueSetter?.Invoke(floatValue); - } - - /// - public override object GetValue() - { - return ValueGetter?.Invoke(); + base.ApplyValue(value); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Converters/GeneralDataBindingConverter.cs b/src/Artemis.Core/Models/Profile/DataBindings/Converters/GeneralDataBindingConverter.cs index 561ef8784..478b17eea 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/Converters/GeneralDataBindingConverter.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/Converters/GeneralDataBindingConverter.cs @@ -3,11 +3,10 @@ namespace Artemis.Core { /// - public class GeneralDataBindingConverter : DataBindingConverter + public class GeneralDataBindingConverter : DataBindingConverter where T : ILayerProperty { public GeneralDataBindingConverter() { - SupportedType = typeof(object); SupportsSum = false; SupportsInterpolate = false; } @@ -23,17 +22,5 @@ namespace Artemis.Core { throw new NotSupportedException(); } - - /// - public override void ApplyValue(object value) - { - ValueSetter?.Invoke(value); - } - - /// - public override object GetValue() - { - return ValueGetter?.Invoke(); - } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Converters/IntDataBindingConverter.cs b/src/Artemis.Core/Models/Profile/DataBindings/Converters/IntDataBindingConverter.cs index c3466a43f..86265461f 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/Converters/IntDataBindingConverter.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/Converters/IntDataBindingConverter.cs @@ -3,11 +3,18 @@ namespace Artemis.Core { /// - public class IntDataBindingConverter : DataBindingConverter + public class IntDataBindingConverter : IntDataBindingConverter { + } + + /// + public class IntDataBindingConverter : DataBindingConverter + { + /// + /// Creates a new instance of the class + /// public IntDataBindingConverter() { - SupportedType = typeof(int); SupportsSum = true; SupportsInterpolate = true; } @@ -19,30 +26,16 @@ namespace Artemis.Core public MidpointRounding InterpolationRoundingMode { get; set; } = MidpointRounding.AwayFromZero; /// - public override object Sum(object a, object b) + public override int Sum(int a, int b) { - return (int) a + (int) b; + return a + b; } /// - public override object Interpolate(object a, object b, double progress) + public override int Interpolate(int a, int b, double progress) { - var intA = (int) a; - var intB = (int) b; - var diff = intB - intA; - return (int) Math.Round(intA + diff * progress, InterpolationRoundingMode); - } - - /// - public override void ApplyValue(object value) - { - ValueSetter?.Invoke(value); - } - - /// - public override object GetValue() - { - return ValueGetter?.Invoke(); + var diff = b - a; + return (int) Math.Round(a + diff * progress, InterpolationRoundingMode); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Converters/Internal/SKColorArgbDataBindingConverter.cs b/src/Artemis.Core/Models/Profile/DataBindings/Converters/Internal/SKColorArgbDataBindingConverter.cs new file mode 100644 index 000000000..e713bd23a --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataBindings/Converters/Internal/SKColorArgbDataBindingConverter.cs @@ -0,0 +1,70 @@ +using System; +using SkiaSharp; + +namespace Artemis.Core +{ + // This is internal because it's mainly a proof-of-concept + internal class SKColorArgbDataBindingConverter : DataBindingConverter + { + private readonly Channel _channel; + + public SKColorArgbDataBindingConverter(Channel channel) + { + _channel = channel; + + SupportsSum = true; + SupportsInterpolate = true; + } + + public override byte Sum(byte a, byte b) + { + return ClampToByte(a + b); + } + + public override byte Interpolate(byte a, byte b, double progress) + { + var diff = b - a; + return ClampToByte(a + diff * progress); + } + + public override void ApplyValue(byte value) + { + switch (_channel) + { + case Channel.Alpha: + DataBinding.LayerProperty.CurrentValue = DataBinding.LayerProperty.CurrentValue.WithAlpha(value); + break; + case Channel.Red: + DataBinding.LayerProperty.CurrentValue = DataBinding.LayerProperty.CurrentValue.WithRed(value); + break; + case Channel.Green: + DataBinding.LayerProperty.CurrentValue = DataBinding.LayerProperty.CurrentValue.WithGreen(value); + break; + case Channel.Blue: + DataBinding.LayerProperty.CurrentValue = DataBinding.LayerProperty.CurrentValue.WithBlue(value); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + public override byte ConvertFromObject(object source) + { + var saveValue = Convert.ToDouble(source); + return ClampToByte(saveValue); + } + + private static byte ClampToByte(double value) + { + return (byte) Math.Clamp(value, 0, 255); + } + + public enum Channel + { + Alpha, + Red, + Green, + Blue + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Converters/Internal/SKColorPartDataBindingConverter.cs b/src/Artemis.Core/Models/Profile/DataBindings/Converters/Internal/SKColorPartDataBindingConverter.cs deleted file mode 100644 index 3073a9721..000000000 --- a/src/Artemis.Core/Models/Profile/DataBindings/Converters/Internal/SKColorPartDataBindingConverter.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using SkiaSharp; - -namespace Artemis.Core -{ - // This is internal because it's mainly a proof-of-concept - internal class SKColorPartDataBindingConverter : DataBindingConverter - { - private readonly Channel _channel; - - public SKColorPartDataBindingConverter(Channel channel) - { - _channel = channel; - - SupportsSum = true; - SupportsInterpolate = true; - SupportedType = _channel switch - { - Channel.Alpha => typeof(byte), - Channel.Red => typeof(byte), - Channel.Green => typeof(byte), - Channel.Blue => typeof(byte), - Channel.Hue => typeof(float), - _ => throw new ArgumentOutOfRangeException() - }; - } - - public override object Sum(object a, object b) - { - return (float) a + (float) b; - } - - public override object Interpolate(object a, object b, double progress) - { - var diff = (float) b - (float) a; - return diff * progress; - } - - public override void ApplyValue(object value) - { - var property = (SKColorLayerProperty) DataBinding.LayerProperty; - switch (_channel) - { - case Channel.Alpha: - property.CurrentValue = property.CurrentValue.WithAlpha((byte) value); - break; - case Channel.Red: - property.CurrentValue = property.CurrentValue.WithRed((byte) value); - break; - case Channel.Green: - property.CurrentValue = property.CurrentValue.WithGreen((byte) value); - break; - case Channel.Blue: - property.CurrentValue = property.CurrentValue.WithBlue((byte) value); - break; - case Channel.Hue: - property.CurrentValue.ToHsv(out var h, out var s, out var v); - property.CurrentValue = SKColor.FromHsv((float) value, s, v); - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - - public override object GetValue() - { - var property = (SKColorLayerProperty) DataBinding.LayerProperty; - switch (_channel) - { - case Channel.Alpha: - return property.CurrentValue.Alpha; - case Channel.Red: - return property.CurrentValue.Red; - case Channel.Green: - return property.CurrentValue.Green; - case Channel.Blue: - return property.CurrentValue.Blue; - case Channel.Hue: - property.CurrentValue.ToHsv(out var h, out _, out _); - return h; - default: - throw new ArgumentOutOfRangeException(); - } - } - - public enum Channel - { - Alpha, - Red, - Green, - Blue, - Hue - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs index f28a911f5..f167832fe 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs @@ -2,60 +2,55 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Reflection; using Artemis.Core.DataModelExpansions; -using Artemis.Core.Services; using Artemis.Storage.Entities.Profile.DataBindings; namespace Artemis.Core { - /// - /// A data binding that binds a certain to a value inside a - /// - public class DataBinding + /// + public class DataBinding : IDataBinding { - private readonly List _modifiers = new List(); + private readonly List> _modifiers = new List>(); - private object _currentValue; - private object _previousValue; + private TProperty _currentValue; + private bool _disposed; private TimeSpan _easingProgress; + private TProperty _previousValue; - internal DataBinding(DataBindingRegistration dataBindingRegistration) + internal DataBinding(DataBindingRegistration dataBindingRegistration) { LayerProperty = dataBindingRegistration.LayerProperty; Entity = new DataBindingEntity(); ApplyRegistration(dataBindingRegistration); - ApplyToEntity(); + Save(); + Initialize(); } - internal DataBinding(BaseLayerProperty layerProperty, DataBindingEntity entity) + internal DataBinding(LayerProperty layerProperty, DataBindingEntity entity) { LayerProperty = layerProperty; Entity = entity; - ApplyToDataBinding(); + // Load will add children so be initialized before that + Initialize(); + Load(); } - /// - /// Gets the layer property this data binding targets - /// - public BaseLayerProperty LayerProperty { get; } - /// /// Gets the data binding registration this data binding is based upon /// - public DataBindingRegistration Registration { get; private set; } + public DataBindingRegistration Registration { get; private set; } + + /// + /// Gets the layer property this data binding targets + /// + public LayerProperty LayerProperty { get; } /// /// Gets the converter used to apply this data binding to the /// - public DataBindingConverter Converter { get; private set; } - - /// - /// Gets the property on the this data binding targets - /// - public PropertyInfo TargetProperty { get; private set; } + public DataBindingConverter Converter { get; private set; } /// /// Gets the currently used instance of the data model that contains the source of the data binding @@ -82,7 +77,7 @@ namespace Artemis.Core /// /// Gets a list of modifiers applied to this data binding /// - public IReadOnlyList Modifiers => _modifiers.AsReadOnly(); + public IReadOnlyList> Modifiers => _modifiers.AsReadOnly(); /// /// Gets the compiled function that gets the current value of the data binding target @@ -91,11 +86,57 @@ namespace Artemis.Core internal DataBindingEntity Entity { get; } + /// + /// Updates the smoothing progress of the data binding + /// + /// The time in seconds that passed since the last update + public void Update(double deltaTime) + { + if (_disposed) + throw new ObjectDisposedException("DataBinding"); + + // Data bindings cannot go back in time like brushes + deltaTime = Math.Max(0, deltaTime); + + _easingProgress = _easingProgress.Add(TimeSpan.FromSeconds(deltaTime)); + if (_easingProgress > EasingTime) + _easingProgress = EasingTime; + } + + /// + public void Apply() + { + if (_disposed) + throw new ObjectDisposedException("DataBinding"); + + if (Converter == null) + return; + + var converterValue = Converter.GetValue(); + var value = GetValue(converterValue); + Converter.ApplyValue(value); + } + + /// + public void Dispose() + { + _disposed = true; + + DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded; + DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved; + + foreach (var dataBindingModifier in Modifiers) + dataBindingModifier.Dispose(); + } + /// /// Adds a modifier to the data binding's collection /// - public void AddModifier(DataBindingModifier modifier) + public void AddModifier(DataBindingModifier modifier) { + if (_disposed) + throw new ObjectDisposedException("DataBinding"); + if (!_modifiers.Contains(modifier)) { modifier.DataBinding = this; @@ -108,8 +149,11 @@ namespace Artemis.Core /// /// Removes a modifier from the data binding's collection /// - public void RemoveModifier(DataBindingModifier modifier) + public void RemoveModifier(DataBindingModifier modifier) { + if (_disposed) + throw new ObjectDisposedException("DataBinding"); + if (_modifiers.Contains(modifier)) { modifier.DataBinding = null; @@ -126,6 +170,9 @@ namespace Artemis.Core /// The path pointing to the source inside the data model public void UpdateSource(DataModel dataModel, string path) { + if (_disposed) + throw new ObjectDisposedException("DataBinding"); + if (dataModel != null && path == null) throw new ArtemisCoreException("If a data model is provided, a path is also required"); if (dataModel == null && path != null) @@ -147,8 +194,11 @@ namespace Artemis.Core /// /// The base value of the property the data binding is applied to /// - public object GetValue(object baseValue) + public TProperty GetValue(TProperty baseValue) { + if (_disposed) + throw new ObjectDisposedException("DataBinding"); + if (CompiledTargetAccessor == null || Converter == null) return baseValue; @@ -156,8 +206,8 @@ namespace Artemis.Core foreach (var dataBindingModifier in Modifiers) dataBindingValue = dataBindingModifier.Apply(dataBindingValue); - var value = Convert.ChangeType(dataBindingValue, TargetProperty.PropertyType); - + TProperty value = Converter.ConvertFromObject(dataBindingValue); + // If no easing is to be applied simple return whatever the current value is if (EasingTime == TimeSpan.Zero || !Converter.SupportsInterpolate) return value; @@ -170,19 +220,12 @@ namespace Artemis.Core return GetInterpolatedValue(); } - private void ResetEasing(object value) - { - _previousValue = GetInterpolatedValue(); - _currentValue = value; - _easingProgress = TimeSpan.Zero; - } - /// /// Returns the type of the target property of this data binding /// public Type GetTargetType() { - return TargetProperty.PropertyType; + return Registration.PropertyExpression.ReturnType; } /// @@ -193,105 +236,47 @@ namespace Artemis.Core return SourceDataModel?.GetTypeAtPath(SourcePropertyPath); } - /// - /// Updates the smoothing progress of the data binding - /// - /// The time in seconds that passed since the last update - public void Update(double deltaTime) + private void Initialize() { - // Data bindings cannot go back in time like brushes - deltaTime = Math.Max(0, deltaTime); + DataModelStore.DataModelAdded += DataModelStoreOnDataModelAdded; + DataModelStore.DataModelRemoved += DataModelStoreOnDataModelRemoved; - _easingProgress = _easingProgress.Add(TimeSpan.FromSeconds(deltaTime)); - if (_easingProgress > EasingTime) - _easingProgress = EasingTime; - } - - /// - /// Updates the value on the according to the data binding - /// - public void ApplyToProperty() - { - if (Converter == null) - return; - - var value = GetValue(Converter.GetValue()); - Converter.ApplyValue(GetValue(value)); - } - - internal void ApplyToEntity() - { - // General - Entity.TargetProperty = TargetProperty?.Name; - Entity.DataBindingMode = (int) Mode; - Entity.EasingTime = EasingTime; - Entity.EasingFunction = (int) EasingFunction; - - // Data model - Entity.SourceDataModelGuid = SourceDataModel?.PluginInfo?.Guid; - Entity.SourcePropertyPath = SourcePropertyPath; - - // Modifiers - Entity.Modifiers.Clear(); - foreach (var dataBindingModifier in Modifiers) - { - dataBindingModifier.ApplyToEntity(); - Entity.Modifiers.Add(dataBindingModifier.Entity); - } - } - - internal void ApplyToDataBinding() - { - // General - ApplyRegistration(LayerProperty.DataBindingRegistrations.FirstOrDefault(p => p.Property.Name == Entity.TargetProperty)); - - Mode = (DataBindingMode) Entity.DataBindingMode; - EasingTime = Entity.EasingTime; - EasingFunction = (Easings.Functions) Entity.EasingFunction; - - // Data model is done during Initialize - - // Modifiers - foreach (var dataBindingModifierEntity in Entity.Modifiers) - _modifiers.Add(new DataBindingModifier(this, dataBindingModifierEntity)); - } - - internal void Initialize(IDataModelService dataModelService, IDataBindingService dataBindingService) - { // Source if (Entity.SourceDataModelGuid != null && SourceDataModel == null) { - var dataModel = dataModelService.GetPluginDataModelByGuid(Entity.SourceDataModelGuid.Value); + var dataModel = DataModelStore.Get(Entity.SourceDataModelGuid.Value)?.DataModel; if (dataModel != null && dataModel.ContainsPath(Entity.SourcePropertyPath)) UpdateSource(dataModel, Entity.SourcePropertyPath); } - - // Modifiers - foreach (var dataBindingModifier in Modifiers) - dataBindingModifier.Initialize(dataModelService, dataBindingService); } - private void ApplyRegistration(DataBindingRegistration dataBindingRegistration) + private void ResetEasing(TProperty value) + { + _previousValue = GetInterpolatedValue(); + _currentValue = value; + _easingProgress = TimeSpan.Zero; + } + + private void ApplyRegistration(DataBindingRegistration dataBindingRegistration) { if (dataBindingRegistration != null) dataBindingRegistration.DataBinding = this; Converter = dataBindingRegistration?.Converter; Registration = dataBindingRegistration; - TargetProperty = dataBindingRegistration?.Property; if (GetTargetType().IsValueType) { if (_currentValue == null) - _currentValue = GetTargetType().GetDefault(); + _currentValue = default; if (_previousValue == null) - _previousValue = _currentValue; + _previousValue = default; } Converter?.Initialize(this); } - private object GetInterpolatedValue() + private TProperty GetInterpolatedValue() { if (_easingProgress == EasingTime || !Converter.SupportsInterpolate) return _currentValue; @@ -318,6 +303,77 @@ namespace Artemis.Core CompiledTargetAccessor = lambda.Compile(); } + #region Storage + + /// + public void Load() + { + if (_disposed) + throw new ObjectDisposedException("DataBinding"); + // General + var registration = LayerProperty.GetDataBindingRegistration(Entity.TargetExpression); + ApplyRegistration(registration); + + Mode = (DataBindingMode) Entity.DataBindingMode; + EasingTime = Entity.EasingTime; + EasingFunction = (Easings.Functions) Entity.EasingFunction; + + // Data model is done during Initialize + + // Modifiers + foreach (var dataBindingModifierEntity in Entity.Modifiers) + _modifiers.Add(new DataBindingModifier(this, dataBindingModifierEntity)); + } + + /// + public void Save() + { + if (_disposed) + throw new ObjectDisposedException("DataBinding"); + + if (!LayerProperty.Entity.DataBindingEntities.Contains(Entity)) + LayerProperty.Entity.DataBindingEntities.Add(Entity); + + // General + Entity.TargetExpression = Registration.PropertyExpression.ToString(); + Entity.DataBindingMode = (int) Mode; + Entity.EasingTime = EasingTime; + Entity.EasingFunction = (int) EasingFunction; + + // Data model + if (SourceDataModel != null) + { + Entity.SourceDataModelGuid = SourceDataModel.PluginInfo.Guid; + Entity.SourcePropertyPath = SourcePropertyPath; + } + + // Modifiers + Entity.Modifiers.Clear(); + foreach (var dataBindingModifier in Modifiers) + dataBindingModifier.Save(); + } + + #endregion + + #region Event handlers + + private void DataModelStoreOnDataModelAdded(object sender, DataModelStoreEvent e) + { + var dataModel = e.Registration.DataModel; + if (dataModel.PluginInfo.Guid == Entity.SourceDataModelGuid && dataModel.ContainsPath(Entity.SourcePropertyPath)) + UpdateSource(dataModel, Entity.SourcePropertyPath); + } + + private void DataModelStoreOnDataModelRemoved(object sender, DataModelStoreEvent e) + { + if (SourceDataModel != e.Registration.DataModel) + return; + SourceDataModel = null; + CompiledTargetAccessor = null; + } + + #endregion + #region Events /// diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingConverter.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingConverter.cs index 3ee8756d2..380caba36 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingConverter.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingConverter.cs @@ -1,29 +1,31 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; namespace Artemis.Core { /// - /// A data binding converter that acts as the bridge between a and a - /// + /// Represents a data binding converter that acts as the bridge between a + /// and a /// - public abstract class DataBindingConverter + public abstract class DataBindingConverter : IDataBindingConverter { - internal Func ValueGetter { get; set; } - internal Action ValueSetter { get; set; } + /// + /// A dynamically compiled getter pointing to the data bound property + /// + public Func GetExpression { get; private set; } + + /// + /// A dynamically compiled setter pointing to the data bound property + /// + public Action ValueTypeSetExpression { get; private set; } + + public Action ReferenceTypeSetExpression { get; private set; } /// /// Gets the data binding this converter is applied to /// - public DataBinding DataBinding { get; private set; } - - /// - /// Gets the type this converter supports - /// - public Type SupportedType { get; protected set; } + public DataBinding DataBinding { get; private set; } /// /// Gets whether or not this data binding converter supports the method @@ -35,17 +37,13 @@ namespace Artemis.Core /// public bool SupportsInterpolate { get; protected set; } - /// - /// Called when the data binding converter has been initialized and the is available - /// - protected virtual void OnInitialized() - { - } + /// + public Type SupportedType => typeof(TProperty); /// /// Returns the sum of and /// - public abstract object Sum(object a, object b); + public abstract TProperty Sum(TProperty a, TProperty b); /// /// Returns the the interpolated value between and on a scale (generally) @@ -56,66 +54,117 @@ namespace Artemis.Core /// The value to interpolate towards /// The progress of the interpolation between 0.0 and 1.0 /// - public abstract object Interpolate(object a, object b, double progress); + public abstract TProperty Interpolate(TProperty a, TProperty b, double progress); /// /// Applies the to the layer property /// /// - public abstract void ApplyValue(object value); + public virtual void ApplyValue(TProperty value) + { + if (ReferenceTypeSetExpression != null) + ReferenceTypeSetExpression(DataBinding.LayerProperty.CurrentValue, value); + else if (ValueTypeSetExpression != null) + ValueTypeSetExpression(value); + } /// /// Returns the current base value of the data binding /// - public abstract object GetValue(); + public virtual TProperty GetValue() + { + return GetExpression(DataBinding.LayerProperty.CurrentValue); + } - internal void Initialize(DataBinding dataBinding) + /// + /// Converts the provided object to a type of + /// + public virtual TProperty ConvertFromObject(object source) + { + return (TProperty) Convert.ChangeType(source, typeof(TProperty)); + } + + /// + /// Called when the data binding converter has been initialized and the is available + /// + protected virtual void OnInitialized() + { + } + + internal void Initialize(DataBinding dataBinding) { DataBinding = dataBinding; - ValueGetter = CreateValueGetter(); - ValueSetter = CreateValueSetter(); + GetExpression = dataBinding.Registration.PropertyExpression.Compile(); + CreateSetExpression(); OnInitialized(); } - private Func CreateValueGetter() + private void CreateSetExpression() { - if (DataBinding.TargetProperty?.DeclaringType == null) - return null; + // If the registration does not point towards a member of LayerProperty.CurrentValue, assign directly to LayerProperty.CurrentValue + if (DataBinding.Registration.Member == null) + { + CreateSetCurrentValueExpression(); + return; + } - var getterMethod = DataBinding.TargetProperty.GetGetMethod(); - if (getterMethod == null) - return null; - - var constant = Expression.Constant(DataBinding.LayerProperty); - // The path is null if the registration is applied to the root (LayerProperty.CurrentValue) - var property = DataBinding.Registration.Path == null - ? Expression.Property(constant, DataBinding.TargetProperty) - : (MemberExpression) DataBinding.Registration.Path.Split('.').Aggregate(constant, Expression.Property); + // Ensure the member of LayerProperty.CurrentValue has a setter + MethodInfo setterMethod = null; + if (DataBinding.Registration.Member is PropertyInfo propertyInfo) + setterMethod = propertyInfo.GetSetMethod(); + // If there is no setter, the built-in data binding cannot do its job, stay null + if (setterMethod == null) + return; - // The get method should cast to the object since it receives whatever type the property is - var body = Expression.Convert(property, typeof(object)); - var lambda = Expression.Lambda>(body); - - return lambda.Compile(); + // If LayerProperty.CurrentValue is a value type, assign it directly to LayerProperty.CurrentValue after applying the changes + if (typeof(TLayerProperty).IsValueType) + CreateSetValueTypeExpression(); + // If it is a reference type it can safely be updated by its reference + else + CreateSetReferenceTypeExpression(); } - private Action CreateValueSetter() + private void CreateSetReferenceTypeExpression() { - if (DataBinding.TargetProperty?.DeclaringType == null) - return null; + var propertyValue = Expression.Parameter(typeof(TProperty), "propertyValue"); + var parameter = Expression.Parameter(typeof(TLayerProperty), "currentValue"); + var memberAccess = Expression.MakeMemberAccess(parameter, DataBinding.Registration.Member); + var assignment = Expression.Assign(memberAccess, propertyValue); + var referenceTypeLambda = Expression.Lambda>(assignment, parameter, propertyValue); - var setterMethod = DataBinding.TargetProperty.GetSetMethod(); - if (setterMethod == null) - return null; + ReferenceTypeSetExpression = referenceTypeLambda.Compile(); + } - var constant = Expression.Constant(DataBinding.LayerProperty); - var propertyValue = Expression.Parameter(typeof(object), "propertyValue"); + private void CreateSetValueTypeExpression() + { + var propertyValue = Expression.Parameter(typeof(TProperty), "propertyValue"); + var variableCurrent = Expression.Variable(typeof(TLayerProperty), "current"); + var layerProperty = Expression.Constant(DataBinding.LayerProperty); + var layerPropertyMemberAccess = Expression.MakeMemberAccess(layerProperty, + DataBinding.LayerProperty.GetType().GetMember(nameof(DataBinding.LayerProperty.CurrentValue))[0]); - // The assign method should cast to the proper type since it receives an object - var body = Expression.Call(constant, setterMethod, Expression.Convert(propertyValue, DataBinding.TargetProperty.PropertyType)); - var lambda = Expression.Lambda>(body, propertyValue); - return lambda.Compile(); + var body = Expression.Block( + new[] {variableCurrent}, + Expression.Assign(variableCurrent, layerPropertyMemberAccess), + Expression.Assign(Expression.MakeMemberAccess(variableCurrent, DataBinding.Registration.Member), propertyValue), + Expression.Assign(layerPropertyMemberAccess, variableCurrent) + ); + + var valueTypeLambda = Expression.Lambda>(body, propertyValue); + ValueTypeSetExpression = valueTypeLambda.Compile(); + } + + private void CreateSetCurrentValueExpression() + { + var propertyValue = Expression.Parameter(typeof(TProperty), "propertyValue"); + var layerProperty = Expression.Constant(DataBinding.LayerProperty); + var layerPropertyMemberAccess = Expression.MakeMemberAccess(layerProperty, + DataBinding.LayerProperty.GetType().GetMember(nameof(DataBinding.LayerProperty.CurrentValue))[0]); + + var body = Expression.Assign(layerPropertyMemberAccess, propertyValue); + var lambda = Expression.Lambda>(body, propertyValue); + ValueTypeSetExpression = lambda.Compile(); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingModifier.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingModifier.cs index 163c35ac4..5ad50d0ee 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingModifier.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingModifier.cs @@ -2,43 +2,43 @@ using System.Linq; using System.Linq.Expressions; using Artemis.Core.DataModelExpansions; -using Artemis.Core.Services; using Artemis.Storage.Entities.Profile.DataBindings; using Newtonsoft.Json; namespace Artemis.Core { - /// - /// Modifies a data model value in a way defined by the modifier type - /// - public class DataBindingModifier + /// + public class DataBindingModifier : IDataBindingModifier { - private DataBinding _dataBinding; + private DataBinding _dataBinding; + private bool _disposed; /// - /// Creates a new instance of the class + /// Creates a new instance of the class /// + /// The data binding the modifier is to be applied to /// The type of the parameter, can either be dynamic (based on a data model value) or static - public DataBindingModifier(ProfileRightSideType parameterType) + public DataBindingModifier(DataBinding dataBinding, ProfileRightSideType parameterType) { + _dataBinding = dataBinding ?? throw new ArgumentNullException(nameof(dataBinding)); ParameterType = parameterType; Entity = new DataBindingModifierEntity(); - - ApplyToEntity(); + Initialize(); + Save(); } - internal DataBindingModifier(DataBinding dataBinding, DataBindingModifierEntity entity) + internal DataBindingModifier(DataBinding dataBinding, DataBindingModifierEntity entity) { - DataBinding = dataBinding; + _dataBinding = dataBinding; Entity = entity; - - ApplyToDataBindingModifier(); + Load(); + Initialize(); } /// /// Gets the data binding this modifier is applied to /// - public DataBinding DataBinding + public DataBinding DataBinding { get => _dataBinding; internal set @@ -86,6 +86,52 @@ namespace Artemis.Core internal DataBindingModifierEntity Entity { get; set; } + + /// + public void Save() + { + if (_disposed) + throw new ObjectDisposedException("DataBindingModifier"); + + if (!DataBinding.Entity.Modifiers.Contains(Entity)) + DataBinding.Entity.Modifiers.Add(Entity); + + // Modifier + if (ModifierType != null) + { + Entity.ModifierType = ModifierType.GetType().Name; + Entity.ModifierTypePluginGuid = ModifierType.PluginInfo.Guid; + } + + // General + Entity.Order = Order; + Entity.ParameterType = (int) ParameterType; + + // Parameter + if (ParameterDataModel != null) + { + Entity.ParameterDataModelGuid = ParameterDataModel.PluginInfo.Guid; + Entity.ParameterPropertyPath = ParameterPropertyPath; + } + + Entity.ParameterStaticValue = JsonConvert.SerializeObject(ParameterStaticValue); + } + + /// + public void Load() + { + if (_disposed) + throw new ObjectDisposedException("DataBindingModifier"); + + // Modifier type is done during Initialize + + // General + Order = Entity.Order; + ParameterType = (ProfileRightSideType) Entity.ParameterType; + + // Parameter is done during initialize + } + /// /// Applies the modifier to the provided value /// @@ -93,6 +139,9 @@ namespace Artemis.Core /// The modified value public object Apply(object currentValue) { + if (_disposed) + throw new ObjectDisposedException("DataBindingModifier"); + if (ModifierType == null) return currentValue; @@ -117,6 +166,9 @@ namespace Artemis.Core /// public void UpdateModifierType(DataBindingModifierType modifierType) { + if (_disposed) + throw new ObjectDisposedException("DataBindingModifier"); + // Calling CreateExpression will clear compiled expressions if (modifierType == null) { @@ -125,7 +177,7 @@ namespace Artemis.Core return; } - var targetType = DataBinding.TargetProperty.PropertyType; + var targetType = DataBinding.GetTargetType(); if (!modifierType.SupportsType(targetType)) { throw new ArtemisCoreException($"Cannot apply modifier type {modifierType.GetType().Name} to this modifier because " + @@ -143,6 +195,9 @@ namespace Artemis.Core /// The path pointing to the parameter inside the data model public void UpdateParameter(DataModel dataModel, string path) { + if (_disposed) + throw new ObjectDisposedException("DataBindingModifier"); + if (dataModel != null && path == null) throw new ArtemisCoreException("If a data model is provided, a path is also required"); if (dataModel == null && path != null) @@ -167,11 +222,14 @@ namespace Artemis.Core /// The static value to use as a parameter public void UpdateParameter(object staticValue) { + if (_disposed) + throw new ObjectDisposedException("DataBindingModifier"); + ParameterType = ProfileRightSideType.Static; ParameterDataModel = null; ParameterPropertyPath = null; - var targetType = DataBinding.TargetProperty.PropertyType; + var targetType = DataBinding.GetTargetType(); // If not null ensure the types match and if not, convert it if (staticValue != null && staticValue.GetType() == targetType) @@ -187,12 +245,17 @@ namespace Artemis.Core CreateExpression(); } - internal void Initialize(IDataModelService dataModelService, IDataBindingService dataBindingService) + private void Initialize() { + DataBindingModifierTypeStore.DataBindingModifierAdded += DataBindingModifierTypeStoreOnDataBindingModifierAdded; + DataBindingModifierTypeStore.DataBindingModifierRemoved += DataBindingModifierTypeStoreOnDataBindingModifierRemoved; + DataModelStore.DataModelAdded += DataModelStoreOnDataModelAdded; + DataModelStore.DataModelRemoved += DataModelStoreOnDataModelRemoved; + // Modifier type if (Entity.ModifierTypePluginGuid != null && ModifierType == null) { - var modifierType = dataBindingService.GetModifierType(Entity.ModifierTypePluginGuid.Value, Entity.ModifierType); + var modifierType = DataBindingModifierTypeStore.Get(Entity.ModifierTypePluginGuid.Value, Entity.ModifierType)?.DataBindingModifierType; if (modifierType != null) UpdateModifierType(modifierType); } @@ -200,7 +263,7 @@ namespace Artemis.Core // Dynamic parameter if (ParameterType == ProfileRightSideType.Dynamic && Entity.ParameterDataModelGuid != null && ParameterDataModel == null) { - var dataModel = dataModelService.GetPluginDataModelByGuid(Entity.ParameterDataModelGuid.Value); + var dataModel = DataModelStore.Get(Entity.ParameterDataModelGuid.Value)?.DataModel; if (dataModel != null && dataModel.ContainsPath(Entity.ParameterPropertyPath)) UpdateParameter(dataModel, Entity.ParameterPropertyPath); } @@ -208,7 +271,7 @@ namespace Artemis.Core else if (ParameterType == ProfileRightSideType.Static && Entity.ParameterStaticValue != null && ParameterStaticValue == null) { // Use the target type so JSON.NET has a better idea what to do - var targetType = DataBinding.TargetProperty.PropertyType; + var targetType = DataBinding.GetTargetType(); object staticValue; try @@ -218,7 +281,7 @@ namespace Artemis.Core // If deserialization fails, use the type's default catch (JsonSerializationException e) { - dataBindingService.LogModifierDeserializationFailure(this, e); + DeserializationLogger.LogModifierDeserializationFailure(GetType().Name, e); staticValue = Activator.CreateInstance(targetType); } @@ -226,34 +289,6 @@ namespace Artemis.Core } } - internal void ApplyToEntity() - { - // Modifier - Entity.ModifierType = ModifierType?.GetType().Name; - Entity.ModifierTypePluginGuid = ModifierType?.PluginInfo.Guid; - - // General - Entity.Order = Order; - Entity.ParameterType = (int) ParameterType; - - // Parameter - Entity.ParameterDataModelGuid = ParameterDataModel?.PluginInfo.Guid; - Entity.ParameterPropertyPath = ParameterPropertyPath; - Entity.ParameterStaticValue = JsonConvert.SerializeObject(ParameterStaticValue); - } - - internal void ApplyToDataBindingModifier() - { - // Modifier type is done during Initialize - - // General - Order = Entity.Order; - ParameterType = (ProfileRightSideType) Entity.ParameterType; - - // Parameter is done during initialize - } - - private void CreateExpression() { CompiledParameterAccessor = null; @@ -285,5 +320,51 @@ namespace Artemis.Core Expression.Property ); } + + #region Event handlers + + private void DataBindingModifierTypeStoreOnDataBindingModifierAdded(object sender, DataBindingModifierTypeStoreEvent e) + { + if (ModifierType != null) + return; + + var modifierType = e.TypeRegistration.DataBindingModifierType; + if (modifierType.PluginInfo.Guid == Entity.ModifierTypePluginGuid && modifierType.GetType().Name == Entity.ModifierType) + UpdateModifierType(modifierType); + } + + private void DataBindingModifierTypeStoreOnDataBindingModifierRemoved(object sender, DataBindingModifierTypeStoreEvent e) + { + if (e.TypeRegistration.DataBindingModifierType == ModifierType) + UpdateModifierType(null); + } + + private void DataModelStoreOnDataModelAdded(object sender, DataModelStoreEvent e) + { + var dataModel = e.Registration.DataModel; + if (dataModel.PluginInfo.Guid == Entity.ParameterDataModelGuid && dataModel.ContainsPath(Entity.ParameterPropertyPath)) + UpdateParameter(dataModel, Entity.ParameterPropertyPath); + } + + private void DataModelStoreOnDataModelRemoved(object sender, DataModelStoreEvent e) + { + if (e.Registration.DataModel != ParameterDataModel) + return; + ParameterDataModel = null; + CompiledParameterAccessor = null; + } + + #endregion + + /// + public void Dispose() + { + _disposed = true; + + DataBindingModifierTypeStore.DataBindingModifierAdded -= DataBindingModifierTypeStoreOnDataBindingModifierAdded; + DataBindingModifierTypeStore.DataBindingModifierRemoved -= DataBindingModifierTypeStoreOnDataBindingModifierRemoved; + DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded; + DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved; + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs index f8d860bf2..d7366ca63 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs @@ -1,22 +1,63 @@ using System; +using System.Linq; +using System.Linq.Expressions; using System.Reflection; namespace Artemis.Core { - public class DataBindingRegistration + /// + public class DataBindingRegistration : IDataBindingRegistration { - internal DataBindingRegistration(BaseLayerProperty layerProperty, PropertyInfo property, DataBindingConverter converter, string path) + internal DataBindingRegistration(LayerProperty layerProperty, + DataBindingConverter converter, + Expression> propertyExpression) { LayerProperty = layerProperty ?? throw new ArgumentNullException(nameof(layerProperty)); - Property = property ?? throw new ArgumentNullException(nameof(property)); Converter = converter ?? throw new ArgumentNullException(nameof(converter)); - Path = path; + PropertyExpression = propertyExpression ?? throw new ArgumentNullException(nameof(propertyExpression)); + + if (propertyExpression.Body is MemberExpression memberExpression) + Member = memberExpression.Member; } - public DataBinding DataBinding { get; internal set; } - public BaseLayerProperty LayerProperty { get; } - public PropertyInfo Property { get; } - public DataBindingConverter Converter { get; } - public string Path { get; } + /// + /// Gets the layer property this registration was made on + /// + public LayerProperty LayerProperty { get; } + + /// + /// Gets the converter that's used by the data binding + /// + public DataBindingConverter Converter { get; } + + /// + /// Gets the expression that that accesses the property + /// + public Expression> PropertyExpression { get; } + + /// + /// Gets the member the targets + /// null if the is not a member expression + /// + public MemberInfo Member { get; } + + /// + /// Gets the data binding created using this registration + /// + public DataBinding DataBinding { get; internal set; } + + /// + public IDataBinding CreateDataBinding() + { + if (DataBinding != null) + return DataBinding; + + var dataBinding = LayerProperty.Entity.DataBindingEntities.FirstOrDefault(e => e.TargetExpression == PropertyExpression.ToString()); + if (dataBinding == null) + return null; + + DataBinding = new DataBinding(LayerProperty, dataBinding); + return DataBinding; + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs new file mode 100644 index 000000000..a609b1b3a --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs @@ -0,0 +1,17 @@ +using System; +using Artemis.Core.DataModelExpansions; + +namespace Artemis.Core +{ + /// + /// Represents a data binding that binds a certain to a value inside a + /// + /// + public interface IDataBinding : IStorageModel, IUpdateModel, IDisposable + { + /// + /// Applies the data binding to the layer property + /// + void Apply(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingConverter.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingConverter.cs new file mode 100644 index 000000000..9f207133f --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingConverter.cs @@ -0,0 +1,16 @@ +using System; + +namespace Artemis.Core +{ + /// + /// Represents a data binding converter that acts as the bridge between a + /// and a + /// + public interface IDataBindingConverter + { + /// + /// Gets the type this converter supports + /// + public Type SupportedType { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingModifier.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingModifier.cs new file mode 100644 index 000000000..6f920c0a1 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingModifier.cs @@ -0,0 +1,11 @@ +using System; + +namespace Artemis.Core +{ + /// + /// Modifies a data model value in a way defined by the modifier type + /// + public interface IDataBindingModifier : IStorageModel, IDisposable + { + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingRegistration.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingRegistration.cs new file mode 100644 index 000000000..2bba9c36a --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingRegistration.cs @@ -0,0 +1,14 @@ +namespace Artemis.Core +{ + /// + /// Represents a data binding registration + /// + public interface IDataBindingRegistration + { + /// + /// If found, creates a data binding from storage + /// + /// + IDataBinding CreateDataBinding(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/Abstract/DataBindingModifierType.cs b/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/Abstract/DataBindingModifierType.cs index 2e01954bf..bf0210834 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/Abstract/DataBindingModifierType.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/Abstract/DataBindingModifierType.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Artemis.Core.Services; namespace Artemis.Core { @@ -10,9 +9,6 @@ namespace Artemis.Core /// public abstract class DataBindingModifierType { - private IDataBindingService _dataBindingService; - private bool _registered; - /// /// Gets the plugin info this data binding modifier belongs to /// Note: Not set until after registering @@ -35,10 +31,16 @@ namespace Artemis.Core public abstract string Icon { get; } /// - /// Gets or sets whether this modifier supports a parameter, defaults to true + /// Gets or sets whether this modifier supports a parameter, defaults to true /// public bool SupportsParameter { get; protected set; } = true; + /// + /// Gets or sets the preferred parameter type + /// If null, the parameter type will match the source property + /// + public Type PreferredParameterType { get; protected set; } = null; + /// /// Returns whether the given type is supported by the modifier /// @@ -63,35 +65,5 @@ namespace Artemis.Core /// /// The modified value, must be a value of a type contained in public abstract object Apply(object currentValue, object parameterValue); - - internal void Register(PluginInfo pluginInfo, IDataBindingService dataBindingService) - { - if (_registered) - return; - - PluginInfo = pluginInfo; - _dataBindingService = dataBindingService; - - if (PluginInfo != Constants.CorePluginInfo) - PluginInfo.Instance.PluginDisabled += InstanceOnPluginDisabled; - - _registered = true; - } - - internal void Unsubscribe() - { - if (!_registered) - return; - - if (PluginInfo != Constants.CorePluginInfo) - PluginInfo.Instance.PluginDisabled -= InstanceOnPluginDisabled; - _registered = false; - } - - private void InstanceOnPluginDisabled(object sender, EventArgs e) - { - // The service will call Unsubscribe - _dataBindingService.RemoveModifierType(this); - } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/DivideModifierType.cs b/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/DivideModifierType.cs index 7f153d14b..0497fde96 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/DivideModifierType.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/DivideModifierType.cs @@ -5,6 +5,11 @@ namespace Artemis.Core { internal class DivideModifierType : DataBindingModifierType { + public DivideModifierType() + { + PreferredParameterType = typeof(float); + } + public override IReadOnlyCollection CompatibleTypes => Constants.NumberTypes; public override string Description => "Divide by"; diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/MultiplicationModifier.cs b/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/MultiplicationModifier.cs index bf400778d..fe71f7ccc 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/MultiplicationModifier.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/Modifiers/MultiplicationModifier.cs @@ -5,6 +5,11 @@ namespace Artemis.Core { internal class MultiplicationModifierType : DataBindingModifierType { + public MultiplicationModifierType() + { + PreferredParameterType = typeof(float); + } + public override IReadOnlyCollection CompatibleTypes => Constants.NumberTypes; public override string Description => "Multiply by"; diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs index 5671181ca..cb495ca70 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -8,24 +8,35 @@ using SkiaSharp; namespace Artemis.Core { + /// + /// Represents a folder in a + /// public sealed class Folder : RenderProfileElement { private SKBitmap _folderBitmap; - public Folder(Profile profile, ProfileElement parent, string name) + /// + /// Creates a new instance of the class and adds itself to the child collection of the provided + /// + /// + /// The parent of the folder + /// The name of the folder + public Folder(ProfileElement parent, string name) { FolderEntity = new FolderEntity(); EntityId = Guid.NewGuid(); - Profile = profile; - Parent = parent; + Parent = parent ?? throw new ArgumentNullException(nameof(parent)); + 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) @@ -42,21 +53,7 @@ namespace Artemis.Core _layerEffects = new List(); _expandedPropertyGroups = new List(); - _expandedPropertyGroups.AddRange(folderEntity.ExpandedPropertyGroups); - - // Load child folders - foreach (var childFolder in Profile.ProfileEntity.Folders.Where(f => f.ParentId == EntityId)) - ChildrenList.Add(new Folder(profile, this, childFolder)); - // Load child layers - foreach (var 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 (var index = 0; index < ChildrenList.Count; index++) - ChildrenList[index].Order = index + 1; - - ApplyRenderElementEntity(); + Load(); } internal FolderEntity FolderEntity { get; set; } @@ -197,21 +194,6 @@ namespace Artemis.Core canvas.Restore(); } - /// - /// Adds a new folder to the bottom of this folder - /// - /// - /// - public Folder AddFolder(string name) - { - if (_disposed) - throw new ObjectDisposedException("Folder"); - - var folder = new Folder(Profile, this, name) {Order = Children.LastOrDefault()?.Order ?? 1}; - AddChild(folder); - return folder; - } - /// public override void AddChild(ProfileElement child, int? order = null) { @@ -260,26 +242,35 @@ namespace Artemis.Core protected override void Dispose(bool disposing) { - if (!disposing) - return; - - foreach (var baseLayerEffect in LayerEffects) - baseLayerEffect.Dispose(); - _layerEffects.Clear(); + _disposed = true; foreach (var profileElement in Children) profileElement.Dispose(); - ChildrenList.Clear(); _folderBitmap?.Dispose(); - _folderBitmap = null; - - Profile = null; - _disposed = true; + base.Dispose(disposing); } + internal override void Load() + { + _expandedPropertyGroups.AddRange(FolderEntity.ExpandedPropertyGroups); - internal override void ApplyToEntity() + // Load child folders + foreach (var childFolder in Profile.ProfileEntity.Folders.Where(f => f.ParentId == EntityId)) + ChildrenList.Add(new Folder(Profile, this, childFolder)); + // Load child layers + foreach (var 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 (var index = 0; index < ChildrenList.Count; index++) + ChildrenList[index].Order = index + 1; + + LoadRenderElement(); + } + + internal override void Save() { if (_disposed) throw new ObjectDisposedException("Folder"); @@ -295,11 +286,7 @@ namespace Artemis.Core FolderEntity.ExpandedPropertyGroups.Clear(); FolderEntity.ExpandedPropertyGroups.AddRange(_expandedPropertyGroups); - ApplyRenderElementToEntity(); - - // Conditions - RenderElementEntity.RootDisplayCondition = DisplayConditionGroup?.Entity; - DisplayConditionGroup?.ApplyToEntity(); + SaveRenderElement(); } #region Events diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index cad95b8d0..97f705548 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Linq; using Artemis.Core.LayerBrushes; using Artemis.Core.LayerEffects; -using Artemis.Core.Services; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.Abstract; using SkiaSharp; @@ -12,8 +12,7 @@ using SkiaSharp; namespace Artemis.Core { /// - /// Represents a layer on a profile. To create new layers use the by injecting - /// into your code + /// Represents a layer in a /// public sealed class Layer : RenderProfileElement { @@ -24,47 +23,49 @@ namespace Artemis.Core private List _leds; private LayerTransformProperties _transform; - internal Layer(Profile profile, ProfileElement parent, string name) + /// + /// Creates a new instance of the class and adds itself to the child collection of the provided + /// + /// + /// The parent of the layer + /// The name of the layer + public Layer(ProfileElement parent, string name) { LayerEntity = new LayerEntity(); EntityId = Guid.NewGuid(); - Profile = profile; - Parent = parent; + Parent = parent ?? throw new ArgumentNullException(nameof(parent)); + Profile = Parent.Profile; Name = name; Enabled = true; DisplayContinuously = true; - General = new LayerGeneralProperties {IsCorePropertyGroup = true}; - Transform = new LayerTransformProperties {IsCorePropertyGroup = true}; + General = new LayerGeneralProperties(); + Transform = new LayerTransformProperties(); _layerEffects = new List(); _leds = new List(); _expandedPropertyGroups = new List(); - General.PropertyGroupInitialized += GeneralOnPropertyGroupInitialized; + Initialize(); ApplyRenderElementDefaults(); + + Parent.AddChild(this); } internal Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity) { LayerEntity = layerEntity; - EntityId = layerEntity.Id; - Profile = profile; Parent = parent; - Name = layerEntity.Name; - Enabled = layerEntity.Enabled; - Order = layerEntity.Order; - General = new LayerGeneralProperties {IsCorePropertyGroup = true}; - Transform = new LayerTransformProperties {IsCorePropertyGroup = true}; + General = new LayerGeneralProperties(); + Transform = new LayerTransformProperties(); _layerEffects = new List(); _leds = new List(); _expandedPropertyGroups = new List(); - _expandedPropertyGroups.AddRange(layerEntity.ExpandedPropertyGroups); - General.PropertyGroupInitialized += GeneralOnPropertyGroupInitialized; - ApplyRenderElementEntity(); + Load(); + Initialize(); } internal LayerEntity LayerEntity { get; set; } @@ -118,56 +119,63 @@ namespace Artemis.Core return $"[Layer] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}"; } - /// - public override List GetAllKeyframes() - { - if (_disposed) - throw new ObjectDisposedException("Layer"); - - var keyframes = base.GetAllKeyframes(); - - foreach (var baseLayerProperty in General.GetAllLayerProperties()) - keyframes.AddRange(baseLayerProperty.BaseKeyframes); - foreach (var baseLayerProperty in Transform.GetAllLayerProperties()) - keyframes.AddRange(baseLayerProperty.BaseKeyframes); - if (LayerBrush?.BaseProperties != null) - { - foreach (var baseLayerProperty in LayerBrush.BaseProperties.GetAllLayerProperties()) - keyframes.AddRange(baseLayerProperty.BaseKeyframes); - } - - return keyframes; - } + #region IDisposable /// protected override void Dispose(bool disposing) { - if (!disposing) - return; - _disposed = true; // Brush first in case it depends on any of the other disposables during it's own disposal _layerBrush?.Dispose(); - _layerBrush = null; - - foreach (var baseLayerEffect in LayerEffects) - baseLayerEffect.Dispose(); - _layerEffects.Clear(); _general?.Dispose(); - _general = null; _layerBitmap?.Dispose(); - _layerBitmap = null; _transform?.Dispose(); - _transform = null; - Profile = null; + base.Dispose(disposing); + } + + #endregion + + private void Initialize() + { + LayerBrushStore.LayerBrushAdded += LayerBrushStoreOnLayerBrushAdded; + LayerBrushStore.LayerBrushRemoved += LayerBrushStoreOnLayerBrushRemoved; + + // Layers have two hardcoded property groups, instantiate them + var generalAttribute = Attribute.GetCustomAttribute( + GetType().GetProperty(nameof(General)), + typeof(PropertyGroupDescriptionAttribute) + ); + var transformAttribute = Attribute.GetCustomAttribute( + GetType().GetProperty(nameof(Transform)), + typeof(PropertyGroupDescriptionAttribute) + ); + General.GroupDescription = (PropertyGroupDescriptionAttribute) generalAttribute; + General.Initialize(this, "General.", Constants.CorePluginInfo); + Transform.GroupDescription = (PropertyGroupDescriptionAttribute) transformAttribute; + Transform.Initialize(this, "Transform.", Constants.CorePluginInfo); + + General.ShapeType.BaseValueChanged += ShapeTypeOnBaseValueChanged; + ApplyShapeType(); + ActivateLayerBrush(); } #region Storage - internal override void ApplyToEntity() + internal override void Load() + { + EntityId = LayerEntity.Id; + Name = LayerEntity.Name; + Enabled = LayerEntity.Enabled; + Order = LayerEntity.Order; + + _expandedPropertyGroups.AddRange(LayerEntity.ExpandedPropertyGroups); + LoadRenderElement(); + } + + internal override void Save() { if (_disposed) throw new ObjectDisposedException("Layer"); @@ -198,24 +206,13 @@ namespace Artemis.Core LayerEntity.Leds.Add(ledEntity); } - // Conditions - RenderElementEntity.RootDisplayCondition = DisplayConditionGroup?.Entity; - DisplayConditionGroup?.ApplyToEntity(); - - ApplyRenderElementToEntity(); + SaveRenderElement(); } #endregion #region Shape management - private void GeneralOnPropertyGroupInitialized(object sender, EventArgs e) - { - ApplyShapeType(); - General.ShapeType.BaseValueChanged -= ShapeTypeOnBaseValueChanged; - General.ShapeType.BaseValueChanged += ShapeTypeOnBaseValueChanged; - } - private void ShapeTypeOnBaseValueChanged(object sender, EventArgs e) { ApplyShapeType(); @@ -312,18 +309,6 @@ namespace Artemis.Core } } - /// - public override List GetAllLayerProperties() - { - var result = base.GetAllLayerProperties(); - result.AddRange(General.GetAllLayerProperties()); - result.AddRange(Transform.GetAllLayerProperties()); - if (LayerBrush?.BaseProperties != null) - result.AddRange(LayerBrush.BaseProperties.GetAllLayerProperties()); - - return result; - } - /// public override void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo) { @@ -383,7 +368,6 @@ namespace Artemis.Core if (Parent is Folder parentFolder) targetLocation = Path.Bounds.Location - parentFolder.Path.Bounds.Location; - canvas.DrawBitmap(_layerBitmap, targetLocation, layerPaint); } @@ -680,7 +664,55 @@ namespace Artemis.Core #endregion - #region Activation + #region Brush management + + /// + /// Changes the current layer brush to the brush described in the provided + /// + public void ChangeLayerBrush(LayerBrushDescriptor descriptor) + { + if (descriptor == null) + throw new ArgumentNullException(nameof(descriptor)); + + if (LayerBrush != null) + { + var brush = LayerBrush; + LayerBrush = null; + brush.Dispose(); + } + + // Ensure the brush reference matches the brush + var current = General.BrushReference.BaseValue; + if (!descriptor.MatchesLayerBrushReference(current)) + General.BrushReference.BaseValue = new LayerBrushReference(descriptor); + + ActivateLayerBrush(); + } + + /// + /// Removes the current layer brush from the layer + /// + public void RemoveLayerBrush() + { + if (LayerBrush == null) + return; + + var brush = LayerBrush; + DeactivateLayerBrush(); + LayerEntity.PropertyEntities.RemoveAll(p => p.PluginGuid == brush.PluginInfo.Guid && p.Path.StartsWith("LayerBrush.")); + } + + internal void ActivateLayerBrush() + { + var current = General.BrushReference.CurrentValue; + if (current == null) + return; + + var descriptor = LayerBrushStore.Get(current.BrushPluginGuid, current.BrushType)?.LayerBrushDescriptor; + descriptor?.CreateInstance(this); + + OnLayerBrushUpdated(); + } internal void DeactivateLayerBrush() { @@ -690,16 +722,29 @@ namespace Artemis.Core var brush = LayerBrush; LayerBrush = null; brush.Dispose(); + + OnLayerBrushUpdated(); } - internal void RemoveLayerBrush() + #endregion + + #region Event handlers + + private void LayerBrushStoreOnLayerBrushRemoved(object sender, LayerBrushStoreEvent e) { - if (LayerBrush == null) + if (LayerBrush?.Descriptor == e.Registration.LayerBrushDescriptor) + DeactivateLayerBrush(); + } + + private void LayerBrushStoreOnLayerBrushAdded(object sender, LayerBrushStoreEvent e) + { + if (LayerBrush != null) return; - var brush = LayerBrush; - DeactivateLayerBrush(); - LayerEntity.PropertyEntities.RemoveAll(p => p.PluginGuid == brush.PluginInfo.Guid && p.Path.StartsWith("LayerBrush.")); + var current = General.BrushReference.CurrentValue; + if (e.Registration.Plugin.PluginInfo.Guid == current.BrushPluginGuid && + e.Registration.LayerBrushDescriptor.LayerBrushType.Name == current.BrushType) + ActivateLayerBrush(); } #endregion diff --git a/src/Artemis.Core/Models/Profile/LayerBrushReference.cs b/src/Artemis.Core/Models/Profile/LayerBrushReference.cs index 588fac723..bcad54663 100644 --- a/src/Artemis.Core/Models/Profile/LayerBrushReference.cs +++ b/src/Artemis.Core/Models/Profile/LayerBrushReference.cs @@ -17,5 +17,15 @@ namespace Artemis.Core /// The full type name of the brush descriptor /// public string BrushType { get; set; } + + public LayerBrushReference() + { + } + + public LayerBrushReference(LayerBrushDescriptor descriptor) + { + BrushPluginGuid = descriptor.LayerBrushProvider.PluginInfo.Guid; + BrushType = descriptor.LayerBrushType.Name; + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs deleted file mode 100644 index 10f0a53f6..000000000 --- a/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs +++ /dev/null @@ -1,273 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using Artemis.Core.Services; -using Artemis.Storage.Entities.Profile; - -namespace Artemis.Core -{ - /// - /// For internal use only, to implement your own layer property type, extend instead. - /// - public abstract class BaseLayerProperty - { - protected readonly List _dataBindingRegistrations = new List(); - protected readonly List _dataBindings = new List(); - - private object _baseValue; - private object _currentValue; - private object _defaultValue; - private bool _isHidden; - private bool _keyframesEnabled; - - internal BaseLayerProperty() - { - } - - /// - /// Gets or sets the base value of this layer property without any keyframes applied - /// - public object BaseValue - { - get => _baseValue; - set - { - if (value != null && value.GetType() != GetPropertyType()) - throw new ArtemisCoreException("Cannot update base value because of a type mismatch"); - _baseValue = value; - } - } - - /// - /// Gets the current value of this property as it is affected by it's keyframes, updated once every frame - /// - public object CurrentValue - { - get => _currentValue; - set - { - if (value != null && value.GetType() != GetPropertyType()) - throw new ArtemisCoreException("Cannot update current value because of a type mismatch"); - _currentValue = value; - } - } - - /// - /// Gets or sets the default value of this layer property. If set, this value is automatically applied if the property - /// has no value in storage - /// - public object DefaultValue - { - get => _defaultValue; - set - { - if (value != null && value.GetType() != GetPropertyType()) - throw new ArtemisCoreException("Cannot update default value because of a type mismatch"); - _defaultValue = value; - } - } - - /// - /// Gets a list containing the active data bindings - /// - public IReadOnlyList DataBindings => _dataBindings.AsReadOnly(); - - /// - /// Gets a list containing all the data binding registrations - /// - public IReadOnlyList DataBindingRegistrations => _dataBindingRegistrations.AsReadOnly(); - - /// - /// Gets the profile element (such as layer or folder) this effect is applied to - /// - public RenderProfileElement ProfileElement { get; internal set; } - - /// - /// The parent group of this layer property, set after construction - /// - public LayerPropertyGroup Parent { get; internal set; } - - /// - /// Gets whether keyframes are supported on this type of property - /// - public bool KeyframesSupported { get; protected internal set; } = true; - - /// - /// Gets whether data bindings are supported on this type of property - /// - public bool DataBindingsSupported { get; protected internal set; } = true; - - /// - /// Gets or sets whether keyframes are enabled on this property, has no effect if is - /// False - /// - public bool KeyframesEnabled - { - get => _keyframesEnabled; - set - { - if (_keyframesEnabled == value) return; - _keyframesEnabled = value; - OnKeyframesToggled(); - } - } - - /// - /// Gets or sets whether the property is hidden in the UI - /// - public bool IsHidden - { - get => _isHidden; - set - { - _isHidden = value; - OnVisibilityChanged(); - } - } - - /// - /// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied - /// - public bool IsLoadedFromStorage { get; internal set; } - - /// - /// Used to declare that this property doesn't belong to a plugin and should use the core plugin GUID - /// - public bool IsCoreProperty { get; internal set; } - - /// - /// Gets the description attribute applied to this property - /// - public PropertyDescriptionAttribute PropertyDescription { get; internal set; } - - /// - /// Gets a list of all the keyframes in their non-generic base form, without their values being available - /// - public abstract IReadOnlyList BaseKeyframes { get; } - - internal PropertyEntity PropertyEntity { get; set; } - internal LayerPropertyGroup LayerPropertyGroup { get; set; } - - /// - /// Overrides the property value with the default value - /// - public abstract void ApplyDefaultValue(); - - /// - /// Returns the type of the property - /// - public abstract Type GetPropertyType(); - - /// - /// Applies the provided property entity to the layer property by deserializing the JSON base value and keyframe values - /// - /// - /// - /// - internal abstract void ApplyToLayerProperty(PropertyEntity entity, LayerPropertyGroup layerPropertyGroup, bool fromStorage); - - /// - /// Saves the property to the underlying property entity that was configured when calling - /// - /// - internal abstract void ApplyToEntity(); - - #region Data bindings - - internal void InitializeDataBindings(IDataModelService dataModelService, IDataBindingService dataBindingService) - { - foreach (var dataBinding in DataBindings) - dataBinding.Initialize(dataModelService, dataBindingService); - } - - /// - /// Adds a new data binding targeting the given property to the collection - /// - /// The newly created data binding - public DataBinding EnableDataBinding(DataBindingRegistration dataBindingRegistration) - { - if (dataBindingRegistration.LayerProperty != this) - throw new ArtemisCoreException("Cannot enable a data binding using a data binding registration of a different layer property"); - - var dataBinding = new DataBinding(dataBindingRegistration); - _dataBindings.Add(dataBinding); - - return dataBinding; - } - - /// - /// Removes the provided data binding from the collection - /// - /// The data binding to remove - public void DisableDataBinding(DataBinding dataBinding) - { - _dataBindings.Remove(dataBinding); - } - - #endregion - - #region Events - - /// - /// Occurs once every frame when the layer property is updated - /// - public event EventHandler Updated; - - /// - /// Occurs when the base value of the layer property was updated - /// - public event EventHandler BaseValueChanged; - - /// - /// Occurs when the value of the layer property was updated - /// - public event EventHandler VisibilityChanged; - - /// - /// Occurs when keyframes are enabled/disabled - /// - public event EventHandler KeyframesToggled; - - /// - /// Occurs when a new keyframe was added to the layer property - /// - public event EventHandler KeyframeAdded; - - /// - /// Occurs when a keyframe was removed from the layer property - /// - public event EventHandler KeyframeRemoved; - - protected virtual void OnUpdated() - { - Updated?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - protected virtual void OnBaseValueChanged() - { - BaseValueChanged?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - protected virtual void OnVisibilityChanged() - { - VisibilityChanged?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - protected virtual void OnKeyframesToggled() - { - KeyframesToggled?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - protected virtual void OnKeyframeAdded() - { - KeyframeAdded?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - protected virtual void OnKeyframeRemoved() - { - KeyframeRemoved?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerPropertyKeyframe.cs b/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerPropertyKeyframe.cs deleted file mode 100644 index ced2050b9..000000000 --- a/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerPropertyKeyframe.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using Stylet; - -namespace Artemis.Core -{ - /// - /// For internal use only, use instead. - /// - public abstract class BaseLayerPropertyKeyframe : PropertyChangedBase - { - internal BaseLayerPropertyKeyframe(BaseLayerProperty baseLayerProperty) - { - BaseLayerProperty = baseLayerProperty; - } - - /// - /// The base class of the layer property this keyframe is applied to - /// - public BaseLayerProperty BaseLayerProperty { get; internal set; } - - /// - /// The position of this keyframe in the timeline - /// - public abstract TimeSpan Position { get; set; } - - /// - /// The easing function applied on the value of the keyframe - /// - public Easings.Functions EasingFunction { get; set; } - - /// - /// Removes the keyframe from the layer property - /// - public abstract void Remove(); - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs new file mode 100644 index 000000000..909255cb5 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using Artemis.Storage.Entities.Profile; + +namespace Artemis.Core +{ + /// + /// Represents a property on a layer. Properties are saved in storage and can optionally be modified from the UI. + /// + /// Note: You cannot initialize layer properties yourself. If properly placed and annotated, the Artemis core will + /// initialize these for you. + /// + /// + public interface ILayerProperty : IStorageModel, IUpdateModel, IDisposable + { + /// + /// Initializes the layer property + /// + /// Note: This isn't done in the constructor to keep it parameterless which is easier for implementations of + /// + /// + /// + void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description); + + /// + /// Returns a list off all data binding registrations + /// + List GetAllDataBindingRegistrations(); + } +} \ 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 ec44b97d3..01ccfc4c7 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -17,52 +16,191 @@ namespace Artemis.Core /// /// /// The type of property encapsulated in this layer property - public abstract class LayerProperty : BaseLayerProperty + public abstract class LayerProperty : ILayerProperty { - private bool _isInitialized; - private List> _keyframes; + private bool _disposed; + /// + /// Creates a new instance of the class + /// protected LayerProperty() { _keyframes = new List>(); } /// - /// Gets or sets the base value of this layer property without any keyframes applied + /// Gets the description attribute applied to this property /// - public new T BaseValue + public PropertyDescriptionAttribute PropertyDescription { get; internal set; } + + /// + /// Updates the property, applying keyframes and data bindings to the current value + /// + public void Update(double deltaTime) { - get => base.BaseValue != null ? (T) base.BaseValue : default; + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + CurrentValue = BaseValue; + + UpdateKeyframes(); + UpdateDataBindings(deltaTime); + + OnUpdated(); + } + + /// + /// Returns the type of the property + /// + public Type GetPropertyType() + { + return typeof(T); + } + + #region Hierarchy + + private bool _isHidden; + + /// + /// Gets or sets whether the property is hidden in the UI + /// + public bool IsHidden + { + get => _isHidden; set { - if (Equals(base.BaseValue, value)) + _isHidden = value; + OnVisibilityChanged(); + } + } + + /// + /// Gets the profile element (such as layer or folder) this property is applied to + /// + public RenderProfileElement ProfileElement { get; internal set; } + + /// + /// The parent group of this layer property, set after construction + /// + public LayerPropertyGroup LayerPropertyGroup { get; internal set; } + + #endregion + + #region Value management + + private T _baseValue; + + /// + /// Called every update (if keyframes are both supported and enabled) to determine the new + /// based on the provided progress + /// + /// The linear current keyframe progress + /// The current keyframe progress, eased with the current easing function + protected virtual void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) + { + throw new NotImplementedException(); + } + + /// + /// Gets or sets the base value of this layer property without any keyframes applied + /// + public T BaseValue + { + get => _baseValue; + set + { + if (Equals(_baseValue, value)) return; - base.BaseValue = value; - Update(); + _baseValue = value; + Update(0); OnBaseValueChanged(); + LayerPropertyGroup.OnLayerPropertyBaseValueChanged(new LayerPropertyEventArgs(this)); } } /// /// Gets the current value of this property as it is affected by it's keyframes, updated once every frame /// - public new T CurrentValue - { - get => base.CurrentValue != null ? (T) base.CurrentValue : default; - set => base.CurrentValue = value; - } + public T CurrentValue { get; set; } /// /// Gets or sets the default value of this layer property. If set, this value is automatically applied if the property /// has no value in storage /// - public new T DefaultValue + public T DefaultValue { get; set; } + + /// + /// Sets the current value, using either keyframes if enabled or the base value. + /// + /// The value to set. + /// + /// An optional time to set the value add, if provided and property is using keyframes the value will be set to an new + /// or existing keyframe. + /// + public void SetCurrentValue(T value, TimeSpan? time) { - get => base.DefaultValue != null ? (T) base.DefaultValue : default; - set => base.DefaultValue = value; + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + if (time == null || !KeyframesEnabled || !KeyframesSupported) + BaseValue = value; + else + { + // If on a keyframe, update the keyframe + var currentKeyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value); + // Create a new keyframe if none found + if (currentKeyframe == null) + AddKeyframe(new LayerPropertyKeyframe(value, time.Value, Easings.Functions.Linear, this)); + else + currentKeyframe.Value = value; + } + + // 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); } + /// + /// Overrides the property value with the default value + /// + public void ApplyDefaultValue() + { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + BaseValue = DefaultValue; + CurrentValue = DefaultValue; + } + + #endregion + + #region Keyframes + + private bool _keyframesEnabled; + private List> _keyframes; + + /// + /// Gets whether keyframes are supported on this type of property + /// + public bool KeyframesSupported { get; protected internal set; } = true; + + /// + /// Gets or sets whether keyframes are enabled on this property, has no effect if is + /// False + /// + public bool KeyframesEnabled + { + get => _keyframesEnabled; + set + { + if (_keyframesEnabled == value) return; + _keyframesEnabled = value; + OnKeyframesToggled(); + } + } + + /// /// Gets a read-only list of all the keyframes on this layer property /// @@ -78,50 +216,22 @@ namespace Artemis.Core /// public LayerPropertyKeyframe NextKeyframe { get; protected set; } - public override IReadOnlyList BaseKeyframes => _keyframes.Cast().ToList().AsReadOnly(); - - /// - /// Sets the current value, using either keyframes if enabled or the base value. - /// - /// The value to set. - /// - /// An optional time to set the value add, if provided and property is using keyframes the value will be set to an new - /// or existing keyframe. - /// - public void SetCurrentValue(T value, TimeSpan? time) - { - if (time == null || !KeyframesEnabled || !KeyframesSupported) - BaseValue = value; - else - { - // If on a keyframe, update the keyframe - var currentKeyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value); - // Create a new keyframe if none found - if (currentKeyframe == null) - AddKeyframe(new LayerPropertyKeyframe(value, time.Value, Easings.Functions.Linear, this)); - else - currentKeyframe.Value = value; - } - - // 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(); - } - /// /// Adds a keyframe to the layer property /// /// The keyframe to add public void AddKeyframe(LayerPropertyKeyframe keyframe) { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + if (_keyframes.Contains(keyframe)) return; keyframe.LayerProperty?.RemoveKeyframe(keyframe); - keyframe.LayerProperty = this; - keyframe.BaseLayerProperty = this; _keyframes.Add(keyframe); + SortKeyframes(); OnKeyframeAdded(); } @@ -132,6 +242,9 @@ namespace Artemis.Core /// The keyframe to remove public LayerPropertyKeyframe CopyKeyframe(LayerPropertyKeyframe keyframe) { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + var newKeyframe = new LayerPropertyKeyframe( keyframe.Value, keyframe.Position, @@ -149,63 +262,18 @@ namespace Artemis.Core /// The keyframe to remove public void RemoveKeyframe(LayerPropertyKeyframe keyframe) { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + if (!_keyframes.Contains(keyframe)) return; _keyframes.Remove(keyframe); keyframe.LayerProperty = null; - keyframe.BaseLayerProperty = null; SortKeyframes(); OnKeyframeRemoved(); } - /// - /// Removes all keyframes from the layer property - /// - public void ClearKeyframes() - { - var keyframes = new List>(_keyframes); - foreach (var layerPropertyKeyframe in keyframes) - RemoveKeyframe(layerPropertyKeyframe); - } - - /// - public override void ApplyDefaultValue() - { - BaseValue = DefaultValue; - CurrentValue = DefaultValue; - } - - /// - public override Type GetPropertyType() - { - return typeof(T); - } - - /// - /// Called every update (if keyframes are both supported and enabled) to determine the new - /// based on the provided progress - /// - /// The linear current keyframe progress - /// The current keyframe progress, eased with the current easing function - protected virtual void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) - { - throw new NotImplementedException(); - } - - /// - /// Updates the property, applying keyframes and data bindings to the current value - /// - internal void Update(double deltaTime = 0) - { - CurrentValue = BaseValue; - - UpdateKeyframes(); - UpdateDataBindings(deltaTime); - - OnUpdated(); - } - /// /// Sorts the keyframes in ascending order by position /// @@ -214,77 +282,6 @@ namespace Artemis.Core _keyframes = _keyframes.OrderBy(k => k.Position).ToList(); } - - internal override void ApplyToLayerProperty(PropertyEntity entity, LayerPropertyGroup layerPropertyGroup, bool fromStorage) - { - // Doubt this will happen but let's make sure - if (_isInitialized) - throw new ArtemisCoreException("Layer property already initialized, wut"); - - PropertyEntity = entity; - LayerPropertyGroup = layerPropertyGroup; - LayerPropertyGroup.PropertyGroupUpdating += (sender, args) => Update(args.DeltaTime); - - try - { - if (entity.Value != null) - BaseValue = JsonConvert.DeserializeObject(entity.Value); - - IsLoadedFromStorage = fromStorage; - CurrentValue = BaseValue; - KeyframesEnabled = entity.KeyframesEnabled; - - _keyframes.Clear(); - _keyframes.AddRange(entity.KeyframeEntities.Select(k => new LayerPropertyKeyframe( - JsonConvert.DeserializeObject(k.Value), - k.Position, - (Easings.Functions) k.EasingFunction, - this - ))); - - _dataBindings.Clear(); - foreach (var entityDataBindingEntity in entity.DataBindingEntities) - { - var dataBinding = new DataBinding(this, entityDataBindingEntity); - _dataBindings.Add(dataBinding); - } - } - catch (JsonException e) - { - // TODO: Properly log the JSON exception - Debug.WriteLine($"JSON exception while deserializing: {e}"); - IsLoadedFromStorage = false; - } - finally - { - SortKeyframes(); - _isInitialized = true; - } - } - - internal override void ApplyToEntity() - { - if (!_isInitialized) - throw new ArtemisCoreException("Layer property is not yet initialized"); - - PropertyEntity.Value = JsonConvert.SerializeObject(BaseValue); - PropertyEntity.KeyframesEnabled = KeyframesEnabled; - PropertyEntity.KeyframeEntities.Clear(); - PropertyEntity.KeyframeEntities.AddRange(Keyframes.Select(k => new KeyframeEntity - { - Value = JsonConvert.SerializeObject(k.Value), - Position = k.Position, - EasingFunction = (int) k.EasingFunction - })); - - PropertyEntity.DataBindingEntities.Clear(); - foreach (var dataBinding in DataBindings) - { - dataBinding.ApplyToEntity(); - PropertyEntity.DataBindingEntities.Add(dataBinding.Entity); - } - } - private void UpdateKeyframes() { if (!KeyframesSupported || !KeyframesEnabled) @@ -311,48 +308,269 @@ namespace Artemis.Core } } + #endregion + #region Data bindings - public void RegisterDataBindingProperty(Expression> propertyLambda, DataBindingConverter converter) - { - // If the lambda references to itself, use the property info of public new T CurrentValue - PropertyInfo propertyInfo; - string path = null; - if (propertyLambda.Parameters[0] == propertyLambda.Body) - { - propertyInfo = GetType().GetProperties().FirstOrDefault(p => p.Name == nameof(CurrentValue) && p.PropertyType == typeof(T)); - } - else - { - propertyInfo = ReflectionUtilities.GetPropertyInfo(CurrentValue, propertyLambda); - // Deconstruct the lambda - var current = (MemberExpression) propertyLambda.Body; - path = current.Member.Name; - while (current.Expression is MemberExpression memberExpression) - { - path = current.Member.Name + "." + path; - current = memberExpression; - } - } + internal readonly List _dataBindingRegistrations = new List(); + internal readonly List _dataBindings = new List(); - if (converter.SupportedType != propertyInfo.PropertyType) + /// + /// Gets whether data bindings are supported on this type of property + /// + public bool DataBindingsSupported { get; protected internal set; } = true; + + public DataBindingRegistration GetDataBindingRegistration(string expression) + { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + var match = _dataBindingRegistrations.FirstOrDefault(r => r is DataBindingRegistration registration && + registration.PropertyExpression.ToString() == expression); + return (DataBindingRegistration) match; + } + + public List GetAllDataBindingRegistrations() + { + return _dataBindingRegistrations; + } + + public void RegisterDataBindingProperty(Expression> propertyExpression, DataBindingConverter converter) + { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + if (propertyExpression.Body.NodeType != ExpressionType.MemberAccess && propertyExpression.Body.NodeType != ExpressionType.Parameter) + throw new ArtemisCoreException("Provided expression is invalid, it must be 'value => value' or 'value => value.Property'"); + + if (converter.SupportedType != propertyExpression.ReturnType) { - throw new ArtemisCoreException($"Cannot register data binding property for property {propertyInfo.Name} " + + throw new ArtemisCoreException($"Cannot register data binding property for property {PropertyDescription.Name} " + "because the provided converter does not support the property's type"); } - _dataBindingRegistrations.Add(new DataBindingRegistration(this, propertyInfo, converter, path)); + _dataBindingRegistrations.Add(new DataBindingRegistration(this, converter, propertyExpression)); + } + + /// + /// Enables a data binding for the provided + /// + /// The newly created data binding + public DataBinding EnableDataBinding(DataBindingRegistration dataBindingRegistration) + { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + if (dataBindingRegistration.LayerProperty != this) + throw new ArtemisCoreException("Cannot enable a data binding using a data binding registration of a different layer property"); + + var dataBinding = new DataBinding(dataBindingRegistration); + _dataBindings.Add(dataBinding); + + return dataBinding; + } + + /// + /// Disables the provided data binding + /// + /// The data binding to remove + public void DisableDataBinding(DataBinding dataBinding) + { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + _dataBindings.Remove(dataBinding); } private void UpdateDataBindings(double deltaTime) { - foreach (var dataBinding in DataBindings) + foreach (var dataBinding in _dataBindings) { dataBinding.Update(deltaTime); - dataBinding.ApplyToProperty(); + dataBinding.Apply(); } } #endregion + + #region Storage + + private bool _isInitialized; + + /// + /// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied + /// + public bool IsLoadedFromStorage { get; internal set; } + + internal PropertyEntity Entity { get; set; } + + /// + public void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description) + { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + _isInitialized = true; + + ProfileElement = profileElement ?? throw new ArgumentNullException(nameof(profileElement)); + LayerPropertyGroup = group ?? throw new ArgumentNullException(nameof(group)); + Entity = entity ?? throw new ArgumentNullException(nameof(entity)); + PropertyDescription = description ?? throw new ArgumentNullException(nameof(description)); + IsLoadedFromStorage = fromStorage; + LayerPropertyGroup.PropertyGroupUpdating += (sender, args) => Update(args.DeltaTime); + } + + /// + public void Load() + { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + if (!_isInitialized) + throw new ArtemisCoreException("Layer property is not yet initialized"); + + if (!IsLoadedFromStorage) + ApplyDefaultValue(); + else + { + try + { + if (Entity.Value != null) + BaseValue = JsonConvert.DeserializeObject(Entity.Value); + } + catch (JsonException e) + { + // ignored for now + } + } + + CurrentValue = BaseValue; + KeyframesEnabled = Entity.KeyframesEnabled; + + _keyframes.Clear(); + try + { + _keyframes.AddRange(Entity.KeyframeEntities.Select(k => new LayerPropertyKeyframe( + JsonConvert.DeserializeObject(k.Value), + k.Position, + (Easings.Functions) k.EasingFunction, + this + ))); + } + catch (JsonException e) + { + // ignored for now + } + + _dataBindings.Clear(); + foreach (var dataBindingRegistration in _dataBindingRegistrations) + { + var dataBinding = dataBindingRegistration.CreateDataBinding(); + if (dataBinding != null) + _dataBindings.Add(dataBinding); + } + } + + /// + /// Saves the property to the underlying property entity that was configured when calling + /// + /// + public void Save() + { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + if (!_isInitialized) + throw new ArtemisCoreException("Layer property is not yet initialized"); + + Entity.Value = JsonConvert.SerializeObject(BaseValue); + Entity.KeyframesEnabled = KeyframesEnabled; + Entity.KeyframeEntities.Clear(); + Entity.KeyframeEntities.AddRange(Keyframes.Select(k => new KeyframeEntity + { + Value = JsonConvert.SerializeObject(k.Value), + Position = k.Position, + EasingFunction = (int) k.EasingFunction + })); + + Entity.DataBindingEntities.Clear(); + foreach (var dataBinding in _dataBindings) + dataBinding.Save(); + } + + #endregion + + #region Events + + /// + /// Occurs once every frame when the layer property is updated + /// + public event EventHandler> Updated; + + /// + /// Occurs when the base value of the layer property was updated + /// + public event EventHandler> BaseValueChanged; + + /// + /// Occurs when the value of the layer property was updated + /// + public event EventHandler> VisibilityChanged; + + /// + /// Occurs when keyframes are enabled/disabled + /// + public event EventHandler> KeyframesToggled; + + /// + /// Occurs when a new keyframe was added to the layer property + /// + public event EventHandler> KeyframeAdded; + + /// + /// Occurs when a keyframe was removed from the layer property + /// + public event EventHandler> KeyframeRemoved; + + protected virtual void OnUpdated() + { + Updated?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + protected virtual void OnBaseValueChanged() + { + BaseValueChanged?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + protected virtual void OnVisibilityChanged() + { + VisibilityChanged?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + protected virtual void OnKeyframesToggled() + { + KeyframesToggled?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + protected virtual void OnKeyframeAdded() + { + KeyframeAdded?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + protected virtual void OnKeyframeRemoved() + { + KeyframeRemoved?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + #endregion + + /// + public void Dispose() + { + _disposed = true; + + foreach (var dataBinding in _dataBindings) + dataBinding.Dispose(); + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs index 8ed7642bf..cdade5471 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs @@ -1,16 +1,28 @@ using System; +using Stylet; namespace Artemis.Core { - public class LayerPropertyKeyframe : BaseLayerPropertyKeyframe + /// + /// Represents a keyframe on a containing a value and a timestamp + /// + public class LayerPropertyKeyframe : PropertyChangedBase { private LayerProperty _layerProperty; private TimeSpan _position; private T _value; - public LayerPropertyKeyframe(T value, TimeSpan position, Easings.Functions easingFunction, LayerProperty layerProperty) : base(layerProperty) + /// + /// Creates a new instance of the class + /// + /// The value of the keyframe + /// The position of this keyframe in the timeline + /// The easing function applied on the value of the keyframe + /// The layer property this keyframe is applied to + public LayerPropertyKeyframe(T value, TimeSpan position, Easings.Functions easingFunction, LayerProperty layerProperty) { _position = position; + Value = value; LayerProperty = layerProperty; EasingFunction = easingFunction; @@ -34,8 +46,11 @@ namespace Artemis.Core set => SetAndNotify(ref _value, value); } - /// - public override TimeSpan Position + + /// + /// The position of this keyframe in the timeline + /// + public TimeSpan Position { get => _position; set @@ -45,8 +60,15 @@ namespace Artemis.Core } } - /// - public override void Remove() + /// + /// The easing function applied on the value of the keyframe + /// + public Easings.Functions EasingFunction { get; set; } + + /// + /// Removes the keyframe from the layer property + /// + public void Remove() { LayerProperty.RemoveKeyframe(this); } diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Types/ColorGradientLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/ColorGradientLayerProperty.cs index ab5d5a91c..fc1f58542 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Types/ColorGradientLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/ColorGradientLayerProperty.cs @@ -1,6 +1,4 @@ -using Artemis.Storage.Entities.Profile; - -namespace Artemis.Core +namespace Artemis.Core { /// public class ColorGradientLayerProperty : LayerProperty @@ -9,10 +7,12 @@ namespace Artemis.Core { KeyframesSupported = false; DataBindingsSupported = false; + + BaseValueChanged += OnBaseValueChanged; } /// - /// Implicitly converts an to a + /// Implicitly converts an to a /// public static implicit operator ColorGradient(ColorGradientLayerProperty p) { @@ -25,12 +25,11 @@ namespace Artemis.Core throw new ArtemisCoreException("Color Gradients do not support keyframes."); } - internal override void ApplyToLayerProperty(PropertyEntity entity, LayerPropertyGroup layerPropertyGroup, bool fromStorage) + private void OnBaseValueChanged(object sender, LayerPropertyEventArgs e) { - base.ApplyToLayerProperty(entity, layerPropertyGroup, fromStorage); - // Don't allow color gradients to be null - BaseValue ??= DefaultValue ?? new ColorGradient(); + if (BaseValue == null) + BaseValue = DefaultValue ?? new ColorGradient(); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKColorLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKColorLayerProperty.cs index dc9032d9a..674086de7 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKColorLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKColorLayerProperty.cs @@ -7,11 +7,10 @@ namespace Artemis.Core { internal SKColorLayerProperty() { - RegisterDataBindingProperty(color => color.Alpha, new SKColorPartDataBindingConverter(SKColorPartDataBindingConverter.Channel.Alpha)); - RegisterDataBindingProperty(color => color.Red, new SKColorPartDataBindingConverter(SKColorPartDataBindingConverter.Channel.Red)); - RegisterDataBindingProperty(color => color.Green, new SKColorPartDataBindingConverter(SKColorPartDataBindingConverter.Channel.Green)); - RegisterDataBindingProperty(color => color.Blue, new SKColorPartDataBindingConverter(SKColorPartDataBindingConverter.Channel.Blue)); - RegisterDataBindingProperty(color => color.Hue, new SKColorPartDataBindingConverter(SKColorPartDataBindingConverter.Channel.Hue)); + RegisterDataBindingProperty(color => color.Alpha, new SKColorArgbDataBindingConverter(SKColorArgbDataBindingConverter.Channel.Alpha)); + RegisterDataBindingProperty(color => color.Red, new SKColorArgbDataBindingConverter(SKColorArgbDataBindingConverter.Channel.Red)); + RegisterDataBindingProperty(color => color.Green, new SKColorArgbDataBindingConverter(SKColorArgbDataBindingConverter.Channel.Green)); + RegisterDataBindingProperty(color => color.Blue, new SKColorArgbDataBindingConverter(SKColorArgbDataBindingConverter.Channel.Blue)); } /// diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKPointLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKPointLayerProperty.cs index 30194bfac..53484042f 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKPointLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKPointLayerProperty.cs @@ -7,8 +7,8 @@ namespace Artemis.Core { internal SKPointLayerProperty() { - RegisterDataBindingProperty(point => point.X, new FloatDataBindingConverter()); - RegisterDataBindingProperty(point => point.Y, new FloatDataBindingConverter()); + RegisterDataBindingProperty(point => point.X, new FloatDataBindingConverter()); + RegisterDataBindingProperty(point => point.Y, new FloatDataBindingConverter()); } /// diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKSizeLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKSizeLayerProperty.cs index 59504ca18..b8f54a741 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKSizeLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Types/SKSizeLayerProperty.cs @@ -7,8 +7,8 @@ namespace Artemis.Core { internal SKSizeLayerProperty() { - RegisterDataBindingProperty(size => size.Width, new FloatDataBindingConverter()); - RegisterDataBindingProperty(size => size.Height, new FloatDataBindingConverter()); + RegisterDataBindingProperty(size => size.Width, new FloatDataBindingConverter()); + RegisterDataBindingProperty(size => size.Height, new FloatDataBindingConverter()); } /// diff --git a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs index 2d2a9231a..a355747ce 100644 --- a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs +++ b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs @@ -2,54 +2,58 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Reflection; using Artemis.Core.LayerBrushes; using Artemis.Core.LayerEffects; using Artemis.Core.Properties; -using Artemis.Core.Services; using Artemis.Storage.Entities.Profile; +using Humanizer; namespace Artemis.Core { public abstract class LayerPropertyGroup : IDisposable { - private readonly List _layerProperties; + private readonly List _layerProperties; private readonly List _layerPropertyGroups; - private ReadOnlyCollection _allLayerProperties; + private bool _disposed; private bool _isHidden; protected LayerPropertyGroup() { - _layerProperties = new List(); + _layerProperties = new List(); _layerPropertyGroups = new List(); } /// - /// Gets the profile element (such as layer or folder) this effect is applied to + /// Gets the description of this group + /// + public PropertyGroupDescriptionAttribute GroupDescription { get; internal set; } + + /// + /// Gets the info of the plugin this group is associated with + /// + public PluginInfo PluginInfo { get; internal set; } + + /// + /// Gets the profile element (such as layer or folder) this group is associated with /// public RenderProfileElement ProfileElement { get; internal set; } + /// + /// The parent group of this group + /// + public LayerPropertyGroup Parent { get; internal set; } + /// /// The path of this property group /// public string Path { get; internal set; } - /// - /// The parent group of this layer property group, set after construction - /// - public LayerPropertyGroup Parent { get; internal set; } - /// /// Gets whether this property groups properties are all initialized /// public bool PropertiesInitialized { get; private set; } - /// - /// Used to declare that this property group doesn't belong to a plugin and should use the core plugin GUID - /// - public bool IsCorePropertyGroup { get; internal set; } - - public PropertyGroupDescriptionAttribute GroupDescription { get; internal set; } - /// /// The layer brush this property group belongs to /// @@ -76,37 +80,45 @@ namespace Artemis.Core /// /// A list of all layer properties in this group /// - public ReadOnlyCollection LayerProperties => _layerProperties.AsReadOnly(); + public ReadOnlyCollection LayerProperties => _layerProperties.AsReadOnly(); /// /// A list of al child groups in this group /// public ReadOnlyCollection LayerPropertyGroups => _layerPropertyGroups.AsReadOnly(); + #region IDisposable + + /// public void Dispose() { + _disposed = true; DisableProperties(); + + foreach (var layerProperty in _layerProperties) + layerProperty.Dispose(); foreach (var layerPropertyGroup in _layerPropertyGroups) layerPropertyGroup.Dispose(); } + #endregion + /// /// Recursively gets all layer properties on this group and any subgroups /// - /// - public IReadOnlyCollection GetAllLayerProperties() + public IReadOnlyCollection GetAllLayerProperties() { - if (!PropertiesInitialized) - return new List(); - if (_allLayerProperties != null) - return _allLayerProperties; + if (_disposed) + throw new ObjectDisposedException("LayerPropertyGroup"); - var result = new List(LayerProperties); + if (!PropertiesInitialized) + return new List(); + + var result = new List(LayerProperties); foreach (var layerPropertyGroup in LayerPropertyGroups) result.AddRange(layerPropertyGroup.GetAllLayerProperties()); - _allLayerProperties = result.AsReadOnly(); - return _allLayerProperties; + return result.AsReadOnly(); } /// @@ -116,7 +128,7 @@ namespace Artemis.Core protected abstract void PopulateDefaults(); /// - /// Called when the property group is deactivated + /// Called when the property group is aactivated /// protected abstract void EnableProperties(); @@ -125,19 +137,26 @@ namespace Artemis.Core /// protected abstract void DisableProperties(); + /// + /// Called when the property group and all its layer properties have been initialized + /// protected virtual void OnPropertyGroupInitialized() { PropertyGroupInitialized?.Invoke(this, EventArgs.Empty); } - internal void InitializeProperties(IRenderElementService renderElementService, RenderProfileElement profileElement, [NotNull] string path) + internal void Initialize(RenderProfileElement profileElement, [NotNull] string path, PluginInfo pluginInfo) { if (path == null) throw new ArgumentNullException(nameof(path)); + if (pluginInfo == null) + throw new ArgumentNullException(nameof(pluginInfo)); + // Doubt this will happen but let's make sure if (PropertiesInitialized) throw new ArtemisCoreException("Layer property group already initialized, wut"); + PluginInfo = pluginInfo; ProfileElement = profileElement; Path = path.TrimEnd('.'); @@ -146,55 +165,21 @@ namespace Artemis.Core { var propertyDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyDescriptionAttribute)); if (propertyDescription != null) - { - if (!typeof(BaseLayerProperty).IsAssignableFrom(propertyInfo.PropertyType)) - throw new ArtemisPluginException($"Layer property with PropertyDescription attribute must be of type LayerProperty at {path + propertyInfo.Name}"); - - var instance = (BaseLayerProperty) Activator.CreateInstance(propertyInfo.PropertyType, true); - if (instance == null) - throw new ArtemisPluginException($"Failed to create instance of layer property at {path + propertyInfo.Name}"); - - instance.ProfileElement = profileElement; - instance.Parent = this; - instance.PropertyDescription = (PropertyDescriptionAttribute) propertyDescription; - if (instance.PropertyDescription.DisableKeyframes) - instance.KeyframesSupported = false; - - InitializeProperty(profileElement, path + propertyInfo.Name, instance); - - propertyInfo.SetValue(this, instance); - _layerProperties.Add(instance); - } + InitializeProperty(propertyInfo, (PropertyDescriptionAttribute) propertyDescription); else { var propertyGroupDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyGroupDescriptionAttribute)); if (propertyGroupDescription != null) - { - if (!typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType)) - throw new ArtemisPluginException("Layer property with PropertyGroupDescription attribute must be of type LayerPropertyGroup"); - - var instance = (LayerPropertyGroup) Activator.CreateInstance(propertyInfo.PropertyType); - if (instance == null) - throw new ArtemisPluginException($"Failed to create instance of layer property group at {path + propertyInfo.Name}"); - - instance.Parent = this; - instance.GroupDescription = (PropertyGroupDescriptionAttribute) propertyGroupDescription; - instance.LayerBrush = LayerBrush; - instance.LayerEffect = LayerEffect; - instance.InitializeProperties(renderElementService, profileElement, $"{path}{propertyInfo.Name}."); - - propertyInfo.SetValue(this, instance); - _layerPropertyGroups.Add(instance); - } + InitializeChildGroup(propertyInfo, (PropertyGroupDescriptionAttribute) propertyGroupDescription); } } // Request the property group to populate defaults PopulateDefaults(); - // Apply the newly populated defaults - foreach (var layerProperty in _layerProperties.Where(p => !p.IsLoadedFromStorage)) - layerProperty.ApplyDefaultValue(); + // Load the layer properties after defaults have been applied + foreach (var layerProperty in _layerProperties) + layerProperty.Load(); EnableProperties(); PropertiesInitialized = true; @@ -206,25 +191,11 @@ namespace Artemis.Core if (!PropertiesInitialized) return; - // Get all properties with a PropertyDescriptionAttribute - foreach (var propertyInfo in GetType().GetProperties()) - { - var propertyDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyDescriptionAttribute)); - if (propertyDescription != null) - { - var layerProperty = (BaseLayerProperty) propertyInfo.GetValue(this); - layerProperty.ApplyToEntity(); - } - else - { - var propertyGroupDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyGroupDescriptionAttribute)); - if (propertyGroupDescription != null) - { - var layerPropertyGroup = (LayerPropertyGroup) propertyInfo.GetValue(this); - layerPropertyGroup.ApplyToEntity(); - } - } - } + foreach (var layerProperty in LayerProperties) + layerProperty.Save(); + + foreach (var layerPropertyGroup in LayerPropertyGroups) + layerPropertyGroup.ApplyToEntity(); } internal void Update(double deltaTime) @@ -234,32 +205,63 @@ namespace Artemis.Core OnPropertyGroupUpdating(new LayerPropertyGroupUpdatingEventArgs(deltaTime)); } - private void InitializeProperty(RenderProfileElement profileElement, string path, BaseLayerProperty instance) + private void InitializeProperty(PropertyInfo propertyInfo, PropertyDescriptionAttribute propertyDescription) { - Guid pluginGuid; - if (IsCorePropertyGroup || instance.IsCoreProperty) - pluginGuid = Constants.CorePluginInfo.Guid; - else if (instance.Parent.LayerBrush != null) - pluginGuid = instance.Parent.LayerBrush.PluginInfo.Guid; - else - pluginGuid = instance.Parent.LayerEffect.PluginInfo.Guid; + var path = Path + "."; - var entity = profileElement.RenderElementEntity.PropertyEntities.FirstOrDefault(p => p.PluginGuid == pluginGuid && p.Path == path); - var fromStorage = true; + if (!typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType)) + throw new ArtemisPluginException($"Layer property with PropertyDescription attribute must be of type LayerProperty at {path + propertyInfo.Name}"); + + var instance = (ILayerProperty) Activator.CreateInstance(propertyInfo.PropertyType, true); + if (instance == null) + throw new ArtemisPluginException($"Failed to create instance of layer property at {path + propertyInfo.Name}"); + + // Ensure the description has a name, if not this is a good point to set it based on the property info + if (string.IsNullOrWhiteSpace(propertyDescription.Name)) + propertyDescription.Name = propertyInfo.Name.Humanize(); + + var entity = GetPropertyEntity(ProfileElement, path + propertyInfo.Name, out var fromStorage); + instance.Initialize(ProfileElement, this, entity, fromStorage, propertyDescription); + propertyInfo.SetValue(this, instance); + _layerProperties.Add(instance); + } + + private void InitializeChildGroup(PropertyInfo propertyInfo, PropertyGroupDescriptionAttribute propertyGroupDescription) + { + var path = Path + "."; + + if (!typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType)) + throw new ArtemisPluginException("Layer property with PropertyGroupDescription attribute must be of type LayerPropertyGroup"); + + var instance = (LayerPropertyGroup) Activator.CreateInstance(propertyInfo.PropertyType); + if (instance == null) + throw new ArtemisPluginException($"Failed to create instance of layer property group at {path + propertyInfo.Name}"); + + // Ensure the description has a name, if not this is a good point to set it based on the property info + if (string.IsNullOrWhiteSpace(propertyGroupDescription.Name)) + propertyGroupDescription.Name = propertyInfo.Name.Humanize(); + + instance.Parent = this; + instance.GroupDescription = propertyGroupDescription; + instance.LayerBrush = LayerBrush; + instance.LayerEffect = LayerEffect; + instance.Initialize(ProfileElement, $"{path}{propertyInfo.Name}.", PluginInfo); + + propertyInfo.SetValue(this, instance); + _layerPropertyGroups.Add(instance); + } + + private PropertyEntity GetPropertyEntity(RenderProfileElement profileElement, string path, out bool fromStorage) + { + var entity = profileElement.RenderElementEntity.PropertyEntities.FirstOrDefault(p => p.PluginGuid == PluginInfo.Guid && p.Path == path); + fromStorage = entity != null; if (entity == null) { - fromStorage = false; - entity = new PropertyEntity {PluginGuid = pluginGuid, Path = path}; + entity = new PropertyEntity {PluginGuid = PluginInfo.Guid, Path = path}; profileElement.RenderElementEntity.PropertyEntities.Add(entity); } - instance.ApplyToLayerProperty(entity, this, fromStorage); - instance.BaseValueChanged += InstanceOnBaseValueChanged; - } - - private void InstanceOnBaseValueChanged(object sender, EventArgs e) - { - OnLayerPropertyBaseValueChanged(new LayerPropertyEventArgs((BaseLayerProperty) sender)); + return entity; } #region Events @@ -282,17 +284,17 @@ namespace Artemis.Core /// public event EventHandler VisibilityChanged; - protected virtual void OnPropertyGroupUpdating(LayerPropertyGroupUpdatingEventArgs e) + internal virtual void OnPropertyGroupUpdating(LayerPropertyGroupUpdatingEventArgs e) { PropertyGroupUpdating?.Invoke(this, e); } - protected virtual void OnVisibilityChanged() + internal virtual void OnVisibilityChanged() { VisibilityChanged?.Invoke(this, EventArgs.Empty); } - protected virtual void OnLayerPropertyBaseValueChanged(LayerPropertyEventArgs e) + internal virtual void OnLayerPropertyBaseValueChanged(LayerPropertyEventArgs e) { LayerPropertyBaseValueChanged?.Invoke(this, e); } diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs index 030cf786f..583c10119 100644 --- a/src/Artemis.Core/Models/Profile/Profile.cs +++ b/src/Artemis.Core/Models/Profile/Profile.cs @@ -16,17 +16,19 @@ namespace Artemis.Core ProfileEntity = new ProfileEntity(); EntityId = Guid.NewGuid(); + Profile = this; Module = module; Name = name; UndoStack = new Stack(); RedoStack = new Stack(); - AddChild(new Folder(this, this, "Root folder")); - ApplyToEntity(); + var _ = new Folder(this, "Root folder"); + Save(); } internal Profile(ProfileModule module, ProfileEntity profileEntity) { + Profile = this; ProfileEntity = profileEntity; EntityId = profileEntity.Id; @@ -34,7 +36,7 @@ namespace Artemis.Core UndoStack = new Stack(); RedoStack = new Stack(); - ApplyToProfile(); + Load(); } public ProfileModule Module { get; } @@ -86,7 +88,7 @@ namespace Artemis.Core return (Folder) Children.Single(); } - public void ApplyToProfile() + internal override void Load() { if (_disposed) throw new ObjectDisposedException("Profile"); @@ -103,7 +105,9 @@ namespace Artemis.Core // Populate the profile starting at the root, the rest is populated recursively var rootFolder = ProfileEntity.Folders.FirstOrDefault(f => f.ParentId == EntityId); if (rootFolder == null) - AddChild(new Folder(this, this, "Root folder")); + { + var _ = new Folder(this, "Root folder"); + } else AddChild(new Folder(this, this, rootFolder)); } @@ -129,7 +133,7 @@ namespace Artemis.Core _disposed = true; } - internal override void ApplyToEntity() + internal override void Save() { if (_disposed) throw new ObjectDisposedException("Profile"); @@ -140,7 +144,7 @@ namespace Artemis.Core ProfileEntity.IsActive = IsActivated; foreach (var profileElement in Children) - profileElement.ApplyToEntity(); + profileElement.Save(); ProfileEntity.Folders.Clear(); ProfileEntity.Folders.AddRange(GetAllFolders().Select(f => f.FolderEntity)); diff --git a/src/Artemis.Core/Models/Profile/ProfileElement.cs b/src/Artemis.Core/Models/Profile/ProfileElement.cs index efa5bdb52..c91bcd2fe 100644 --- a/src/Artemis.Core/Models/Profile/ProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/ProfileElement.cs @@ -23,18 +23,27 @@ namespace Artemis.Core ChildrenList = new List(); } + /// + /// Gets the unique ID of this profile element + /// public Guid EntityId { get => _entityId; internal set => SetAndNotify(ref _entityId, value); } + /// + /// Gets the profile this element belongs to + /// public Profile Profile { get => _profile; internal set => SetAndNotify(ref _profile, value); } + /// + /// Gets the parent of this element + /// public ProfileElement Parent { get => _parent; @@ -73,12 +82,6 @@ namespace Artemis.Core set => SetAndNotify(ref _enabled, value); } - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - /// /// Updates the element /// @@ -90,39 +93,13 @@ namespace Artemis.Core /// public abstract void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo); - public List GetAllFolders() + /// + public override string ToString() { - if (_disposed) - throw new ObjectDisposedException(GetType().Name); - - var folders = new List(); - foreach (var childFolder in Children.Where(c => c is Folder).Cast()) - { - // Add all folders in this element - folders.Add(childFolder); - // Add all folders in folders inside this element - folders.AddRange(childFolder.GetAllFolders()); - } - - return folders; + return $"{nameof(EntityId)}: {EntityId}, {nameof(Order)}: {Order}, {nameof(Name)}: {Name}"; } - public List GetAllLayers() - { - if (_disposed) - throw new ObjectDisposedException(GetType().Name); - - var layers = new List(); - - // Add all layers in this element - layers.AddRange(Children.Where(c => c is Layer).Cast()); - - // Add all layers in folders inside this element - foreach (var childFolder in Children.Where(c => c is Folder).Cast()) - layers.AddRange(childFolder.GetAllLayers()); - - return layers; - } + #region Hierarchy /// /// Adds a profile element to the collection, optionally at the given position (1-based) @@ -133,9 +110,12 @@ namespace Artemis.Core { if (_disposed) throw new ObjectDisposedException(GetType().Name); - + lock (ChildrenList) { + if (ChildrenList.Contains(child)) + return; + // Add to the end of the list if (order == null) { @@ -189,9 +169,64 @@ namespace Artemis.Core OnChildRemoved(); } - public override string ToString() + /// + /// Returns a flattened list of all child folders + /// + /// + public List GetAllFolders() { - return $"{nameof(EntityId)}: {EntityId}, {nameof(Order)}: {Order}, {nameof(Name)}: {Name}"; + if (_disposed) + throw new ObjectDisposedException(GetType().Name); + + var folders = new List(); + foreach (var childFolder in Children.Where(c => c is Folder).Cast()) + { + // Add all folders in this element + folders.Add(childFolder); + // Add all folders in folders inside this element + folders.AddRange(childFolder.GetAllFolders()); + } + + return folders; + } + + /// + /// Returns a flattened list of all child layers + /// + /// + public List GetAllLayers() + { + if (_disposed) + throw new ObjectDisposedException(GetType().Name); + + var layers = new List(); + + // Add all layers in this element + layers.AddRange(Children.Where(c => c is Layer).Cast()); + + // Add all layers in folders inside this element + foreach (var childFolder in Children.Where(c => c is Folder).Cast()) + layers.AddRange(childFolder.GetAllLayers()); + + return layers; + } + + #endregion + + #region Storage + + internal abstract void Load(); + internal abstract void Save(); + + #endregion + + #region IDisposable + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) @@ -201,10 +236,7 @@ namespace Artemis.Core } } - /// - /// Applies the profile element's properties to the underlying storage entity - /// - internal abstract void ApplyToEntity(); + #endregion #region Events diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index 34aac49d9..e225f6b57 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Artemis.Core.LayerEffects; +using Artemis.Core.LayerEffects.Placeholder; using Artemis.Core.Properties; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.Abstract; @@ -12,36 +13,33 @@ namespace Artemis.Core { public abstract class RenderProfileElement : ProfileElement { - /// - /// Returns a list of all keyframes on all properties and effects of this layer - /// - public virtual List GetAllKeyframes() + protected RenderProfileElement() { - var keyframes = new List(); - foreach (var layerEffect in LayerEffects) - { - foreach (var baseLayerProperty in layerEffect.BaseProperties.GetAllLayerProperties()) - keyframes.AddRange(baseLayerProperty.BaseKeyframes); - } - - return keyframes; + LayerEffectStore.LayerEffectAdded += LayerEffectStoreOnLayerEffectAdded; + LayerEffectStore.LayerEffectRemoved += LayerEffectStoreOnLayerEffectRemoved; } - protected void ApplyRenderElementDefaults() + internal void ApplyRenderElementDefaults() { MainSegmentLength = TimeSpan.FromSeconds(5); } - protected void ApplyRenderElementEntity() + internal void LoadRenderElement() { StartSegmentLength = RenderElementEntity.StartSegmentLength; MainSegmentLength = RenderElementEntity.MainSegmentLength; EndSegmentLength = RenderElementEntity.EndSegmentLength; DisplayContinuously = RenderElementEntity.DisplayContinuously; AlwaysFinishTimeline = RenderElementEntity.AlwaysFinishTimeline; + + DisplayConditionGroup = RenderElementEntity.RootDisplayCondition != null + ? new DisplayConditionGroup(null, RenderElementEntity.RootDisplayCondition) + : new DisplayConditionGroup(null); + + ActivateEffects(); } - protected void ApplyRenderElementToEntity() + internal void SaveRenderElement() { RenderElementEntity.StartSegmentLength = StartSegmentLength; RenderElementEntity.MainSegmentLength = MainSegmentLength; @@ -55,8 +53,8 @@ namespace Artemis.Core var layerEffectEntity = new LayerEffectEntity { Id = layerEffect.EntityId, - PluginGuid = layerEffect.PluginInfo.Guid, - EffectType = layerEffect.GetType().Name, + PluginGuid = layerEffect.Descriptor.PlaceholderFor ?? layerEffect.PluginInfo.Guid, + EffectType = layerEffect.GetEffectTypeName(), Name = layerEffect.Name, Enabled = layerEffect.Enabled, HasBeenRenamed = layerEffect.HasBeenRenamed, @@ -65,6 +63,10 @@ namespace Artemis.Core RenderElementEntity.LayerEffects.Add(layerEffectEntity); layerEffect.BaseProperties.ApplyToEntity(); } + + // Conditions + RenderElementEntity.RootDisplayCondition = DisplayConditionGroup?.Entity; + DisplayConditionGroup?.Save(); } #region Properties @@ -211,7 +213,6 @@ namespace Artemis.Core return (TimelinePosition - oldPosition).TotalSeconds; } - /// /// Overrides the progress of the element /// @@ -221,7 +222,7 @@ namespace Artemis.Core #endregion - #region Effects + #region Effect management protected List _layerEffects; @@ -230,13 +231,45 @@ namespace Artemis.Core /// public ReadOnlyCollection LayerEffects => _layerEffects.AsReadOnly(); - internal void RemoveLayerEffect([NotNull] BaseLayerEffect effect) + /// + /// Adds a the layer effect described inthe provided + /// + public void AddLayerEffect(LayerEffectDescriptor descriptor) + { + if (descriptor == null) + throw new ArgumentNullException(nameof(descriptor)); + + var entity = new LayerEffectEntity + { + Id = Guid.NewGuid(), + Enabled = true, + Order = LayerEffects.Count + 1 + }; + descriptor.CreateInstance(this, entity); + + OrderEffects(); + OnLayerEffectsUpdated(); + } + + /// + /// Removes the provided layer + /// + /// + public void RemoveLayerEffect([NotNull] BaseLayerEffect effect) { if (effect == null) throw new ArgumentNullException(nameof(effect)); - DeactivateLayerEffect(effect); + // Remove the effect from the layer and dispose it + _layerEffects.Remove(effect); + effect.Dispose(); // Update the order on the remaining effects + OrderEffects(); + OnLayerEffectsUpdated(); + } + + private void OrderEffects() + { var index = 0; foreach (var baseLayerEffect in LayerEffects.OrderBy(e => e.Order)) { @@ -244,23 +277,69 @@ namespace Artemis.Core index++; } + _layerEffects.Sort((a, b) => a.Order.CompareTo(b.Order)); + } + + internal void ActivateEffects() + { + foreach (var layerEffectEntity in RenderElementEntity.LayerEffects) + { + // If there is a non-placeholder existing effect, skip this entity + var existing = _layerEffects.FirstOrDefault(e => e.EntityId == layerEffectEntity.Id); + if (existing != null && existing.Descriptor.PlaceholderFor == null) + continue; + + var descriptor = LayerEffectStore.Get(layerEffectEntity.PluginGuid, layerEffectEntity.EffectType)?.LayerEffectDescriptor; + if (descriptor != null) + { + // If a descriptor is found but there is an existing placeholder, remove the placeholder + if (existing != null) + { + _layerEffects.Remove(existing); + existing.Dispose(); + } + + // Create an instance with the descriptor + descriptor.CreateInstance(this, layerEffectEntity); + } + else if (existing == null) + { + // If no descriptor was found and there was no existing placeholder, create a placeholder + descriptor = PlaceholderLayerEffectDescriptor.Create(layerEffectEntity.PluginGuid); + descriptor.CreateInstance(this, layerEffectEntity); + } + } + + OrderEffects(); + } + + + internal void ActivateLayerEffect(BaseLayerEffect layerEffect) + { + _layerEffects.Add(layerEffect); OnLayerEffectsUpdated(); } - internal void AddLayerEffect([NotNull] BaseLayerEffect effect) + private void LayerEffectStoreOnLayerEffectRemoved(object sender, LayerEffectStoreEvent e) { - if (effect == null) throw new ArgumentNullException(nameof(effect)); - _layerEffects.Add(effect); - OnLayerEffectsUpdated(); + // If effects provided by the plugin are on the element, replace them with placeholders + var pluginEffects = _layerEffects.Where(ef => ef.Descriptor.LayerEffectProvider != null && + ef.PluginInfo.Guid == e.Registration.Plugin.PluginInfo.Guid).ToList(); + foreach (var pluginEffect in pluginEffects) + { + var entity = RenderElementEntity.LayerEffects.First(en => en.Id == pluginEffect.EntityId); + _layerEffects.Remove(pluginEffect); + pluginEffect.Dispose(); + + var descriptor = PlaceholderLayerEffectDescriptor.Create(pluginEffect.PluginInfo.Guid); + descriptor.CreateInstance(this, entity); + } } - internal void DeactivateLayerEffect([NotNull] BaseLayerEffect effect) + private void LayerEffectStoreOnLayerEffectAdded(object sender, LayerEffectStoreEvent e) { - if (effect == null) throw new ArgumentNullException(nameof(effect)); - - // Remove the effect from the layer and dispose it - _layerEffects.Remove(effect); - effect.Dispose(); + if (RenderElementEntity.LayerEffects.Any(ef => ef.PluginGuid == e.Registration.Plugin.PluginInfo.Guid)) + ActivateEffects(); } #endregion @@ -301,6 +380,21 @@ namespace Artemis.Core #endregion + #region IDisposable + + protected override void Dispose(bool disposing) + { + LayerEffectStore.LayerEffectAdded -= LayerEffectStoreOnLayerEffectAdded; + LayerEffectStore.LayerEffectRemoved -= LayerEffectStoreOnLayerEffectRemoved; + + foreach (var baseLayerEffect in LayerEffects) + baseLayerEffect.Dispose(); + + base.Dispose(disposing); + } + + #endregion + #region Events public event EventHandler LayerEffectsUpdated; @@ -311,17 +405,5 @@ namespace Artemis.Core } #endregion - - /// - /// Returns all the layer properties of this profile element - /// - public virtual List GetAllLayerProperties() - { - var result = new List(); - foreach (var baseLayerEffect in LayerEffects) - result.AddRange(baseLayerEffect.BaseProperties.GetAllLayerProperties()); - - return result; - } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs b/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs index 9edd2a6cc..e0fbc4f35 100644 --- a/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs +++ b/src/Artemis.Core/Plugins/DataModelExpansions/DataModel.cs @@ -22,6 +22,12 @@ namespace Artemis.Core.DataModelExpansions [DataModelIgnore] public DataModelPropertyAttribute DataModelDescription { get; internal set; } + /// + /// Gets the is expansion status indicating whether this data model expands the main data model + /// + [DataModelIgnore] + public bool IsExpansion { get; internal set; } + public bool ContainsPath(string path) { var parts = path.Split('.'); diff --git a/src/Artemis.Core/Plugins/DataModelExpansions/DataModelExpansion.cs b/src/Artemis.Core/Plugins/DataModelExpansions/DataModelExpansion.cs index 8230e08f8..28946a89b 100644 --- a/src/Artemis.Core/Plugins/DataModelExpansions/DataModelExpansion.cs +++ b/src/Artemis.Core/Plugins/DataModelExpansions/DataModelExpansion.cs @@ -10,7 +10,8 @@ namespace Artemis.Core.DataModelExpansions public abstract class DataModelExpansion : BaseDataModelExpansion where T : DataModel { /// - /// The data model driving this module + /// The main data model of this data model expansion + /// Note: This default data model is automatically registered upon plugin enable /// public T DataModel { diff --git a/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs index 117a7656e..910432692 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs @@ -105,7 +105,7 @@ namespace Artemis.Core.LayerBrushes // Not only is this needed to initialize properties on the layer brushes, it also prevents implementing anything // but LayerBrush and RgbNetLayerBrush outside the core - internal abstract void Initialize(IRenderElementService renderElementService); + internal abstract void Initialize(); internal abstract void InternalRender(SKCanvas canvas, SKImageInfo canvasInfo, SKPath path, SKPaint paint); diff --git a/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs index 52a2de9d6..88c2d3421 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs @@ -1,5 +1,4 @@ using System; -using Artemis.Core.Services; namespace Artemis.Core.LayerBrushes { @@ -33,11 +32,12 @@ namespace Artemis.Core.LayerBrushes internal set => _properties = value; } - internal void InitializeProperties(IRenderElementService renderElementService) + internal void InitializeProperties() { Properties = Activator.CreateInstance(); + Properties.GroupDescription ??= new PropertyGroupDescriptionAttribute {Name = Descriptor.DisplayName, Description = Descriptor.Description}; Properties.LayerBrush = this; - Properties.InitializeProperties(renderElementService, Layer, "LayerBrush."); + Properties.Initialize(Layer, "LayerBrush.", PluginInfo); PropertiesInitialized = true; EnableLayerBrush(); diff --git a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs index f1e75b77f..7ae5e268a 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs @@ -26,9 +26,9 @@ namespace Artemis.Core.LayerBrushes Render(canvas, canvasInfo, path, paint); } - internal override void Initialize(IRenderElementService renderElementService) + internal override void Initialize() { - InitializeProperties(renderElementService); + InitializeProperties(); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs index 685435646..c03948109 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs @@ -1,4 +1,6 @@ using System; +using Artemis.Core.Services; +using Ninject; namespace Artemis.Core.LayerBrushes { @@ -41,5 +43,30 @@ namespace Artemis.Core.LayerBrushes /// The plugin that provided this /// public LayerBrushProvider LayerBrushProvider { get; } + + /// + /// Creates an instance of the described brush and applies it to the layer + /// + internal void CreateInstance(Layer layer) + { + if (layer.LayerBrush != null) + throw new ArtemisCoreException("Layer already has an instantiated layer brush"); + + var brush = (BaseLayerBrush) CoreService.Kernel.Get(LayerBrushType); + brush.Layer = layer; + brush.Descriptor = this; + brush.Initialize(); + brush.Update(0); + + layer.LayerBrush = brush; + layer.OnLayerBrushUpdated(); + } + + public bool MatchesLayerBrushReference(LayerBrushReference reference) + { + if (reference == null) + return false; + return LayerBrushProvider.PluginInfo.Guid == reference.BrushPluginGuid && LayerBrushType.Name == reference.BrushType; + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushProvider.cs b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushProvider.cs index 8470cc57e..3766edb13 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushProvider.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushProvider.cs @@ -41,11 +41,14 @@ namespace Artemis.Core.LayerBrushes if (!Enabled) throw new ArtemisPluginException(PluginInfo, "Can only add a layer brush descriptor when the plugin is enabled"); - _layerBrushDescriptors.Add(new LayerBrushDescriptor(displayName, description, icon, typeof(T), this)); + var descriptor = new LayerBrushDescriptor(displayName, description, icon, typeof(T), this); + _layerBrushDescriptors.Add(descriptor); + LayerBrushStore.Add(descriptor); } private void OnPluginDisabled(object sender, EventArgs e) { + // The store will clean up the registrations by itself, the plugin just needs to clear its own list _layerBrushDescriptors.Clear(); } } diff --git a/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs index 13972ac87..8ad27213a 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs @@ -70,9 +70,9 @@ namespace Artemis.Core.LayerBrushes } } - internal override void Initialize(IRenderElementService renderElementService) + internal override void Initialize() { - InitializeProperties(renderElementService); + InitializeProperties(); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/RgbNetLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/RgbNetLayerBrush.cs index 1f270cf8e..395486165 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/RgbNetLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/RgbNetLayerBrush.cs @@ -46,12 +46,12 @@ namespace Artemis.Core.LayerBrushes LedGroup.Brush = GetBrush(); } - internal override void Initialize(IRenderElementService renderElementService) + internal override void Initialize() { LedGroup = new ListLedGroup(); Layer.RenderPropertiesUpdated += LayerOnRenderPropertiesUpdated; - InitializeProperties(renderElementService); + InitializeProperties(); UpdateLedGroup(); } diff --git a/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs b/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs index 06b63c649..e4ec96c9e 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs @@ -95,7 +95,7 @@ namespace Artemis.Core.LayerEffects /// /// Gets the plugin info that defined this effect /// - public PluginInfo PluginInfo => Descriptor.LayerEffectProvider.PluginInfo; + public PluginInfo PluginInfo => Descriptor.LayerEffectProvider?.PluginInfo; /// /// Gets a reference to the layer property group without knowing it's type @@ -108,7 +108,7 @@ namespace Artemis.Core.LayerEffects public void Dispose() { DisableLayerEffect(); - BaseProperties.Dispose(); + BaseProperties?.Dispose(); } /// @@ -147,6 +147,8 @@ namespace Artemis.Core.LayerEffects // Not only is this needed to initialize properties on the layer effects, it also prevents implementing anything // but LayerEffect outside the core - internal abstract void Initialize(IRenderElementService renderElementService); + internal abstract void Initialize(); + + internal virtual string GetEffectTypeName() => GetType().Name; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerEffects/LayerEffect.cs b/src/Artemis.Core/Plugins/LayerEffects/LayerEffect.cs index 57db9e522..ff7c6adec 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/LayerEffect.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/LayerEffect.cs @@ -30,19 +30,19 @@ namespace Artemis.Core.LayerEffects internal set => _properties = value; } - internal void InitializeProperties(IRenderElementService renderElementService) + internal void InitializeProperties() { Properties = Activator.CreateInstance(); Properties.LayerEffect = this; - Properties.InitializeProperties(renderElementService, ProfileElement, PropertyRootPath); + Properties.Initialize(ProfileElement, PropertyRootPath, PluginInfo); PropertiesInitialized = true; EnableLayerEffect(); } - internal override void Initialize(IRenderElementService renderElementService) + internal override void Initialize() { - InitializeProperties(renderElementService); + InitializeProperties(); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs b/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs index b3c1db3b5..50ffaa310 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs @@ -1,4 +1,9 @@ using System; +using System.Linq; +using Artemis.Core.LayerEffects.Placeholder; +using Artemis.Core.Services; +using Artemis.Storage.Entities.Profile; +using Ninject; namespace Artemis.Core.LayerEffects { @@ -41,5 +46,46 @@ namespace Artemis.Core.LayerEffects /// The plugin that provided this /// public LayerEffectProvider LayerEffectProvider { get; } + + /// + /// Gets the GUID this descriptor is acting as a placeholder for. If null, this descriptor is not a placeholder + /// + public Guid? PlaceholderFor { get; internal set; } + + /// + /// Creates an instance of the described effect and applies it to the render element + /// + internal void CreateInstance(RenderProfileElement renderElement, LayerEffectEntity entity) + { + // Skip effects already on the element + if (renderElement.LayerEffects.Any(e => e.EntityId == entity.Id)) + return; + + if (PlaceholderFor != null) + { + CreatePlaceHolderInstance(renderElement, entity); + return; + } + + var effect = (BaseLayerEffect)CoreService.Kernel.Get(LayerEffectType); + effect.ProfileElement = renderElement; + effect.EntityId = entity.Id; + effect.Order = entity.Order; + effect.Name = entity.Name; + effect.Enabled = entity.Enabled; + effect.Descriptor = this; + + effect.Initialize(); + effect.Update(0); + + renderElement.ActivateLayerEffect(effect); + } + + private void CreatePlaceHolderInstance(RenderProfileElement renderElement, LayerEffectEntity entity) + { + var effect = new PlaceholderLayerEffect(entity, PlaceholderFor.Value) {ProfileElement = renderElement, Descriptor = this}; + effect.Initialize(); + renderElement.ActivateLayerEffect(effect); + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerEffects/LayerEffectProvider.cs b/src/Artemis.Core/Plugins/LayerEffects/LayerEffectProvider.cs index e28716ce9..91f86414c 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/LayerEffectProvider.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/LayerEffectProvider.cs @@ -41,11 +41,14 @@ namespace Artemis.Core.LayerEffects if (!Enabled) throw new ArtemisPluginException(PluginInfo, "Can only add a layer effect descriptor when the plugin is enabled"); - _layerEffectDescriptors.Add(new LayerEffectDescriptor(displayName, description, icon, typeof(T), this)); + var descriptor = new LayerEffectDescriptor(displayName, description, icon, typeof(T), this); + _layerEffectDescriptors.Add(descriptor); + LayerEffectStore.Add(descriptor); } private void OnPluginDisabled(object sender, EventArgs e) { + // The store will clean up the registrations by itself, the plugin just needs to clear its own list _layerEffectDescriptors.Clear(); } } diff --git a/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffect.cs b/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffect.cs new file mode 100644 index 000000000..995b244e9 --- /dev/null +++ b/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffect.cs @@ -0,0 +1,75 @@ +using System; +using Artemis.Storage.Entities.Profile; +using SkiaSharp; + +namespace Artemis.Core.LayerEffects.Placeholder +{ + /// + /// Represents a layer effect that could not be loaded due to a missing plugin + /// + internal class PlaceholderLayerEffect : LayerEffect + { + internal PlaceholderLayerEffect(LayerEffectEntity originalEntity, Guid placeholderFor) + { + OriginalEntity = originalEntity; + PlaceholderFor = placeholderFor; + + EntityId = OriginalEntity.Id; + Order = OriginalEntity.Order; + Name = OriginalEntity.Name; + Enabled = OriginalEntity.Enabled; + HasBeenRenamed = OriginalEntity.HasBeenRenamed; + } + + internal LayerEffectEntity OriginalEntity { get; } + public Guid PlaceholderFor { get; } + + /// + public override void EnableLayerEffect() + { + } + + /// + public override void DisableLayerEffect() + { + } + + /// + public override void Update(double deltaTime) + { + } + + /// + public override void PreProcess(SKCanvas canvas, SKImageInfo canvasInfo, SKPath renderBounds, SKPaint paint) + { + } + + /// + public override void PostProcess(SKCanvas canvas, SKImageInfo canvasInfo, SKPath renderBounds, SKPaint paint) + { + } + + internal override string GetEffectTypeName() + { + return OriginalEntity.EffectType; + } + } + + /// + /// This is in place so that the UI has something to show + /// + internal class PlaceholderProperties : LayerPropertyGroup + { + protected override void PopulateDefaults() + { + } + + protected override void EnableProperties() + { + } + + protected override void DisableProperties() + { + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffectDescriptor.cs b/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffectDescriptor.cs new file mode 100644 index 000000000..63e738c37 --- /dev/null +++ b/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffectDescriptor.cs @@ -0,0 +1,17 @@ +using System; + +namespace Artemis.Core.LayerEffects.Placeholder +{ + internal static class PlaceholderLayerEffectDescriptor + { + public static LayerEffectDescriptor Create(Guid missingPluginGuid) + { + var descriptor = new LayerEffectDescriptor("Missing effect", "This effect could not be loaded", "FileQuestion", null, Constants.EffectPlaceholderPlugin) + { + PlaceholderFor = missingPluginGuid + }; + + return descriptor; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Modules/Module.cs b/src/Artemis.Core/Plugins/Modules/Module.cs index 31efee688..e67eb1174 100644 --- a/src/Artemis.Core/Plugins/Modules/Module.cs +++ b/src/Artemis.Core/Plugins/Modules/Module.cs @@ -14,6 +14,7 @@ namespace Artemis.Core.Modules { /// /// The data model driving this module + /// Note: This default data model is automatically registered upon plugin enable /// public T DataModel { diff --git a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs b/src/Artemis.Core/Plugins/Modules/ProfileModule.cs index 9b7c303ff..86afbcc0a 100644 --- a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs +++ b/src/Artemis.Core/Plugins/Modules/ProfileModule.cs @@ -18,6 +18,7 @@ namespace Artemis.Core.Modules { /// /// The data model driving this module + /// Note: This default data model is automatically registered upon plugin enable /// public T DataModel { @@ -118,7 +119,7 @@ namespace Artemis.Core.Modules /// Indicates whether or not a profile change is being animated /// public bool AnimatingProfileChange { get; private set; } - + /// /// Called after the profile has updated /// diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs index d8f260b1b..dd6c0542f 100644 --- a/src/Artemis.Core/Plugins/Plugin.cs +++ b/src/Artemis.Core/Plugins/Plugin.cs @@ -16,7 +16,7 @@ namespace Artemis.Core /// /// Gets whether the plugin is enabled /// - public bool Enabled { get; private set; } + public bool Enabled { get; internal set; } /// /// Gets or sets a configuration dialog for this plugin that is accessible in the UI under Settings > Plugins diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index 1c739c425..e981a567e 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -9,6 +9,7 @@ using Artemis.Core.JsonConverters; using Artemis.Core.Ninject; using Artemis.Storage; using Newtonsoft.Json; +using Ninject; using RGB.NET.Core; using Serilog; using Serilog.Events; @@ -22,6 +23,8 @@ namespace Artemis.Core.Services /// internal class CoreService : ICoreService { + internal static IKernel Kernel; + private readonly Stopwatch _frameStopWatch; private readonly ILogger _logger; private readonly PluginSetting _loggingLevel; @@ -34,9 +37,10 @@ namespace Artemis.Core.Services private List _modules; // ReSharper disable once UnusedParameter.Local - Storage migration service is injected early to ensure it runs before anything else - public CoreService(ILogger logger, StorageMigrationService _, ISettingsService settingsService, IPluginService pluginService, + public CoreService(IKernel kernel, ILogger logger, StorageMigrationService _, ISettingsService settingsService, IPluginService pluginService, IRgbService rgbService, ISurfaceService surfaceService, IProfileService profileService, IModuleService moduleService) { + Kernel = kernel; _logger = logger; _pluginService = pluginService; _rgbService = rgbService; @@ -77,6 +81,8 @@ namespace Artemis.Core.Services _logger.Information("Initializing Artemis Core version {version}", versionAttribute?.InformationalVersion); ApplyLoggingLevel(); + DeserializationLogger.Initialize(Kernel); + // Initialize the services _pluginService.CopyBuiltInPlugins(); _pluginService.LoadPlugins(StartupArguments.Contains("--ignore-plugin-lock")); diff --git a/src/Artemis.Core/Services/DataBindingService.cs b/src/Artemis.Core/Services/DataBindingService.cs deleted file mode 100644 index 199408fa2..000000000 --- a/src/Artemis.Core/Services/DataBindingService.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using Serilog; - -namespace Artemis.Core.Services -{ - internal class DataBindingService : IDataBindingService - { - private readonly ILogger _logger; - private readonly List _registeredDataBindingModifierTypes; - - public DataBindingService(ILogger logger) - { - _logger = logger; - _registeredDataBindingModifierTypes = new List(); - - RegisterBuiltInModifiers(); - } - - public IReadOnlyCollection RegisteredDataBindingModifierTypes - { - get - { - lock (_registeredDataBindingModifierTypes) - { - return _registeredDataBindingModifierTypes.AsReadOnly(); - } - } - } - - public void RegisterModifierType(PluginInfo pluginInfo, DataBindingModifierType dataBindingModifierType) - { - if (pluginInfo == null) - throw new ArgumentNullException(nameof(pluginInfo)); - if (dataBindingModifierType == null) - throw new ArgumentNullException(nameof(dataBindingModifierType)); - - lock (_registeredDataBindingModifierTypes) - { - if (_registeredDataBindingModifierTypes.Contains(dataBindingModifierType)) - return; - - dataBindingModifierType.Register(pluginInfo, this); - _registeredDataBindingModifierTypes.Add(dataBindingModifierType); - } - } - - public void RemoveModifierType(DataBindingModifierType dataBindingModifierType) - { - if (dataBindingModifierType == null) - throw new ArgumentNullException(nameof(dataBindingModifierType)); - - lock (_registeredDataBindingModifierTypes) - { - if (!_registeredDataBindingModifierTypes.Contains(dataBindingModifierType)) - return; - - dataBindingModifierType.Unsubscribe(); - _registeredDataBindingModifierTypes.Remove(dataBindingModifierType); - } - } - - public List GetCompatibleModifierTypes(Type type) - { - lock (_registeredDataBindingModifierTypes) - { - if (type == null) - return new List(_registeredDataBindingModifierTypes); - - var candidates = _registeredDataBindingModifierTypes.Where(c => c.CompatibleTypes.Any(t => t.IsCastableFrom(type))).ToList(); - - // If there are multiple modifier types with the same description, use the closest match - foreach (var dataBindingModifierTypes in candidates.GroupBy(c => c.Description).Where(g => g.Count() > 1).ToList()) - { - var bestCandidate = dataBindingModifierTypes.OrderByDescending(c => c.CompatibleTypes.Contains(type)).FirstOrDefault(); - foreach (var dataBindingModifierType in dataBindingModifierTypes) - { - if (dataBindingModifierType != bestCandidate) - candidates.Remove(dataBindingModifierType); - } - } - - return candidates; - } - } - - public DataBindingModifierType GetModifierType(Guid modifierTypePluginGuid, string modifierType) - { - return RegisteredDataBindingModifierTypes.FirstOrDefault(o => o.PluginInfo.Guid == modifierTypePluginGuid && o.GetType().Name == modifierType); - } - - public void LogModifierDeserializationFailure(DataBindingModifier dataBindingModifier, JsonSerializationException exception) - { - _logger.Warning( - exception, - "Failed to deserialize static parameter for operator {order}. {operatorType}", - dataBindingModifier.Entity.Order, - dataBindingModifier.Entity.ModifierType - ); - } - - private void RegisterBuiltInModifiers() - { - RegisterModifierType(Constants.CorePluginInfo, new MultiplicationModifierType()); - RegisterModifierType(Constants.CorePluginInfo, new DivideModifierType()); - RegisterModifierType(Constants.CorePluginInfo, new FloorModifierType()); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Services/DataModelService.cs b/src/Artemis.Core/Services/DataModelService.cs deleted file mode 100644 index 8d5d8956d..000000000 --- a/src/Artemis.Core/Services/DataModelService.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Artemis.Core.DataModelExpansions; -using Artemis.Core.Modules; -using Newtonsoft.Json; -using Serilog; - -namespace Artemis.Core.Services -{ - /// - /// Provides access to the main data model - /// - internal class DataModelService : IDataModelService - { - private readonly List _dataModelExpansions; - private readonly ILogger _logger; - private readonly IPluginService _pluginService; - private readonly List _registeredConditionOperators; - - public DataModelService(IPluginService pluginService, ILogger logger) - { - _pluginService = pluginService; - _logger = logger; - _dataModelExpansions = new List(); - _registeredConditionOperators = new List(); - - _pluginService.PluginEnabled += PluginServiceOnPluginEnabled; - _pluginService.PluginDisabled += PluginServiceOnPluginDisabled; - - RegisterBuiltInConditionOperators(); - - foreach (var module in _pluginService.GetPluginsOfType().Where(m => m.InternalExpandsMainDataModel)) - AddModuleDataModel(module); - foreach (var dataModelExpansion in _pluginService.GetPluginsOfType()) - AddDataModelExpansionDataModel(dataModelExpansion); - } - - public IReadOnlyCollection RegisteredConditionOperators - { - get - { - lock (_registeredConditionOperators) - { - return _registeredConditionOperators.AsReadOnly(); - } - } - } - - public IReadOnlyCollection DataModelExpansions - { - get - { - lock (_dataModelExpansions) - { - return new List(_dataModelExpansions).AsReadOnly(); - } - } - } - - public void AddExpansion(DataModel dataModelExpansion) - { - lock (_dataModelExpansions) - { - _dataModelExpansions.Add(dataModelExpansion); - // TODO SpoinkyNL 3-3-2018: Initialize the expansion and fire an event - } - } - - public void RemoveExpansion(DataModel dataModelExpansion) - { - lock (_dataModelExpansions) - { - if (!_dataModelExpansions.Contains(dataModelExpansion)) - throw new ArtemisCoreException("Cannot remove a data model expansion that wasn't previously added."); - - // TODO SpoinkyNL 3-3-2018: Dispose the expansion and fire an event - _dataModelExpansions.Remove(dataModelExpansion); - } - } - - public DataModel GetPluginDataModel(Plugin plugin) - { - if (plugin is Module module) - return module.InternalDataModel; - if (plugin is BaseDataModelExpansion dataModelExpansion) - return dataModelExpansion.InternalDataModel; - return null; - } - - public DataModel GetPluginDataModelByGuid(Guid pluginGuid) - { - var pluginInfo = _pluginService.GetAllPluginInfo().FirstOrDefault(i => i.Guid == pluginGuid); - if (pluginInfo == null || !pluginInfo.Enabled) - return null; - - return GetPluginDataModel(pluginInfo.Instance); - } - - public bool GetPluginExtendsDataModel(Plugin plugin) - { - if (plugin is Module module) - return module.InternalExpandsMainDataModel; - if (plugin is BaseDataModelExpansion) - return true; - - return false; - } - - - public void RegisterConditionOperator(PluginInfo pluginInfo, DisplayConditionOperator displayConditionOperator) - { - if (pluginInfo == null) - throw new ArgumentNullException(nameof(pluginInfo)); - if (displayConditionOperator == null) - throw new ArgumentNullException(nameof(displayConditionOperator)); - - lock (_registeredConditionOperators) - { - if (_registeredConditionOperators.Contains(displayConditionOperator)) - return; - - displayConditionOperator.Register(pluginInfo, this); - _registeredConditionOperators.Add(displayConditionOperator); - } - } - - public void RemoveConditionOperator(DisplayConditionOperator displayConditionOperator) - { - if (displayConditionOperator == null) - throw new ArgumentNullException(nameof(displayConditionOperator)); - - lock (_registeredConditionOperators) - { - if (!_registeredConditionOperators.Contains(displayConditionOperator)) - return; - - displayConditionOperator.Unsubscribe(); - _registeredConditionOperators.Remove(displayConditionOperator); - } - } - - public List GetCompatibleConditionOperators(Type type) - { - lock (_registeredConditionOperators) - { - if (type == null) - return new List(_registeredConditionOperators); - - var candidates = _registeredConditionOperators.Where(c => c.CompatibleTypes.Any(t => t.IsCastableFrom(type))).ToList(); - - // If there are multiple operators with the same description, use the closest match - foreach (var displayConditionOperators in candidates.GroupBy(c => c.Description).Where(g => g.Count() > 1).ToList()) - { - var bestCandidate = displayConditionOperators.OrderByDescending(c => c.CompatibleTypes.Contains(type)).FirstOrDefault(); - foreach (var displayConditionOperator in displayConditionOperators) - { - if (displayConditionOperator != bestCandidate) - candidates.Remove(displayConditionOperator); - } - } - - return candidates; - } - } - - public DisplayConditionOperator GetConditionOperator(Guid operatorPluginGuid, string operatorType) - { - return RegisteredConditionOperators.FirstOrDefault(o => o.PluginInfo.Guid == operatorPluginGuid && o.GetType().Name == operatorType); - } - - public void LogPredicateDeserializationFailure(DisplayConditionPredicate displayConditionPredicate, JsonException exception) - { - _logger.Warning( - exception, - "Failed to deserialize display condition predicate {left} {operator} {right}", - displayConditionPredicate.Entity.LeftPropertyPath, - displayConditionPredicate.Entity.OperatorType, - displayConditionPredicate.Entity.RightPropertyPath - ); - } - - public void LogListPredicateDeserializationFailure(DisplayConditionListPredicate displayConditionPredicate, JsonException exception) - { - _logger.Warning( - exception, - "Failed to deserialize display condition list predicate {list} => {left} {operator} {right}", - displayConditionPredicate.Entity.ListPropertyPath, - displayConditionPredicate.Entity.LeftPropertyPath, - displayConditionPredicate.Entity.OperatorType, - displayConditionPredicate.Entity.RightPropertyPath - ); - } - - private void RegisterBuiltInConditionOperators() - { - // General usage for any type - RegisterConditionOperator(Constants.CorePluginInfo, new EqualsConditionOperator()); - RegisterConditionOperator(Constants.CorePluginInfo, new NotEqualConditionOperator()); - - // Numeric operators - RegisterConditionOperator(Constants.CorePluginInfo, new LessThanConditionOperator()); - RegisterConditionOperator(Constants.CorePluginInfo, new GreaterThanConditionOperator()); - RegisterConditionOperator(Constants.CorePluginInfo, new LessThanOrEqualConditionOperator()); - RegisterConditionOperator(Constants.CorePluginInfo, new GreaterThanOrEqualConditionOperator()); - - // String operators - RegisterConditionOperator(Constants.CorePluginInfo, new StringEqualsConditionOperator()); - RegisterConditionOperator(Constants.CorePluginInfo, new StringNotEqualConditionOperator()); - RegisterConditionOperator(Constants.CorePluginInfo, new StringContainsConditionOperator()); - RegisterConditionOperator(Constants.CorePluginInfo, new StringNotContainsConditionOperator()); - RegisterConditionOperator(Constants.CorePluginInfo, new StringStartsWithConditionOperator()); - RegisterConditionOperator(Constants.CorePluginInfo, new StringEndsWithConditionOperator()); - RegisterConditionOperator(Constants.CorePluginInfo, new StringNullConditionOperator()); - } - - private void PluginServiceOnPluginEnabled(object sender, PluginEventArgs e) - { - if (e.PluginInfo.Instance is Module module && module.InternalExpandsMainDataModel) - AddModuleDataModel(module); - else if (e.PluginInfo.Instance is BaseDataModelExpansion dataModelExpansion) - AddDataModelExpansionDataModel(dataModelExpansion); - } - - private void AddDataModelExpansionDataModel(BaseDataModelExpansion dataModelExpansion) - { - if (dataModelExpansion.InternalDataModel.DataModelDescription == null) - throw new ArtemisPluginException(dataModelExpansion.PluginInfo, "Data model expansion overrides GetDataModelDescription but returned null"); - - AddExpansion(dataModelExpansion.InternalDataModel); - } - - private void AddModuleDataModel(Module module) - { - if (module.InternalDataModel.DataModelDescription == null) - throw new ArtemisPluginException(module.PluginInfo, "Module overrides GetDataModelDescription but returned null"); - - AddExpansion(module.InternalDataModel); - } - - private void PluginServiceOnPluginDisabled(object sender, PluginEventArgs e) - { - // Remove all data models related to the plugin - lock (_dataModelExpansions) - { - var toRemove = _dataModelExpansions.Where(d => d.PluginInfo == e.PluginInfo).ToList(); - foreach (var dataModel in toRemove) - _dataModelExpansions.Remove(dataModel); - } - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Services/Interfaces/ICoreService.cs b/src/Artemis.Core/Services/Interfaces/ICoreService.cs index 674db94ef..b506f18ac 100644 --- a/src/Artemis.Core/Services/Interfaces/ICoreService.cs +++ b/src/Artemis.Core/Services/Interfaces/ICoreService.cs @@ -3,6 +3,9 @@ using System.Collections.Generic; namespace Artemis.Core.Services { + /// + /// A service that initializes the Core and manages the render loop + /// public interface ICoreService : IArtemisService, IDisposable { /// diff --git a/src/Artemis.Core/Services/Interfaces/IDataModelService.cs b/src/Artemis.Core/Services/Interfaces/IDataModelService.cs deleted file mode 100644 index 7b2cb5582..000000000 --- a/src/Artemis.Core/Services/Interfaces/IDataModelService.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; -using Artemis.Core.DataModelExpansions; -using Artemis.Core.Properties; -using Newtonsoft.Json; - -namespace Artemis.Core.Services -{ - public interface IDataModelService : IArtemisService - { - /// - /// Gets a read-only collection of all registered condition operators - /// - IReadOnlyCollection RegisteredConditionOperators { get; } - - /// - /// Gets a read-only collection of all registered data model expansions - /// - IReadOnlyCollection DataModelExpansions { get; } - - /// - /// Add an expansion to the datamodel to be available for use after the next update - /// - /// - void AddExpansion(DataModel baseDataModelExpansion); - - /// - /// Remove a previously added expansion so that it is no longer available and updated - /// - /// - void RemoveExpansion(DataModel baseDataModelExpansion); - - /// - /// If found, returns the data model of the provided plugin - /// - /// Should be a module with a data model or a data model expansion - DataModel GetPluginDataModel(Plugin plugin); - - /// - /// If found, returns the data model of the provided plugin - /// - /// Should be a module with a data model or a data model expansion - DataModel GetPluginDataModelByGuid(Guid pluginGuid); - - /// - /// Determines whether the given plugin expands the main data model - /// - /// - /// - bool GetPluginExtendsDataModel(Plugin plugin); - - /// - /// Registers a new condition operator for use in layer conditions - /// - /// The PluginInfo of the plugin this condition operator belongs to - /// The condition operator to register - void RegisterConditionOperator([NotNull] PluginInfo pluginInfo, [NotNull] DisplayConditionOperator displayConditionOperator); - - /// - /// Removes a condition operator so it is no longer available for use in layer conditions - /// - /// The layer condition operator to remove - void RemoveConditionOperator([NotNull] DisplayConditionOperator displayConditionOperator); - - /// - /// Returns all the display condition operators compatible with the provided type - /// - List GetCompatibleConditionOperators(Type type); - - /// - /// Gets a condition operator by its plugin GUID and type name - /// - /// The operator's plugin GUID - /// The type name of the operator - DisplayConditionOperator GetConditionOperator(Guid operatorPluginGuid, string operatorType); - - /// - /// Logs a predicate deserialization failure - /// - /// The predicate that failed to deserialize - /// The JSON exception that occurred - void LogPredicateDeserializationFailure(DisplayConditionPredicate displayConditionPredicate, JsonException exception); - - /// - /// Logs a list predicate deserialization failure - /// - /// The list predicate that failed to deserialize - /// The JSON exception that occurred - void LogListPredicateDeserializationFailure(DisplayConditionListPredicate displayConditionListPredicate, JsonException exception); - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Services/Interfaces/IDeviceService.cs b/src/Artemis.Core/Services/Interfaces/IDeviceService.cs index cb09e83e7..154ad5466 100644 --- a/src/Artemis.Core/Services/Interfaces/IDeviceService.cs +++ b/src/Artemis.Core/Services/Interfaces/IDeviceService.cs @@ -1,5 +1,8 @@ namespace Artemis.Core.Services { + /// + /// A service that allows you manage an + /// public interface IDeviceService : IArtemisService { /// diff --git a/src/Artemis.Core/Services/Interfaces/IRenderElementService.cs b/src/Artemis.Core/Services/Interfaces/IRenderElementService.cs deleted file mode 100644 index 79319f578..000000000 --- a/src/Artemis.Core/Services/Interfaces/IRenderElementService.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Artemis.Core.LayerBrushes; -using Artemis.Core.LayerEffects; - -namespace Artemis.Core.Services -{ - public interface IRenderElementService : IArtemisService - { - /// - /// Creates a new layer - /// - /// - /// - /// - /// - Layer CreateLayer(Profile profile, ProfileElement parent, string name); - - /// - /// Removes the currently active layer brush from the and deletes any settings - /// - /// The layer to remove the active brush from - void RemoveLayerBrush(Layer layer); - - /// - /// Deactivates the currently active layer brush from the but keeps all settings - /// - /// The layer to deactivate the active brush on - void DeactivateLayerBrush(Layer layer); - - /// - /// Instantiates and adds the described by the provided - /// - /// to the . - /// - /// The layer to instantiate the brush for - /// - BaseLayerBrush InstantiateLayerBrush(Layer layer); - - /// - /// Instantiates and adds the described by the provided - /// to the . - /// - /// The layer/folder to instantiate the effect for - void InstantiateLayerEffects(RenderProfileElement renderProfileElement); - - /// - /// Adds the described by the provided to the - /// . - /// - /// The layer/folder to instantiate the effect for - /// - /// - BaseLayerEffect AddLayerEffect(RenderProfileElement renderProfileElement, LayerEffectDescriptor layerEffectDescriptor); - - void RemoveLayerEffect(BaseLayerEffect layerEffect); - - void InstantiateDisplayConditions(RenderProfileElement renderElement); - void InstantiateDataBindings(RenderProfileElement renderElement); - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Services/Interfaces/IRgbService.cs b/src/Artemis.Core/Services/Interfaces/IRgbService.cs index 5b78f6bfb..e5cd0c213 100644 --- a/src/Artemis.Core/Services/Interfaces/IRgbService.cs +++ b/src/Artemis.Core/Services/Interfaces/IRgbService.cs @@ -4,7 +4,10 @@ using RGB.NET.Core; namespace Artemis.Core.Services { - public interface IRgbService : IArtemisService + /// + /// A service that allows you to manage the and its contents + /// + public interface IRgbService : IArtemisService, IDisposable { /// /// Gets or sets the RGB surface rendering is performed on @@ -26,7 +29,14 @@ namespace Artemis.Core.Services /// IReadOnlyCollection LoadedDevices { get; } + /// + /// Gets the update trigger that drives the render loop + /// TimerUpdateTrigger UpdateTrigger { get; } + + /// + /// Gets or sets whether rendering should be paused + /// bool IsRenderPaused { get; set; } /// @@ -35,8 +45,6 @@ namespace Artemis.Core.Services /// void AddDeviceProvider(IRGBDeviceProvider deviceProvider); - void Dispose(); - /// /// Occurs when a single device has loaded /// @@ -47,6 +55,9 @@ namespace Artemis.Core.Services /// event EventHandler DeviceReloaded; + /// + /// Recalculates the LED group used by the + /// void UpdateSurfaceLedGroup(); } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Registration/ConditionOperatorService.cs b/src/Artemis.Core/Services/Registration/ConditionOperatorService.cs new file mode 100644 index 000000000..9ea6fab80 --- /dev/null +++ b/src/Artemis.Core/Services/Registration/ConditionOperatorService.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Artemis.Core.Services +{ + internal class ConditionOperatorService : IConditionOperatorService + { + public ConditionOperatorService() + { + RegisterBuiltInConditionOperators(); + } + + public ConditionOperatorRegistration RegisterConditionOperator(PluginInfo pluginInfo, ConditionOperator conditionOperator) + { + if (pluginInfo == null) + throw new ArgumentNullException(nameof(pluginInfo)); + if (conditionOperator == null) + throw new ArgumentNullException(nameof(conditionOperator)); + + conditionOperator.PluginInfo = pluginInfo; + return ConditionOperatorStore.Add(conditionOperator); + } + + public void RemoveConditionOperator(ConditionOperatorRegistration registration) + { + if (registration == null) + throw new ArgumentNullException(nameof(registration)); + ConditionOperatorStore.Remove(registration); + } + + public List GetConditionOperatorsForType(Type type) + { + return ConditionOperatorStore.GetForType(type).Select(r => r.ConditionOperator).ToList(); + } + + public ConditionOperator GetConditionOperator(Guid operatorPluginGuid, string operatorType) + { + return ConditionOperatorStore.Get(operatorPluginGuid, operatorType)?.ConditionOperator; + } + + private void RegisterBuiltInConditionOperators() + { + // General usage for any type + RegisterConditionOperator(Constants.CorePluginInfo, new EqualsConditionOperator()); + RegisterConditionOperator(Constants.CorePluginInfo, new NotEqualConditionOperator()); + + // Numeric operators + RegisterConditionOperator(Constants.CorePluginInfo, new LessThanConditionOperator()); + RegisterConditionOperator(Constants.CorePluginInfo, new GreaterThanConditionOperator()); + RegisterConditionOperator(Constants.CorePluginInfo, new LessThanOrEqualConditionOperator()); + RegisterConditionOperator(Constants.CorePluginInfo, new GreaterThanOrEqualConditionOperator()); + + // String operators + RegisterConditionOperator(Constants.CorePluginInfo, new StringEqualsConditionOperator()); + RegisterConditionOperator(Constants.CorePluginInfo, new StringNotEqualConditionOperator()); + RegisterConditionOperator(Constants.CorePluginInfo, new StringContainsConditionOperator()); + RegisterConditionOperator(Constants.CorePluginInfo, new StringNotContainsConditionOperator()); + RegisterConditionOperator(Constants.CorePluginInfo, new StringStartsWithConditionOperator()); + RegisterConditionOperator(Constants.CorePluginInfo, new StringEndsWithConditionOperator()); + RegisterConditionOperator(Constants.CorePluginInfo, new StringNullConditionOperator()); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/Registration/DataBindingService.cs b/src/Artemis.Core/Services/Registration/DataBindingService.cs new file mode 100644 index 000000000..8deed0c63 --- /dev/null +++ b/src/Artemis.Core/Services/Registration/DataBindingService.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Artemis.Core.Services +{ + internal class DataBindingService : IDataBindingService + { + public DataBindingService() + { + RegisterBuiltInModifiers(); + } + + public DataBindingModifierTypeRegistration RegisterModifierType(PluginInfo pluginInfo, DataBindingModifierType dataBindingModifierType) + { + if (pluginInfo == null) + throw new ArgumentNullException(nameof(pluginInfo)); + if (dataBindingModifierType == null) + throw new ArgumentNullException(nameof(dataBindingModifierType)); + + dataBindingModifierType.PluginInfo = pluginInfo; + return DataBindingModifierTypeStore.Add(dataBindingModifierType); + } + + public void RemoveModifierType(DataBindingModifierTypeRegistration registration) + { + if (registration == null) + throw new ArgumentNullException(nameof(registration)); + DataBindingModifierTypeStore.Remove(registration); + } + + public List GetCompatibleModifierTypes(Type type) + { + return DataBindingModifierTypeStore.GetForType(type).Select(r => r.DataBindingModifierType).ToList(); + } + + public DataBindingModifierType GetModifierType(Guid modifierTypePluginGuid, string modifierType) + { + return DataBindingModifierTypeStore.Get(modifierTypePluginGuid, modifierType)?.DataBindingModifierType; + } + + private void RegisterBuiltInModifiers() + { + RegisterModifierType(Constants.CorePluginInfo, new MultiplicationModifierType()); + RegisterModifierType(Constants.CorePluginInfo, new DivideModifierType()); + RegisterModifierType(Constants.CorePluginInfo, new FloorModifierType()); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/Registration/DataModelService.cs b/src/Artemis.Core/Services/Registration/DataModelService.cs new file mode 100644 index 000000000..f3dcfaeb9 --- /dev/null +++ b/src/Artemis.Core/Services/Registration/DataModelService.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Artemis.Core.DataModelExpansions; +using Artemis.Core.Modules; + +namespace Artemis.Core.Services +{ + internal class DataModelService : IDataModelService + { + public DataModelService(IPluginService pluginService) + { + // Add data models of already loaded plugins + foreach (var module in pluginService.GetPluginsOfType()) + AddModuleDataModel(module); + foreach (var dataModelExpansion in pluginService.GetPluginsOfType()) + AddDataModelExpansionDataModel(dataModelExpansion); + + // Add data models of new plugins when they get enabled + pluginService.PluginEnabled += PluginServiceOnPluginEnabled; + } + + public DataModelRegistration RegisterDataModel(DataModel dataModel) + { + if (dataModel == null) + throw new ArgumentNullException(nameof(dataModel)); + return DataModelStore.Add(dataModel); + } + + public void RemoveDataModel(DataModelRegistration registration) + { + if (registration == null) + throw new ArgumentNullException(nameof(registration)); + DataModelStore.Remove(registration); + } + + public List GetDataModels() + { + return DataModelStore.GetAll().Select(d => d.DataModel).ToList(); + } + + public T GetDataModel() where T : DataModel + { + return (T) DataModelStore.GetAll().FirstOrDefault(d => d.DataModel is T)?.DataModel; + } + + public DataModel GetPluginDataModel(Plugin plugin) + { + return DataModelStore.Get(plugin.PluginInfo.Guid)?.DataModel; + } + + public DataModel GetPluginDataModel(Guid pluginGuid) + { + return DataModelStore.Get(pluginGuid)?.DataModel; + } + + private void PluginServiceOnPluginEnabled(object sender, PluginEventArgs e) + { + if (e.PluginInfo.Instance is Module module) + AddModuleDataModel(module); + else if (e.PluginInfo.Instance is BaseDataModelExpansion dataModelExpansion) + AddDataModelExpansionDataModel(dataModelExpansion); + } + + private void AddModuleDataModel(Module module) + { + if (module.InternalDataModel == null) + return; + + if (module.InternalDataModel.DataModelDescription == null) + throw new ArtemisPluginException(module.PluginInfo, "Module overrides GetDataModelDescription but returned null"); + + module.InternalDataModel.IsExpansion = module.InternalExpandsMainDataModel; + RegisterDataModel(module.InternalDataModel); + } + + private void AddDataModelExpansionDataModel(BaseDataModelExpansion dataModelExpansion) + { + if (dataModelExpansion.InternalDataModel.DataModelDescription == null) + throw new ArtemisPluginException(dataModelExpansion.PluginInfo, "Data model expansion overrides GetDataModelDescription but returned null"); + + dataModelExpansion.InternalDataModel.IsExpansion = true; + RegisterDataModel(dataModelExpansion.InternalDataModel); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/Registration/Interfaces/IConditionOperatorService.cs b/src/Artemis.Core/Services/Registration/Interfaces/IConditionOperatorService.cs new file mode 100644 index 000000000..f59eb587d --- /dev/null +++ b/src/Artemis.Core/Services/Registration/Interfaces/IConditionOperatorService.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using Artemis.Core.Properties; + +namespace Artemis.Core.Services +{ + /// + /// A service that allows you to register and retrieve conditions operators used by display conditions + /// + public interface IConditionOperatorService : IArtemisService + { + /// + /// Registers a new condition operator for use in layer conditions + /// + /// The PluginInfo of the plugin this condition operator belongs to + /// The condition operator to register + ConditionOperatorRegistration RegisterConditionOperator([NotNull] PluginInfo pluginInfo, [NotNull] ConditionOperator conditionOperator); + + /// + /// Removes a condition operator so it is no longer available for use in layer conditions + /// + /// The registration of the condition operator to remove + void RemoveConditionOperator([NotNull] ConditionOperatorRegistration registration); + + /// + /// Returns all the condition operators compatible with the provided type + /// + List GetConditionOperatorsForType(Type type); + + /// + /// Gets a condition operator by its plugin GUID and type name + /// + /// The operator's plugin GUID + /// The type name of the operator + ConditionOperator GetConditionOperator(Guid operatorPluginGuid, string operatorType); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/Interfaces/IDataBindingService.cs b/src/Artemis.Core/Services/Registration/Interfaces/IDataBindingService.cs similarity index 60% rename from src/Artemis.Core/Services/Interfaces/IDataBindingService.cs rename to src/Artemis.Core/Services/Registration/Interfaces/IDataBindingService.cs index 8d9201383..b40248b37 100644 --- a/src/Artemis.Core/Services/Interfaces/IDataBindingService.cs +++ b/src/Artemis.Core/Services/Registration/Interfaces/IDataBindingService.cs @@ -1,29 +1,26 @@ using System; using System.Collections.Generic; using Artemis.Core.Properties; -using Newtonsoft.Json; namespace Artemis.Core.Services { + /// + /// A service that allows you to register and retrieve data binding modifiers used by the data bindings system + /// public interface IDataBindingService : IArtemisService { - /// - /// Gets a read-only collection of all registered modifier types - /// - IReadOnlyCollection RegisteredDataBindingModifierTypes { get; } - /// /// Registers a new modifier type for use in data bindings /// /// The PluginInfo of the plugin this modifier type belongs to /// The modifier type to register - void RegisterModifierType([NotNull] PluginInfo pluginInfo, [NotNull] DataBindingModifierType dataBindingModifierType); + DataBindingModifierTypeRegistration RegisterModifierType([NotNull] PluginInfo pluginInfo, [NotNull] DataBindingModifierType dataBindingModifierType); /// /// Removes a modifier type so it is no longer available for use in data bindings /// - /// The modifier type to remove - void RemoveModifierType([NotNull] DataBindingModifierType dataBindingModifierType); + /// The registration of the modifier type to remove + void RemoveModifierType([NotNull] DataBindingModifierTypeRegistration dataBindingModifierType); /// /// Returns all the data binding modifier types compatible with the provided type @@ -37,12 +34,5 @@ namespace Artemis.Core.Services /// The type name of the modifier type /// DataBindingModifierType GetModifierType(Guid modifierTypePluginGuid, string modifierType); - - /// - /// Logs a modifier deserialization failure - /// - /// The modifier that failed to deserialize - /// The JSON exception that occurred - void LogModifierDeserializationFailure(DataBindingModifier dataBindingModifier, JsonSerializationException exception); } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Registration/Interfaces/IDataModelService.cs b/src/Artemis.Core/Services/Registration/Interfaces/IDataModelService.cs new file mode 100644 index 000000000..c9ab8a279 --- /dev/null +++ b/src/Artemis.Core/Services/Registration/Interfaces/IDataModelService.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using Artemis.Core.DataModelExpansions; + +namespace Artemis.Core.Services +{ + /// + /// A service that allows you to register and retrieve data models + /// + public interface IDataModelService : IArtemisService + { + /// + /// Add a data model to so that it is available to conditions and data bindings + /// + /// + DataModelRegistration RegisterDataModel(DataModel dataModel); + + /// + /// Remove a previously added data model so that it is no longer available + /// + void RemoveDataModel(DataModelRegistration registration); + + /// + /// Returns a list of all registered data models + /// + List GetDataModels(); + + /// + /// If found, returns the registered data model of type + /// + /// The type of the data model to find + T GetDataModel() where T : DataModel; + + /// + /// If found, returns the data model of the provided plugin + /// + /// The plugin to find the data model of + DataModel GetPluginDataModel(Plugin plugin); + + /// + /// If found, returns the data model of the provided plugin GUID + /// + /// The GUID of the plugin to find the data model of + DataModel GetPluginDataModel(Guid pluginGuid); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/Registration/Interfaces/ILayerBrushService.cs b/src/Artemis.Core/Services/Registration/Interfaces/ILayerBrushService.cs new file mode 100644 index 000000000..a8e576312 --- /dev/null +++ b/src/Artemis.Core/Services/Registration/Interfaces/ILayerBrushService.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Artemis.Core.LayerBrushes; + +namespace Artemis.Core.Services +{ + /// + /// A service that allows you to register and retrieve layer brushes + /// + public interface ILayerBrushService : IArtemisService + { + /// + /// Add a layer brush descriptor so that it is available to layers + /// + LayerBrushRegistration RegisterLayerBrush(LayerBrushDescriptor descriptor); + + /// + /// Remove a previously added layer brush descriptor so that it is no longer available + /// + void RemoveLayerBrush(LayerBrushRegistration registration); + + /// + /// Returns a list of all registered layer brush descriptors + /// + List GetLayerBrushes(); + + /// + /// Returns the descriptor of the default layer brush + /// + LayerBrushDescriptor GetDefaultLayerBrush(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/Registration/Interfaces/ILayerEffectService.cs b/src/Artemis.Core/Services/Registration/Interfaces/ILayerEffectService.cs new file mode 100644 index 000000000..1bb093200 --- /dev/null +++ b/src/Artemis.Core/Services/Registration/Interfaces/ILayerEffectService.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Artemis.Core.LayerBrushes; +using Artemis.Core.LayerEffects; + +namespace Artemis.Core.Services +{ + /// + /// A service that allows you to register and retrieve layer brushes + /// + public interface ILayerEffectService : IArtemisService + { + /// + /// Add an effect descriptor so that it is available to profile elements + /// + LayerEffectRegistration RegisterLayerEffect(LayerEffectDescriptor descriptor); + + /// + /// Remove a previously added layer effect descriptor so that it is no longer available + /// + void RemoveLayerEffect(LayerEffectRegistration registration); + + /// + /// Returns a list of all registered layer effect descriptors + /// + List GetLayerEffects(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/Registration/LayerBrushService.cs b/src/Artemis.Core/Services/Registration/LayerBrushService.cs new file mode 100644 index 000000000..8a3a69e55 --- /dev/null +++ b/src/Artemis.Core/Services/Registration/LayerBrushService.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Artemis.Core.LayerBrushes; + +namespace Artemis.Core.Services +{ + internal class LayerBrushService : ILayerBrushService + { + private readonly ISettingsService _settingsService; + + public LayerBrushService(ISettingsService settingsService) + { + _settingsService = settingsService; + } + + public LayerBrushRegistration RegisterLayerBrush(LayerBrushDescriptor descriptor) + { + if (descriptor == null) + throw new ArgumentNullException(nameof(descriptor)); + + return LayerBrushStore.Add(descriptor); + } + + public void RemoveLayerBrush(LayerBrushRegistration registration) + { + if (registration == null) + throw new ArgumentNullException(nameof(registration)); + LayerBrushStore.Remove(registration); + } + + public List GetLayerBrushes() + { + return LayerBrushStore.GetAll().Select(r => r.LayerBrushDescriptor).ToList(); + } + + public LayerBrushDescriptor GetDefaultLayerBrush() + { + var defaultReference = _settingsService.GetSetting("ProfileEditor.DefaultLayerBrushDescriptor", new LayerBrushReference + { + BrushPluginGuid = Guid.Parse("92a9d6ba-6f7a-4937-94d5-c1d715b4141a"), + BrushType = "ColorBrush" + }); + + return LayerBrushStore.Get(defaultReference.Value.BrushPluginGuid, defaultReference.Value.BrushType)?.LayerBrushDescriptor; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/Registration/LayerEffectService.cs b/src/Artemis.Core/Services/Registration/LayerEffectService.cs new file mode 100644 index 000000000..a845776b0 --- /dev/null +++ b/src/Artemis.Core/Services/Registration/LayerEffectService.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Artemis.Core.LayerEffects; + +namespace Artemis.Core.Services +{ + internal class LayerEffectService : ILayerEffectService + { + public LayerEffectRegistration RegisterLayerEffect(LayerEffectDescriptor descriptor) + { + if (descriptor == null) + throw new ArgumentNullException(nameof(descriptor)); + + return LayerEffectStore.Add(descriptor); + } + + public void RemoveLayerEffect(LayerEffectRegistration registration) + { + if (registration == null) + throw new ArgumentNullException(nameof(registration)); + LayerEffectStore.Remove(registration); + } + + public List GetLayerEffects() + { + return LayerEffectStore.GetAll().Select(r => r.LayerEffectDescriptor).ToList(); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/RenderElementService.cs b/src/Artemis.Core/Services/RenderElementService.cs deleted file mode 100644 index 6b26c94de..000000000 --- a/src/Artemis.Core/Services/RenderElementService.cs +++ /dev/null @@ -1,169 +0,0 @@ -using System; -using System.Linq; -using Artemis.Core.LayerBrushes; -using Artemis.Core.LayerEffects; -using Ninject; -using Serilog; - -namespace Artemis.Core.Services -{ - internal class RenderElementService : IRenderElementService - { - private readonly IDataModelService _dataModelService; - private readonly IDataBindingService _dataBindingService; - private readonly IKernel _kernel; - private readonly ILogger _logger; - private readonly IPluginService _pluginService; - - public RenderElementService(IKernel kernel, ILogger logger, IPluginService pluginService, IDataModelService dataModelService, IDataBindingService dataBindingService) - { - _kernel = kernel; - _logger = logger; - _pluginService = pluginService; - _dataModelService = dataModelService; - _dataBindingService = dataBindingService; - } - - public Layer CreateLayer(Profile profile, ProfileElement parent, string name) - { - var layer = new Layer(profile, parent, name); - parent.AddChild(layer); - - // Layers have two hardcoded property groups, instantiate them - layer.General.InitializeProperties(this, layer, "General."); - layer.Transform.InitializeProperties(this, layer, "Transform."); - - // With the properties loaded, the layer brush and effect can be instantiated - InstantiateLayerBrush(layer); - InstantiateLayerEffects(layer); - InstantiateDisplayConditions(layer); - InstantiateDataBindings(layer); - return layer; - } - - public void RemoveLayerBrush(Layer layer) - { - layer.RemoveLayerBrush(); - layer.OnLayerBrushUpdated(); - } - - public void DeactivateLayerBrush(Layer layer) - { - layer.DeactivateLayerBrush(); - layer.OnLayerBrushUpdated(); - } - - public BaseLayerBrush InstantiateLayerBrush(Layer layer) - { - if (layer.LayerBrush != null) - throw new ArtemisCoreException("Layer already has an instantiated layer brush"); - - var descriptorReference = layer.General.BrushReference?.CurrentValue; - if (descriptorReference == null) - return null; - - // Get a matching descriptor - var layerBrushProviders = _pluginService.GetPluginsOfType(); - var descriptors = layerBrushProviders.SelectMany(l => l.LayerBrushDescriptors).ToList(); - var descriptor = descriptors.FirstOrDefault(d => d.LayerBrushProvider.PluginInfo.Guid == descriptorReference.BrushPluginGuid && - d.LayerBrushType.Name == descriptorReference.BrushType); - - if (descriptor == null) - return null; - - var brush = (BaseLayerBrush) _kernel.Get(descriptor.LayerBrushType); - brush.Layer = layer; - brush.Descriptor = descriptor; - brush.Initialize(this); - brush.Update(0); - - layer.LayerBrush = brush; - layer.OnLayerBrushUpdated(); - - return brush; - } - - public BaseLayerEffect AddLayerEffect(RenderProfileElement renderElement, LayerEffectDescriptor layerEffectDescriptor) - { - // Create the effect with dependency injection - var effect = (BaseLayerEffect) _kernel.Get(layerEffectDescriptor.LayerEffectType); - - effect.ProfileElement = renderElement; - effect.EntityId = Guid.NewGuid(); - effect.Enabled = true; - effect.Order = renderElement.LayerEffects.Count + 1; - effect.Descriptor = layerEffectDescriptor; - - effect.Initialize(this); - effect.Update(0); - - renderElement.AddLayerEffect(effect); - _logger.Debug("Added layer effect with root path {rootPath}", effect.PropertyRootPath); - return effect; - } - - public void RemoveLayerEffect(BaseLayerEffect layerEffect) - { - layerEffect.ProfileElement.RemoveLayerEffect(layerEffect); - } - - public void InstantiateLayerEffects(RenderProfileElement renderElement) - { - var layerEffectProviders = _pluginService.GetPluginsOfType(); - var descriptors = layerEffectProviders.SelectMany(l => l.LayerEffectDescriptors).ToList(); - var entities = renderElement.RenderElementEntity.LayerEffects.OrderByDescending(e => e.Order).ToList(); - - foreach (var layerEffectEntity in entities) - { - // Skip effects already on the element - if (renderElement.LayerEffects.Any(e => e.EntityId == layerEffectEntity.Id)) - continue; - - // Get a matching descriptor - var descriptor = descriptors.FirstOrDefault(d => d.LayerEffectProvider.PluginInfo.Guid == layerEffectEntity.PluginGuid && - d.LayerEffectType.Name == layerEffectEntity.EffectType); - if (descriptor == null) - continue; - - // Create the effect with dependency injection - var effect = (BaseLayerEffect) _kernel.Get(descriptor.LayerEffectType); - - effect.ProfileElement = renderElement; - effect.EntityId = layerEffectEntity.Id; - effect.Order = layerEffectEntity.Order; - effect.Name = layerEffectEntity.Name; - effect.Enabled = layerEffectEntity.Enabled; - effect.Descriptor = descriptor; - - effect.Initialize(this); - effect.Update(0); - - renderElement.AddLayerEffect(effect); - _logger.Debug("Instantiated layer effect with root path {rootPath}", effect.PropertyRootPath); - } - } - - public void InstantiateDisplayConditions(RenderProfileElement renderElement) - { - var displayCondition = renderElement.RenderElementEntity.RootDisplayCondition != null - ? new DisplayConditionGroup(null, renderElement.RenderElementEntity.RootDisplayCondition) - : new DisplayConditionGroup(null); - - try - { - displayCondition.Initialize(_dataModelService); - renderElement.DisplayConditionGroup = displayCondition; - } - catch (Exception e) - { - _logger.Warning(e, $"Failed to init display conditions for {renderElement}"); - } - } - - public void InstantiateDataBindings(RenderProfileElement renderElement) - { - foreach (var baseLayerProperty in renderElement.GetAllLayerProperties()) - baseLayerProperty.InitializeDataBindings(_dataModelService, _dataBindingService); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs index df10d1a78..c80e36736 100644 --- a/src/Artemis.Core/Services/RgbService.cs +++ b/src/Artemis.Core/Services/RgbService.cs @@ -10,7 +10,7 @@ namespace Artemis.Core.Services /// /// Provides wrapped access the RGB.NET /// - internal class RgbService : IRgbService, IDisposable + internal class RgbService : IRgbService { private readonly List _loadedDevices; private readonly ILogger _logger; diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index f66031fca..f31724d93 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Artemis.Core.LayerBrushes; -using Artemis.Core.LayerEffects; using Artemis.Core.Modules; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Repositories.Interfaces; @@ -17,21 +15,22 @@ namespace Artemis.Core.Services private readonly ILogger _logger; private readonly IPluginService _pluginService; private readonly IProfileRepository _profileRepository; - private readonly IRenderElementService _renderElementService; private readonly ISurfaceService _surfaceService; - internal ProfileService(ILogger logger, IPluginService pluginService, ISurfaceService surfaceService, IRenderElementService renderElementService, IProfileRepository profileRepository) + internal ProfileService(ILogger logger, + IPluginService pluginService, + ISurfaceService surfaceService, + IConditionOperatorService conditionOperatorService, + IDataBindingService dataBindingService, + IProfileRepository profileRepository) { _logger = logger; _pluginService = pluginService; _surfaceService = surfaceService; - _renderElementService = renderElementService; _profileRepository = profileRepository; _surfaceService.ActiveSurfaceConfigurationSelected += OnActiveSurfaceConfigurationSelected; _surfaceService.SurfaceConfigurationUpdated += OnSurfaceConfigurationUpdated; - _pluginService.PluginEnabled += OnPluginToggled; - _pluginService.PluginDisabled += OnPluginToggled; } public JsonSerializerSettings MementoSettings { get; set; } = new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.All}; @@ -158,13 +157,13 @@ namespace Artemis.Core.Services profile.RedoStack.Clear(); profile.UndoStack.Push(memento); - profile.ApplyToEntity(); + profile.Save(); if (includeChildren) { foreach (var folder in profile.GetAllFolders()) - folder.ApplyToEntity(); + folder.Save(); foreach (var layer in profile.GetAllLayers()) - layer.ApplyToEntity(); + layer.Save(); } _profileRepository.Save(profile.ProfileEntity); @@ -186,7 +185,7 @@ namespace Artemis.Core.Services profile.RedoStack.Push(memento); profile.ProfileEntity = JsonConvert.DeserializeObject(top, MementoSettings); - profile.ApplyToProfile(); + profile.Load(); InstantiateProfile(profile); } @@ -210,7 +209,7 @@ namespace Artemis.Core.Services profile.UndoStack.Push(memento); profile.ProfileEntity = JsonConvert.DeserializeObject(top, MementoSettings); - profile.ApplyToProfile(); + profile.Load(); InstantiateProfile(profile); _logger.Debug("Redo profile update - Success"); @@ -221,9 +220,6 @@ namespace Artemis.Core.Services public void InstantiateProfile(Profile profile) { profile.PopulateLeds(_surfaceService.ActiveSurface); - InitializeLayerProperties(profile); - InstantiateLayers(profile); - InstantiateFolders(profile); } public string ExportProfile(ProfileDescriptor profileDescriptor) @@ -269,66 +265,6 @@ namespace Artemis.Core.Services } } - /// - /// Initializes the properties on the layers of the given profile - /// - /// - private void InitializeLayerProperties(Profile profile) - { - foreach (var layer in profile.GetAllLayers()) - { - if (!layer.General.PropertiesInitialized) - layer.General.InitializeProperties(_renderElementService, layer, "General."); - if (!layer.Transform.PropertiesInitialized) - layer.Transform.InitializeProperties(_renderElementService, layer, "Transform."); - } - } - - /// - /// Instantiates all plugin-related classes on the folders of the given profile - /// - private void InstantiateFolders(Profile profile) - { - foreach (var folder in profile.GetAllFolders()) - { - // Instantiate effects - _renderElementService.InstantiateLayerEffects(folder); - // Remove effects of plugins that are disabled - var disabledEffects = new List(folder.LayerEffects.Where(layerLayerEffect => !layerLayerEffect.PluginInfo.Enabled)); - foreach (var layerLayerEffect in disabledEffects) - _renderElementService.RemoveLayerEffect(layerLayerEffect); - - _renderElementService.InstantiateDisplayConditions(folder); - _renderElementService.InstantiateDataBindings(folder); - } - } - - /// - /// Instantiates all plugin-related classes on the layers of the given profile - /// - private void InstantiateLayers(Profile profile) - { - foreach (var layer in profile.GetAllLayers()) - { - // Instantiate brush - if (layer.LayerBrush == null) - _renderElementService.InstantiateLayerBrush(layer); - // Remove brush if plugin is disabled - else if (!layer.LayerBrush.PluginInfo.Enabled) - _renderElementService.DeactivateLayerBrush(layer); - - // Instantiate effects - _renderElementService.InstantiateLayerEffects(layer); - // Remove effects of plugins that are disabled - var disabledEffects = new List(layer.LayerEffects.Where(layerLayerEffect => !layerLayerEffect.PluginInfo.Enabled)); - foreach (var layerLayerEffect in disabledEffects) - _renderElementService.RemoveLayerEffect(layerLayerEffect); - - _renderElementService.InstantiateDisplayConditions(layer); - _renderElementService.InstantiateDataBindings(layer); - } - } - /// /// Populates all missing LEDs on all currently active profiles /// @@ -340,20 +276,6 @@ namespace Artemis.Core.Services profileModule.ActiveProfile.PopulateLeds(surface); } - - /// - /// Instantiates all missing plugin-related classes on the profile trees of all currently active profiles - /// - private void ActiveProfilesInstantiatePlugins() - { - var profileModules = _pluginService.GetPluginsOfType(); - foreach (var profileModule in profileModules.Where(p => p.ActiveProfile != null).ToList()) - { - InstantiateLayers(profileModule.ActiveProfile); - InstantiateFolders(profileModule.ActiveProfile); - } - } - #region Event handlers private void OnActiveSurfaceConfigurationSelected(object sender, SurfaceConfigurationEventArgs e) @@ -367,14 +289,6 @@ namespace Artemis.Core.Services ActiveProfilesPopulateLeds(e.Surface); } - private void OnPluginToggled(object sender, PluginEventArgs e) - { - if (e.PluginInfo.Instance is LayerBrushProvider) - ActiveProfilesInstantiatePlugins(); - if (e.PluginInfo.Instance is LayerEffectProvider) - ActiveProfilesInstantiatePlugins(); - } - #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Stores/ConditionOperatorStore.cs b/src/Artemis.Core/Stores/ConditionOperatorStore.cs new file mode 100644 index 000000000..194188084 --- /dev/null +++ b/src/Artemis.Core/Stores/ConditionOperatorStore.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Artemis.Core +{ + internal class ConditionOperatorStore + { + private static readonly List Registrations = new List(); + + public static ConditionOperatorRegistration Add(ConditionOperator conditionOperator) + { + ConditionOperatorRegistration registration; + lock (Registrations) + { + if (Registrations.Any(r => r.ConditionOperator == conditionOperator)) + throw new ArtemisCoreException($"Condition operator store store already contains operator '{conditionOperator.Description}'"); + + registration = new ConditionOperatorRegistration(conditionOperator, conditionOperator.PluginInfo.Instance) {IsInStore = true}; + Registrations.Add(registration); + } + + OnConditionOperatorAdded(new ConditionOperatorStoreEvent(registration)); + return registration; + } + + public static void Remove(ConditionOperatorRegistration registration) + { + lock (Registrations) + { + if (!Registrations.Contains(registration)) + throw new ArtemisCoreException($"Condition operator store does not contain operator '{registration.ConditionOperator.Description}'"); + + Registrations.Remove(registration); + registration.IsInStore = false; + } + + OnConditionOperatorRemoved(new ConditionOperatorStoreEvent(registration)); + } + + public static ConditionOperatorRegistration Get(Guid pluginGuid, string type) + { + lock (Registrations) + { + return Registrations.FirstOrDefault(r => r.Plugin.PluginInfo.Guid == pluginGuid && r.ConditionOperator.GetType().Name == type); + } + } + + public static List GetForType(Type type) + { + lock (Registrations) + { + if (type == null) + return new List(Registrations); + + var candidates = Registrations.Where(r => r.ConditionOperator.CompatibleTypes.Any(t => t.IsCastableFrom(type))).ToList(); + + // If there are multiple operators with the same description, use the closest match + foreach (var displayConditionOperators in candidates.GroupBy(r => r.ConditionOperator.Description).Where(g => g.Count() > 1).ToList()) + { + var closest = displayConditionOperators.OrderByDescending(r => r.ConditionOperator.CompatibleTypes.Contains(type)).FirstOrDefault(); + foreach (var displayConditionOperator in displayConditionOperators) + { + if (displayConditionOperator != closest) + candidates.Remove(displayConditionOperator); + } + } + + return candidates; + } + } + + #region Events + + public static event EventHandler ConditionOperatorAdded; + public static event EventHandler ConditionOperatorRemoved; + + private static void OnConditionOperatorAdded(ConditionOperatorStoreEvent e) + { + ConditionOperatorAdded?.Invoke(null, e); + } + + private static void OnConditionOperatorRemoved(ConditionOperatorStoreEvent e) + { + ConditionOperatorRemoved?.Invoke(null, e); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Stores/DataBindingModifierTypeStore.cs b/src/Artemis.Core/Stores/DataBindingModifierTypeStore.cs new file mode 100644 index 000000000..b398ed925 --- /dev/null +++ b/src/Artemis.Core/Stores/DataBindingModifierTypeStore.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Artemis.Core +{ + internal class DataBindingModifierTypeStore + { + private static readonly List Registrations = new List(); + + public static DataBindingModifierTypeRegistration Add(DataBindingModifierType modifierType) + { + DataBindingModifierTypeRegistration typeRegistration; + lock (Registrations) + { + if (Registrations.Any(r => r.DataBindingModifierType == modifierType)) + throw new ArtemisCoreException($"Data binding modifier type store already contains modifier '{modifierType.Description}'"); + + typeRegistration = new DataBindingModifierTypeRegistration(modifierType, modifierType.PluginInfo.Instance) { IsInStore = true }; + Registrations.Add(typeRegistration); + } + + OnDataBindingModifierAdded(new DataBindingModifierTypeStoreEvent(typeRegistration)); + return typeRegistration; + } + + public static void Remove(DataBindingModifierTypeRegistration typeRegistration) + { + lock (Registrations) + { + if (!Registrations.Contains(typeRegistration)) + throw new ArtemisCoreException($"Data binding modifier type store does not contain modifier type '{typeRegistration.DataBindingModifierType.Description}'"); + + Registrations.Remove(typeRegistration); + typeRegistration.IsInStore = false; + } + + OnDataBindingModifierRemoved(new DataBindingModifierTypeStoreEvent(typeRegistration)); + } + + public static DataBindingModifierTypeRegistration Get(Guid pluginGuid, string type) + { + lock (Registrations) + { + return Registrations.FirstOrDefault(r => r.Plugin.PluginInfo.Guid == pluginGuid && r.DataBindingModifierType.GetType().Name == type); + } + } + + public static List GetForType(Type type) + { + lock (Registrations) + { + if (type == null) + return new List(Registrations); + + var candidates = Registrations.Where(r => r.DataBindingModifierType.CompatibleTypes.Any(t => t.IsCastableFrom(type))).ToList(); + + // If there are multiple operators with the same description, use the closest match + foreach (var displayDataBindingModifiers in candidates.GroupBy(r => r.DataBindingModifierType.Description).Where(g => g.Count() > 1).ToList()) + { + var closest = displayDataBindingModifiers.OrderByDescending(r => r.DataBindingModifierType.CompatibleTypes.Contains(type)).FirstOrDefault(); + foreach (var displayDataBindingModifier in displayDataBindingModifiers) + { + if (displayDataBindingModifier != closest) + candidates.Remove(displayDataBindingModifier); + } + } + + return candidates; + } + } + + #region Events + + public static event EventHandler DataBindingModifierAdded; + public static event EventHandler DataBindingModifierRemoved; + + private static void OnDataBindingModifierAdded(DataBindingModifierTypeStoreEvent e) + { + DataBindingModifierAdded?.Invoke(null, e); + } + + private static void OnDataBindingModifierRemoved(DataBindingModifierTypeStoreEvent e) + { + DataBindingModifierRemoved?.Invoke(null, e); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Stores/DataModelStore.cs b/src/Artemis.Core/Stores/DataModelStore.cs new file mode 100644 index 000000000..2617e91f7 --- /dev/null +++ b/src/Artemis.Core/Stores/DataModelStore.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Artemis.Core.DataModelExpansions; + +namespace Artemis.Core +{ + internal class DataModelStore + { + private static readonly List Registrations = new List(); + + public static DataModelRegistration Add(DataModel dataModel) + { + DataModelRegistration registration; + lock (Registrations) + { + if (Registrations.Any(r => r.DataModel == dataModel)) + throw new ArtemisCoreException($"Data model store already contains data model '{dataModel.DataModelDescription}'"); + + registration = new DataModelRegistration(dataModel, dataModel.PluginInfo.Instance) {IsInStore = true}; + Registrations.Add(registration); + } + + OnDataModelAdded(new DataModelStoreEvent(registration)); + return registration; + } + + public static void Remove(DataModelRegistration registration) + { + lock (Registrations) + { + if (!Registrations.Contains(registration)) + throw new ArtemisCoreException($"Data model store does not contain data model '{registration.DataModel.DataModelDescription}'"); + + Registrations.Remove(registration); + registration.IsInStore = false; + } + + OnDataModelRemoved(new DataModelStoreEvent(registration)); + } + + public static List GetAll() + { + lock (Registrations) + { + return new List(Registrations); + } + } + + public static DataModelRegistration Get(Guid pluginGuid) + { + lock (Registrations) + { + return Registrations.FirstOrDefault(d => d.Plugin.PluginInfo.Guid == pluginGuid); + } + } + + #region Events + + public static event EventHandler DataModelAdded; + public static event EventHandler DataModelRemoved; + + private static void OnDataModelAdded(DataModelStoreEvent e) + { + DataModelAdded?.Invoke(null, e); + } + + private static void OnDataModelRemoved(DataModelStoreEvent e) + { + DataModelRemoved?.Invoke(null, e); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Stores/LayerBrushStore.cs b/src/Artemis.Core/Stores/LayerBrushStore.cs new file mode 100644 index 000000000..9dd77dc9b --- /dev/null +++ b/src/Artemis.Core/Stores/LayerBrushStore.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Artemis.Core.LayerBrushes; + +namespace Artemis.Core +{ + internal class LayerBrushStore + { + private static readonly List Registrations = new List(); + + public static LayerBrushRegistration Add(LayerBrushDescriptor descriptor) + { + LayerBrushRegistration registration; + lock (Registrations) + { + if (Registrations.Any(r => r.LayerBrushDescriptor == descriptor)) + throw new ArtemisCoreException($"Store already contains layer brush '{descriptor.DisplayName}'"); + + registration = new LayerBrushRegistration(descriptor, descriptor.LayerBrushProvider.PluginInfo.Instance) {IsInStore = true}; + Registrations.Add(registration); + } + + OnLayerBrushAdded(new LayerBrushStoreEvent(registration)); + return registration; + } + + public static void Remove(LayerBrushRegistration registration) + { + lock (Registrations) + { + if (!Registrations.Contains(registration)) + throw new ArtemisCoreException($"Store does not contain layer brush '{registration.LayerBrushDescriptor.DisplayName}'"); + + Registrations.Remove(registration); + registration.IsInStore = false; + } + + OnLayerBrushRemoved(new LayerBrushStoreEvent(registration)); + } + + public static List GetAll() + { + lock (Registrations) + { + return new List(Registrations); + } + } + + public static LayerBrushRegistration Get(Guid pluginGuid, string typeName) + { + lock (Registrations) + { + return Registrations.FirstOrDefault(d => d.Plugin.PluginInfo.Guid == pluginGuid && + d.LayerBrushDescriptor.LayerBrushType.Name == typeName); + } + } + + #region Events + + public static event EventHandler LayerBrushAdded; + public static event EventHandler LayerBrushRemoved; + + private static void OnLayerBrushAdded(LayerBrushStoreEvent e) + { + LayerBrushAdded?.Invoke(null, e); + } + + private static void OnLayerBrushRemoved(LayerBrushStoreEvent e) + { + LayerBrushRemoved?.Invoke(null, e); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Stores/LayerEffectStore.cs b/src/Artemis.Core/Stores/LayerEffectStore.cs new file mode 100644 index 000000000..7e78e22bc --- /dev/null +++ b/src/Artemis.Core/Stores/LayerEffectStore.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Artemis.Core.LayerEffects; + +namespace Artemis.Core +{ + internal class LayerEffectStore + { + private static readonly List Registrations = new List(); + + public static LayerEffectRegistration Add(LayerEffectDescriptor descriptor) + { + LayerEffectRegistration registration; + lock (Registrations) + { + if (Registrations.Any(r => r.LayerEffectDescriptor == descriptor)) + throw new ArtemisCoreException($"Store already contains layer brush '{descriptor.DisplayName}'"); + + registration = new LayerEffectRegistration(descriptor, descriptor.LayerEffectProvider.PluginInfo.Instance) { IsInStore = true }; + Registrations.Add(registration); + } + + OnLayerEffectAdded(new LayerEffectStoreEvent(registration)); + return registration; + } + + public static void Remove(LayerEffectRegistration registration) + { + lock (Registrations) + { + if (!Registrations.Contains(registration)) + throw new ArtemisCoreException($"Store does not contain layer brush '{registration.LayerEffectDescriptor.DisplayName}'"); + + Registrations.Remove(registration); + registration.IsInStore = false; + } + + OnLayerEffectRemoved(new LayerEffectStoreEvent(registration)); + } + + public static List GetAll() + { + lock (Registrations) + { + return new List(Registrations); + } + } + + public static LayerEffectRegistration Get(Guid pluginGuid, string typeName) + { + lock (Registrations) + { + return Registrations.FirstOrDefault(d => d.Plugin.PluginInfo.Guid == pluginGuid && d.LayerEffectDescriptor.LayerEffectType.Name == typeName); + } + } + + #region Events + + public static event EventHandler LayerEffectAdded; + public static event EventHandler LayerEffectRemoved; + + private static void OnLayerEffectAdded(LayerEffectStoreEvent e) + { + LayerEffectAdded?.Invoke(null, e); + } + + private static void OnLayerEffectRemoved(LayerEffectStoreEvent e) + { + LayerEffectRemoved?.Invoke(null, e); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Stores/Registrations/ConditionOperatorRegistration.cs b/src/Artemis.Core/Stores/Registrations/ConditionOperatorRegistration.cs new file mode 100644 index 000000000..babd6da7e --- /dev/null +++ b/src/Artemis.Core/Stores/Registrations/ConditionOperatorRegistration.cs @@ -0,0 +1,40 @@ +using System; + +namespace Artemis.Core +{ + /// + /// Represents a data model registration + /// + public class ConditionOperatorRegistration + { + internal ConditionOperatorRegistration(ConditionOperator conditionOperator, Plugin plugin) + { + ConditionOperator = conditionOperator; + Plugin = plugin; + + Plugin.PluginDisabled += PluginOnPluginDisabled; + } + + /// + /// Gets the condition operator that has been registered + /// + public ConditionOperator ConditionOperator { get; } + + /// + /// Gets the plugin the condition operator is associated with + /// + public Plugin Plugin { get; } + + /// + /// Gets a boolean indicating whether the registration is in the internal Core store + /// + public bool IsInStore { get; internal set; } + + private void PluginOnPluginDisabled(object sender, EventArgs e) + { + Plugin.PluginDisabled -= PluginOnPluginDisabled; + if (IsInStore) + ConditionOperatorStore.Remove(this); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Stores/Registrations/DataBindingModifierTypeRegistration.cs b/src/Artemis.Core/Stores/Registrations/DataBindingModifierTypeRegistration.cs new file mode 100644 index 000000000..d00e19444 --- /dev/null +++ b/src/Artemis.Core/Stores/Registrations/DataBindingModifierTypeRegistration.cs @@ -0,0 +1,40 @@ +using System; + +namespace Artemis.Core +{ + /// + /// Represents a data model registration + /// + public class DataBindingModifierTypeRegistration + { + internal DataBindingModifierTypeRegistration(DataBindingModifierType dataBindingModifierType, Plugin plugin) + { + DataBindingModifierType = dataBindingModifierType; + Plugin = plugin; + + Plugin.PluginDisabled += PluginOnPluginDisabled; + } + + /// + /// Gets the data binding modifier that has been registered + /// + public DataBindingModifierType DataBindingModifierType { get; } + + /// + /// Gets the plugin the data binding modifier is associated with + /// + public Plugin Plugin { get; } + + /// + /// Gets a boolean indicating whether the registration is in the internal Core store + /// + public bool IsInStore { get; internal set; } + + private void PluginOnPluginDisabled(object sender, EventArgs e) + { + Plugin.PluginDisabled -= PluginOnPluginDisabled; + if (IsInStore) + DataBindingModifierTypeStore.Remove(this); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Stores/Registrations/DataModelRegistration.cs b/src/Artemis.Core/Stores/Registrations/DataModelRegistration.cs new file mode 100644 index 000000000..336a64c96 --- /dev/null +++ b/src/Artemis.Core/Stores/Registrations/DataModelRegistration.cs @@ -0,0 +1,41 @@ +using System; +using Artemis.Core.DataModelExpansions; + +namespace Artemis.Core +{ + /// + /// Represents a data model registration + /// + public class DataModelRegistration + { + internal DataModelRegistration(DataModel dataModel, Plugin plugin) + { + DataModel = dataModel; + Plugin = plugin; + + Plugin.PluginDisabled += PluginOnPluginDisabled; + } + + /// + /// Gets the data model that has been registered + /// + public DataModel DataModel { get; } + + /// + /// Gets the plugin the data model is associated with + /// + public Plugin Plugin { get; } + + /// + /// Gets a boolean indicating whether the registration is in the internal Core store + /// + public bool IsInStore { get; internal set; } + + private void PluginOnPluginDisabled(object sender, EventArgs e) + { + Plugin.PluginDisabled -= PluginOnPluginDisabled; + if (IsInStore) + DataModelStore.Remove(this); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Stores/Registrations/LayerBrushRegistration.cs b/src/Artemis.Core/Stores/Registrations/LayerBrushRegistration.cs new file mode 100644 index 000000000..eeda91812 --- /dev/null +++ b/src/Artemis.Core/Stores/Registrations/LayerBrushRegistration.cs @@ -0,0 +1,41 @@ +using System; +using Artemis.Core.LayerBrushes; + +namespace Artemis.Core +{ + /// + /// Represents a layer brush registration + /// + public class LayerBrushRegistration + { + internal LayerBrushRegistration(LayerBrushDescriptor descriptor, Plugin plugin) + { + LayerBrushDescriptor = descriptor; + Plugin = plugin; + + Plugin.PluginDisabled += PluginOnPluginDisabled; + } + + /// + /// Gets the layer brush descriptor that has been registered + /// + public LayerBrushDescriptor LayerBrushDescriptor { get; } + + /// + /// Gets the plugin the layer brush is associated with + /// + public Plugin Plugin { get; } + + /// + /// Gets a boolean indicating whether the registration is in the internal Core store + /// + public bool IsInStore { get; internal set; } + + private void PluginOnPluginDisabled(object sender, EventArgs e) + { + Plugin.PluginDisabled -= PluginOnPluginDisabled; + if (IsInStore) + LayerBrushStore.Remove(this); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Stores/Registrations/LayerEffectRegistration.cs b/src/Artemis.Core/Stores/Registrations/LayerEffectRegistration.cs new file mode 100644 index 000000000..992acf616 --- /dev/null +++ b/src/Artemis.Core/Stores/Registrations/LayerEffectRegistration.cs @@ -0,0 +1,41 @@ +using System; +using Artemis.Core.LayerEffects; + +namespace Artemis.Core +{ + /// + /// Represents a layer effect registration + /// + public class LayerEffectRegistration + { + internal LayerEffectRegistration(LayerEffectDescriptor descriptor, Plugin plugin) + { + LayerEffectDescriptor = descriptor; + Plugin = plugin; + + Plugin.PluginDisabled += PluginOnPluginDisabled; + } + + /// + /// Gets the layer effect descriptor that has been registered + /// + public LayerEffectDescriptor LayerEffectDescriptor { get; } + + /// + /// Gets the plugin the layer effect is associated with + /// + public Plugin Plugin { get; } + + /// + /// Gets a boolean indicating whether the registration is in the internal Core store + /// + public bool IsInStore { get; internal set; } + + private void PluginOnPluginDisabled(object sender, EventArgs e) + { + Plugin.PluginDisabled -= PluginOnPluginDisabled; + if (IsInStore) + LayerEffectStore.Remove(this); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Utilities/CorePlugin.cs b/src/Artemis.Core/Utilities/CorePlugin.cs new file mode 100644 index 000000000..8a87a9028 --- /dev/null +++ b/src/Artemis.Core/Utilities/CorePlugin.cs @@ -0,0 +1,40 @@ +using Artemis.Core.LayerEffects; + +namespace Artemis.Core +{ + /// + /// An empty plugin used by + /// + internal class CorePlugin : Plugin + { + public CorePlugin() + { + Constants.CorePluginInfo.Instance = this; + Enabled = true; + } + + public override void EnablePlugin() + { + } + + public override void DisablePlugin() + { + } + } + + internal class EffectPlaceholderPlugin : LayerEffectProvider + { + public EffectPlaceholderPlugin() + { + Enabled = true; + } + + public override void EnablePlugin() + { + } + + public override void DisablePlugin() + { + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Utilities/DeserializationLogger.cs b/src/Artemis.Core/Utilities/DeserializationLogger.cs new file mode 100644 index 000000000..1be7ad7fb --- /dev/null +++ b/src/Artemis.Core/Utilities/DeserializationLogger.cs @@ -0,0 +1,44 @@ +using Newtonsoft.Json; +using Ninject; +using Serilog; + +namespace Artemis.Core +{ + internal static class DeserializationLogger + { + private static ILogger _logger; + + public static void Initialize(IKernel kernel) + { + _logger = kernel.Get(); + } + + public static void LogPredicateDeserializationFailure(DisplayConditionPredicate displayConditionPredicate, JsonException exception) + { + _logger.Warning( + exception, + "Failed to deserialize display condition predicate {left} {operator} {right}", + displayConditionPredicate.Entity.LeftPropertyPath, + displayConditionPredicate.Entity.OperatorType, + displayConditionPredicate.Entity.RightPropertyPath + ); + } + + public static void LogListPredicateDeserializationFailure(DisplayConditionListPredicate displayConditionPredicate, JsonException exception) + { + _logger.Warning( + exception, + "Failed to deserialize display condition list predicate {list} => {left} {operator} {right}", + displayConditionPredicate.Entity.ListPropertyPath, + displayConditionPredicate.Entity.LeftPropertyPath, + displayConditionPredicate.Entity.OperatorType, + displayConditionPredicate.Entity.RightPropertyPath + ); + } + + 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.Storage/Entities/Profile/DataBindings/DataBindingEntity.cs b/src/Artemis.Storage/Entities/Profile/DataBindings/DataBindingEntity.cs index 761acbeee..14011e683 100644 --- a/src/Artemis.Storage/Entities/Profile/DataBindings/DataBindingEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/DataBindings/DataBindingEntity.cs @@ -10,7 +10,7 @@ namespace Artemis.Storage.Entities.Profile.DataBindings Modifiers = new List(); } - public string TargetProperty { get; set; } + public string TargetExpression { get; set; } public Guid? SourceDataModelGuid { get; set; } public string SourcePropertyPath { get; set; } public int DataBindingMode { get; set; } diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs index bc718153d..34635c846 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs @@ -13,7 +13,7 @@ using Stylet; namespace Artemis.UI.Shared.Input { - public class DataModelDynamicViewModel : PropertyChangedBase + public class DataModelDynamicViewModel : PropertyChangedBase, IDisposable { private readonly IDataModelUIService _dataModelUIService; private readonly Module _module; @@ -89,10 +89,7 @@ namespace Artemis.UI.Shared.Input private void Initialize() { // Get the data models - DataModelViewModel = _dataModelUIService.GetMainDataModelVisualization(); - if (!_dataModelUIService.GetPluginExtendsDataModel(_module)) - DataModelViewModel.Children.Add(_dataModelUIService.GetPluginDataModelVisualization(_module)); - + DataModelViewModel = _dataModelUIService.GetPluginDataModelVisualization(_module, true); DataModelViewModel.UpdateRequested += DataModelOnUpdateRequested; _updateTimer.Start(); @@ -130,5 +127,12 @@ namespace Artemis.UI.Shared.Input } #endregion + + public void Dispose() + { + _updateTimer.Stop(); + _updateTimer.Dispose(); + _updateTimer.Elapsed -= OnUpdateTimerOnElapsed; + } } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/DataModelUIService.cs b/src/Artemis.UI.Shared/Services/DataModelUIService.cs index 73e34e9e6..280bc4b5f 100644 --- a/src/Artemis.UI.Shared/Services/DataModelUIService.cs +++ b/src/Artemis.UI.Shared/Services/DataModelUIService.cs @@ -32,7 +32,7 @@ namespace Artemis.UI.Shared.Services public DataModelPropertiesViewModel GetMainDataModelVisualization() { var viewModel = new DataModelPropertiesViewModel(null, null, null); - foreach (var dataModelExpansion in _dataModelService.DataModelExpansions) + foreach (var dataModelExpansion in _dataModelService.GetDataModels()) viewModel.Children.Add(new DataModelPropertiesViewModel(dataModelExpansion, viewModel, null)); // Update to populate children @@ -41,8 +41,23 @@ namespace Artemis.UI.Shared.Services return viewModel; } - public DataModelPropertiesViewModel GetPluginDataModelVisualization(Plugin plugin) + public DataModelPropertiesViewModel GetPluginDataModelVisualization(Plugin plugin, bool includeMainDataModel) { + if (includeMainDataModel) + { + var mainDataModel = GetMainDataModelVisualization(); + + // If the main data model already includes the plugin data model we're done + if (mainDataModel.Children.Any(c => c.DataModel.PluginInfo.Instance == plugin)) + return mainDataModel; + // Otherwise get just the plugin data model and add it + var pluginDataModel = GetPluginDataModelVisualization(plugin, false); + if (pluginDataModel != null) + mainDataModel.Children.Add(pluginDataModel); + + return mainDataModel; + } + var dataModel = _dataModelService.GetPluginDataModel(plugin); if (dataModel == null) return null; @@ -55,12 +70,7 @@ namespace Artemis.UI.Shared.Services viewModel.UpdateRequested += (sender, args) => viewModel.Update(this); return viewModel; } - - public bool GetPluginExtendsDataModel(Plugin plugin) - { - return _dataModelService.GetPluginExtendsDataModel(plugin); - } - + public DataModelVisualizationRegistration RegisterDataModelInput(PluginInfo pluginInfo, IReadOnlyCollection compatibleConversionTypes = null) where T : DataModelInputViewModel { if (compatibleConversionTypes == null) diff --git a/src/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs b/src/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs index 049a1bf11..f9cb711df 100644 --- a/src/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs +++ b/src/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs @@ -12,14 +12,7 @@ namespace Artemis.UI.Shared.Services IReadOnlyCollection RegisteredDataModelEditors { get; } IReadOnlyCollection RegisteredDataModelDisplays { get; } DataModelPropertiesViewModel GetMainDataModelVisualization(); - DataModelPropertiesViewModel GetPluginDataModelVisualization(Plugin plugin); - - /// - /// Determines whether the given plugin expands the main data model - /// - /// - /// - bool GetPluginExtendsDataModel(Plugin plugin); + DataModelPropertiesViewModel GetPluginDataModelVisualization(Plugin plugin, bool includeMainDataModel); DataModelVisualizationRegistration RegisterDataModelInput(PluginInfo pluginInfo, IReadOnlyCollection compatibleConversionTypes) where T : DataModelInputViewModel; DataModelVisualizationRegistration RegisterDataModelDisplay(PluginInfo pluginInfo) where T : DataModelDisplayViewModel; diff --git a/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs b/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs index 92df0c90c..85cf152e0 100644 --- a/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs @@ -10,7 +10,7 @@ namespace Artemis.UI.Shared.Services { Profile SelectedProfile { get; } RenderProfileElement SelectedProfileElement { get; } - BaseLayerProperty SelectedDataBinding { get; } + ILayerProperty SelectedDataBinding { get; } TimeSpan CurrentTime { get; set; } int PixelsPerSecond { get; set; } IReadOnlyList RegisteredPropertyEditors { get; } @@ -19,7 +19,7 @@ namespace Artemis.UI.Shared.Services void UpdateSelectedProfile(); void ChangeSelectedProfileElement(RenderProfileElement profileElement); void UpdateSelectedProfileElement(); - void ChangeSelectedDataBinding(BaseLayerProperty layerProperty); + void ChangeSelectedDataBinding(ILayerProperty layerProperty); void UpdateProfilePreview(); bool UndoUpdateProfile(); bool RedoUpdateProfile(); @@ -74,6 +74,16 @@ namespace Artemis.UI.Shared.Services /// PropertyInputRegistration RegisterPropertyInput(PluginInfo pluginInfo) where T : PropertyInputViewModel; + /// + /// Registers a new property input view model used in the profile editor for the generic type defined in + /// + /// Note: Registration will remove itself on plugin disable so you don't have to + /// + /// + /// + /// + PropertyInputRegistration RegisterPropertyInput(Type viewModelType, PluginInfo pluginInfo); + void RemovePropertyInput(PropertyInputRegistration registration); /// @@ -84,9 +94,13 @@ namespace Artemis.UI.Shared.Services /// How close the time must be to snap /// Enable snapping to timeline segments /// Enable snapping to the current time of the editor - /// Enable snapping to visible keyframes - /// A keyframe to exclude during keyframe snapping + /// An optional extra list of times to snap to /// - TimeSpan SnapToTimeline(TimeSpan time, TimeSpan tolerance, bool snapToSegments, bool snapToCurrentTime, bool snapToKeyframes, BaseLayerPropertyKeyframe excludedKeyframe = null); + TimeSpan SnapToTimeline(TimeSpan time, TimeSpan tolerance, bool snapToSegments, bool snapToCurrentTime, List snapTimes = null); + + /// + /// If a matching registration is found, creates a new supporting + /// + PropertyInputViewModel CreatePropertyInputViewModel(LayerProperty layerProperty); } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs index ac437396b..5eafbff57 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs @@ -5,6 +5,7 @@ using Artemis.Core; using Artemis.Core.Modules; using Artemis.Core.Services; using Ninject; +using Ninject.Parameters; using Serilog; using Stylet; @@ -34,7 +35,7 @@ namespace Artemis.UI.Shared.Services public IReadOnlyList RegisteredPropertyEditors => _registeredPropertyEditors.AsReadOnly(); public Profile SelectedProfile { get; private set; } public RenderProfileElement SelectedProfileElement { get; private set; } - public BaseLayerProperty SelectedDataBinding { get; private set; } + public ILayerProperty SelectedDataBinding { get; private set; } public TimeSpan CurrentTime { @@ -127,7 +128,7 @@ namespace Artemis.UI.Shared.Services } } - public void ChangeSelectedDataBinding(BaseLayerProperty layerProperty) + public void ChangeSelectedDataBinding(ILayerProperty layerProperty) { SelectedDataBinding = layerProperty; OnSelectedDataBindingChanged(); @@ -195,10 +196,25 @@ namespace Artemis.UI.Shared.Services public PropertyInputRegistration RegisterPropertyInput(PluginInfo pluginInfo) where T : PropertyInputViewModel { - var viewModelType = typeof(T); + return RegisterPropertyInput(typeof(T), pluginInfo); + } + + public PropertyInputRegistration RegisterPropertyInput(Type viewModelType, PluginInfo pluginInfo) + { + if (!typeof(PropertyInputViewModel).IsAssignableFrom(viewModelType)) + throw new ArtemisSharedUIException($"Property input VM type must implement {nameof(PropertyInputViewModel)}"); + lock (_registeredPropertyEditors) { var supportedType = viewModelType.BaseType.GetGenericArguments()[0]; + // If the supported type is a generic, assume there is a base type + if (supportedType.IsGenericParameter) + { + if (supportedType.BaseType == null) + throw new ArtemisSharedUIException($"Generic property input VM type must have a type constraint"); + supportedType = supportedType.BaseType; + } + var existing = _registeredPropertyEditors.FirstOrDefault(r => r.SupportedType == supportedType); if (existing != null) { @@ -228,7 +244,7 @@ namespace Artemis.UI.Shared.Services } } - public TimeSpan SnapToTimeline(TimeSpan time, TimeSpan tolerance, bool snapToSegments, bool snapToCurrentTime, bool snapToKeyframes, BaseLayerPropertyKeyframe excludedKeyframe = null) + public TimeSpan SnapToTimeline(TimeSpan time, TimeSpan tolerance, bool snapToSegments, bool snapToCurrentTime, List snapTimes = null) { if (snapToSegments) { @@ -253,22 +269,39 @@ namespace Artemis.UI.Shared.Services return SelectedProfileElement.StartSegmentLength; } - if (snapToKeyframes) + if (snapTimes != null) { - // Get all visible keyframes - var keyframes = SelectedProfileElement.GetAllKeyframes() - .Where(k => k != excludedKeyframe && SelectedProfileElement.IsPropertyGroupExpanded(k.BaseLayerProperty.Parent)) - .ToList(); - // Find the closest keyframe - var closeKeyframe = keyframes.FirstOrDefault(k => Math.Abs(time.TotalMilliseconds - k.Position.TotalMilliseconds) < tolerance.TotalMilliseconds); - if (closeKeyframe != null) - return closeKeyframe.Position; + var closeSnapTime = snapTimes.FirstOrDefault(s => Math.Abs(time.TotalMilliseconds - s.TotalMilliseconds) < tolerance.TotalMilliseconds); + if (closeSnapTime != TimeSpan.Zero) + return closeSnapTime; } return time; } + public PropertyInputViewModel CreatePropertyInputViewModel(LayerProperty layerProperty) + { + Type viewModelType = null; + var registration = RegisteredPropertyEditors.FirstOrDefault(r => r.SupportedType == typeof(T)); + + // Check for enums if no supported type was found + if (registration == null && typeof(T).IsEnum) + { + // The enum VM will likely be a generic, that requires creating a generic type matching the layer property + registration = RegisteredPropertyEditors.FirstOrDefault(r => r.SupportedType == typeof(Enum)); + if (registration != null && registration.ViewModelType.IsGenericType) + viewModelType = registration.ViewModelType.MakeGenericType(layerProperty.GetType().GenericTypeArguments); + } + else if (registration != null) + viewModelType = registration.ViewModelType; + else + return null; + + var parameter = new ConstructorArgument("layerProperty", layerProperty); + return (PropertyInputViewModel) Kernel.Get(viewModelType, parameter); + } + public ProfileModule GetCurrentModule() { return SelectedProfile?.Module; diff --git a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs index 68df7dd44..cd50678b0 100644 --- a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -1,13 +1,10 @@ -using System.Reflection; -using Artemis.Core; +using Artemis.Core; using Artemis.Core.Modules; using Artemis.UI.Screens.Modules; using Artemis.UI.Screens.Modules.Tabs; using Artemis.UI.Screens.ProfileEditor; using Artemis.UI.Screens.ProfileEditor.DisplayConditions; -using Artemis.UI.Screens.ProfileEditor.DisplayConditions.Abstract; using Artemis.UI.Screens.ProfileEditor.LayerProperties; -using Artemis.UI.Screens.ProfileEditor.LayerProperties.Abstract; using Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings; using Artemis.UI.Screens.ProfileEditor.LayerProperties.LayerEffects; using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline; @@ -19,7 +16,6 @@ using Artemis.UI.Screens.Settings.Debug; using Artemis.UI.Screens.Settings.Tabs.Devices; using Artemis.UI.Screens.Settings.Tabs.Plugins; using Stylet; -using Module = Artemis.Core.Modules.Module; namespace Artemis.UI.Ninject.Factories { @@ -46,15 +42,10 @@ namespace Artemis.UI.Ninject.Factories DeviceDebugViewModel Create(ArtemisDevice device); } - public interface IFolderVmFactory : IVmFactory + public interface IProfileTreeVmFactory : IVmFactory { - FolderViewModel Create(ProfileElement folder); - FolderViewModel Create(TreeItemViewModel parent, ProfileElement folder); - } - - public interface ILayerVmFactory : IVmFactory - { - LayerViewModel Create(TreeItemViewModel parent, ProfileElement folder); + FolderViewModel FolderViewModel(ProfileElement folder); + LayerViewModel LayerViewModel(ProfileElement layer); } public interface IProfileLayerVmFactory : IVmFactory @@ -72,26 +63,35 @@ namespace Artemis.UI.Ninject.Factories public interface IDisplayConditionsVmFactory : IVmFactory { - DisplayConditionGroupViewModel DisplayConditionGroupViewModel(DisplayConditionGroup displayConditionGroup, DisplayConditionViewModel parent, bool isListGroup); - DisplayConditionListViewModel DisplayConditionListViewModel(DisplayConditionList displayConditionList, DisplayConditionViewModel parent); - DisplayConditionPredicateViewModel DisplayConditionPredicateViewModel(DisplayConditionPredicate displayConditionPredicate, DisplayConditionViewModel parent); - DisplayConditionListPredicateViewModel DisplayConditionListPredicateViewModel(DisplayConditionListPredicate displayConditionListPredicate, DisplayConditionViewModel parent); - } - - public interface IDataBindingsVmFactory : IVmFactory - { - DataBindingsViewModel DataBindingsViewModel(BaseLayerProperty layerProperty); - DataBindingViewModel DataBindingViewModel(DataBindingRegistration registration); - DataBindingModifierViewModel DataBindingModifierViewModel(DataBindingModifier modifier); + DisplayConditionGroupViewModel DisplayConditionGroupViewModel(DisplayConditionGroup displayConditionGroup, bool isListGroup); + DisplayConditionListViewModel DisplayConditionListViewModel(DisplayConditionList displayConditionList); + DisplayConditionPredicateViewModel DisplayConditionPredicateViewModel(DisplayConditionPredicate displayConditionPredicate); + DisplayConditionListPredicateViewModel DisplayConditionListPredicateViewModel(DisplayConditionListPredicate displayConditionListPredicate); } public interface ILayerPropertyVmFactory : IVmFactory { - LayerPropertyGroupViewModel LayerPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, PropertyGroupDescriptionAttribute propertyGroupDescription); + LayerPropertyViewModel LayerPropertyViewModel(ILayerProperty layerProperty); + + LayerPropertyGroupViewModel LayerPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup); + TreeGroupViewModel TreeGroupViewModel(LayerPropertyGroupViewModel layerPropertyGroupViewModel); + TimelineGroupViewModel TimelineGroupViewModel(LayerPropertyGroupViewModel layerPropertyGroupViewModel); + TreeViewModel TreeViewModel(LayerPropertiesViewModel layerPropertiesViewModel, BindableCollection layerPropertyGroups); EffectsViewModel EffectsViewModel(LayerPropertiesViewModel layerPropertiesViewModel); TimelineViewModel TimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel, BindableCollection layerPropertyGroups); - TreePropertyGroupViewModel TreePropertyGroupViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel); - TimelinePropertyGroupViewModel TimelinePropertyGroupViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel); + TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, BindableCollection layerPropertyGroups); + } + + public interface IDataBindingsVmFactory + { + IDataBindingViewModel DataBindingViewModel(IDataBindingRegistration registration); + DataBindingModifierViewModel DataBindingModifierViewModel(DataBindingModifier modifier); + } + + public interface IPropertyVmFactory + { + ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel); + ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel); } } \ No newline at end of file diff --git a/src/Artemis.UI/Ninject/InstanceProviders/DataBindingsViewModelInstanceProvider.cs b/src/Artemis.UI/Ninject/InstanceProviders/DataBindingsViewModelInstanceProvider.cs new file mode 100644 index 000000000..8a8e829fe --- /dev/null +++ b/src/Artemis.UI/Ninject/InstanceProviders/DataBindingsViewModelInstanceProvider.cs @@ -0,0 +1,26 @@ +using System; +using System.Reflection; +using Artemis.Core; +using Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings; +using Ninject.Extensions.Factory; + +namespace Artemis.UI.Ninject.InstanceProviders +{ + public class DataBindingsViewModelInstanceProvider : StandardInstanceProvider + { + protected override Type GetType(MethodInfo methodInfo, object[] arguments) + { + if (methodInfo.ReturnType != typeof(IDataBindingViewModel)) + return base.GetType(methodInfo, arguments); + + // Find LayerProperty type + var descriptionPropertyType = arguments[0].GetType(); + while (descriptionPropertyType != null && (!descriptionPropertyType.IsGenericType || descriptionPropertyType.GetGenericTypeDefinition() != typeof(DataBindingRegistration<,>))) + descriptionPropertyType = descriptionPropertyType.BaseType; + if (descriptionPropertyType == null) + return base.GetType(methodInfo, arguments); + + return typeof(DataBindingViewModel<,>).MakeGenericType(descriptionPropertyType.GetGenericArguments()); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Ninject/InstanceProviders/LayerPropertyViewModelInstanceProvider.cs b/src/Artemis.UI/Ninject/InstanceProviders/LayerPropertyViewModelInstanceProvider.cs new file mode 100644 index 000000000..34ed14551 --- /dev/null +++ b/src/Artemis.UI/Ninject/InstanceProviders/LayerPropertyViewModelInstanceProvider.cs @@ -0,0 +1,32 @@ +using System; +using System.Reflection; +using Artemis.Core; +using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline; +using Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree; +using Ninject.Extensions.Factory; + +namespace Artemis.UI.Ninject.InstanceProviders +{ + public class LayerPropertyViewModelInstanceProvider : StandardInstanceProvider + { + protected override Type GetType(MethodInfo methodInfo, object[] arguments) + { + if (methodInfo.ReturnType != typeof(ITreePropertyViewModel) && methodInfo.ReturnType != typeof(ITimelinePropertyViewModel)) + return base.GetType(methodInfo, arguments); + + // Find LayerProperty type + var layerPropertyType = arguments[0].GetType(); + while (layerPropertyType != null && (!layerPropertyType.IsGenericType || layerPropertyType.GetGenericTypeDefinition() != typeof(LayerProperty<>))) + layerPropertyType = layerPropertyType.BaseType; + if (layerPropertyType == null) + return base.GetType(methodInfo, arguments); + + if (methodInfo.ReturnType == typeof(ITreePropertyViewModel)) + return typeof(TreePropertyViewModel<>).MakeGenericType(layerPropertyType.GetGenericArguments()); + if (methodInfo.ReturnType == typeof(ITimelinePropertyViewModel)) + return typeof(TimelinePropertyViewModel<>).MakeGenericType(layerPropertyType.GetGenericArguments()); + + return base.GetType(methodInfo, arguments); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Ninject/UiModule.cs b/src/Artemis.UI/Ninject/UiModule.cs index 2903c5a48..3d807d1d9 100644 --- a/src/Artemis.UI/Ninject/UiModule.cs +++ b/src/Artemis.UI/Ninject/UiModule.cs @@ -1,5 +1,6 @@ using System; using Artemis.UI.Ninject.Factories; +using Artemis.UI.Ninject.InstanceProviders; using Artemis.UI.Screens; using Artemis.UI.Screens.ProfileEditor; using Artemis.UI.Services.Interfaces; @@ -7,6 +8,7 @@ using Artemis.UI.Shared.Services; using Artemis.UI.Stylet; using FluentValidation; using Ninject.Extensions.Conventions; +using Ninject.Extensions.Factory; using Ninject.Modules; using Stylet; @@ -48,12 +50,15 @@ namespace Artemis.UI.Ninject .BindToFactory(); }); + Kernel.Bind().ToFactory(() => new DataBindingsViewModelInstanceProvider()); + Kernel.Bind().ToFactory(() => new LayerPropertyViewModelInstanceProvider()); + // Bind profile editor VMs Kernel.Bind(x => { x.FromThisAssembly() .SelectAllClasses() - .InheritedFrom() + .InheritedFrom() .BindAllBaseClasses(); }); diff --git a/src/Artemis.UI/PropertyInput/BrushPropertyInputView.xaml b/src/Artemis.UI/PropertyInput/BrushPropertyInputView.xaml index c698304b8..820bbbdc4 100644 --- a/src/Artemis.UI/PropertyInput/BrushPropertyInputView.xaml +++ b/src/Artemis.UI/PropertyInput/BrushPropertyInputView.xaml @@ -11,28 +11,11 @@ d:DesignHeight="450" d:DesignWidth="800" d:DataContext="{d:DesignInstance {x:Type propertyInput:BrushPropertyInputViewModel}}"> - - - - - - - - - - - - - - - - - - - - - - + + + + + @@ -46,8 +29,8 @@ ItemsSource="{Binding Path=Descriptors}" SelectedValue="{Binding Path=SelectedDescriptor}" ItemTemplateSelector="{dataTemplateSelectors:ComboBoxTemplateSelector - SelectedItemTemplate={StaticResource SimpleTemplate}, - DropdownItemsTemplate={StaticResource ExtendedTemplate}}" /> + SelectedItemTemplate={StaticResource SimpleLayerBrushDescriptorTemplate}, + DropdownItemsTemplate={StaticResource ExtendedLayerBrushDescriptorTemplate}}" /> \ No newline at end of file diff --git a/src/Artemis.UI/PropertyInput/BrushPropertyInputViewModel.cs b/src/Artemis.UI/PropertyInput/BrushPropertyInputViewModel.cs index a51d405fb..61c512175 100644 --- a/src/Artemis.UI/PropertyInput/BrushPropertyInputViewModel.cs +++ b/src/Artemis.UI/PropertyInput/BrushPropertyInputViewModel.cs @@ -5,19 +5,19 @@ using Artemis.Core.LayerBrushes; using Artemis.Core.Services; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; +using Stylet; namespace Artemis.UI.PropertyInput { public class BrushPropertyInputViewModel : PropertyInputViewModel { private readonly IPluginService _pluginService; - private readonly IRenderElementService _renderElementService; - private List _descriptors; + private BindableCollection _descriptors; - public BrushPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, - IRenderElementService renderElementService, IPluginService pluginService) : base(layerProperty, profileEditorService) + public BrushPropertyInputViewModel(LayerProperty layerProperty, + IProfileEditorService profileEditorService, + IPluginService pluginService) : base(layerProperty, profileEditorService) { - _renderElementService = renderElementService; _pluginService = pluginService; _pluginService.PluginEnabled += PluginServiceOnPluginLoaded; @@ -25,7 +25,7 @@ namespace Artemis.UI.PropertyInput UpdateEnumValues(); } - public List Descriptors + public BindableCollection Descriptors { get => _descriptors; set => SetAndNotify(ref _descriptors, value); @@ -33,14 +33,14 @@ namespace Artemis.UI.PropertyInput public LayerBrushDescriptor SelectedDescriptor { - get => Descriptors.FirstOrDefault(d => d.LayerBrushProvider.PluginInfo.Guid == InputValue?.BrushPluginGuid && d.LayerBrushType.Name == InputValue?.BrushType); + get => Descriptors.FirstOrDefault(d => d.MatchesLayerBrushReference(InputValue)); set => SetBrushByDescriptor(value); } public void UpdateEnumValues() { var layerBrushProviders = _pluginService.GetPluginsOfType(); - Descriptors = layerBrushProviders.SelectMany(l => l.LayerBrushDescriptors).ToList(); + Descriptors = new BindableCollection(layerBrushProviders.SelectMany(l => l.LayerBrushDescriptors)); NotifyOfPropertyChange(nameof(SelectedDescriptor)); } @@ -55,15 +55,12 @@ namespace Artemis.UI.PropertyInput protected override void OnInputValueApplied() { if (LayerProperty.ProfileElement is Layer layer) - { - _renderElementService.RemoveLayerBrush(layer); - _renderElementService.InstantiateLayerBrush(layer); - } + layer.ChangeLayerBrush(SelectedDescriptor); } private void SetBrushByDescriptor(LayerBrushDescriptor value) { - InputValue = new LayerBrushReference {BrushPluginGuid = value.LayerBrushProvider.PluginInfo.Guid, BrushType = value.LayerBrushType.Name}; + InputValue = new LayerBrushReference(value); } private void PluginServiceOnPluginLoaded(object sender, PluginEventArgs e) diff --git a/src/Artemis.UI/ResourceDictionaries/LayerBrushDescriptors.xaml b/src/Artemis.UI/ResourceDictionaries/LayerBrushDescriptors.xaml new file mode 100644 index 000000000..059b43857 --- /dev/null +++ b/src/Artemis.UI/ResourceDictionaries/LayerBrushDescriptors.xaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Modules/ModuleRootViewModel.cs b/src/Artemis.UI/Screens/Modules/ModuleRootViewModel.cs index 93c6985d0..6437a5cd5 100644 --- a/src/Artemis.UI/Screens/Modules/ModuleRootViewModel.cs +++ b/src/Artemis.UI/Screens/Modules/ModuleRootViewModel.cs @@ -29,7 +29,7 @@ namespace Artemis.UI.Screens.Modules AddTabs(); base.OnActivate(); } - + private void AddTabs() { // Create the profile editor and module VMs diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Abstract/DisplayConditionViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Abstract/DisplayConditionViewModel.cs index 743fec551..65565fdb2 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Abstract/DisplayConditionViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Abstract/DisplayConditionViewModel.cs @@ -1,44 +1,23 @@ -using System; -using Artemis.Core; +using Artemis.Core; using Stylet; namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions.Abstract { - public abstract class DisplayConditionViewModel : PropertyChangedBase, IDisposable + public abstract class DisplayConditionViewModel : Conductor.Collection.AllActive { - protected DisplayConditionViewModel(DisplayConditionPart model, DisplayConditionViewModel parent) + protected DisplayConditionViewModel(DisplayConditionPart model) { Model = model; - Parent = parent; - Children = new BindableCollection(); } public DisplayConditionPart Model { get; } - public DisplayConditionViewModel Parent { get; set; } - public BindableCollection Children { get; } - - public void Dispose() - { - foreach (var child in Children) - child.Dispose(); - - Dispose(true); - GC.SuppressFinalize(this); - } public abstract void Update(); public virtual void Delete() { Model.Parent.RemoveChild(Model); - Parent.Update(); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - } + ((DisplayConditionViewModel) Parent).Update(); } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionGroupView.xaml b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionGroupView.xaml index b0ec8a679..f1a770d08 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionGroupView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionGroupView.xaml @@ -124,7 +124,7 @@ - + diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionGroupViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionGroupViewModel.cs index 1f1deae4d..a5caf78dd 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionGroupViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionGroupViewModel.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using System.Threading.Tasks; -using System.Windows; using Artemis.Core; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.ProfileEditor.DisplayConditions.Abstract; @@ -11,21 +10,24 @@ using Stylet; namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions { - public class DisplayConditionGroupViewModel : DisplayConditionViewModel, IViewAware + public class DisplayConditionGroupViewModel : DisplayConditionViewModel { private readonly IDisplayConditionsVmFactory _displayConditionsVmFactory; private readonly IProfileEditorService _profileEditorService; private bool _isInitialized; private bool _isRootGroup; - public DisplayConditionGroupViewModel(DisplayConditionGroup displayConditionGroup, DisplayConditionViewModel parent, bool isListGroup, - IProfileEditorService profileEditorService, IDisplayConditionsVmFactory displayConditionsVmFactory) : base(displayConditionGroup, parent) + public DisplayConditionGroupViewModel(DisplayConditionGroup displayConditionGroup, + bool isListGroup, + IProfileEditorService profileEditorService, + IDisplayConditionsVmFactory displayConditionsVmFactory) + : base(displayConditionGroup) { IsListGroup = isListGroup; _profileEditorService = profileEditorService; _displayConditionsVmFactory = displayConditionsVmFactory; - Children.CollectionChanged += (sender, args) => NotifyOfPropertyChange(nameof(DisplayBooleanOperator)); + Items.CollectionChanged += (sender, args) => NotifyOfPropertyChange(nameof(DisplayBooleanOperator)); Execute.PostToUIThread(async () => { @@ -50,16 +52,9 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions set => SetAndNotify(ref _isInitialized, value); } - public bool DisplayBooleanOperator => Children.Count > 1; + public bool DisplayBooleanOperator => Items.Count > 1; public string SelectedBooleanOperator => DisplayConditionGroup.BooleanOperator.Humanize(); - public void AttachView(UIElement view) - { - View = view; - } - - public UIElement View { get; set; } - public void SelectBooleanOperator(string type) { var enumValue = Enum.Parse(type); @@ -105,39 +100,36 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions NotifyOfPropertyChange(nameof(SelectedBooleanOperator)); // Remove VMs of effects no longer applied on the layer - var toRemove = Children.Where(c => !DisplayConditionGroup.Children.Contains(c.Model)).ToList(); + var toRemove = Items.Where(c => !DisplayConditionGroup.Children.Contains(c.Model)).ToList(); // Using RemoveRange breaks our lovely animations foreach (var displayConditionViewModel in toRemove) - { - Children.Remove(displayConditionViewModel); - displayConditionViewModel.Dispose(); - } + CloseItem(displayConditionViewModel); foreach (var childModel in Model.Children) { - if (Children.Any(c => c.Model == childModel)) + if (Items.Any(c => c.Model == childModel)) continue; switch (childModel) { case DisplayConditionGroup displayConditionGroup: - Children.Add(_displayConditionsVmFactory.DisplayConditionGroupViewModel(displayConditionGroup, this, IsListGroup)); + ActivateItem(_displayConditionsVmFactory.DisplayConditionGroupViewModel(displayConditionGroup, IsListGroup)); break; case DisplayConditionList displayConditionListPredicate: - Children.Add(_displayConditionsVmFactory.DisplayConditionListViewModel(displayConditionListPredicate, this)); + ActivateItem(_displayConditionsVmFactory.DisplayConditionListViewModel(displayConditionListPredicate)); break; case DisplayConditionPredicate displayConditionPredicate: if (!IsListGroup) - Children.Add(_displayConditionsVmFactory.DisplayConditionPredicateViewModel(displayConditionPredicate, this)); + ActivateItem(_displayConditionsVmFactory.DisplayConditionPredicateViewModel(displayConditionPredicate)); break; case DisplayConditionListPredicate displayConditionListPredicate: if (IsListGroup) - Children.Add(_displayConditionsVmFactory.DisplayConditionListPredicateViewModel(displayConditionListPredicate, this)); + ActivateItem(_displayConditionsVmFactory.DisplayConditionListPredicateViewModel(displayConditionListPredicate)); break; } } - foreach (var childViewModel in Children) + foreach (var childViewModel in Items) childViewModel.Update(); } } diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListPredicateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListPredicateViewModel.cs index e02fadd81..07738d392 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListPredicateViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListPredicateViewModel.cs @@ -17,38 +17,37 @@ using Stylet; namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions { - public class DisplayConditionListPredicateViewModel : DisplayConditionViewModel, IHandle, IHandle + public class DisplayConditionListPredicateViewModel : DisplayConditionViewModel, IHandle, IHandle, IDisposable { - private readonly IDataModelService _dataModelService; + private readonly IConditionOperatorService _conditionOperatorService; private readonly IDataModelUIService _dataModelUIService; private readonly IEventAggregator _eventAggregator; private readonly IProfileEditorService _profileEditorService; private readonly Timer _updateTimer; private bool _isInitialized; private DataModelVisualizationViewModel _leftSideDataModel; - private BindableCollection _operators; + private BindableCollection _operators; private DataModelVisualizationViewModel _rightSideDataModel; private DataModelInputViewModel _rightSideInputViewModel; private int _rightSideTransitionIndex; private object _rightStaticValue; private DataModelVisualizationViewModel _selectedLeftSideProperty; - private DisplayConditionOperator _selectedOperator; + private ConditionOperator _selectedOperator; private DataModelVisualizationViewModel _selectedRightSideProperty; private List _supportedInputTypes; public DisplayConditionListPredicateViewModel( DisplayConditionListPredicate displayConditionListPredicate, - DisplayConditionViewModel parent, IProfileEditorService profileEditorService, IDataModelUIService dataModelUIService, - IDataModelService dataModelService, + IConditionOperatorService conditionOperatorService, ISettingsService settingsService, - IEventAggregator eventAggregator) : base(displayConditionListPredicate, parent) + IEventAggregator eventAggregator) : base(displayConditionListPredicate) { _profileEditorService = profileEditorService; _dataModelUIService = dataModelUIService; - _dataModelService = dataModelService; + _conditionOperatorService = conditionOperatorService; _eventAggregator = eventAggregator; _updateTimer = new Timer(500); _supportedInputTypes = new List(); @@ -56,7 +55,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions SelectLeftPropertyCommand = new DelegateCommand(ExecuteSelectLeftProperty); SelectRightPropertyCommand = new DelegateCommand(ExecuteSelectRightProperty); SelectOperatorCommand = new DelegateCommand(ExecuteSelectOperatorCommand); - Operators = new BindableCollection(); + Operators = new BindableCollection(); ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues"); @@ -125,13 +124,13 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions set => SetAndNotify(ref _rightSideInputViewModel, value); } - public BindableCollection Operators + public BindableCollection Operators { get => _operators; set => SetAndNotify(ref _operators, value); } - public DisplayConditionOperator SelectedOperator + public ConditionOperator SelectedOperator { get => _selectedOperator; set => SetAndNotify(ref _selectedOperator, value); @@ -206,7 +205,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions // Get the supported operators Operators.Clear(); - Operators.AddRange(_dataModelService.GetCompatibleConditionOperators(leftSideType)); + Operators.AddRange(_conditionOperatorService.GetConditionOperatorsForType(leftSideType)); if (DisplayConditionListPredicate.Operator == null) DisplayConditionListPredicate.UpdateOperator(Operators.FirstOrDefault(o => o.SupportsType(leftSideType))); SelectedOperator = DisplayConditionListPredicate.Operator; @@ -277,12 +276,6 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions _eventAggregator.Subscribe(this); } - protected override void Dispose(bool disposing) - { - _updateTimer.Stop(); - _updateTimer.Elapsed -= OnUpdateTimerOnElapsed; - } - private void OnUpdateTimerOnElapsed(object sender, ElapsedEventArgs e) { if (LeftSideDataModelOpen) @@ -315,10 +308,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions if (DisplayConditionListPredicate.ListDataModel == null || DisplayConditionListPredicate.ListPropertyPath == null) throw new ArtemisUIException("Cannot create a list predicate without first selecting a target list"); - var dataModel = _dataModelUIService.GetMainDataModelVisualization(); - if (!_dataModelUIService.GetPluginExtendsDataModel(_profileEditorService.GetCurrentModule())) - dataModel.Children.Add(_dataModelUIService.GetPluginDataModelVisualization(_profileEditorService.GetCurrentModule())); - + var dataModel = _dataModelUIService.GetPluginDataModelVisualization(_profileEditorService.GetCurrentModule(), true); var listDataModel = (DataModelListViewModel) dataModel.GetChildByPath( DisplayConditionListPredicate.ListDataModel.PluginInfo.Guid, DisplayConditionListPredicate.ListPropertyPath @@ -347,11 +337,17 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions private void ExecuteSelectOperatorCommand(object context) { - if (!(context is DisplayConditionOperator displayConditionOperator)) + if (!(context is ConditionOperator displayConditionOperator)) return; SelectedOperator = displayConditionOperator; ApplyOperator(); } + + public void Dispose() + { + _updateTimer.Dispose(); + _updateTimer.Elapsed -= OnUpdateTimerOnElapsed; + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListView.xaml b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListView.xaml index 3ed7ca5d1..634955989 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListView.xaml @@ -105,7 +105,7 @@ - + diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListViewModel.cs index a2ab2fb6f..f627d87bc 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionListViewModel.cs @@ -14,7 +14,7 @@ using Humanizer; namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions { - public class DisplayConditionListViewModel : DisplayConditionViewModel + public class DisplayConditionListViewModel : DisplayConditionViewModel, IDisposable { private readonly IDataModelUIService _dataModelUIService; private readonly IDisplayConditionsVmFactory _displayConditionsVmFactory; @@ -30,7 +30,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions IProfileEditorService profileEditorService, IDataModelUIService dataModelUIService, IDisplayConditionsVmFactory displayConditionsVmFactory, - ISettingsService settingsService) : base(displayConditionList, parent) + ISettingsService settingsService) : base(displayConditionList) { _profileEditorService = profileEditorService; _dataModelUIService = dataModelUIService; @@ -109,10 +109,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions public void Initialize() { // Get the data models - TargetDataModel = _dataModelUIService.GetMainDataModelVisualization(); - if (!_dataModelUIService.GetPluginExtendsDataModel(_profileEditorService.GetCurrentModule())) - TargetDataModel.Children.Add(_dataModelUIService.GetPluginDataModelVisualization(_profileEditorService.GetCurrentModule())); - + TargetDataModel = _dataModelUIService.GetPluginDataModelVisualization(_profileEditorService.GetCurrentModule(), true); TargetDataModel.UpdateRequested += TargetDataModelUpdateRequested; Update(); @@ -152,36 +149,27 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions TargetDataModel.ApplyTypeFilter(true, typeof(IList)); // Remove VMs of effects no longer applied on the layer - var toRemove = Children.Where(c => !DisplayConditionList.Children.Contains(c.Model)).ToList(); + var toRemove = Items.Where(c => !DisplayConditionList.Children.Contains(c.Model)).ToList(); // Using RemoveRange breaks our lovely animations foreach (var displayConditionViewModel in toRemove) - { - Children.Remove(displayConditionViewModel); - displayConditionViewModel.Dispose(); - } + CloseItem(displayConditionViewModel); foreach (var childModel in Model.Children) { - if (Children.Any(c => c.Model == childModel)) + if (Items.Any(c => c.Model == childModel)) continue; if (!(childModel is DisplayConditionGroup displayConditionGroup)) continue; - var viewModel = _displayConditionsVmFactory.DisplayConditionGroupViewModel(displayConditionGroup, this, true); + var viewModel = _displayConditionsVmFactory.DisplayConditionGroupViewModel(displayConditionGroup, true); viewModel.IsRootGroup = true; - Children.Add(viewModel); + ActivateItem(viewModel); } - foreach (var childViewModel in Children) + foreach (var childViewModel in Items) childViewModel.Update(); } - protected override void Dispose(bool disposing) - { - _updateTimer.Stop(); - _updateTimer.Elapsed -= OnUpdateTimerOnElapsed; - } - private void OnUpdateTimerOnElapsed(object sender, ElapsedEventArgs e) { if (TargetDataModelOpen) @@ -205,5 +193,11 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions SelectedListProperty = dataModelListViewModel; ApplyList(); } + + public void Dispose() + { + _updateTimer.Dispose(); + _updateTimer.Elapsed -= OnUpdateTimerOnElapsed; + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionPredicateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionPredicateViewModel.cs index 2b38b9c51..645237a34 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionPredicateViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionPredicateViewModel.cs @@ -16,38 +16,37 @@ using Stylet; namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions { - public class DisplayConditionPredicateViewModel : DisplayConditionViewModel, IHandle, IHandle + public class DisplayConditionPredicateViewModel : DisplayConditionViewModel, IHandle, IHandle, IDisposable { - private readonly IDataModelService _dataModelService; + private readonly IConditionOperatorService _conditionOperatorService; private readonly IDataModelUIService _dataModelUIService; private readonly IEventAggregator _eventAggregator; private readonly IProfileEditorService _profileEditorService; private readonly Timer _updateTimer; private bool _isInitialized; private DataModelPropertiesViewModel _leftSideDataModel; - private BindableCollection _operators; + private BindableCollection _operators; private DataModelPropertiesViewModel _rightSideDataModel; private DataModelInputViewModel _rightSideInputViewModel; private int _rightSideTransitionIndex; private object _rightStaticValue; private DataModelVisualizationViewModel _selectedLeftSideProperty; - private DisplayConditionOperator _selectedOperator; + private ConditionOperator _selectedOperator; private DataModelVisualizationViewModel _selectedRightSideProperty; private List _supportedInputTypes; public DisplayConditionPredicateViewModel( DisplayConditionPredicate displayConditionPredicate, - DisplayConditionViewModel parent, IProfileEditorService profileEditorService, IDataModelUIService dataModelUIService, - IDataModelService dataModelService, + IConditionOperatorService conditionOperatorService, ISettingsService settingsService, - IEventAggregator eventAggregator) : base(displayConditionPredicate, parent) + IEventAggregator eventAggregator) : base(displayConditionPredicate) { _profileEditorService = profileEditorService; _dataModelUIService = dataModelUIService; - _dataModelService = dataModelService; + _conditionOperatorService = conditionOperatorService; _eventAggregator = eventAggregator; _updateTimer = new Timer(500); _supportedInputTypes = new List(); @@ -55,7 +54,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions SelectLeftPropertyCommand = new DelegateCommand(ExecuteSelectLeftProperty); SelectRightPropertyCommand = new DelegateCommand(ExecuteSelectRightProperty); SelectOperatorCommand = new DelegateCommand(ExecuteSelectOperatorCommand); - Operators = new BindableCollection(); + Operators = new BindableCollection(); ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues"); @@ -124,13 +123,13 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions set => SetAndNotify(ref _rightSideInputViewModel, value); } - public BindableCollection Operators + public BindableCollection Operators { get => _operators; set => SetAndNotify(ref _operators, value); } - public DisplayConditionOperator SelectedOperator + public ConditionOperator SelectedOperator { get => _selectedOperator; set => SetAndNotify(ref _selectedOperator, value); @@ -169,14 +168,9 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions public void Initialize() { // Get the data models - LeftSideDataModel = _dataModelUIService.GetMainDataModelVisualization(); - RightSideDataModel = _dataModelUIService.GetMainDataModelVisualization(); - if (!_dataModelUIService.GetPluginExtendsDataModel(_profileEditorService.GetCurrentModule())) - { - LeftSideDataModel.Children.Add(_dataModelUIService.GetPluginDataModelVisualization(_profileEditorService.GetCurrentModule())); - RightSideDataModel.Children.Add(_dataModelUIService.GetPluginDataModelVisualization(_profileEditorService.GetCurrentModule())); - } - + LeftSideDataModel = _dataModelUIService.GetPluginDataModelVisualization(_profileEditorService.GetCurrentModule(), true); + RightSideDataModel = _dataModelUIService.GetPluginDataModelVisualization(_profileEditorService.GetCurrentModule(), true); + // Determine which types are currently supported var editors = _dataModelUIService.RegisteredDataModelEditors; _supportedInputTypes = editors.Select(e => e.SupportedType).ToList(); @@ -208,7 +202,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions // Get the supported operators Operators.Clear(); - Operators.AddRange(_dataModelService.GetCompatibleConditionOperators(leftSideType)); + Operators.AddRange(_conditionOperatorService.GetConditionOperatorsForType(leftSideType)); if (DisplayConditionPredicate.Operator == null) DisplayConditionPredicate.UpdateOperator(Operators.FirstOrDefault(o => o.SupportsType(leftSideType))); SelectedOperator = DisplayConditionPredicate.Operator; @@ -278,13 +272,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions ); _eventAggregator.Subscribe(this); } - - protected override void Dispose(bool disposing) - { - _updateTimer.Stop(); - _updateTimer.Elapsed -= OnUpdateTimerOnElapsed; - } - + private void OnUpdateTimerOnElapsed(object sender, ElapsedEventArgs e) { if (LeftSideDataModelOpen) @@ -329,11 +317,17 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions private void ExecuteSelectOperatorCommand(object context) { - if (!(context is DisplayConditionOperator displayConditionOperator)) + if (!(context is ConditionOperator displayConditionOperator)) return; SelectedOperator = displayConditionOperator; ApplyOperator(); } + + public void Dispose() + { + _updateTimer.Dispose(); + _updateTimer.Elapsed -= OnUpdateTimerOnElapsed; + } } } \ 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 43ca3526b..68c8ef8f9 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml @@ -43,7 +43,7 @@ - + diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs index 2931a2907..5392d0f52 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs @@ -3,17 +3,15 @@ using Artemis.Core; using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; +using Stylet; namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions { - public class DisplayConditionsViewModel : ProfileEditorPanelViewModel + public class DisplayConditionsViewModel : Conductor, IProfileEditorPanelViewModel { private readonly IDisplayConditionsVmFactory _displayConditionsVmFactory; private readonly IProfileEditorService _profileEditorService; - private bool _alwaysFinishTimeline; - private bool _displayContinuously; private RenderProfileElement _renderProfileElement; - private DisplayConditionGroupViewModel _rootGroup; private int _transitionerIndex; public DisplayConditionsViewModel(IProfileEditorService profileEditorService, IDisplayConditionsVmFactory displayConditionsVmFactory) @@ -28,11 +26,6 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions set => SetAndNotify(ref _transitionerIndex, value); } - public DisplayConditionGroupViewModel RootGroup - { - get => _rootGroup; - set => SetAndNotify(ref _rootGroup, value); - } public RenderProfileElement RenderProfileElement { @@ -42,20 +35,22 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions public bool DisplayContinuously { - get => _displayContinuously; + get => RenderProfileElement?.DisplayContinuously ?? false; set { - if (!SetAndNotify(ref _displayContinuously, value)) return; + if (RenderProfileElement == null || RenderProfileElement.DisplayContinuously == value) return; + RenderProfileElement.DisplayContinuously = value; _profileEditorService.UpdateSelectedProfileElement(); } } public bool AlwaysFinishTimeline { - get => _alwaysFinishTimeline; + get => RenderProfileElement?.AlwaysFinishTimeline ?? false; set { - if (!SetAndNotify(ref _alwaysFinishTimeline, value)) return; + if (RenderProfileElement == null || RenderProfileElement.AlwaysFinishTimeline == value) return; + RenderProfileElement.AlwaysFinishTimeline = value; _profileEditorService.UpdateSelectedProfileElement(); } } @@ -70,25 +65,18 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions protected override void OnDeactivate() { _profileEditorService.ProfileElementSelected -= ProfileEditorServiceOnProfileElementSelected; - - RootGroup?.Dispose(); - RootGroup = null; } private void ProfileEditorServiceOnProfileElementSelected(object sender, RenderProfileElementEventArgs e) { RenderProfileElement = e.RenderProfileElement; - NotifyOfPropertyChange(nameof(ConditionBehaviourEnabled)); - - _displayContinuously = RenderProfileElement?.DisplayContinuously ?? false; NotifyOfPropertyChange(nameof(DisplayContinuously)); - _alwaysFinishTimeline = RenderProfileElement?.AlwaysFinishTimeline ?? false; NotifyOfPropertyChange(nameof(AlwaysFinishTimeline)); + NotifyOfPropertyChange(nameof(ConditionBehaviourEnabled)); if (e.RenderProfileElement == null) { - RootGroup?.Dispose(); - RootGroup = null; + ActiveItem = null; return; } @@ -96,14 +84,13 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions if (e.RenderProfileElement.DisplayConditionGroup == null) e.RenderProfileElement.DisplayConditionGroup = new DisplayConditionGroup(null); - RootGroup?.Dispose(); - RootGroup = _displayConditionsVmFactory.DisplayConditionGroupViewModel(e.RenderProfileElement.DisplayConditionGroup, null, false); - RootGroup.IsRootGroup = true; - RootGroup.Update(); + ActiveItem = _displayConditionsVmFactory.DisplayConditionGroupViewModel(e.RenderProfileElement.DisplayConditionGroup, false); + ActiveItem.IsRootGroup = true; + ActiveItem.Update(); // Only show the intro to conditions once, and only if the layer has no conditions if (TransitionerIndex != 1) - TransitionerIndex = RootGroup.Children.Any() ? 1 : 0; + TransitionerIndex = ActiveItem.Items.Any() ? 1 : 0; } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorPanelViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/IProfileEditorPanelViewModel.cs similarity index 55% rename from src/Artemis.UI/Screens/ProfileEditor/ProfileEditorPanelViewModel.cs rename to src/Artemis.UI/Screens/ProfileEditor/IProfileEditorPanelViewModel.cs index e2829cefe..cf5601e85 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorPanelViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/IProfileEditorPanelViewModel.cs @@ -2,7 +2,7 @@ namespace Artemis.UI.Screens.ProfileEditor { - public class ProfileEditorPanelViewModel : Screen + public interface IProfileEditorPanelViewModel : IScreen { } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Abstract/LayerPropertyBaseViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Abstract/LayerPropertyBaseViewModel.cs deleted file mode 100644 index 96278e0c3..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Abstract/LayerPropertyBaseViewModel.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using Artemis.Core; -using Stylet; - -namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Abstract -{ - public abstract class LayerPropertyBaseViewModel : PropertyChangedBase, IDisposable - { - private BindableCollection _children; - private bool _isExpanded; - - protected LayerPropertyBaseViewModel() - { - Children = new BindableCollection(); - } - - public abstract bool IsVisible { get; } - - public virtual bool IsExpanded - { - get => _isExpanded; - set => SetAndNotify(ref _isExpanded, value); - } - - public BindableCollection Children - { - get => _children; - set => SetAndNotify(ref _children, value); - } - - public abstract void Dispose(); - - public abstract List GetKeyframes(bool expandedOnly); - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierView.xaml index a3aaf5c56..48f6007e6 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierView.xaml @@ -18,14 +18,6 @@ - - - - - - - - @@ -62,7 +54,6 @@ Style="{StaticResource DisplayConditionButtonLeftClickMenu}" Background="#7B7B7B" BorderBrush="#7B7B7B" - Content="{Binding SelectedModifierType.Description}" Click="PropertyButton_OnClick"> @@ -83,6 +74,15 @@ + + + + « Select a modifier » + + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierViewModel.cs index 58849824e..431185848 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingModifierViewModel.cs @@ -1,5 +1,4 @@ using System; -using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Exceptions; @@ -10,16 +9,16 @@ using Stylet; namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings { - public class DataBindingModifierViewModel : PropertyChangedBase + public class DataBindingModifierViewModel : PropertyChangedBase, IDisposable { private readonly IDataBindingService _dataBindingService; private readonly IDataModelUIService _dataModelUIService; private readonly IProfileEditorService _profileEditorService; - private DataBindingModifierType _selectedModifierType; private DataModelDynamicViewModel _dynamicSelectionViewModel; + private DataBindingModifierType _selectedModifierType; private DataModelStaticViewModel _staticInputViewModel; - public DataBindingModifierViewModel(DataBindingModifier modifier, + public DataBindingModifierViewModel(DataBindingModifier modifier, IDataBindingService dataBindingService, ISettingsService settingsService, IDataModelUIService dataModelUIService, @@ -36,13 +35,12 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings SelectModifierTypeCommand = new DelegateCommand(ExecuteSelectModifierTypeCommand); - // Initialize async, no need to wait for it - Execute.PostToUIThread(Update); + Update(); } public DelegateCommand SelectModifierTypeCommand { get; } public PluginSetting ShowDataModelValues { get; } - public DataBindingModifier Modifier { get; } + public DataBindingModifier Modifier { get; } public BindableCollection ModifierTypes { get; } public DataBindingModifierType SelectedModifierType @@ -62,7 +60,22 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings get => _staticInputViewModel; private set => SetAndNotify(ref _staticInputViewModel, value); } - + + public void Delete() + { + Modifier.DataBinding.RemoveModifier(Modifier); + } + + public void SwapType() + { + if (Modifier.ParameterType == ProfileRightSideType.Dynamic) + Modifier.UpdateParameter(Modifier.DataBinding.GetSourceType().GetDefault()); + else + Modifier.UpdateParameter(null, null); + + Update(); + } + private void ParameterSelectionViewModelOnPropertySelected(object sender, DataModelInputDynamicEventArgs e) { Modifier.UpdateParameter(e.DataModelVisualizationViewModel.DataModel, e.DataModelVisualizationViewModel.PropertyPath); @@ -79,6 +92,12 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings if (sourceType == null) throw new ArtemisUIException("Cannot use a data binding modifier VM for a data binding without a source"); + if (DynamicSelectionViewModel != null) + { + DynamicSelectionViewModel.Dispose(); + DynamicSelectionViewModel.PropertySelected -= ParameterSelectionViewModelOnPropertySelected; + } + if (Modifier.ModifierType == null || !Modifier.ModifierType.SupportsParameter) { StaticInputViewModel = null; @@ -88,14 +107,22 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings { StaticInputViewModel = null; DynamicSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule()); - DynamicSelectionViewModel.PropertySelected += ParameterSelectionViewModelOnPropertySelected; - DynamicSelectionViewModel.FilterTypes = new[] { sourceType }; + if (DynamicSelectionViewModel != null) + { + DynamicSelectionViewModel.PropertySelected += ParameterSelectionViewModelOnPropertySelected; + DynamicSelectionViewModel.FilterTypes = new[] {sourceType}; + } } else { DynamicSelectionViewModel = null; - StaticInputViewModel = _dataModelUIService.GetStaticInputViewModel(sourceType); - StaticInputViewModel.ValueUpdated += StaticInputViewModelOnValueUpdated; + if (Modifier.ModifierType.PreferredParameterType != null && sourceType.IsCastableFrom(Modifier.ModifierType.PreferredParameterType)) + StaticInputViewModel = _dataModelUIService.GetStaticInputViewModel(Modifier.ModifierType.PreferredParameterType); + else + StaticInputViewModel = _dataModelUIService.GetStaticInputViewModel(sourceType); + + if (StaticInputViewModel != null) + StaticInputViewModel.ValueUpdated += StaticInputViewModelOnValueUpdated; } // Modifier type @@ -105,7 +132,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings // Parameter if (DynamicSelectionViewModel != null) - DynamicSelectionViewModel?.PopulateSelectedPropertyViewModel(Modifier.ParameterDataModel, Modifier.ParameterPropertyPath); + DynamicSelectionViewModel.PopulateSelectedPropertyViewModel(Modifier.ParameterDataModel, Modifier.ParameterPropertyPath); else if (StaticInputViewModel != null) StaticInputViewModel.Value = Modifier.ParameterStaticValue; } @@ -121,19 +148,13 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings Update(); } - public void Delete() + public void Dispose() { - Modifier.DataBinding.RemoveModifier(Modifier); - } - - public void SwapType() - { - if (Modifier.ParameterType == ProfileRightSideType.Dynamic) - Modifier.UpdateParameter(Modifier.DataBinding.GetSourceType().GetDefault()); - else - Modifier.UpdateParameter(null, null); - - Update(); + if (DynamicSelectionViewModel != null) + { + DynamicSelectionViewModel.Dispose(); + DynamicSelectionViewModel.PropertySelected -= ParameterSelectionViewModelOnPropertySelected; + } } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml index 0afdbd491..12554e038 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml @@ -4,14 +4,10 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" - xmlns:converters="clr-namespace:Artemis.UI.Converters" - xmlns:utilities="clr-namespace:Artemis.UI.Utilities" xmlns:dd="urn:gong-wpf-dragdrop" xmlns:s="https://github.com/canton7/Stylet" - xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings" mc:Ignorable="d" - d:DesignHeight="450" d:DesignWidth="800" - d:DataContext="{d:DesignInstance local:DataBindingViewModel}"> + d:DesignHeight="450" d:DesignWidth="800"> @@ -130,7 +126,7 @@ FontSize="12" Padding="6 4" Height="22" - IsEnabled="{Binding IsDataBindingEnabled}" + IsEnabled="{Binding CanAddModifier}" Command="{s:Action AddModifier}"> diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs index 6397b9437..3ee2cda64 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs @@ -6,27 +6,29 @@ using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline; using Artemis.UI.Shared; using Artemis.UI.Shared.Input; using Artemis.UI.Shared.Services; +using Ninject.Planning.Targets; using Stylet; namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings { - public class DataBindingViewModel : PropertyChangedBase, IDisposable + public class DataBindingViewModel : Screen, IDataBindingViewModel { private readonly IDataBindingsVmFactory _dataBindingsVmFactory; private readonly IDataModelUIService _dataModelUIService; private readonly IProfileEditorService _profileEditorService; - private DataBinding _dataBinding; + private DataBinding _dataBinding; private int _easingTime; private bool _isDataBindingEnabled; private bool _isEasingTimeEnabled; private DataBindingMode _selectedDataBindingMode; private TimelineEasingViewModel _selectedEasingViewModel; private DataModelDynamicViewModel _targetSelectionViewModel; - private object _testInputValue; - private object _testResultValue; + private TProperty _testInputValue; + private TProperty _testResultValue; private bool _updating; + private bool _canAddModifier; - public DataBindingViewModel(DataBindingRegistration registration, + public DataBindingViewModel(DataBindingRegistration registration, IProfileEditorService profileEditorService, IDataModelUIService dataModelUIService, IDataBindingsVmFactory dataBindingsVmFactory) @@ -36,11 +38,14 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings _dataModelUIService = dataModelUIService; _dataBindingsVmFactory = dataBindingsVmFactory; - DisplayName = Registration.Property.Name.ToUpper(); + if (Registration.Member != null) + DisplayName = Registration.Member.Name.ToUpper(); + else + DisplayName = Registration.LayerProperty.PropertyDescription.Name.ToUpper(); DataBindingModes = new BindableCollection(EnumUtilities.GetAllValuesAndDescriptions(typeof(DataBindingMode))); EasingViewModels = new BindableCollection(); - ModifierViewModels = new BindableCollection(); + ModifierViewModels = new BindableCollection>(); DataBinding = Registration.DataBinding; if (DataBinding != null) @@ -48,16 +53,14 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings _isDataBindingEnabled = DataBinding != null; - // Initialize async, no need to wait for it - Execute.PostToUIThread(Initialize); + Initialize(); } - public DataBindingRegistration Registration { get; } - public string DisplayName { get; } + public DataBindingRegistration Registration { get; } public BindableCollection DataBindingModes { get; } public BindableCollection EasingViewModels { get; } - public BindableCollection ModifierViewModels { get; } + public BindableCollection> ModifierViewModels { get; } public DataBindingMode SelectedDataBindingMode { @@ -114,7 +117,13 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings } } - public DataBinding DataBinding + public bool CanAddModifier + { + get => _canAddModifier; + private set => SetAndNotify(ref _canAddModifier, value); + } + + public DataBinding DataBinding { get => _dataBinding; set @@ -124,13 +133,13 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings } } - public object TestInputValue + public TProperty TestInputValue { get => _testInputValue; set => SetAndNotify(ref _testInputValue, value); } - public object TestResultValue + public TProperty TestResultValue { get => _testResultValue; set => SetAndNotify(ref _testResultValue, value); @@ -167,7 +176,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings if (DataBinding == null) return; - var modifier = new DataBindingModifier(ProfileRightSideType.Dynamic); + var modifier = new DataBindingModifier(DataBinding, ProfileRightSideType.Dynamic); DataBinding.AddModifier(modifier); _profileEditorService.UpdateSelectedProfileElement(); @@ -175,7 +184,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings private void Initialize() { - EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)).Cast().Select(v => new TimelineEasingViewModel(null, v))); + EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)).Cast().Select(v => new TimelineEasingViewModel(v, false))); TargetSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule()); TargetSelectionViewModel.PropertySelected += TargetSelectionViewModelOnPropertySelected; _profileEditorService.ProfilePreviewUpdated += ProfileEditorServiceOnProfilePreviewUpdated; @@ -188,6 +197,8 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings if (_updating) return; + CanAddModifier = IsDataBindingEnabled && DataBinding.SourceDataModel != null; + if (DataBinding == null) { TargetSelectionViewModel.IsEnabled = false; @@ -204,7 +215,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings TargetSelectionViewModel.IsEnabled = true; TargetSelectionViewModel.PopulateSelectedPropertyViewModel(DataBinding.SourceDataModel, DataBinding.SourcePropertyPath); - TargetSelectionViewModel.FilterTypes = new[] {DataBinding.TargetProperty.PropertyType}; + TargetSelectionViewModel.FilterTypes = new[] {DataBinding.GetTargetType()}; UpdateModifierViewModels(); @@ -216,9 +227,12 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings if (_updating) return; - DataBinding.Mode = SelectedDataBindingMode; - DataBinding.EasingTime = TimeSpan.FromMilliseconds(EasingTime); - DataBinding.EasingFunction = SelectedEasingViewModel?.EasingFunction ?? Easings.Functions.Linear; + if (DataBinding != null) + { + DataBinding.Mode = SelectedDataBindingMode; + DataBinding.EasingTime = TimeSpan.FromMilliseconds(EasingTime); + DataBinding.EasingFunction = SelectedEasingViewModel?.EasingFunction ?? Easings.Functions.Linear; + } _profileEditorService.UpdateSelectedProfileElement(); Update(); @@ -226,17 +240,30 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings private void UpdateTestResult() { - var currentValue = TargetSelectionViewModel.SelectedPropertyViewModel?.GetCurrentValue(); - if (currentValue == null && Registration.Property.PropertyType.IsValueType) - currentValue = Activator.CreateInstance(Registration.Property.PropertyType); + if (DataBinding == null) + { + TestInputValue = default; + TestResultValue = default; + return; + } - TestInputValue = Convert.ChangeType(currentValue, Registration.Property.PropertyType); - TestResultValue = DataBinding?.GetValue(TestInputValue); + var currentValue = TargetSelectionViewModel.SelectedPropertyViewModel?.GetCurrentValue(); + if (currentValue == null) + currentValue = default(TProperty); + + TestInputValue = Registration.Converter.ConvertFromObject(currentValue); + if (DataBinding != null) + TestResultValue = DataBinding.GetValue(TestInputValue); + else + TestInputValue = default; } private void UpdateModifierViewModels() { + foreach (var dataBindingModifierViewModel in ModifierViewModels) + dataBindingModifierViewModel.Dispose(); ModifierViewModels.Clear(); + if (DataBinding == null) return; @@ -257,6 +284,8 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings private void TargetSelectionViewModelOnPropertySelected(object sender, DataModelInputDynamicEventArgs e) { DataBinding.UpdateSource(e.DataModelVisualizationViewModel.DataModel, e.DataModelVisualizationViewModel.PropertyPath); + Update(); + _profileEditorService.UpdateSelectedProfileElement(); } @@ -264,8 +293,18 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings { _profileEditorService.ProfilePreviewUpdated -= ProfileEditorServiceOnProfilePreviewUpdated; TargetSelectionViewModel.PropertySelected -= TargetSelectionViewModelOnPropertySelected; + TargetSelectionViewModel.Dispose(); if (DataBinding != null) DataBinding.ModifiersUpdated -= DataBindingOnModifiersUpdated; + + foreach (var dataBindingModifierViewModel in ModifierViewModels) + dataBindingModifierViewModel.Dispose(); + ModifierViewModels.Clear(); + } } + + public interface IDataBindingViewModel : IScreen, IDisposable + { + } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsTabsView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsTabsView.xaml deleted file mode 100644 index 371cdb7a6..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsTabsView.xaml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsTabsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsTabsViewModel.cs deleted file mode 100644 index 18355e690..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsTabsViewModel.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Stylet; - -namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings -{ - public class DataBindingsTabsViewModel : PropertyChangedBase - { - public DataBindingsTabsViewModel() - { - Tabs = new BindableCollection(); - } - - public BindableCollection Tabs { get; set; } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsView.xaml index 625d041b5..35e1c740c 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsView.xaml @@ -3,14 +3,14 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings" xmlns:s="https://github.com/canton7/Stylet" mc:Ignorable="d" - d:DesignHeight="450" d:DesignWidth="800" - d:DataContext="{d:DesignInstance local:DataBindingsViewModel}"> - - - - - + d:DesignHeight="450" d:DesignWidth="800"> + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs index 585abf21f..b9b454236 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs @@ -1,63 +1,58 @@ using System; -using System.Linq; -using Artemis.Core; using Artemis.UI.Ninject.Factories; +using Artemis.UI.Shared.Services; using Stylet; namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings { - public class DataBindingsViewModel : PropertyChangedBase, IDisposable + public class DataBindingsViewModel : Conductor.Collection.AllActive { private readonly IDataBindingsVmFactory _dataBindingsVmFactory; - private DataBindingsTabsViewModel _dataBindingsTabsViewModel; - private DataBindingViewModel _dataBindingViewModel; + private readonly IProfileEditorService _profileEditorService; + private int _selectedItemIndex; - public DataBindingsViewModel(BaseLayerProperty layerProperty, IDataBindingsVmFactory dataBindingsVmFactory) + public DataBindingsViewModel(IProfileEditorService profileEditorService, IDataBindingsVmFactory dataBindingsVmFactory) { + _profileEditorService = profileEditorService; _dataBindingsVmFactory = dataBindingsVmFactory; - LayerProperty = layerProperty; - Initialise(); + + _profileEditorService.SelectedDataBindingChanged += ProfileEditorServiceOnSelectedDataBindingChanged; + CreateDataBindingViewModels(); } - public BaseLayerProperty LayerProperty { get; } - - public DataBindingViewModel DataBindingViewModel + public int SelectedItemIndex { - get => _dataBindingViewModel; - set => SetAndNotify(ref _dataBindingViewModel, value); + get => _selectedItemIndex; + set => SetAndNotify(ref _selectedItemIndex, value); } - public DataBindingsTabsViewModel DataBindingsTabsViewModel + private void CreateDataBindingViewModels() { - get => _dataBindingsTabsViewModel; - set => SetAndNotify(ref _dataBindingsTabsViewModel, value); - } + Items.Clear(); - private void Initialise() - { - DataBindingViewModel?.Dispose(); - DataBindingViewModel = null; - DataBindingsTabsViewModel = null; - - var registrations = LayerProperty.DataBindingRegistrations; - if (registrations == null || registrations.Count == 0) + var layerProperty = _profileEditorService.SelectedDataBinding; + if (layerProperty == null) return; + var registrations = layerProperty.GetAllDataBindingRegistrations(); + // Create a data binding VM for each data bindable property. These VMs will be responsible for retrieving // and creating the actual data bindings - if (registrations.Count == 1) - DataBindingViewModel = _dataBindingsVmFactory.DataBindingViewModel(registrations.First()); - else - { - DataBindingsTabsViewModel = new DataBindingsTabsViewModel(); - foreach (var registration in registrations) - DataBindingsTabsViewModel.Tabs.Add(_dataBindingsVmFactory.DataBindingViewModel(registration)); - } + foreach (var registration in registrations) + ActivateItem(_dataBindingsVmFactory.DataBindingViewModel(registration)); + + SelectedItemIndex = 0; } - public void Dispose() + protected override void OnClose() { - DataBindingViewModel?.Dispose(); + _profileEditorService.SelectedDataBindingChanged -= ProfileEditorServiceOnSelectedDataBindingChanged; + base.OnClose(); + } + + private void ProfileEditorServiceOnSelectedDataBindingChanged(object? sender, EventArgs e) + { + CreateDataBindingViewModels(); } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerEffects/EffectsView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerEffects/EffectsView.xaml index 792b2a58a..2e8460ced 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerEffects/EffectsView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerEffects/EffectsView.xaml @@ -23,7 +23,7 @@ Think of things like blur, black & white but also audio visualization etc. - - - + + - - + + - - \ No newline at end of file + diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineGroupViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineGroupViewModel.cs new file mode 100644 index 000000000..ea75c170b --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineGroupViewModel.cs @@ -0,0 +1,67 @@ +using System; +using System.ComponentModel; +using System.Linq; +using Artemis.Core; +using Artemis.UI.Shared.Services; +using Stylet; + +namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline +{ + public class TimelineGroupViewModel : PropertyChangedBase, IDisposable + { + private readonly IProfileEditorService _profileEditorService; + + public TimelineGroupViewModel(LayerPropertyGroupViewModel layerPropertyGroupViewModel, IProfileEditorService profileEditorService) + { + _profileEditorService = profileEditorService; + + LayerPropertyGroupViewModel = layerPropertyGroupViewModel; + LayerPropertyGroup = LayerPropertyGroupViewModel.LayerPropertyGroup; + KeyframePositions = new BindableCollection(); + + _profileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged; + LayerPropertyGroupViewModel.PropertyChanged += LayerPropertyGroupViewModelOnPropertyChanged; + + UpdateKeyframePositions(); + } + + + public LayerPropertyGroupViewModel LayerPropertyGroupViewModel { get; } + public LayerPropertyGroup LayerPropertyGroup { get; } + + public BindableCollection KeyframePositions { get; } + + public void UpdateKeyframePositions() + { + KeyframePositions.Clear(); + KeyframePositions.AddRange(LayerPropertyGroupViewModel + .GetAllKeyframeViewModels(false) + .Select(p => p.Position.TotalSeconds * _profileEditorService.PixelsPerSecond)); + } + + #region IDisposable + + public void Dispose() + { + _profileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged; + LayerPropertyGroupViewModel.PropertyChanged -= LayerPropertyGroupViewModelOnPropertyChanged; + } + + #endregion + + #region Event handlers + + private void LayerPropertyGroupViewModelOnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(LayerPropertyGroupViewModel.IsExpanded)) + UpdateKeyframePositions(); + } + + private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e) + { + UpdateKeyframePositions(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineKeyframeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineKeyframeViewModel.cs index 34210bdae..4c48afda8 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineKeyframeViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineKeyframeViewModel.cs @@ -7,22 +7,160 @@ using Stylet; namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline { - public class TimelineKeyframeViewModel : TimelineKeyframeViewModel + public class TimelineKeyframeViewModel : Screen, ITimelineKeyframeViewModel { private readonly IProfileEditorService _profileEditorService; - public TimelineKeyframeViewModel(IProfileEditorService profileEditorService, LayerPropertyKeyframe layerPropertyKeyframe) - : base(profileEditorService, layerPropertyKeyframe) + private BindableCollection _easingViewModels; + private bool _isSelected; + private string _timestamp; + private double _x; + + public TimelineKeyframeViewModel(LayerPropertyKeyframe layerPropertyKeyframe, IProfileEditorService profileEditorService) { _profileEditorService = profileEditorService; LayerPropertyKeyframe = layerPropertyKeyframe; + EasingViewModels = new BindableCollection(); + + _profileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged; + LayerPropertyKeyframe.PropertyChanged += LayerPropertyKeyframeOnPropertyChanged; } + public LayerPropertyKeyframe LayerPropertyKeyframe { get; } + public BindableCollection EasingViewModels { get; } + + public double X + { + get => _x; + set => SetAndNotify(ref _x, value); + } + + public string Timestamp + { + get => _timestamp; + set => SetAndNotify(ref _timestamp, value); + } + + public bool IsSelected + { + get => _isSelected; + set => SetAndNotify(ref _isSelected, value); + } + + public TimeSpan Position => LayerPropertyKeyframe.Position; + + public void Dispose() + { + _profileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged; + LayerPropertyKeyframe.PropertyChanged -= LayerPropertyKeyframeOnPropertyChanged; + + foreach (var timelineEasingViewModel in EasingViewModels) + timelineEasingViewModel.EasingModeSelected -= TimelineEasingViewModelOnEasingModeSelected; + } + + public void Update() + { + X = _profileEditorService.PixelsPerSecond * LayerPropertyKeyframe.Position.TotalSeconds; + Timestamp = $"{Math.Floor(LayerPropertyKeyframe.Position.TotalSeconds):00}.{LayerPropertyKeyframe.Position.Milliseconds:000}"; + } + + private void ProfileEditorServiceOnPixelsPerSecondChanged(object? sender, EventArgs e) + { + Update(); + } + + private void LayerPropertyKeyframeOnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(LayerPropertyKeyframe.Position)) + Update(); + } + + #region Easing + + public void PopulateEasingViewModels() + { + if (EasingViewModels.Any()) + return; + + EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)) + .Cast() + .Select(e => new TimelineEasingViewModel(e, e == LayerPropertyKeyframe.EasingFunction))); + + foreach (var timelineEasingViewModel in EasingViewModels) + timelineEasingViewModel.EasingModeSelected += TimelineEasingViewModelOnEasingModeSelected; + } + + public void ClearEasingViewModels() + { + EasingViewModels.Clear(); + } + + private void TimelineEasingViewModelOnEasingModeSelected(object sender, EventArgs e) + { + SelectEasingMode((TimelineEasingViewModel) sender); + } + + public void SelectEasingMode(TimelineEasingViewModel easingViewModel) + { + LayerPropertyKeyframe.EasingFunction = easingViewModel.EasingFunction; + // Set every selection to false except on the VM that made the change + foreach (var propertyTrackEasingViewModel in EasingViewModels.Where(vm => vm != easingViewModel)) + propertyTrackEasingViewModel.IsEasingModeSelected = false; + + _profileEditorService.UpdateSelectedProfileElement(); + } + + #endregion + + #region Movement + + private TimeSpan? _offset; + + public void ReleaseMovement() + { + _offset = null; + } + + public void SaveOffsetToKeyframe(ITimelineKeyframeViewModel source) + { + if (source == this) + { + _offset = null; + return; + } + + if (_offset != null) + return; + + _offset = LayerPropertyKeyframe.Position - source.Position; + } + + public void ApplyOffsetToKeyframe(ITimelineKeyframeViewModel source) + { + if (source == this || _offset == null) + return; + + UpdatePosition(source.Position + _offset.Value); + } + + public void UpdatePosition(TimeSpan position) + { + if (position < TimeSpan.Zero) + LayerPropertyKeyframe.Position = TimeSpan.Zero; + else if (position > _profileEditorService.SelectedProfileElement.TimelineLength) + LayerPropertyKeyframe.Position = _profileEditorService.SelectedProfileElement.TimelineLength; + else + LayerPropertyKeyframe.Position = position; + + Update(); + } + + #endregion #region Context menu actions - public override void Copy() + public void Copy() { var newKeyframe = new LayerPropertyKeyframe( LayerPropertyKeyframe.Value, @@ -46,7 +184,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline _profileEditorService.UpdateSelectedProfileElement(); } - public override void Delete() + public void Delete() { LayerPropertyKeyframe.LayerProperty.RemoveKeyframe(LayerPropertyKeyframe); _profileEditorService.UpdateSelectedProfileElement(); @@ -55,135 +193,26 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline #endregion } - public abstract class TimelineKeyframeViewModel : PropertyChangedBase, IDisposable + public interface ITimelineKeyframeViewModel : IScreen, IDisposable { - private readonly IProfileEditorService _profileEditorService; - private BindableCollection _easingViewModels; - private bool _isSelected; - private int _pixelsPerSecond; - private string _timestamp; - private double _x; - - protected TimelineKeyframeViewModel(IProfileEditorService profileEditorService, BaseLayerPropertyKeyframe baseLayerPropertyKeyframe) - { - _profileEditorService = profileEditorService; - BaseLayerPropertyKeyframe = baseLayerPropertyKeyframe; - EasingViewModels = new BindableCollection(); - - BaseLayerPropertyKeyframe.PropertyChanged += BaseLayerPropertyKeyframeOnPropertyChanged; - } - - public BaseLayerPropertyKeyframe BaseLayerPropertyKeyframe { get; } - - public BindableCollection EasingViewModels - { - get => _easingViewModels; - set => SetAndNotify(ref _easingViewModels, value); - } - - public bool IsSelected - { - get => _isSelected; - set => SetAndNotify(ref _isSelected, value); - } - - public double X - { - get => _x; - set => SetAndNotify(ref _x, value); - } - - public string Timestamp - { - get => _timestamp; - set => SetAndNotify(ref _timestamp, value); - } - - public void Dispose() - { - BaseLayerPropertyKeyframe.PropertyChanged -= BaseLayerPropertyKeyframeOnPropertyChanged; - } - - public void Update(int pixelsPerSecond) - { - _pixelsPerSecond = pixelsPerSecond; - - X = pixelsPerSecond * BaseLayerPropertyKeyframe.Position.TotalSeconds; - Timestamp = $"{Math.Floor(BaseLayerPropertyKeyframe.Position.TotalSeconds):00}.{BaseLayerPropertyKeyframe.Position.Milliseconds:000}"; - } - - public abstract void Copy(); - - public abstract void Delete(); - - private void BaseLayerPropertyKeyframeOnPropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(BaseLayerPropertyKeyframe.Position)) - Update(_pixelsPerSecond); - } - - #region Easing - - public void CreateEasingViewModels() - { - EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)).Cast().Select(v => new TimelineEasingViewModel(this, v))); - } - - public void SelectEasingMode(TimelineEasingViewModel easingViewModel) - { - BaseLayerPropertyKeyframe.EasingFunction = easingViewModel.EasingFunction; - // Set every selection to false except on the VM that made the change - foreach (var propertyTrackEasingViewModel in EasingViewModels.Where(vm => vm != easingViewModel)) - propertyTrackEasingViewModel.IsEasingModeSelected = false; - - - _profileEditorService.UpdateSelectedProfileElement(); - } - - #endregion + bool IsSelected { get; set; } + TimeSpan Position { get; } #region Movement - private TimeSpan? _offset; + void SaveOffsetToKeyframe(ITimelineKeyframeViewModel source); + void ApplyOffsetToKeyframe(ITimelineKeyframeViewModel source); + void UpdatePosition(TimeSpan position); + void ReleaseMovement(); - public void ReleaseMovement() - { - _offset = null; - } + #endregion - public void SaveOffsetToKeyframe(TimelineKeyframeViewModel keyframeViewModel) - { - if (keyframeViewModel == this) - { - _offset = null; - return; - } + #region Context menu actions - if (_offset != null) - return; - - _offset = BaseLayerPropertyKeyframe.Position - keyframeViewModel.BaseLayerPropertyKeyframe.Position; - } - - public void ApplyOffsetToKeyframe(TimelineKeyframeViewModel keyframeViewModel) - { - if (keyframeViewModel == this || _offset == null) - return; - - UpdatePosition(keyframeViewModel.BaseLayerPropertyKeyframe.Position + _offset.Value); - } - - public void UpdatePosition(TimeSpan position) - { - if (position < TimeSpan.Zero) - BaseLayerPropertyKeyframe.Position = TimeSpan.Zero; - else if (position > _profileEditorService.SelectedProfileElement.TimelineLength) - BaseLayerPropertyKeyframe.Position = _profileEditorService.SelectedProfileElement.TimelineLength; - else - BaseLayerPropertyKeyframe.Position = position; - - Update(_pixelsPerSecond); - } + void PopulateEasingViewModels(); + void ClearEasingViewModels(); + void Copy(); + void Delete(); #endregion } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyGroupViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyGroupViewModel.cs deleted file mode 100644 index ea05a499d..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyGroupViewModel.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.ComponentModel; -using System.Linq; -using Artemis.UI.Screens.ProfileEditor.LayerProperties.Abstract; -using Artemis.UI.Shared.Services; -using Stylet; - -namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline -{ - public class TimelinePropertyGroupViewModel : PropertyChangedBase - { - private readonly IProfileEditorService _profileEditorService; - private BindableCollection _timelineKeyframeViewModels; - - public TimelinePropertyGroupViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel, IProfileEditorService profileEditorService) - { - _profileEditorService = profileEditorService; - LayerPropertyGroupViewModel = (LayerPropertyGroupViewModel) layerPropertyBaseViewModel; - TimelineKeyframeViewModels = new BindableCollection(); - - LayerPropertyGroupViewModel.ProfileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged; - LayerPropertyGroupViewModel.PropertyChanged += LayerPropertyGroupViewModelOnPropertyChanged; - UpdateKeyframes(); - } - - public LayerPropertyGroupViewModel LayerPropertyGroupViewModel { get; } - - public BindableCollection TimelineKeyframeViewModels - { - get => _timelineKeyframeViewModels; - set => SetAndNotify(ref _timelineKeyframeViewModels, value); - } - - public void UpdateKeyframes() - { - TimelineKeyframeViewModels.Clear(); - TimelineKeyframeViewModels.AddRange(LayerPropertyGroupViewModel.GetKeyframes(false) - .Select(k => LayerPropertyGroupViewModel.ProfileEditorService.PixelsPerSecond * k.Position.TotalSeconds)); - } - - - public void Dispose() - { - LayerPropertyGroupViewModel.ProfileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged; - LayerPropertyGroupViewModel.PropertyChanged -= LayerPropertyGroupViewModelOnPropertyChanged; - } - - private void LayerPropertyGroupViewModelOnPropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(LayerPropertyGroupViewModel.IsExpanded)) - UpdateKeyframes(); - } - - private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e) - { - UpdateKeyframes(); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyView.xaml index 5fcb44d45..4edbc83ac 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyView.xaml @@ -9,12 +9,10 @@ xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" - d:DataContext="{d:DesignInstance local:TimelinePropertyViewModel}" - Visibility="{Binding LayerPropertyBaseViewModel.IsVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}" MinWidth="{Binding Width}" HorizontalAlignment="Stretch"> - @@ -40,7 +38,7 @@ MouseDown="{s:Action KeyframeMouseDown}" MouseUp="{s:Action KeyframeMouseUp}" MouseMove="{s:Action KeyframeMouseMove}" - ContextMenuOpening="{s:Action ContextMenuOpening}" + ContextMenuOpening="{s:Action ContextMenuOpening}" ContextMenuClosing="{s:Action ContextMenuClosing}"> - General + General @@ -57,14 +57,14 @@ - Transform + Transform @@ -79,23 +79,23 @@ Brush -  @@ -125,7 +125,7 @@ - - + + - - + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeViewModel.cs index 15ba072bb..5c84fa57f 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeViewModel.cs @@ -5,11 +5,13 @@ using Stylet; namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree { - public class TreeViewModel : PropertyChangedBase + public class TreeViewModel : Screen { public TreeViewModel(LayerPropertiesViewModel layerPropertiesViewModel, BindableCollection layerPropertyGroups) { LayerPropertiesViewModel = layerPropertiesViewModel; + + // Not using the Items collection because the list should persist even after this VM gets closed LayerPropertyGroups = layerPropertyGroups; } diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs index 52d80ed29..f76a54e97 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs @@ -17,7 +17,7 @@ using Stylet; namespace Artemis.UI.Screens.ProfileEditor { - public class ProfileEditorViewModel : Conductor.Collection.AllActive + public class ProfileEditorViewModel : Conductor.Collection.AllActive { private readonly IModuleService _moduleService; private readonly IProfileEditorService _profileEditorService; @@ -36,7 +36,10 @@ namespace Artemis.UI.Screens.ProfileEditor private PluginSetting _sidePanelsWidth; public ProfileEditorViewModel(ProfileModule module, - ICollection viewModels, + ProfileViewModel profileViewModel, + ProfileTreeViewModel profileTreeViewModel, + DisplayConditionsViewModel displayConditionsViewModel, + LayerPropertiesViewModel layerPropertiesViewModel, IProfileEditorService profileEditorService, IProfileService profileService, IDialogService dialogService, @@ -55,15 +58,17 @@ namespace Artemis.UI.Screens.ProfileEditor DialogService = dialogService; Profiles = new BindableCollection(); - - // Run this first to let VMs activate without causing constant UI updates - Items.AddRange(viewModels); - + // Populate the panels - ProfileViewModel = (ProfileViewModel) viewModels.First(vm => vm is ProfileViewModel); - ProfileTreeViewModel = (ProfileTreeViewModel) viewModels.First(vm => vm is ProfileTreeViewModel); - DisplayConditionsViewModel = (DisplayConditionsViewModel) viewModels.First(vm => vm is DisplayConditionsViewModel); - LayerPropertiesViewModel = (LayerPropertiesViewModel) viewModels.First(vm => vm is LayerPropertiesViewModel); + ProfileViewModel = profileViewModel; + ProfileTreeViewModel = profileTreeViewModel; + DisplayConditionsViewModel = displayConditionsViewModel; + LayerPropertiesViewModel = layerPropertiesViewModel; + + Items.Add(ProfileViewModel); + Items.Add(ProfileTreeViewModel); + Items.Add(DisplayConditionsViewModel); + Items.Add(LayerPropertiesViewModel); } public ProfileModule Module { get; } diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeView.xaml b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeView.xaml index c4cbdf640..32eb00d36 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeView.xaml @@ -27,7 +27,7 @@ - + - + diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeViewModel.cs index 1593bfecc..1502ac21e 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeViewModel.cs @@ -11,24 +11,19 @@ using Stylet; namespace Artemis.UI.Screens.ProfileEditor.ProfileTree { - public class ProfileTreeViewModel : ProfileEditorPanelViewModel, IDropTarget + public class ProfileTreeViewModel : Conductor, IProfileEditorPanelViewModel, IDropTarget { - private readonly IFolderVmFactory _folderVmFactory; + private readonly IProfileTreeVmFactory _profileTreeVmFactory; private readonly IProfileEditorService _profileEditorService; - private FolderViewModel _rootFolder; private TreeItemViewModel _selectedTreeItem; private bool _updatingTree; - public ProfileTreeViewModel(IProfileEditorService profileEditorService, IFolderVmFactory folderVmFactory) + public ProfileTreeViewModel(IProfileEditorService profileEditorService, IProfileTreeVmFactory profileTreeVmFactory) { _profileEditorService = profileEditorService; - _folderVmFactory = folderVmFactory; - } + _profileTreeVmFactory = profileTreeVmFactory; - public FolderViewModel RootFolder - { - get => _rootFolder; - set => SetAndNotify(ref _rootFolder, value); + CreateRootFolderViewModel(); } public TreeItemViewModel SelectedTreeItem @@ -73,7 +68,7 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree switch (dragDropType) { case DragDropType.Add: - source.Parent.RemoveExistingElement(source); + ((TreeItemViewModel) source.Parent).RemoveExistingElement(source); target.AddExistingElement(source); break; case DragDropType.InsertBefore: @@ -92,28 +87,25 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree // ReSharper disable once UnusedMember.Global - Called from view public void AddFolder() { - RootFolder?.AddFolder(); + ActiveItem?.AddFolder(); } // ReSharper disable once UnusedMember.Global - Called from view public void AddLayer() { - RootFolder?.AddLayer(); + ActiveItem?.AddLayer(); } - protected override void OnInitialActivate() + protected override void OnActivate() { Subscribe(); - CreateRootFolderViewModel(); + base.OnActivate(); } - protected override void OnClose() + protected override void OnDeactivate() { Unsubscribe(); - - RootFolder?.Dispose(); - RootFolder = null; - base.OnClose(); + base.OnDeactivate(); } private void CreateRootFolderViewModel() @@ -122,12 +114,11 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree var firstChild = _profileEditorService.SelectedProfile?.Children?.FirstOrDefault(); if (!(firstChild is Folder folder)) { - RootFolder = null; + ActivateItem(null); return; } - RootFolder?.Dispose(); - RootFolder = _folderVmFactory.Create(folder); + ActivateItem(_profileTreeVmFactory.FolderViewModel(folder)); _updatingTree = false; // Auto-select the first layer @@ -150,7 +141,7 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree { if (parent == source) return DragDropType.None; - parent = parent.Parent; + parent = parent.Parent as TreeItemViewModel; } switch (dropInfo.InsertPosition) @@ -186,20 +177,20 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree if (e.RenderProfileElement == SelectedTreeItem?.ProfileElement) return; - if (RootFolder == null) + if (ActiveItem == null) { CreateRootFolderViewModel(); return; } _updatingTree = true; - RootFolder.UpdateProfileElements(); + ActiveItem.UpdateProfileElements(); _updatingTree = false; if (e.RenderProfileElement == null) SelectedTreeItem = null; else { - var match = RootFolder.GetAllChildren().FirstOrDefault(vm => vm.ProfileElement == e.RenderProfileElement); + var match = ActiveItem.GetAllChildren().FirstOrDefault(vm => vm.ProfileElement == e.RenderProfileElement); if (match != null) SelectedTreeItem = match; } diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/FolderViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/FolderViewModel.cs index 16d594f11..310e1324f 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/FolderViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/FolderViewModel.cs @@ -11,36 +11,13 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem public FolderViewModel(ProfileElement folder, IProfileEditorService profileEditorService, IDialogService dialogService, - IRenderElementService renderElementService, - IFolderVmFactory folderVmFactory, - ILayerVmFactory layerVmFactory) : - base(null, folder, profileEditorService, dialogService, renderElementService, folderVmFactory, layerVmFactory) - { - } - - public FolderViewModel(TreeItemViewModel parent, - ProfileElement folder, - IProfileEditorService profileEditorService, - IDialogService dialogService, - IRenderElementService renderElementService, - IFolderVmFactory folderVmFactory, - ILayerVmFactory layerVmFactory) : - base(parent, folder, profileEditorService, dialogService, renderElementService, folderVmFactory, layerVmFactory) + IProfileTreeVmFactory profileTreeVmFactory, + ILayerBrushService layerBrushService, + ISurfaceService surfaceService) : + base(folder, profileEditorService, dialogService, profileTreeVmFactory, layerBrushService, surfaceService) { } public override bool SupportsChildren => true; - - protected override void Dispose(bool disposing) - { - if (disposing) - { - foreach (var treeItemViewModel in Children) - treeItemViewModel.Dispose(); - Children.Clear(); - } - - base.Dispose(disposing); - } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerViewModel.cs index c539a5a55..b338ddd13 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerViewModel.cs @@ -7,14 +7,13 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem { public class LayerViewModel : TreeItemViewModel { - public LayerViewModel(TreeItemViewModel parent, - ProfileElement folder, + public LayerViewModel(ProfileElement layer, IProfileEditorService profileEditorService, IDialogService dialogService, - IRenderElementService renderElementService, - IFolderVmFactory folderVmFactory, - ILayerVmFactory layerVmFactory) : - base(parent, folder, profileEditorService, dialogService, renderElementService, folderVmFactory, layerVmFactory) + IProfileTreeVmFactory profileTreeVmFactory, + ILayerBrushService layerBrushService, + ISurfaceService surfaceService) : + base(layer, profileEditorService, dialogService, profileTreeVmFactory, layerBrushService, surfaceService) { } diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs index a8d7f006b..435ca6937 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs @@ -12,66 +12,51 @@ using Stylet; namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem { - public abstract class TreeItemViewModel : PropertyChangedBase, IDisposable + public abstract class TreeItemViewModel : Conductor.Collection.AllActive, IDisposable { private readonly IDialogService _dialogService; - private readonly IFolderVmFactory _folderVmFactory; - private readonly ILayerVmFactory _layerVmFactory; private readonly IProfileEditorService _profileEditorService; - private readonly IRenderElementService _renderElementService; - private TreeItemViewModel _parent; + private readonly IProfileTreeVmFactory _profileTreeVmFactory; + private readonly ILayerBrushService _layerBrushService; + private readonly ISurfaceService _surfaceService; private ProfileElement _profileElement; - protected TreeItemViewModel(TreeItemViewModel parent, - ProfileElement profileElement, + protected TreeItemViewModel(ProfileElement profileElement, IProfileEditorService profileEditorService, IDialogService dialogService, - IRenderElementService renderElementService, - IFolderVmFactory folderVmFactory, - ILayerVmFactory layerVmFactory) + IProfileTreeVmFactory profileTreeVmFactory, + ILayerBrushService layerBrushService, + ISurfaceService surfaceService) { _profileEditorService = profileEditorService; _dialogService = dialogService; - _renderElementService = renderElementService; - _folderVmFactory = folderVmFactory; - _layerVmFactory = layerVmFactory; + _profileTreeVmFactory = profileTreeVmFactory; + _layerBrushService = layerBrushService; + _surfaceService = surfaceService; - Parent = parent; ProfileElement = profileElement; - Children = new BindableCollection(); - Subscribe(); UpdateProfileElements(); } - public TreeItemViewModel Parent - { - get => _parent; - set => SetAndNotify(ref _parent, value); - } - public ProfileElement ProfileElement { get => _profileElement; set => SetAndNotify(ref _profileElement, value); } - public BindableCollection Children { get; } - public abstract bool SupportsChildren { get; } public void Dispose() { Unsubscribe(); - Dispose(true); - GC.SuppressFinalize(this); } public List GetAllChildren() { var children = new List(); - foreach (var childFolder in Children) + foreach (var childFolder in Items) { // Add all children in this element children.Add(childFolder); @@ -84,34 +69,38 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem public void SetElementInFront(TreeItemViewModel source) { + var sourceParent = (TreeItemViewModel) source.Parent; + var parent = (TreeItemViewModel) Parent; if (source.Parent != Parent) { - source.Parent.RemoveExistingElement(source); - Parent.AddExistingElement(source); + sourceParent.RemoveExistingElement(source); + parent.AddExistingElement(source); } - Parent.Unsubscribe(); - Parent.ProfileElement.RemoveChild(source.ProfileElement); - Parent.ProfileElement.AddChild(source.ProfileElement, ProfileElement.Order); - Parent.Subscribe(); + parent.Unsubscribe(); + parent.ProfileElement.RemoveChild(source.ProfileElement); + parent.ProfileElement.AddChild(source.ProfileElement, ProfileElement.Order); + parent.Subscribe(); - Parent.UpdateProfileElements(); + parent.UpdateProfileElements(); } public void SetElementBehind(TreeItemViewModel source) { + var sourceParent = (TreeItemViewModel) source.Parent; + var parent = (TreeItemViewModel) Parent; if (source.Parent != Parent) { - source.Parent.RemoveExistingElement(source); - Parent.AddExistingElement(source); + sourceParent.RemoveExistingElement(source); + parent.AddExistingElement(source); } - Parent.Unsubscribe(); - Parent.ProfileElement.RemoveChild(source.ProfileElement); - Parent.ProfileElement.AddChild(source.ProfileElement, ProfileElement.Order + 1); - Parent.Subscribe(); + parent.Unsubscribe(); + parent.ProfileElement.RemoveChild(source.ProfileElement); + parent.ProfileElement.AddChild(source.ProfileElement, ProfileElement.Order + 1); + parent.Subscribe(); - Parent.UpdateProfileElements(); + parent.UpdateProfileElements(); } public void RemoveExistingElement(TreeItemViewModel treeItem) @@ -138,7 +127,7 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem if (!SupportsChildren) throw new ArtemisUIException("Cannot add a folder to a profile element of type " + ProfileElement.GetType().Name); - ProfileElement.AddChild(new Folder(ProfileElement.Profile, ProfileElement, "New folder")); + var _ = new Folder(ProfileElement, "New folder"); _profileEditorService.UpdateSelectedProfile(); } @@ -147,8 +136,11 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem if (!SupportsChildren) throw new ArtemisUIException("Cannot add a layer to a profile element of type " + ProfileElement.GetType().Name); - _renderElementService.CreateLayer(ProfileElement.Profile, ProfileElement, "New layer"); + var layer = new Layer(ProfileElement, "New layer"); + layer.ChangeLayerBrush(_layerBrushService.GetDefaultLayerBrush()); + layer.AddLeds(_surfaceService.ActiveSurface.Devices.SelectMany(d => d.Leds)); _profileEditorService.UpdateSelectedProfile(); + _profileEditorService.ChangeSelectedProfileElement(layer); } // ReSharper disable once UnusedMember.Global - Called from view @@ -180,27 +172,28 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem return; // Farewell, cruel world - var parent = Parent; + var parent = (TreeItemViewModel) Parent; ProfileElement.Parent?.RemoveChild(ProfileElement); parent.RemoveExistingElement(this); _profileEditorService.UpdateSelectedProfile(); + _profileEditorService.ChangeSelectedProfileElement(null); } public void UpdateProfileElements() { // Remove VMs that are no longer a child - var toRemove = Children.Where(c => c.ProfileElement.Parent != ProfileElement).ToList(); + var toRemove = Items.Where(c => c.ProfileElement.Parent != ProfileElement).ToList(); foreach (var treeItemViewModel in toRemove) - Children.Remove(treeItemViewModel); + DeactivateItem(treeItemViewModel); // Order the children - var vmsList = Children.OrderBy(v => v.ProfileElement.Order).ToList(); + var vmsList = Items.OrderBy(v => v.ProfileElement.Order).ToList(); for (var index = 0; index < vmsList.Count; index++) { var profileElementViewModel = vmsList[index]; - if (Children.IndexOf(profileElementViewModel) != index) - Children.Move(Children.IndexOf(profileElementViewModel), index); + if (Items.IndexOf(profileElementViewModel) != index) + ((BindableCollection) Items).Move(Items.IndexOf(profileElementViewModel), index); } // Ensure every child element has an up-to-date VM @@ -212,13 +205,13 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem { if (profileElement is Folder folder) { - if (Children.FirstOrDefault(p => p is FolderViewModel vm && vm.ProfileElement == folder) == null) - newChildren.Add(_folderVmFactory.Create((FolderViewModel) this, folder)); + if (Items.FirstOrDefault(p => p is FolderViewModel vm && vm.ProfileElement == folder) == null) + newChildren.Add(_profileTreeVmFactory.FolderViewModel(folder)); } else if (profileElement is Layer layer) { - if (Children.FirstOrDefault(p => p is LayerViewModel vm && vm.ProfileElement == layer) == null) - newChildren.Add(_layerVmFactory.Create((FolderViewModel) this, layer)); + if (Items.FirstOrDefault(p => p is LayerViewModel vm && vm.ProfileElement == layer) == null) + newChildren.Add(_profileTreeVmFactory.LayerViewModel(layer)); } } @@ -229,7 +222,7 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem foreach (var treeItemViewModel in newChildren) { treeItemViewModel.UpdateProfileElements(); - Children.Add(treeItemViewModel); + ActivateItem(treeItemViewModel); } } @@ -238,13 +231,6 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem _profileEditorService.UpdateSelectedProfile(); } - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - } - } - private void Subscribe() { ProfileElement.ChildAdded += ProfileElementOnChildAdded; diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileDeviceView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileDeviceView.xaml deleted file mode 100644 index 82a471bc7..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileDeviceView.xaml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileDeviceViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileDeviceViewModel.cs deleted file mode 100644 index 828600820..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileDeviceViewModel.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System.Collections.ObjectModel; -using System.Linq; -using System.Threading.Tasks; -using System.Windows; -using Artemis.Core; -using Stylet; - -namespace Artemis.UI.Screens.ProfileEditor.Visualization -{ - public class ProfileDeviceViewModel : CanvasViewModel - { - private bool _addedLeds; - private ArtemisDevice _device; - private ObservableCollection _leds; - - public ProfileDeviceViewModel(ArtemisDevice device) - { - Device = device; - Leds = new ObservableCollection(); - - Task.Run(AddLedsAsync); - } - - public ObservableCollection Leds - { - get => _leds; - set => SetAndNotify(ref _leds, value); - } - - public ArtemisDevice Device - { - get => _device; - set => SetAndNotify(ref _device, value); - } - - public bool AddedLeds - { - get => _addedLeds; - private set => SetAndNotify(ref _addedLeds, value); - } - - public new double X - { - get => Device.X; - set => Device.X = value; - } - - public new double Y - { - get => Device.Y; - set => Device.Y = value; - } - - public int ZIndex - { - get => Device.ZIndex; - set => Device.ZIndex = value; - } - - - public Rect DeviceRectangle => Device.RgbDevice == null - ? new Rect() - : new Rect(X, Y, Device.RgbDevice.Size.Width, Device.RgbDevice.Size.Height); - - /// - /// Update the color of all LEDs if finished adding - /// - public void Update() - { - if (!AddedLeds) - return; - - foreach (var ledViewModel in Leds) - ledViewModel.Update(); - } - - /// - /// Adds LEDs in batches of 5 to avoid UI freezes - /// - /// - private async Task AddLedsAsync() - { - var index = 0; - foreach (var led in Device.Leds.ToList()) - { - Execute.OnUIThreadSync(() => Leds.Add(new ProfileLedViewModel(led))); - if (index % 5 == 0) - await Task.Delay(1); - - index++; - } - - AddedLeds = true; - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLedView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLedView.xaml deleted file mode 100644 index 083bfc23c..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLedView.xaml +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLedViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLedViewModel.cs deleted file mode 100644 index 26abba437..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileLedViewModel.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Windows; -using System.Windows.Media; -using Artemis.Core; -using Artemis.UI.Extensions; -using RGB.NET.Core; -using Stylet; -using Color = System.Windows.Media.Color; - -namespace Artemis.UI.Screens.ProfileEditor.Visualization -{ - public class ProfileLedViewModel : PropertyChangedBase - { - private Color _displayColor; - private Geometry _displayGeometry; - private bool _isDimmed; - private bool _isSelected; - private Geometry _strokeGeometry; - - public ProfileLedViewModel(ArtemisLed led) - { - Led = led; - - // Don't want ActualLocation here since rotation is done in XAML - X = led.RgbLed.Location.X * led.RgbLed.Device.Scale.Horizontal; - Y = led.RgbLed.Location.Y * led.RgbLed.Device.Scale.Vertical; - Width = led.RgbLed.ActualSize.Width; - Height = led.RgbLed.ActualSize.Height; - - Execute.PostToUIThread(CreateLedGeometry); - } - - public ArtemisLed Led { get; } - - public double X { get; } - public double Y { get; } - public double Width { get; } - public double Height { get; } - - public bool IsSelected - { - get => _isSelected; - set => SetAndNotify(ref _isSelected, value); - } - - public bool IsDimmed - { - get => _isDimmed; - set => SetAndNotify(ref _isDimmed, value); - } - - public Geometry DisplayGeometry - { - get => _displayGeometry; - private set => SetAndNotify(ref _displayGeometry, value); - } - - public Geometry StrokeGeometry - { - get => _strokeGeometry; - private set => SetAndNotify(ref _strokeGeometry, value); - } - - public Color DisplayColor - { - get => _displayColor; - private set => SetAndNotify(ref _displayColor, value); - } - - public void Update() - { - var newColor = Led.RgbLed.Color.ToMediaColor(); - Execute.PostToUIThread(() => - { - if (!DisplayColor.Equals(newColor)) - DisplayColor = newColor; - }); - } - - private void CreateLedGeometry() - { - switch (Led.RgbLed.Shape) - { - case Shape.Custom: - if (Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keyboard || Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keypad) - CreateCustomGeometry(2.0); - else - CreateCustomGeometry(1.0); - break; - case Shape.Rectangle: - if (Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keyboard || Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keypad) - CreateKeyCapGeometry(); - else - CreateRectangleGeometry(); - break; - case Shape.Circle: - CreateCircleGeometry(); - break; - default: - throw new ArgumentOutOfRangeException(); - } - - // Stroke geometry is the display geometry excluding the inner geometry - StrokeGeometry = DisplayGeometry.GetWidenedPathGeometry(new Pen(null, 1.0), 0.1, ToleranceType.Absolute); - DisplayGeometry.Freeze(); - StrokeGeometry.Freeze(); - } - - private void CreateRectangleGeometry() - { - DisplayGeometry = new RectangleGeometry(new Rect(0.5, 0.5, Width - 1, Height - 1)); - } - - private void CreateCircleGeometry() - { - DisplayGeometry = new EllipseGeometry(new Rect(0.5, 0.5, Width - 1, Height - 1)); - } - - private void CreateKeyCapGeometry() - { - DisplayGeometry = new RectangleGeometry(new Rect(1, 1, Width - 2, Height - 2), 1.6, 1.6); - } - - private void CreateCustomGeometry(double deflateAmount) - { - try - { - DisplayGeometry = Geometry.Combine( - Geometry.Empty, - Geometry.Parse(Led.RgbLed.ShapeData), - GeometryCombineMode.Union, - new TransformGroup - { - Children = new TransformCollection - { - new ScaleTransform(Width - deflateAmount, Height - deflateAmount), - new TranslateTransform(deflateAmount / 2, deflateAmount / 2) - } - } - ); - } - catch (Exception) - { - CreateRectangleGeometry(); - } - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs index c64e1ffcc..32bc7f957 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs @@ -15,7 +15,7 @@ using Stylet; namespace Artemis.UI.Screens.ProfileEditor.Visualization { - public class ProfileViewModel : ProfileEditorPanelViewModel, IHandle, IHandle + public class ProfileViewModel : Screen, IProfileEditorPanelViewModel, IHandle, IHandle { private readonly IProfileEditorService _profileEditorService; private readonly IProfileLayerVmFactory _profileLayerVmFactory; @@ -173,8 +173,10 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization .ToList(); } - protected override void OnInitialActivate() + protected override void OnActivate() { + ApplyActiveProfile(); + OnlyShowSelectedShape = _settingsService.GetSetting("ProfileEditor.OnlyShowSelectedShape", true); HighlightSelectedLayer = _settingsService.GetSetting("ProfileEditor.HighlightSelectedLayer", true); @@ -184,10 +186,10 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization _profileEditorService.ProfileElementSelected += OnProfileElementSelected; _profileEditorService.SelectedProfileElementUpdated += OnSelectedProfileElementUpdated; - base.OnInitialActivate(); + base.OnActivate(); } - protected override void OnClose() + protected override void OnDeactivate() { HighlightSelectedLayer.SettingChanged -= HighlightSelectedLayerOnSettingChanged; _surfaceService.ActiveSurfaceConfigurationSelected -= OnActiveSurfaceConfigurationSelected; @@ -204,12 +206,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization canvasViewModel.Dispose(); CanvasViewModels.Clear(); - base.OnClose(); - } - - protected override void OnActivate() - { - ApplyActiveProfile(); + base.OnDeactivate(); } private void OnActiveSurfaceConfigurationSelected(object sender, SurfaceConfigurationEventArgs e) diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs index c6e84ce99..5e08f3227 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs @@ -1,4 +1,5 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; using System.Linq; using System.Windows; using System.Windows.Input; @@ -11,13 +12,14 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization.Tools { public class SelectionToolViewModel : VisualizationToolViewModel { - private readonly IRenderElementService _renderElementService; + private readonly ILayerBrushService _layerBrushService; private Rect _dragRectangle; - public SelectionToolViewModel(ProfileViewModel profileViewModel, IProfileEditorService profileEditorService, IRenderElementService renderElementService) - : base(profileViewModel, profileEditorService) + public SelectionToolViewModel(ProfileViewModel profileViewModel, + IProfileEditorService profileEditorService, + ILayerBrushService layerBrushService) : base(profileViewModel, profileEditorService) { - _renderElementService = renderElementService; + _layerBrushService = layerBrushService; using (var stream = new MemoryStream(Resources.aero_crosshair)) { Cursor = new Cursor(stream); @@ -56,20 +58,12 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization.Tools } // If no layer selected, apply it to a new layer in the selected folder else if (ProfileEditorService.SelectedProfileElement is Folder folder) - { - var newLayer = _renderElementService.CreateLayer(folder.Profile, folder, "New layer"); - newLayer.AddLeds(selectedLeds); - ProfileEditorService.ChangeSelectedProfileElement(newLayer); - ProfileEditorService.UpdateSelectedProfileElement(); - } + CreateLayer(folder, selectedLeds); // If no folder selected, apply it to a new layer in the root folder else { var rootFolder = ProfileEditorService.SelectedProfile.GetRootFolder(); - var newLayer = _renderElementService.CreateLayer(rootFolder.Profile, rootFolder, "New layer"); - newLayer.AddLeds(selectedLeds); - ProfileEditorService.ChangeSelectedProfileElement(newLayer); - ProfileEditorService.UpdateSelectedProfileElement(); + CreateLayer(rootFolder, selectedLeds); } } @@ -93,5 +87,14 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization.Tools DragRectangle = selectedRect; } + + private void CreateLayer(Folder folder, List selectedLeds) + { + var newLayer = new Layer(folder, "New layer"); + newLayer.ChangeLayerBrush(_layerBrushService.GetDefaultLayerBrush()); + newLayer.AddLeds(selectedLeds); + ProfileEditorService.ChangeSelectedProfileElement(newLayer); + ProfileEditorService.UpdateSelectedProfileElement(); + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerBrushSettingsWindowView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Windows/LayerBrushSettingsWindowView.xaml similarity index 97% rename from src/Artemis.UI/Screens/ProfileEditor/LayerBrushSettingsWindowView.xaml rename to src/Artemis.UI/Screens/ProfileEditor/Windows/LayerBrushSettingsWindowView.xaml index 6acacbc9a..42507087e 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerBrushSettingsWindowView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Windows/LayerBrushSettingsWindowView.xaml @@ -1,4 +1,4 @@ - { diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerEffectSettingsWindowView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Windows/LayerEffectSettingsWindowView.xaml similarity index 97% rename from src/Artemis.UI/Screens/ProfileEditor/LayerEffectSettingsWindowView.xaml rename to src/Artemis.UI/Screens/ProfileEditor/Windows/LayerEffectSettingsWindowView.xaml index f7f6d2977..1cc495742 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerEffectSettingsWindowView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Windows/LayerEffectSettingsWindowView.xaml @@ -1,4 +1,4 @@ - { diff --git a/src/Artemis.UI/Screens/RootViewModel.cs b/src/Artemis.UI/Screens/RootViewModel.cs index 448a7d04b..0f551b77c 100644 --- a/src/Artemis.UI/Screens/RootViewModel.cs +++ b/src/Artemis.UI/Screens/RootViewModel.cs @@ -247,13 +247,14 @@ namespace Artemis.UI.Screens _colorScheme.SettingChanged -= ColorSchemeOnSettingChanged; _themeWatcher.ThemeChanged -= ThemeWatcherOnThemeChanged; SidebarViewModel.PropertyChanged -= SidebarViewModelOnPropertyChanged; + + base.OnDeactivate(); } protected override void OnClose() { SidebarViewModel.Dispose(); - - + // Lets force the GC to run after closing the window so it is obvious to users watching task manager // that closing the UI will decrease the memory footprint of the application. Task.Run(async () => @@ -263,6 +264,8 @@ namespace Artemis.UI.Screens GC.WaitForPendingFinalizers(); GC.Collect(); }); + + base.OnClose(); } #endregion diff --git a/src/Artemis.UI/Screens/Settings/Debug/Tabs/DataModelDebugViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/Tabs/DataModelDebugViewModel.cs index a962339f5..4ec06306d 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 private void GetDataModel() { MainDataModel = SelectedModule != null - ? _dataModelUIService.GetPluginDataModelVisualization(SelectedModule) + ? _dataModelUIService.GetPluginDataModelVisualization(SelectedModule, false) : _dataModelUIService.GetMainDataModelVisualization(); } diff --git a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml index 4ecdf5077..71e96e90e 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml @@ -6,9 +6,17 @@ xmlns:local="clr-namespace:Artemis.UI.Screens.Settings.Tabs.General" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:s="https://github.com/canton7/Stylet" + xmlns:dataTemplateSelectors="clr-namespace:Artemis.UI.DataTemplateSelectors" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" d:DataContext="{d:DesignInstance local:GeneralSettingsTabViewModel}"> + + + + + + + @@ -187,6 +195,39 @@ + + + + + + + + + + + + + + Default brush + + Sets the default brush that is applied to new layers + + + + + + diff --git a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs index 907f352b2..fc8ed1d98 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Artemis.Core; +using Artemis.Core.LayerBrushes; using Artemis.Core.Services; using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; @@ -22,8 +23,9 @@ namespace Artemis.UI.Screens.Settings.Tabs.General private List> _renderScales; private List _sampleSizes; private List> _targetFrameRates; + private readonly PluginSetting _defaultLayerBrushDescriptor; - public GeneralSettingsTabViewModel(IDialogService dialogService, IDebugService debugService, ISettingsService settingsService) + public GeneralSettingsTabViewModel(IDialogService dialogService, IDebugService debugService, ISettingsService settingsService, IPluginService pluginService) { DisplayName = "GENERAL"; @@ -43,6 +45,27 @@ namespace Artemis.UI.Screens.Settings.Tabs.General // Anything else is kinda broken right now SampleSizes = new List {1, 9}; + + var layerBrushProviders = pluginService.GetPluginsOfType(); + + LayerBrushDescriptors = new BindableCollection(layerBrushProviders.SelectMany(l => l.LayerBrushDescriptors)); + _defaultLayerBrushDescriptor = _settingsService.GetSetting("ProfileEditor.DefaultLayerBrushDescriptor", new LayerBrushReference + { + BrushPluginGuid = Guid.Parse("92a9d6ba-6f7a-4937-94d5-c1d715b4141a"), + BrushType = "ColorBrush" + }); + } + + public BindableCollection LayerBrushDescriptors { get; } + + public LayerBrushDescriptor SelectedLayerBrushDescriptor + { + get => LayerBrushDescriptors.FirstOrDefault(d => d.MatchesLayerBrushReference(_defaultLayerBrushDescriptor.Value)); + set + { + _defaultLayerBrushDescriptor.Value = new LayerBrushReference(value); + _defaultLayerBrushDescriptor.Save(); + } } public BindableCollection LogLevels { get; } @@ -66,7 +89,6 @@ namespace Artemis.UI.Screens.Settings.Tabs.General set => SetAndNotify(ref _sampleSizes, value); } - public bool StartWithWindows { get => _settingsService.GetSetting("UI.AutoRun", false).Value; diff --git a/src/Artemis.UI/Services/RegistrationService.cs b/src/Artemis.UI/Services/RegistrationService.cs index e4bf69965..1b036c4df 100644 --- a/src/Artemis.UI/Services/RegistrationService.cs +++ b/src/Artemis.UI/Services/RegistrationService.cs @@ -55,6 +55,7 @@ namespace Artemis.UI.Services _profileEditorService.RegisterPropertyInput(Constants.CorePluginInfo); _profileEditorService.RegisterPropertyInput(Constants.CorePluginInfo); _profileEditorService.RegisterPropertyInput(Constants.CorePluginInfo); + _profileEditorService.RegisterPropertyInput(typeof(EnumPropertyInputViewModel<>), Constants.CorePluginInfo); _registeredBuiltInPropertyEditors = true; } diff --git a/src/Plugins/Artemis.Plugins.LayerBrushes.Color/PropertyGroups/ColorBrushProperties.cs b/src/Plugins/Artemis.Plugins.LayerBrushes.Color/PropertyGroups/ColorBrushProperties.cs index b840bfc07..beeaec69a 100644 --- a/src/Plugins/Artemis.Plugins.LayerBrushes.Color/PropertyGroups/ColorBrushProperties.cs +++ b/src/Plugins/Artemis.Plugins.LayerBrushes.Color/PropertyGroups/ColorBrushProperties.cs @@ -37,21 +37,26 @@ namespace Artemis.Plugins.LayerBrushes.Color.PropertyGroups protected override void EnableProperties() { - GradientType.BaseValueChanged += OnBaseValueChanged; + GradientType.BaseValueChanged += GradientTypeOnBaseValueChanged; if (ProfileElement is Layer layer) - layer.General.ResizeMode.BaseValueChanged += OnBaseValueChanged; + layer.General.ResizeMode.BaseValueChanged += ResizeModeOnBaseValueChanged; UpdateVisibility(); } protected override void DisableProperties() { - GradientType.BaseValueChanged -= OnBaseValueChanged; + GradientType.BaseValueChanged -= GradientTypeOnBaseValueChanged; if (ProfileElement is Layer layer) - layer.General.ResizeMode.BaseValueChanged -= OnBaseValueChanged; + layer.General.ResizeMode.BaseValueChanged -= ResizeModeOnBaseValueChanged; } - private void OnBaseValueChanged(object sender, LayerPropertyEventArgs e) + private void GradientTypeOnBaseValueChanged(object sender, LayerPropertyEventArgs e) + { + UpdateVisibility(); + } + + private void ResizeModeOnBaseValueChanged(object sender, LayerPropertyEventArgs e) { UpdateVisibility(); } diff --git a/src/Plugins/Artemis.Plugins.LayerBrushes.Color/PropertyGroups/RadialGradientProperties.cs b/src/Plugins/Artemis.Plugins.LayerBrushes.Color/PropertyGroups/RadialGradientProperties.cs index 6af1093b9..21afb4bf5 100644 --- a/src/Plugins/Artemis.Plugins.LayerBrushes.Color/PropertyGroups/RadialGradientProperties.cs +++ b/src/Plugins/Artemis.Plugins.LayerBrushes.Color/PropertyGroups/RadialGradientProperties.cs @@ -18,7 +18,7 @@ namespace Artemis.Plugins.LayerBrushes.Color.PropertyGroups protected override void EnableProperties() { if (ProfileElement is Layer layer) - layer.General.ResizeMode.BaseValueChanged += OnBaseValueChanged; + layer.General.ResizeMode.BaseValueChanged += ResizeModeOnBaseValueChanged; UpdateVisibility(); } @@ -26,10 +26,10 @@ namespace Artemis.Plugins.LayerBrushes.Color.PropertyGroups protected override void DisableProperties() { if (ProfileElement is Layer layer) - layer.General.ResizeMode.BaseValueChanged -= OnBaseValueChanged; + layer.General.ResizeMode.BaseValueChanged -= ResizeModeOnBaseValueChanged; } - private void OnBaseValueChanged(object sender, LayerPropertyEventArgs e) + private void ResizeModeOnBaseValueChanged(object sender, LayerPropertyEventArgs e) { UpdateVisibility(); }