mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Timeline - Implemented segments
This commit is contained in:
parent
913117ad0a
commit
642823add5
@ -18,7 +18,7 @@ namespace Artemis.Core
|
|||||||
/// Gets the description attribute applied to this property
|
/// Gets the description attribute applied to this property
|
||||||
/// </summary>
|
/// </summary>
|
||||||
PropertyDescriptionAttribute PropertyDescription { get; }
|
PropertyDescriptionAttribute PropertyDescription { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the profile element (such as layer or folder) this property is applied to
|
/// Gets the profile element (such as layer or folder) this property is applied to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -68,7 +68,7 @@ namespace Artemis.Core
|
|||||||
/// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied
|
/// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool IsLoadedFromStorage { get; }
|
bool IsLoadedFromStorage { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes the layer property
|
/// Initializes the layer property
|
||||||
/// <para>
|
/// <para>
|
||||||
@ -102,6 +102,20 @@ namespace Artemis.Core
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
void UpdateDataBinding();
|
void UpdateDataBinding();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a keyframe from the layer property without knowing it's type.
|
||||||
|
/// <para>Prefer <see cref="LayerProperty{T}.RemoveKeyframe"/>.</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keyframe"></param>
|
||||||
|
void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a keyframe to the layer property without knowing it's type.
|
||||||
|
/// <para>Prefer <see cref="LayerProperty{T}.AddKeyframe"/>.</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keyframe"></param>
|
||||||
|
void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when the layer property is disposed
|
/// Occurs when the layer property is disposed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -96,6 +96,24 @@ namespace Artemis.Core
|
|||||||
OnUpdated();
|
OnUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe)
|
||||||
|
{
|
||||||
|
if (keyframe is not LayerPropertyKeyframe<T> typedKeyframe)
|
||||||
|
throw new ArtemisCoreException($"Can't remove a keyframe that is not of type {typeof(T).FullName}.");
|
||||||
|
|
||||||
|
RemoveKeyframe(typedKeyframe);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe)
|
||||||
|
{
|
||||||
|
if (keyframe is not LayerPropertyKeyframe<T> typedKeyframe)
|
||||||
|
throw new ArtemisCoreException($"Can't add a keyframe that is not of type {typeof(T).FullName}.");
|
||||||
|
|
||||||
|
AddKeyframe(typedKeyframe);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
@ -177,22 +195,25 @@ namespace Artemis.Core
|
|||||||
/// An optional time to set the value add, if provided and property is using keyframes the value will be set to an new
|
/// 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.
|
/// or existing keyframe.
|
||||||
/// </param>
|
/// </param>
|
||||||
public void SetCurrentValue(T value, TimeSpan? time)
|
/// <returns>The new keyframe if one was created.</returns>
|
||||||
|
public LayerPropertyKeyframe<T>? SetCurrentValue(T value, TimeSpan? time)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
throw new ObjectDisposedException("LayerProperty");
|
throw new ObjectDisposedException("LayerProperty");
|
||||||
|
|
||||||
|
LayerPropertyKeyframe<T>? newKeyframe = null;
|
||||||
if (time == null || !KeyframesEnabled || !KeyframesSupported)
|
if (time == null || !KeyframesEnabled || !KeyframesSupported)
|
||||||
{
|
|
||||||
BaseValue = value;
|
BaseValue = value;
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// If on a keyframe, update the keyframe
|
// If on a keyframe, update the keyframe
|
||||||
LayerPropertyKeyframe<T>? currentKeyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value);
|
LayerPropertyKeyframe<T>? currentKeyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value);
|
||||||
// Create a new keyframe if none found
|
// Create a new keyframe if none found
|
||||||
if (currentKeyframe == null)
|
if (currentKeyframe == null)
|
||||||
AddKeyframe(new LayerPropertyKeyframe<T>(value, time.Value, Easings.Functions.Linear, this));
|
{
|
||||||
|
newKeyframe = new LayerPropertyKeyframe<T>(value, time.Value, Easings.Functions.Linear, this);
|
||||||
|
AddKeyframe(newKeyframe);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
currentKeyframe.Value = value;
|
currentKeyframe.Value = value;
|
||||||
}
|
}
|
||||||
@ -200,6 +221,7 @@ namespace Artemis.Core
|
|||||||
// Force an update so that the base value is applied to the current value and
|
// 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
|
// keyframes/data bindings are applied using the new base value
|
||||||
ReapplyUpdate();
|
ReapplyUpdate();
|
||||||
|
return newKeyframe;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -247,6 +269,7 @@ namespace Artemis.Core
|
|||||||
{
|
{
|
||||||
if (_keyframesEnabled == value) return;
|
if (_keyframesEnabled == value) return;
|
||||||
_keyframesEnabled = value;
|
_keyframesEnabled = value;
|
||||||
|
ReapplyUpdate();
|
||||||
OnKeyframesToggled();
|
OnKeyframesToggled();
|
||||||
OnPropertyChanged(nameof(KeyframesEnabled));
|
OnPropertyChanged(nameof(KeyframesEnabled));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,517 +2,529 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
|
||||||
using Artemis.Storage.Entities.Profile;
|
using Artemis.Storage.Entities.Profile;
|
||||||
|
|
||||||
namespace Artemis.Core
|
namespace Artemis.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a timeline used by profile elements
|
||||||
|
/// </summary>
|
||||||
|
public class Timeline : CorePropertyChanged, IStorageModel
|
||||||
{
|
{
|
||||||
|
private const int MaxExtraTimelines = 15;
|
||||||
|
private readonly object _lock = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a timeline used by profile elements
|
/// Creates a new instance of the <see cref="Timeline" /> class
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Timeline : CorePropertyChanged, IStorageModel
|
public Timeline()
|
||||||
{
|
{
|
||||||
private const int MaxExtraTimelines = 15;
|
Entity = new TimelineEntity();
|
||||||
private readonly object _lock = new();
|
MainSegmentLength = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
/// <summary>
|
_extraTimelines = new List<Timeline>();
|
||||||
/// Creates a new instance of the <see cref="Timeline" /> class
|
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
|
||||||
/// </summary>
|
|
||||||
public Timeline()
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Timeline(TimelineEntity entity)
|
||||||
|
{
|
||||||
|
Entity = entity;
|
||||||
|
_extraTimelines = new List<Timeline>();
|
||||||
|
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
|
||||||
|
|
||||||
|
Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Timeline(Timeline parent)
|
||||||
|
{
|
||||||
|
Entity = new TimelineEntity();
|
||||||
|
Parent = parent;
|
||||||
|
StartSegmentLength = Parent.StartSegmentLength;
|
||||||
|
MainSegmentLength = Parent.MainSegmentLength;
|
||||||
|
EndSegmentLength = Parent.EndSegmentLength;
|
||||||
|
|
||||||
|
_extraTimelines = new List<Timeline>();
|
||||||
|
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"Progress: {Position}/{Length} - delta: {Delta}";
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Extra timelines
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an extra timeline to this timeline
|
||||||
|
/// </summary>
|
||||||
|
public void AddExtraTimeline()
|
||||||
|
{
|
||||||
|
_extraTimelines.Add(new Timeline(this));
|
||||||
|
if (_extraTimelines.Count > MaxExtraTimelines)
|
||||||
|
_extraTimelines.RemoveAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes all extra timelines from this timeline
|
||||||
|
/// </summary>
|
||||||
|
public void ClearExtraTimelines()
|
||||||
|
{
|
||||||
|
_extraTimelines.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
private TimeSpan _position;
|
||||||
|
private TimeSpan _lastDelta;
|
||||||
|
private TimelinePlayMode _playMode;
|
||||||
|
private TimelineStopMode _stopMode;
|
||||||
|
private readonly List<Timeline> _extraTimelines;
|
||||||
|
private TimeSpan _startSegmentLength;
|
||||||
|
private TimeSpan _mainSegmentLength;
|
||||||
|
private TimeSpan _endSegmentLength;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the parent this timeline is an extra timeline of
|
||||||
|
/// </summary>
|
||||||
|
public Timeline? Parent { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current position of the timeline
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan Position
|
||||||
|
{
|
||||||
|
get => _position;
|
||||||
|
private set => SetAndNotify(ref _position, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the cumulative delta of all calls to <see cref="Update" /> that took place after the last call to
|
||||||
|
/// <see cref="ClearDelta" />
|
||||||
|
/// <para>
|
||||||
|
/// Note: If this is an extra timeline <see cref="Delta" /> is always equal to <see cref="DeltaToParent" />
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan Delta
|
||||||
|
{
|
||||||
|
get => Parent == null ? _lastDelta : DeltaToParent;
|
||||||
|
private set => SetAndNotify(ref _lastDelta, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the delta to this timeline's <see cref="Parent" />
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan DeltaToParent => Parent != null ? Position - Parent.Position : TimeSpan.Zero;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the mode in which the render element starts its timeline when display conditions are met
|
||||||
|
/// </summary>
|
||||||
|
public TimelinePlayMode PlayMode
|
||||||
|
{
|
||||||
|
get => _playMode;
|
||||||
|
set => SetAndNotify(ref _playMode, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met
|
||||||
|
/// </summary>
|
||||||
|
public TimelineStopMode StopMode
|
||||||
|
{
|
||||||
|
get => _stopMode;
|
||||||
|
set => SetAndNotify(ref _stopMode, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a list of extra copies of the timeline applied to this timeline
|
||||||
|
/// </summary>
|
||||||
|
public ReadOnlyCollection<Timeline> ExtraTimelines { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a boolean indicating whether the timeline has finished its run
|
||||||
|
/// </summary>
|
||||||
|
public bool IsFinished => Position > Length && !ExtraTimelines.Any();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a boolean indicating whether the timeline progress has been overridden
|
||||||
|
/// </summary>
|
||||||
|
public bool IsOverridden { get; private set; }
|
||||||
|
|
||||||
|
#region Segments
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the total length of this timeline
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan Length => StartSegmentLength + MainSegmentLength + EndSegmentLength;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the length of the start segment
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan StartSegmentLength
|
||||||
|
{
|
||||||
|
get => _startSegmentLength;
|
||||||
|
set
|
||||||
{
|
{
|
||||||
Entity = new TimelineEntity();
|
if (SetAndNotify(ref _startSegmentLength, value))
|
||||||
MainSegmentLength = TimeSpan.FromSeconds(5);
|
NotifySegmentShiftAt(TimelineSegment.Start, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_extraTimelines = new List<Timeline>();
|
/// <summary>
|
||||||
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
|
/// Gets or sets the length of the main segment
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan MainSegmentLength
|
||||||
|
{
|
||||||
|
get => _mainSegmentLength;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetAndNotify(ref _mainSegmentLength, value))
|
||||||
|
NotifySegmentShiftAt(TimelineSegment.Main, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Save();
|
/// <summary>
|
||||||
|
/// Gets or sets the length of the end segment
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan EndSegmentLength
|
||||||
|
{
|
||||||
|
get => _endSegmentLength;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetAndNotify(ref _endSegmentLength, value))
|
||||||
|
NotifySegmentShiftAt(TimelineSegment.End, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the start position of the main segment
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan MainSegmentStartPosition
|
||||||
|
{
|
||||||
|
get => StartSegmentEndPosition;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
StartSegmentEndPosition = value;
|
||||||
|
NotifySegmentShiftAt(TimelineSegment.Main, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the end position of the end segment
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan EndSegmentStartPosition
|
||||||
|
{
|
||||||
|
get => MainSegmentEndPosition;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
MainSegmentEndPosition = value;
|
||||||
|
NotifySegmentShiftAt(TimelineSegment.End, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the end position of the start segment
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan StartSegmentEndPosition
|
||||||
|
{
|
||||||
|
get => StartSegmentLength;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
StartSegmentLength = value;
|
||||||
|
NotifySegmentShiftAt(TimelineSegment.Start, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the end position of the main segment
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan MainSegmentEndPosition
|
||||||
|
{
|
||||||
|
get => StartSegmentEndPosition + MainSegmentLength;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
MainSegmentLength = value - StartSegmentEndPosition >= TimeSpan.Zero ? value - StartSegmentEndPosition : TimeSpan.Zero;
|
||||||
|
NotifySegmentShiftAt(TimelineSegment.Main, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the end position of the end segment
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan EndSegmentEndPosition
|
||||||
|
{
|
||||||
|
get => MainSegmentEndPosition + EndSegmentLength;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
EndSegmentLength = value - MainSegmentEndPosition >= TimeSpan.Zero ? value - MainSegmentEndPosition : TimeSpan.Zero;
|
||||||
|
NotifySegmentShiftAt(TimelineSegment.End, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal TimelineEntity Entity { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Notifies the right segments in a way that I don't have to think about it
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="segment">The segment that was updated</param>
|
||||||
|
/// <param name="startUpdated">Whether the start point of the <paramref name="segment" /> was updated</param>
|
||||||
|
private void NotifySegmentShiftAt(TimelineSegment segment, bool startUpdated)
|
||||||
|
{
|
||||||
|
if (segment <= TimelineSegment.End)
|
||||||
|
{
|
||||||
|
if (startUpdated || segment < TimelineSegment.End)
|
||||||
|
OnPropertyChanged(nameof(EndSegmentStartPosition));
|
||||||
|
OnPropertyChanged(nameof(EndSegmentEndPosition));
|
||||||
}
|
}
|
||||||
|
|
||||||
internal Timeline(TimelineEntity entity)
|
if (segment <= TimelineSegment.Main)
|
||||||
{
|
{
|
||||||
Entity = entity;
|
if (startUpdated || segment < TimelineSegment.Main)
|
||||||
_extraTimelines = new List<Timeline>();
|
OnPropertyChanged(nameof(MainSegmentStartPosition));
|
||||||
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
|
OnPropertyChanged(nameof(MainSegmentEndPosition));
|
||||||
|
|
||||||
Load();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Timeline(Timeline parent)
|
if (segment <= TimelineSegment.Start)
|
||||||
{
|
OnPropertyChanged(nameof(StartSegmentEndPosition));
|
||||||
Entity = new TimelineEntity();
|
|
||||||
Parent = parent;
|
|
||||||
StartSegmentLength = Parent.StartSegmentLength;
|
|
||||||
MainSegmentLength = Parent.MainSegmentLength;
|
|
||||||
EndSegmentLength = Parent.EndSegmentLength;
|
|
||||||
|
|
||||||
_extraTimelines = new List<Timeline>();
|
OnPropertyChanged(nameof(Length));
|
||||||
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
|
OnTimelineChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs when changes have been made to any of the segments of the timeline.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler? TimelineChanged;
|
||||||
|
|
||||||
|
private void OnTimelineChanged()
|
||||||
|
{
|
||||||
|
TimelineChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Updating
|
||||||
|
|
||||||
|
private TimeSpan _lastOverridePosition;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the timeline, applying the provided <paramref name="delta" /> to the <see cref="Position" />
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="delta">The amount of time to apply to the position</param>
|
||||||
|
/// <param name="stickToMainSegment">Whether to stick to the main segment, wrapping around if needed</param>
|
||||||
|
public void Update(TimeSpan delta, bool stickToMainSegment)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
Delta += delta;
|
||||||
|
Position += delta;
|
||||||
|
|
||||||
|
IsOverridden = false;
|
||||||
|
_lastOverridePosition = Position;
|
||||||
|
|
||||||
|
if (stickToMainSegment && Position > MainSegmentEndPosition)
|
||||||
|
{
|
||||||
|
// If the main segment has no length, simply stick to the start of the segment
|
||||||
|
if (MainSegmentLength == TimeSpan.Zero)
|
||||||
|
Position = MainSegmentStartPosition;
|
||||||
|
// Ensure wrapping back around retains the delta time
|
||||||
|
else
|
||||||
|
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(delta.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
_extraTimelines.RemoveAll(t => t.IsFinished);
|
||||||
|
foreach (Timeline extraTimeline in _extraTimelines)
|
||||||
|
extraTimeline.Update(delta, false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public override string ToString()
|
/// Moves the position of the timeline backwards to the very start of the timeline
|
||||||
|
/// </summary>
|
||||||
|
public void JumpToStart()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
{
|
{
|
||||||
return $"Progress: {Position}/{Length} - delta: {Delta}";
|
if (Position == TimeSpan.Zero)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Delta = TimeSpan.Zero - Position;
|
||||||
|
Position = TimeSpan.Zero;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#region Extra timelines
|
/// <summary>
|
||||||
|
/// Moves the position of the timeline forwards to the beginning of the end segment
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// Adds an extra timeline to this timeline
|
public void JumpToEndSegment()
|
||||||
/// </summary>
|
{
|
||||||
public void AddExtraTimeline()
|
lock (_lock)
|
||||||
{
|
{
|
||||||
_extraTimelines.Add(new Timeline(this));
|
if (Position >= EndSegmentStartPosition)
|
||||||
if (_extraTimelines.Count > MaxExtraTimelines)
|
return;
|
||||||
_extraTimelines.RemoveAt(0);
|
|
||||||
|
Delta = EndSegmentStartPosition - Position;
|
||||||
|
Position = EndSegmentStartPosition;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes all extra timelines from this timeline
|
/// Moves the position of the timeline forwards to the very end of the timeline
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ClearExtraTimelines()
|
public void JumpToEnd()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
{
|
{
|
||||||
|
if (Position >= EndSegmentEndPosition)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Delta = EndSegmentEndPosition - Position;
|
||||||
|
Position = EndSegmentEndPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Overrides the <see cref="Position" /> to the specified time and clears any extra time lines
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="position">The position to set the timeline to</param>
|
||||||
|
/// <param name="stickToMainSegment">Whether to stick to the main segment, wrapping around if needed</param>
|
||||||
|
public void Override(TimeSpan position, bool stickToMainSegment)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
Delta += position - _lastOverridePosition;
|
||||||
|
Position = position;
|
||||||
|
|
||||||
|
IsOverridden = true;
|
||||||
|
_lastOverridePosition = position;
|
||||||
|
|
||||||
|
if (stickToMainSegment && Position >= MainSegmentStartPosition)
|
||||||
|
{
|
||||||
|
bool atSegmentStart = Position == MainSegmentStartPosition;
|
||||||
|
if (MainSegmentLength > TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
|
||||||
|
// If the cursor is at the end of the timeline we don't want to wrap back around yet so only allow going to the start if the cursor
|
||||||
|
// is actually at the start of the segment
|
||||||
|
if (Position == MainSegmentStartPosition && !atSegmentStart)
|
||||||
|
Position = MainSegmentEndPosition;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Position = MainSegmentStartPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_extraTimelines.Clear();
|
_extraTimelines.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
|
|
||||||
private TimeSpan _position;
|
|
||||||
private TimeSpan _lastDelta;
|
|
||||||
private TimelinePlayMode _playMode;
|
|
||||||
private TimelineStopMode _stopMode;
|
|
||||||
private readonly List<Timeline> _extraTimelines;
|
|
||||||
private TimeSpan _startSegmentLength;
|
|
||||||
private TimeSpan _mainSegmentLength;
|
|
||||||
private TimeSpan _endSegmentLength;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the parent this timeline is an extra timeline of
|
|
||||||
/// </summary>
|
|
||||||
public Timeline? Parent { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the current position of the timeline
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan Position
|
|
||||||
{
|
|
||||||
get => _position;
|
|
||||||
private set => SetAndNotify(ref _position, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the cumulative delta of all calls to <see cref="Update" /> that took place after the last call to
|
|
||||||
/// <see cref="ClearDelta" />
|
|
||||||
/// <para>
|
|
||||||
/// Note: If this is an extra timeline <see cref="Delta" /> is always equal to <see cref="DeltaToParent" />
|
|
||||||
/// </para>
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan Delta
|
|
||||||
{
|
|
||||||
get => Parent == null ? _lastDelta : DeltaToParent;
|
|
||||||
private set => SetAndNotify(ref _lastDelta, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the delta to this timeline's <see cref="Parent" />
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan DeltaToParent => Parent != null ? Position - Parent.Position : TimeSpan.Zero;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the mode in which the render element starts its timeline when display conditions are met
|
|
||||||
/// </summary>
|
|
||||||
public TimelinePlayMode PlayMode
|
|
||||||
{
|
|
||||||
get => _playMode;
|
|
||||||
set => SetAndNotify(ref _playMode, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met
|
|
||||||
/// </summary>
|
|
||||||
public TimelineStopMode StopMode
|
|
||||||
{
|
|
||||||
get => _stopMode;
|
|
||||||
set => SetAndNotify(ref _stopMode, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a list of extra copies of the timeline applied to this timeline
|
|
||||||
/// </summary>
|
|
||||||
public ReadOnlyCollection<Timeline> ExtraTimelines { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a boolean indicating whether the timeline has finished its run
|
|
||||||
/// </summary>
|
|
||||||
public bool IsFinished => Position > Length && !ExtraTimelines.Any();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a boolean indicating whether the timeline progress has been overridden
|
|
||||||
/// </summary>
|
|
||||||
public bool IsOverridden { get; private set; }
|
|
||||||
|
|
||||||
#region Segments
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the total length of this timeline
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan Length => StartSegmentLength + MainSegmentLength + EndSegmentLength;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the length of the start segment
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan StartSegmentLength
|
|
||||||
{
|
|
||||||
get => _startSegmentLength;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (SetAndNotify(ref _startSegmentLength, value))
|
|
||||||
NotifySegmentShiftAt(TimelineSegment.Start, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the length of the main segment
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan MainSegmentLength
|
|
||||||
{
|
|
||||||
get => _mainSegmentLength;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (SetAndNotify(ref _mainSegmentLength, value))
|
|
||||||
NotifySegmentShiftAt(TimelineSegment.Main, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the length of the end segment
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan EndSegmentLength
|
|
||||||
{
|
|
||||||
get => _endSegmentLength;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (SetAndNotify(ref _endSegmentLength, value))
|
|
||||||
NotifySegmentShiftAt(TimelineSegment.End, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the start position of the main segment
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan MainSegmentStartPosition
|
|
||||||
{
|
|
||||||
get => StartSegmentEndPosition;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
StartSegmentEndPosition = value;
|
|
||||||
NotifySegmentShiftAt(TimelineSegment.Main, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the end position of the end segment
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan EndSegmentStartPosition
|
|
||||||
{
|
|
||||||
get => MainSegmentEndPosition;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
MainSegmentEndPosition = value;
|
|
||||||
NotifySegmentShiftAt(TimelineSegment.End, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the end position of the start segment
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan StartSegmentEndPosition
|
|
||||||
{
|
|
||||||
get => StartSegmentLength;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
StartSegmentLength = value;
|
|
||||||
NotifySegmentShiftAt(TimelineSegment.Start, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the end position of the main segment
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan MainSegmentEndPosition
|
|
||||||
{
|
|
||||||
get => StartSegmentEndPosition + MainSegmentLength;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
MainSegmentLength = value - StartSegmentEndPosition >= TimeSpan.Zero ? value - StartSegmentEndPosition : TimeSpan.Zero;
|
|
||||||
NotifySegmentShiftAt(TimelineSegment.Main, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the end position of the end segment
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan EndSegmentEndPosition
|
|
||||||
{
|
|
||||||
get => MainSegmentEndPosition + EndSegmentLength;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
EndSegmentLength = value - MainSegmentEndPosition >= TimeSpan.Zero ? value - MainSegmentEndPosition : TimeSpan.Zero;
|
|
||||||
NotifySegmentShiftAt(TimelineSegment.End, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal TimelineEntity Entity { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Notifies the right segments in a way that I don't have to think about it
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="segment">The segment that was updated</param>
|
|
||||||
/// <param name="startUpdated">Whether the start point of the <paramref name="segment" /> was updated</param>
|
|
||||||
private void NotifySegmentShiftAt(TimelineSegment segment, bool startUpdated)
|
|
||||||
{
|
|
||||||
if (segment <= TimelineSegment.End)
|
|
||||||
{
|
|
||||||
if (startUpdated || segment < TimelineSegment.End)
|
|
||||||
OnPropertyChanged(nameof(EndSegmentStartPosition));
|
|
||||||
OnPropertyChanged(nameof(EndSegmentEndPosition));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (segment <= TimelineSegment.Main)
|
|
||||||
{
|
|
||||||
if (startUpdated || segment < TimelineSegment.Main)
|
|
||||||
OnPropertyChanged(nameof(MainSegmentStartPosition));
|
|
||||||
OnPropertyChanged(nameof(MainSegmentEndPosition));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (segment <= TimelineSegment.Start) OnPropertyChanged(nameof(StartSegmentEndPosition));
|
|
||||||
|
|
||||||
OnPropertyChanged(nameof(Length));
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Updating
|
|
||||||
|
|
||||||
private TimeSpan _lastOverridePosition;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates the timeline, applying the provided <paramref name="delta" /> to the <see cref="Position" />
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="delta">The amount of time to apply to the position</param>
|
|
||||||
/// <param name="stickToMainSegment">Whether to stick to the main segment, wrapping around if needed</param>
|
|
||||||
public void Update(TimeSpan delta, bool stickToMainSegment)
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
Delta += delta;
|
|
||||||
Position += delta;
|
|
||||||
|
|
||||||
IsOverridden = false;
|
|
||||||
_lastOverridePosition = Position;
|
|
||||||
|
|
||||||
if (stickToMainSegment && Position > MainSegmentEndPosition)
|
|
||||||
{
|
|
||||||
// If the main segment has no length, simply stick to the start of the segment
|
|
||||||
if (MainSegmentLength == TimeSpan.Zero)
|
|
||||||
Position = MainSegmentStartPosition;
|
|
||||||
// Ensure wrapping back around retains the delta time
|
|
||||||
else
|
|
||||||
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(delta.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
_extraTimelines.RemoveAll(t => t.IsFinished);
|
|
||||||
foreach (Timeline extraTimeline in _extraTimelines)
|
|
||||||
extraTimeline.Update(delta, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Moves the position of the timeline backwards to the very start of the timeline
|
|
||||||
/// </summary>
|
|
||||||
public void JumpToStart()
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
if (Position == TimeSpan.Zero)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Delta = TimeSpan.Zero - Position;
|
|
||||||
Position = TimeSpan.Zero;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Moves the position of the timeline forwards to the beginning of the end segment
|
|
||||||
/// </summary>
|
|
||||||
public void JumpToEndSegment()
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
if (Position >= EndSegmentStartPosition)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Delta = EndSegmentStartPosition - Position;
|
|
||||||
Position = EndSegmentStartPosition;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Moves the position of the timeline forwards to the very end of the timeline
|
|
||||||
/// </summary>
|
|
||||||
public void JumpToEnd()
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
if (Position >= EndSegmentEndPosition)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Delta = EndSegmentEndPosition - Position;
|
|
||||||
Position = EndSegmentEndPosition;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Overrides the <see cref="Position" /> to the specified time and clears any extra time lines
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="position">The position to set the timeline to</param>
|
|
||||||
/// <param name="stickToMainSegment">Whether to stick to the main segment, wrapping around if needed</param>
|
|
||||||
public void Override(TimeSpan position, bool stickToMainSegment)
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
Delta += position - _lastOverridePosition;
|
|
||||||
Position = position;
|
|
||||||
|
|
||||||
IsOverridden = true;
|
|
||||||
_lastOverridePosition = position;
|
|
||||||
|
|
||||||
if (stickToMainSegment && Position >= MainSegmentStartPosition)
|
|
||||||
{
|
|
||||||
bool atSegmentStart = Position == MainSegmentStartPosition;
|
|
||||||
if (MainSegmentLength > TimeSpan.Zero)
|
|
||||||
{
|
|
||||||
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
|
|
||||||
// If the cursor is at the end of the timeline we don't want to wrap back around yet so only allow going to the start if the cursor
|
|
||||||
// is actually at the start of the segment
|
|
||||||
if (Position == MainSegmentStartPosition && !atSegmentStart)
|
|
||||||
Position = MainSegmentEndPosition;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
Position = MainSegmentStartPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
_extraTimelines.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the <see cref="Delta" /> to <see cref="TimeSpan.Zero" />
|
|
||||||
/// </summary>
|
|
||||||
public void ClearDelta()
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
Delta = TimeSpan.Zero;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Storage
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Load()
|
|
||||||
{
|
|
||||||
StartSegmentLength = Entity.StartSegmentLength;
|
|
||||||
MainSegmentLength = Entity.MainSegmentLength;
|
|
||||||
EndSegmentLength = Entity.EndSegmentLength;
|
|
||||||
PlayMode = (TimelinePlayMode) Entity.PlayMode;
|
|
||||||
StopMode = (TimelineStopMode) Entity.StopMode;
|
|
||||||
|
|
||||||
JumpToEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Save()
|
|
||||||
{
|
|
||||||
Entity.StartSegmentLength = StartSegmentLength;
|
|
||||||
Entity.MainSegmentLength = MainSegmentLength;
|
|
||||||
Entity.EndSegmentLength = EndSegmentLength;
|
|
||||||
Entity.PlayMode = (int) PlayMode;
|
|
||||||
Entity.StopMode = (int) StopMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
internal enum TimelineSegment
|
|
||||||
{
|
|
||||||
Start,
|
|
||||||
Main,
|
|
||||||
End
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a mode for render elements to start their timeline when display conditions are met
|
/// Sets the <see cref="Delta" /> to <see cref="TimeSpan.Zero" />
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum TimelinePlayMode
|
public void ClearDelta()
|
||||||
{
|
{
|
||||||
/// <summary>
|
lock (_lock)
|
||||||
/// Continue repeating the main segment of the timeline while the condition is met
|
{
|
||||||
/// </summary>
|
Delta = TimeSpan.Zero;
|
||||||
Repeat,
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Only play the timeline once when the condition is met
|
|
||||||
/// </summary>
|
|
||||||
Once
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Storage
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Load()
|
||||||
|
{
|
||||||
|
StartSegmentLength = Entity.StartSegmentLength;
|
||||||
|
MainSegmentLength = Entity.MainSegmentLength;
|
||||||
|
EndSegmentLength = Entity.EndSegmentLength;
|
||||||
|
PlayMode = (TimelinePlayMode) Entity.PlayMode;
|
||||||
|
StopMode = (TimelineStopMode) Entity.StopMode;
|
||||||
|
|
||||||
|
JumpToEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
Entity.StartSegmentLength = StartSegmentLength;
|
||||||
|
Entity.MainSegmentLength = MainSegmentLength;
|
||||||
|
Entity.EndSegmentLength = EndSegmentLength;
|
||||||
|
Entity.PlayMode = (int) PlayMode;
|
||||||
|
Entity.StopMode = (int) StopMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum TimelineSegment
|
||||||
|
{
|
||||||
|
Start,
|
||||||
|
Main,
|
||||||
|
End
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a mode for render elements to start their timeline when display conditions are met
|
||||||
|
/// </summary>
|
||||||
|
public enum TimelinePlayMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Continue repeating the main segment of the timeline while the condition is met
|
||||||
|
/// </summary>
|
||||||
|
Repeat,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a mode for render elements to stop their timeline when display conditions are no longer met
|
/// Only play the timeline once when the condition is met
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum TimelineStopMode
|
Once
|
||||||
{
|
}
|
||||||
/// <summary>
|
|
||||||
/// When conditions are no longer met, finish the the current run of the main timeline
|
|
||||||
/// </summary>
|
|
||||||
Finish,
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When conditions are no longer met, skip to the end segment of the timeline
|
/// Represents a mode for render elements to stop their timeline when display conditions are no longer met
|
||||||
/// </summary>
|
/// </summary>
|
||||||
SkipToEnd
|
public enum TimelineStopMode
|
||||||
}
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// When conditions are no longer met, finish the the current run of the main timeline
|
||||||
|
/// </summary>
|
||||||
|
Finish,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a mode for render elements to start their timeline when display conditions events are fired
|
/// When conditions are no longer met, skip to the end segment of the timeline
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum TimeLineEventOverlapMode
|
SkipToEnd
|
||||||
{
|
}
|
||||||
/// <summary>
|
|
||||||
/// Stop the current run and restart the timeline
|
|
||||||
/// </summary>
|
|
||||||
Restart,
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ignore subsequent event fires until the timeline finishes
|
/// Represents a mode for render elements to start their timeline when display conditions events are fired
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Ignore,
|
public enum TimeLineEventOverlapMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Stop the current run and restart the timeline
|
||||||
|
/// </summary>
|
||||||
|
Restart,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Play another copy of the timeline on top of the current run
|
/// Ignore subsequent event fires until the timeline finishes
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Copy,
|
Ignore,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Repeat the timeline until the event fires again
|
/// Play another copy of the timeline on top of the current run
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Toggle
|
Copy,
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Repeat the timeline until the event fires again
|
||||||
|
/// </summary>
|
||||||
|
Toggle
|
||||||
}
|
}
|
||||||
@ -337,7 +337,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
|
|||||||
{
|
{
|
||||||
if (end < TimeSpan.FromMilliseconds(100))
|
if (end < TimeSpan.FromMilliseconds(100))
|
||||||
end = TimeSpan.FromMilliseconds(100);
|
end = TimeSpan.FromMilliseconds(100);
|
||||||
|
|
||||||
if (Segment == SegmentViewModelType.Start)
|
if (Segment == SegmentViewModelType.Start)
|
||||||
SelectedProfileElement.Timeline.StartSegmentEndPosition = end;
|
SelectedProfileElement.Timeline.StartSegmentEndPosition = end;
|
||||||
else if (Segment == SegmentViewModelType.Main)
|
else if (Segment == SegmentViewModelType.Main)
|
||||||
|
|||||||
@ -5,14 +5,14 @@ namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a profile editor command that can be used to delete a keyframe.
|
/// Represents a profile editor command that can be used to delete a keyframe.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DeleteKeyframe<T> : IProfileEditorCommand
|
public class DeleteKeyframe : IProfileEditorCommand
|
||||||
{
|
{
|
||||||
private readonly LayerPropertyKeyframe<T> _keyframe;
|
private readonly ILayerPropertyKeyframe _keyframe;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new instance of the <see cref="DeleteKeyframe{T}" /> class.
|
/// Creates a new instance of the <see cref="DeleteKeyframe{T}" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DeleteKeyframe(LayerPropertyKeyframe<T> keyframe)
|
public DeleteKeyframe(ILayerPropertyKeyframe keyframe)
|
||||||
{
|
{
|
||||||
_keyframe = keyframe;
|
_keyframe = keyframe;
|
||||||
}
|
}
|
||||||
@ -25,13 +25,13 @@ public class DeleteKeyframe<T> : IProfileEditorCommand
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Execute()
|
public void Execute()
|
||||||
{
|
{
|
||||||
_keyframe.LayerProperty.RemoveKeyframe(_keyframe);
|
_keyframe.UntypedLayerProperty.RemoveUntypedKeyframe(_keyframe);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Undo()
|
public void Undo()
|
||||||
{
|
{
|
||||||
_keyframe.LayerProperty.AddKeyframe(_keyframe);
|
_keyframe.UntypedLayerProperty.AddUntypedKeyframe(_keyframe);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@ -0,0 +1,108 @@
|
|||||||
|
using System;
|
||||||
|
using Artemis.Core;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a profile editor command that can be used to change the length of a timeline segment.
|
||||||
|
/// </summary>
|
||||||
|
public class ResizeTimelineSegment : IProfileEditorCommand
|
||||||
|
{
|
||||||
|
private readonly TimeSpan _length;
|
||||||
|
private readonly TimeSpan _originalLength;
|
||||||
|
private readonly RenderProfileElement _profileElement;
|
||||||
|
private readonly SegmentType _segmentType;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the <see cref="ResizeTimelineSegment" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="segmentType">The type of segment to resize.</param>
|
||||||
|
/// <param name="profileElement">The render profile element whose segment to resize.</param>
|
||||||
|
/// <param name="length">The new length of the segment</param>
|
||||||
|
public ResizeTimelineSegment(SegmentType segmentType, RenderProfileElement profileElement, TimeSpan length)
|
||||||
|
{
|
||||||
|
_segmentType = segmentType;
|
||||||
|
_profileElement = profileElement;
|
||||||
|
_length = length;
|
||||||
|
_originalLength = _segmentType switch
|
||||||
|
{
|
||||||
|
SegmentType.Start => _profileElement.Timeline.StartSegmentLength,
|
||||||
|
SegmentType.Main => _profileElement.Timeline.MainSegmentLength,
|
||||||
|
SegmentType.End => _profileElement.Timeline.EndSegmentLength,
|
||||||
|
_ => _originalLength
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the <see cref="ResizeTimelineSegment" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="segmentType">The type of segment to resize.</param>
|
||||||
|
/// <param name="profileElement">The render profile element whose segment to resize.</param>
|
||||||
|
/// <param name="length">The new length of the segment</param>
|
||||||
|
/// <param name="originalLength">The original length of the segment</param>
|
||||||
|
public ResizeTimelineSegment(SegmentType segmentType, RenderProfileElement profileElement, TimeSpan length, TimeSpan originalLength)
|
||||||
|
{
|
||||||
|
_segmentType = segmentType;
|
||||||
|
_profileElement = profileElement;
|
||||||
|
_length = length;
|
||||||
|
_originalLength = originalLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string DisplayName => $"Resize {_segmentType.ToString().ToLower()} segment";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Execute()
|
||||||
|
{
|
||||||
|
switch (_segmentType)
|
||||||
|
{
|
||||||
|
case SegmentType.Start:
|
||||||
|
_profileElement.Timeline.StartSegmentLength = _length;
|
||||||
|
break;
|
||||||
|
case SegmentType.Main:
|
||||||
|
_profileElement.Timeline.MainSegmentLength = _length;
|
||||||
|
break;
|
||||||
|
case SegmentType.End:
|
||||||
|
_profileElement.Timeline.EndSegmentLength = _length;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Undo()
|
||||||
|
{
|
||||||
|
switch (_segmentType)
|
||||||
|
{
|
||||||
|
case SegmentType.Start:
|
||||||
|
_profileElement.Timeline.StartSegmentLength = _originalLength;
|
||||||
|
break;
|
||||||
|
case SegmentType.Main:
|
||||||
|
_profileElement.Timeline.MainSegmentLength = _originalLength;
|
||||||
|
break;
|
||||||
|
case SegmentType.End:
|
||||||
|
_profileElement.Timeline.EndSegmentLength = _originalLength;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a type of segment on a timeline.
|
||||||
|
/// </summary>
|
||||||
|
public enum SegmentType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The start segment.
|
||||||
|
/// </summary>
|
||||||
|
Start,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The main segment.
|
||||||
|
/// </summary>
|
||||||
|
Main,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The end segment.
|
||||||
|
/// </summary>
|
||||||
|
End
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@ public class UpdateLayerProperty<T> : IProfileEditorCommand
|
|||||||
private readonly T _newValue;
|
private readonly T _newValue;
|
||||||
private readonly T _originalValue;
|
private readonly T _originalValue;
|
||||||
private readonly TimeSpan? _time;
|
private readonly TimeSpan? _time;
|
||||||
|
private LayerPropertyKeyframe<T>? _newKeyframe;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new instance of the <see cref="UpdateLayerProperty{T}" /> class.
|
/// Creates a new instance of the <see cref="UpdateLayerProperty{T}" /> class.
|
||||||
@ -43,13 +44,16 @@ public class UpdateLayerProperty<T> : IProfileEditorCommand
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Execute()
|
public void Execute()
|
||||||
{
|
{
|
||||||
_layerProperty.SetCurrentValue(_newValue, _time);
|
_newKeyframe = _layerProperty.SetCurrentValue(_newValue, _time);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Undo()
|
public void Undo()
|
||||||
{
|
{
|
||||||
_layerProperty.SetCurrentValue(_originalValue, _time);
|
if (_newKeyframe != null)
|
||||||
|
_layerProperty.RemoveKeyframe(_newKeyframe);
|
||||||
|
else
|
||||||
|
_layerProperty.SetCurrentValue(_originalValue, _time);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cdevice_005Ctabs/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cdevice_005Ctabs/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cplugins_005Cdialogs/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cplugins_005Cdialogs/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Cpanels/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Cpanels/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Cpanels_005Cproperties_005Ctimeline_005Ckeyframes/@EntryIndexedValue">False</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Ctools/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Ctools/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Csettings_005Ctabs/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Csettings_005Ctabs/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Csidebar_005Ccontentdialogs/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Csidebar_005Ccontentdialogs/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ using Artemis.UI.Screens.ProfileEditor;
|
|||||||
using Artemis.UI.Screens.ProfileEditor.ProfileTree;
|
using Artemis.UI.Screens.ProfileEditor.ProfileTree;
|
||||||
using Artemis.UI.Screens.ProfileEditor.Properties;
|
using Artemis.UI.Screens.ProfileEditor.Properties;
|
||||||
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
||||||
|
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
|
||||||
using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
|
using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
|
||||||
using Artemis.UI.Screens.Settings;
|
using Artemis.UI.Screens.Settings;
|
||||||
using Artemis.UI.Screens.Sidebar;
|
using Artemis.UI.Screens.Sidebar;
|
||||||
@ -73,14 +74,9 @@ namespace Artemis.UI.Ninject.Factories
|
|||||||
PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerEffect layerEffect);
|
PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerEffect layerEffect);
|
||||||
|
|
||||||
TreeGroupViewModel TreeGroupViewModel(PropertyGroupViewModel propertyGroupViewModel);
|
TreeGroupViewModel TreeGroupViewModel(PropertyGroupViewModel propertyGroupViewModel);
|
||||||
|
|
||||||
TimelineViewModel TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels);
|
TimelineViewModel TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels);
|
||||||
TimelineGroupViewModel TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel);
|
TimelineGroupViewModel TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel);
|
||||||
|
|
||||||
// TreeViewModel TreeViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
|
|
||||||
// EffectsViewModel EffectsViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel);
|
|
||||||
// TimelineViewModel TimelineViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
|
|
||||||
// TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IPropertyVmFactory
|
public interface IPropertyVmFactory
|
||||||
|
|||||||
@ -6,6 +6,9 @@
|
|||||||
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties"
|
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.PropertiesView">
|
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.PropertiesView">
|
||||||
|
<UserControl.Styles>
|
||||||
|
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
|
||||||
|
</UserControl.Styles>
|
||||||
<Grid ColumnDefinitions="*,Auto,*" Name="ContainerGrid">
|
<Grid ColumnDefinitions="*,Auto,*" Name="ContainerGrid">
|
||||||
<Grid RowDefinitions="48,*">
|
<Grid RowDefinitions="48,*">
|
||||||
<ContentControl Grid.Row="0" Content="{Binding PlaybackViewModel}" />
|
<ContentControl Grid.Row="0" Content="{Binding PlaybackViewModel}" />
|
||||||
@ -31,76 +34,103 @@
|
|||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
Margin="0 0 -5 0" />
|
Margin="0 0 -5 0" />
|
||||||
|
|
||||||
<Grid Grid.Column="2" RowDefinitions="48,*">
|
<!-- Horizontal scrolling -->
|
||||||
<!-- Timeline header body -->
|
<ScrollViewer Grid.Column="2" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||||
<controls:TimelineHeader Grid.Row="0"
|
<Grid RowDefinitions="48,*">
|
||||||
Name="TimelineHeader"
|
<!-- Timeline header body -->
|
||||||
Margin="0 18 0 0"
|
<controls:TimelineHeader Grid.Row="0"
|
||||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
Name="TimelineHeader"
|
||||||
PixelsPerSecond="{Binding PixelsPerSecond}"
|
Margin="0 18 0 0"
|
||||||
HorizontalOffset="{Binding #TimelineScrollViewer.Offset.X, Mode=OneWay}"
|
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||||
VisibleWidth="{Binding #TimelineScrollViewer.Bounds.Width}"
|
HorizontalAlignment="Left"
|
||||||
OffsetFirstValue="True"
|
PixelsPerSecond="{Binding PixelsPerSecond}"
|
||||||
PointerReleased="TimelineHeader_OnPointerReleased"
|
HorizontalOffset="{Binding #TimelineScrollViewer.Offset.X, Mode=OneWay}"
|
||||||
Width="{Binding #TimelineScrollViewer.Viewport.Width}"
|
VisibleWidth="{Binding #TimelineScrollViewer.Bounds.Width}"
|
||||||
Cursor="Hand" />
|
OffsetFirstValue="True"
|
||||||
|
PointerReleased="TimelineHeader_OnPointerReleased"
|
||||||
|
Width="{Binding #TimelineScrollViewer.Viewport.Width}"
|
||||||
|
Cursor="Hand" />
|
||||||
|
|
||||||
<Canvas Grid.Row="0" ZIndex="2">
|
<Canvas Grid.Row="0" ZIndex="2">
|
||||||
<!-- Timeline segments -->
|
<!-- Segment dividers -->
|
||||||
<ContentControl Canvas.Left="{Binding EndTimelineSegmentViewModel.SegmentStartPosition}" Content="{Binding EndTimelineSegmentViewModel}" />
|
<Line Name="TimelineLine"
|
||||||
<ContentControl Canvas.Left="{Binding MainTimelineSegmentViewModel.SegmentStartPosition}" Content="{Binding MainTimelineSegmentViewModel}" />
|
Canvas.Left="{Binding TimelineViewModel.CaretPosition}"
|
||||||
<ContentControl Canvas.Left="{Binding StartTimelineSegmentViewModel.SegmentStartPosition}" Content="{Binding StartTimelineSegmentViewModel}" />
|
Cursor="SizeWestEast"
|
||||||
|
PointerPressed="TimelineCaret_OnPointerPressed"
|
||||||
|
PointerReleased="TimelineCaret_OnPointerReleased"
|
||||||
|
PointerMoved="TimelineCaret_OnPointerMoved"
|
||||||
|
StartPoint="0,0"
|
||||||
|
EndPoint="{Binding #ContainerGrid.Bounds.BottomLeft}"
|
||||||
|
StrokeThickness="2"
|
||||||
|
Stroke="{DynamicResource SystemAccentColorLight1}">
|
||||||
|
</Line>
|
||||||
|
|
||||||
<!-- Timeline caret -->
|
<Line Name="StartSegmentLine"
|
||||||
<Polygon Name="TimelineCaret"
|
Canvas.Left="{Binding TimelineViewModel.StartSegmentViewModel.EndX}"
|
||||||
Canvas.Left="{Binding TimelineViewModel.CaretPosition}"
|
IsVisible="{Binding !TimelineViewModel.MainSegmentViewModel.ShowAddStart}"
|
||||||
Cursor="SizeWestEast"
|
StartPoint="0,0"
|
||||||
PointerPressed="TimelineCaret_OnPointerPressed"
|
EndPoint="{Binding #ContainerGrid.Bounds.BottomLeft}"
|
||||||
PointerReleased="TimelineCaret_OnPointerReleased"
|
StrokeThickness="2"
|
||||||
PointerMoved="TimelineCaret_OnPointerMoved"
|
Stroke="{DynamicResource SystemAccentColorLight1}"
|
||||||
Points="-8,0 -8,8 0,20, 8,8 8,0"
|
StrokeDashArray="6,2"
|
||||||
Fill="{DynamicResource SystemAccentColorLight1}">
|
Opacity="0.5">
|
||||||
<!-- <Polygon.Transitions> -->
|
</Line>
|
||||||
<!-- <Transitions> -->
|
<Line Name="MainSegmentLine"
|
||||||
<!-- <DoubleTransition Property="Canvas.Left" Duration="0.05"></DoubleTransition> -->
|
Canvas.Left="{Binding TimelineViewModel.MainSegmentViewModel.EndX}"
|
||||||
<!-- </Transitions> -->
|
IsVisible="{Binding !TimelineViewModel.MainSegmentViewModel.ShowAddMain}"
|
||||||
<!-- </Polygon.Transitions> -->
|
StartPoint="0,0"
|
||||||
</Polygon>
|
EndPoint="{Binding #ContainerGrid.Bounds.BottomLeft}"
|
||||||
<Line Name="TimelineLine"
|
StrokeThickness="2"
|
||||||
Canvas.Left="{Binding TimelineViewModel.CaretPosition}"
|
Stroke="{DynamicResource SystemAccentColorLight1}"
|
||||||
Cursor="SizeWestEast"
|
StrokeDashArray="6,2"
|
||||||
PointerPressed="TimelineCaret_OnPointerPressed"
|
Opacity="0.5">
|
||||||
PointerReleased="TimelineCaret_OnPointerReleased"
|
</Line>
|
||||||
PointerMoved="TimelineCaret_OnPointerMoved"
|
<Line Name="EndSegmentLine"
|
||||||
StartPoint="0,0"
|
Canvas.Left="{Binding TimelineViewModel.EndSegmentViewModel.EndX}"
|
||||||
EndPoint="0,1"
|
IsVisible="{Binding !TimelineViewModel.MainSegmentViewModel.ShowAddEnd}"
|
||||||
StrokeThickness="2"
|
StartPoint="0,0"
|
||||||
Stroke="{DynamicResource SystemAccentColorLight1}"
|
EndPoint="{Binding #ContainerGrid.Bounds.BottomLeft}"
|
||||||
RenderTransformOrigin="0,0">
|
StrokeThickness="2"
|
||||||
<!-- <Line.Transitions> -->
|
Stroke="{DynamicResource SystemAccentColorLight1}"
|
||||||
<!-- <Transitions> -->
|
StrokeDashArray="6,2"
|
||||||
<!-- <DoubleTransition Property="Canvas.Left" Duration="0.05"></DoubleTransition> -->
|
Opacity="0.5">
|
||||||
<!-- </Transitions> -->
|
</Line>
|
||||||
<!-- </Line.Transitions> -->
|
|
||||||
<Line.RenderTransform>
|
|
||||||
<ScaleTransform ScaleX="1" ScaleY="{Binding #ContainerGrid.Bounds.Height}" />
|
|
||||||
</Line.RenderTransform>
|
|
||||||
</Line>
|
|
||||||
</Canvas>
|
|
||||||
|
|
||||||
<!-- Horizontal scrolling -->
|
<!-- Timeline segments -->
|
||||||
<ScrollViewer Grid.Row="1"
|
<ContentControl Canvas.Left="{Binding TimelineViewModel.EndSegmentViewModel.StartX}"
|
||||||
Name="TimelineScrollViewer"
|
Classes="segment-content-control"
|
||||||
Offset="{Binding #TreeScrollViewer.Offset, Mode=OneWay}"
|
Content="{Binding TimelineViewModel.EndSegmentViewModel}" />
|
||||||
VerticalScrollBarVisibility="Hidden"
|
<ContentControl Canvas.Left="{Binding TimelineViewModel.MainSegmentViewModel.StartX}"
|
||||||
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}">
|
Classes="segment-content-control"
|
||||||
<ContentControl Content="{Binding TimelineViewModel}" />
|
Content="{Binding TimelineViewModel.MainSegmentViewModel}" />
|
||||||
</ScrollViewer>
|
<ContentControl Canvas.Left="{Binding TimelineViewModel.StartSegmentViewModel.StartX}"
|
||||||
|
Classes="segment-content-control"
|
||||||
|
Content="{Binding TimelineViewModel.StartSegmentViewModel}" />
|
||||||
|
|
||||||
<!-- TODO: Databindings here -->
|
<!-- Timeline caret -->
|
||||||
|
<Polygon Name="TimelineCaret"
|
||||||
|
Canvas.Left="{Binding TimelineViewModel.CaretPosition}"
|
||||||
|
Cursor="SizeWestEast"
|
||||||
|
PointerPressed="TimelineCaret_OnPointerPressed"
|
||||||
|
PointerReleased="TimelineCaret_OnPointerReleased"
|
||||||
|
PointerMoved="TimelineCaret_OnPointerMoved"
|
||||||
|
Points="-8,0 -8,8 0,20, 8,8 8,0"
|
||||||
|
Fill="{DynamicResource SystemAccentColorLight1}">
|
||||||
|
</Polygon>
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
</Grid>
|
<ScrollViewer Grid.Row="1"
|
||||||
|
Name="TimelineScrollViewer"
|
||||||
|
Offset="{Binding #TreeScrollViewer.Offset, Mode=OneWay}"
|
||||||
|
VerticalScrollBarVisibility="Hidden">
|
||||||
|
<ContentControl Content="{Binding TimelineViewModel}"
|
||||||
|
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}" />
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!-- TODO: Databindings here -->
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ using Artemis.Core.LayerBrushes;
|
|||||||
using Artemis.Core.LayerEffects;
|
using Artemis.Core.LayerEffects;
|
||||||
using Artemis.UI.Ninject.Factories;
|
using Artemis.UI.Ninject.Factories;
|
||||||
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
||||||
|
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||||
using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
|
using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Services.PropertyInput;
|
using Artemis.UI.Shared.Services.PropertyInput;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||||
|
|
||||||
public interface ITimelineKeyframeViewModel
|
public interface ITimelineKeyframeViewModel
|
||||||
{
|
{
|
||||||
@ -3,7 +3,7 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineEasingView">
|
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes.TimelineEasingView">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<Polyline Stroke="{DynamicResource TextFillColorPrimaryBrush}"
|
<Polyline Stroke="{DynamicResource TextFillColorPrimaryBrush}"
|
||||||
StrokeThickness="1"
|
StrokeThickness="1"
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline
|
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes
|
||||||
{
|
{
|
||||||
public partial class TimelineEasingView : UserControl
|
public partial class TimelineEasingView : UserControl
|
||||||
{
|
{
|
||||||
@ -4,7 +4,7 @@ using Artemis.UI.Shared;
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Humanizer;
|
using Humanizer;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||||
|
|
||||||
public class TimelineEasingViewModel : ViewModelBase
|
public class TimelineEasingViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
@ -5,7 +5,7 @@
|
|||||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline"
|
xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineKeyframeView"
|
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes.TimelineKeyframeView"
|
||||||
ClipToBounds="False"
|
ClipToBounds="False"
|
||||||
Height="{DynamicResource RailsHeight}">
|
Height="{DynamicResource RailsHeight}">
|
||||||
<Ellipse Fill="{DynamicResource SystemAccentColorLight2}"
|
<Ellipse Fill="{DynamicResource SystemAccentColorLight2}"
|
||||||
@ -4,7 +4,7 @@ using Avalonia.LogicalTree;
|
|||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||||
|
|
||||||
public class TimelineKeyframeView : ReactiveUserControl<ITimelineKeyframeViewModel>
|
public class TimelineKeyframeView : ReactiveUserControl<ITimelineKeyframeViewModel>
|
||||||
{
|
{
|
||||||
@ -7,11 +7,10 @@ using Artemis.UI.Shared;
|
|||||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||||
using Avalonia.Controls.Mixins;
|
using Avalonia.Controls.Mixins;
|
||||||
using Avalonia.Input;
|
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||||
|
|
||||||
public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineKeyframeViewModel
|
public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineKeyframeViewModel
|
||||||
{
|
{
|
||||||
@ -97,7 +96,7 @@ public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineK
|
|||||||
|
|
||||||
public void Delete()
|
public void Delete()
|
||||||
{
|
{
|
||||||
_profileEditorService.ExecuteCommand(new DeleteKeyframe<T>(LayerPropertyKeyframe));
|
_profileEditorService.ExecuteCommand(new DeleteKeyframe(LayerPropertyKeyframe));
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="18"
|
||||||
|
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.EndSegmentView">
|
||||||
|
<UserControl.Styles>
|
||||||
|
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
|
||||||
|
</UserControl.Styles>
|
||||||
|
<Border Classes="segment-container">
|
||||||
|
<Grid Name="SegmentGrid"
|
||||||
|
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}"
|
||||||
|
Width="{Binding Width}"
|
||||||
|
ColumnDefinitions="Auto, Auto,*,Auto">
|
||||||
|
|
||||||
|
<Rectangle Name="KeyframeDragVisualLeft"
|
||||||
|
Grid.Column="0"
|
||||||
|
Classes="resize-visual" />
|
||||||
|
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Name="AddMainSegment"
|
||||||
|
Classes="AppBarButton icon-button icon-button-small"
|
||||||
|
ToolTip.Tip="Add main segment"
|
||||||
|
Command="{Binding AddMainSegment}"
|
||||||
|
IsVisible="{Binding ShowAddMain}">
|
||||||
|
<avalonia:MaterialIcon Kind="PlusCircle" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" ClipToBounds="True">
|
||||||
|
<TextBlock Name="SegmentTitle"
|
||||||
|
FontSize="13"
|
||||||
|
ToolTip.Tip="This segment is played once a condition is no longer met">
|
||||||
|
End
|
||||||
|
</TextBlock>
|
||||||
|
<Button Name="SegmentClose"
|
||||||
|
Classes="AppBarButton icon-button icon-button-small"
|
||||||
|
ToolTip.Tip="Remove this segment"
|
||||||
|
Command="{Binding RemoveSegment}">
|
||||||
|
<avalonia:MaterialIcon Kind="CloseCircle" />
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Rectangle Name="KeyframeDragVisualRight"
|
||||||
|
Grid.Column="3"
|
||||||
|
Classes="resize-visual" />
|
||||||
|
<Rectangle Name="KeyframeDragAnchor"
|
||||||
|
Grid.Column="3"
|
||||||
|
Classes="resize-anchor"
|
||||||
|
PointerPressed="KeyframeDragAnchor_OnPointerPressed"
|
||||||
|
PointerMoved="KeyframeDragAnchor_OnPointerMoved"
|
||||||
|
PointerReleased="KeyframeDragAnchor_OnPointerReleased"
|
||||||
|
ToolTip.Tip="{Binding EndTimestamp}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Shapes;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments
|
||||||
|
{
|
||||||
|
public partial class EndSegmentView : ReactiveUserControl<EndSegmentViewModel>
|
||||||
|
{
|
||||||
|
private readonly Rectangle _keyframeDragAnchor;
|
||||||
|
private double _dragOffset;
|
||||||
|
|
||||||
|
public EndSegmentView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_keyframeDragAnchor = this.Get<Rectangle>("KeyframeDragAnchor");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void KeyframeDragAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
if (ViewModel == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||||
|
return;
|
||||||
|
e.Pointer.Capture(_keyframeDragAnchor);
|
||||||
|
|
||||||
|
_dragOffset = ViewModel.Width - e.GetCurrentPoint(this).Position.X;
|
||||||
|
ViewModel.StartResize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void KeyframeDragAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
|
||||||
|
{
|
||||||
|
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
|
||||||
|
return;
|
||||||
|
ViewModel.UpdateResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void KeyframeDragAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||||
|
{
|
||||||
|
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
|
||||||
|
return;
|
||||||
|
e.Pointer.Capture(null);
|
||||||
|
ViewModel.FinishResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using Artemis.Core;
|
||||||
|
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
|
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||||
|
using Avalonia.Controls.Mixins;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
|
||||||
|
|
||||||
|
public class EndSegmentViewModel : TimelineSegmentViewModel
|
||||||
|
{
|
||||||
|
private readonly IProfileEditorService _profileEditorService;
|
||||||
|
private RenderProfileElement? _profileElement;
|
||||||
|
private int _pixelsPerSecond;
|
||||||
|
private TimeSpan _time;
|
||||||
|
private ObservableAsPropertyHelper<double>? _start;
|
||||||
|
private ObservableAsPropertyHelper<double>? _end;
|
||||||
|
private ObservableAsPropertyHelper<string?>? _endTimestamp;
|
||||||
|
private readonly ObservableAsPropertyHelper<double> _width;
|
||||||
|
private TimeSpan _initialLength;
|
||||||
|
|
||||||
|
|
||||||
|
public EndSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
|
||||||
|
{
|
||||||
|
_profileEditorService = profileEditorService;
|
||||||
|
this.WhenActivated(d =>
|
||||||
|
{
|
||||||
|
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
|
||||||
|
profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
|
||||||
|
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d);
|
||||||
|
|
||||||
|
_start = profileEditorService.ProfileElement
|
||||||
|
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentStartPosition) ?? Observable.Never<TimeSpan>())
|
||||||
|
.Switch()
|
||||||
|
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
|
||||||
|
.ToProperty(this, vm => vm.StartX)
|
||||||
|
.DisposeWith(d);
|
||||||
|
_end = profileEditorService.ProfileElement
|
||||||
|
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentEndPosition) ?? Observable.Never<TimeSpan>())
|
||||||
|
.Switch()
|
||||||
|
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
|
||||||
|
.ToProperty(this, vm => vm.EndX)
|
||||||
|
.DisposeWith(d);
|
||||||
|
_endTimestamp = profileEditorService.ProfileElement
|
||||||
|
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentEndPosition) ?? Observable.Never<TimeSpan>())
|
||||||
|
.Switch()
|
||||||
|
.Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}")
|
||||||
|
.ToProperty(this, vm => vm.EndTimestamp)
|
||||||
|
.DisposeWith(d);
|
||||||
|
});
|
||||||
|
|
||||||
|
_width = this.WhenAnyValue(vm => vm.StartX, vm => vm.EndX).Select(t => t.Item2 - t.Item1).ToProperty(this, vm => vm.Width);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override TimeSpan Start => _profileElement?.Timeline.EndSegmentStartPosition ?? TimeSpan.Zero;
|
||||||
|
public override double StartX => _start?.Value ?? 0;
|
||||||
|
public override TimeSpan End => _profileElement?.Timeline.EndSegmentEndPosition ?? TimeSpan.Zero;
|
||||||
|
public override double EndX => _end?.Value ?? 0;
|
||||||
|
public override TimeSpan Length
|
||||||
|
{
|
||||||
|
get => _profileElement?.Timeline.EndSegmentLength ?? TimeSpan.Zero;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_profileElement != null)
|
||||||
|
_profileElement.Timeline.EndSegmentLength = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override double Width => _width.Value;
|
||||||
|
|
||||||
|
public override string? EndTimestamp => _endTimestamp?.Value;
|
||||||
|
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.End;
|
||||||
|
}
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="18"
|
||||||
|
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.MainSegmentView">
|
||||||
|
<UserControl.Styles>
|
||||||
|
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
|
||||||
|
</UserControl.Styles>
|
||||||
|
<Border Classes="segment-container">
|
||||||
|
<Grid Name="SegmentGrid"
|
||||||
|
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}"
|
||||||
|
Width="{Binding Width}"
|
||||||
|
ColumnDefinitions="Auto,Auto,*,Auto,Auto">
|
||||||
|
|
||||||
|
<Rectangle Name="KeyframeDragVisualLeft"
|
||||||
|
Grid.Column="0"
|
||||||
|
IsVisible="{Binding !ShowAddStart}"
|
||||||
|
Classes="resize-visual" />
|
||||||
|
|
||||||
|
<Button Name="AddStartSegment"
|
||||||
|
Grid.Column="1"
|
||||||
|
Classes="AppBarButton icon-button icon-button-small"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
ToolTip.Tip="Add a start segment"
|
||||||
|
Command="{Binding AddStartSegment}"
|
||||||
|
IsVisible="{Binding ShowAddStart}">
|
||||||
|
<avalonia:MaterialIcon Kind="PlusCircle" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||||
|
<TextBlock Name="SegmentTitle"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
FontSize="13"
|
||||||
|
ToolTip.Tip="This segment is played while a condition is met, either once or on a repeating loop">
|
||||||
|
Main
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<Button Name="SegmentClose"
|
||||||
|
Classes="AppBarButton icon-button icon-button-small"
|
||||||
|
ToolTip.Tip="Remove this segment"
|
||||||
|
Command="{Binding DisableSegment}">
|
||||||
|
<avalonia:MaterialIcon Kind="CloseCircle" />
|
||||||
|
</Button>
|
||||||
|
<ToggleButton Name="SegmentRepeat"
|
||||||
|
Classes="icon-button icon-button-small"
|
||||||
|
ToolTip.Tip="Repeat this segment"
|
||||||
|
IsChecked="{Binding RepeatSegment}"
|
||||||
|
Padding="0">
|
||||||
|
<avalonia:MaterialIcon Kind="Repeat" />
|
||||||
|
</ToggleButton>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Button Name="AddEndSegment"
|
||||||
|
Grid.Column="3"
|
||||||
|
Classes="AppBarButton icon-button icon-button-small"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
ToolTip.Tip="Add an end segment"
|
||||||
|
Command="{Binding AddEndSegment}"
|
||||||
|
IsVisible="{Binding ShowAddEnd}">
|
||||||
|
<avalonia:MaterialIcon Kind="PlusCircle" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Rectangle Name="KeyframeDragVisual"
|
||||||
|
Grid.Column="4"
|
||||||
|
Classes="resize-visual" />
|
||||||
|
<Rectangle Name="KeyframeDragAnchor"
|
||||||
|
Grid.Column="4"
|
||||||
|
Classes="resize-anchor"
|
||||||
|
PointerPressed="KeyframeDragAnchor_OnPointerPressed"
|
||||||
|
PointerMoved="KeyframeDragAnchor_OnPointerMoved"
|
||||||
|
PointerReleased="KeyframeDragAnchor_OnPointerReleased"
|
||||||
|
ToolTip.Tip="{Binding EndTimestamp}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Shapes;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments
|
||||||
|
{
|
||||||
|
public partial class MainSegmentView : ReactiveUserControl<MainSegmentViewModel>
|
||||||
|
{
|
||||||
|
private readonly Rectangle _keyframeDragAnchor;
|
||||||
|
private double _dragOffset;
|
||||||
|
|
||||||
|
public MainSegmentView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_keyframeDragAnchor = this.Get<Rectangle>("KeyframeDragAnchor");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void KeyframeDragAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
if (ViewModel == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||||
|
return;
|
||||||
|
e.Pointer.Capture(_keyframeDragAnchor);
|
||||||
|
|
||||||
|
_dragOffset = ViewModel.Width - e.GetCurrentPoint(this).Position.X;
|
||||||
|
ViewModel.StartResize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void KeyframeDragAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
|
||||||
|
{
|
||||||
|
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
|
||||||
|
return;
|
||||||
|
ViewModel.UpdateResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void KeyframeDragAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||||
|
{
|
||||||
|
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
|
||||||
|
return;
|
||||||
|
e.Pointer.Capture(null);
|
||||||
|
ViewModel.FinishResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void KeyframeDragStartAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private void KeyframeDragStartAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private void KeyframeDragStartAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using Artemis.Core;
|
||||||
|
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
|
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||||
|
using Avalonia.Controls.Mixins;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
|
||||||
|
|
||||||
|
public class MainSegmentViewModel : TimelineSegmentViewModel
|
||||||
|
{
|
||||||
|
private readonly IProfileEditorService _profileEditorService;
|
||||||
|
private RenderProfileElement? _profileElement;
|
||||||
|
private int _pixelsPerSecond;
|
||||||
|
private TimeSpan _time;
|
||||||
|
private ObservableAsPropertyHelper<double>? _start;
|
||||||
|
private ObservableAsPropertyHelper<double>? _end;
|
||||||
|
private ObservableAsPropertyHelper<string?>? _endTimestamp;
|
||||||
|
private readonly ObservableAsPropertyHelper<double> _width;
|
||||||
|
private TimeSpan _initialLength;
|
||||||
|
|
||||||
|
public MainSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
|
||||||
|
{
|
||||||
|
_profileEditorService = profileEditorService;
|
||||||
|
this.WhenActivated(d =>
|
||||||
|
{
|
||||||
|
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
|
||||||
|
profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
|
||||||
|
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d);
|
||||||
|
|
||||||
|
_start = profileEditorService.ProfileElement
|
||||||
|
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentStartPosition) ?? Observable.Never<TimeSpan>())
|
||||||
|
.Switch()
|
||||||
|
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
|
||||||
|
.ToProperty(this, vm => vm.StartX)
|
||||||
|
.DisposeWith(d);
|
||||||
|
_end = profileEditorService.ProfileElement
|
||||||
|
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentEndPosition) ?? Observable.Never<TimeSpan>())
|
||||||
|
.Switch()
|
||||||
|
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
|
||||||
|
.ToProperty(this, vm => vm.EndX)
|
||||||
|
.DisposeWith(d);
|
||||||
|
_endTimestamp = profileEditorService.ProfileElement
|
||||||
|
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentEndPosition) ?? Observable.Never<TimeSpan>())
|
||||||
|
.Switch()
|
||||||
|
.Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}")
|
||||||
|
.ToProperty(this, vm => vm.EndTimestamp)
|
||||||
|
.DisposeWith(d);
|
||||||
|
});
|
||||||
|
|
||||||
|
_width = this.WhenAnyValue(vm => vm.StartX, vm => vm.EndX).Select(t => t.Item2 - t.Item1).ToProperty(this, vm => vm.Width);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override TimeSpan Start => _profileElement?.Timeline.MainSegmentStartPosition ?? TimeSpan.Zero;
|
||||||
|
public override double StartX => _start?.Value ?? 0;
|
||||||
|
public override TimeSpan End => _profileElement?.Timeline.MainSegmentEndPosition ?? TimeSpan.Zero;
|
||||||
|
public override double EndX => _end?.Value ?? 0;
|
||||||
|
public override TimeSpan Length
|
||||||
|
{
|
||||||
|
get => _profileElement?.Timeline.MainSegmentLength ?? TimeSpan.Zero;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_profileElement != null)
|
||||||
|
_profileElement.Timeline.MainSegmentLength = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override double Width => _width.Value;
|
||||||
|
|
||||||
|
public override string? EndTimestamp => _endTimestamp?.Value;
|
||||||
|
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.Main;
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
<Styles xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<Design.PreviewWith>
|
||||||
|
<Border Padding="20">
|
||||||
|
<!-- Add Controls for Previewer Here -->
|
||||||
|
</Border>
|
||||||
|
</Design.PreviewWith>
|
||||||
|
|
||||||
|
<Style Selector="ContentControl.segment-content-control">
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Border.segment-container">
|
||||||
|
<Setter Property="Height" Value="18"></Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Rectangle.resize-visual">
|
||||||
|
<Setter Property="Fill" Value="{DynamicResource SystemAccentColorLight2}"/>
|
||||||
|
<Setter Property="Width" Value="4"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Right"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Rectangle.resize-anchor">
|
||||||
|
<Setter Property="Fill" Value="Transparent"/>
|
||||||
|
<Setter Property="Width" Value="10"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Right"/>
|
||||||
|
<Setter Property="Cursor" Value="SizeWestEast"/>
|
||||||
|
</Style>
|
||||||
|
</Styles>
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:system="clr-namespace:System;assembly=System.Runtime"
|
||||||
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="18"
|
||||||
|
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.StartSegmentView">
|
||||||
|
<UserControl.Styles>
|
||||||
|
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
|
||||||
|
</UserControl.Styles>
|
||||||
|
<Border Classes="segment-container">
|
||||||
|
<Grid Name="SegmentGrid"
|
||||||
|
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}"
|
||||||
|
Width="{Binding Width}"
|
||||||
|
ColumnDefinitions="*,Auto,Auto">
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||||
|
<TextBlock Name="SegmentTitle"
|
||||||
|
FontSize="13"
|
||||||
|
ToolTip.Tip="This segment is played when a layer starts displaying because it's conditions are met">
|
||||||
|
Start
|
||||||
|
</TextBlock>
|
||||||
|
<Button Name="SegmentClose"
|
||||||
|
Classes="AppBarButton icon-button icon-button-small"
|
||||||
|
ToolTip.Tip="Remove this segment"
|
||||||
|
Command="{Binding RemoveSegment}">
|
||||||
|
<avalonia:MaterialIcon Kind="CloseCircle" />
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Name="AddMainSegment"
|
||||||
|
Classes="AppBarButton icon-button icon-button-small"
|
||||||
|
ToolTip.Tip="Add main segment"
|
||||||
|
Command="{Binding AddMainSegment}"
|
||||||
|
IsVisible="{Binding ShowAddMain}">
|
||||||
|
<avalonia:MaterialIcon Kind="PlusCircle" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Rectangle Name="KeyframeDragVisual"
|
||||||
|
Grid.Column="2"
|
||||||
|
Classes="resize-visual" />
|
||||||
|
<Rectangle Name="KeyframeDragAnchor"
|
||||||
|
Grid.Column="2"
|
||||||
|
Classes="resize-anchor"
|
||||||
|
PointerPressed="KeyframeDragAnchor_OnPointerPressed"
|
||||||
|
PointerMoved="KeyframeDragAnchor_OnPointerMoved"
|
||||||
|
PointerReleased="KeyframeDragAnchor_OnPointerReleased"
|
||||||
|
ToolTip.Tip="{Binding EndTimestamp}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Shapes;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments
|
||||||
|
{
|
||||||
|
public partial class StartSegmentView : ReactiveUserControl<StartSegmentViewModel>
|
||||||
|
{
|
||||||
|
private readonly Rectangle _keyframeDragAnchor;
|
||||||
|
private double _dragOffset;
|
||||||
|
|
||||||
|
public StartSegmentView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_keyframeDragAnchor = this.Get<Rectangle>("KeyframeDragAnchor");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void KeyframeDragAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
if (ViewModel == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||||
|
return;
|
||||||
|
e.Pointer.Capture(_keyframeDragAnchor);
|
||||||
|
|
||||||
|
_dragOffset = ViewModel.Width - e.GetCurrentPoint(this).Position.X;
|
||||||
|
ViewModel.StartResize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void KeyframeDragAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
|
||||||
|
{
|
||||||
|
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
|
||||||
|
return;
|
||||||
|
ViewModel.UpdateResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void KeyframeDragAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||||
|
{
|
||||||
|
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
|
||||||
|
return;
|
||||||
|
e.Pointer.Capture(null);
|
||||||
|
ViewModel.FinishResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using Artemis.Core;
|
||||||
|
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
|
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||||
|
using Avalonia.Controls.Mixins;
|
||||||
|
using Castle.DynamicProxy.Generators;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
|
||||||
|
|
||||||
|
public class StartSegmentViewModel : TimelineSegmentViewModel
|
||||||
|
{
|
||||||
|
private readonly IProfileEditorService _profileEditorService;
|
||||||
|
private RenderProfileElement? _profileElement;
|
||||||
|
private int _pixelsPerSecond;
|
||||||
|
private TimeSpan _time;
|
||||||
|
private ObservableAsPropertyHelper<double>? _end;
|
||||||
|
private ObservableAsPropertyHelper<string?>? _endTimestamp;
|
||||||
|
private readonly ObservableAsPropertyHelper<double> _width;
|
||||||
|
private TimeSpan _initialLength;
|
||||||
|
|
||||||
|
public StartSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
|
||||||
|
{
|
||||||
|
_profileEditorService = profileEditorService;
|
||||||
|
this.WhenActivated(d =>
|
||||||
|
{
|
||||||
|
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
|
||||||
|
profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
|
||||||
|
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d);
|
||||||
|
|
||||||
|
_end = profileEditorService.ProfileElement
|
||||||
|
.Select(p => p?.WhenAnyValue(element => element.Timeline.StartSegmentEndPosition) ?? Observable.Never<TimeSpan>())
|
||||||
|
.Switch()
|
||||||
|
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
|
||||||
|
.ToProperty(this, vm => vm.EndX)
|
||||||
|
.DisposeWith(d);
|
||||||
|
_endTimestamp = profileEditorService.ProfileElement
|
||||||
|
.Select(p => p?.WhenAnyValue(element => element.Timeline.StartSegmentEndPosition) ?? Observable.Never<TimeSpan>())
|
||||||
|
.Switch()
|
||||||
|
.Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}")
|
||||||
|
.ToProperty(this, vm => vm.EndTimestamp)
|
||||||
|
.DisposeWith(d);
|
||||||
|
});
|
||||||
|
|
||||||
|
_width = this.WhenAnyValue(vm => vm.StartX, vm => vm.EndX).Select(t => t.Item2 - t.Item1).ToProperty(this, vm => vm.Width);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override TimeSpan Start => TimeSpan.Zero;
|
||||||
|
public override double StartX => 0;
|
||||||
|
public override TimeSpan End => _profileElement?.Timeline.StartSegmentEndPosition ?? TimeSpan.Zero;
|
||||||
|
public override double EndX => _end?.Value ?? 0;
|
||||||
|
public override TimeSpan Length
|
||||||
|
{
|
||||||
|
get => _profileElement?.Timeline.StartSegmentLength ?? TimeSpan.Zero;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_profileElement != null)
|
||||||
|
_profileElement.Timeline.StartSegmentLength = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override double Width => _width.Value;
|
||||||
|
|
||||||
|
public override string? EndTimestamp => _endTimestamp?.Value;
|
||||||
|
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.Start;
|
||||||
|
}
|
||||||
@ -0,0 +1,174 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using Artemis.Core;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
|
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||||
|
using Avalonia.Controls.Mixins;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
|
||||||
|
|
||||||
|
public abstract class TimelineSegmentViewModel : ActivatableViewModelBase
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan NewSegmentLength = TimeSpan.FromSeconds(2);
|
||||||
|
private readonly IProfileEditorService _profileEditorService;
|
||||||
|
private RenderProfileElement? _profileElement;
|
||||||
|
private int _pixelsPerSecond;
|
||||||
|
private Dictionary<ILayerPropertyKeyframe, TimeSpan> _originalKeyframePositions = new();
|
||||||
|
|
||||||
|
private ObservableAsPropertyHelper<bool>? _showAddStart;
|
||||||
|
private ObservableAsPropertyHelper<bool>? _showAddMain;
|
||||||
|
private ObservableAsPropertyHelper<bool>? _showAddEnd;
|
||||||
|
private TimeSpan _initialLength;
|
||||||
|
|
||||||
|
protected TimelineSegmentViewModel(IProfileEditorService profileEditorService)
|
||||||
|
{
|
||||||
|
_profileEditorService = profileEditorService;
|
||||||
|
this.WhenActivated(d =>
|
||||||
|
{
|
||||||
|
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
|
||||||
|
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d);
|
||||||
|
|
||||||
|
_showAddStart = profileEditorService.ProfileElement
|
||||||
|
.Select(p => p?.WhenAnyValue(element => element.Timeline.StartSegmentLength) ?? Observable.Never<TimeSpan>())
|
||||||
|
.Switch()
|
||||||
|
.Select(t => t == TimeSpan.Zero)
|
||||||
|
.ToProperty(this, vm => vm.ShowAddStart)
|
||||||
|
.DisposeWith(d);
|
||||||
|
_showAddMain = profileEditorService.ProfileElement
|
||||||
|
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentLength) ?? Observable.Never<TimeSpan>())
|
||||||
|
.Switch()
|
||||||
|
.Select(t => t == TimeSpan.Zero)
|
||||||
|
.ToProperty(this, vm => vm.ShowAddMain)
|
||||||
|
.DisposeWith(d);
|
||||||
|
_showAddEnd = profileEditorService.ProfileElement
|
||||||
|
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentLength) ?? Observable.Never<TimeSpan>())
|
||||||
|
.Switch()
|
||||||
|
.Select(t => t == TimeSpan.Zero)
|
||||||
|
.ToProperty(this, vm => vm.ShowAddEnd)
|
||||||
|
.DisposeWith(d);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShowAddStart => _showAddStart?.Value ?? false;
|
||||||
|
public bool ShowAddMain => _showAddMain?.Value ?? false;
|
||||||
|
public bool ShowAddEnd => _showAddEnd?.Value ?? false;
|
||||||
|
|
||||||
|
public abstract TimeSpan Start { get; }
|
||||||
|
public abstract double StartX { get; }
|
||||||
|
public abstract TimeSpan End { get; }
|
||||||
|
public abstract double EndX { get; }
|
||||||
|
public abstract TimeSpan Length { get; set; }
|
||||||
|
public abstract double Width { get; }
|
||||||
|
public abstract string? EndTimestamp { get; }
|
||||||
|
public abstract ResizeTimelineSegment.SegmentType Type { get; }
|
||||||
|
|
||||||
|
public void AddStartSegment()
|
||||||
|
{
|
||||||
|
if (_profileElement == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Add start segment");
|
||||||
|
ShiftKeyframes(_profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes), NewSegmentLength);
|
||||||
|
ApplyPendingKeyframeMovement();
|
||||||
|
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(ResizeTimelineSegment.SegmentType.Start, _profileElement, NewSegmentLength));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddMainSegment()
|
||||||
|
{
|
||||||
|
if (_profileElement == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Add main segment");
|
||||||
|
ShiftKeyframes(_profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes).Where(s => s.Position > _profileElement.Timeline.StartSegmentEndPosition), NewSegmentLength);
|
||||||
|
ApplyPendingKeyframeMovement();
|
||||||
|
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(ResizeTimelineSegment.SegmentType.Main, _profileElement, NewSegmentLength));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddEndSegment()
|
||||||
|
{
|
||||||
|
if (_profileElement == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Add end segment");
|
||||||
|
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(ResizeTimelineSegment.SegmentType.End, _profileElement, NewSegmentLength));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartResize()
|
||||||
|
{
|
||||||
|
if (_profileElement == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_initialLength = Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateResize(double x)
|
||||||
|
{
|
||||||
|
if (_profileElement == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TimeSpan difference = GetTimeFromX(x) - Length;
|
||||||
|
List<ILayerPropertyKeyframe> keyframes = _profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes).ToList();
|
||||||
|
ShiftKeyframes(keyframes.Where(k => k.Position > End.Add(difference)), difference);
|
||||||
|
Length = GetTimeFromX(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FinishResize(double x)
|
||||||
|
{
|
||||||
|
if (_profileElement == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Resize segment");
|
||||||
|
ApplyPendingKeyframeMovement();
|
||||||
|
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(Type, _profileElement, GetTimeFromX(x), _initialLength));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveSegment()
|
||||||
|
{
|
||||||
|
if (_profileElement == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Remove segment");
|
||||||
|
IEnumerable<ILayerPropertyKeyframe> keyframes = _profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes);
|
||||||
|
|
||||||
|
// Delete keyframes in the segment
|
||||||
|
foreach (ILayerPropertyKeyframe layerPropertyKeyframe in keyframes)
|
||||||
|
if (layerPropertyKeyframe.Position > Start && layerPropertyKeyframe.Position <= End)
|
||||||
|
_profileEditorService.ExecuteCommand(new DeleteKeyframe(layerPropertyKeyframe));
|
||||||
|
|
||||||
|
// Move keyframes after the segment forwards
|
||||||
|
ShiftKeyframes(keyframes.Where(s => s.Position > End), new TimeSpan(Length.Ticks * -1));
|
||||||
|
ApplyPendingKeyframeMovement();
|
||||||
|
|
||||||
|
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(Type, _profileElement, TimeSpan.Zero));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected TimeSpan GetTimeFromX(double x)
|
||||||
|
{
|
||||||
|
TimeSpan length = TimeSpan.FromSeconds(x / _pixelsPerSecond);
|
||||||
|
if (length < TimeSpan.Zero)
|
||||||
|
length = TimeSpan.Zero;
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ShiftKeyframes(IEnumerable<ILayerPropertyKeyframe> keyframes, TimeSpan amount)
|
||||||
|
{
|
||||||
|
foreach (ILayerPropertyKeyframe layerPropertyKeyframe in keyframes)
|
||||||
|
{
|
||||||
|
if (!_originalKeyframePositions.ContainsKey(layerPropertyKeyframe))
|
||||||
|
_originalKeyframePositions[layerPropertyKeyframe] = layerPropertyKeyframe.Position;
|
||||||
|
layerPropertyKeyframe.Position = layerPropertyKeyframe.Position.Add(amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ApplyPendingKeyframeMovement()
|
||||||
|
{
|
||||||
|
foreach ((ILayerPropertyKeyframe keyframe, TimeSpan originalPosition) in _originalKeyframePositions)
|
||||||
|
_profileEditorService.ExecuteCommand(new MoveKeyframe(keyframe, keyframe.Position, originalPosition));
|
||||||
|
|
||||||
|
_originalKeyframePositions.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,8 +12,8 @@
|
|||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.Styles>
|
<ItemsControl.Styles>
|
||||||
<Style Selector="ContentPresenter">
|
<Style Selector="ItemsControl > ContentPresenter">
|
||||||
<Setter Property="Canvas.Left" Value="{Binding X}" />
|
<Setter Property="Canvas.Left" Value="{Binding X, TargetNullValue=0}" />
|
||||||
</Style>
|
</Style>
|
||||||
</ItemsControl.Styles>
|
</ItemsControl.Styles>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
|
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
using Avalonia.Controls.Mixins;
|
using Avalonia.Controls.Mixins;
|
||||||
@ -24,6 +25,9 @@ public class TimelinePropertyViewModel<T> : ActivatableViewModelBase, ITimelineP
|
|||||||
|
|
||||||
this.WhenActivated(d =>
|
this.WhenActivated(d =>
|
||||||
{
|
{
|
||||||
|
Observable.FromEventPattern<LayerPropertyEventArgs>(x => LayerProperty.KeyframesToggled += x, x => LayerProperty.KeyframesToggled -= x)
|
||||||
|
.Subscribe(_ => UpdateKeyframes())
|
||||||
|
.DisposeWith(d);
|
||||||
Observable.FromEventPattern<LayerPropertyEventArgs>(x => LayerProperty.KeyframeAdded += x, x => LayerProperty.KeyframeAdded -= x)
|
Observable.FromEventPattern<LayerPropertyEventArgs>(x => LayerProperty.KeyframeAdded += x, x => LayerProperty.KeyframeAdded -= x)
|
||||||
.Subscribe(_ => UpdateKeyframes())
|
.Subscribe(_ => UpdateKeyframes())
|
||||||
.DisposeWith(d);
|
.DisposeWith(d);
|
||||||
|
|||||||
@ -10,8 +10,11 @@
|
|||||||
<x:Double x:Key="RailsHeight">28</x:Double>
|
<x:Double x:Key="RailsHeight">28</x:Double>
|
||||||
<x:Double x:Key="RailsBorderHeight">29</x:Double>
|
<x:Double x:Key="RailsBorderHeight">29</x:Double>
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
<Grid Background="Transparent" PointerReleased="InputElement_OnPointerReleased">
|
<Grid Background="Transparent" PointerReleased="InputElement_OnPointerReleased" Focusable="True" MinWidth="{Binding MinWidth}">
|
||||||
<ItemsControl Items="{Binding PropertyGroupViewModels}" Padding="0 0 8 0">
|
<Grid.KeyBindings>
|
||||||
|
<KeyBinding Command="{Binding DeleteKeyframes}" Gesture="Delete"/>
|
||||||
|
</Grid.KeyBindings>
|
||||||
|
<ItemsControl Items="{Binding PropertyGroupViewModels}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<TreeDataTemplate DataType="{x:Type local:PropertyGroupViewModel}" ItemsSource="{Binding Children}">
|
<TreeDataTemplate DataType="{x:Type local:PropertyGroupViewModel}" ItemsSource="{Binding Children}">
|
||||||
<ContentControl Content="{Binding TimelineGroupViewModel}" />
|
<ContentControl Content="{Binding TimelineGroupViewModel}" />
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||||
using Artemis.UI.Shared.Controls;
|
using Artemis.UI.Shared.Controls;
|
||||||
using Artemis.UI.Shared.Events;
|
using Artemis.UI.Shared.Events;
|
||||||
using Artemis.UI.Shared.Extensions;
|
using Artemis.UI.Shared.Extensions;
|
||||||
|
|||||||
@ -3,6 +3,9 @@ using System.Collections.Generic;
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
|
using Artemis.UI.Ninject.Factories;
|
||||||
|
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||||
|
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||||
@ -17,10 +20,18 @@ public class TimelineViewModel : ActivatableViewModelBase
|
|||||||
private ObservableAsPropertyHelper<double>? _caretPosition;
|
private ObservableAsPropertyHelper<double>? _caretPosition;
|
||||||
private ObservableAsPropertyHelper<int>? _pixelsPerSecond;
|
private ObservableAsPropertyHelper<int>? _pixelsPerSecond;
|
||||||
private List<ITimelineKeyframeViewModel>? _moveKeyframes;
|
private List<ITimelineKeyframeViewModel>? _moveKeyframes;
|
||||||
|
private ObservableAsPropertyHelper<double> _minWidth;
|
||||||
|
|
||||||
public TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels, IProfileEditorService profileEditorService)
|
public TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels,
|
||||||
|
StartSegmentViewModel startSegmentViewModel,
|
||||||
|
MainSegmentViewModel mainSegmentViewModel,
|
||||||
|
EndSegmentViewModel endSegmentViewModel,
|
||||||
|
IProfileEditorService profileEditorService)
|
||||||
{
|
{
|
||||||
PropertyGroupViewModels = propertyGroupViewModels;
|
PropertyGroupViewModels = propertyGroupViewModels;
|
||||||
|
StartSegmentViewModel = startSegmentViewModel;
|
||||||
|
MainSegmentViewModel = mainSegmentViewModel;
|
||||||
|
EndSegmentViewModel = endSegmentViewModel;
|
||||||
|
|
||||||
_profileEditorService = profileEditorService;
|
_profileEditorService = profileEditorService;
|
||||||
this.WhenActivated(d =>
|
this.WhenActivated(d =>
|
||||||
@ -29,14 +40,24 @@ public class TimelineViewModel : ActivatableViewModelBase
|
|||||||
.CombineLatest(_profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
|
.CombineLatest(_profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
|
||||||
.ToProperty(this, vm => vm.CaretPosition)
|
.ToProperty(this, vm => vm.CaretPosition)
|
||||||
.DisposeWith(d);
|
.DisposeWith(d);
|
||||||
|
|
||||||
_pixelsPerSecond = _profileEditorService.PixelsPerSecond.ToProperty(this, vm => vm.PixelsPerSecond).DisposeWith(d);
|
_pixelsPerSecond = _profileEditorService.PixelsPerSecond.ToProperty(this, vm => vm.PixelsPerSecond).DisposeWith(d);
|
||||||
|
_minWidth = profileEditorService.ProfileElement
|
||||||
|
.Select(p => p?.WhenAnyValue(element => element.Timeline.Length) ?? Observable.Never<TimeSpan>())
|
||||||
|
.Switch()
|
||||||
|
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p + 100)
|
||||||
|
.ToProperty(this, vm => vm.MinWidth)
|
||||||
|
.DisposeWith(d);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<PropertyGroupViewModel> PropertyGroupViewModels { get; }
|
public ObservableCollection<PropertyGroupViewModel> PropertyGroupViewModels { get; }
|
||||||
|
public StartSegmentViewModel StartSegmentViewModel { get; }
|
||||||
|
public MainSegmentViewModel MainSegmentViewModel { get; }
|
||||||
|
public EndSegmentViewModel EndSegmentViewModel { get; }
|
||||||
|
|
||||||
public double CaretPosition => _caretPosition?.Value ?? 0.0;
|
public double CaretPosition => _caretPosition?.Value ?? 0.0;
|
||||||
public int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0;
|
public int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0;
|
||||||
|
public double MinWidth => _minWidth?.Value ?? 0;
|
||||||
|
|
||||||
public void ChangeTime(TimeSpan newTime)
|
public void ChangeTime(TimeSpan newTime)
|
||||||
{
|
{
|
||||||
@ -118,10 +139,10 @@ public class TimelineViewModel : ActivatableViewModelBase
|
|||||||
|
|
||||||
#region Keyframe actions
|
#region Keyframe actions
|
||||||
|
|
||||||
public void DuplicateKeyframes(ITimelineKeyframeViewModel source)
|
public void DuplicateKeyframes(ITimelineKeyframeViewModel? source = null)
|
||||||
{
|
{
|
||||||
if (!source.IsSelected)
|
if (source is { IsSelected: false })
|
||||||
source.Duplicate();
|
source.Delete();
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
|
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
|
||||||
@ -131,9 +152,9 @@ public class TimelineViewModel : ActivatableViewModelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CopyKeyframes(ITimelineKeyframeViewModel source)
|
public void CopyKeyframes(ITimelineKeyframeViewModel? source = null)
|
||||||
{
|
{
|
||||||
if (!source.IsSelected)
|
if (source is { IsSelected: false })
|
||||||
source.Copy();
|
source.Copy();
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -144,9 +165,9 @@ public class TimelineViewModel : ActivatableViewModelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PasteKeyframes(ITimelineKeyframeViewModel source)
|
public void PasteKeyframes(ITimelineKeyframeViewModel? source = null)
|
||||||
{
|
{
|
||||||
if (!source.IsSelected)
|
if (source is { IsSelected: false })
|
||||||
source.Paste();
|
source.Paste();
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -157,9 +178,9 @@ public class TimelineViewModel : ActivatableViewModelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteKeyframes(ITimelineKeyframeViewModel source)
|
public void DeleteKeyframes(ITimelineKeyframeViewModel? source = null)
|
||||||
{
|
{
|
||||||
if (!source.IsSelected)
|
if (source is {IsSelected: false})
|
||||||
source.Delete();
|
source.Delete();
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user