using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Artemis.Core.Annotations; using Artemis.Core.Events; using Artemis.Core.Exceptions; using Artemis.Core.Models.Profile.LayerProperties; using Artemis.Core.Models.Profile.LayerProperties.Attributes; using Artemis.Core.Plugins.Exceptions; using Artemis.Core.Plugins.LayerBrushes; using Artemis.Core.Plugins.LayerBrushes.Internal; using Artemis.Core.Plugins.LayerEffects; using Artemis.Core.Services.Interfaces; using Artemis.Storage.Entities.Profile; namespace Artemis.Core.Models.Profile { public abstract class LayerPropertyGroup : IDisposable { private readonly List _layerProperties; private readonly List _layerPropertyGroups; private ReadOnlyCollection _allLayerProperties; private bool _isHidden; protected LayerPropertyGroup() { _layerProperties = new List(); _layerPropertyGroups = new List(); } /// /// Gets the profile element (such as layer or folder) this effect is applied to /// public RenderProfileElement ProfileElement { get; internal set; } /// /// The path of this property group /// public string Path { get; internal set; } /// /// The parent group of this layer property group, set after construction /// public LayerPropertyGroup Parent { get; internal set; } /// /// Gets whether this property groups properties are all initialized /// public bool PropertiesInitialized { get; private set; } /// /// Used to declare that this property group doesn't belong to a plugin and should use the core plugin GUID /// public bool IsCorePropertyGroup { get; internal set; } public PropertyGroupDescriptionAttribute GroupDescription { get; internal set; } /// /// The layer brush this property group belongs to /// 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 /// public bool IsHidden { get => _isHidden; set { _isHidden = value; OnVisibilityChanged(); } } /// /// A list of all layer properties in this group /// public ReadOnlyCollection LayerProperties => _layerProperties.AsReadOnly(); /// /// A list of al child groups in this group /// public ReadOnlyCollection LayerPropertyGroups => _layerPropertyGroups.AsReadOnly(); /// /// Recursively gets all layer properties on this group and any subgroups /// /// public IReadOnlyCollection GetAllLayerProperties() { if (!PropertiesInitialized) return new List(); if (_allLayerProperties != null) return _allLayerProperties; var result = new List(LayerProperties); foreach (var layerPropertyGroup in LayerPropertyGroups) result.AddRange(layerPropertyGroup.GetAllLayerProperties()); _allLayerProperties = result.AsReadOnly(); return _allLayerProperties; } public void Dispose() { DisableProperties(); foreach (var layerPropertyGroup in _layerPropertyGroups) layerPropertyGroup.Dispose(); } /// /// 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 deactivated /// 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(); protected virtual void OnPropertyGroupInitialized() { PropertyGroupInitialized?.Invoke(this, EventArgs.Empty); } internal void InitializeProperties(IRenderElementService renderElementService, RenderProfileElement profileElement, [NotNull] string path) { if (path == null) throw new ArgumentNullException(nameof(path)); // Doubt this will happen but let's make sure if (PropertiesInitialized) throw new ArtemisCoreException("Layer property group already initialized, wut"); ProfileElement = profileElement; Path = path.TrimEnd('.'); // Get all properties with a PropertyDescriptionAttribute foreach (var propertyInfo in GetType().GetProperties()) { var propertyDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyDescriptionAttribute)); if (propertyDescription != null) { if (!typeof(BaseLayerProperty).IsAssignableFrom(propertyInfo.PropertyType)) throw new ArtemisPluginException($"Layer property with PropertyDescription attribute must be of type LayerProperty at {path + propertyInfo.Name}"); var instance = (BaseLayerProperty) Activator.CreateInstance(propertyInfo.PropertyType, true); if (instance == null) throw new ArtemisPluginException($"Failed to create instance of layer property at {path + propertyInfo.Name}"); instance.ProfileElement = profileElement; instance.Parent = this; instance.PropertyDescription = (PropertyDescriptionAttribute) propertyDescription; if (instance.PropertyDescription.DisableKeyframes) instance.KeyframesSupported = false; InitializeProperty(profileElement, path + propertyInfo.Name, instance); propertyInfo.SetValue(this, instance); _layerProperties.Add(instance); } else { var propertyGroupDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyGroupDescriptionAttribute)); if (propertyGroupDescription != null) { if (!typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType)) throw new ArtemisPluginException("Layer property with PropertyGroupDescription attribute must be of type LayerPropertyGroup"); var instance = (LayerPropertyGroup) Activator.CreateInstance(propertyInfo.PropertyType); if (instance == null) throw new ArtemisPluginException($"Failed to create instance of layer property group at {path + propertyInfo.Name}"); instance.Parent = this; instance.GroupDescription = (PropertyGroupDescriptionAttribute) propertyGroupDescription; instance.LayerBrush = LayerBrush; instance.LayerEffect = LayerEffect; instance.InitializeProperties(renderElementService, profileElement, $"{path}{propertyInfo.Name}."); propertyInfo.SetValue(this, instance); _layerPropertyGroups.Add(instance); } } } // Request the property group to populate defaults PopulateDefaults(); // Apply the newly populated defaults foreach (var layerProperty in _layerProperties.Where(p => !p.IsLoadedFromStorage)) layerProperty.ApplyDefaultValue(); EnableProperties(); PropertiesInitialized = true; OnPropertyGroupInitialized(); } internal void ApplyToEntity() { if (!PropertiesInitialized) return; // Get all properties with a PropertyDescriptionAttribute foreach (var propertyInfo in GetType().GetProperties()) { var propertyDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyDescriptionAttribute)); if (propertyDescription != null) { var layerProperty = (BaseLayerProperty) propertyInfo.GetValue(this); layerProperty.ApplyToEntity(); } else { var propertyGroupDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyGroupDescriptionAttribute)); if (propertyGroupDescription != null) { var layerPropertyGroup = (LayerPropertyGroup) propertyInfo.GetValue(this); layerPropertyGroup.ApplyToEntity(); } } } } internal void Update() { // Since at this point we don't know what properties the group has without using reflection, // let properties subscribe to the update event and update themselves OnPropertyGroupUpdating(); } private void InitializeProperty(RenderProfileElement profileElement, string path, BaseLayerProperty instance) { Guid pluginGuid; if (IsCorePropertyGroup || instance.IsCoreProperty) pluginGuid = Constants.CorePluginInfo.Guid; else if (instance.Parent.LayerBrush != null) pluginGuid = instance.Parent.LayerBrush.PluginInfo.Guid; else pluginGuid = instance.Parent.LayerEffect.PluginInfo.Guid; var entity = profileElement.RenderElementEntity.PropertyEntities.FirstOrDefault(p => p.PluginGuid == pluginGuid && p.Path == path); var fromStorage = true; if (entity == null) { fromStorage = false; entity = new PropertyEntity {PluginGuid = pluginGuid, Path = path}; profileElement.RenderElementEntity.PropertyEntities.Add(entity); } instance.ApplyToLayerProperty(entity, this, fromStorage); instance.BaseValueChanged += InstanceOnBaseValueChanged; } private void InstanceOnBaseValueChanged(object sender, EventArgs e) { OnLayerPropertyBaseValueChanged(new LayerPropertyEventArgs((BaseLayerProperty) sender)); } #region Events internal event EventHandler PropertyGroupUpdating; /// /// Occurs when the property group has initialized all its children /// public event EventHandler PropertyGroupInitialized; /// /// Occurs when one of the base value of one of the layer properties in this group changes /// Note: Will not trigger on properties in child groups /// public event EventHandler LayerPropertyBaseValueChanged; /// /// Occurs when the value of the layer property was updated /// public event EventHandler VisibilityChanged; protected virtual void OnPropertyGroupUpdating() { PropertyGroupUpdating?.Invoke(this, EventArgs.Empty); } protected virtual void OnVisibilityChanged() { VisibilityChanged?.Invoke(this, EventArgs.Empty); } protected virtual void OnLayerPropertyBaseValueChanged(LayerPropertyEventArgs e) { LayerPropertyBaseValueChanged?.Invoke(this, e); } #endregion } }