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
}
}