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
|
||||
/// </summary>
|
||||
PropertyDescriptionAttribute PropertyDescription { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the profile element (such as layer or folder) this property is applied to
|
||||
/// </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
|
||||
/// </summary>
|
||||
bool IsLoadedFromStorage { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the layer property
|
||||
/// <para>
|
||||
@ -102,6 +102,20 @@ namespace Artemis.Core
|
||||
/// </summary>
|
||||
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>
|
||||
/// Occurs when the layer property is disposed
|
||||
/// </summary>
|
||||
|
||||
@ -96,6 +96,24 @@ namespace Artemis.Core
|
||||
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 />
|
||||
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
|
||||
/// or existing keyframe.
|
||||
/// </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)
|
||||
throw new ObjectDisposedException("LayerProperty");
|
||||
|
||||
LayerPropertyKeyframe<T>? newKeyframe = null;
|
||||
if (time == null || !KeyframesEnabled || !KeyframesSupported)
|
||||
{
|
||||
BaseValue = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If on a keyframe, update the keyframe
|
||||
LayerPropertyKeyframe<T>? currentKeyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value);
|
||||
// Create a new keyframe if none found
|
||||
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
|
||||
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
|
||||
// keyframes/data bindings are applied using the new base value
|
||||
ReapplyUpdate();
|
||||
return newKeyframe;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -247,6 +269,7 @@ namespace Artemis.Core
|
||||
{
|
||||
if (_keyframesEnabled == value) return;
|
||||
_keyframesEnabled = value;
|
||||
ReapplyUpdate();
|
||||
OnKeyframesToggled();
|
||||
OnPropertyChanged(nameof(KeyframesEnabled));
|
||||
}
|
||||
|
||||
@ -2,517 +2,529 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
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>
|
||||
/// Represents a timeline used by profile elements
|
||||
/// Creates a new instance of the <see cref="Timeline" /> class
|
||||
/// </summary>
|
||||
public class Timeline : CorePropertyChanged, IStorageModel
|
||||
public Timeline()
|
||||
{
|
||||
private const int MaxExtraTimelines = 15;
|
||||
private readonly object _lock = new();
|
||||
Entity = new TimelineEntity();
|
||||
MainSegmentLength = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="Timeline" /> class
|
||||
/// </summary>
|
||||
public Timeline()
|
||||
_extraTimelines = new List<Timeline>();
|
||||
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
|
||||
|
||||
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();
|
||||
MainSegmentLength = TimeSpan.FromSeconds(5);
|
||||
if (SetAndNotify(ref _startSegmentLength, value))
|
||||
NotifySegmentShiftAt(TimelineSegment.Start, false);
|
||||
}
|
||||
}
|
||||
|
||||
_extraTimelines = new List<Timeline>();
|
||||
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
_extraTimelines = new List<Timeline>();
|
||||
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
|
||||
|
||||
Load();
|
||||
if (startUpdated || segment < TimelineSegment.Main)
|
||||
OnPropertyChanged(nameof(MainSegmentStartPosition));
|
||||
OnPropertyChanged(nameof(MainSegmentEndPosition));
|
||||
}
|
||||
|
||||
private Timeline(Timeline parent)
|
||||
{
|
||||
Entity = new TimelineEntity();
|
||||
Parent = parent;
|
||||
StartSegmentLength = Parent.StartSegmentLength;
|
||||
MainSegmentLength = Parent.MainSegmentLength;
|
||||
EndSegmentLength = Parent.EndSegmentLength;
|
||||
if (segment <= TimelineSegment.Start)
|
||||
OnPropertyChanged(nameof(StartSegmentEndPosition));
|
||||
|
||||
_extraTimelines = new List<Timeline>();
|
||||
ExtraTimelines = new ReadOnlyCollection<Timeline>(_extraTimelines);
|
||||
OnPropertyChanged(nameof(Length));
|
||||
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 />
|
||||
public override string ToString()
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// Adds an extra timeline to this timeline
|
||||
/// </summary>
|
||||
public void AddExtraTimeline()
|
||||
/// <summary>
|
||||
/// Moves the position of the timeline forwards to the beginning of the end segment
|
||||
/// </summary>
|
||||
public void JumpToEndSegment()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_extraTimelines.Add(new Timeline(this));
|
||||
if (_extraTimelines.Count > MaxExtraTimelines)
|
||||
_extraTimelines.RemoveAt(0);
|
||||
if (Position >= EndSegmentStartPosition)
|
||||
return;
|
||||
|
||||
Delta = EndSegmentStartPosition - Position;
|
||||
Position = EndSegmentStartPosition;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all extra timelines from this timeline
|
||||
/// </summary>
|
||||
public void ClearExtraTimelines()
|
||||
/// <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();
|
||||
}
|
||||
|
||||
#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>
|
||||
/// 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>
|
||||
public enum TimelinePlayMode
|
||||
public void ClearDelta()
|
||||
{
|
||||
/// <summary>
|
||||
/// Continue repeating the main segment of the timeline while the condition is met
|
||||
/// </summary>
|
||||
Repeat,
|
||||
|
||||
/// <summary>
|
||||
/// Only play the timeline once when the condition is met
|
||||
/// </summary>
|
||||
Once
|
||||
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>
|
||||
/// 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>
|
||||
/// 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>
|
||||
public enum TimelineStopMode
|
||||
{
|
||||
/// <summary>
|
||||
/// When conditions are no longer met, finish the the current run of the main timeline
|
||||
/// </summary>
|
||||
Finish,
|
||||
Once
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When conditions are no longer met, skip to the end segment of the timeline
|
||||
/// </summary>
|
||||
SkipToEnd
|
||||
}
|
||||
/// <summary>
|
||||
/// Represents a mode for render elements to stop their timeline when display conditions are no longer met
|
||||
/// </summary>
|
||||
public enum TimelineStopMode
|
||||
{
|
||||
/// <summary>
|
||||
/// When conditions are no longer met, finish the the current run of the main timeline
|
||||
/// </summary>
|
||||
Finish,
|
||||
|
||||
/// <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>
|
||||
public enum TimeLineEventOverlapMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Stop the current run and restart the timeline
|
||||
/// </summary>
|
||||
Restart,
|
||||
SkipToEnd
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ignore subsequent event fires until the timeline finishes
|
||||
/// </summary>
|
||||
Ignore,
|
||||
/// <summary>
|
||||
/// Represents a mode for render elements to start their timeline when display conditions events are fired
|
||||
/// </summary>
|
||||
public enum TimeLineEventOverlapMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Stop the current run and restart the timeline
|
||||
/// </summary>
|
||||
Restart,
|
||||
|
||||
/// <summary>
|
||||
/// Play another copy of the timeline on top of the current run
|
||||
/// </summary>
|
||||
Copy,
|
||||
/// <summary>
|
||||
/// Ignore subsequent event fires until the timeline finishes
|
||||
/// </summary>
|
||||
Ignore,
|
||||
|
||||
/// <summary>
|
||||
/// Repeat the timeline until the event fires again
|
||||
/// </summary>
|
||||
Toggle
|
||||
}
|
||||
/// <summary>
|
||||
/// Play another copy of the timeline on top of the current run
|
||||
/// </summary>
|
||||
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))
|
||||
end = TimeSpan.FromMilliseconds(100);
|
||||
|
||||
|
||||
if (Segment == SegmentViewModelType.Start)
|
||||
SelectedProfileElement.Timeline.StartSegmentEndPosition = end;
|
||||
else if (Segment == SegmentViewModelType.Main)
|
||||
|
||||
@ -5,14 +5,14 @@ namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
/// <summary>
|
||||
/// Represents a profile editor command that can be used to delete a keyframe.
|
||||
/// </summary>
|
||||
public class DeleteKeyframe<T> : IProfileEditorCommand
|
||||
public class DeleteKeyframe : IProfileEditorCommand
|
||||
{
|
||||
private readonly LayerPropertyKeyframe<T> _keyframe;
|
||||
private readonly ILayerPropertyKeyframe _keyframe;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="DeleteKeyframe{T}" /> class.
|
||||
/// </summary>
|
||||
public DeleteKeyframe(LayerPropertyKeyframe<T> keyframe)
|
||||
public DeleteKeyframe(ILayerPropertyKeyframe keyframe)
|
||||
{
|
||||
_keyframe = keyframe;
|
||||
}
|
||||
@ -25,13 +25,13 @@ public class DeleteKeyframe<T> : IProfileEditorCommand
|
||||
/// <inheritdoc />
|
||||
public void Execute()
|
||||
{
|
||||
_keyframe.LayerProperty.RemoveKeyframe(_keyframe);
|
||||
_keyframe.UntypedLayerProperty.RemoveUntypedKeyframe(_keyframe);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Undo()
|
||||
{
|
||||
_keyframe.LayerProperty.AddKeyframe(_keyframe);
|
||||
_keyframe.UntypedLayerProperty.AddUntypedKeyframe(_keyframe);
|
||||
}
|
||||
|
||||
#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 _originalValue;
|
||||
private readonly TimeSpan? _time;
|
||||
private LayerPropertyKeyframe<T>? _newKeyframe;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="UpdateLayerProperty{T}" /> class.
|
||||
@ -43,13 +44,16 @@ public class UpdateLayerProperty<T> : IProfileEditorCommand
|
||||
/// <inheritdoc />
|
||||
public void Execute()
|
||||
{
|
||||
_layerProperty.SetCurrentValue(_newValue, _time);
|
||||
_newKeyframe = _layerProperty.SetCurrentValue(_newValue, _time);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Undo()
|
||||
{
|
||||
_layerProperty.SetCurrentValue(_originalValue, _time);
|
||||
if (_newKeyframe != null)
|
||||
_layerProperty.RemoveKeyframe(_newKeyframe);
|
||||
else
|
||||
_layerProperty.SetCurrentValue(_originalValue, _time);
|
||||
}
|
||||
|
||||
#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_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_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_005Csettings_005Ctabs/@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.Properties;
|
||||
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.Settings;
|
||||
using Artemis.UI.Screens.Sidebar;
|
||||
@ -73,14 +74,9 @@ namespace Artemis.UI.Ninject.Factories
|
||||
PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerEffect layerEffect);
|
||||
|
||||
TreeGroupViewModel TreeGroupViewModel(PropertyGroupViewModel propertyGroupViewModel);
|
||||
|
||||
|
||||
TimelineViewModel TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels);
|
||||
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
|
||||
|
||||
@ -6,6 +6,9 @@
|
||||
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
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 RowDefinitions="48,*">
|
||||
<ContentControl Grid.Row="0" Content="{Binding PlaybackViewModel}" />
|
||||
@ -31,76 +34,103 @@
|
||||
Background="Transparent"
|
||||
Margin="0 0 -5 0" />
|
||||
|
||||
<Grid Grid.Column="2" RowDefinitions="48,*">
|
||||
<!-- Timeline header body -->
|
||||
<controls:TimelineHeader Grid.Row="0"
|
||||
Name="TimelineHeader"
|
||||
Margin="0 18 0 0"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
PixelsPerSecond="{Binding PixelsPerSecond}"
|
||||
HorizontalOffset="{Binding #TimelineScrollViewer.Offset.X, Mode=OneWay}"
|
||||
VisibleWidth="{Binding #TimelineScrollViewer.Bounds.Width}"
|
||||
OffsetFirstValue="True"
|
||||
PointerReleased="TimelineHeader_OnPointerReleased"
|
||||
Width="{Binding #TimelineScrollViewer.Viewport.Width}"
|
||||
Cursor="Hand" />
|
||||
<!-- Horizontal scrolling -->
|
||||
<ScrollViewer Grid.Column="2" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<Grid RowDefinitions="48,*">
|
||||
<!-- Timeline header body -->
|
||||
<controls:TimelineHeader Grid.Row="0"
|
||||
Name="TimelineHeader"
|
||||
Margin="0 18 0 0"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
HorizontalAlignment="Left"
|
||||
PixelsPerSecond="{Binding PixelsPerSecond}"
|
||||
HorizontalOffset="{Binding #TimelineScrollViewer.Offset.X, Mode=OneWay}"
|
||||
VisibleWidth="{Binding #TimelineScrollViewer.Bounds.Width}"
|
||||
OffsetFirstValue="True"
|
||||
PointerReleased="TimelineHeader_OnPointerReleased"
|
||||
Width="{Binding #TimelineScrollViewer.Viewport.Width}"
|
||||
Cursor="Hand" />
|
||||
|
||||
<Canvas Grid.Row="0" ZIndex="2">
|
||||
<!-- Timeline segments -->
|
||||
<ContentControl Canvas.Left="{Binding EndTimelineSegmentViewModel.SegmentStartPosition}" Content="{Binding EndTimelineSegmentViewModel}" />
|
||||
<ContentControl Canvas.Left="{Binding MainTimelineSegmentViewModel.SegmentStartPosition}" Content="{Binding MainTimelineSegmentViewModel}" />
|
||||
<ContentControl Canvas.Left="{Binding StartTimelineSegmentViewModel.SegmentStartPosition}" Content="{Binding StartTimelineSegmentViewModel}" />
|
||||
<Canvas Grid.Row="0" ZIndex="2">
|
||||
<!-- Segment dividers -->
|
||||
<Line Name="TimelineLine"
|
||||
Canvas.Left="{Binding TimelineViewModel.CaretPosition}"
|
||||
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 -->
|
||||
<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.Transitions> -->
|
||||
<!-- <Transitions> -->
|
||||
<!-- <DoubleTransition Property="Canvas.Left" Duration="0.05"></DoubleTransition> -->
|
||||
<!-- </Transitions> -->
|
||||
<!-- </Polygon.Transitions> -->
|
||||
</Polygon>
|
||||
<Line Name="TimelineLine"
|
||||
Canvas.Left="{Binding TimelineViewModel.CaretPosition}"
|
||||
Cursor="SizeWestEast"
|
||||
PointerPressed="TimelineCaret_OnPointerPressed"
|
||||
PointerReleased="TimelineCaret_OnPointerReleased"
|
||||
PointerMoved="TimelineCaret_OnPointerMoved"
|
||||
StartPoint="0,0"
|
||||
EndPoint="0,1"
|
||||
StrokeThickness="2"
|
||||
Stroke="{DynamicResource SystemAccentColorLight1}"
|
||||
RenderTransformOrigin="0,0">
|
||||
<!-- <Line.Transitions> -->
|
||||
<!-- <Transitions> -->
|
||||
<!-- <DoubleTransition Property="Canvas.Left" Duration="0.05"></DoubleTransition> -->
|
||||
<!-- </Transitions> -->
|
||||
<!-- </Line.Transitions> -->
|
||||
<Line.RenderTransform>
|
||||
<ScaleTransform ScaleX="1" ScaleY="{Binding #ContainerGrid.Bounds.Height}" />
|
||||
</Line.RenderTransform>
|
||||
</Line>
|
||||
</Canvas>
|
||||
<Line Name="StartSegmentLine"
|
||||
Canvas.Left="{Binding TimelineViewModel.StartSegmentViewModel.EndX}"
|
||||
IsVisible="{Binding !TimelineViewModel.MainSegmentViewModel.ShowAddStart}"
|
||||
StartPoint="0,0"
|
||||
EndPoint="{Binding #ContainerGrid.Bounds.BottomLeft}"
|
||||
StrokeThickness="2"
|
||||
Stroke="{DynamicResource SystemAccentColorLight1}"
|
||||
StrokeDashArray="6,2"
|
||||
Opacity="0.5">
|
||||
</Line>
|
||||
<Line Name="MainSegmentLine"
|
||||
Canvas.Left="{Binding TimelineViewModel.MainSegmentViewModel.EndX}"
|
||||
IsVisible="{Binding !TimelineViewModel.MainSegmentViewModel.ShowAddMain}"
|
||||
StartPoint="0,0"
|
||||
EndPoint="{Binding #ContainerGrid.Bounds.BottomLeft}"
|
||||
StrokeThickness="2"
|
||||
Stroke="{DynamicResource SystemAccentColorLight1}"
|
||||
StrokeDashArray="6,2"
|
||||
Opacity="0.5">
|
||||
</Line>
|
||||
<Line Name="EndSegmentLine"
|
||||
Canvas.Left="{Binding TimelineViewModel.EndSegmentViewModel.EndX}"
|
||||
IsVisible="{Binding !TimelineViewModel.MainSegmentViewModel.ShowAddEnd}"
|
||||
StartPoint="0,0"
|
||||
EndPoint="{Binding #ContainerGrid.Bounds.BottomLeft}"
|
||||
StrokeThickness="2"
|
||||
Stroke="{DynamicResource SystemAccentColorLight1}"
|
||||
StrokeDashArray="6,2"
|
||||
Opacity="0.5">
|
||||
</Line>
|
||||
|
||||
<!-- Horizontal scrolling -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
Name="TimelineScrollViewer"
|
||||
Offset="{Binding #TreeScrollViewer.Offset, Mode=OneWay}"
|
||||
VerticalScrollBarVisibility="Hidden"
|
||||
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}">
|
||||
<ContentControl Content="{Binding TimelineViewModel}" />
|
||||
</ScrollViewer>
|
||||
<!-- Timeline segments -->
|
||||
<ContentControl Canvas.Left="{Binding TimelineViewModel.EndSegmentViewModel.StartX}"
|
||||
Classes="segment-content-control"
|
||||
Content="{Binding TimelineViewModel.EndSegmentViewModel}" />
|
||||
<ContentControl Canvas.Left="{Binding TimelineViewModel.MainSegmentViewModel.StartX}"
|
||||
Classes="segment-content-control"
|
||||
Content="{Binding TimelineViewModel.MainSegmentViewModel}" />
|
||||
<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>
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ using Artemis.Core.LayerBrushes;
|
||||
using Artemis.Core.LayerEffects;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
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.Shared;
|
||||
using Artemis.UI.Shared.Services.PropertyInput;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
using System;
|
||||
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
|
||||
{
|
||||
@ -3,7 +3,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
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">
|
||||
<Polyline Stroke="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
StrokeThickness="1"
|
||||
@ -1,7 +1,7 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes
|
||||
{
|
||||
public partial class TimelineEasingView : UserControl
|
||||
{
|
||||
@ -4,7 +4,7 @@ using Artemis.UI.Shared;
|
||||
using Avalonia;
|
||||
using Humanizer;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||
|
||||
public class TimelineEasingViewModel : ViewModelBase
|
||||
{
|
||||
@ -5,7 +5,7 @@
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline"
|
||||
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"
|
||||
Height="{DynamicResource RailsHeight}">
|
||||
<Ellipse Fill="{DynamicResource SystemAccentColorLight2}"
|
||||
@ -4,7 +4,7 @@ using Avalonia.LogicalTree;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||
|
||||
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.Commands;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using Avalonia.Input;
|
||||
using DynamicData;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||
|
||||
public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineKeyframeViewModel
|
||||
{
|
||||
@ -97,7 +96,7 @@ public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineK
|
||||
|
||||
public void Delete()
|
||||
{
|
||||
_profileEditorService.ExecuteCommand(new DeleteKeyframe<T>(LayerPropertyKeyframe));
|
||||
_profileEditorService.ExecuteCommand(new DeleteKeyframe(LayerPropertyKeyframe));
|
||||
}
|
||||
|
||||
#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>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.Styles>
|
||||
<Style Selector="ContentPresenter">
|
||||
<Setter Property="Canvas.Left" Value="{Binding X}" />
|
||||
<Style Selector="ItemsControl > ContentPresenter">
|
||||
<Setter Property="Canvas.Left" Value="{Binding X, TargetNullValue=0}" />
|
||||
</Style>
|
||||
</ItemsControl.Styles>
|
||||
</ItemsControl>
|
||||
|
||||
@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Avalonia.Controls.Mixins;
|
||||
@ -24,6 +25,9 @@ public class TimelinePropertyViewModel<T> : ActivatableViewModelBase, ITimelineP
|
||||
|
||||
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)
|
||||
.Subscribe(_ => UpdateKeyframes())
|
||||
.DisposeWith(d);
|
||||
|
||||
@ -10,8 +10,11 @@
|
||||
<x:Double x:Key="RailsHeight">28</x:Double>
|
||||
<x:Double x:Key="RailsBorderHeight">29</x:Double>
|
||||
</UserControl.Resources>
|
||||
<Grid Background="Transparent" PointerReleased="InputElement_OnPointerReleased">
|
||||
<ItemsControl Items="{Binding PropertyGroupViewModels}" Padding="0 0 8 0">
|
||||
<Grid Background="Transparent" PointerReleased="InputElement_OnPointerReleased" Focusable="True" MinWidth="{Binding MinWidth}">
|
||||
<Grid.KeyBindings>
|
||||
<KeyBinding Command="{Binding DeleteKeyframes}" Gesture="Delete"/>
|
||||
</Grid.KeyBindings>
|
||||
<ItemsControl Items="{Binding PropertyGroupViewModels}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<TreeDataTemplate DataType="{x:Type local:PropertyGroupViewModel}" ItemsSource="{Binding Children}">
|
||||
<ContentControl Content="{Binding TimelineGroupViewModel}" />
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
|
||||
using Artemis.UI.Shared.Controls;
|
||||
using Artemis.UI.Shared.Events;
|
||||
using Artemis.UI.Shared.Extensions;
|
||||
|
||||
@ -3,6 +3,9 @@ using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.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.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
@ -17,10 +20,18 @@ public class TimelineViewModel : ActivatableViewModelBase
|
||||
private ObservableAsPropertyHelper<double>? _caretPosition;
|
||||
private ObservableAsPropertyHelper<int>? _pixelsPerSecond;
|
||||
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;
|
||||
StartSegmentViewModel = startSegmentViewModel;
|
||||
MainSegmentViewModel = mainSegmentViewModel;
|
||||
EndSegmentViewModel = endSegmentViewModel;
|
||||
|
||||
_profileEditorService = profileEditorService;
|
||||
this.WhenActivated(d =>
|
||||
@ -29,14 +40,24 @@ public class TimelineViewModel : ActivatableViewModelBase
|
||||
.CombineLatest(_profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
|
||||
.ToProperty(this, vm => vm.CaretPosition)
|
||||
.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 StartSegmentViewModel StartSegmentViewModel { get; }
|
||||
public MainSegmentViewModel MainSegmentViewModel { get; }
|
||||
public EndSegmentViewModel EndSegmentViewModel { get; }
|
||||
|
||||
public double CaretPosition => _caretPosition?.Value ?? 0.0;
|
||||
public int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0;
|
||||
public double MinWidth => _minWidth?.Value ?? 0;
|
||||
|
||||
public void ChangeTime(TimeSpan newTime)
|
||||
{
|
||||
@ -118,10 +139,10 @@ public class TimelineViewModel : ActivatableViewModelBase
|
||||
|
||||
#region Keyframe actions
|
||||
|
||||
public void DuplicateKeyframes(ITimelineKeyframeViewModel source)
|
||||
public void DuplicateKeyframes(ITimelineKeyframeViewModel? source = null)
|
||||
{
|
||||
if (!source.IsSelected)
|
||||
source.Duplicate();
|
||||
if (source is { IsSelected: false })
|
||||
source.Delete();
|
||||
else
|
||||
{
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
else
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user