From 1832a25426e3fce72ed2175766bd46c24746c102 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 16 Jan 2022 00:46:19 +0100 Subject: [PATCH] Core - Reworked brush/effect property storage Profile editor - Added brush selection Profile editor - Added playback controls --- src/Artemis.Core/Models/Profile/Layer.cs | 83 +++----- .../PropertyDescriptionAttribute.cs | 5 + .../PropertyGroupDescriptionAttribute.cs | 11 +- .../Profile/LayerProperties/ILayerProperty.cs | 11 +- .../Profile/LayerProperties/LayerProperty.cs | 20 +- .../Models/Profile/LayerPropertyGroup.cs | 120 ++++++----- .../Models/Profile/RenderProfileElement.cs | 20 +- .../LayerBrushes/Internal/BaseLayerBrush.cs | 14 ++ .../Internal/PropertiesLayerBrush.cs | 8 +- .../Plugins/LayerBrushes/LayerBrush.cs | 2 +- .../LayerBrushes/LayerBrushDescriptor.cs | 16 +- .../Plugins/LayerBrushes/PerLedLayerBrush.cs | 2 +- .../LayerEffects/Internal/BaseLayerEffect.cs | 24 ++- .../Plugins/LayerEffects/LayerEffect.cs | 3 +- .../LayerEffects/LayerEffectDescriptor.cs | 30 ++- .../Placeholder/PlaceholderLayerEffect.cs | 2 +- .../Entities/Profile/LayerBrushEntity.cs | 11 + .../Entities/Profile/LayerEffectEntity.cs | 3 + .../Entities/Profile/LayerEntity.cs | 5 +- .../Entities/Profile/PropertyEntity.cs | 24 +-- .../Entities/Profile/PropertyGroupEntity.cs | 10 + .../BrushPropertyInputViewModel.cs | 14 +- .../LayerPropertiesViewModel.cs | 50 ++--- .../LayerPropertyGroupViewModel.cs | 4 +- .../Timeline/Models/KeyframeClipboardModel.cs | 20 +- .../Tree/TreeGroupViewModel.cs | 180 ++++++++-------- .../ProfileTree/TreeItem/TreeItemViewModel.cs | 4 +- .../Tools/SelectionToolViewModel.cs | 4 +- .../Commands/ChangeLayerBrush.cs | 36 +++- .../ProfileEditor/IProfileEditorService.cs | 94 +++++++-- .../ProfileEditor/ProfileEditorService.cs | 92 ++++++++- .../Artemis.UI.Shared/Styles/Artemis.axaml | 4 +- .../Styles/{TextBox.axaml => Condensed.axaml} | 33 ++- src/Avalonia/Artemis.UI/Artemis.UI.csproj | 1 + .../Assets/Images/Logo/application.ico | Bin 0 -> 113388 bytes .../Converters/SKColorToColor2Converter.cs | 29 +++ .../Converters/SKColorToStringConverter.cs | 28 +++ .../PropertyInput/BoolPropertyInputView.axaml | 3 +- .../BoolPropertyInputViewModel.cs | 20 +- .../BrushPropertyInputView.axaml | 2 +- .../BrushPropertyInputViewModel.cs | 10 + .../PropertyInput/EnumPropertyInputView.axaml | 5 +- .../SKColorPropertyInputView.axaml | 28 ++- src/Avalonia/Artemis.UI/MainWindow.axaml | 2 +- .../Ninject/Factories/IVMFactory.cs | 4 + .../Screens/Debugger/DebugView.axaml | 2 +- .../Screens/Device/DevicePropertiesView.axaml | 2 +- .../Plugins/PluginSettingsWindowView.axaml | 2 +- .../Panels/Playback/PlaybackView.axaml | 63 ++++++ .../Panels/Playback/PlaybackView.axaml.cs | 18 ++ .../Panels/Playback/PlaybackViewModel.cs | 193 ++++++++++++++++++ .../ProfileElementPropertiesView.axaml | 49 ++++- .../ProfileElementPropertiesView.axaml.cs | 25 ++- .../ProfileElementPropertiesViewModel.cs | 92 ++++----- .../ProfileElementPropertyGroupViewModel.cs | 17 ++ .../Tree/ITreePropertyViewModel.cs | 4 +- .../Tree/TreeGroupView.axaml | 32 +-- .../Tree/TreeGroupViewModel.cs | 23 ++- .../Tree/TreePropertyView.axaml.cs | 18 +- .../Tree/TreePropertyViewModel.cs | 2 + .../ProfileEditor/ProfileEditorView.axaml | 10 +- .../Artemis.UI/Screens/Root/RootViewModel.cs | 2 +- .../Artemis.UI/Screens/Root/SplashView.axaml | 2 +- .../ProfileConfigurationEditView.axaml | 2 +- 64 files changed, 1168 insertions(+), 481 deletions(-) create mode 100644 src/Artemis.Storage/Entities/Profile/LayerBrushEntity.cs create mode 100644 src/Artemis.Storage/Entities/Profile/PropertyGroupEntity.cs rename src/Avalonia/Artemis.UI.Shared/Styles/{TextBox.axaml => Condensed.axaml} (61%) create mode 100644 src/Avalonia/Artemis.UI/Assets/Images/Logo/application.ico create mode 100644 src/Avalonia/Artemis.UI/Converters/SKColorToColor2Converter.cs create mode 100644 src/Avalonia/Artemis.UI/Converters/SKColorToStringConverter.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs 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 0000000000000000000000000000000000000000..bdb89dbd0af2610300c383d3f10662157b49ccfe GIT binary patch literal 113388 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 literal 0 HcmV?d00001 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}}">