using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reflection; using Artemis.Core.LayerBrushes; using Artemis.Core.LayerEffects; using Artemis.Core.Properties; using Artemis.Storage.Entities.Profile; using Humanizer; namespace Artemis.Core { public abstract class LayerPropertyGroup : IDisposable { private readonly List _layerProperties; private readonly List _layerPropertyGroups; private bool _disposed; private bool _isHidden; protected LayerPropertyGroup() { _layerProperties = new List(); _layerPropertyGroups = new List(); } /// /// Gets the description of this group /// public PropertyGroupDescriptionAttribute GroupDescription { get; internal set; } /// /// Gets the info of the plugin this group is associated with /// public PluginInfo PluginInfo { get; internal set; } /// /// Gets the profile element (such as layer or folder) this group is associated with /// public RenderProfileElement ProfileElement { get; internal set; } /// /// The parent group of this group /// public LayerPropertyGroup Parent { get; internal set; } /// /// The path of this property group /// public string Path { get; internal set; } /// /// 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 /// 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(); #region IDisposable /// public void Dispose() { _disposed = true; DisableProperties(); foreach (var layerProperty in _layerProperties) layerProperty.Dispose(); foreach (var layerPropertyGroup in _layerPropertyGroups) layerPropertyGroup.Dispose(); } #endregion /// /// Recursively gets all layer properties on this group and any subgroups /// public IReadOnlyCollection GetAllLayerProperties() { if (_disposed) throw new ObjectDisposedException("LayerPropertyGroup"); if (!PropertiesInitialized) return new List(); var result = new List(LayerProperties); foreach (var layerPropertyGroup in LayerPropertyGroups) result.AddRange(layerPropertyGroup.GetAllLayerProperties()); return result.AsReadOnly(); } /// /// 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 aactivated /// 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); } internal void Initialize(RenderProfileElement profileElement, [NotNull] string path, PluginInfo pluginInfo) { if (path == null) throw new ArgumentNullException(nameof(path)); if (pluginInfo == null) throw new ArgumentNullException(nameof(pluginInfo)); // Doubt this will happen but let's make sure if (PropertiesInitialized) throw new ArtemisCoreException("Layer property group already initialized, wut"); PluginInfo = pluginInfo; ProfileElement = profileElement; Path = path.TrimEnd('.'); // Get all properties with a PropertyDescriptionAttribute foreach (var propertyInfo in GetType().GetProperties()) { var propertyDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyDescriptionAttribute)); if (propertyDescription != null) InitializeProperty(propertyInfo, (PropertyDescriptionAttribute) propertyDescription); else { var propertyGroupDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyGroupDescriptionAttribute)); if (propertyGroupDescription != null) InitializeChildGroup(propertyInfo, (PropertyGroupDescriptionAttribute) propertyGroupDescription); } } // Request the property group to populate defaults PopulateDefaults(); // Load the layer properties after defaults have been applied foreach (var layerProperty in _layerProperties) layerProperty.Load(); EnableProperties(); PropertiesInitialized = true; OnPropertyGroupInitialized(); } internal void ApplyToEntity() { if (!PropertiesInitialized) return; foreach (var layerProperty in LayerProperties) layerProperty.Save(); foreach (var layerPropertyGroup in LayerPropertyGroups) layerPropertyGroup.ApplyToEntity(); } internal void Update(double deltaTime) { // 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(new LayerPropertyGroupUpdatingEventArgs(deltaTime)); } private void InitializeProperty(PropertyInfo propertyInfo, PropertyDescriptionAttribute propertyDescription) { var 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}"); var instance = (ILayerProperty) Activator.CreateInstance(propertyInfo.PropertyType, true); if (instance == null) 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 if (string.IsNullOrWhiteSpace(propertyDescription.Name)) propertyDescription.Name = propertyInfo.Name.Humanize(); var entity = GetPropertyEntity(ProfileElement, path, out var fromStorage); instance.Initialize(ProfileElement, this, entity, fromStorage, propertyDescription, path); propertyInfo.SetValue(this, instance); _layerProperties.Add(instance); } private void InitializeChildGroup(PropertyInfo propertyInfo, PropertyGroupDescriptionAttribute propertyGroupDescription) { var path = Path + "."; if (!typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType)) throw new ArtemisPluginException("Layer property with PropertyGroupDescription attribute must be of type LayerPropertyGroup"); var instance = (LayerPropertyGroup) Activator.CreateInstance(propertyInfo.PropertyType); if (instance == null) throw new ArtemisPluginException($"Failed to create instance of layer property group at {path + propertyInfo.Name}"); // Ensure the description has a name, if not this is a good point to set it based on the property info if (string.IsNullOrWhiteSpace(propertyGroupDescription.Name)) propertyGroupDescription.Name = propertyInfo.Name.Humanize(); instance.Parent = this; instance.GroupDescription = propertyGroupDescription; instance.LayerBrush = LayerBrush; instance.LayerEffect = LayerEffect; instance.Initialize(ProfileElement, $"{path}{propertyInfo.Name}.", PluginInfo); propertyInfo.SetValue(this, instance); _layerPropertyGroups.Add(instance); } private PropertyEntity GetPropertyEntity(RenderProfileElement profileElement, string path, out bool fromStorage) { var entity = profileElement.RenderElementEntity.PropertyEntities.FirstOrDefault(p => p.PluginGuid == PluginInfo.Guid && p.Path == path); fromStorage = entity != null; if (entity == null) { entity = new PropertyEntity {PluginGuid = PluginInfo.Guid, Path = path}; profileElement.RenderElementEntity.PropertyEntities.Add(entity); } return entity; } #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 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; internal virtual void OnPropertyGroupUpdating(LayerPropertyGroupUpdatingEventArgs e) { PropertyGroupUpdating?.Invoke(this, e); } internal virtual void OnVisibilityChanged() { VisibilityChanged?.Invoke(this, EventArgs.Empty); } internal virtual void OnLayerPropertyOnCurrentValueSet(LayerPropertyEventArgs e) { LayerPropertyOnCurrentValueSet?.Invoke(this, e); } #endregion } }