diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index fab80bf52..4eadc28c6 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -94,7 +94,7 @@ namespace Artemis.Core /// /// Gets the general properties of the layer /// - [PropertyGroupDescription(Name = "General", Description = "A collection of general properties")] + [PropertyGroupDescription(Identifier = "General", Name = "General", Description = "A collection of general properties")] public LayerGeneralProperties General { get => _general; @@ -104,7 +104,7 @@ namespace Artemis.Core /// /// Gets the transform properties of the layer /// - [PropertyGroupDescription(Name = "Transform", Description = "A collection of transformation properties")] + [PropertyGroupDescription(Identifier = "Transform", Name = "Transform", Description = "A collection of transformation properties")] public LayerTransformProperties Transform { get => _transform; @@ -208,19 +208,20 @@ namespace Artemis.Core LayerBrushStore.LayerBrushRemoved += LayerBrushStoreOnLayerBrushRemoved; // Layers have two hardcoded property groups, instantiate them - Attribute generalAttribute = Attribute.GetCustomAttribute( + PropertyGroupDescriptionAttribute generalAttribute = (PropertyGroupDescriptionAttribute) Attribute.GetCustomAttribute( GetType().GetProperty(nameof(General))!, typeof(PropertyGroupDescriptionAttribute) )!; - Attribute transformAttribute = Attribute.GetCustomAttribute( + PropertyGroupDescriptionAttribute transformAttribute = (PropertyGroupDescriptionAttribute) Attribute.GetCustomAttribute( GetType().GetProperty(nameof(Transform))!, typeof(PropertyGroupDescriptionAttribute) )!; - General.GroupDescription = (PropertyGroupDescriptionAttribute) generalAttribute; - General.Initialize(this, "General.", Constants.CorePluginFeature); - Transform.GroupDescription = (PropertyGroupDescriptionAttribute) transformAttribute; - Transform.Initialize(this, "Transform.", Constants.CorePluginFeature); + LayerEntity.GeneralPropertyGroup ??= new PropertyGroupEntity {Identifier = generalAttribute.Identifier}; + LayerEntity.TransformPropertyGroup ??= new PropertyGroupEntity {Identifier = transformAttribute.Identifier}; + + General.Initialize(this, null, generalAttribute, LayerEntity.GeneralPropertyGroup); + Transform.Initialize(this, null, transformAttribute, LayerEntity.TransformPropertyGroup); General.ShapeType.CurrentValueSet += ShapeTypeOnCurrentValueSet; ApplyShapeType(); @@ -241,8 +242,7 @@ namespace Artemis.Core return; LayerBrushReference? current = General.BrushReference.CurrentValue; - if (e.Registration.PluginFeature.Id == current?.LayerBrushProviderId && - e.Registration.LayerBrushDescriptor.LayerBrushType.Name == current.BrushType) + if (e.Registration.PluginFeature.Id == current?.LayerBrushProviderId && e.Registration.LayerBrushDescriptor.LayerBrushType.Name == current.BrushType) ActivateLayerBrush(); } @@ -282,7 +282,13 @@ namespace Artemis.Core General.ApplyToEntity(); Transform.ApplyToEntity(); - LayerBrush?.BaseProperties?.ApplyToEntity(); + + // Don't override the old value of LayerBrush if the current value is null, this avoid losing settings of an unavailable brush + if (LayerBrush != null) + { + LayerBrush.Save(); + LayerEntity.LayerBrush = LayerBrush.LayerBrushEntity; + } // LEDs LayerEntity.Leds.Clear(); @@ -712,53 +718,32 @@ namespace Artemis.Core #region Brush management /// - /// Changes the current layer brush to the brush described in the provided + /// Changes the current layer brush to the provided layer brush and activates it /// - public void ChangeLayerBrush(LayerBrushDescriptor descriptor) + public void ChangeLayerBrush(BaseLayerBrush? layerBrush) { - if (descriptor == null) - throw new ArgumentNullException(nameof(descriptor)); + General.BrushReference.SetCurrentValue(layerBrush != null ? new LayerBrushReference(layerBrush.Descriptor) : null, null); + LayerBrush = layerBrush; if (LayerBrush != null) - { - BaseLayerBrush brush = LayerBrush; - LayerBrush = null; - brush.Dispose(); - } - - // Ensure the brush reference matches the brush - LayerBrushReference? current = General.BrushReference.BaseValue; - if (!descriptor.MatchesLayerBrushReference(current)) - General.BrushReference.SetCurrentValue(new LayerBrushReference(descriptor), null); - - ActivateLayerBrush(); - } - - /// - /// Removes the current layer brush from the layer - /// - public void RemoveLayerBrush() - { - if (LayerBrush == null) - return; - - BaseLayerBrush brush = LayerBrush; - DeactivateLayerBrush(); - LayerEntity.PropertyEntities.RemoveAll(p => p.FeatureId == brush.ProviderId && p.Path.StartsWith("LayerBrush.")); + ActivateLayerBrush(); + else + OnLayerBrushUpdated(); } internal void ActivateLayerBrush() { try { - LayerBrushReference? current = General.BrushReference.CurrentValue; - if (current == null) + if (LayerBrush == null) + { + // If the brush is null, try to instantiate it + LayerBrushReference? brushReference = General.BrushReference.CurrentValue; + if (brushReference?.LayerBrushProviderId != null && brushReference.BrushType != null) + ChangeLayerBrush(LayerBrushStore.Get(brushReference.LayerBrushProviderId, brushReference.BrushType)?.LayerBrushDescriptor.CreateInstance(this, LayerEntity.LayerBrush)); + // If that's not possible there's nothing to do return; - - LayerBrushDescriptor? descriptor = current.LayerBrushProviderId != null && current.BrushType != null - ? LayerBrushStore.Get(current.LayerBrushProviderId, current.BrushType)?.LayerBrushDescriptor - : null; - descriptor?.CreateInstance(this); + } General.ShapeType.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation; General.BlendMode.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation; @@ -778,9 +763,9 @@ namespace Artemis.Core if (LayerBrush == null) return; - BaseLayerBrush brush = LayerBrush; + BaseLayerBrush? brush = LayerBrush; LayerBrush = null; - brush.Dispose(); + brush?.Dispose(); OnLayerBrushUpdated(); } diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs index e4b63a567..b3b58b7bb 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs @@ -7,6 +7,11 @@ namespace Artemis.Core /// public class PropertyDescriptionAttribute : Attribute { + /// + /// The identifier of this property used for storage, if not set one will be generated property name in code + /// + public string? Identifier { get; set; } + /// /// The user-friendly name for this property, shown in the UI /// diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs index 98cdb74db..4049688b6 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs @@ -8,12 +8,17 @@ namespace Artemis.Core public class PropertyGroupDescriptionAttribute : Attribute { /// - /// The user-friendly name for this property, shown in the UI. + /// The identifier of this property group used for storage, if not set one will be generated based on the group name in code /// - public string? Name { get; set; } + public string? Identifier { get; set; } /// - /// The user-friendly description for this property, shown in the UI. + /// The user-friendly name for this property group, shown in the UI. + /// + public string? Name { get; set; } + + /// + /// The user-friendly description for this property group, shown in the UI. /// public string? Description { get; set; } } diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs index 52ae56bf1..9a4fd8333 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs @@ -16,6 +16,11 @@ namespace Artemis.Core /// Gets the description attribute applied to this property /// PropertyDescriptionAttribute PropertyDescription { get; } + + /// + /// Gets the profile element (such as layer or folder) this property is applied to + /// + RenderProfileElement ProfileElement { get; } /// /// The parent group of this layer property, set after construction @@ -43,7 +48,7 @@ namespace Artemis.Core public bool DataBindingsSupported { get; } /// - /// Gets the unique path of the property on the layer + /// Gets the unique path of the property on the render element /// string Path { get; } @@ -56,7 +61,7 @@ namespace Artemis.Core /// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied /// bool IsLoadedFromStorage { get; } - + /// /// Initializes the layer property /// @@ -64,7 +69,7 @@ namespace Artemis.Core /// /// /// - void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description, string path); + void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description); /// /// Attempts to load and add the provided keyframe entity to the layer property diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 4ebd5bb61..9cfb5769d 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Text; using Artemis.Storage.Entities.Profile; using Newtonsoft.Json; @@ -27,10 +28,10 @@ namespace Artemis.Core // These are set right after construction to keep the constructor (and inherited constructs) clean ProfileElement = null!; LayerPropertyGroup = null!; - Path = null!; Entity = null!; PropertyDescription = null!; DataBinding = null!; + Path = ""; CurrentValue = default!; DefaultValue = default!; @@ -117,13 +118,11 @@ namespace Artemis.Core } } - /// - /// Gets the profile element (such as layer or folder) this property is applied to - /// - public RenderProfileElement ProfileElement { get; internal set; } + /// + public RenderProfileElement ProfileElement { get; private set; } /// - public LayerPropertyGroup LayerPropertyGroup { get; internal set; } + public LayerPropertyGroup LayerPropertyGroup { get; private set; } #endregion @@ -457,16 +456,18 @@ namespace Artemis.Core internal PropertyEntity Entity { get; set; } /// - public void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description, string path) + public void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); + if (description.Identifier == null) + throw new ArtemisCoreException("Can't initialize a property group without an identifier"); + _isInitialized = true; ProfileElement = profileElement ?? throw new ArgumentNullException(nameof(profileElement)); LayerPropertyGroup = group ?? throw new ArgumentNullException(nameof(group)); - Path = path; Entity = entity ?? throw new ArgumentNullException(nameof(entity)); PropertyDescription = description ?? throw new ArgumentNullException(nameof(description)); IsLoadedFromStorage = fromStorage; @@ -475,6 +476,9 @@ namespace Artemis.Core if (PropertyDescription.DisableKeyframes) KeyframesSupported = false; + // Create the path to this property by walking up the tree + Path = LayerPropertyGroup.Path + "." + description.Identifier; + OnInitialize(); } diff --git a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs index 130f06e87..3cdbeec17 100644 --- a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs +++ b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs @@ -31,9 +31,7 @@ namespace Artemis.Core { // These are set right after construction to keep the constructor (and inherited constructs) clean GroupDescription = null!; - Feature = null!; - ProfileElement = null!; - Path = null!; + Path = ""; _layerProperties = new List(); _layerPropertyGroups = new List(); @@ -42,47 +40,32 @@ namespace Artemis.Core LayerPropertyGroups = new ReadOnlyCollection(_layerPropertyGroups); } - /// - /// Gets the description of this group - /// - public PropertyGroupDescriptionAttribute GroupDescription { get; internal set; } - - /// - /// Gets the plugin feature this group is associated with - /// - public PluginFeature Feature { get; set; } - /// /// Gets the profile element (such as layer or folder) this group is associated with /// - public RenderProfileElement ProfileElement { get; internal set; } + public RenderProfileElement ProfileElement { get; private set; } + + /// + /// Gets the description of this group + /// + public PropertyGroupDescriptionAttribute GroupDescription { get; private set; } /// /// The parent group of this group /// - [LayerPropertyIgnore] + [LayerPropertyIgnore] // Ignore the parent when selecting child groups public LayerPropertyGroup? Parent { get; internal set; } /// - /// The path of this property group + /// Gets the unique path of the property on the render element /// - public string Path { get; internal set; } + public string Path { get; private set; } /// /// Gets whether this property groups properties are all initialized /// public bool PropertiesInitialized { get; private set; } - /// - /// The layer brush this property group belongs to - /// - public BaseLayerBrush? LayerBrush { get; internal set; } - - /// - /// The layer effect this property group belongs to - /// - public BaseLayerEffect? LayerEffect { get; internal set; } - /// /// Gets or sets whether the property is hidden in the UI /// @@ -96,6 +79,11 @@ namespace Artemis.Core } } + /// + /// Gets the entity this property group uses for persistent storage + /// + public PropertyGroupEntity? PropertyGroupEntity { get; internal set; } + /// /// A list of all layer properties in this group /// @@ -194,17 +182,20 @@ namespace Artemis.Core } } - internal void Initialize(RenderProfileElement profileElement, string path, PluginFeature feature) + internal void Initialize(RenderProfileElement profileElement, LayerPropertyGroup? parent, PropertyGroupDescriptionAttribute groupDescription, PropertyGroupEntity? propertyGroupEntity) { - if (path == null) throw new ArgumentNullException(nameof(path)); + if (groupDescription.Identifier == null) + throw new ArtemisCoreException("Can't initialize a property group without an identifier"); // Doubt this will happen but let's make sure if (PropertiesInitialized) throw new ArtemisCoreException("Layer property group already initialized, wut"); - Feature = feature ?? throw new ArgumentNullException(nameof(feature)); - ProfileElement = profileElement ?? throw new ArgumentNullException(nameof(profileElement)); - Path = path.TrimEnd('.'); + ProfileElement = profileElement; + Parent = parent; + GroupDescription = groupDescription; + PropertyGroupEntity = propertyGroupEntity ?? new PropertyGroupEntity {Identifier = groupDescription.Identifier}; + Path = parent != null ? parent.Path + "." + groupDescription.Identifier : groupDescription.Identifier; // Get all properties implementing ILayerProperty or LayerPropertyGroup foreach (PropertyInfo propertyInfo in GetType().GetProperties()) @@ -271,61 +262,68 @@ namespace Artemis.Core private void InitializeProperty(PropertyInfo propertyInfo, PropertyDescriptionAttribute propertyDescription) { - string path = $"{Path}.{propertyInfo.Name}"; - - if (!typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType)) - throw new ArtemisPluginException($"Layer property with PropertyDescription attribute must be of type LayerProperty at {path}"); - - if (!(Activator.CreateInstance(propertyInfo.PropertyType, true) is ILayerProperty instance)) - throw new ArtemisPluginException($"Failed to create instance of layer property at {path}"); - - // Ensure the description has a name, if not this is a good point to set it based on the property info + // Ensure the description has an identifier and name, if not this is a good point to set it based on the property info + if (string.IsNullOrWhiteSpace(propertyDescription.Identifier)) + propertyDescription.Identifier = propertyInfo.Name; if (string.IsNullOrWhiteSpace(propertyDescription.Name)) propertyDescription.Name = propertyInfo.Name.Humanize(); - PropertyEntity entity = GetPropertyEntity(ProfileElement, path, out bool fromStorage); - instance.Initialize(ProfileElement, this, entity, fromStorage, propertyDescription, path); + if (!typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType)) + throw new ArtemisPluginException($"Property with PropertyDescription attribute must be of type ILayerProperty: {propertyDescription.Identifier}"); + if (Activator.CreateInstance(propertyInfo.PropertyType, true) is not ILayerProperty instance) + throw new ArtemisPluginException($"Failed to create instance of layer property: {propertyDescription.Identifier}"); + + PropertyEntity entity = GetPropertyEntity(propertyDescription.Identifier, out bool fromStorage); + instance.Initialize(ProfileElement, this, entity, fromStorage, propertyDescription); propertyInfo.SetValue(this, instance); + _layerProperties.Add(instance); } private void InitializeChildGroup(PropertyInfo propertyInfo, PropertyGroupDescriptionAttribute propertyGroupDescription) { - string path = Path + "."; - - if (!typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType)) - throw new ArtemisPluginException("Layer property with PropertyGroupDescription attribute must be of type LayerPropertyGroup"); - - if (!(Activator.CreateInstance(propertyInfo.PropertyType) is LayerPropertyGroup instance)) - 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 + // Ensure the description has an identifier and name name, if not this is a good point to set it based on the property info + if (string.IsNullOrWhiteSpace(propertyGroupDescription.Identifier)) + propertyGroupDescription.Identifier = propertyInfo.Name; if (string.IsNullOrWhiteSpace(propertyGroupDescription.Name)) propertyGroupDescription.Name = propertyInfo.Name.Humanize(); - instance.Parent = this; - instance.GroupDescription = propertyGroupDescription; - instance.LayerBrush = LayerBrush; - instance.LayerEffect = LayerEffect; - instance.Initialize(ProfileElement, $"{path}{propertyInfo.Name}.", Feature); + if (!typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType)) + throw new ArtemisPluginException($"Property with PropertyGroupDescription attribute must be of type LayerPropertyGroup: {propertyGroupDescription.Identifier}"); + if (!(Activator.CreateInstance(propertyInfo.PropertyType) is LayerPropertyGroup instance)) + throw new ArtemisPluginException($"Failed to create instance of layer property group: {propertyGroupDescription.Identifier}"); + + PropertyGroupEntity entity = GetPropertyGroupEntity(propertyGroupDescription.Identifier); + instance.Initialize(ProfileElement, this, propertyGroupDescription, entity); propertyInfo.SetValue(this, instance); _layerPropertyGroups.Add(instance); } - private PropertyEntity GetPropertyEntity(RenderProfileElement profileElement, string path, out bool fromStorage) + private PropertyEntity GetPropertyEntity(string identifier, out bool fromStorage) { - PropertyEntity? entity = profileElement.RenderElementEntity.PropertyEntities.FirstOrDefault(p => p.FeatureId == Feature.Id && p.Path == path); + if (PropertyGroupEntity == null) + throw new ArtemisCoreException($"Can't execute {nameof(GetPropertyEntity)} without {nameof(PropertyGroupEntity)} being setup"); + + PropertyEntity? entity = PropertyGroupEntity.Properties.FirstOrDefault(p => p.Identifier == identifier); fromStorage = entity != null; if (entity == null) { - entity = new PropertyEntity {FeatureId = Feature.Id, Path = path}; - profileElement.RenderElementEntity.PropertyEntities.Add(entity); + entity = new PropertyEntity {Identifier = identifier}; + PropertyGroupEntity.Properties.Add(entity); } return entity; } + private PropertyGroupEntity GetPropertyGroupEntity(string identifier) + { + if (PropertyGroupEntity == null) + throw new ArtemisCoreException($"Can't execute {nameof(GetPropertyGroupEntity)} without {nameof(PropertyGroupEntity)} being setup"); + + return PropertyGroupEntity.PropertyGroups.FirstOrDefault(g => g.Identifier == identifier) ?? new PropertyGroupEntity() {Identifier = identifier}; + } + /// public void Dispose() { diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index 44066b0db..be2ec4a2b 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -97,20 +97,10 @@ namespace Artemis.Core internal void SaveRenderElement() { RenderElementEntity.LayerEffects.Clear(); - foreach (BaseLayerEffect layerEffect in LayerEffects) + foreach (BaseLayerEffect baseLayerEffect in LayerEffects) { - LayerEffectEntity layerEffectEntity = new() - { - Id = layerEffect.EntityId, - ProviderId = layerEffect.Descriptor?.PlaceholderFor ?? layerEffect.ProviderId, - EffectType = layerEffect.GetEffectTypeName(), - Name = layerEffect.Name, - Suspended = layerEffect.Suspended, - HasBeenRenamed = layerEffect.HasBeenRenamed, - Order = layerEffect.Order - }; - RenderElementEntity.LayerEffects.Add(layerEffectEntity); - layerEffect.BaseProperties?.ApplyToEntity(); + baseLayerEffect.Save(); + RenderElementEntity.LayerEffects.Add(baseLayerEffect.LayerEffectEntity); } // Condition @@ -310,7 +300,7 @@ namespace Artemis.Core foreach (LayerEffectEntity layerEffectEntity in RenderElementEntity.LayerEffects) { // If there is a non-placeholder existing effect, skip this entity - BaseLayerEffect? existing = LayerEffectsList.FirstOrDefault(e => e.EntityId == layerEffectEntity.Id); + BaseLayerEffect? existing = LayerEffectsList.FirstOrDefault(e => e.LayerEffectEntity.Id == layerEffectEntity.Id); if (existing != null && existing.Descriptor.PlaceholderFor == null) continue; @@ -351,7 +341,7 @@ namespace Artemis.Core List pluginEffects = LayerEffectsList.Where(ef => ef.ProviderId == e.Registration.PluginFeature.Id).ToList(); foreach (BaseLayerEffect pluginEffect in pluginEffects) { - LayerEffectEntity entity = RenderElementEntity.LayerEffects.First(en => en.Id == pluginEffect.EntityId); + LayerEffectEntity entity = RenderElementEntity.LayerEffects.First(en => en.Id == pluginEffect.LayerEffectEntity.Id); LayerEffectsList.Remove(pluginEffect); pluginEffect.Dispose(); diff --git a/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs index 40a37b09d..69027def6 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Artemis.Storage.Entities.Profile; using SkiaSharp; namespace Artemis.Core.LayerBrushes @@ -24,6 +25,7 @@ namespace Artemis.Core.LayerBrushes // Both are set right after construction to keep the constructor of inherited classes clean _layer = null!; _descriptor = null!; + LayerBrushEntity = null!; } /// @@ -35,6 +37,11 @@ namespace Artemis.Core.LayerBrushes internal set => SetAndNotify(ref _layer, value); } + /// + /// Gets the brush entity this brush uses for persistent storage + /// + public LayerBrushEntity LayerBrushEntity { get; internal set; } + /// /// Gets the descriptor of this brush /// @@ -185,6 +192,13 @@ namespace Artemis.Core.LayerBrushes public override string BrokenDisplayName => Descriptor.DisplayName; #endregion + + internal void Save() + { + // No need to update the type or provider ID, they're set once by the LayerBrushDescriptors CreateInstance and can't change + BaseProperties?.ApplyToEntity(); + LayerBrushEntity.PropertyGroup = BaseProperties?.PropertyGroupEntity; + } } /// diff --git a/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs index 4225b0455..fca826e2d 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs @@ -1,4 +1,5 @@ using System; +using Artemis.Storage.Entities.Profile; namespace Artemis.Core.LayerBrushes { @@ -32,12 +33,11 @@ namespace Artemis.Core.LayerBrushes internal set => _properties = value; } - internal void InitializeProperties() + internal void InitializeProperties(PropertyGroupEntity? propertyGroupEntity) { Properties = Activator.CreateInstance(); - Properties.GroupDescription = new PropertyGroupDescriptionAttribute {Name = Descriptor.DisplayName, Description = Descriptor.Description}; - Properties.LayerBrush = this; - Properties.Initialize(Layer, "LayerBrush.", Descriptor.Provider); + PropertyGroupDescriptionAttribute groupDescription = new() {Identifier = "Brush", Name = Descriptor.DisplayName, Description = Descriptor.Description}; + Properties.Initialize(Layer, null, groupDescription, propertyGroupEntity); PropertiesInitialized = true; EnableLayerBrush(); diff --git a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs index 9e69380d0..749d64bfa 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs @@ -33,7 +33,7 @@ namespace Artemis.Core.LayerBrushes internal override void Initialize() { - TryOrBreak(InitializeProperties, "Failed to initialize"); + TryOrBreak(() => InitializeProperties(Layer.LayerEntity.LayerBrush?.PropertyGroup), "Failed to initialize"); } } } \ 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 6fb91ca71..8aa7026e0 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs @@ -1,4 +1,5 @@ using System; +using Artemis.Storage.Entities.Profile; using Ninject; namespace Artemis.Core.LayerBrushes @@ -57,23 +58,20 @@ namespace Artemis.Core.LayerBrushes /// /// Creates an instance of the described brush and applies it to the layer /// - internal void CreateInstance(Layer layer) + public BaseLayerBrush CreateInstance(Layer layer, LayerBrushEntity? entity) { - if (layer == null) throw new ArgumentNullException(nameof(layer)); - if (layer.LayerBrush != null) - throw new ArtemisCoreException("Layer already has an instantiated layer brush"); + if (layer == null) + throw new ArgumentNullException(nameof(layer)); BaseLayerBrush brush = (BaseLayerBrush) Provider.Plugin.Kernel!.Get(LayerBrushType); brush.Layer = layer; brush.Descriptor = this; + brush.LayerBrushEntity = entity ?? new LayerBrushEntity { ProviderId = Provider.Id, BrushType = LayerBrushType.FullName }; + brush.Initialize(); brush.Update(0); - layer.LayerBrush = brush; - layer.OnLayerBrushUpdated(); - - if (layer.ShouldBeEnabled) - brush.InternalEnable(); + return brush; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs index 1f1948808..479738e15 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs @@ -77,7 +77,7 @@ namespace Artemis.Core.LayerBrushes internal override void Initialize() { - TryOrBreak(InitializeProperties, "Failed to initialize"); + TryOrBreak(() => InitializeProperties(Layer.LayerEntity.LayerBrush?.PropertyGroup), "Failed to initialize"); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs b/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs index ce32edfd0..24c46589a 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs @@ -1,4 +1,5 @@ using System; +using Artemis.Storage.Entities.Profile; using SkiaSharp; namespace Artemis.Core.LayerEffects @@ -24,16 +25,13 @@ namespace Artemis.Core.LayerEffects _profileElement = null!; _descriptor = null!; _name = null!; + LayerEffectEntity = null!; } /// - /// Gets the unique ID of this effect + /// Gets the /// - public Guid EntityId - { - get => _entityId; - internal set => SetAndNotify(ref _entityId, value); - } + public LayerEffectEntity LayerEffectEntity { get; internal set; } /// /// Gets the profile element (such as layer or folder) this effect is applied to @@ -109,8 +107,6 @@ namespace Artemis.Core.LayerEffects /// public virtual LayerPropertyGroup? BaseProperties => null; - internal string PropertyRootPath => $"LayerEffect.{EntityId}.{GetType().Name}."; - /// /// Gets a boolean indicating whether the layer effect is enabled or not /// @@ -227,5 +223,17 @@ namespace Artemis.Core.LayerEffects public override string BrokenDisplayName => Name; #endregion + + public void Save() + { + // No need to update the ID, type and provider ID. They're set once by the LayerBrushDescriptors CreateInstance and can't change + LayerEffectEntity.Name = Name; + LayerEffectEntity.Suspended = Suspended; + LayerEffectEntity.HasBeenRenamed = HasBeenRenamed; + LayerEffectEntity.Order = Order; + + BaseProperties?.ApplyToEntity(); + LayerEffectEntity.PropertyGroup = BaseProperties?.PropertyGroupEntity; + } } } \ 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 3a1b03e62..8916758e7 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/LayerEffect.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/LayerEffect.cs @@ -36,8 +36,7 @@ namespace Artemis.Core.LayerEffects internal void InitializeProperties() { Properties = Activator.CreateInstance(); - Properties.LayerEffect = this; - Properties.Initialize(ProfileElement, PropertyRootPath, Descriptor.Provider); + Properties.Initialize(ProfileElement, null, new PropertyGroupDescriptionAttribute(){Identifier = "LayerEffect"}, LayerEffectEntity.PropertyGroup); PropertiesInitialized = true; EnableLayerEffect(); diff --git a/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs b/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs index f724f367a..34f501845 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs @@ -54,11 +54,28 @@ namespace Artemis.Core.LayerEffects /// /// Creates an instance of the described effect and applies it to the render element /// - internal void CreateInstance(RenderProfileElement renderElement, LayerEffectEntity entity) + internal void CreateInstance(RenderProfileElement renderElement, LayerEffectEntity? entity) { - // Skip effects already on the element - if (renderElement.LayerEffects.Any(e => e.EntityId == entity.Id)) - return; + if (LayerEffectType == null) + throw new ArtemisCoreException("Cannot create an instance of a layer effect because this descriptor is not a placeholder but is still missing its LayerEffectType"); + + if (entity == null) + { + entity = new LayerEffectEntity + { + Id = Guid.NewGuid(), + Suspended = false, + Order = renderElement.LayerEffects.Count + 1, + ProviderId = Provider.Id, + EffectType = LayerEffectType.FullName + }; + } + else + { + // Skip effects already on the element + if (renderElement.LayerEffects.Any(e => e.LayerEffectEntity.Id == entity.Id)) + return; + } if (PlaceholderFor != null) { @@ -66,12 +83,9 @@ namespace Artemis.Core.LayerEffects return; } - if (LayerEffectType == null) - throw new ArtemisCoreException("Cannot create an instance of a layer effect because this descriptor is not a placeholder but is still missing its LayerEffectType"); - BaseLayerEffect effect = (BaseLayerEffect) Provider.Plugin.Kernel!.Get(LayerEffectType); effect.ProfileElement = renderElement; - effect.EntityId = entity.Id; + effect.LayerEffectEntity = entity; effect.Order = entity.Order; effect.Name = entity.Name; effect.Suspended = entity.Suspended; diff --git a/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffect.cs b/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffect.cs index 07f57491b..a1ec92371 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffect.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffect.cs @@ -13,7 +13,7 @@ namespace Artemis.Core.LayerEffects.Placeholder OriginalEntity = originalEntity; PlaceholderFor = placeholderFor; - EntityId = OriginalEntity.Id; + LayerEffectEntity = originalEntity; Order = OriginalEntity.Order; Name = OriginalEntity.Name; Suspended = OriginalEntity.Suspended; diff --git a/src/Artemis.Storage/Entities/Profile/LayerBrushEntity.cs b/src/Artemis.Storage/Entities/Profile/LayerBrushEntity.cs new file mode 100644 index 000000000..1e99d1318 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/LayerBrushEntity.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Artemis.Storage.Entities.Profile; + +public class LayerBrushEntity +{ + public string ProviderId { get; set; } + public string BrushType { get; set; } + + public PropertyGroupEntity PropertyGroup { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/LayerEffectEntity.cs b/src/Artemis.Storage/Entities/Profile/LayerEffectEntity.cs index a575e24a9..236d1d2fb 100644 --- a/src/Artemis.Storage/Entities/Profile/LayerEffectEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/LayerEffectEntity.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Artemis.Storage.Entities.Profile { @@ -11,5 +12,7 @@ namespace Artemis.Storage.Entities.Profile public bool Suspended { get; set; } public bool HasBeenRenamed { get; set; } public int Order { get; set; } + + public PropertyGroupEntity PropertyGroup { get; set; } } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/LayerEntity.cs b/src/Artemis.Storage/Entities/Profile/LayerEntity.cs index 4be0f1d02..1c8ff29ea 100644 --- a/src/Artemis.Storage/Entities/Profile/LayerEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/LayerEntity.cs @@ -24,9 +24,12 @@ namespace Artemis.Storage.Entities.Profile public List Leds { get; set; } public List AdaptionHints { get; set; } + public PropertyGroupEntity GeneralPropertyGroup { get; set; } + public PropertyGroupEntity TransformPropertyGroup { get; set; } + public LayerBrushEntity LayerBrush { get; set; } + [BsonRef("ProfileEntity")] public ProfileEntity Profile { get; set; } - public Guid ProfileId { get; set; } } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs b/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs index ec5fc0344..21036efcc 100644 --- a/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs @@ -1,22 +1,14 @@ using System.Collections.Generic; using Artemis.Storage.Entities.Profile.DataBindings; -namespace Artemis.Storage.Entities.Profile +namespace Artemis.Storage.Entities.Profile; + +public class PropertyEntity { - public class PropertyEntity - { - public PropertyEntity() - { - KeyframeEntities = new List(); - } + public string Identifier { get; set; } + public string Value { get; set; } + public bool KeyframesEnabled { get; set; } - public string FeatureId { get; set; } - public string Path { get; set; } - public DataBindingEntity DataBinding { get; set; } - - public string Value { get; set; } - public bool KeyframesEnabled { get; set; } - - public List KeyframeEntities { get; set; } - } + public DataBindingEntity DataBinding { get; set; } + public List KeyframeEntities { get; set; } = new(); } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/PropertyGroupEntity.cs b/src/Artemis.Storage/Entities/Profile/PropertyGroupEntity.cs new file mode 100644 index 000000000..3f4825782 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/PropertyGroupEntity.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Artemis.Storage.Entities.Profile; + +public class PropertyGroupEntity +{ + public string Identifier { get; set; } + public List Properties { get; set; } = new(); + public List PropertyGroups { get; set; } = new(); +} \ No newline at end of file diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs index bdea29e96..2b4be9532 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs @@ -49,13 +49,13 @@ namespace Artemis.UI.DefaultTypes.PropertyInput { if (LayerProperty.ProfileElement is Layer layer) { - layer.ChangeLayerBrush(SelectedDescriptor); - if (layer.LayerBrush?.Presets != null && layer.LayerBrush.Presets.Any()) - Execute.PostToUIThread(async () => - { - await Task.Delay(400); - await _dialogService.ShowDialogAt("LayerProperties", new Dictionary {{"layerBrush", layer.LayerBrush}}); - }); + // layer.ChangeLayerBrush(SelectedDescriptor); + // if (layer.LayerBrush?.Presets != null && layer.LayerBrush.Presets.Any()) + // Execute.PostToUIThread(async () => + // { + // await Task.Delay(400); + // await _dialogService.ShowDialogAt("LayerProperties", new Dictionary {{"layerBrush", layer.LayerBrush}}); + // }); } } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs index 4c506471b..41c7a1a23 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs @@ -332,19 +332,19 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties if (SelectedProfileElement == null) return; - // Remove VMs of effects no longer applied on the layer - List toRemove = Items - .Where(l => l.LayerPropertyGroup.LayerEffect != null && !SelectedProfileElement.LayerEffects.Contains(l.LayerPropertyGroup.LayerEffect)) - .ToList(); - Items.RemoveRange(toRemove); - - foreach (BaseLayerEffect layerEffect in SelectedProfileElement.LayerEffects) - { - if (Items.Any(l => l.LayerPropertyGroup.LayerEffect == layerEffect) || layerEffect.BaseProperties == null) - continue; - - Items.Add(_layerPropertyVmFactory.LayerPropertyGroupViewModel(layerEffect.BaseProperties)); - } + // // Remove VMs of effects no longer applied on the layer + // List toRemove = Items + // .Where(l => l.LayerPropertyGroup.LayerEffect != null && !SelectedProfileElement.LayerEffects.Contains(l.LayerPropertyGroup.LayerEffect)) + // .ToList(); + // Items.RemoveRange(toRemove); + // + // foreach (BaseLayerEffect layerEffect in SelectedProfileElement.LayerEffects) + // { + // if (Items.Any(l => l.LayerPropertyGroup.LayerEffect == layerEffect) || layerEffect.BaseProperties == null) + // continue; + // + // Items.Add(_layerPropertyVmFactory.LayerPropertyGroupViewModel(layerEffect.BaseProperties)); + // } SortProperties(); } @@ -355,11 +355,11 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties List nonEffectProperties = Items .Where(l => l.TreeGroupViewModel.GroupType != LayerEffectRoot) .ToList(); - // Order the effects - List effectProperties = Items - .Where(l => l.TreeGroupViewModel.GroupType == LayerEffectRoot && l.LayerPropertyGroup.LayerEffect != null) - .OrderBy(l => l.LayerPropertyGroup.LayerEffect.Order) - .ToList(); + // // Order the effects + // List effectProperties = Items + // .Where(l => l.TreeGroupViewModel.GroupType == LayerEffectRoot && l.LayerPropertyGroup.LayerEffect != null) + // .OrderBy(l => l.LayerPropertyGroup.LayerEffect.Order) + // .ToList(); // Put the non-effect properties in front for (int index = 0; index < nonEffectProperties.Count; index++) @@ -369,13 +369,13 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties ((BindableCollection) Items).Move(Items.IndexOf(layerPropertyGroupViewModel), index); } - // Put the effect properties after, sorted by their order - for (int index = 0; index < effectProperties.Count; index++) - { - LayerPropertyGroupViewModel layerPropertyGroupViewModel = effectProperties[index]; - if (Items.IndexOf(layerPropertyGroupViewModel) != index + nonEffectProperties.Count) - ((BindableCollection) Items).Move(Items.IndexOf(layerPropertyGroupViewModel), index + nonEffectProperties.Count); - } + // // Put the effect properties after, sorted by their order + // for (int index = 0; index < effectProperties.Count; index++) + // { + // LayerPropertyGroupViewModel layerPropertyGroupViewModel = effectProperties[index]; + // if (Items.IndexOf(layerPropertyGroupViewModel) != index + nonEffectProperties.Count) + // ((BindableCollection) Items).Move(Items.IndexOf(layerPropertyGroupViewModel), index + nonEffectProperties.Count); + // } } public async void ToggleEffectsViewModel() diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertyGroupViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertyGroupViewModel.cs index 84e28c8a3..ce26802e1 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertyGroupViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertyGroupViewModel.cs @@ -87,8 +87,8 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties public void UpdateOrder(int order) { - if (LayerPropertyGroup.LayerEffect != null) - LayerPropertyGroup.LayerEffect.Order = order; + // if (LayerPropertyGroup.LayerEffect != null) + // LayerPropertyGroup.LayerEffect.Order = order; NotifyOfPropertyChange(nameof(IsExpanded)); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Models/KeyframeClipboardModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Models/KeyframeClipboardModel.cs index c05e3f26b..caaebf9bb 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Models/KeyframeClipboardModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Models/KeyframeClipboardModel.cs @@ -59,7 +59,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Models public KeyframeClipboardModel(ILayerPropertyKeyframe layerPropertyKeyframe) { - FeatureId = layerPropertyKeyframe.UntypedLayerProperty.LayerPropertyGroup.Feature.Id; + // FeatureId = layerPropertyKeyframe.UntypedLayerProperty.LayerPropertyGroup.Feature.Id; Path = layerPropertyKeyframe.UntypedLayerProperty.Path; KeyframeEntity = layerPropertyKeyframe.GetKeyframeEntity(); } @@ -70,15 +70,15 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Models public ILayerPropertyKeyframe Paste(List properties, TimeSpan offset) { - ILayerProperty property = properties.FirstOrDefault(p => p.LayerPropertyGroup.Feature.Id == FeatureId && p.Path == Path); - if (property != null) - { - KeyframeEntity.Position += offset; - ILayerPropertyKeyframe keyframe = property.AddKeyframeEntity(KeyframeEntity); - KeyframeEntity.Position -= offset; - - return keyframe; - } + // ILayerProperty property = properties.FirstOrDefault(p => p.LayerPropertyGroup.Feature.Id == FeatureId && p.Path == Path); + // if (property != null) + // { + // KeyframeEntity.Position += offset; + // ILayerPropertyKeyframe keyframe = property.AddKeyframeEntity(KeyframeEntity); + // KeyframeEntity.Position -= offset; + // + // return keyframe; + // } return null; } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeGroupViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeGroupViewModel.cs index 01b7417db..cf003c3aa 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeGroupViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeGroupViewModel.cs @@ -56,94 +56,94 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree ? LayerPropertyGroupViewModel.Items : null; - public void OpenBrushSettings() - { - BaseLayerBrush layerBrush = LayerPropertyGroup.LayerBrush; - LayerBrushConfigurationDialog configurationViewModel = (LayerBrushConfigurationDialog) layerBrush.ConfigurationDialog; - if (configurationViewModel == null) - return; + // public void OpenBrushSettings() + // { + // BaseLayerBrush layerBrush = LayerPropertyGroup.LayerBrush; + // LayerBrushConfigurationDialog configurationViewModel = (LayerBrushConfigurationDialog) layerBrush.ConfigurationDialog; + // if (configurationViewModel == null) + // return; + // + // try + // { + // // Limit to one constructor, there's no need to have more and it complicates things anyway + // ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors(); + // if (constructors.Length != 1) + // throw new ArtemisUIException("Brush configuration dialogs must have exactly one constructor"); + // + // // Find the BaseLayerBrush parameter, it is required by the base constructor so its there for sure + // ParameterInfo brushParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerBrush).IsAssignableFrom(p.ParameterType)); + // ConstructorArgument argument = new(brushParameter.Name, layerBrush); + // BrushConfigurationViewModel viewModel = (BrushConfigurationViewModel) layerBrush.Descriptor.Provider.Plugin.Kernel.Get(configurationViewModel.Type, argument); + // + // _layerBrushSettingsWindowVm = new LayerBrushSettingsWindowViewModel(viewModel, configurationViewModel); + // _windowManager.ShowDialog(_layerBrushSettingsWindowVm); + // + // // Save changes after the dialog closes + // _profileEditorService.SaveSelectedProfileConfiguration(); + // } + // catch (Exception e) + // { + // _dialogService.ShowExceptionDialog("An exception occured while trying to show the brush's settings window", e); + // } + // } - try - { - // Limit to one constructor, there's no need to have more and it complicates things anyway - ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors(); - if (constructors.Length != 1) - throw new ArtemisUIException("Brush configuration dialogs must have exactly one constructor"); + // public void OpenEffectSettings() + // { + // BaseLayerEffect layerEffect = LayerPropertyGroup.LayerEffect; + // LayerEffectConfigurationDialog configurationViewModel = (LayerEffectConfigurationDialog) layerEffect.ConfigurationDialog; + // if (configurationViewModel == null) + // return; + // + // try + // { + // // Limit to one constructor, there's no need to have more and it complicates things anyway + // ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors(); + // if (constructors.Length != 1) + // throw new ArtemisUIException("Effect configuration dialogs must have exactly one constructor"); + // + // ParameterInfo effectParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerEffect).IsAssignableFrom(p.ParameterType)); + // ConstructorArgument argument = new(effectParameter.Name, layerEffect); + // EffectConfigurationViewModel viewModel = (EffectConfigurationViewModel) layerEffect.Descriptor.Provider.Plugin.Kernel.Get(configurationViewModel.Type, argument); + // + // _layerEffectSettingsWindowVm = new LayerEffectSettingsWindowViewModel(viewModel, configurationViewModel); + // _windowManager.ShowDialog(_layerEffectSettingsWindowVm); + // + // // Save changes after the dialog closes + // _profileEditorService.SaveSelectedProfileConfiguration(); + // } + // catch (Exception e) + // { + // _dialogService.ShowExceptionDialog("An exception occured while trying to show the effect's settings window", e); + // throw; + // } + // } - // Find the BaseLayerBrush parameter, it is required by the base constructor so its there for sure - ParameterInfo brushParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerBrush).IsAssignableFrom(p.ParameterType)); - ConstructorArgument argument = new(brushParameter.Name, layerBrush); - BrushConfigurationViewModel viewModel = (BrushConfigurationViewModel) layerBrush.Descriptor.Provider.Plugin.Kernel.Get(configurationViewModel.Type, argument); - - _layerBrushSettingsWindowVm = new LayerBrushSettingsWindowViewModel(viewModel, configurationViewModel); - _windowManager.ShowDialog(_layerBrushSettingsWindowVm); - - // Save changes after the dialog closes - _profileEditorService.SaveSelectedProfileConfiguration(); - } - catch (Exception e) - { - _dialogService.ShowExceptionDialog("An exception occured while trying to show the brush's settings window", e); - } - } - - public void OpenEffectSettings() - { - BaseLayerEffect layerEffect = LayerPropertyGroup.LayerEffect; - LayerEffectConfigurationDialog configurationViewModel = (LayerEffectConfigurationDialog) layerEffect.ConfigurationDialog; - if (configurationViewModel == null) - return; - - try - { - // Limit to one constructor, there's no need to have more and it complicates things anyway - ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors(); - if (constructors.Length != 1) - throw new ArtemisUIException("Effect configuration dialogs must have exactly one constructor"); - - ParameterInfo effectParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerEffect).IsAssignableFrom(p.ParameterType)); - ConstructorArgument argument = new(effectParameter.Name, layerEffect); - EffectConfigurationViewModel viewModel = (EffectConfigurationViewModel) layerEffect.Descriptor.Provider.Plugin.Kernel.Get(configurationViewModel.Type, argument); - - _layerEffectSettingsWindowVm = new LayerEffectSettingsWindowViewModel(viewModel, configurationViewModel); - _windowManager.ShowDialog(_layerEffectSettingsWindowVm); - - // Save changes after the dialog closes - _profileEditorService.SaveSelectedProfileConfiguration(); - } - catch (Exception e) - { - _dialogService.ShowExceptionDialog("An exception occured while trying to show the effect's settings window", e); - throw; - } - } - - public async void RenameEffect() - { - object result = await _dialogService.ShowDialogAt( - "PropertyTreeDialogHost", - new Dictionary - { - {"subject", "effect"}, - {"currentName", LayerPropertyGroup.LayerEffect.Name} - } - ); - if (result is string newName) - { - LayerPropertyGroup.LayerEffect.Name = newName; - LayerPropertyGroup.LayerEffect.HasBeenRenamed = true; - _profileEditorService.SaveSelectedProfileConfiguration(); - } - } - - public void DeleteEffect() - { - if (LayerPropertyGroup.LayerEffect == null) - return; - - LayerPropertyGroup.ProfileElement.RemoveLayerEffect(LayerPropertyGroup.LayerEffect); - _profileEditorService.SaveSelectedProfileConfiguration(); - } + // public async void RenameEffect() + // { + // object result = await _dialogService.ShowDialogAt( + // "PropertyTreeDialogHost", + // new Dictionary + // { + // {"subject", "effect"}, + // {"currentName", LayerPropertyGroup.LayerEffect.Name} + // } + // ); + // if (result is string newName) + // { + // LayerPropertyGroup.LayerEffect.Name = newName; + // LayerPropertyGroup.LayerEffect.HasBeenRenamed = true; + // _profileEditorService.SaveSelectedProfileConfiguration(); + // } + // } + // + // public void DeleteEffect() + // { + // if (LayerPropertyGroup.LayerEffect == null) + // return; + // + // LayerPropertyGroup.ProfileElement.RemoveLayerEffect(LayerPropertyGroup.LayerEffect); + // _profileEditorService.SaveSelectedProfileConfiguration(); + // } public void SuspendedToggled() { @@ -175,10 +175,10 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree GroupType = LayerPropertyGroupType.General; else if (LayerPropertyGroup is LayerTransformProperties) GroupType = LayerPropertyGroupType.Transform; - else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerBrush != null) - GroupType = LayerPropertyGroupType.LayerBrushRoot; - else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerEffect != null) - GroupType = LayerPropertyGroupType.LayerEffectRoot; + // else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerBrush != null) + // GroupType = LayerPropertyGroupType.LayerBrushRoot; + // else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerEffect != null) + // GroupType = LayerPropertyGroupType.LayerEffectRoot; else GroupType = LayerPropertyGroupType.None; } diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs index ae3ed6dde..fb6a06c69 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs @@ -151,8 +151,8 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem ProfileElement.AddChild(layer, 0); // Could be null if the default brush got disabled LayerBrushDescriptor brush = _layerBrushService.GetDefaultLayerBrush(); - if (brush != null) - layer.ChangeLayerBrush(brush); + // if (brush != null) + // layer.ChangeLayerBrush(brush); layer.AddLeds(_rgbService.EnabledDevices.SelectMany(d => d.Leds)); _profileEditorService.SaveSelectedProfileConfiguration(); diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs index 9e56b678c..45181bd84 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs @@ -93,8 +93,8 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization.Tools folder.AddChild(newLayer); LayerBrushDescriptor brush = _layerBrushService.GetDefaultLayerBrush(); - if (brush != null) - newLayer.ChangeLayerBrush(brush); + // if (brush != null) + // newLayer.ChangeLayerBrush(brush); newLayer.AddLeds(selectedLeds); ProfileEditorService.ChangeSelectedProfileElement(newLayer); diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeLayerBrush.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeLayerBrush.cs index 2032431a6..a93d99142 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeLayerBrush.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeLayerBrush.cs @@ -1,4 +1,5 @@ -using Artemis.Core; +using System; +using Artemis.Core; using Artemis.Core.LayerBrushes; namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; @@ -6,11 +7,14 @@ namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; /// /// Represents a profile editor command that can be used to change the brush of a layer. /// -public class ChangeLayerBrush : IProfileEditorCommand +public class ChangeLayerBrush : IProfileEditorCommand, IDisposable { private readonly Layer _layer; private readonly LayerBrushDescriptor _layerBrushDescriptor; - private readonly LayerBrushDescriptor? _previousDescriptor; + private readonly BaseLayerBrush? _previousBrush; + + private BaseLayerBrush? _newBrush; + private bool _executed; /// /// Creates a new instance of the class. @@ -19,7 +23,7 @@ public class ChangeLayerBrush : IProfileEditorCommand { _layer = layer; _layerBrushDescriptor = layerBrushDescriptor; - _previousDescriptor = layer.LayerBrush?.Descriptor; + _previousBrush = _layer.LayerBrush; } #region Implementation of IProfileEditorCommand @@ -30,14 +34,32 @@ public class ChangeLayerBrush : IProfileEditorCommand /// public void Execute() { - _layer.ChangeLayerBrush(_layerBrushDescriptor); + // Create the new brush + _newBrush ??= _layerBrushDescriptor.CreateInstance(_layer, null); + // Change the brush to the new brush + _layer.ChangeLayerBrush(_newBrush); + + _executed = true; } /// public void Undo() { - if (_previousDescriptor != null) - _layer.ChangeLayerBrush(_previousDescriptor); + _layer.ChangeLayerBrush(_previousBrush); + _executed = false; + } + + #endregion + + #region IDisposable + + /// + public void Dispose() + { + if (_executed) + _previousBrush?.Dispose(); + else + _newBrush?.Dispose(); } #endregion diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs index b759fe8cc..aaf57c0be 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs @@ -3,22 +3,86 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.UI.Shared.Services.Interfaces; -namespace Artemis.UI.Shared.Services.ProfileEditor +namespace Artemis.UI.Shared.Services.ProfileEditor; + +/// +/// Provides access the the profile editor back-end logic. +/// +public interface IProfileEditorService : IArtemisSharedUIService { - public interface IProfileEditorService : IArtemisSharedUIService - { - IObservable ProfileConfiguration { get; } - IObservable ProfileElement { get; } - IObservable History { get; } - IObservable Time { get; } - IObservable PixelsPerSecond { get; } + /// + /// Gets an observable of the currently selected profile configuration. + /// + IObservable ProfileConfiguration { get; } - void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration); - void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement); - void ChangeTime(TimeSpan time); + /// + /// Gets an observable of the currently selected profile element. + /// + IObservable ProfileElement { get; } - void ExecuteCommand(IProfileEditorCommand command); - void SaveProfile(); - Task SaveProfileAsync(); - } + /// + /// Gets an observable of the current editor history. + /// + IObservable History { get; } + + /// + /// Gets an observable of the profile preview playback time. + /// + IObservable Time { get; } + + /// + /// Gets an observable of the profile preview playing state. + /// + IObservable Playing { get; } + + /// + /// Gets an observable of the zoom level. + /// + IObservable PixelsPerSecond { get; } + + + /// + /// Changes the selected profile by its . + /// + /// The profile configuration of the profile to select. + void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration); + + /// + /// Changes the selected profile element. + /// + /// The profile element to select. + void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement); + + /// + /// Changes the current profile preview playback time. + /// + /// The new time. + void ChangeTime(TimeSpan time); + + /// + /// Executes the provided command and adds it to the history. + /// + /// The command to execute. + void ExecuteCommand(IProfileEditorCommand command); + + /// + /// Saves the current profile. + /// + void SaveProfile(); + + /// + /// Asynchronously saves the current profile. + /// + /// A task representing the save action. + Task SaveProfileAsync(); + + /// + /// Resumes profile preview playback. + /// + void Play(); + + /// + /// Pauses profile preview playback. + /// + void Pause(); } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs index 0441f068f..ec9d5f05e 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Shared.Services.Interfaces; +using Serilog; namespace Artemis.UI.Shared.Services.ProfileEditor; @@ -15,19 +16,27 @@ internal class ProfileEditorService : IProfileEditorService private readonly Dictionary _profileEditorHistories = new(); private readonly BehaviorSubject _profileElementSubject = new(null); private readonly BehaviorSubject _timeSubject = new(TimeSpan.Zero); + private readonly BehaviorSubject _playingSubject = new(false); + private readonly BehaviorSubject _suspendedEditingSubject = new(false); private readonly BehaviorSubject _pixelsPerSecondSubject = new(300); + private readonly ILogger _logger; private readonly IProfileService _profileService; + private readonly IModuleService _moduleService; private readonly IWindowService _windowService; - public ProfileEditorService(IProfileService profileService, IWindowService windowService) + public ProfileEditorService(ILogger logger, IProfileService profileService, IModuleService moduleService, IWindowService windowService) { + _logger = logger; _profileService = profileService; + _moduleService = moduleService; _windowService = windowService; ProfileConfiguration = _profileConfigurationSubject.AsObservable(); ProfileElement = _profileElementSubject.AsObservable(); History = Observable.Defer(() => Observable.Return(GetHistory(_profileConfigurationSubject.Value))).Concat(ProfileConfiguration.Select(GetHistory)); Time = _timeSubject.AsObservable(); + Playing = _playingSubject.AsObservable(); + SuspendedEditing = _suspendedEditingSubject.AsObservable(); PixelsPerSecond = _pixelsPerSecondSubject.AsObservable(); } @@ -47,10 +56,46 @@ internal class ProfileEditorService : IProfileEditorService public IObservable ProfileElement { get; } public IObservable History { get; } public IObservable Time { get; } + public IObservable Playing { get; } + public IObservable SuspendedEditing { get; } public IObservable PixelsPerSecond { get; } public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration) { + if (ReferenceEquals(_profileConfigurationSubject.Value, profileConfiguration)) + return; + + _logger.Verbose("ChangeCurrentProfileConfiguration {profile}", profileConfiguration); + + // Stop playing and save the current profile + Pause(); + if (_profileConfigurationSubject.Value?.Profile != null) + _profileConfigurationSubject.Value.Profile.LastSelectedProfileElement = _profileElementSubject.Value; + SaveProfile(); + + // No need to deactivate the profile, if needed it will be deactivated next update + if (_profileConfigurationSubject.Value != null) + _profileConfigurationSubject.Value.IsBeingEdited = false; + + // Deselect whatever profile element was active + ChangeCurrentProfileElement(null); + + // The new profile may need activation + if (profileConfiguration != null) + { + profileConfiguration.IsBeingEdited = true; + _moduleService.SetActivationOverride(profileConfiguration.Module); + _profileService.ActivateProfile(profileConfiguration); + _profileService.RenderForEditor = true; + + if (profileConfiguration.Profile?.LastSelectedProfileElement is RenderProfileElement renderProfileElement) + ChangeCurrentProfileElement(renderProfileElement); + } + else + { + _moduleService.SetActivationOverride(null); + _profileService.RenderForEditor = false; + } _profileConfigurationSubject.OnNext(profileConfiguration); } @@ -61,6 +106,7 @@ internal class ProfileEditorService : IProfileEditorService public void ChangeTime(TimeSpan time) { + Tick(time); _timeSubject.OnNext(time); } @@ -101,4 +147,48 @@ internal class ProfileEditorService : IProfileEditorService { await Task.Run(SaveProfile); } + + /// + public void Play() + { + if (!_playingSubject.Value) + _playingSubject.OnNext(true); + } + + /// + public void Pause() + { + if (_playingSubject.Value) + _playingSubject.OnNext(false); + } + + private void Tick(TimeSpan time) + { + if (_profileConfigurationSubject.Value?.Profile == null || _suspendedEditingSubject.Value) + return; + + TickProfileElement(_profileConfigurationSubject.Value.Profile.GetRootFolder(), time); + } + + private void TickProfileElement(ProfileElement profileElement, TimeSpan time) + { + if (profileElement is not RenderProfileElement renderElement) + return; + + if (renderElement.Suspended) + { + renderElement.Disable(); + } + else + { + renderElement.Enable(); + renderElement.Timeline.Override( + time, + (renderElement != _profileElementSubject.Value || renderElement.Timeline.Length < time) && renderElement.Timeline.PlayMode == TimelinePlayMode.Repeat + ); + + foreach (ProfileElement child in renderElement.Children) + TickProfileElement(child, time); + } + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml index b359ef18f..302197215 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml @@ -24,8 +24,8 @@ - - + + diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/TextBox.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Condensed.axaml similarity index 61% rename from src/Avalonia/Artemis.UI.Shared/Styles/TextBox.axaml rename to src/Avalonia/Artemis.UI.Shared/Styles/Condensed.axaml index 5ce52297f..a2aad151b 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/TextBox.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Condensed.axaml @@ -1,6 +1,7 @@  + xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:controls1="clr-namespace:Artemis.UI.Shared.Controls"> @@ -24,6 +25,9 @@ Bluheheheheh Bluhgfdgdsheheh + + + @@ -31,19 +35,34 @@ + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj index e9c0b05c0..f6b0cd9c0 100644 --- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Avalonia/Artemis.UI/Assets/Images/Logo/application.ico b/src/Avalonia/Artemis.UI/Assets/Images/Logo/application.ico new file mode 100644 index 000000000..bdb89dbd0 Binary files /dev/null and b/src/Avalonia/Artemis.UI/Assets/Images/Logo/application.ico differ diff --git a/src/Avalonia/Artemis.UI/Converters/SKColorToColor2Converter.cs b/src/Avalonia/Artemis.UI/Converters/SKColorToColor2Converter.cs new file mode 100644 index 000000000..bd27466a6 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Converters/SKColorToColor2Converter.cs @@ -0,0 +1,29 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using FluentAvalonia.UI.Media; +using SkiaSharp; + +namespace Artemis.UI.Converters; + +/// +/// Converts into . +/// +public class SKColorToColor2Converter : IValueConverter +{ + /// + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is SKColor skColor) + return new Color2(skColor.Red, skColor.Green, skColor.Blue, skColor.Alpha); + return new Color2(); + } + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is Color2 color2) + return new SKColor(color2.R, color2.G, color2.B, color2.A); + return SKColor.Empty; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Converters/SKColorToStringConverter.cs b/src/Avalonia/Artemis.UI/Converters/SKColorToStringConverter.cs new file mode 100644 index 000000000..e7707d8f1 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Converters/SKColorToStringConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using SkiaSharp; + +namespace Artemis.UI.Converters; + +/// +/// +/// Converts into . +/// +public class SKColorToStringConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value?.ToString(); + } + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (string.IsNullOrWhiteSpace(value as string)) + return SKColor.Empty; + + return SKColor.TryParse((string) value!, out SKColor color) ? color : SKColor.Empty; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml index 84c152c51..8452fec0f 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml @@ -2,7 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.DefaultTypes.PropertyInput.BoolPropertyInputView"> - TODO + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputViewModel.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputViewModel.cs index 60a4fbc8a..7d98828d3 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputViewModel.cs +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputViewModel.cs @@ -1,13 +1,31 @@ -using Artemis.Core; +using System; +using Artemis.Core; using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.PropertyInput; +using ReactiveUI; namespace Artemis.UI.DefaultTypes.PropertyInput; public class BoolPropertyInputViewModel : PropertyInputViewModel { + private BooleanOptions _selectedBooleanOption; + public BoolPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService) : base(layerProperty, profileEditorService, propertyInputService) { + this.WhenAnyValue(vm => vm.InputValue).Subscribe(v => SelectedBooleanOption = v ? BooleanOptions.True : BooleanOptions.False); + this.WhenAnyValue(vm => vm.SelectedBooleanOption).Subscribe(v => InputValue = v == BooleanOptions.True); } + + public BooleanOptions SelectedBooleanOption + { + get => _selectedBooleanOption; + set => this.RaiseAndSetIfChanged(ref _selectedBooleanOption, value); + } +} + +public enum BooleanOptions +{ + True, + False } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml index adab728e6..ec3832869 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml @@ -23,7 +23,7 @@ diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs index 796b1e439..41059cafa 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs @@ -66,6 +66,16 @@ public class BrushPropertyInputViewModel : PropertyInputViewModel _windowService.CreateContentDialog().WithViewModel(out LayerBrushPresetViewModel _, ("layerBrush", layer.LayerBrush)).ShowAsync()); } + #region Overrides of PropertyInputViewModel + + /// + protected override void OnInputValueChanged() + { + this.RaisePropertyChanged(nameof(SelectedDescriptor)); + } + + #endregion + private void UpdateDescriptorsIfChanged(PluginFeatureEventArgs e) { if (e.PluginFeature is not LayerBrushProvider) diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputView.axaml index c8d1d0490..f04e9e77c 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputView.axaml +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputView.axaml @@ -2,7 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.DefaultTypes.PropertyInput.EnumPropertyInputView"> - TODO - + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml index 7ac5dc871..233631978 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml @@ -2,7 +2,31 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:converters="clr-namespace:Artemis.UI.Converters" + xmlns:shared="clr-namespace:Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.DefaultTypes.PropertyInput.SKColorPropertyInputView"> - TODO - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/MainWindow.axaml b/src/Avalonia/Artemis.UI/MainWindow.axaml index fb57c2f6f..590aef1a0 100644 --- a/src/Avalonia/Artemis.UI/MainWindow.axaml +++ b/src/Avalonia/Artemis.UI/MainWindow.axaml @@ -5,7 +5,7 @@ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.MainWindow" - Icon="/Assets/Images/Logo/bow.ico" + Icon="/Assets/Images/Logo/application.ico" Title="Artemis 2.0"> diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs index ffbda5e3a..38b9dd0fc 100644 --- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -1,5 +1,7 @@ using System.Collections.ObjectModel; using Artemis.Core; +using Artemis.Core.LayerBrushes; +using Artemis.Core.LayerEffects; using Artemis.UI.Screens.Device; using Artemis.UI.Screens.Plugins; using Artemis.UI.Screens.ProfileEditor; @@ -67,6 +69,8 @@ namespace Artemis.UI.Ninject.Factories { ProfileElementPropertyViewModel ProfileElementPropertyViewModel(ILayerProperty layerProperty); ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup); + ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush layerBrush); + ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerEffect layerEffect); TreeGroupViewModel TreeGroupViewModel(ProfileElementPropertyGroupViewModel profileElementPropertyGroupViewModel); // TimelineGroupViewModel TimelineGroupViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel); diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml index 6c7eff869..325cd5362 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml @@ -7,7 +7,7 @@ xmlns:reactiveUi="http://reactiveui.net" mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800" x:Class="Artemis.UI.Screens.Debugger.DebugView" - Icon="/Assets/Images/Logo/bow.ico" + Icon="/Assets/Images/Logo/application.ico" Title="Artemis | Debugger" Width="1200" Height="800"> diff --git a/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml b/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml index 96cdfc9b7..7a27ae925 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml @@ -6,7 +6,7 @@ xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800" x:Class="Artemis.UI.Screens.Device.DevicePropertiesView" - Icon="/Assets/Images/Logo/bow.ico" + Icon="/Assets/Images/Logo/application.ico" Title="Artemis | Device Properties" Width="1250" Height="900"> diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml index 988cb4734..7bfea49dc 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml @@ -5,7 +5,7 @@ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Plugins.PluginSettingsWindowView" - Icon="/Assets/Images/Logo/bow.ico" + Icon="/Assets/Images/Logo/application.ico" Title="{Binding DisplayName}" Width="800" Height="800" diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackView.axaml new file mode 100644 index 000000000..e180d3ab9 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackView.axaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + Don't repeat the timeline + + + This setting only applies to the editor and does not affect the repeat mode during normal profile playback + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackView.axaml.cs new file mode 100644 index 000000000..ed5b99c6b --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.Playback +{ + public partial class PlaybackView : ReactiveUserControl + { + public PlaybackView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs new file mode 100644 index 000000000..70c099028 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs @@ -0,0 +1,193 @@ +using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.ProfileEditor; +using ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.Playback; + +public class PlaybackViewModel : ActivatableViewModelBase +{ + private readonly ICoreService _coreService; + private readonly IProfileEditorService _profileEditorService; + private readonly ISettingsService _settingsService; + private RenderProfileElement? _profileElement; + private ObservableAsPropertyHelper? _currentTime; + private ObservableAsPropertyHelper? _formattedCurrentTime; + private ObservableAsPropertyHelper? _playing; + private bool _repeating; + private bool _repeatTimeline; + private bool _repeatSegment; + + public PlaybackViewModel(ICoreService coreService, IProfileEditorService profileEditorService, ISettingsService settingsService) + { + _coreService = coreService; + _profileEditorService = profileEditorService; + _settingsService = settingsService; + + this.WhenActivated(d => + { + _profileEditorService.ProfileElement.Subscribe(e => _profileElement = e).DisposeWith(d); + _currentTime = _profileEditorService.Time.ToProperty(this, vm => vm.CurrentTime).DisposeWith(d); + _formattedCurrentTime = _profileEditorService.Time.Select(t => $"{Math.Floor(t.TotalSeconds):00}.{t.Milliseconds:000}").ToProperty(this, vm => vm.FormattedCurrentTime).DisposeWith(d); + _playing = _profileEditorService.Playing.ToProperty(this, vm => vm.Playing).DisposeWith(d); + + Observable.FromEventPattern(x => coreService.FrameRendering += x, x => coreService.FrameRendering -= x) + .Subscribe(e => CoreServiceOnFrameRendering(e.EventArgs)) + .DisposeWith(d); + }); + } + + public TimeSpan CurrentTime => _currentTime?.Value ?? TimeSpan.Zero; + public string? FormattedCurrentTime => _formattedCurrentTime?.Value; + public bool Playing => _playing?.Value ?? false; + + public bool Repeating + { + get => _repeating; + set => this.RaiseAndSetIfChanged(ref _repeating, value); + } + + public bool RepeatTimeline + { + get => _repeatTimeline; + set => this.RaiseAndSetIfChanged(ref _repeatTimeline, value); + } + + public bool RepeatSegment + { + get => _repeatSegment; + set => this.RaiseAndSetIfChanged(ref _repeatSegment, value); + } + + public void PlayFromStart() + { + GoToStart(); + if (!Playing) + _profileEditorService.Play(); + } + + public void TogglePlay() + { + if (!Playing) + _profileEditorService.Play(); + else + _profileEditorService.Pause(); + } + + public void GoToStart() + { + _profileEditorService.ChangeTime(TimeSpan.Zero); + } + + public void GoToEnd() + { + if (_profileElement == null) + return; + + _profileEditorService.ChangeTime(_profileElement.Timeline.EndSegmentEndPosition); + } + + public void GoToPreviousFrame() + { + if (_profileElement == null) + return; + + double frameTime = 1000.0 / _settingsService.GetSetting("Core.TargetFrameRate", 30).Value; + double newTime = Math.Max(0, Math.Round((CurrentTime.TotalMilliseconds - frameTime) / frameTime) * frameTime); + _profileEditorService.ChangeTime(TimeSpan.FromMilliseconds(newTime)); + } + + public void GoToNextFrame() + { + if (_profileElement == null) + return; + + double frameTime = 1000.0 / _settingsService.GetSetting("Core.TargetFrameRate", 30).Value; + double newTime = Math.Round((CurrentTime.TotalMilliseconds + frameTime) / frameTime) * frameTime; + newTime = Math.Min(newTime, _profileElement.Timeline.EndSegmentEndPosition.TotalMilliseconds); + _profileEditorService.ChangeTime(TimeSpan.FromMilliseconds(newTime)); + } + + public void CycleRepeating() + { + if (!Repeating) + { + RepeatTimeline = true; + RepeatSegment = false; + Repeating = true; + } + else if (RepeatTimeline) + { + RepeatTimeline = false; + RepeatSegment = true; + } + else if (RepeatSegment) + { + RepeatTimeline = true; + RepeatSegment = false; + Repeating = false; + } + } + + private TimeSpan GetCurrentSegmentStart() + { + if (_profileElement == null) + return TimeSpan.Zero; + + if (CurrentTime < _profileElement.Timeline.StartSegmentEndPosition) + return TimeSpan.Zero; + if (CurrentTime < _profileElement.Timeline.MainSegmentEndPosition) + return _profileElement.Timeline.MainSegmentStartPosition; + if (CurrentTime < _profileElement.Timeline.EndSegmentEndPosition) + return _profileElement.Timeline.EndSegmentStartPosition; + + return TimeSpan.Zero; + } + + private TimeSpan GetCurrentSegmentEnd() + { + if (_profileElement == null) + return TimeSpan.Zero; + + if (CurrentTime < _profileElement.Timeline.StartSegmentEndPosition) + return _profileElement.Timeline.StartSegmentEndPosition; + if (CurrentTime < _profileElement.Timeline.MainSegmentEndPosition) + return _profileElement.Timeline.MainSegmentEndPosition; + if (CurrentTime < _profileElement.Timeline.EndSegmentEndPosition) + return _profileElement.Timeline.EndSegmentEndPosition; + + return TimeSpan.Zero; + } + + private void CoreServiceOnFrameRendering(FrameRenderingEventArgs e) + { + if (!Playing) + return; + + TimeSpan newTime = CurrentTime.Add(TimeSpan.FromSeconds(e.DeltaTime)); + if (_profileElement != null) + { + if (Repeating && RepeatTimeline) + { + if (newTime > _profileElement.Timeline.Length) + newTime = TimeSpan.Zero; + } + else if (Repeating && RepeatSegment) + { + if (newTime > GetCurrentSegmentEnd()) + newTime = GetCurrentSegmentStart(); + } + else if (newTime > _profileElement.Timeline.Length) + { + newTime = _profileElement.Timeline.Length; + _profileEditorService.Pause(); + } + } + + _profileEditorService.ChangeTime(newTime); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml index 031ee5c09..de14a7b71 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml @@ -3,18 +3,45 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileElementProperties" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.ProfileElementPropertiesView"> - - - - - - - - - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml.cs index ae979f444..69bd34970 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml.cs @@ -1,18 +1,17 @@ using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties -{ - public partial class ProfileElementPropertiesView : ReactiveUserControl - { - public ProfileElementPropertiesView() - { - InitializeComponent(); - } +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } +public class ProfileElementPropertiesView : ReactiveUserControl +{ + public ProfileElementPropertiesView() + { + InitializeComponent(); } -} + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesViewModel.cs index 93e518660..5e396dd9b 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesViewModel.cs @@ -6,9 +6,10 @@ using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using Artemis.Core; +using Artemis.Core.LayerBrushes; using Artemis.Core.LayerEffects; using Artemis.UI.Ninject.Factories; -using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; +using Artemis.UI.Screens.ProfileEditor.Playback; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.ProfileEditor; using ReactiveUI; @@ -17,20 +18,18 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; public class ProfileElementPropertiesViewModel : ActivatableViewModelBase { + private readonly Dictionary _cachedViewModels; private readonly ILayerPropertyVmFactory _layerPropertyVmFactory; - private readonly IProfileEditorService _profileEditorService; - private ProfileElementPropertyGroupViewModel? _brushPropertyGroup; private ObservableAsPropertyHelper? _profileElement; - private readonly Dictionary> _profileElementGroups; private ObservableCollection _propertyGroupViewModels; /// - public ProfileElementPropertiesViewModel(IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory) + public ProfileElementPropertiesViewModel(IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory, PlaybackViewModel playbackViewModel) { - _profileEditorService = profileEditorService; _layerPropertyVmFactory = layerPropertyVmFactory; - PropertyGroupViewModels = new ObservableCollection(); - _profileElementGroups = new Dictionary>(); + _propertyGroupViewModels = new ObservableCollection(); + _cachedViewModels = new Dictionary(); + PlaybackViewModel = playbackViewModel; // Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940 this.WhenAnyValue(vm => vm.ProfileElement) @@ -47,10 +46,11 @@ public class ProfileElementPropertiesViewModel : ActivatableViewModelBase .Subscribe(_ => UpdateGroups()); // React to service profile element changes as long as the VM is active - this.WhenActivated(d => _profileElement = _profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d)); + this.WhenActivated(d => _profileElement = profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d)); this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdateGroups()); } + public PlaybackViewModel PlaybackViewModel { get; } public RenderProfileElement? ProfileElement => _profileElement?.Value; public Layer? Layer => _profileElement?.Value as Layer; @@ -68,57 +68,51 @@ public class ProfileElementPropertiesViewModel : ActivatableViewModelBase return; } - if (!_profileElementGroups.TryGetValue(ProfileElement, out List? viewModels)) - { - viewModels = new List(); - _profileElementGroups[ProfileElement] = viewModels; - } - - List groups = new(); - + ObservableCollection viewModels = new(); if (Layer != null) { - // Add default layer groups - groups.Add(Layer.General); - groups.Add(Layer.Transform); - // Add brush group + // Add base VMs + viewModels.Add(GetOrCreateViewModel(Layer.General, null, null)); + viewModels.Add(GetOrCreateViewModel(Layer.Transform, null, null)); + + // Add brush VM if the brush has properties if (Layer.LayerBrush?.BaseProperties != null) - groups.Add(Layer.LayerBrush.BaseProperties); + viewModels.Add(GetOrCreateViewModel(Layer.LayerBrush.BaseProperties, Layer.LayerBrush, null)); } - // Add effect groups - foreach (BaseLayerEffect layerEffect in ProfileElement.LayerEffects) - { + // Add effect VMs + foreach (BaseLayerEffect layerEffect in ProfileElement.LayerEffects.OrderBy(e => e.Order)) if (layerEffect.BaseProperties != null) - groups.Add(layerEffect.BaseProperties); - } + viewModels.Add(GetOrCreateViewModel(layerEffect.BaseProperties, null, layerEffect)); - // Remove redundant VMs - viewModels.RemoveAll(vm => !groups.Contains(vm.LayerPropertyGroup)); - - // Create VMs for missing groups - foreach (LayerPropertyGroup group in groups) + // Map the most recent collection of VMs to the current list of VMs, making as little changes to the collection as possible + for (int index = 0; index < viewModels.Count; index++) { - if (viewModels.All(vm => vm.LayerPropertyGroup != group)) - viewModels.Add(_layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(group)); + ProfileElementPropertyGroupViewModel profileElementPropertyGroupViewModel = viewModels[index]; + if (index > PropertyGroupViewModels.Count - 1) + PropertyGroupViewModels.Add(profileElementPropertyGroupViewModel); + else if (!ReferenceEquals(PropertyGroupViewModels[index], profileElementPropertyGroupViewModel)) + PropertyGroupViewModels[index] = profileElementPropertyGroupViewModel; } - // Get all non-effect properties - List nonEffectProperties = viewModels - .Where(l => l.TreeGroupViewModel.GroupType != LayerPropertyGroupType.LayerEffectRoot) - .ToList(); - // Order the effects - List effectProperties = viewModels - .Where(l => l.TreeGroupViewModel.GroupType == LayerPropertyGroupType.LayerEffectRoot && l.LayerPropertyGroup.LayerEffect != null) - .OrderBy(l => l.LayerPropertyGroup.LayerEffect?.Order) - .ToList(); + while (PropertyGroupViewModels.Count > viewModels.Count) + PropertyGroupViewModels.RemoveAt(PropertyGroupViewModels.Count - 1); + } - ObservableCollection propertyGroupViewModels = new(); - foreach (ProfileElementPropertyGroupViewModel viewModel in nonEffectProperties) - propertyGroupViewModels.Add(viewModel); - foreach (ProfileElementPropertyGroupViewModel viewModel in effectProperties) - propertyGroupViewModels.Add(viewModel); + private ProfileElementPropertyGroupViewModel GetOrCreateViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush? layerBrush, BaseLayerEffect? layerEffect) + { + if (_cachedViewModels.TryGetValue(layerPropertyGroup, out ProfileElementPropertyGroupViewModel? cachedVm)) + return cachedVm; - PropertyGroupViewModels = propertyGroupViewModels; + ProfileElementPropertyGroupViewModel createdVm; + if (layerBrush != null) + createdVm = _layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(layerPropertyGroup, layerBrush); + else if (layerEffect != null) + createdVm = _layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(layerPropertyGroup, layerEffect); + else + createdVm = _layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(layerPropertyGroup); + + _cachedViewModels[layerPropertyGroup] = createdVm; + return createdVm; } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs index 081c3209c..a3b6426a3 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs @@ -3,6 +3,8 @@ using System.Collections.ObjectModel; using System.Linq; using System.Reflection; using Artemis.Core; +using Artemis.Core.LayerBrushes; +using Artemis.Core.LayerEffects; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; using Artemis.UI.Shared; @@ -33,8 +35,23 @@ public class ProfileElementPropertyGroupViewModel : ViewModelBase PopulateChildren(); } + public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService, BaseLayerBrush layerBrush) + : this(layerPropertyGroup, layerPropertyVmFactory, propertyInputService) + { + LayerBrush = layerBrush; + } + + public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService, BaseLayerEffect layerEffect) + : this(layerPropertyGroup, layerPropertyVmFactory, propertyInputService) + { + LayerEffect = layerEffect; + } + public ObservableCollection Children { get; } public LayerPropertyGroup LayerPropertyGroup { get; } + public BaseLayerBrush? LayerBrush { get; } + public BaseLayerEffect? LayerEffect { get; } + public TreeGroupViewModel TreeGroupViewModel { get; } public bool IsVisible diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/ITreePropertyViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/ITreePropertyViewModel.cs index 16fcc9eea..15e9f84a3 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/ITreePropertyViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/ITreePropertyViewModel.cs @@ -1,9 +1,11 @@ -using ReactiveUI; +using Artemis.Core; +using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; public interface ITreePropertyViewModel : IReactiveObject { + ILayerProperty BaseLayerProperty { get; } bool HasDataBinding { get; } double GetDepth(); } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml index 0ccb5b649..5f693289b 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml @@ -70,24 +70,24 @@ Brush -  + IsVisible="{Binding LayerBrush.ConfigurationDialog, Converter={x:Static ObjectConverters.IsNotNull}}"> Extra options available! @@ -108,31 +108,31 @@ - + Effect - + IsVisible="{Binding !LayerEffect.Name, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" /> + IsVisible="{Binding LayerEffect.Name, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" /> @@ -160,7 +160,7 @@ Height="24" VerticalAlignment="Center" Command="{Binding OpenEffectSettings}" - IsVisible="{Binding LayerPropertyGroup.LayerEffect.ConfigurationDialog, Converter={x:Static ObjectConverters.IsNotNull}}"> + IsVisible="{Binding LayerEffect.ConfigurationDialog, Converter={x:Static ObjectConverters.IsNotNull}}">