using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reflection; using Artemis.Storage.Entities.Profile; using Humanizer; namespace Artemis.Core; /// /// Represents a property group on a layer /// /// Note: You cannot initialize property groups yourself. If properly placed and annotated, the Artemis core will /// initialize these for you. /// /// public abstract class LayerPropertyGroup : IDisposable { private readonly List _layerProperties; private readonly List _layerPropertyGroups; private bool _disposed; private bool _isHidden; /// /// A base constructor for a /// protected LayerPropertyGroup() { // These are set right after construction to keep the constructor (and inherited constructs) clean ProfileElement = null!; GroupDescription = null!; Path = ""; _layerProperties = new List(); _layerPropertyGroups = new List(); LayerProperties = new ReadOnlyCollection(_layerProperties); LayerPropertyGroups = new ReadOnlyCollection(_layerPropertyGroups); } /// /// Gets the profile element (such as layer or folder) this group is associated with /// 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] // Ignore the parent when selecting child groups public LayerPropertyGroup? Parent { get; internal set; } /// /// Gets the unique path of the property on the render element /// public string Path { get; private set; } /// /// Gets whether this property groups properties are all initialized /// public bool PropertiesInitialized { get; private set; } /// /// Gets or sets whether the property is hidden in the UI /// public bool IsHidden { get => _isHidden; set { _isHidden = value; OnVisibilityChanged(); } } /// /// 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 /// public ReadOnlyCollection LayerProperties { get; } /// /// A list of al child groups in this group /// public ReadOnlyCollection LayerPropertyGroups { get; } /// /// Recursively gets all layer properties on this group and any subgroups /// public IReadOnlyCollection GetAllLayerProperties() { if (_disposed) throw new ObjectDisposedException("LayerPropertyGroup"); if (!PropertiesInitialized) return new List(); List result = new(LayerProperties); foreach (LayerPropertyGroup layerPropertyGroup in LayerPropertyGroups) result.AddRange(layerPropertyGroup.GetAllLayerProperties()); return result.AsReadOnly(); } /// /// Applies the default value to all layer properties /// public void ResetAllLayerProperties() { foreach (ILayerProperty layerProperty in GetAllLayerProperties()) layerProperty.ApplyDefaultValue(); } /// /// Occurs when the property group has initialized all its children /// public event EventHandler? PropertyGroupInitialized; /// /// Occurs when one of the current value of one of the layer properties in this group changes by some form of input /// Note: Will not trigger on properties in child groups /// public event EventHandler? LayerPropertyOnCurrentValueSet; /// /// Occurs when the value of the layer property was updated /// public event EventHandler? VisibilityChanged; /// /// Called before property group is activated to allow you to populate on /// the properties you want /// protected abstract void PopulateDefaults(); /// /// Called when the property group is activated /// protected abstract void EnableProperties(); /// /// Called when the property group is deactivated (either the profile unloaded or the related brush/effect was removed) /// protected abstract void DisableProperties(); /// /// Called when the property group and all its layer properties have been initialized /// protected virtual void OnPropertyGroupInitialized() { PropertyGroupInitialized?.Invoke(this, EventArgs.Empty); } /// /// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// /// /// to release both managed and unmanaged resources; /// to release only unmanaged resources. /// protected virtual void Dispose(bool disposing) { if (disposing) { _disposed = true; DisableProperties(); foreach (ILayerProperty layerProperty in _layerProperties) layerProperty.Dispose(); foreach (LayerPropertyGroup layerPropertyGroup in _layerPropertyGroups) layerPropertyGroup.Dispose(); } } internal void Initialize(RenderProfileElement profileElement, LayerPropertyGroup? parent, PropertyGroupDescriptionAttribute groupDescription, PropertyGroupEntity? propertyGroupEntity) { 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"); 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()) { if (Attribute.IsDefined(propertyInfo, typeof(LayerPropertyIgnoreAttribute))) continue; if (typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType)) { PropertyDescriptionAttribute? propertyDescription = (PropertyDescriptionAttribute?) Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyDescriptionAttribute)); InitializeProperty(propertyInfo, propertyDescription ?? new PropertyDescriptionAttribute()); } else if (typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType)) { PropertyGroupDescriptionAttribute? propertyGroupDescription = (PropertyGroupDescriptionAttribute?) Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyGroupDescriptionAttribute)); InitializeChildGroup(propertyInfo, propertyGroupDescription ?? new PropertyGroupDescriptionAttribute()); } } // Request the property group to populate defaults PopulateDefaults(); // Load the layer properties after defaults have been applied foreach (ILayerProperty layerProperty in _layerProperties) layerProperty.Load(); EnableProperties(); PropertiesInitialized = true; OnPropertyGroupInitialized(); } internal void ApplyToEntity() { if (!PropertiesInitialized || PropertyGroupEntity == null) return; foreach (ILayerProperty layerProperty in LayerProperties) layerProperty.Save(); PropertyGroupEntity.PropertyGroups.Clear(); foreach (LayerPropertyGroup layerPropertyGroup in LayerPropertyGroups) { layerPropertyGroup.ApplyToEntity(); PropertyGroupEntity.PropertyGroups.Add(layerPropertyGroup.PropertyGroupEntity); } } internal void Update(Timeline timeline) { foreach (ILayerProperty layerProperty in LayerProperties) layerProperty.Update(timeline); foreach (LayerPropertyGroup layerPropertyGroup in LayerPropertyGroups) layerPropertyGroup.Update(timeline); } internal void MoveLayerProperty(ILayerProperty layerProperty, int index) { if (!_layerProperties.Contains(layerProperty)) return; _layerProperties.Remove(layerProperty); _layerProperties.Insert(index, layerProperty); } internal virtual void OnVisibilityChanged() { VisibilityChanged?.Invoke(this, EventArgs.Empty); } internal virtual void OnLayerPropertyOnCurrentValueSet(LayerPropertyEventArgs e) { Parent?.OnLayerPropertyOnCurrentValueSet(e); LayerPropertyOnCurrentValueSet?.Invoke(this, e); } private void InitializeProperty(PropertyInfo propertyInfo, PropertyDescriptionAttribute propertyDescription) { // 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(); 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) { // 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(); 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(string identifier, out bool fromStorage) { 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 {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() { Dispose(true); GC.SuppressFinalize(this); } }