using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
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 abstract class LayerProperty : ILayerProperty
{
private bool _disposed;
///
/// Creates a new instance of the class
///
protected LayerProperty()
{
_keyframes = new List>();
}
///
/// Gets the description attribute applied to this property
///
public PropertyDescriptionAttribute PropertyDescription { get; internal set; }
///
/// Updates the property, applying keyframes and data bindings to the current value
///
public void Update(double deltaTime)
{
if (_disposed)
throw new ObjectDisposedException("LayerProperty");
CurrentValue = BaseValue;
UpdateKeyframes();
UpdateDataBindings(deltaTime);
OnUpdated();
}
///
/// Returns the type of the property
///
public Type GetPropertyType()
{
return typeof(T);
}
#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;
Update(0);
OnBaseValueChanged();
LayerPropertyGroup.OnLayerPropertyBaseValueChanged(new LayerPropertyEventArgs(this));
}
}
///
/// 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
var 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
Update(0);
}
///
/// Overrides the property value with the default value
///
public void ApplyDefaultValue()
{
if (_disposed)
throw new ObjectDisposedException("LayerProperty");
BaseValue = DefaultValue;
CurrentValue = DefaultValue;
}
#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 IReadOnlyList> 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 LayerPropertyKeyframe CopyKeyframe(LayerPropertyKeyframe keyframe)
{
if (_disposed)
throw new ObjectDisposedException("LayerProperty");
var newKeyframe = new LayerPropertyKeyframe(
keyframe.Value,
keyframe.Position,
keyframe.EasingFunction,
keyframe.LayerProperty
);
AddKeyframe(newKeyframe);
return newKeyframe;
}
///
/// 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);
keyframe.LayerProperty = null;
SortKeyframes();
OnKeyframeRemoved();
}
///
/// Sorts the keyframes in ascending order by position
///
internal void SortKeyframes()
{
_keyframes = _keyframes.OrderBy(k => k.Position).ToList();
}
private void UpdateKeyframes()
{
if (!KeyframesSupported || !KeyframesEnabled)
return;
// The current keyframe is the last keyframe before the current time
CurrentKeyframe = _keyframes.LastOrDefault(k => k.Position <= ProfileElement.TimelinePosition);
// Keyframes are sorted by position so we can safely assume the next keyframe's position is after the current
var nextIndex = _keyframes.IndexOf(CurrentKeyframe) + 1;
NextKeyframe = _keyframes.Count > nextIndex ? _keyframes[nextIndex] : 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
{
var timeDiff = NextKeyframe.Position - CurrentKeyframe.Position;
var keyframeProgress = (float) ((ProfileElement.TimelinePosition - CurrentKeyframe.Position).TotalMilliseconds / timeDiff.TotalMilliseconds);
var keyframeProgressEased = (float) Easings.Interpolate(keyframeProgress, CurrentKeyframe.EasingFunction);
UpdateCurrentValue(keyframeProgress, keyframeProgressEased);
}
}
#endregion
#region Data bindings
internal readonly List _dataBindingRegistrations = new List();
internal readonly List _dataBindings = new List();
///
/// Gets whether data bindings are supported on this type of property
///
public bool DataBindingsSupported { get; protected internal set; } = true;
public DataBindingRegistration GetDataBindingRegistration(string expression)
{
if (_disposed)
throw new ObjectDisposedException("LayerProperty");
var match = _dataBindingRegistrations.FirstOrDefault(r => r is DataBindingRegistration registration &&
registration.PropertyExpression.ToString() == expression);
return (DataBindingRegistration) match;
}
public List GetAllDataBindingRegistrations()
{
return _dataBindingRegistrations;
}
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");
var dataBinding = new DataBinding(dataBindingRegistration);
_dataBindings.Add(dataBinding);
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);
}
private void UpdateDataBindings(double deltaTime)
{
foreach (var dataBinding in _dataBindings)
{
dataBinding.Update(deltaTime);
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)
{
if (_disposed)
throw new ObjectDisposedException("LayerProperty");
_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;
LayerPropertyGroup.PropertyGroupUpdating += (sender, args) => Update(args.DeltaTime);
}
///
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 = JsonConvert.DeserializeObject(Entity.Value);
}
catch (JsonException e)
{
// ignored for now
}
}
CurrentValue = BaseValue;
KeyframesEnabled = Entity.KeyframesEnabled;
_keyframes.Clear();
try
{
_keyframes.AddRange(Entity.KeyframeEntities.Select(k => new LayerPropertyKeyframe(
JsonConvert.DeserializeObject(k.Value),
k.Position,
(Easings.Functions) k.EasingFunction,
this
)));
}
catch (JsonException e)
{
// ignored for now
}
_dataBindings.Clear();
foreach (var dataBindingRegistration in _dataBindingRegistrations)
{
var dataBinding = dataBindingRegistration.CreateDataBinding();
if (dataBinding != null)
_dataBindings.Add(dataBinding);
}
}
///
/// Saves the property to the underlying property entity that was configured when calling
///
///
public void Save()
{
if (_disposed)
throw new ObjectDisposedException("LayerProperty");
if (!_isInitialized)
throw new ArtemisCoreException("Layer property is not yet initialized");
Entity.Value = JsonConvert.SerializeObject(BaseValue);
Entity.KeyframesEnabled = KeyframesEnabled;
Entity.KeyframeEntities.Clear();
Entity.KeyframeEntities.AddRange(Keyframes.Select(k => new KeyframeEntity
{
Value = JsonConvert.SerializeObject(k.Value),
Position = k.Position,
EasingFunction = (int) k.EasingFunction
}));
Entity.DataBindingEntities.Clear();
foreach (var 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 base value of the layer property was updated
///
public event EventHandler> BaseValueChanged;
///
/// 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;
protected virtual void OnUpdated()
{
Updated?.Invoke(this, new LayerPropertyEventArgs(this));
}
protected virtual void OnBaseValueChanged()
{
BaseValueChanged?.Invoke(this, new LayerPropertyEventArgs(this));
}
protected virtual void OnVisibilityChanged()
{
VisibilityChanged?.Invoke(this, new LayerPropertyEventArgs(this));
}
protected virtual void OnKeyframesToggled()
{
KeyframesToggled?.Invoke(this, new LayerPropertyEventArgs(this));
}
protected virtual void OnKeyframeAdded()
{
KeyframeAdded?.Invoke(this, new LayerPropertyEventArgs(this));
}
protected virtual void OnKeyframeRemoved()
{
KeyframeRemoved?.Invoke(this, new LayerPropertyEventArgs(this));
}
#endregion
///
public void Dispose()
{
_disposed = true;
foreach (var dataBinding in _dataBindings)
dataBinding.Dispose();
}
}
}