using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Text; 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 : CorePropertyChanged, 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!; Entity = null!; PropertyDescription = null!; DataBinding = null!; Path = ""; CurrentValue = default!; DefaultValue = default!; // We'll try our best... // TODO: Consider alternatives if (typeof(T).IsValueType) _baseValue = default!; else if (typeof(T).GetConstructor(Type.EmptyTypes) != null) _baseValue = Activator.CreateInstance(); else _baseValue = default!; _keyframes = new List>(); Keyframes = new ReadOnlyCollection>(_keyframes); } /// /// 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) { _disposed = true; DataBinding.Dispose(); Disposed?.Invoke(this, EventArgs.Empty); } /// public PropertyDescriptionAttribute PropertyDescription { get; internal set; } /// public string Path { get; private set; } /// public Type PropertyType => typeof(T); /// public void Update(Timeline timeline) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); CurrentValue = BaseValue; UpdateKeyframes(timeline); UpdateDataBinding(); // UpdateDataBinding called OnUpdated() } /// public void UpdateDataBinding() { DataBinding.Update(); DataBinding.Apply(); OnUpdated(); } /// public void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe) { if (keyframe is not LayerPropertyKeyframe typedKeyframe) throw new ArtemisCoreException($"Can't remove a keyframe that is not of type {typeof(T).FullName}."); RemoveKeyframe(typedKeyframe); } /// public void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe) { if (keyframe is not LayerPropertyKeyframe typedKeyframe) throw new ArtemisCoreException($"Can't add a keyframe that is not of type {typeof(T).FullName}."); AddKeyframe(typedKeyframe); } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #region Hierarchy private bool _isHidden; /// public bool IsHidden { get => _isHidden; set { _isHidden = value; OnVisibilityChanged(); } } /// public RenderProfileElement ProfileElement { get; private set; } /// public LayerPropertyGroup LayerPropertyGroup { get; private 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(); OnPropertyChanged(nameof(BaseValue)); } } /// /// 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. /// /// The new keyframe if one was created. public LayerPropertyKeyframe? SetCurrentValue(T value, TimeSpan? time) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); LayerPropertyKeyframe? newKeyframe = null; 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) { newKeyframe = new LayerPropertyKeyframe(value, time.Value, Easings.Functions.Linear, this); AddKeyframe(newKeyframe); } 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(); return newKeyframe; } /// public void ApplyDefaultValue() { if (_disposed) throw new ObjectDisposedException("LayerProperty"); string json = CoreJson.SerializeObject(DefaultValue, true); KeyframesEnabled = false; SetCurrentValue(CoreJson.DeserializeObject(json)!, null); } internal void ReapplyUpdate() { // Create a timeline with the same position but a delta of zero Timeline temporaryTimeline = new(); temporaryTimeline.Override(ProfileElement.Timeline.Position, false); temporaryTimeline.ClearDelta(); Update(temporaryTimeline); OnCurrentValueSet(); } #endregion #region Keyframes private bool _keyframesEnabled; private readonly 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; ReapplyUpdate(); OnKeyframesToggled(); OnPropertyChanged(nameof(KeyframesEnabled)); } } /// /// Gets a read-only list of all the keyframes on this layer property /// public ReadOnlyCollection> Keyframes { get; } /// public ReadOnlyCollection UntypedKeyframes => new(Keyframes.Cast().ToList()); /// /// 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); if (!KeyframesEnabled) KeyframesEnabled = true; SortKeyframes(); ReapplyUpdate(); OnKeyframeAdded(); } /// public ILayerPropertyKeyframe? AddKeyframeEntity(KeyframeEntity keyframeEntity) { if (keyframeEntity.Position > ProfileElement.Timeline.Length) return null; T? value = CoreJson.DeserializeObject(keyframeEntity.Value); if (value == null) return null; LayerPropertyKeyframe keyframe = new( CoreJson.DeserializeObject(keyframeEntity.Value)!, keyframeEntity.Position, (Easings.Functions) keyframeEntity.EasingFunction, this ); AddKeyframe(keyframe); return keyframe; } /// /// 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(); ReapplyUpdate(); OnKeyframeRemoved(); } /// /// Sorts the keyframes in ascending order by position /// internal void SortKeyframes() { _keyframes.Sort((a, b) => a.Position.CompareTo(b.Position)); } 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 /// /// Gets the data binding of this property /// public DataBinding DataBinding { get; private set; } /// public bool DataBindingsSupported => DataBinding.Properties.Any(); /// public IDataBinding BaseDataBinding => DataBinding; /// public bool HasDataBinding => DataBinding.IsEnabled; #endregion #region Visbility /// /// Set up a condition to hide the provided layer property when the condition evaluates to /// Note: overrides previous calls to IsHiddenWhen and IsVisibleWhen /// /// The type of the target layer property /// The target layer property /// The condition to evaluate to determine whether to hide the current layer property public void IsHiddenWhen(TP layerProperty, Func condition) where TP : ILayerProperty { IsHiddenWhen(layerProperty, condition, false); } /// /// Set up a condition to show the provided layer property when the condition evaluates to /// Note: overrides previous calls to IsHiddenWhen and IsVisibleWhen /// /// The type of the target layer property /// The target layer property /// The condition to evaluate to determine whether to hide the current layer property public void IsVisibleWhen(TP layerProperty, Func condition) where TP : ILayerProperty { IsHiddenWhen(layerProperty, condition, true); } private void IsHiddenWhen(TP layerProperty, Func condition, bool inverse) where TP : ILayerProperty { layerProperty.VisibilityChanged += LayerPropertyChanged; layerProperty.CurrentValueSet += LayerPropertyChanged; layerProperty.Disposed += LayerPropertyOnDisposed; void LayerPropertyChanged(object? sender, LayerPropertyEventArgs e) { if (inverse) IsHidden = !condition(layerProperty); else IsHidden = condition(layerProperty); } void LayerPropertyOnDisposed(object? sender, EventArgs e) { layerProperty.VisibilityChanged -= LayerPropertyChanged; layerProperty.CurrentValueSet -= LayerPropertyChanged; layerProperty.Disposed -= LayerPropertyOnDisposed; } if (inverse) IsHidden = !condition(layerProperty); else IsHidden = condition(layerProperty); } #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) { 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)); Entity = entity ?? throw new ArgumentNullException(nameof(entity)); PropertyDescription = description ?? throw new ArgumentNullException(nameof(description)); IsLoadedFromStorage = fromStorage; DataBinding = Entity.DataBinding != null ? new DataBinding(this, Entity.DataBinding) : new DataBinding(this); if (PropertyDescription.DisableKeyframes) KeyframesSupported = false; // Create the path to this property by walking up the tree Path = LayerPropertyGroup.Path + "." + description.Identifier; OnInitialize(); } /// public void Load() { if (_disposed) throw new ObjectDisposedException("LayerProperty"); if (!_isInitialized) throw new ArtemisCoreException("Layer property is not yet initialized"); if (!IsLoadedFromStorage) ApplyDefaultValue(); else try { if (Entity.Value != null) BaseValue = CoreJson.DeserializeObject(Entity.Value)!; } catch (JsonException) { // ignored for now } CurrentValue = BaseValue; KeyframesEnabled = Entity.KeyframesEnabled; _keyframes.Clear(); try { foreach (KeyframeEntity keyframeEntity in Entity.KeyframeEntities.Where(k => k.Position <= ProfileElement.Timeline.Length)) AddKeyframeEntity(keyframeEntity); } catch (JsonException) { // ignored for now } DataBinding.Load(); } /// /// 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())); DataBinding.Save(); Entity.DataBinding = DataBinding.Entity; } /// /// Called when the layer property has been initialized /// protected virtual void OnInitialize() { } #endregion #region Events /// public event EventHandler? Disposed; /// public event EventHandler? Updated; /// public event EventHandler? CurrentValueSet; /// public event EventHandler? VisibilityChanged; /// public event EventHandler? KeyframesToggled; /// public event EventHandler? KeyframeAdded; /// public event EventHandler? KeyframeRemoved; /// /// 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)); } #endregion } }