using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Linq.Expressions; using Artemis.Storage.Entities.Profile; using Newtonsoft.Json; namespace Artemis.Core { /// /// Represents a property on a layer. Properties are saved in storage and can optionally be modified from the UI. /// /// Note: You cannot initialize layer properties yourself. If properly placed and annotated, the Artemis core will /// initialize these for you. /// /// /// The type of property encapsulated in this layer property public class LayerProperty : ILayerProperty { private bool _disposed; /// /// Creates a new instance of the class /// protected LayerProperty() { // These are set right after construction to keep the constructor (and inherited constructs) clean ProfileElement = null!; LayerPropertyGroup = null!; Path = null!; Entity = null!; PropertyDescription = null!; CurrentValue = default!; DefaultValue = default!; _baseValue = default!; _keyframes = new List>(); } /// /// Returns the type of the property /// public Type GetPropertyType() { return typeof(T); } /// public PropertyDescriptionAttribute PropertyDescription { get; internal set; } /// public string Path { get; private set; } /// public void Update(Timeline timeline) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); CurrentValue = BaseValue; UpdateKeyframes(timeline); UpdateDataBindings(timeline); OnUpdated(); } #region IDisposable /// /// 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; foreach (IDataBinding dataBinding in _dataBindings) dataBinding.Dispose(); } } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion #region Hierarchy private bool _isHidden; /// /// Gets or sets whether the property is hidden in the UI /// public bool IsHidden { get => _isHidden; set { _isHidden = value; OnVisibilityChanged(); } } /// /// Gets the profile element (such as layer or folder) this property is applied to /// public RenderProfileElement ProfileElement { get; internal set; } /// /// The parent group of this layer property, set after construction /// public LayerPropertyGroup LayerPropertyGroup { get; internal set; } #endregion #region Value management private T _baseValue; /// /// Called every update (if keyframes are both supported and enabled) to determine the new /// based on the provided progress /// /// The linear current keyframe progress /// The current keyframe progress, eased with the current easing function protected virtual void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) { throw new NotImplementedException(); } /// /// Gets or sets the base value of this layer property without any keyframes applied /// public T BaseValue { get => _baseValue; set { if (Equals(_baseValue, value)) return; _baseValue = value; ReapplyUpdate(); } } /// /// Gets the current value of this property as it is affected by it's keyframes, updated once every frame /// public T CurrentValue { get; set; } /// /// Gets or sets the default value of this layer property. If set, this value is automatically applied if the property /// has no value in storage /// public T DefaultValue { get; set; } /// /// Sets the current value, using either keyframes if enabled or the base value. /// /// The value to set. /// /// An optional time to set the value add, if provided and property is using keyframes the value will be set to an new /// or existing keyframe. /// public void SetCurrentValue(T value, TimeSpan? time) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); if (time == null || !KeyframesEnabled || !KeyframesSupported) { BaseValue = value; } else { // If on a keyframe, update the keyframe LayerPropertyKeyframe? currentKeyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value); // Create a new keyframe if none found if (currentKeyframe == null) AddKeyframe(new LayerPropertyKeyframe(value, time.Value, Easings.Functions.Linear, this)); else currentKeyframe.Value = value; } // Force an update so that the base value is applied to the current value and // keyframes/data bindings are applied using the new base value ReapplyUpdate(); } /// /// Overrides the property value with the default value /// public void ApplyDefaultValue(TimeSpan? time) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); string json = CoreJson.SerializeObject(DefaultValue, true); SetCurrentValue(CoreJson.DeserializeObject(json)!, time); } private void ReapplyUpdate() { ProfileElement.Timeline.ClearDelta(); Update(ProfileElement.Timeline); OnCurrentValueSet(); } #endregion #region Keyframes private bool _keyframesEnabled; private List> _keyframes; /// /// Gets whether keyframes are supported on this type of property /// public bool KeyframesSupported { get; protected internal set; } = true; /// /// Gets or sets whether keyframes are enabled on this property, has no effect if is /// False /// public bool KeyframesEnabled { get => _keyframesEnabled; set { if (_keyframesEnabled == value) return; _keyframesEnabled = value; OnKeyframesToggled(); } } /// /// Gets a read-only list of all the keyframes on this layer property /// public ReadOnlyCollection> Keyframes => _keyframes.AsReadOnly(); /// /// Gets the current keyframe in the timeline according to the current progress /// public LayerPropertyKeyframe? CurrentKeyframe { get; protected set; } /// /// Gets the next keyframe in the timeline according to the current progress /// public LayerPropertyKeyframe? NextKeyframe { get; protected set; } /// /// Adds a keyframe to the layer property /// /// The keyframe to add public void AddKeyframe(LayerPropertyKeyframe keyframe) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); if (_keyframes.Contains(keyframe)) return; keyframe.LayerProperty?.RemoveKeyframe(keyframe); keyframe.LayerProperty = this; _keyframes.Add(keyframe); SortKeyframes(); OnKeyframeAdded(); } /// /// Removes a keyframe from the layer property /// /// The keyframe to remove public void RemoveKeyframe(LayerPropertyKeyframe keyframe) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); if (!_keyframes.Contains(keyframe)) return; _keyframes.Remove(keyframe); SortKeyframes(); OnKeyframeRemoved(); } /// /// Sorts the keyframes in ascending order by position /// internal void SortKeyframes() { _keyframes = _keyframes.OrderBy(k => k.Position).ToList(); } private void UpdateKeyframes(Timeline timeline) { if (!KeyframesSupported || !KeyframesEnabled) return; // The current keyframe is the last keyframe before the current time CurrentKeyframe = _keyframes.LastOrDefault(k => k.Position <= timeline.Position); // Keyframes are sorted by position so we can safely assume the next keyframe's position is after the current if (CurrentKeyframe != null) { int nextIndex = _keyframes.IndexOf(CurrentKeyframe) + 1; NextKeyframe = _keyframes.Count > nextIndex ? _keyframes[nextIndex] : null; } else { NextKeyframe = null; } // No need to update the current value if either of the keyframes are null if (CurrentKeyframe == null) { CurrentValue = _keyframes.Any() ? _keyframes[0].Value : BaseValue; } else if (NextKeyframe == null) { CurrentValue = CurrentKeyframe.Value; } // Only determine progress and current value if both keyframes are present else { TimeSpan timeDiff = NextKeyframe.Position - CurrentKeyframe.Position; float keyframeProgress = (float) ((timeline.Position - CurrentKeyframe.Position).TotalMilliseconds / timeDiff.TotalMilliseconds); float keyframeProgressEased = (float) Easings.Interpolate(keyframeProgress, CurrentKeyframe.EasingFunction); UpdateCurrentValue(keyframeProgress, keyframeProgressEased); } } #endregion #region Data bindings // ReSharper disable InconsistentNaming internal readonly List _dataBindingRegistrations = new List(); internal readonly List _dataBindings = new List(); // ReSharper restore InconsistentNaming /// /// Gets whether data bindings are supported on this type of property /// public bool DataBindingsSupported { get; protected internal set; } = true; /// /// Gets whether the layer has any active data bindings /// public bool HasDataBinding => GetAllDataBindingRegistrations().Any(r => r.GetDataBinding() != null); /// /// Gets a data binding registration by the expression used to register it /// Note: The expression must exactly match the one used to register the data binding /// public DataBindingRegistration? GetDataBindingRegistration(Expression> propertyExpression) { return GetDataBindingRegistration(propertyExpression.ToString()); } internal DataBindingRegistration? GetDataBindingRegistration(string expression) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); IDataBindingRegistration? match = _dataBindingRegistrations.FirstOrDefault(r => r is DataBindingRegistration registration && registration.PropertyExpression.ToString() == expression); return (DataBindingRegistration?) match; } /// /// Gets a list containing all data binding registrations of this layer property /// /// A list containing all data binding registrations of this layer property public List GetAllDataBindingRegistrations() { return _dataBindingRegistrations; } /// /// Registers a data binding property so that is available to the data binding system /// /// The type of the layer property /// The expression pointing to the value to register /// The converter to use while applying the data binding public void RegisterDataBindingProperty(Expression> propertyExpression, DataBindingConverter converter) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); if (propertyExpression.Body.NodeType != ExpressionType.MemberAccess && propertyExpression.Body.NodeType != ExpressionType.Parameter) throw new ArtemisCoreException("Provided expression is invalid, it must be 'value => value' or 'value => value.Property'"); if (converter.SupportedType != propertyExpression.ReturnType) throw new ArtemisCoreException($"Cannot register data binding property for property {PropertyDescription.Name} " + "because the provided converter does not support the property's type"); _dataBindingRegistrations.Add(new DataBindingRegistration(this, converter, propertyExpression)); } /// /// Enables a data binding for the provided /// /// The newly created data binding public DataBinding EnableDataBinding(DataBindingRegistration dataBindingRegistration) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); if (dataBindingRegistration.LayerProperty != this) throw new ArtemisCoreException("Cannot enable a data binding using a data binding registration of a different layer property"); if (dataBindingRegistration.DataBinding != null) throw new ArtemisCoreException("Provided data binding registration already has an enabled data binding"); DataBinding dataBinding = new DataBinding(dataBindingRegistration); _dataBindings.Add(dataBinding); OnDataBindingEnabled(new LayerPropertyEventArgs(dataBinding.LayerProperty)); return dataBinding; } /// /// Disables the provided data binding /// /// The data binding to remove public void DisableDataBinding(DataBinding dataBinding) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); _dataBindings.Remove(dataBinding); if (dataBinding.Registration != null) dataBinding.Registration.DataBinding = null; dataBinding.Dispose(); OnDataBindingDisabled(new LayerPropertyEventArgs(dataBinding.LayerProperty)); } private void UpdateDataBindings(Timeline timeline) { // To avoid data bindings applying at non-regular updating (during editing) only update when not overriden if (timeline.IsOverridden) return; foreach (IDataBinding dataBinding in _dataBindings) { dataBinding.Update(timeline); dataBinding.Apply(); } } #endregion #region Storage private bool _isInitialized; /// /// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied /// public bool IsLoadedFromStorage { get; internal set; } internal PropertyEntity Entity { get; set; } /// public void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description, string path) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); _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; if (PropertyDescription.DisableKeyframes) KeyframesSupported = false; } /// public void Load() { if (_disposed) throw new ObjectDisposedException("LayerProperty"); if (!_isInitialized) throw new ArtemisCoreException("Layer property is not yet initialized"); if (!IsLoadedFromStorage) ApplyDefaultValue(null); else try { if (Entity.Value != null) BaseValue = CoreJson.DeserializeObject(Entity.Value)!; } catch (JsonException) { // ignored for now } CurrentValue = BaseValue; KeyframesEnabled = Entity.KeyframesEnabled; _keyframes.Clear(); try { _keyframes.AddRange(Entity.KeyframeEntities .Where(k => k.Position <= ProfileElement.Timeline.Length) .Select(k => new LayerPropertyKeyframe( CoreJson.DeserializeObject(k.Value)!, k.Position, (Easings.Functions) k.EasingFunction, this )) ); } catch (JsonException) { // ignored for now } _dataBindings.Clear(); foreach (IDataBindingRegistration dataBindingRegistration in _dataBindingRegistrations) { IDataBinding? dataBinding = dataBindingRegistration.CreateDataBinding(); if (dataBinding != null) _dataBindings.Add(dataBinding); } } /// /// Saves the property to the underlying property entity /// public void Save() { if (_disposed) throw new ObjectDisposedException("LayerProperty"); if (!_isInitialized) throw new ArtemisCoreException("Layer property is not yet initialized"); Entity.Value = CoreJson.SerializeObject(BaseValue); Entity.KeyframesEnabled = KeyframesEnabled; Entity.KeyframeEntities.Clear(); Entity.KeyframeEntities.AddRange(Keyframes.Select(k => k.GetKeyframeEntity())); Entity.DataBindingEntities.Clear(); foreach (IDataBinding dataBinding in _dataBindings) dataBinding.Save(); } #endregion #region Events /// /// Occurs once every frame when the layer property is updated /// public event EventHandler>? Updated; /// /// Occurs when the current value of the layer property was updated by some form of input /// public event EventHandler>? CurrentValueSet; /// /// Occurs when the value of the layer property was updated /// public event EventHandler>? VisibilityChanged; /// /// Occurs when keyframes are enabled/disabled /// public event EventHandler>? KeyframesToggled; /// /// Occurs when a new keyframe was added to the layer property /// public event EventHandler>? KeyframeAdded; /// /// Occurs when a keyframe was removed from the layer property /// public event EventHandler>? KeyframeRemoved; /// /// Occurs when a data binding has been enabled /// public event EventHandler>? DataBindingEnabled; /// /// Occurs when a data binding has been disabled /// public event EventHandler>? DataBindingDisabled; /// /// Invokes the event /// protected virtual void OnUpdated() { Updated?.Invoke(this, new LayerPropertyEventArgs(this)); } /// /// Invokes the event /// protected virtual void OnCurrentValueSet() { CurrentValueSet?.Invoke(this, new LayerPropertyEventArgs(this)); LayerPropertyGroup.OnLayerPropertyOnCurrentValueSet(new LayerPropertyEventArgs(this)); } /// /// Invokes the event /// protected virtual void OnVisibilityChanged() { VisibilityChanged?.Invoke(this, new LayerPropertyEventArgs(this)); } /// /// Invokes the event /// protected virtual void OnKeyframesToggled() { KeyframesToggled?.Invoke(this, new LayerPropertyEventArgs(this)); } /// /// Invokes the event /// protected virtual void OnKeyframeAdded() { KeyframeAdded?.Invoke(this, new LayerPropertyEventArgs(this)); } /// /// Invokes the event /// protected virtual void OnKeyframeRemoved() { KeyframeRemoved?.Invoke(this, new LayerPropertyEventArgs(this)); } /// /// Invokes the event /// protected virtual void OnDataBindingEnabled(LayerPropertyEventArgs e) { DataBindingEnabled?.Invoke(this, e); } /// /// Invokes the event /// protected virtual void OnDataBindingDisabled(LayerPropertyEventArgs e) { DataBindingDisabled?.Invoke(this, e); } #endregion } }